From 7bcb439d5744a9f2c6c32cf5b4be17c5c9fad593 Mon Sep 17 00:00:00 2001 From: TsMask <340112800@qq.com> Date: Fri, 22 Nov 2024 10:16:12 +0800 Subject: [PATCH] =?UTF-8?q?marge:=20=E5=90=88=E5=B9=B611.2=E7=89=88?= =?UTF-8?q?=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.go | 8 +- .../deb/iperf_2.0.13+dfsg1-1build1_amd64.deb | Bin 0 -> 76492 bytes .../iperf/rpm/iperf-2.1.6-2.el8.aarch64.rpm | Bin 0 -> 133076 bytes .../iperf/rpm/iperf3-3.6-6.ky10.aarch64.rpm | Bin 0 -> 76348 bytes .../iperf3/deb/iperf3_3.1.3-1_amd64.deb | Bin 0 -> 8802 bytes .../iperf3/deb/libiperf0_3.1.3-1_amd64.deb | Bin 0 -> 55184 bytes .../libsctp1_1.0.19+dfsg-1build1_amd64.deb | Bin 0 -> 9370 bytes .../iperf3/rpm/iperf3-3.6-6.ky10.aarch64.rpm | Bin 0 -> 76348 bytes .../rpm/iperf3-help-3.6-6.ky10.noarch.rpm | Bin 0 -> 14436 bytes .../excel/student_import_template_en.xlsx | Bin 11077 -> 0 bytes .../excel/student_import_template_zh.xlsx | Bin 11094 -> 0 bytes .../config/config/config.default.yaml | 11 +- src/framework/config/config/config.local.yaml | 16 +- src/framework/constants/result/result.go | 9 +- src/framework/datasource/datasource.go | 3 + src/framework/errorcatch/errorcatch.go | 6 +- src/framework/i18n/i18n.go | 4 +- src/framework/middleware/crypto_api.go | 151 +++ src/framework/middleware/report.go | 7 +- src/framework/redis/conn.go | 79 ++ src/framework/redis/redis.go | 115 +- src/framework/socket/tcp_client.go | 96 ++ src/framework/socket/tcp_server.go | 77 ++ src/framework/socket/udp_client.go | 96 ++ src/framework/socket/udp_server.go | 67 ++ src/framework/{utils => }/telnet/parse.go | 0 src/framework/{utils => }/telnet/telnet.go | 20 +- .../{utils => }/telnet/telnet_session.go | 15 +- src/framework/utils/crypto/aes.go | 33 +- src/framework/utils/ctx/ctx.go | 5 +- src/framework/utils/fetch/fetch.go | 4 +- src/framework/utils/file/file.go | 3 +- src/framework/utils/file/tar.go | 76 ++ src/framework/utils/machine/launch.go | 7 +- src/framework/utils/parse/parse.go | 45 +- src/framework/utils/ssh/files.go | 34 +- src/framework/utils/ssh/sftp.go | 6 +- src/framework/utils/ssh/ssh.go | 9 +- src/framework/utils/ua/ua.go | 6 +- src/framework/vo/result/result.go | 22 +- src/modules/chart/chart.go | 6 +- src/modules/chart/controller/chart_graph.go | 6 +- src/modules/chart/model/chart_graph.go | 47 +- src/modules/chart/repository/chart_graph.go | 203 +++- .../chart/repository/chart_graph.impl.go | 194 ---- src/modules/chart/service/chart_graph.go | 365 +++++- src/modules/chart/service/chart_graph.impl.go | 359 ------ src/modules/common/common.go | 2 + src/modules/common/controller/account.go | 5 +- src/modules/common/controller/bootloader.go | 5 +- src/modules/common/service/account.go | 208 +++- src/modules/common/service/account.impl.go | 190 ---- .../processor/exportTable/exportTable.go | 160 +++ .../monitor_sys_resource.go | 4 +- .../ne_config_backup/ne_config_backup.go | 13 +- .../processor/ne_data_udm/ne_data_udm.go | 45 + src/modules/crontask/processor/processor.go | 7 + .../processor/removeFile/removeFile.go | 159 +++ src/modules/monitor/controller/monitor.go | 26 +- src/modules/monitor/controller/sys_cache.go | 2 - src/modules/monitor/controller/sys_job.go | 10 +- src/modules/monitor/controller/sys_job_log.go | 12 +- .../monitor/controller/sys_user_online.go | 54 +- src/modules/monitor/controller/system_info.go | 10 +- src/modules/monitor/model/monitor_base.go | 33 +- src/modules/monitor/model/monitor_io.go | 30 +- src/modules/monitor/model/monitor_network.go | 24 +- src/modules/monitor/monitor.go | 2 +- src/modules/monitor/service/monitor.go | 278 ++++- src/modules/monitor/service/monitor.impl.go | 271 ----- src/modules/monitor/service/monitor_test.go | 32 +- src/modules/monitor/service/sys_job.go | 190 +++- src/modules/monitor/service/sys_job.impl.go | 169 --- src/modules/monitor/service/sys_job_log.go | 51 +- .../monitor/service/sys_job_log.impl.go | 42 - .../monitor/service/sys_user_online.go | 29 +- .../monitor/service/sys_user_online.impl.go | 33 - src/modules/monitor/service/system_info.go | 183 ++- .../monitor/service/system_info.impl.go | 173 --- .../network_data/controller/all_alarm.go | 12 +- .../network_data/controller/all_kpi.go | 12 +- src/modules/network_data/controller/amf.go | 20 +- src/modules/network_data/controller/ims.go | 45 +- src/modules/network_data/controller/mme.go | 18 +- src/modules/network_data/controller/smf.go | 205 +++- src/modules/network_data/controller/smsc.go | 195 ++++ .../network_data/controller/udm_auth.go | 155 ++- .../network_data/controller/udm_sub.go | 246 ++-- src/modules/network_data/controller/upf.go | 19 +- src/modules/network_data/model/alarm.go | 2 +- .../network_data/model/cdr_event_ims.go | 2 +- .../network_data/model/cdr_event_smsc.go | 30 + src/modules/network_data/model/udm_auth.go | 19 +- src/modules/network_data/model/udm_sub.go | 62 +- .../network_data/model/udm_user_info.go | 15 + src/modules/network_data/network_data.go | 57 +- src/modules/network_data/repository/alarm.go | 197 +++- .../network_data/repository/alarm.impl.go | 194 ---- .../network_data/repository/cdr_event_ims.go | 192 +++- .../network_data/repository/cdr_event_smf.go | 173 ++- .../repository/cdr_event_smf.impl.go | 170 --- ...dr_event_ims.impl.go => cdr_event_smsc.go} | 38 +- .../network_data/repository/perf_kpi.go | 134 ++- .../network_data/repository/perf_kpi.impl.go | 131 --- .../network_data/repository/udm_auth.go | 140 ++- .../network_data/repository/udm_sub.go | 143 ++- .../network_data/repository/udm_sub.impl.go | 211 ---- .../{udm_auth.impl.go => udm_user_info.go} | 73 +- .../network_data/repository/ue_event_amf.go | 178 ++- .../repository/ue_event_amf.impl.go | 175 --- .../network_data/repository/ue_event_mme.go | 178 ++- .../repository/ue_event_mme.impl.go | 175 --- src/modules/network_data/service/alarm.go | 41 +- .../network_data/service/alarm.impl.go | 40 - .../network_data/service/cdr_event_ims.go | 41 +- .../service/cdr_event_ims.impl.go | 40 - .../network_data/service/cdr_event_smf.go | 41 +- ...dr_event_smf.impl.go => cdr_event_smsc.go} | 16 +- src/modules/network_data/service/perf_kpi.go | 84 +- .../network_data/service/perf_kpi.impl.go | 80 -- src/modules/network_data/service/udm_auth.go | 222 +++- .../network_data/service/udm_auth.impl.go | 146 --- src/modules/network_data/service/udm_sub.go | 383 ++++++- .../network_data/service/udm_sub.impl.go | 164 --- .../network_data/service/udm_user_info.go | 33 + .../network_data/service/ue_event_amf.go | 42 +- .../network_data/service/ue_event_amf.impl.go | 40 - .../network_data/service/ue_event_mme.go | 41 +- .../network_data/service/ue_event_mme.impl.go | 40 - .../network_element/controller/action.go | 162 ++- .../network_element/controller/ne_config.go | 56 +- .../controller/ne_config_backup.go | 10 +- .../network_element/controller/ne_host.go | 30 +- .../network_element/controller/ne_host_cmd.go | 5 +- .../network_element/controller/ne_info.go | 23 +- .../network_element/controller/ne_license.go | 10 +- .../network_element/controller/ne_software.go | 5 +- .../network_element/controller/ne_version.go | 7 +- src/modules/network_element/fetch_link/hlr.go | 68 ++ .../network_element/fetch_link/ne_config.go | 4 +- .../network_element/fetch_link/ne_state.go | 2 + .../network_element/fetch_link/ne_trace.go | 121 ++ src/modules/network_element/fetch_link/smf.go | 66 ++ src/modules/network_element/fetch_link/udm.go | 6 +- .../network_element/model/ne_config.go | 1 + src/modules/network_element/model/ne_host.go | 9 +- src/modules/network_element/model/ne_info.go | 4 +- .../network_element/model/ne_license.go | 2 +- .../network_element/model/ne_software.go | 2 +- .../network_element/model/ne_version.go | 2 +- src/modules/network_element/ne_config_test.go | 14 +- .../network_element/network_element.go | 44 +- .../network_element/repository/ne_config.go | 271 ++++- .../repository/ne_config.impl.go | 259 ----- .../repository/ne_config_backup.go | 274 ++++- .../repository/ne_config_backup.impl.go | 262 ----- .../network_element/repository/ne_host.go | 418 ++++++- .../repository/ne_host.impl.go | 432 ------- .../network_element/repository/ne_host_cmd.go | 321 +++++- .../repository/ne_host_cmd.impl.go | 306 ----- .../network_element/repository/ne_info.go | 426 ++++++- .../repository/ne_info.impl.go | 413 ------- .../network_element/repository/ne_license.go | 309 ++++- .../repository/ne_license.impl.go | 297 ----- .../network_element/repository/ne_software.go | 332 +++++- .../repository/ne_software.impl.go | 317 ------ .../network_element/repository/ne_version.go | 341 +++++- .../repository/ne_version.impl.go | 329 ------ .../network_element/service/ne_config.go | 189 ++- .../network_element/service/ne_config.impl.go | 164 --- .../service/ne_config_backup.go | 221 +++- .../service/ne_config_backup.impl.go | 202 ---- .../network_element/service/ne_host.go | 198 +++- .../network_element/service/ne_host.impl.go | 92 -- .../network_element/service/ne_host_cmd.go | 96 +- .../service/ne_host_cmd.impl.go | 80 -- .../network_element/service/ne_info.go | 1013 +++++++++++++++-- .../network_element/service/ne_info.impl.go | 798 ------------- .../network_element/service/ne_license.go | 214 +++- .../service/ne_license.impl.go | 191 ---- .../network_element/service/ne_software.go | 162 ++- .../service/ne_software.impl.go | 143 --- .../network_element/service/ne_version.go | 766 ++++++++++++- .../service/ne_version.impl.go | 642 ----------- src/modules/system/controller/sys_config.go | 2 +- .../system/controller/sys_dict_data.go | 10 +- .../system/controller/sys_dict_type.go | 5 +- .../system/controller/sys_log_login.go | 5 +- src/modules/system/controller/sys_profile.go | 21 +- src/modules/system/controller/sys_role.go | 7 +- src/modules/system/controller/sys_user.go | 145 +-- .../system/repository/sys_dict_data.impl.go | 2 +- .../system/repository/sys_dict_type.impl.go | 2 +- .../system/repository/sys_user.impl.go | 6 +- src/modules/system/service/sys_dict_data.go | 137 ++- .../system/service/sys_dict_data.impl.go | 116 -- src/modules/system/service/sys_dict_type.go | 247 +++- .../system/service/sys_dict_type.impl.go | 212 ---- src/modules/system/system.go | 2 +- src/modules/tool/controller/iperf.go | 154 +++ src/modules/tool/controller/ping.go | 180 +++ src/modules/tool/model/ping.go | 62 + src/modules/tool/service/iperf.go | 289 +++++ src/modules/tool/service/ping.go | 261 +++++ src/modules/tool/tool.go | 57 + src/modules/trace/controller/packet.go | 121 ++ src/modules/trace/controller/tcpdump.go | 79 +- src/modules/trace/controller/trace_data.go | 62 + src/modules/trace/controller/trace_task.go | 155 +++ .../trace/controller/trace_task_hlr.go | 240 ++++ src/modules/trace/model/trace_data.go | 23 + src/modules/trace/model/trace_task.go | 31 + src/modules/trace/model/trace_task_hlr.go | 35 + src/modules/trace/packet_task/packet.go | 245 ++++ src/modules/trace/packet_task/packet_frame.go | 843 ++++++++++++++ .../trace/packet_task/packet_frame_util.go | 346 ++++++ src/modules/trace/repository/trace_data.go | 246 ++++ src/modules/trace/repository/trace_task.go | 358 ++++++ .../trace/repository/trace_task_hlr.go | 316 +++++ src/modules/trace/service/packet.go | 67 ++ src/modules/trace/service/tcpdump.go | 287 ++++- src/modules/trace/service/tcpdump.impl.go | 193 ---- src/modules/trace/service/trace_data.go | 54 + src/modules/trace/service/trace_task.go | 343 ++++++ src/modules/trace/service/trace_task_hlr.go | 206 ++++ .../trace/service/trace_task_udp_data.go | 334 ++++++ src/modules/trace/trace.go | 119 +- src/modules/ws/controller/ws.go | 242 +--- src/modules/ws/controller/ws_redis.go | 69 ++ src/modules/ws/controller/ws_ssh.go | 118 ++ src/modules/ws/controller/ws_telnet.go | 111 ++ src/modules/ws/controller/ws_view.go | 97 ++ src/modules/ws/model/net_connect.go | 14 +- src/modules/ws/model/ps_process.go | 32 +- src/modules/ws/processor/cdr_connect.go | 44 +- src/modules/ws/processor/ne_state.go | 2 +- src/modules/ws/processor/net_connect.go | 13 +- src/modules/ws/processor/ps_process.go | 106 +- src/modules/ws/processor/shell_command.go | 71 ++ src/modules/ws/processor/ue_connect.go | 4 +- src/modules/ws/processor/upf_total_flow.go | 4 +- src/modules/ws/service/ws.go | 213 +++- src/modules/ws/service/ws.impl.go | 223 ---- src/modules/ws/service/ws_receive.go | 335 +++++- src/modules/ws/service/ws_receive.impl.go | 101 -- src/modules/ws/service/ws_send.go | 91 +- src/modules/ws/service/ws_send.impl.go | 89 -- src/modules/ws/ws.go | 14 +- 248 files changed, 18401 insertions(+), 11120 deletions(-) create mode 100644 src/assets/dependency/iperf/deb/iperf_2.0.13+dfsg1-1build1_amd64.deb create mode 100644 src/assets/dependency/iperf/rpm/iperf-2.1.6-2.el8.aarch64.rpm create mode 100644 src/assets/dependency/iperf/rpm/iperf3-3.6-6.ky10.aarch64.rpm create mode 100644 src/assets/dependency/iperf3/deb/iperf3_3.1.3-1_amd64.deb create mode 100644 src/assets/dependency/iperf3/deb/libiperf0_3.1.3-1_amd64.deb create mode 100644 src/assets/dependency/iperf3/deb/libsctp1_1.0.19+dfsg-1build1_amd64.deb create mode 100644 src/assets/dependency/iperf3/rpm/iperf3-3.6-6.ky10.aarch64.rpm create mode 100644 src/assets/dependency/iperf3/rpm/iperf3-help-3.6-6.ky10.noarch.rpm delete mode 100644 src/assets/template/excel/student_import_template_en.xlsx delete mode 100644 src/assets/template/excel/student_import_template_zh.xlsx create mode 100644 src/framework/middleware/crypto_api.go create mode 100644 src/framework/redis/conn.go create mode 100644 src/framework/socket/tcp_client.go create mode 100644 src/framework/socket/tcp_server.go create mode 100644 src/framework/socket/udp_client.go create mode 100644 src/framework/socket/udp_server.go rename src/framework/{utils => }/telnet/parse.go (100%) rename src/framework/{utils => }/telnet/telnet.go (81%) rename src/framework/{utils => }/telnet/telnet_session.go (73%) create mode 100644 src/framework/utils/file/tar.go delete mode 100644 src/modules/chart/repository/chart_graph.impl.go delete mode 100644 src/modules/chart/service/chart_graph.impl.go delete mode 100644 src/modules/common/service/account.impl.go create mode 100644 src/modules/crontask/processor/exportTable/exportTable.go create mode 100644 src/modules/crontask/processor/ne_data_udm/ne_data_udm.go create mode 100644 src/modules/crontask/processor/removeFile/removeFile.go delete mode 100644 src/modules/monitor/service/monitor.impl.go delete mode 100644 src/modules/monitor/service/sys_job.impl.go delete mode 100644 src/modules/monitor/service/sys_job_log.impl.go delete mode 100644 src/modules/monitor/service/sys_user_online.impl.go delete mode 100644 src/modules/monitor/service/system_info.impl.go create mode 100644 src/modules/network_data/controller/smsc.go create mode 100644 src/modules/network_data/model/cdr_event_smsc.go create mode 100644 src/modules/network_data/model/udm_user_info.go delete mode 100644 src/modules/network_data/repository/alarm.impl.go delete mode 100644 src/modules/network_data/repository/cdr_event_smf.impl.go rename src/modules/network_data/repository/{cdr_event_ims.impl.go => cdr_event_smsc.go} (79%) delete mode 100644 src/modules/network_data/repository/perf_kpi.impl.go delete mode 100644 src/modules/network_data/repository/udm_sub.impl.go rename src/modules/network_data/repository/{udm_auth.impl.go => udm_user_info.go} (71%) delete mode 100644 src/modules/network_data/repository/ue_event_amf.impl.go delete mode 100644 src/modules/network_data/repository/ue_event_mme.impl.go delete mode 100644 src/modules/network_data/service/alarm.impl.go delete mode 100644 src/modules/network_data/service/cdr_event_ims.impl.go rename src/modules/network_data/service/{cdr_event_smf.impl.go => cdr_event_smsc.go} (51%) delete mode 100644 src/modules/network_data/service/perf_kpi.impl.go delete mode 100644 src/modules/network_data/service/udm_auth.impl.go delete mode 100644 src/modules/network_data/service/udm_sub.impl.go create mode 100644 src/modules/network_data/service/udm_user_info.go delete mode 100644 src/modules/network_data/service/ue_event_amf.impl.go delete mode 100644 src/modules/network_data/service/ue_event_mme.impl.go create mode 100644 src/modules/network_element/fetch_link/hlr.go create mode 100644 src/modules/network_element/fetch_link/ne_trace.go create mode 100644 src/modules/network_element/fetch_link/smf.go delete mode 100644 src/modules/network_element/repository/ne_config.impl.go delete mode 100644 src/modules/network_element/repository/ne_config_backup.impl.go delete mode 100644 src/modules/network_element/repository/ne_host.impl.go delete mode 100644 src/modules/network_element/repository/ne_host_cmd.impl.go delete mode 100644 src/modules/network_element/repository/ne_info.impl.go delete mode 100644 src/modules/network_element/repository/ne_license.impl.go delete mode 100644 src/modules/network_element/repository/ne_software.impl.go delete mode 100644 src/modules/network_element/repository/ne_version.impl.go delete mode 100644 src/modules/network_element/service/ne_config.impl.go delete mode 100644 src/modules/network_element/service/ne_config_backup.impl.go delete mode 100644 src/modules/network_element/service/ne_host.impl.go delete mode 100644 src/modules/network_element/service/ne_host_cmd.impl.go delete mode 100644 src/modules/network_element/service/ne_info.impl.go delete mode 100644 src/modules/network_element/service/ne_license.impl.go delete mode 100644 src/modules/network_element/service/ne_software.impl.go delete mode 100644 src/modules/network_element/service/ne_version.impl.go delete mode 100644 src/modules/system/service/sys_dict_data.impl.go delete mode 100644 src/modules/system/service/sys_dict_type.impl.go create mode 100644 src/modules/tool/controller/iperf.go create mode 100644 src/modules/tool/controller/ping.go create mode 100644 src/modules/tool/model/ping.go create mode 100644 src/modules/tool/service/iperf.go create mode 100644 src/modules/tool/service/ping.go create mode 100644 src/modules/tool/tool.go create mode 100644 src/modules/trace/controller/packet.go create mode 100644 src/modules/trace/controller/trace_data.go create mode 100644 src/modules/trace/controller/trace_task.go create mode 100644 src/modules/trace/controller/trace_task_hlr.go create mode 100644 src/modules/trace/model/trace_data.go create mode 100644 src/modules/trace/model/trace_task.go create mode 100644 src/modules/trace/model/trace_task_hlr.go create mode 100644 src/modules/trace/packet_task/packet.go create mode 100644 src/modules/trace/packet_task/packet_frame.go create mode 100644 src/modules/trace/packet_task/packet_frame_util.go create mode 100644 src/modules/trace/repository/trace_data.go create mode 100644 src/modules/trace/repository/trace_task.go create mode 100644 src/modules/trace/repository/trace_task_hlr.go create mode 100644 src/modules/trace/service/packet.go delete mode 100644 src/modules/trace/service/tcpdump.impl.go create mode 100644 src/modules/trace/service/trace_data.go create mode 100644 src/modules/trace/service/trace_task.go create mode 100644 src/modules/trace/service/trace_task_hlr.go create mode 100644 src/modules/trace/service/trace_task_udp_data.go create mode 100644 src/modules/ws/controller/ws_redis.go create mode 100644 src/modules/ws/controller/ws_ssh.go create mode 100644 src/modules/ws/controller/ws_telnet.go create mode 100644 src/modules/ws/controller/ws_view.go create mode 100644 src/modules/ws/processor/shell_command.go delete mode 100644 src/modules/ws/service/ws.impl.go delete mode 100644 src/modules/ws/service/ws_receive.impl.go delete mode 100644 src/modules/ws/service/ws_send.impl.go diff --git a/src/app.go b/src/app.go index b48d87fd..44480c6f 100644 --- a/src/app.go +++ b/src/app.go @@ -17,6 +17,7 @@ import ( networkelement "be.ems/src/modules/network_element" practicalTraining "be.ems/src/modules/practical_training" "be.ems/src/modules/system" + "be.ems/src/modules/tool" "be.ems/src/modules/trace" "be.ems/src/modules/ws" @@ -98,7 +99,10 @@ func initAppEngine() *gin.Engine { // 初始全局默认 func initDefeat(app *gin.Engine) { // 全局中间件 - app.Use(errorcatch.ErrorCatch(), middleware.Report(), middleware.Cors(), security.Security()) + if config.Env() == "local" { + app.Use(middleware.Report()) + } + app.Use(errorcatch.ErrorCatch(), middleware.Cors(), security.Security()) // 静态目录-静态资源 if v := config.Get("staticFile.default"); v != nil { @@ -143,6 +147,8 @@ func initModulesRoute(app *gin.Engine) { trace.Setup(app) // 图表模块 chart.Setup(app) + // 工具模块 + tool.Setup(app) // ws 模块 ws.Setup(app) // 调度任务模块--暂无接口 diff --git a/src/assets/dependency/iperf/deb/iperf_2.0.13+dfsg1-1build1_amd64.deb b/src/assets/dependency/iperf/deb/iperf_2.0.13+dfsg1-1build1_amd64.deb new file mode 100644 index 0000000000000000000000000000000000000000..323ae584e25836b48e96189ba31566993acc6912 GIT binary patch literal 76492 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%0D&T{`8h>A&3R8R-7O~S1%i=^!- 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%0D-o0fIp*nN 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 a0hyNvxc(Fg=Ve^B>E9dz000003RzmHLZ{XM literal 0 HcmV?d00001 diff --git a/src/assets/dependency/iperf/rpm/iperf3-3.6-6.ky10.aarch64.rpm b/src/assets/dependency/iperf/rpm/iperf3-3.6-6.ky10.aarch64.rpm new file mode 100644 index 0000000000000000000000000000000000000000..b5d5a4a558a3a6685b6678a3f171ab2589710be2 GIT binary patch literal 76348 zcmeFWcQl>N*FSvp7LjN{l!Itd4+n?nooG>_m*bS^MDM+m=q+jxEkub<1kp>>AfgjN zh%TbUd%5F&zTfZjJnOgCZ@uqY??3mPHP@cco;@>r_LO~H`)t4d@dXP6Br+>UloMKj zTYyK1TZqTT6T)}J{l7~XAe{f+;CcBv#x;)s1iF<3lqG zkcoi;?w>LUXp3?C8k=1SfIwL50L2E1xz~6Tpg?~Z1waPcV{QGxm@@zc`p4PbH?3Yub~!khD9i~ch>+mc z-yDs0d{9l&p~Epne**(+|3xguyA9Wg%lD7;i+)`)=>-op2|sk|%Pd+lwtu1g=68(S zi{m~G@$ZITu$Gja66SbLS-of720DErZu3u-Cu;lNJm7kjSNh!SR4U0*%ZWm;8uGG> zYNWO5M<~VehlWjBA&&!0_oP8Z3X`S%go|f1&rwFA^%w7JBd zh9J6Pm}{@muv&YEUNVP0JcVgWjsDpe$d=Zr?n!b9o1OeEapG7g*4Kp<8B|TrF^;ot zVeZCQNjdc=&njMGEyR~H4@*psQN6a(mv#9*RIvB558g0J9ZjoP=WtW7VMjl$L-s)* z_Re8j@$vH^1qIMB2pTOSf)IuygoFh6MflK2xCH`<78JJNgY!cyU{DZ}PXs9<022@p zu;7CVh(Lspf@mQD2vP_wA|N1$Mxs#yP?(SaiVrP}=0gftK;a_7!Z1O!02<0?Ap}JM zD@IlDf4@fM_whmH0?*?^Ioo%kdQl&z&u~-QFAmkLwc)r6Zwb`yG+yWx7;UJfnB2;q zT>Aa9fj%_9Fa&pju)+P;rjHGcUwS9?*n8Ot!)ZNH4uRrQF;?rOp1VZhc1XVsNEJP)>|^&%G}Z4DH&3Y;XKY@)s!|*6-<^# z%SXYg^||;>e8>uIPvk`EVisc^*-No6g!H$^)t>8!Vka10hHnmAYG%2~f9|-`oP9K! ze-_tusLI<{>qFK(R4E%juDc%d9^DMZL8xrpu5d(pUfcOPS639TDgYXcbS29Q- z;{*9B{T℘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 GWkh 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%0Dm+V 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 aOC%Hm4W;SGW!f}i^9u9d9Tc3N{{H}V)9`)( literal 0 HcmV?d00001 diff --git a/src/assets/dependency/iperf3/deb/libiperf0_3.1.3-1_amd64.deb b/src/assets/dependency/iperf3/deb/libiperf0_3.1.3-1_amd64.deb new file mode 100644 index 0000000000000000000000000000000000000000..c81f6622c6958e7b4806b5bc292dbce9446afbe3 GIT binary patch literal 55184 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�n3!U%o5eD2WpiH$U@~DycK1+g8A&1%`dc)%+G@tvdFlI#U|0 zsM`$P0Ll42ulH4zJv6QF*cef?#bgje-UA&dsJ4FXsM3Dq_c_xX^S~hpuWwvrB=vD@ zckCbHYlImVlnl6g*n&TBEN#;)qJQf^l$t<|5k%o1@CcW*A$ss0hM99(K&3zBwc+=x z)Sj$o@R_ti;CkW#Zd`6sAe~*2im|~$NjIq^!OmBVBH7fTg8z4{*<-`_#afLEeE2Tl!&-oVtL-^_XoF76^g4B1Yh*}bU=KvgwAOK)jm>gFK zCLx^D!w8(~<)bSoYZgS{u-kuJ{g4lQa)raXSZvtP-~QQ(xkrKgJ?>8vo9CE8Y+lzp zvKPj}B?*~lxQ_g?EdpO39KVg#C;oElZS$w1)=V9_$qS{ABsI;eF0Fmup{(2QS^hSH zKvvU|T8Xm$_eTX362E2&dWvq`?2};dkn|jM{#?IKMXu6->o^L8?9~Q7WJm&_m@F*Z zY6-9ues-hO19xZhLVeTT0oh}@J|IDEAhVaZ_-M`*`?Ujwdz}+?oLtSYb(~bdjw?q= z&(Robe0*cM;;(k~{JFZVNcPa*68oCy=AKtRAdQEk{0*Dp`g^-D(tkIQkVZJOo`%yMc0yLrnZ7tTqa42%|61p#vbi9az#s zF&MfVd{fw4LLxM|!|~kWe9|}KVTL_BjLVZU(wRWuY<#2pCVS~d)1n=bZdpYz4`c^0 z;Bq2Zso%~(E*yJ6uB_KRqCuQyZer!FV%Drhvf_`8w>^DVRHda9Ii zS|+>`g^V*;Coj9u0d*ys3g~8u(3xr~l*V_e@j_$dO%?kFS3Y6%=3yD<*y*Mjt@4 zJX2(%x(%q0>t=Q$+L4UO?vWBIgAT7L;nOY?QMH-qBt^k%$zZRt1xlMYtR+DeHuTX< z0JKYJkGQ(LR|c6XXiJci2)m#da9@=Px@#YL>eeSI&>;nAdshZ08L0fHjXH%3)4hop zrU$VyQ!&62mEsLHj4?1x5tTQhjpJO(vom=p5}hmZhlOLTn*`KWz-^bP?T=`1R+~!I z&nDD!OCZwP_zB1F6}Q;W5c{x6U=^w-!WDvBmYGZ<;WN85+k?C1#P7Xun&ETv=BBSa zar;4_t4{0CRc#zY)1hZMUppHzcfOKSB|`tv*_<~UA(dH?;fP3uOw37g{E|`qbVl$matuc!bAW!0^ghW;-O?A z@yi!X7Ph90!KK@Tep7c6&#^jS@Wc;FJv=3|_?LZ9mau-&pfwu`&C zhOAupqLVKQg3x2`gL~}yQ~|XIm2<;>hc0^BDJp)e0nuNJ)M)=?d)=z=sU2>>WX|MK z6B^HH7?3BK%DT-TIIU$A_;qi~5hIEXH+;Ri-FG(}vg}^+xuD~K(o6=_iBHlpTbUr1 zgab%Y-V_-tb;AFRJo*EdUB1$XW_0{hzN#jlW0wvpZmv&Hfz{<3b>_ov6CfLPyg)Kc z9*+|RFLaK!_xsZOcbgJ?r7aR04+Lav$Gco~(Xy6g-}QOm{~%+`z11a#L!_yGTxAK2 zg|3#P<&1S-by?MHF~GLpGuDi~3pTxo?1>J_g(vWLnKR7)BSgzT7TVSE;+Loms#v-; zDAP*JqPAWyxVcwhl~%rhx+Jqcq#o9us8}&lcX=-0O$XJ+2?$G#JJ)udIdM-)7Ix%x zuh!=#FUom4(Y7s4+smUJz+XZ^s-Naxe}xx{@4qQY&Fa_CewI$YwG!}}DbSgM9e|W_ zbNhKS4H=X7CPBji>8<|*J_U#zimn*vVR>68J$In)3n;`y3fQO(WB%OsR9II00!qdb zkJ!V4um0lSQ$Tc$*AyYD4N=$YXCkcw0uUiaJzNn>_fzNl%kEN)@rtj1S_zoTHQmqB z_uel`6K%Mo2VeMK4oN8QEWGze1la>VAJ?YDazE-$vBK}M=&d_f@zh!L!(Mbto3r_; z6~mPdw8mnko^HJyle9pr$4G=y6d*rFK>%RXO%U6)({vqa)Hpl46u{(oxt`UeM!hEg zR+DpNed8_D!6|()_U!!$(`KgSpTtVqk}+qz0QaGWC>L7i5szL^EG2ax+l6pj&~-Oi zC{!h5C*D^l(+W@%nZ-YT(|1C0I}JVE^UB8`l}la$kI11@m@7!zS^IR*N4Ey?sC<5( zT+JSQI~EGt?M5nOx=8fE{o4S#71rDa|N5i~tK0 zJg0t>9i9Zx7<$^w=Dh!i$h5@(&QG3jW+VJ;KkWV;c3cst(yM-j5|zg&W3RLy96G-F z@8cf@4{WtUs;TZzx6pK`q4>v1TS|nU3m%q$M%#t-mk-eypC%p{T*O$FZPi`Pt)rF% zM_%Q-*j|OCD0f1VB8PhuX!{$Q(|jXg4;Va)6kB=}k$O{ZY)#ma1^GJVwl^I zEu-48d$!wcu|&@l2or9xfQ{@2ZKQ-^c- z8Lzb(4sODO)q=u6b2&}X;w@bHtP)4mYooazaBo93<1+*Wc_lHXBXp0_I`)n$9p-V6 z9%YOZ4kqlS^G|aXwAKnf<8XPB3Mu%{Pc{7XZV*3y(WwVf^G$7*N#qC}V2)Or=n#~R z&Iy(`dv2t(lrv#mW!ek)%4`yG#=Iq_-$qC1=R7XVLLsU`SAT)L!PgKNWYWH<%Pq{4 zNWn3fp>`Fpt1y~iogPa^^N;FoS#=?1Sm}@f{Y=`|fpmh^Mupgt#(M9Tv4}@BW`sNF zOMN7$T4sWB@&!vSF2BH#M}NM^FTI2zEj=LF_(HXYtQ=!f@c1jthsh> z4BpKVH#|U<3>jxICcSZ)po6bHde@3uGS>>Q{mgOqXa9w#o&ORugfwY&4V@(w;!t+&Vf(B5w6Lp z^Iki*35y*E)!6V#Z)jjak@e%Q<;2yPeybQyee0zhM-^FE!qyvgaP2I~tIc!0#~Jsl zw0LePR@f8t-3wzo>Ku+%;{B?RnU>2-VQMXkE>zBQ)(E&x01t=$F!gb{}Zt z-O4pZ19nHI*+?Vg=slEU?HBl5o9>F1ME6Zx!pH@z4obz)9T?sx`SG4OHIS>TTDT6AhlMh2N$ zD;a#UGcPwJT-i_&w}6Wzk|RyL?ljIsN$H<1+CMQuL!2SWuN&Rm_sHyv171XLU~6D) z(4PK3S^wL+9S~U>GwT*sYXe5OB@LSLA;PTrF0Sc6+q0vpIcH}Uo&G|!v~^IQv>I-) z9=~dcf?3by&dgxAVl@psN6{s54=J$UCtziTwldyCY}I787`dt;WXUcSa{(^FrUAq`qR zdfKUI_4QNL)V{nQkc{hU$58Y7%|-mE=YA+GetyG6c-h2q+pun?(ufcjyARf>M7Y=n z_gCX=6-bZy*+`xU_jj?Q*^mm3Y0j&16r>3qi?yGjTZ##W%O2fOiut}It$rpDAAr3( zSrk~6$<5d*8mz5&f()+-hyv(6_KGWY&V{WNSBX=Ux}xyKLW(Qfqa0&?Ydzod$%gJL zx?eKpEgTFF5;CkzN%EaBZkdf1c~*`B_V!%Jj{x5LQd<};%vf=A_etGJf9u7QxdH`j z({uT-?W2c@__P?7f8PQ?95{`e zJ1<60Fn}dovghfk5F}qF-%c~c)Xu9^y0Y1Zh#cCibVfka4dzST+Jh{q6tdE>jeqhX zGDiXJ7gL=4J_SI}nhs0fCU@m|7yu?V-Gu|&PAw8KN(PjI8s2_@p($PqEBImftT*`U zoCX#d4L^fDYntIQPb^UJsEVBTg2L>X?2)2eu#E$|^4|Tk)aMmU5G3`|iYJCTLnn12 zZ<0@Cw|H*>4b>gT#IIO{;uy*&*NJ>*KU2mz3W|M(9&xo({*Y`Nkz>=uiD-6|^_d1M zuuFMUt8Wj~7X$^XS;r9d^x{uN@#)y{m6E)$;;CfCzk;C~ljjUCL<~IbS~JuF=6*iUXKJhV1fH zWcfb@8ctGYhW*VM;v5|6H&jj(S`OqqU- zr;d9)Xt?_Zg<$5XC(NZtd7pj*D15b0e1u$)e%>GQz2h=}t=D*ZCfU~5i2CA@9-Z&j z%FVmt&^kjy8N91@1l>JvWF*52P4kw2fM|>Q-Iw?<@rXnyDu^SeZGk7@FrBAw212wo z?+V$~8Mr5DDd+4LLVx~qMKLvc00x;tEq@p8o-^h9z9Uyy68VeHVRzyKvX|P>E@u2@ zz#{oL9IpPEQ^ig?LX4&loOVC$JicksJ2&l(MY-GGZK{;fDQ0nO!YA7BiWu}UgJp2= z)m0PCyh3m5B+}oF6?_mw(Gpojr5%KRDoE)UY{zzv5BqXGF#ir$*)(#2;*+K(+X!zJ z+75FUlN>g4+*w7)xms-AZ`IW@5^x7sDoQ>~C$%4W;qm)JT9m_PDz+*JOC;?ELdy_@ z9-tOkKwH{6@sC#g-{o;sj+m36Ytpb0_nKS(v4k~3sgcZaH7(9U*3!26>taoP2@^Xl zZ>*+}xY8YAvnm0$nMq~s?YkYlXu~yz(0V7a$Y#P%=R&CJ%-K0xa4`T}=*&r8A^6ch z{bpvJO-bV4l@iQ5Y5Wy_@B1^6s0nCJS;Y9uJw#0NJW`OF+B`ch2sx zcqB|zb5}C%PY6_JvdIsy!5Lo}5eBJtgJ#oDpnR|-YZxKn37T0mOPi0|T=f;jn*5gK zj(a-Sp%q!qR;iH(PL{2(;w@@qY`PiC?--*QpXK1|R(;69-USIfU3==^_?}85a$}0W z@aL61GNJ#6%9FIBa+CU|Pm-{M@=Z>FG6v0FQs>UaT*{jnMsz6+f?-DPTSu2X2tVLy zyT?eTNEcaB?m8KS4HJ-lS#C4<)3VHZ{veD!(a7}`)GhQSIyY&c9^45oHQs#`@#{IW z7&$>Fewb&(8whJ?z5J$d96d9S!>e3R6!7k1qg{9`z$h(89&kAAxJ4Dzx>5peU4)gu zBwa=k3#qlGJSN{wiT6{yP!*HY#&TJ5C*;%yd0N*eO=JD&jI0*cUP1$g=3g+cfx)r_ z^=hgZkLZnZB2KnmCrD15hA{8$@$QxShS@tDx4XPEjLY)oXa_Q_(tNSQtxMQq^}V;t zb-X(Bm?J)=1s`j03q|CAgLd>UszKdWB$?;R9L^I$<44K2jd{V-1~)x!Pn1B+^Vm>VPN zIHHf)CdZQ;UoHquu_|?hWK$3iEbZ>%)B0dL8YE{~!KT)K$M6QClM{=e_}Q{j;x0hO zIJag3vSKjA_65Z#4^L)CTI{$p-8lO_F9{+Nj(OZ`+v!T@6_kf|9;m8@rlx_Uyx7{0 z{V3sMIr%Oi0HNa9Uank}jKRi?LWKvdrL_#=+>;C}@F8S!NMF^hqAh zK*R$r+$!6bt;Te;LhUcmHHuLq=%XIj>V^UEuy*|oZFgrobfLu;K0YWt#)Lpcq$tI(JnC(tvGA`pY)3A8$KX*LgKN#i2%hc)ds{A{q?>fbeCJi~4iJa#}KdncS3m`;IJl#R; zc}|Oo5i98-KKU(-Dc%HC;qOT#WaF{AG=-Q-917_47Z8}?O}eZ+Z9ejObs$KI_O*~w%>v5? zbq401_=KFB>I>AFBa%UoP5(2FZ~YSDrc9#g!-rCnk(Sjr&!M8a*4PC-YQM0iu6y93?bRBpM=K+(6K{D;rfL$52-!zw22+ zww*%gupFsjtpm^aJ?0b;>eSHjKP}45f^)6Nwlt5xhXj^81I~Lw58@95fQ=*8MjZL8|ZP=(QwjWz)v}0a=V{n z;?&|T+FHnh1j+_g4J*{3G)PCP=9lg!&7PsP97$HAFj;DaOrDjgxl%g&V%P%mor4a< zTMC1T`Kj$mZS^vzOdyV-=Py!lIlWyO&Nl%A`{mrayp#~VOth%AM*v42XBu{g=c4Yz z_UKQZX4^R*|kb+unV?MuMdTS=iLCfKz!iLOO7<>zGfr9k+ch&hjZaSuH1GK z8qW6u!HJ{c886#`xg7rXv_i{{o$?AA!JM!lZkyL!lHmMv0$CmC%vQ4I*p*)Uq;+CH zMmX>Jahrt<8BQ@!VNCO$g#&6(tGI(8IE|>}22Z5iRCZ;xi9EVQLC+PhP{>GmB0o_{ zGXd9n`E;)nTV02zY=DSQ*U_xrFH#KK!4A=(*yuB}`wz_e<-R^$STI=G8TdXD^SJ}z(VyB907XR zt4-#&pT_0>0=^vPnuOO?%}$HFLey0t6mgN3*^;W0J5FZXe8H!``t37Q zXg1fIszwy60BzfUjP_3djdwq4-BxYsO5@E@n?hnmI`QH zp8)kD!-ec{KWL_c=6A{9mZ`TgNpWneG1*S>%TqH@JovD3bSxq_PCjyA6u1PYy`y*L zNNNP>aWQCbE)W@b)a`9y!XefLn4QSDtW8%mve(W(n;_XhW#%{Ru<78s-i9qot57~R z5)~W<3*EbBSW7T*tu0(g%+@uEYCS_mP-4XznBfcA_|L)ERa46i=BH7df(--6Fr^dqc z`inv3?w7qj3F;Ehab6@=^Iy9xCEg~nqsA?ak3JkuSe!)zWC5zl#kfnh6QavDasv z1y#s^Es(bCB`iu$upM$_vQwO6!-KAYd1dv-yG&0rP1CLKGkLZ2D0*?BZjR-D4SlZK)G`Y(5pg}oIh>zK1$+Y=-UYn)q~aA@-kW>aiBL397^q_Ll!JJvWp-NaY#s#fO`UFv*q6Q{Y=-{jqbHw&{TMmi#9kTYXcQ-7mC znn&0(D5o|L8P>> z{xmem5G5kA|Mgy<)>r07c-f@zpgxp>ljGI?QnK*6t41RSbdtQ=Y8mT2cO; zBiBNmz&mlK?asT3aA>^-4flOP+sYbwM@NBCX+4JENff%=kK!ebnW4CUVvej}UvX!FDWX(9g67ELUyW;|9 zajw{XKp+tVhVNLzgh2M!86GxLRm1PJgAoeP3%ti)UKam2>O0F~znNp|kjXYsZJ1Xg zHFCXMYA5pQVo#Mkyv|TmqD`1vV=WCXFzR}GZ+lH4!K`N$47l5}?Tyu`>^_hhkQeP& zgcUmwlxQ)2S6N=ZMX|-l|0-!4Vqfa@(zgy*Lv)~dtI6}%f@(U{j1Oic4Kf@j6C&~8 zb(-8vL!-E{6F$N|%pn-`4c8Bq1(0GZL((?{GxaFoX829R;1jbfj;Dhca0sbbeS--RO#g}FY3Hfa4nbtq!75gm+ z@+#oGBRhUT*U0$2Z!EC-*uIXJ|3DZX$QJlnjGlZhi|(c3fRY$Z9W=vx;9?dbxlnCA z_-dX@HUm|emp>W34*}w7_#7Aryy|?)zH78M4K~-;W3cRBmdf=ugDbHMBMiBu8Hp(| zl^%?bff+>zXv`=+7w0l?*nS_gLu;N0VDi~aKYzlJz7nkbgwa-HB0x*96c?b-B(JG5 z9>Vij5ox`lCMxd++Q4~-=&PCUkd<`%BB4dVW}H}z(Q1G9cy67ja1b`Vk|=Iz!MHyU z%3co%P=du=?n=uYThDBR2~C*7HZWB^FKgqpQ@zG#{4V+=j^SdGO;zq#2d%gEE&K>? zR7y|3Q9HVX4_i?*_(fe@LGDL7#J}qa0&LfJGj3cd&Nx7C7(s<=rM7^kg;mg4l0s`MO!L7N$0Gec(1mF}Buv|@{i}b{I^IjZ` zp~fZ74IEjC?AekI)?gQ90ScXq$a#jJ)?b&~QY>Z>19M9P)-c>t>#k;F5jL2q*hz;} zxlzEci>U=G{OKF76c4x*uIFO={;StJ_7O8oyBL-uSO zI#-pnRukPE3C&#(9P&Z$0K7}z%Xwb%(kci_S!JzHGD3=E((_I4xOKOdrX9qIiSzjW z)-$ml+m}(~t<+;F5o*w50Ov}V!OoNLAsc^rFIdvG=!1NN!wDQjA_^_@f9tlGz6+WtC;u@lX((P`scz+_RR?(e9x9D3#E&C~ZUo|faM(WM3M z5l4p_%d%9EBDe-oJ3*Mn%dVjUI;o+1LRi2gb#^Ri6|PpRBnpj)#1blqGuEMxQaemC z>vEwa9R%WTM|>ZGj!?fdNJBbgW&pv#2s8{@&NdF;-OCa1H5G)_C<|ua>N_rZ|XL zk&1Hne5}n6)O8?(5`VVXPh5Q(b^SA(dt$}|X%%6d#$3as2z?QWjCR^){T1%-31do* z6$R>Tkq+*YN~(C>l;vE6lc_njZ15$E)RS$E{iV9<@m$JJx(s3)q=}09MX2k-R1cEi z{)s^`;bhuiq~5odx)#TklG~e7PYswN4^{6pb18Rxy7RdfjMsL3aAs(=^3JfTWr_sd z#M4&D$Q9iIDwYWx`S_62PE}ioMvlFo1Bkg&z|*$jX^C^dfeOI`l70#%)r!uJeXYmu zA19%Nm!Q{y;W=v$4?YxP$#WAR1O0loAfE$k?M%_88C-e7 z|8cQ}LD*6urx`D7rsHFkJ!rY&dVGAduB5Qc(M$$MD(mzn!m(RbcaU0b(ppxsDx4Mg znmBew&K=`Cl@`KAUtbX>eo*1pD3}A*q1-$C>or0@X>R8IcVQW}vt!5qofvJUrgrQ8 zlPJJ^eJ9~QrtA+%?2Viv6M@UkbpiXH`IV3b^Iae8zSQRhkQLEQwNBxbqmw?s7;4=- z(!@L#s&b)aFa;&c?Q++w%zU1j3hcFl5NndL4HD~Xcd~ZP@fPZ_7{bVSbfysQ&+hxH z>z`0&EtTfG52;7<;t_=M#JE;s!z=bA62KjUxDavgfw+lEYi~Se;f$-pWd1Ki7n_3R zL@=B)8wR40B}B1Gz0_jLw#V6NpirM62#R}8{q_iRt?XS*O~E2(ifAw2tFX0?>s6Eb zBZukIXFSU(>_TA80(yR})U;SEi|nFd5wYM7Pse&P8NI~xFINNq;6&~+`Lv|fjyR)$ zp$5tU!{~JUPm(ADwslWU6G&NJgZa%DK7Z_x{Qd5A;LJaoUK7FB$nMUq9F?(_8z3KO zV;J1L5yejry(E@0P>REI9hvc)BhQYp;R=FcbRYkgYEs_>A5RwbAQS3>d?!1CazaTs zOce%4=F#?1#W&di5ELCca4FvF%(X(Lp5UF$J+!V8t7DMw#|=-oAKzK}EZEtU?&S2oS&`=Qy1&-t^z*b32g(DtVmrct$m6-j9j<5!pP4%j%Z)MJ-= zbOPq*y#O%cR{C|UtFN>TEoFMt8jpo_E(x>0UE`F#zN}ejNEg42s@zRco(!ityz;&F zi)zQdx;eDABT{u{Zj?*Wwqkm4!tBors#F`oQD&w1bq>|JJIdGQga?(R1q39{eRF~} zZE}%6hf+9}&f-dYR@jutAtzSR&U`{H>VLlph=v4<%K8XiRK34TXkKo9HXUf>rYmX1 zJ<-vq2d%~_GpibK2Db6v z@oMnn@4q<}ky&wX5Z3u1JUx_)GnB5h^FKE<*+YXc)Sc$+CvOCVOTKshm8K^9S+QWj zH_+~8P&@F5E5dV%c%jf`cyGDB(}?3QS$d+gCKsv5to=YxPOU#J>30 zeMzMOxpPhKxbT8;H)je?TeTpCFGbxBTR{z5rbHnkK$YdT329OAt}QQL=CmTi?mmcU zV^Vx%Kb35p%C((b!;8q+0jO0eu^XMKxp*3!Csm)_X0-O}j6-|`nWYf)73qx)R8%@i z-mHc4(qDx6s|FHQYT zs#}WCpLJvaBp0t9uVGo>49X|YE!iCnj+%o){7{UUj3G*EN28sIpIDyr);=X6c0~bX zMk!DP=_&$vew4T$RxB-lGXrKXmheThp(iVH^>)QaEPxY_i#P)~)ob7br}N#%_YIT(11~_(zYl%Y z;!a_D3Gb;1c;SiBXP-ED%+X*JY+YwV{yvrxHWnKPKQ83b95mvZpv;*=>m$Lw8LQql zc8P?(svf|xB6X5BNS|)oNneay@sFL2(~I^b_#7-J1Pdug?tiF(*suocb z7-In4vF=S0o+yd{8loKuxo6vFXW@sb(!KYS_h)w?Ey>ET4{taeCHX$>ep7oEVID2@G2=W&-D&)sWdFLPVdxLeG>jqjc@DD z+kT*}T|+`)3u#Z(ZsUD}t1@$zXf`WKV-y{#yDxQL2LGSr)SVJJ^*<6uNa55fAR(dQ zaP>h@1AklMrgCA+q}9@rY1qolYaS8OdJ0>t!OcMS1%2C~Hx2tn)sDR0Y}&nu^=dpP zhTp6x%z=>}N6Jw%qf`Vmt5~4W<`;_6q@0kaL(;{&i5)T864YT{7$N8AH(?-3`guOa z&q1t+aP5Gb;L_}vm%~%%;j1~hb;+zk+O;>&Q(&0s_~U`x5o`lamo89Sj8n3h6FrPk zo~r3Oggi92?kr4ZR=wRkc8Sx}=$&Z=C&%sj$ei!C6I$Lp8(-)H$#i6Zyr6mKy;ic@ zo|fE0iG`T0sbtM{GPa42y<2M@WHl80`NEUC=~mdB1W|~TStWYg&Ra$J;5|LNzhGH3 zmJ2>D;M6u6mf+4~T3ty@p0c7mj(!uX5V{vn3%jrVmj5re5*I1X16dZ07JSDtf>Qrbyh{Ag=`oC>v-$fyj| z-INZJ<4zMB-~Vrf)BOW7?q16IxSMXy)xJ8jd(D_?6k*Q4dh7kqdaQlCGscZ#=7K2^ zZag&;&$CKy{d7xcT!=KdL86_At|UvDj$!v{^)piZvJVnahmYX(AZHApzSouvc-d0P zHQ7n33Aq1}nD1n4-BT2_)a&I2UvLV|5e(UsBGeZPK%xqhjg;kqB1W>LDtnmVPNo+?pzSx_|c0t_wC~y^lMsDWz^zdQSW*-xO8g zFN+3EYqM?$7cyH>X1dN+hv$_UE=gbTLVT8mBL6d9X~}}|<%XHl+%QTNoJyh2a}Hvd9*AmcVB*&TX=979=PXV zQ7yGJdDV4S8}H+#r8YQSd|ZttCj#&)eQVKVxPg9YzuPa!_aMA>5C3{j!kV;8aXa(P ziL+L4DB9)ZF9N>P=RcK~-h!6vOF{0mj5zi9t_{J~jDGZUhCwxAhB!d)4BlLBK$XQo z#BVc!pe!^c)b6GOYF;&Dv2JPS7iS0(9Efxx=3xfm>&v^pBzGAuTus>wpzNBr+%uHf z-)A)gxeW4zY%!+7C8>6E#i?{B-=)m#&^5Y28T$(wJvt^$Gf;>1M=;&P8vQNB2?R6m zDwokit;#lu-L!UeT|?u>kHvx412)kc^wCKV=$Tt)r0xZnv3WBxrPdfN9P`T{&hK{L zU=9MNCwK2iELVLF%idt*WxQ`o+924cC1&SRa`0THcCRLgsW}}>8|Px_&P&&>)~d#PEzx)4e^MSWXm#EJ>i^mETbvr>K=L8TYpMG*?? z7e3t#1dj&6oi&leL!90LJ%7XyrfS;!bOz43uEaL)Kgh2%m<#@gk1`Js3FRp|>hL4r zI}&TpH@|m-M=4uxoNS{Vn4y`4Q3OsNyeO#EUkcE5NT`+9?aXHpLeihufMOoq&W-$YF6czg?`0%0cbAFOtMOCWwur#e<}6QT+#i1ipU z_t-*Y*Owmh0{pd~2=G4TojPH#6r*_)Nf>QRTySONaz0s6|3`OI^G8)sh`_tJHNg^8 zs&7|2;M0HYTSxpuDlf^CPzyMe^5|(l(nObXF^jdVR6ZCKe){q6RX15l%>rhejqb>F%T+?$70dgH#Z_P zA5u>a);00bN|gh;C(IUrrwWlSg7FJBVeMaQWbr*P_*@bD%!QC#JUVb)2Ttil(EN_< zi$s^=8`9{r`1zoOi0^TnP>8otR0Jb}GaO@1r)WGokz{Na`BD%53M;F7=+ORgdD*In zC4-a016UIW3)CqhcJ}`#5O?e71NQ1Y@@f^&vNE~hgF4zl&H@2(!fa8$6b8LZ91O<} z#U$hLe2%B@xxyl_PF&9#JhcbBj3JxgZ^9 z0k+Ss8}ZV~2;~!r*w;|y*iy5QBdqLzS3#Y^pa^EItRZOM83Y-K8IXMlKfZ8AW`g6e zg33nrNY&Sg)F8_)CVB2@b`6w`Y^0BVTBc#-Jz`TvLC$IH$DLldYmHZpx0KVfi5FI* zc1%2(ehn_(9`nx{0i|IBK?M3+w>T*UC7Wug5~YFoqgbrD%m@jH2G**8ICeP5kL8nJ zS2+{_QD1Fe!V2qbZa^c#jboTh_ zH34Iu#zwfq>;AT#VHS%AS!;omK@)P-e-Z&RUaeul;CNI>Dv)|zUZ|S4qox^a`u39g zlC)!NERTD7yVR!y;t3mDVwO|alJe5p44GO9P<@^hCQNP}9v-)tq}Vz7^5>0#TGkJzQ&5j%WM2eO4}Ho%BrEq(OSqDjRrX-N z?pmIs*8S4Tnm0~-ec0K4!|}$}gE}gpA)iLptCoZjbW0TvWsPvrp9jsQWuxZhuOA!8 zV=S288!$&|-tIFBn}1!oe`jbwlK$|v43pQzXu8Td!{%E0S=7b*729dv-FxPf9EzG0axB?w2?Lqq0f_6wUGFh+c58= zcxTHQ7*CTD&vGtuBE*Db zS^;X900*dWZOom6hBVZTu=NAX^g~-{B&o^8>x3j8orVLW*_bsi^9uqp*O}fheEY)! z8rjCS?Z`sYDAQ0~=hTSI7EcR*ESa>#9p*XSc$i}RF$1u%@P~+^nOE03k)&2-@j_HZ zgP}Fza5n-!^Y<>5@K}~_JMbbcG5*Mg$M0gHca(z?X>SNsku=}_oV5v@Yn5BZ)pwBb z(sghCOX&5R^@!ly4sgG&{Tz#;mY3GZ&b6z-uXZ67GZmliywG>PxI)Y>dAYe6-&sC1X2L)?YnN%{3;|>=18HhV3Esq_5(@N(Qx< ztC^xrIT!J4*$M;b~DVn^x`l?R{>Bg8={x|(l z;wp?^Rx$xq)BFn!$-=|F>KKpSc+h2m+`AvUHbm|%A7Zod7#cC&UnMiEJ%PK-h@GeC zaJ9#gwcn6O>9^Wol-C46EHQCQGR!il`^2kr?;Y2u>zW679c;EnAX@gR&-=XVFsU#* zC9O!oxPhwPJ>CCUHrYDl-+-78d{j23QA|IjtN%$Gfpi@gh@S;ZvEFG$3XD$=sTL@c zEUxCqoX0S~bTJqQPdelk`Uj^tk3e3@brd*TxsN2!+$SCb!uKz4%hOuv#i@DlipMZI zJC0U^rwko}Zqik&_}}|vA}f&S16|PbX$!JV)nxsg@N|v$Prb({|8RyOsmq&Y3TaX5 zjFa2aCIM?c?G+7q#8RL#~>7^g66~mEhjSxM6FW5q&E-OYt$;G*7ESYQt z#6P1?OV;)-ZK2sl`k9Q_64Hg0(L4bDW_hN2=3NT$incZ+FENhrJf{jh*9*wMS5|AE zt+k*YGrqh5cLIP~W%Pp9+1uE#Y(I80l2M+4K`PHlps!~sK%EB$i)Bo1(;exHJJq>F zn3G7%1wFh;U-bNml{ZwyNo|g1FXc5u<2NPz9^@FsXjuj(=&T;Bf1-R=%bV-P{V*O< z&`qnG*vxX2?(&fpv`)e%D&|wN9uUw#FWBur&&kSqt{8gs-7w_rHaIJ*mV#O|srWg% zY1Hgeq}{B5-PLcg5B6^hFvS|q5@V6DOw?=GGXKwsY7*KV9>*fN-K%}fzkcBjx*9sBfu;3Us?$wh*t3hAmqw_4IV%o4!>VALFMKb5}w*-mkNb8*IR{ocZ> z*b?*XzIIV@@L__rsLH1B`P2$^Pd6uVfbOof{z<;nWknTx_edAiv@wORh}puNI7|DP z?!f&5gjMIHTDO+_$IwTp{fqo2Ka&Q?&4OaH)-AhfWoh3 z&Tula?-Rl^JAsS;1%*o&mf=)9IE1BJ;Lh38NWMKqtlN`fnFrL4s49q?Gc-J}l72)! zM%Z%`2?wpb_ePY~kPAh%(BHND2Qk0M6=QG%-Gi7;^9Y)}+~7C@{6iOcYm^Pu;&09} z%YQX5vh~TEZMaZcF`D7O8MLg^+jWten;_c?beZKEW7Jf+4FIxKE6M}3SlP#5SiOpT zh?DS?7(msC#($mn-h_O_<4)IgCr7FT8d>L6+}=pDs@+f9%g87- z3IE_BY>e80G}en+B(FH2@-9`-@?ITWo~%!rA;4!(q}qfZhp33bqLmq?DQdWq+OFjz?-Jj%4bU4rJ9DFCW zzcDY{&O3&jU;{9vqQi+D+!!if`oo5=d1>u8M0|3of`#G1gGT@UCrO8D5QUGAP-=Z- z;mlC1i96tkw7itJ>eeHy>&bwWbr@FVz7X`G`;A~)OYns!U!{?O>YC+me@jZZ*Up18 zkfB2Y`?k~WLuI`NTsw0)>F4G09J&f`C*F-hb~fKG!AhPKstq#9&sk;&gLLKb% zZFfk5WheKDUNh`wzG%*-2VL8G`xAHX>+=F=yADDIb?x(!xIKF4qvA|K=$)cweWC1N zY23ibENe^0c40Q&OI+joIN|GD@zkZ>&S8%t3)(pDZtFU_B+ltiQ{T@_6u$VH)!=^ zwbr(Z-Z2H6NEAi`e2XwLn10-h?eHrY4ug8&L2R;x%t4S)aEw}2AP}uV=?xTPeaQgX zsWiWRM|w4+!aYcQ-;zmj8g8Q^xR%9oozPM0q$pj0*2!uYx6U4q1du)4O0-}bI+ht= zp5(pEKKAS0z)S~+*jdAIsgB268d^M!t^ob{0I?2l(zNX%KjX0QWV+I`B4hR0rxYx5?K8L;&M%<=9 z%r#~Xiv$tc%Pe@8M77^!EN|IlZcYCQ9W4vr)a(;0TEld7TsKt&T*W+4B!FE6h#^IP zgaLWoOib!d05ns<lxk=s-~?BoDV6p6-fxCMG{>78y42!CdwQKK znGM+TyDJyj2eMtw$q)gz%LpimL-FCG2+`{+)IL?H7jM@zpA(^e-hYfvZ1M$Zk+eTx zIyQk;mzR)h7!j|c8Q$*eR z3mZDvwC*84Oh*r*5zQL|4DJuF1@%s9liLpL^`@HiR-d}?&uCegKn2CL-b83UK1t)R z_<>@p2pmblRTx7*Q=hy41EG~@dF1BK5s7bMUcV_Ey{#Ogn39*MwiO&GVuQ(+9qrp> z?oc%3dLwz?-M>Z4s84rxA%Z=wQgMPLimCL@( z1?D3Vn+$s!dBd+u8J4gl{y^fMjhBsLp|SOvdtZ3CBoYw2;rX9S0APr@^)j~3+bV76 z0G4%BZ6aq*^SJaxZbt!?e5f>6>j!JOTmpU1;-OCgU|Yx$G*6Urt5h3o=cb{xY{En9Q9Tqn4PB)I$6D`r-#-%GF z6%3?_5kEr!&lb2VWO0Pe9{(FXL6+>#xGJ$D@QKYXFS{}VSjlJq{(~QhX?tk%SwBbn zi)_LqODE5s87~ST5#sZfVeL>Ve5j#V(+?Jhy|xZiz6{0`wO`{WHZL@TvwYRB2B+Or zUDv^wG<1miM*hypZar{pgu&ozB)qhDH!>qwe`!m20Ny*4<-sIfP*c!eisuxG3r5J- z`|NbPjS9j-2;qML5~)j*+z>qxq@&c;=vBDo{f8BNDFue3^E4ED&c(8e%-S7y5t*uz z8_LN^VWHlfgnE?9p~ebxUM0VH>xN23Bi?zLhk-0Yx31dMJX+&V!xM^`kIR-i>dPr1 z?L8Fb;c^XpSHP!MBfylbQh5O(&{_j>~r^k1YMG+K8A+eTxL>wSlaKAqqK`Yc*mDmb9t6Gj_W<1O%u_x~b zS)h2U4F~F-nb@roUnA0KRClkC63Gg$^}~8UrE#s4=VBI=GI={TIA3NV{ZhV8#}K8Slps6Zs8~&u*-C2RoR|&TARl+2TmN_MFn!aLX1W^9&-5TCN@(jc8CITy;q4E_;_KOad5SL0QR z1B)1TtQ|trrP65mFL(0~J{49$QskhtB6wJXg6!yWO>MFMsp0S91=r$$TSPt9p6E-g z&r5#z-%$1ERqkN=@Xqg0ha(#=9MauhbUjCH{>y2Ec`E*Ns_S*!8(S#_JpR9v?QCR7 z2ZIFFW7uFtSh&>+VN<)F7NtKP-F>Gdk+yl5a;G)?i)YJ=k~(v{%0V9!qGDiGZ(f+x zUVR;63lm*hO7#ErVB}`RQi9hU+p`U<>M5cTMPd3586`wX1M6=>MjR=B`mJeOezqQ)Kaq4NVs;_%hhtwemE>|NjMC=jeUs8SkglJnG?*!% z^l~FqE|kUx2~iR5pu92h|5Y_*j;UPCG);tdxw&`(QxI^85eZ(>k%C_as zAOg^zi&kUzmD-?O^sHXTUquTVW7h0_XXnq%t&j!j47OeoppZ6tctqB_kt2JnHI zQ3+amVJ@{}-}VJO{cSAkSolBsE=>P4c<6ys-k|0;TKeuKx%a-yJ5B_NEX&b#++X@2 z0Fz8Q_qW3?z_TRRSVxcvB%=FR$6D{h;SD-TDiLT*7+L{bu!2S7*~yt2Z5rNw(L$6% z%W^ZNR35@4uPZl3H;oRaztPvef9dZ z4X=rwgYcSQc7WbFX8!&RfMdg0W)UYO%|r=^nnbA{^6=)#`NJg7Vmg5%q}b{SZj;&# zZyED^lfK+_?)RzbPI#VGa0Z1K`Lp8?7=^AKUWk+?pQ(>-u)$L_Oh4g$o-1Hw&)uhm z8F=YRqOx}LCbk>u6S7)jgk4PR)zzplV9iWEZ1(3iOAj>~}}+5Ff{*%{T3`Ldw*yCnQq^*(_XGngQH# z=4P3Xh=lj*^aKOZ{3E%%iiCCfnBpSNeJ|x&QzZb zbJE#TXxeH2kFz__Z3~BRG~6n?A!ix_Q~SumoZjsyU^%?5wfF!Q2>uFyvK`ox(qP=U z)+olvYwIq^Y9SJNDUd2!K-C4Hodc?{?)K=&KLO=e_m=mlKeSICz)dPmc}_VPM*zsb zp19!nElW0%p0PtYdY`d=`;9aOmIxcz=&!(wCx<;l91Uf*>3pDXD+QE55S&`m7|@_k z`(`M!N%Un2(m^#(W8IeUmqSVTF@fL>LUMhgXV~*>Jw?V3>FfoFtY>C7Gm!h!vzs((nKaxvGHs!xoo?l>>oNw z25F_D>ESV<%H$O05{=bGF&VWT&@Fk`7J6io?%kh5=0hQ_2&z}iP*#okb%~Y3OYY6C zDT1ac1`xqH@nvr^Z8iuuk$ZLP1vep~KGXu}J=_URuCe?o&PkW>Y1*TVC>P5Jw1|>| zWk*_pSm?b4y*I1bdd~h@5AN?MNL%VW(HtvO?HqOtY2ou8|*_O`f~I60=9Gjt^BqTTVHE>Bm?TLt@&D1INE;hWKrLGxp` zPgY3|4%pWN;A|BJqQGTh9?Jk{t__h&12KH)+S*)|HJ*Y#`eL>wd)xy-Ygt3bZM?&s z1|=yM!XDmosp0{;PWrMP9sXg|XxmjEqxVI5L z!y@~O@^-iFB)ySKbcO|mCp$p1_hMvExbgaM1K{*<;H3eXQW4eu zlkWd=(z#V@+bg0?SYmOS@nkD0tcwN|6!sxFsZZyxn9xA9bV&19{}`21;hMec+q>Fv zz}B5j5Y=9zUSGibf=w72+Onb%BmZJlA_bMdtDx+T3Gwh>R9{9BjC9^L{S=)Fn5ZjK z0xTn6;Ehj17Z6O79>5M3h9ZR~Fz$W2}zh#?CWw2QbCp7S(9^0f!2no#!pCFs5*aUO84c zfi`bJ7}NU)GclkG6>l_)!HGN(qG|HP_C{)aYRmr6kgv9yx|WZ|M^xAnBT!5V36Qik zr6$oS$xhm1ZznO54!^R^-ad=DnE`JY_2W(wP1H0UtEHVDCs@(PY{Hc9yD20@nnae; zaM}<+RTglRAtb!$P!llo$iO`p&DS(@EF=61EAmcNG#drEGEXU1&Q6SR{)f~4D*U#< zHT`uOXFnXmSBR7dEc|mU{vQ^ZM%LpGoIbOkr!2ooMGG=y8`p@3W_qYLHQIqlgI%Ay zb=EI`=3tu2DZEj=I70M=+#vr+$!#u-8W;N)!HxMXdNWINh(HCqTYcI#>I%9$YLA^7 z6a1ajpO+eH;c&4nhI)}yai)t;(c3*lFDh)v{4(;TMI;Z&T&(B<@U1v8NJ_~j5CyMZ z)>e9}d)&H8c!mjwq80B~Gmx6EZn>P7PNX6c_8fL5Cx`n#^ub@)_|$y_l+7e>5D;O| zAyF@3Y6s}?9?y)7O*BdX5brM-4b}(e-sJF;nM*UmCm@t#UG0`*ra6?DY>1Fx46p9# zRn{kay*6wq(bNV}3KKQ%Lw08P`whJ~a#p%qbkZx;GVUNW4xDNZx$d-d91 zWGrJf*Hvk(vy9QpKZVMlLF6~ebCVR8p%&G$8SI#uXsipL1o*ogUC5p2{tW&1Mx!0r z$=zS7;Bcd+jd0CWt0N|`k)tt*#g-2ZB6X@|Z`bvY-~#Ex9O+566OnlBp76PN^Sm})+- zy}nm)5~f;^Aryfw1;rZv1ERg9a9P;hWe}fJ^#BV?WdeWnuL2p{%Y0-Avn2dhbP`{S zAaq?_e2^NyQ;Z*qc>S)nk&9%HK(lzGpt!L!$}4lN+{J3(L%x@0^lHi!X{Z@*1U?R` zUbqC%y-&-G1y<{7ppoJm7ifqqXZ>!zZfE_(5+$|{v+^0%3^3*E*M+)fO77-P zGa?bBh2sw9ZN@>ud-VrZ46b1UQbrwsAgURYLa0?RGg`Wk`^;gN*>zgAnQGJ&2+pFQ z?K*HQG*_CEVd+WJ85mek;c&N5+9xx!i}&UP?^)mh*E?Uh&vHfR_B(rWaG2^$_D&K1 zcbQFE$&|5t7xXTzb+9QeIo zhML)FjgNyylnkwcSKn+Q0vD5+_-!^<$)`-^NBl?Exy+rbXAWH06L3hE7jww3|MIw& zwbb*VfPQ?mbt75Mg3ssWG zfv4iu^HR;LS`Wbh}QHY&E70OlkoYpFYg)vj1_*o39k3jGuk_^fbT*`AOS|o*~yh>Ot zKw5oKvUS^Oe_nUcHaFrBYdG=yGp4Vp{b3wuKMc%{8q;$r7dE4QPlY&$iK;-T_+&Vw z>dG|~kM{J<=#c5bW=?vzmPYuUuU3(Fa-`9I1Nr>_2@5M^FQpv+7vnsO542NS$8pNQ#2m+Kp+>d{FM;#~+$i$aI8Nf+nRpYVJAVVFMhBy&w zO+Gt=*!OA^x9Q6e0Q2}v1Ok%vI_IJN3^#s+zI2GjwOxfBEJFq37iMVmp4#*^Rsi10 zS0M{$d=)sxg<9)Y})cB-R~oqHrLUvRx%tpkg^B9M8|EQ<*{*^qc>(B zad5?Q9=s@XJ?gil6M^8DeTXP1#f&K|JE8C`4?2^Z5w znCTA^H|4#{+X)q=T8&hx;)t^DVWB6QTOW8kL_+|uy3b3OHEbzLw0j4FLCuXTyeh^vh zj<@xJd$*!aW)5u`)87_6@d_{|YBvmvu zFGT|lQANLAS_2{j0pA{HfI?!iL6>A2G>$a1nXNMMzyj{#T=l4QOPFfaa zUQ=9zm|Pfus;PjYg|2{3mOeRa{OfC--*~3z1z^*5Y+x!Nv@Ov`fK9h?(Ya?N)g+0X zPV^Vy>NaakAC$>Rim2;FkM7)S*`3+(j70$P2WEO32!v}q&vK$WrLjOK z5@%FtSO#Vf?qrS3ohVAnE*&dwh=JRIq=`zmEC^}7^yW5$P0xN}&En%aet2eyBfg4A zb+fJ9)xV40;}xs^%1ZdEWHJ2W$tz1PuZFo*V1&? zQ3tCi{+YZVu&9N&y>yAS+BlqKe(}(Plj_2Q7=3v%f8!}hYZ{ro-c|>M!7FcuNUtb0 zTgAqd^80hH>g9!oSK=oB@Hmg4=UJTh8>8fVQQ6ql+c?E~1WoJdzK6hN@e3hmov+g=KRk3N#;`K-%*maRIB3+IZ z5XRA5`cv2ZJsjo!c#}a3swRg=2wG(}ha7eFg?*y4Ss;KztZ^Ff;9~MuBOk!pBaTWM zoX~3Kn4N%m8kAr0dus^LVjG;Blrc-ri#tGYM2aixy9uG2|%>^{vq0Ru?zOO*y z=p5j`h47whjf2$6GyXoQCege$NLYCTF_056Ky#;tzW`LQVq4P@BGaBoXbO z`#}c(!YHCF;*nZcO8qsv5nN6AB+5z}Qpuf7NMWpEo3HRg}SV=oU zuzXp|V=CNJQ4<1Eja=^5%f&O(5iUR=;jz~@;KBS%DS}JDslw*jM?Ta>FbBZZ(e?eP zGtbuxUNA%DlxuH?)FK#c>}k4+^z3Rs6aqiMJo81;lOA0ld2*RgTgF9EBMiSsq})mR z(3dqZpyS%^7Pn_OaR9)w*7>I(?0spDCZreYBkDQWLV?$D1p5S_$&x- zyyz#1{rL@lw;>J`^B@b(;wG|vxso8u-=rd=A`Ri^H3d#a3Zz;@gfqx(4hzpniM^#KO z9i7laV|pXAN)2>f`qzCmgJijLe|$$!!fMVzn4u#TTySbn(|dKmZJO+;61UVdn_F4gJ5bF8JAjo4Xufa7b!eMeuWyF9^qywMjwT4kTw!vOhKVJ_+wi^VUf zPDP51$u$^OG0WfM*K;Ri<{fE1L_OY0(rnJqOUx(DAoW?u8fL1O4Qx;bNGY$eKRDO( zPneCoF$j<%vBQ6~$(w%s6&h=LUHGte?IjNY(E}a^F*{B;_k-HI2w{~fh)=Mhxvu;Q zm?CC0iWIi_zbb>ah`vA`7v|&l|D(dlcT8$!jUmDA)bq%OCyWAvf&|4SwG2e6Mr!y$ zAau4Xht}5v^KO1xS4AaDy%)+kW6~W@~vJ`imC;iWKB0OcAdY%uinNU>TydE9HWyRhP*f#G;|b=94YfmgyYgqK?5h zZYpxVrh4zp%{_xSZZ#4Az_a=Q7uZ)FR+PoFdY?9lh+2#jG6702aWIbB{EyC$ofWX+ zVHL;2s}+X9tQ|T^aU9<*aPGuvc~+$%q};jPs;UDtS-N$ABf?PF(9?GUnJABfLVG-t zExRvUuO_S#3Ut-9=J8M&2)@`dEv? z(eOMWmSNvAkFNY@<#HoDJ>#cvaYsQ%#M=9b5&0gGJbZ&6(h38O$74e)pIzcpJ&_I~ z5r}TvJpk_iv~_bOYE|v8rVW^0=xlwLrF9@}S2886SlK!g^Sg)i${uyyMYYg|8WK9) z8ewsh#&Dy~Bl3X2hu>~2``2bz-md@?@V=G16}w!NUgL1aC#8WsYmBDC^~z~ZNrtvy zik&Bn`nV^t!=|&Yh!mZcLC*-z0Ow(z%2Q_oUUN`~ z3KFU)z0UG@>Zw84NZvuAw@oO;raua72|wd4OV5t&*UQdQk0gs*D(xe?u*xV=V>FlG z8+j48j(E@okla7u$uMB|ihI|E!sT6U+M|PS9n4yj6#upRB%W2E&EzIM#RKNhTB2>_ z9np4FHPbqA5%T2|O)(fNoA!~LFZ3t-usDskH%)UIenLTz)^LfETXmA#WwBhq;FZ7JQ`LN)8LvNSQ#eTL%NTK70t@p1)!&2MSAC2S_}?CQ;4WL zt}{d}{-9B@#Hss8wZJ3$nafFb>X&xW@KQ#G_lE*?R*GI%)os0!)l%K{(;aN&P&It_0LHdq=6)V`?Toz}o;4F*BH zz@ee;+=_Ol#o+ij7ED+U+e*{2|8cuj<&W2i>GB5CNggnaM=4z8TiIDki*+6zWZRP9 z8Gx6LR-pD&1={A=9jd+VS2h}D#kOd2la?*?K;OIR;aQP-85O9 zX{hW)j(z=qwC{rw%Mq$^7rdOA{d)G;oq%5|KO!auzjSI0!3y#3Md1srJz zpQ9hLIeO5sT(rHFvjqJXiuWwQs>qF1NV_g{vCz|(+iNgO5fN9jor3phFVb}WhP=~K zOPXR@FsUpsK1taXzkax!)nD~{Hu*CMJ(leGL@grj2XNRIZTwGAUW46h8MM?#*0Wh8 zXjMr&55{fA)&j$C+nNZ#I9RxO9{}r#3`c}R$7r)Jm+b5U30(w{*~V#KK#Rc zvSu9E2AjAWxqSgB0X)lFq%@d^4(f_ixV-UoDX}dl-nYKm#I-9dBn8ouhld69JHp!R z1%Lw5e*!xj;fX}u(aB6?6e)FrH9eJbAA=0SV|byji;ZPCupy9tLe)v{vK^poD0M`0 zDHL{Y2-D<^Ao!GXsF(&<-_m34W>=}0DFdPEuyh3)isiuc*KwsQMeWbxw%=jw&*hzcR~QA zsoD)v_-46)D_(}7itczF3{#I5bY%%UiSeBp-|t=VFZ4WoWw8aYKHEIQHxw)H3HJJa z+&qC2A`)KiKE8Kz_>m$-nK9}&22`gchzQZ7qwvMGH!8lX7sE*D$Jf{x(orMSAVPls zO3+mpoKGs(?H1+EbrmFG zvN??i`2z_nOi^y(>fV`Vm`}|7ILMH|ew{V#yYI5$6t!!V30UsXjDroeo>5a?e(3J_ z#-_H#{9uzZNY&}+^d^F41y#X>JMDivs(Oz4?$+R49F+l+HiJ{$6{BOsm#$? z_K}n$UgUmefbOZ1oob+W9Y43QA6OlUnrBWG=cqGKa$o9J473dU_2~b^nm~4kz5)~5 zKG*@w9&2)7ZnT{TuGY^-1PBL5E)Lf4m2I&P@Js zsTr03cGZzJAMGl1Tn?SARf$)`K7h={rp@x>2+8#-*)`i7T^8$nCTWqGl=qhNoIvXz ztD$*+zk|mezklOp=n9=kDPAfYk|?!cRUAZ~^88;x32xvo(y)m5V)5pvXq3wpfj6sQ zSG>jo4Z~GGH{pK{;SC!~?uQMDqpP3ZsHMQyrYNa=fWBs8${3Jb|=XDNhT9DRiB1$FU96zZu=JW8GuYr^qONi21HP%D8bAk z@7jga%4|=P^ns@_FML;Pp#3hmvGD}@8WUG^9=~TT;Yb<`C$l^moL~sTP|ionkg4_( zGle-Yurc;lKTH=RK+M@`dP=RvCS6vC+IT(y?$Yn}oKPhTD(9*C`AAqdP2AOPY-|ux z5QZ8xPlrYB8MZj>0I0Hh0?!Nxi5sVy)EzBn;FZL?(}`w-Ckw1AAp2FWjyX+bTF3;x zKw<4|Q*Ql)7ch%0t8CKd4wH;z_?r3WenNSiPK92ZPZk(Cx+PANG*1eU0m1d5@z6I= zreXRy$c?L_j`(!Ph}cFnV1=KV7rWRSG;Cfk0%1}`6~GwPIz z`}n$SGM>x0bfVK6$)mSdBz*}6#X4*Jy@%lb!Uf9EaCv1eLN=O-{BJ)dKBqS)IfMk_ zx$|1Q*T6PRnEU-aGS4zqikjjF;Peo}r$N{*6ZEdX;L!g^+#bG=*~A`n@`piw@5@N` z7$@EVi|e?!NNp9t<0bVVapUjbWA3iV<$a1|ZBNvD@7I)A!5a~s6U91ons4!MMcDnb zH_3p;!^{NF9=~7@f%~+qc*GI|9^2d=yS7XIUKmh+;a2_&^H?`MgG9YF2^naJbgUGw zGPW|2{{nNRmC;0Y&>J;eVlRUVU?bdEq)5w_6Z;W6F|xuE_4-pvb$7r#RAu}T%iIsb zEF%C};3CmlhU(?I9-FNX^WBX_v>_qO*Y8OXZb{GAllZajJrG`rp!m(ivj$V^oK=o_ z0rDhL6l@%H0EZ_==wm@UU1Ld@P%SkG^p8f29U(9a6KlF7W+H7cZ0iSckSe(}B24g# zlPf4=TBtcGuwCghw=92t9}yYtIv}Nl)%& z{`Ie&l^Bsp0=xW9uPU{}ANQdft)h2H069oH6tPtl2f{$;8|_oVY6fQd+)>N&{4c=D zD=;GCCZ0WA30N43dJaU1el}`~#@V6XrOB9pSC`@;f<)1U4cbLsXXE)r!EJt;06{>$ zzmfk-W$}CeUiaxlO|vzF1Ru9Xh!tZ|)U zx=PJvAU93uF!&_dD6=DQNHw=Q{IA^S&zZ&|;1LiMFC|vbYt^~dMvM{HEQcLxlq730)2DZ3?mfL4|+QQASgs$K$o8b(n`L`1N6 zt)>%?`gOQ*KsBp-)OT-wYXVVmn0UR^B`m!R;EFMuRw?CvNiV|R@!1Q2yeqhx?(H@J zrJ(Z69KV;E-DhZS%5E4l>Z+6s*z?6BdWdo(KN+NqFY%jj)rp04A_Hof_?u`r%c+B^ zRfoNWb#Dx!potCj`kdNf?oK`?8awtIsJeT2nO!F5YpGQ6j6~N1MPl)F$40 zNiNUm_>11DdS?pvMALY|j3?)Df)XB2@4S27KFn%tS0aFU^mm^!^=NPPgHuYlL1lFx z4)$x|K>;+*qRtNA?Ua!rhe;q?UM|+>v%#D(<^p?VE~w)k@M3SqGWwv6*U=G}4V#o_ zq&H|t17MwoYs5t&K_Z~7$8l@)j?FxOC5B)%$7h+ zrI|}QyiqN4&7ic8gir$q)q%n+(RDQ|Lh=zXHS1e8podNT6k+g3*q_rs>l8@Gpvl;_ ze3O3^sKBH=NbRSF+CUQ;gOwDU=Jy}~!n{@8E9?8HO;Eqdu8h3a$Y1Kf8dX20e;k!X`#-_BvlR&p@-;grBD2QBHw;FS zg1xS^Zq2;Hv|}e9rA2W6);;NA)&qTzi$JpuFMIB ziOqSrH2@7&o+Zp;-vt2=wEP=XGGViJkN3yf);-z}@&faDg5!*mb7JCHArT$_x@j&H z|G1^exRQ#u<8nnKHqUJq|KDPZ$5QcZx1;tU)rfe3$mgIfje3Atz*l-cyIm0dB|7CO z8g(&sx?C+jEjx1!ZH87}%dxj}YmCP`nMK6NSV{4FlDSLkV+A*o*$ZM=%-mKRqrSB$*JC`?Dvq$vCqc(=vmYGK85r^6Z6A#}Za|lFfnS?rwPAAv$ zEhKg{DvNzl14EY9F}Dlj*V{mlFcS8`AtXt~j%Q?j%37I6@5WHbcz+?m$NtNKN@JSyiUPDsGD7RvsM>xhOUr<-E3y3SuRp@7lx&kzYu7sQBw}?qLwuKONR~#P*s8Nh;~9pkB`lbTTj>|^U%-G zV@nx_#9d&`m4KdID?MD|tAIq`-AF za~$KPsed%A0_h28wDPjAyKeb(VU^$5)@WUum;1aebMA_0z5ZTCbiPaloC-@REbI+Ly_=X=)|9-P_nVX1#j+8?WB1v0(#c6lUTe?Zb(R0PKOu?&< zMD0k%^x|i{)MUGtU*d(y!^$|HK7fxN?lTX-)*}2D=C4wU#zsFo=ykIP#9);J582w) z6{-b2_%wN?a%h*s1=2>G<=Ny=v#odRQdBFH5w)`fmNhvv0`M$$MB`OGe|h+`${sun z;;JSa?_AqEs0Luyo^t#ceR|YoCL@W&#u_93T5W4cea2oTPjd~5Dg3G^SPbUDPa`>I z#=)Q?lC_PfVQB-rxh(UOH*fpje6JzOiofmmdj~c#T4n(f%_69evm6=8LK=!(Zu@D) z1Y>Ij4qTf2$|t-YHKM7@#DeQYPi!2syo!m}-@6?y{u4glhff);Bv~U4VQ^mh6YM-L zJ^4lugAD47@PwZzb@oBA9c2mah4~q4!~jz@Mw+Ce|;;}A$l=6&rfCpT)bcmBypI~kQ<&*Z^@n3E#zIyEJM*{{606p;jo43+V?$}`t z)~3sq9!okN4enD>e^ddXX zBYW}vk~~S~44rd<2QfVpq~GN%%tXBLFreWbw>ZHF?=0Ep`;tm zuihArEH4bCxd3bo^JKEiGyRwhNx(;#U%ho1OQYw_=-}c~J_a1Us7PVe^k~+1Pb~LA zYL!UE+dq@W+~z!O>k)hrKf2)F#!#FkyHY^nXg6>4oFU&SHIu4B?DM{t+TARi(C}TG z^3xYr>cz+`3Idr+lVxAmO*Njc{UvpNJyBir`&kX<+NdV404E3DuYE6mK&-0`SAX2b zSDv1ZDRoClDf(Z_dOFHNC3HVAsf~GtCjNL`AOaFN#I)50126Xn5O%560?OZJE|@_j z$q)gth3SOKtHIiHLbN)LT3=>`dcKhKyKHj+Bz50Tg}sf*$OK(OeceJvzwsK#D^G=x zt{;1Xj>6mAREY;tf~mAJ(%anSnkzXcG;Y52*)tOPEG+V*pa)v?#e#NgM6)Klkq<{~ zZ;$mxFY8AnQAxgGKfQDZ89_uLr1lSi`?^BsQs$=63_j^$p6fV8eXCgdBxDrTjZEpN zOutT`E5k^m4TAJmWlAhR6P-bbY%bg-Dzo&qx zoaCK(ZLF3P$Q7+0I)Cpvq)ZrV;a46*WABky>1cc(c(xx2?Df-949L|O>WkPxOL2Pz z(_L)OlvovQqzKYgzV7+KpC(sVv!P5}%PcqPCY4zqofuVAF8tH4r{yfIYSxfCm3`G^ zvEYoG_SNWBNMHHGJH53)bJbvn?`GzEu6@~Dz{ifNqs;@MSo8y-Zw!to*P}xX63zRc zl-emx2)@xRt(WU0G`!w)svWpSCSxTXpSvW*s1`=`?5Zg-u0*#l7HFC|I@+MXTg|qi zzWLxPur=!tWhgZ2J-XIfCW0y2McQU8TKG6tqZZGIjQ*)VaT64DI$}EVmgjf5R7cSg z44=nctrg}9ZaJp%Njb6T1q@g}aocKNa?7Eb+t@aS1-E9;K*G$@ZK?OaYT-?1?^q3O zWA`WcBNGTaDo@WX1AL}-86m6O<37~qw2w7JL~H9@x*42D1@z&3<4_JSLwd1~nOLrn z4R%_k*^&cB`LPRX0;)uuw0y{@X^+frG_&+$AmekRnE=NBX|XUx)kAM`RuE;~c8=!S zK~QA3_1R5w#QZT;0`rxRsKm8Ds@orn!^{i32k+s}NPJ$WI)9=vQ}Q%}TwJ$)nbFUF zhezjR=3C~nORzD9`CYOibI8@<{C;vB24C(!XsN=LE7F!3GYGkna!*DpfHbQT<84w9 za7;WaQWn4Ch#>ue4F~*5{9{1p1t$1w{x}V6{G|`?pE4u;?304uSF4IJmrx#6(!?g44ZZ?j@E+@ zQqNB|+{!(pKcMcEus*G-@lNo?;T~mKiWOr8AVRd!{FERX97Uj|q;_A)h`>oeOo+i$ zfYXR3(6NJ%t5HptgiHfHJkQdkfss9@Vo+x0|6$wKASBA#N*UB1-&FL2XQ)(Z?@92k z3;6JbZDUjet^dxXh%i2`?!8~Xo~lLCHfnXdMy7TqNbAd|MBHJm6~dVHGL6E^&Cyfd zBij?rh2J%XPN&X4n=qihfiL2s>d<#bgcYDZ<%A2&4TTbsiM1m4bF80&|JGzb1oVmT zw=;r_8H-c~*vzjK=@t&~IdOp)(c^dzl458@BPz{0Ji;Ujf)PMbWeSfVFTcUAn%Eh$ zU993RtI(&_nN;eSuefbwp-xj_IXjl74EC>Y|CdE^M&Nx%{VD74zT@nidsZq94BQJH zTI*t5U=e#`vfOUdbv;vWaGJTX(cF_uX$wk9M($_7314B$JT=EY-M6lTH{gmT=ahfG{#Gw40?g`1=dXrK9rJ%ceMeH}zD76Dn-}0oFb66doxAGcp}6*C^-`$2jOc={pMcM*LspM$NN=(CSb%}f-)rGG zCi~#{vfbJ|UInPXsks2(!sTu846P~)6}^EV@2(ro6KM;6z)XD-cvTd2!1vN#G(rD& z;BivEy{&25e~Ks8#Wu?VCj#HzvjsvUVz;DuIXLPx?j!W}ozrRW$Ieg%EQ_9szr%!P zAXp9rW8UHQ+K41u%Z#g_>IDC%4l)@15_qdom*l@)N7>1tMyzIN77i{U*P&=T5!?H) zCYZrIZVQAL#_k}MolHsXjDo^aMM(D#{neK!ZV;xCKg2D`&=3nnY|R#;zy8PBbi(8~ zMn(Lk?Yd^bbeM=28RgOs3vjM0F@h3n09|(G;Z_V0j_3=OjP2}44tj+OM^$1F2SKq2 zPqFfPFaEI$iMl}=Xf4pCQ@AfoZ3<^wa@L=Xz330Oay1-9hW^@!vH z#)$eRExVT0HlQGgFmA+7Ado}!vF3)z529Bh-EOM7!iV}uk;|-Aya{N4#^lR5VL|fc=I-6i@f%*l$&T??atXE-zwcfh zv5>N2O7Ss)8pOu)Uh@TzR=p#OQDov}J>6pTU!SNKgB8Yvy)ONx;9Uu3By->qQ})U4 zj8>wztbp%ilS#Ma$2eQQZ*G(h4E+PPr*QbA0UD=v?I~T(*<#qLO;{>f>$U&L#y4Xg z(VFMPNoUKhBUBCfS$L&rGJwRfws7`a?L3`dI?A3~o@*zB%0@OOB4RX&YvjRSMZ5F* z)LBAz<%W-=+meq8t>4EyvmC81*Pp*ABK2>{#?&y5;bt;Vgy0*`>=Q%ftady^isk-n z#h_pDBOKrIjYwh*sVu+1B|2Y`S4Tl5CksOWTHCO2#uG?)d9%i?>kzy30^(VwOxT7e zhN%$R{E-PBq-8xO5^!{=TB zdte4Z>HGU8q^WTf;k?zK5KT}iDlh>*~}u*t>NeWq=q#q|e08<2i%3gHG&kORDc zveDrh?0v^&29ed_guexG$wrpPC)JXUY5x>&tzICbeA1wcL)Ktj92i9EW<3}EB0?XL(4+3wO7wby}lsfx9ty@%d4kytVQc~Ki>uFI5}V) z#N6DknXt_zN3|TAON%B+g(hcUA4#9u3;s0jdlfj~ieoWY4NiB3W{|dwo@6jjsbq{( zOq6j^#wvg$u+#Mqc9VhQ`+$sf8S;r(HCHKxt?qq*n6EV!HqBubdyNKYdaB^F4DOaY zsV|A$t1)~ba8e%C079AuMR>uFMo4tv9DN~L=OJs6;sfB6@S{=warRwdk2Rky;piQ9 zaumW3_2!W0;BhPY?8*)Wsq7TGOUOYeCYZhj;(m_}24+QcIXsph9tvWn2J3iJ=Sn75 zq&qjl$MeqEZHYHXP5hysi62>3GQVd?Cb37wIIBF3T>EmOE%uu%XBtKILy9nMu3u?Y zi1XP4fXIef{IFO6wxa)QGT$d;Xth%ERB}U8g_$~y)Fh0^5g>gk<*#Zk-c)*FNVwO~ zmpj+p5(2+mpB}PYK)bC~NJ1&vaw z4xIwP;24AKwCHXS^jQ0yDt_9K0p1|@94Z9iMT$YhdkCKJy-hQavt=FV?5bBUIf>U) zx}fKuj(bpSxO!URNk#XUlT63>Ghn*L z*Y9SQp$?9oHRiD1w5QWT|&jH{AQ)4C3;r=^Js z_Y;LVJ@^ytbqq>5oZ+zn8T0kqshEi;P4HNU1-X>y>padoZ%i`#(~1UcREPZ?A0F2C z2n-i}boEKD78HX)4fqwpn|qmgGBqsLg@X7t55yuxP=944RoXK-Z-Ch?kCE}cJMm8^ zqkqQ*zNH7)u2e18iTXSh>20K$t!2$n=armnfBm{Yky7}&M1M!K;&&P>e2_g3NlZFs zVsi`0B>PW-c=Vb)lbc+8b%Qsww~ezo;}NgkN#6T*Sl84KX zN%jEVQ!C3SXiq}J`@1r_3@x&LbJMDXj#?ID5F=9SHeLDsTJQPz*-5#30 zn}6c2BTi(-jpDIHvTcv$hu;j4V&qZDWo!xTxun z5d@S#iKz42rUPxUpV~*;X(>YQwGr*r&ppB<+_Gj~^+Z?02%b!tdLzj-k(ig;Ei~%R zxT<|nYrvw{tFiHbHEbgh-m<%T-!nt^5=JiqJKo1KuhOdLt@A8^@1rZ>2YIhxFWujg zTMv=-WEQ4}AE}p9UV{|N8p3m?y>dC^w#_Owdq@X;#De-C&|?g$>piMTTBW;~g&`tp z9mS#4K{GGb!>#B7GAj0H@U4#7dcOmkIm67zKS9%V>`g&7zqncL`5 z)L)7-k0XY5mOi1j0XB6gwL#<^OMz5_GO5m9y|9CtG%)oDOelz+(F&F3S8QhxeMgL; zeh{XS!#ep>t`{rlk3E;VG&SmtbR!!Xepl7PW%8tH%9S{Jb_!(+`GX$LIH>B0FW_Da z8e1L3g(*k&gG^O)Hs|MUF=ZmhdB0&Z6GlTr-(U~qzm+qLXwz!zt+zFH!<}DJ*zun- zG_eK!F0$J2UiE%xpiKgxFf2#hz(N*b@zx^x(Nh2h={|4=i-qdLiyUE6!Kw~1Q4y_f zR%N*n4{{2Y<63XXiPtRSvkiZ=?4OrJ{KIyE1J+UJ#m2P@bf<_$i=RmyhcMMw0W=`; zJ|`xh+|u!qSI0HGh+OgwBfd{l%|X_2A$QxHck-aByWNDA{#&nVtrh=N(bsthD_v$% za%z^RbSMObRUt%_KmON8zsz)I1rxQ#xKdNe5nJMO{dkr3IVJd1a4z00^apae#6?07 z7{7UyqP1O!=b$r5YXXtx>0q3|no?M5+L%d$8imkpj#}_ZtO^)_onHl{hX1yCS zdCDVU?vu|>UidmZYj$>Sd9;Onr~rYAfFfa)tpXb<0i62;_x z!)tl4K2Ow#D+g4vKWyxrXPtUBYy|gEplN&-mIFm3A9zF=ggZmvS?md{YMr+22Ll8*| zddaWfw~7s9db6xAlvwdg|FQO!__2?Ht|(Gim1|-Tj6b$?NK%(%6q;w08j51r*4Ljf zA$Dx#nrIR33xT1ssZ*Rn23rN$|L&wD6I)i_w}suT)8IWK{am_c(qB;6b?6Kd@U>4{$hZhGtUAv zzN<*?wSul(xO33lO`n^=Vi6o_1zJjOGz%y;`$N24i4s$1+^<5QFhEUvM-0YRLKTzz z*Oz#Rv25Xr_EbRcQ9=Yh$F&= zAn;u~+WwEoM5oB3o*#T)#IZpD`gh^S;^2rpvhJXmHkRBgF}DZOn`fy(>7m}pYR<@P zPbq5FC@V8`z~)J{h>F6@A^YU<&CsCJyw4Hcx(myEap}7;byA-Bl^uE(edTFzr(gY+ zzadNLuzsF33e)Yg0wh=Jn8Q(t_|RK0er3a~xDu-tpH_nzQONlE@PTWIV=`kyMB|D_ z2il-NEmPHPZ+*e#*v9ijR1SY((}?--GH zW|P#Idfc(vovy-LlWtmaJV|ase0r*)V*)LRigQVU-`Tt)+P^&tEMDp|t^{G%S1yod_p zb%m;8?w*1Z7J1wb|Lu9RfGpl*1f0WEvgV_Z*##&MOX7^M3_K48Rh1!$?uHoemAr*< z$PDyFF+MFzALM97;5Yy}`s57nd2FPFwW=V{4)(efrXA-BYkc--Co&=?OJyaryFmNh z?H(V!=Yg>XuVk~5Unv2oG>myFL+Gpv)w}W?1n_K?xKKyyJ8HKp<(s73@0=F(jSmn! z-ttogxCN|n9nyf^zIa4!_};47HOAq{PTgZrt5SD4K2Cf=1*`H0ZaOR~tIG*B1{)q; zK(h6llQlE)*ukI zNRDm`JjPd&2I3!WmWhAB-;TIJQq7rY9}(5gpcvXw(5|#snF<;qQ0R}j7i-W+&24>i zyk}Tr5NudGEt65?j#5J)ErOGO$9j-dj$ubE-{=m@=rMN?HPz{e8}h3$W?Q(lHq6WQO}5M2`ir>cA*Jm7 z?es8X(u$R!NKE{XX@1Fjy6Ya}_~$_IIJKc{_P||-$sAo-9SO7b<_|mlv;=IF`(lpp zXR>VE#}HYYNt;+BL=U!*a`%ZXC&d7N8j3_begH`J`5i=uC_wN7)H3oFcy!a?@`1tHf||5DePiaM=9+!YSBt6@9WN{ zva4vpVf3mr1tZVgmbU(a;i}#=T4|672=i_g7+vf{nKN)Q1AkDddAM)gmBnhN+&ou- z*AK(5kS++^^Z~OR%V|*Y=;;kEu;VEBOqa6<792%t3e%Wo{7DT?5uQAZkV+aEp7q@aEHD0MF3+Nd^+J z=SxLXr?T6sWi2%c%4G*q)hn%#hCM2qHcB!mi2&KTph$T*U*m@|85Erbe{*(~zI~ta(DcZ;C;a zu?0K4l`rFdGC2W7>{}Lw1;+bm8Omsh7?vZlMwc7D-jLI>zwee+^E`7H|9h8tG0v7m z`E0FwbA`tmpcA)Ruhy~Yw1O?APt?c(42|QO7bbaFvTk4J43m*p=`Gc=Ukp#yhm6ZR ztFjr4WWq{{Dt(09Jg{E$!xI%4uB7{9^q|P;D|`jhGS2eXcoq@P6&BknB6PikbZOB9 z*r@6IZZmcth9b8IITdvr+a;o)$+X1Y=Qvp6EOt3cWD3=AbUd(%0#dM~Z6CYiq|0JV z{nmQ+&10afAbUX2u1KhFm@~w(&6;!NE|b`e7pjep+Srvph*jFuX{z|wNT1gQoJoG2 zTiQkJkIhVNZJh}+?9CAS)kd^@UVlGL-9cWv0__z`9ac&8&`2dUCF`ylNjpHg3++;J zoWJ<;tcW*n`O~qcU{ueW) zThMlvQBI(RUeIEr8X#$t>&SO|mhz3y zp~Uwt)qF|?oT)J`SCo}_>Z1#&;lCMIF{IeNaEu!o&v^ygNN9v_MHO(%lki?$Oq;3W z3d#lR%_NZ`mi+9M_lTDxijKZp^J;D~UqS0evGBD!?Fr<4U;=9bNYC^_^bV(lLCrw4 zD1A6lZ4qh_(GwV9srO=ANq#&HTGTqPa=hM2A)4v3d#8${r(Kg(n@h^v%A?|&rD*u| zdTFW|g8a~Vby04C|w#Z7lg*~rMBCgi@Lc%u8Ij08Ja zE;A8h8k!G{V(O6<-Y&Jh%`k4UaVOZWdE&cG(xojMZ!&IBupQ z-Mpo(U(HEkSj%0MP0;ufw*^1;03YeEN=o|?(_|1tcv4lAdtNqqr{325hW@E#JLY@D zJw|n!l`g5+VGimOonj2by=jLU8&Mu+tpwO}HXf&)yMK`T<$%3#HjmyfHe%Exmtpx@ z7kXDhVinahOKDwyJUAw!k%!)XKwB{q#t8;U2q6~7_vi)hA^c^ik*V%hRqB#91pup4 zGKH@6paf>^4m0GQGoGNal;+Y!h_3!u&z|LLX0lKPNy5w!uP9VtC)&k$kPQ_b#TJTj z^vq6-vd5{_@yhSWb4{qbj`Q~Pl=FT2rKO$sf+Ywt4JDw_eSeMW>U@$mw3k&>#4qmq zcV4*1MV`B=FU}51$ItJzL*B^_5^=je{`Uq`>S6GEy(rgPIfNoeZ3AflZwg5UI%+Y- z{k>I_W`tr3pwuGt($>S2q~O&8am7vL`v{d+ZNzZA9F}#z(ozJdSj{J)+1uh;bX*N2}VjT`wjVehp!NTu~+n zg_Yc=aHirBjVOhh?O^B5{DCfT*hs+(EYh_$%93hU68)^_l({lK-r0JGd{GiCH@`@1 zcethstwhR&dH+A>+l&L>Y^Z&!I}8s=Y%y48R4el~oymAS~u=kK0B-zQfEX+JjYR zymCy(To4RE-W30mp{GSR)dCv&G%3Ax^ngrxs?S8yR}dKB7et}#?5rQW*#_R;O@Yyt zqSsZW2aREYUQF)sfHVNlsV?1!;!&{=Ex|LUuc~sBEnD4@lPSAE`r?|J<|kC#M(9f5eR zi*@AJ)I7vy%7wpB&bHxz%omKt{PE6jTenui7uxTMLA>VBg6^v_+2ucvE>Fq-Pod8I z{g&|)V&brip@R$Dz#P#nIG);QqQ%G77}l|jSf-h~+p@@LxN+BN{L|>GE~s`~7E-8| zDJnK~9!Q`Ejvsza`TA6dBsM60N_9}5>$#RPs z3z0q_e(vZ1lQiz_X8I-uT!F=hZ^IpIvb85Iq`mf-lm&J6D0`ALA58=$BmQF)CjwNA z12Q~~XZ!-dw%f{exbK48d1U=Y>0DUoB!z3-EC)Ka{9|>x${}W+)V$r{v4$U7Zrx8s zb7WHk7kQ~oo~t+)ZPAjiOP^tmLYIvM5&aeA@MYbex@oumy;ynJw2%VBp2$6g%~#=blO#9BluxHfxfHO{9 z&*?fQA+A_$l@z7#<}#7e*-t&FPfS7D7JC0|4}5D5IG;#4ea#{eZ}5_i8tt{*ZVc`< zJfRCeuzV=hyc9)rqH?VSt^Sp)h~y31ZBc2j6t@$`?W$h~j)?)Sn;gF!$|`}V?{jo( z9NiQ+-U(IyvGEx`3{DKV&h)O^E?$K%9p>OPo^#T{1VX?z4xY#gyV zjcm8;=_wfafsrpMt<` z`;F&=oT&3?v@YDex1+j1)Q~Z>{v#Zidrtq{U-etNjb9hb+r7D#H5CSt>O!gIA3j*1$&B|S z*Fk(l27w^o9r0}(fL7L(K2{+mgrQ@~)z5CTi9jZwS(>~2IyS_``^QJmUa-~N55K1B>^6x)A!FHG9U zpTG;NY!dSP-UzU$zXvfixRN!a5$Y`iaG4AywuDyu=bTEa%Vl6UUlqtRjjMpCvi_<8 zjMZw$Ezy>t($0{^)9OD9VFloTH10Yong?6ZNQd=7tyi*XfGFdUdF~c;!JJ)N035Xzx2=-i7$!3P zxfJ;>1}>#VvitKe+pjm6+FCUu_AZz+@@m~oqc3(qBUDdy&`k73bjso<_$$>u z?@(){ms22%CQCft6FP?Y#r9N`W4~gw@U$PesQf-dTnYiKWHQv&L^%OPk%N4MVtu?} zt_@pGt&HRkxoZ05+-H6gnJI6SB_j9uP-DaRB~zJ_RUM!}sA&^QI(&?FK4P)&l)LgD zcbzUPLQA_sbpn{iBe0hfgOMQ%=rzmUdjjL#&w2FmKjo6M@?jMJkPs}RJ20l18$cr) z=QXO%@8<@*M@7^qG0v_Ong`v5Zmuh0BGb+TLF$PC-_>Hm@}#U+em)B7m8y=^sF&gu zLZ24yWt&vf1GUv@T?+6$-ttF4co(iFZv5NtD^3Qk9#AXI0_jeT!u}ayp0`<#Dp5uO zbpe9^;YGB$aYLrhB3sp%WcNt=UyF%0Q?g}O2B9C(WMWIDb>Ig6AF;+3tKE$4>8Gc zKpXhQwIjH$Ep(q(5SJYF0-?TBNj&}M2h`i85Zwf@;ribQDf7t<+>GxmVE{^N&A!J$ zPTwb&p2GCE%HQOkrleC4%siLHw`|3*VO3}1+egD*1P%vDR5~uii5NEhi(uoMM%-wU z=b7!Tj0TRLdpgux`b*0pgc`c$L!5NT&h4npyoy)3AFKN0Uo^0!Uk5;~P^~9l>=te< zjP`XKcv|vZ0i8P3gRwzvLHtx(7O%r+di1dJ#l}Wgl0!7MOi&v$HcnB3>T%@}Po|BK zcPO!g9igd|c)Inw9?F5LkB7;*3YIl8oYY*1hQ75R$n8DoH2j*=2DAHR))>7ofQdZJ zC;?Nsim89vjFZlaF{DWzdSYo<-b(3q9*UpYA!7U++7Y5>^J=iQ{MkBH*P1+j?SrZS z!nfAx^Lls&y=`zytt%PB*~LXy=A=orOPL#r11zZ!-0_r6u5r(J zGAx=Hsu+fvV_i44EswI+-~+En=SZ#ibF$*+SPORa#^p&oTgs;mPOkkBJ?RivN_{!y z$^j+k9~OE}rD7D&)8KQnKqOUEK7jU&%4q-WMv6o2ZN1MlEpEf6`2e<5GFgMh1@tx< zsNx2oFkjick6|rieRyC4S<{0;q=RN#*tpoa*4NQvm%h=*k;&imC{(`p*1`Ka8cH79 zVe+b(l4?-}BNsVX;P22xr}~lyk8#uxF3TeuvghOtaCVnnri=C62g!Ux+wkiT@1LFWJ>-0J zfys+jHoy2v-O+Imqx7G24CAIU?p7L*TfpsB1$UN8tJzC=wE1UFz{)bfu4m`TNkN;w z6m`7rOurFbf>2xYFcd>coAeCRBKD{$2z30a&sHqWxSdo6FE~|}Ex4nu9lPZaZ z3P&H_pDPc`he1Fbzna&NK+yalHlS2<(~OwUJ8&P2(U^U$2L5JsC#4oZ@07+IK4IDL z62|ofVp}QEb0nZ*R*ZTvI+@$^4d}Z62eQN3%A`8N{Gp%|q4~{MEsz^#bI*mHIfSxG)R9M~RBa&=1RnjY9&!XnD7do5zUEXJp3-k;U22-og6BaL^3AdUy(yxWfC^rZG4K> zsYv*^V)w2Pq3CtyxJCg*A&+G47%L$1B>`Q^0P|% z1IO2?H}r9*6!~JAptg0Zp1k12VTR=Di2NJ&_f=)@)p&7$o%`PsU*wiWeWQF__d+>> z*EW1qm4RTn?n~(6P_0!c@&#UtCPMFcc+PR1fA^ck^vzT<~N;DK>jFrSM=6}ZLf1VF&22-2#GAD%$#M6a|}-{r)6 z`7saWTjmQS3aqaN)ydHKw+wZKMgTSt}!6D%r%zO7;mBXuj4ANFXMVeLyc;4J~)$<9L z!Cp$27UV~tMtK)QDIDXufHB>XX5F82MbzFz=nVN^?@YS=9+-GxV&FTG3H;v;4)KVJ zJ;V<4BsgT*2gIqPy~iUE+z^aUdmV1?z8zGJL2X3jx!?=zgMz(=JR(MC@~KG&m)ne# zI@Mf58gu=l7|6uv+#(xrTax}gVl+Mc3hZhcfVDYp6yiIXV^V64oiSl!6iuq6ygNPT zEdP8KHDoo(hXq62ooVGH%=UUjM!UmD328&Pc)iXYHkN=^K$7Cdh=Snr;DmP--VIDZ z$a3BDBX8ArdVMl)#nsQ03U6+)1h=VasE{lxZAom7wb(V>F3( z4EjIJl4~Q)7k$X&v^L0fnP-%(U~A-L>ZE}2k@}W1$IL_F`{Ke; z9#g3zk*Qo@k?kt`RirtB+${`cx<(x`pFaR99c{)OV9Fd63T~YBT^&r4Gf~7>{|~rM zv>Q2Pxjc+h=F=6K9UO=8I|f~NRukD)t9I+!)xHfiZPQ-^l`_#>rM>qF&f@ZNDSku5 zFEicMX1y;krWYE7&2hDWsiO@!qc|B{VM==GFGT^+j|KxGC<}_KalJKOlh-@3-IECq zfV<*t&hyF(3Kj=7p^;c)YwAsQmYc19gCrE7kf@$pyep%wlv?| ziDqI{7OQ*`v$KnWaLO#!DCe@MF^c%dBleRu|}*tB;sJWfC+^n|T%kjbFc2 zvlA7>f#o2cQoTSoe&yuY#ilnfS1kZk2jVE<`E4}6ZUstd;tR$d?2*W3n z(H&FUu7gfx1I_hPBe!mPkngV7#pu_@@|`x{@O)O-LZ;eP}`^0Zyi+9Vw1lTQm` z$Bzlq-4jQ%+9t&OwN)!PZ}nK$tE(&6ucco(m)C}aE=|04T{4o48G2y5)!b=0F%KHp z0Mp=nM1G-s(BRFqv8@7e=djagg~MDaw#>w}h9LD;EG#VV`%7hW;aN#mU#~ULb(xW7 zMvl-R^6(owe~nLM)im+Y#ZnlY5^~5AV=8_esa#WbB}}!R794gY{KH%RRa)=cB!FDe zb;4UhHJXJ{Uh)ry%hjjMWXs5RQ+pxw&drqhqGGYk}7yDNrK>9AcTrQS&Evf~QLei9)!9h5w5fR>8c!V?oXq)h& zd2??bj@tD}v^kAl!ahD+wg9e(sf`ALpPy$-MiaGuZN;MU{pc zfF>dwRCFS*2yAkNT`jl9Z8q4zd%TPK{%3& z=`?Hb=XuU`>jG}fg@!AA4*H6%ZKL8s!L(1zE^}aTvsnM!iTy%s-j^k{e0#wP#>V5| zXg)#3N9PFR!CwMr`yup@LC&eli#5&6egeEU*j^_S)9g@GH&zYbBw) zjRab_LxV^BvbsWDyjHlmsG5)jfw6+L-M3Q1LyREqeBIhof0Sdxs4! zAs{t$)tAUgix%YMx#oK4~Hc!?@Z~*SAP>e9~TP;s^92^ zQSNc_FBXp)@~cECOlnV4s9>vzcRb2n)PCLPD97G3!Nf>hbGFB~k7_udNA=6}^A-Re z*(~r=TJj%_v7S|7sG;xXRxWDmO@BUh?83(8nZ^TP39Tp@-KccQUuj3Onz-O6}6*4lcfB}*Q zp1U(T6SSd@T&ZeK&~0LOE;EsHm`?O2Lj!qKPDTmwkE7k-vLZ<|t@x^&{hvezbf`@Oj;~uM%3qeaLjV$855l*+LhZWSO=CrP?E0BHxIT z=t-(|N6=3PfzOTR@9r~}UEfkFJ@f+`QtpYIKW{1^tu3_sMYTr3C<+?~ASS!I~k_ zA?$$Z#rP?0pSfYkG|r5qU^zh*Lli!gP>RIDkj){0m$~v#&M*=0ZnoczK z`|&-A3M@2&nC1+UznCD~=8e#-^4Y0sH z>jA6UTH$?)N~O_2N4z4oQc}I$ei$CW2H3-^KmJ`DIvC}9!u#iPPLL%*04yL0;FYWw za;g$yViGgE+60Vx=7Ia>Iz*JAR{-0u|#SdLPW*j zl|3}|z0RABKAOpS25YlV4p1)t-g`Eb2l-P>(b5?9M6&!Ek7Jxv6{>i`7sq3AaL^e3Gv2myfp1vWb z3d-Z`g>Ud9Zw1wrMOBg^W!YfK0|(x-)FPWgSt(i%IQh4NU$U{dQ_IgJ`H1|h)u&J# zSTF$c2D8FJG;XAy->N*IhSDg~AHnU2_Xm1;tR=in1e;?HjnnVH*EWDcrWD{j)$ZxH zG&y&l_ifuU#)zF2?luio3tkK-Y_?I>%-uehPTT{CJr<)+FPQe`fUOpN&|xD^TX~sa z7!{9xJxXgNIypq0Z$@2dW3-eSBvE$qTMn}Rn`;hu{9hw;C9a!r^!O+33TApA*<0LQsr#Y4{pG-xH_nW))r}MzPPjEXYk2l0y&IIz zAu>WGN1B6pWko`oV|2GkdRE70+)Fz2n-7?0HeeMRoeMk>EEP#&(6#)q0iP_F)I42I zfZxhsC%}R2nk)@hK*mCY2CPPSl{^Aa#4{#;ON-$Y?3WKh$dp2I>@&GAF#J zkZQN1`4jI$e|aDvuWji>=j}cixyv&kko`G3ilrCn|CUcq43(n>IJFjI4B0lTb#%c5 zO^k0=A{K5uIHN6UIc&cjTLc!Y2AQEPZlZKEQh!%)ZYnv8r~tX$WAVSLT+&><&f)lt z7so>2wJeXOy&x5fsC&&=Ntwp^GjUBP$|>-t*iAD@8*dO%AEA-=A42y= z3EhcvUKQD1N%nrNP5G$WgkLB9(>=60U1Kls;Al1_Wn?^x;&t)?xy)Q&Kn{Up{>5}F zUCS9}L&t}@5T86I4KP`N&2a1oDke)q(tD2u&hW%lc~TQO7Md|L0666AdUA*PBx-pGWr z*8ZQ3qKhD-+4I#7Xr{LmKWdbzX8c1Ns;`KCX3g@d?#%!6CwqR`!!LIiP7@Z{K3Zi$=VGO>3~A)DY#;5kFv^5gAB z%~#yzY1^pltUFmS%aPSxcne{Tfc8z>fw0F|pg^S7NG-$|7ZTK#-2DNc5M>z%?i8Q= zy7YBu2be|G2_iel71@3WDngTr-Ztfsg1c-R85H<4aMwKQ7|OM~;j_3MS<$Ov1NWyN zlpVOA*STv;u~{XfveH76hd8Kt9NGD8%Q)DM*4V2oAXl>3AzRr47Mx9vh?nQDqHEd+ zJm9bicWR|(pC!oTn1yS-ZrF#CwmWIWLIBHbm$$XJ0@kmes5_0*7j?$YE{MKB@(?OT z^hl1(g6vF}ZRB`PbdAMYEE|!M*reIQPSS`p7xqYcGH5>xp+PH4Q?>|J8-$9AuKBr4 zb*CUlDA&sGbF5GD*=dHIk?k`R_7+aSfJdUol>Tlx~@r2B`VFRr$oDH zN$<7lXkLHn*U5+Wodb!g`d7feWHbC}6t1rdy;iEO%Lr9)pAr>CzL334Py+Yu$n>wE zXdx)H$umy2p<5GIBbzh#Txhd9z7E8)cr}lNP-%&*= zSUgP{kEpkVMz5t2`!;JrrBn#NeW44dNv&G*k<)0U4tIB9SS@vzFvrp5P4!)$tq6jLY)T z*3`W+NgUeV+L^tgx@45Jb$To<45~p86qUV}w}C}w%DaqM3-cylYuHnO;?*d~;E34p zsLQ{DLp|Gu4G1Sj1p^)kd>IvM1Ul!hgJ1cnoE%x+Kb}m)*Rz5|^hjEhu0I`6W)YYGtFH?u#iZVN|{tX^T!`a0)$oh&~|LvX2&NS(yQR2 z%=)xsZ3bPFhlbn|w#E!3o%WEFK*tCJ6D2NUMY$RM3OJkwI@&_B$2gU)j0I^D5`-YC#=MFy?dv@64Q*2KPhDAg#C6OQ%-_Bv zgfeMZ1WlWkrN9YVBGB)#uUF#o&^%2Okx=D4)>M0~=t;HvjpKSylv^USsAKydnV-Kr z0KuT`^Fz{gnbcXbI1ZFb)5lOeP$o?j?v3xNi$1X;tS1$0W`KTKl62qQY}kp)=AuJh zj1g*gM@FRfu@#WyeQH?(P^m2tAg@&0;LRGx^zg&}M!mj}PS^{yTD%e`f!6+)i7){x zz7fPab&J}B4~>!@n;HmA?Dl6+5_s=1@(-`zvN+{#<*B}O*q|KS4z*Z=rq~bNmI%;; zZ2Ziw`$}z1R?hoTz0)WNt*fLQ4`rSL=flSKS-Jqu$1Io40W}a`$=p?JQm+MZFmA)A z#TCd*<4NJ`B|9RH`PdoZSr@EWGPWi_ewh88EvH7X`#11MWjEnty}!JwV*s$qMC&aiO;2dS7ClJ#0(A63u5u*?|Ky#(MZe?6 z4P~*~V&*Snw)467mG0v~^$enmNVBfN8^`r({C2$an2+=UQGN`W@B){UNNNz^W{Dct z`J*3>f+f7C=c~h5zhrwqGf?*Yjjqn}(%pPjo$Kbf)W=5Z-B*kCVcu~(2);Xfe0Nhz zxIRSg&M4DeoULV1*7YB7xF2UmRosggpGNtY_ zlx!t>>*0;Hjv+$Q>~e9s(H!4{rJ0p@+qC*5)U7~hQjH)tlDSrlpyImI?@C#V*4F#e zcO>9$$QqF0tcjte7HizDDBBj#ES8XCOHLkqZnky(!o6C|qnT-PS)=UNKsJt83wGuc zUu)}_$A+vxAnF<*w)umRqoV6_&b0B&GaodF@|UHSy;t|FX(K{{U_W)WB}h-@4HNbx z!`YdV>TP?QbsL2_bgSN1tyO<}+}03D=#{!Ly!#}>(TM|pYfCj76tOUqgSzq9bR&{My9OYJ!RzCu{YIxD3P7l`elxe3^a zStut+j|O4H->i7{?4bov=S;f4yEh-moh12JWSw{marLsTWZisNTMx=DzYnqt3L)RT0gJ8keca#=(Q z5R!*=;GB@a*iI?5_qf^yL$p;zj!%-xO=;IlGiTGIlH~t*KgCSw`8IDc>r(gK?*qjX zENbs#mWax;@f!m3Yt6FvSAW`I)V4gernl?T8k1c6hOx+!-b&31a_s#H3P<2ZIi69_ z;aXQM4XxJG#qrA$&5h*K9~CW4>xSmBF0FMv(O0nIm~{{1$PO)49h>&VZT9`sv-xqH zP9-2nu$Y8jkiuRT7+qRuF>WQt!)@hQ)#<}0j#p~VK*_r>t z?#s^ns6;K?t?a~6tiM{?TR5{>+B;jg`%qF+a`E#Da`6cA@pDsBa{ed(@8sg-`qJ_v$7{1MDuR4g=%H==*sH`;=*w|srzIVv(IWl89R0>oU)NSRD8KNy%8OHEz zQ-^66sx2wcuS@17z(C$yp0Txi(?J@zjktoFrqt^-PEUUS&aM+^pBDj4#45QB~K-Y;z*Ug zZXNom}=rT_RQ3o&$D2pX#h6oPvFiX9&LBFI_nOChVU#^vGj^#(1tJUlk zgPs7G#jqJ?sJ*=ZG$8>Y4Yhz0*m@o@+e}ZXaJI3wU%=^Ev3dSj&O*g z$D@GVC(e(S%&?V{fEQP0BeM4|0&sLViOKhp=fT=|QfWs+`kImFg=ko3P z9Br`g(0+-@-Rfmvp>8fe|Ik%Qf}QZG>ANrxdNQ6V5B z;{c!m@lkNNR{$S$V}wD3-_+X~> z586EjqwlinXT?Mfj$g9I_Il>U=`3>-qEG$`C-e(9{3P5woHD%4G{5j2jSfcjhZq$5V3@w=*5ge3(* z)(fhL0}6WiULA2TaqRQ3-*5-Uhs5LVpeMmiV)T>G-JcHhLzIU6M z_N?{qI!03qxvsuq^%9P8bSyX4%`s0O4KOz-c;+OG>ZO|$l?M*$>fRK}@awwHkm@9w zwB3N_)^X3qh8dfiikXU~%U)_~CHbvS$b=-)@goY0g7x@$UdMIabIHNnhB&0l?__+k;L4E{L9urnn-k<1x!`sZdEns&h4$>=$q)gbH>ndj-*twhDh~9a3 z+m!t=Ck;p5^x<&VekqQm?u}|9X(YiFG*@_ex4iV*>K@yt?c$CUXB8gI9OYMYRIpiS z<}W3#1wA$NCjIMl7Y3h&)=x}55L+VPnacf|m562U>lFm|V*m>a5r*6Q1>$Gj_muoF zZf%-4U8)15j~!>F(^+DAP#mvgOtXI&&Cdjf)8vou63QRGYzZ^f zx}v$^4&7TKy9cJaeYv~a1Gs}W)kYV5vCMiiJ*{7K!1bWkNhYSJ*Epz(PPe8WI-Il|-ec3WTN|R5N#Ya#-;9*d_dAVDrvJ5ZpzR#ada;-55 z{N93_ppu%CixuS8+DMZL^W87hQv6_Dn*Y!1QY-n?aRYWSMZAqJ10bp30^2YFYo&1G zxwoh|0^nXYM1Jbee)P{@V26&tlt9pvbvD7V^$sUH7gg$nwfh9vfEPbew(rZ<$%^xB z7V!34cf#*JBgaR^?zHx0+KRR?QEDfPRuy@=L+t3y2WmWtF}Ht9yqP&({f$6KN-{WE zuKeg`+yh9T*jV=4+P}Y#$2&v!hsZD8s?NV-`XPn(*0Gj+5LZ&r(JnOF4P2I`M zT<-p&D3ST@0^xe|6`f=B+fTW3vjXtnqrFFygxUGuL8gd4ySqWr>ox^QY!npI``b@f zN5a1;V-S$JIBTa>2p^*QdT{2cM4JOIxf>my$fmp+ZXmem0WR&LfR$+V>x*Fok-!GV z`Tc*&BO>q+$Zj^WMCc17c`cyTXYL>BJshGB$W>(<%v7WL7XsngjWXfbnp4W*%%i`z z=n-m>QP7vElk4OYU9kPV3mHMr4a$e?4qd}EiNT?al_R?im8h#au;}rb5Z$DA`w-KX z6>2|5F2wX1^yrn?K4Pt{ns2(cIK3IXY|syec@EU4UZzc%Ge6?0D4aS7py!+W$^9Fh zCV#zu1^-zZ66+@9`NCQ^#zcb;^XFG}qR1o|v%ybWRce^loFOOoUz~4|NTn7R3VjpS zpLmdC(&a7!&fz)+&Sx0)C`3Yj9W?4OM_HUDzto0_z+R_bW!^Bm>Lgr%85kK>GdRO= z_)={aXep+9QOrW{IYS>Jbu#_utp9dW@5GVv3-_ zt^XL{1rGDf<)>3K-CE*Dh#3JORhim~xqo&n~3%7Vq=SKe*%d3hWdsE)l3x zG_Uy?cv3{~URu5^ah$E)nQF2Y&`4N-6bMZ*yc5pf-+kro7!~iLyV+TR&I-!Dg@_SG zE;QbsUjLo`9P~(adYS1Yw|u*r;CwsHXToJaR{mX_Mt(O+u2kgfNC_^M;PQgEn4 zyMs|KD_vjQsvtN}6@;p8V3Ajtp~muh8@uxk2(j#B5xg+l$P;nvaCmuVuu4^F%XHlA zI&@()SrTrIROCDV@dlBL__{~Po$KVk(YESpNSn3<2^?!;MUxjSLramJf7d-cz*7&<?ISkKM|xg?s-G04SDMD3pOlboVoL9Q0|s>(f) zM#jkTuwO`9t825b{}$G%tku7Xud&Emje0DBgD5*hsK{HwBi6|h-B0o&XL)i}r&Uc^ zgdHWYBvlbda*0hhFa!_m>a%ZXLfkUc@@leiB$?ve^hU9EgLD*;IG7TevE^&yVcbnu ztfc0#M-b*Y9Obe0Zl%x+rJ^XRxySay*7Y{liHQX-dE?M3uth1it2LH*tmywu!v@Gr z%E5>UQNYZ%2us5t9mi7vC@CLqXlT>QtPru_i!rgnT#pWbtB8!wC2dh$d)I^8jzv%? z>>dJHMKak!__!Bt3e#OTgR@DN0hCN52s}`GcxiRscwC`vA$M})nNaMbV%9~!n?PLV zR#WxB3n`LDSV88hP|0zq4(}lIyFs*3!M(|Ky%8FDb5ub-k)V|k}bhmLX^CM#OCcp z8HHZbuO7`xJe8rFC^8wmJ;+rgwi*OS`b1cOZbBDcAd|*dSbrFf zZ{KHQ!i@9wlSie3jg@ngvWWpI75DQAMc&EWAep!#rVCtkBs15Pm|-~#cS4jX0u7?O zFaX>njU#n?@nJsbosxS@F$6f39mOT%_LU=rHz+(+7WiE`gBMLi2hE34IVT#WC_~=7 zO|^O9YeN{3274Gf-%Nx!A)Xr*LjeNYeCY`+C!V3J+fVZsXrR;#27@%p0Y*u2?kp4? zZu?*A4T~BKf^Tq~h~c@g#=kL3#6k$cDNRuSd}LvXIAW2P&AB4xse&php@KA}ze6$0 zqo28Y0f~=dQj8P^M~mUz;24}_KN{e&%c}k&Nde9jVzjOAf(QvIG5?`9wZO?j+mh(% z3uysdp(^5+2caclm*v|ENnKbKCM81*%Acl&Mq73w@m_jKsUMThxyq!cb+=iP!b@nB5fHhv+}m@T zsUFj$Lc_xeG=!5mVm9SYQFBUu3pe8^x<*Gan#Uqh@L7Jd%ehetAr&QR)tXYMCN3PiwP}$CINS!ty{Qq$-IFhmnV8f#ER0>AL3U zJ#BMK`K=0TCxL&V0|Z?W(=`#tB^7}H(KrO+ zf}4X1L!*7P5iFxhiiuCKp$1x(()5G6%UaD}o?6DkZC%*e+H1dw(kCY73Wkm_@f^>;}0Q*zIq)@bAB)e z8gByF*1!-xVMH6DHVCKFs>RRjF$9Wb%<6ivEJxbj!`CMxj%Odwx8;kOb{l&)$4tLax4XvX2G41u_XhU%^ZGR!2mJGG;;HT@{8Q&x zQ#SpZSvRMf&MF~+E6*RXS0m9F>{qK!Z1mYFDY~^wT0hPwmw#LLXXYiTDtt>>m;|a) z*Dw1yoExYriSLWQJGdMz6kK$+uD<6L+8hG5p9*H@CYCPMAHA;LM0)d48ucvD2@eI{ zrv^?Rex3zdqmi#^-|g=&?7!ucmYiM>TwTM3o!#QwP_Dm8t)Cwka)>%uy4o8rq>c>X zLyyxQVo&^rW8Ijv@vv{N16R>I!Nu`cF4$o6Y=miobhNn2bJUpV@zB$h(iey)+bqU| z^fYzZ90+A~bXCFMQNp>)XU+gM8N^(rOLpn0{FIYA{PjiCXDu~KF*Zt-ixs>meDP{r z#oBa9jJoP8%D%55M0_dG_1S|JDKBx0a%$x|6Jgx>8hp4hp27|yO3Kw)dMY{W?Ah8g z8`W|AnY^4eG5j?tO1avq_IP-zB#N4Rnc9^p%#)7%`E%9u_e7zFCBU}T#gbr6yL1nf zl`y613{A%G;JOz3YPOZINpC$S`wBEh-w9*>`kEs39H#i1n8k&P?G^Xt6~E2?12f~= z$BLU~c7m&uwPw=V{BPA~mN<{7PcZh4coY2tO@=i7v6JQXAR2UF@~+t+`)UpPNTWd$ zX>WOald*rC@``0=j}9X`D@^eAze_lnSgM4!XMLo4FVw79-S{3euN|`U#kP)b@LB@7 zCCVhnT^Yal!3PBCQ{msc+q^TBVC|U+9~*Yem)SRRE+@}ynwQ@@OoN}u%E8n;Mh{_X zg}iN^YU!#>Z9fhJB*$!V}`{j$T5U+(U5$tJ5_UDhDD{c*4(6ySBt`_%ERf3TTlH70Pum~^71 ze9`CM_ONBWK<27eZ5shaDLT?rqjzS7OHb@&%Hm+}dAUukyrTRR<8n+R^Mid{ENZW+ zltk)q!fa`&mOggwt`RL8e)Oi81uJ!8o_ck|V}oV>AZsRbD7$zpzv>V1ShWp173J-p z#~W`BsGc15EKfdZr(XOQ9hF5n9_~}1bdSf`W>Y~tzS!?Kq=hAkpqdUvlKmBSJw=8E zxp5i>hRRYLyAU~OUbi%}YGW33Ud?`Yd%)`E-x)nnmi)FzY&;A#6;;6g%0Wga%b5GN zZUaFgOs^s7Tw-4fZ_Hb)(vN%D{%#*H5qN5%SoyMZO#$VHZ`Ee<#zw3JKc+ySdQejn zCoL^t+o`}ybD+z6S<4;SN5|4qi8ytPwt}6V)ypwfLSu6)S6JTHa}Stg11J1VOVU!0 zh0ajMa|j0}Zs<0YDmOlRF&KV^U^JDMoiR>oA?+IsXr+VkK&w2DC)h)Fe1Vu)TXI!A z@7Qbcw}SNY2?iDdiGQ;+To6;R@Ygc05J}T`~t(FX9YHNpVX;f()Q03Z$TVernJ)Ymu{_bxFcN<)pb# z0bbaAwBRw|SZkb@@uCOqy={>2Fordv7MkyjuxKZd8jezZgZt#TRp=ezNbrlSclgEh zl^W>@n#SLk8DUNo@1qMmWENF!(mV;IV{x2Sd+2~+aaE6lJzDY#x|e_Dz{jd#xE?Ck zr4xvn$TY}3N`$SBRYAczoKRe}$YZvV(?%(H>gkSxA#cvmHCYz^hbfkHM?oP~8yl!4 zoZCcza6}}Gh=Rr0nyM*bnm+C4eH-?5sN02_At`Y zh+q%7O!QUkij+wxIPd29pcF#qlOyx&+w9^+$_R-n86aN_v9($;5m7R|;s!b_9u*@c zZd}ELue+7bWEvVA7d2H1k#w4*0x}Q?ldS-Qt(g>37C!nh;USiP#%#TEG9Bag09`@g z6m&VsRRJgZcRO>|5xH{U3OfRO$(?+fFM_I}7m+hDQ#)5BK#FN zvPq~GV9HhTn>a&L^`oMS-o}fw%T$je-z7zu#n{CByNFjjLm1)9E+&BvS*J4d*OEPW|VHSj(I?M7RBfkrDMCl_Wg>YPI~Z0 zM*ix!DoOMW=o^>jkc8ZtZ|A79*Ax^%l^}Ngs{Ud7p|Ij)I>d(E0>77Gtg+NzU8)#*~>Ql#|^is)T$^(B@Qm02UdkoKH61OOEIx>)amN?5Vp@1!Ubq>S4fZ)<6h^-MMRitaeL3EQTa z9wtrZWAz)?jCZ`G*0{=0c4Lxg8O67z_NIgn<0b?)Tuxb+rf*;4pY{H{V0&ZS_|tKgzh@$o>;a?9&w}^h8WU%Fe^iuoaa!WE zwoMo$;()EK02r(BCCx`PO6gFJhCnEG-WnOp9zPeM7Fk?LN<2cEUBV=3sn)6wyKjYb zpQ$h>Uf*E#JR~G}Bw-i5Jy0#77Q3P4I;qm>qzEF)R06B6lRlvT0j*T<>Gzaec<|^C z&HU`{c?B^F&Gt*za?m37mEF~%T!iA1I~e^Xvybt~`wB=qXvPjd~Is{IzZ z0dp3tJ@k>)nt-CjJQ0I`Kd9uaJL4U9HD)7NtG*Uq-OL7slU&RjSTL)YZK@PXabQO* zw+Bnv$G6GTSh?Ky+-IsDJK6p=IMf?aOW-ZT5QfTUIfzSwz_=zSZ9rWfiE z_4f6uk0z9RLP^+y!$=~@C&9idFDaxr72Gmh*c67#z`MZ{m#x^9c2TvH~u-scW9|&j`$S+!BoC8eeh`5XGB$W z$#towqMfIKGtxW{j-UlV%4)9&=lGrJ7+xT#&1IBz=jnBXb!noEcfH~A3P zTA6~AQ|(4-;>$azWx&FCL^R6wS>p6pi_mis=Nlt`u(Y=O+sgvd50eNP)>;mF%IGws zPs)Q|{AOq+9n&S6xjLmW4L$Z9E%9)C%FtB7LIx^tt_XYT~_v(`?E zgE)Jt-X1*q7gmd7BjXkPr;4Zx44Jekail;eI)C(a=F@n6*-xBOy=^_Vl?;X72u?lN~Mcb#ZL#COLms{u2eN`g=7 z^`m>#dq3n0U`|IiLnZ6P(far)u3Zz=zEr=}$UZ zYs+MO+{?8nVe1qB%=e9;a>vGGuYr(>+CL@P}JHVBFB!wro zW)1*{;GaHaleLx}z7a$wHVx)-S5L3=h=ZLLYs}wAF?v&s_<}zgLMTq-eB!QYEdZ~f zp-cv&ovb>}Q8|*Ms>2C1aHs+u?)D19Wf>D{8QN7Rz0_ph{xH_0{qV&xb0+@FB&xEA zGxTGcWpGDZ*e`NA#tu+wQYTb7cCJ{(15+v9>l1o_+g{~@9fXPyY_^({Iq|Dz0k!CI z$5M?rsGsr~NybGkv1XN%R9|2Or|qQv=m|cs1WDJIT%!QIn9z}tK19Lis7NteSjiTG zw8o>6l|vNxITpcfc6mR$jz`?m3&~xB|Ga&9s@mNR0BFy8^9& zaB+&Z8z{1@X9>x`QTJE+TR%?gbLQrFbLl6#FswqQ#vJgWk?3mvGn4RSe$%0gCT~Lz zEre8aWTMmre#ve_wMY6Qo?Z$ZCZ)*p!sw0p0e8G6ScwJ@lHtA0+22G##KaRoeB}~g ztd(Hz)_2eGwElSe8&5lyyqJ*u;iI})gJ<{m^=jIStR33&!tm!1=CoCT}`G)?XkDkyVwzuB}_hlCvJhkW6Tk4|(OStfqun|k>5r%bz+ie63TMQ7WSX0x$7(gByA!oV2UtpF6*O!17BeYQj zMh3f#27r$C<;{*k3B-p+oQba+ErJk>kh0;EVBk!kLaxE-!-k1Zn zKU#10jF-N{Tqwg-)H8o&rchqk$w27{f~`!?idUYI9o(>-yV6l*Vm@vo7qdJd z(Glz`OJO%{sTR=h@q7vWGQ1b@@s83&SUgsGm<~omLDp@lPouabbbtqow*d^-7G%X&O?$+`eHpgV0=B+S8?+9xOwgsv%s=`0)o0a9&Ss4yA%6|cIn#qR% literal 0 HcmV?d00001 diff --git a/src/assets/dependency/iperf3/rpm/iperf3-3.6-6.ky10.aarch64.rpm b/src/assets/dependency/iperf3/rpm/iperf3-3.6-6.ky10.aarch64.rpm new file mode 100644 index 0000000000000000000000000000000000000000..b5d5a4a558a3a6685b6678a3f171ab2589710be2 GIT binary patch literal 76348 zcmeFWcQl>N*FSvp7LjN{l!Itd4+n?nooG>_m*bS^MDM+m=q+jxEkub<1kp>>AfgjN zh%TbUd%5F&zTfZjJnOgCZ@uqY??3mPHP@cco;@>r_LO~H`)t4d@dXP6Br+>UloMKj zTYyK1TZqTT6T)}J{l7~XAe{f+;CcBv#x;)s1iF<3lqG zkcoi;?w>LUXp3?C8k=1SfIwL50L2E1xz~6Tpg?~Z1waPcV{QGxm@@zc`p4PbH?3Yub~!khD9i~ch>+mc z-yDs0d{9l&p~Epne**(+|3xguyA9Wg%lD7;i+)`)=>-op2|sk|%Pd+lwtu1g=68(S zi{m~G@$ZITu$Gja66SbLS-of720DErZu3u-Cu;lNJm7kjSNh!SR4U0*%ZWm;8uGG> zYNWO5M<~VehlWjBA&&!0_oP8Z3X`S%go|f1&rwFA^%w7JBd zh9J6Pm}{@muv&YEUNVP0JcVgWjsDpe$d=Zr?n!b9o1OeEapG7g*4Kp<8B|TrF^;ot zVeZCQNjdc=&njMGEyR~H4@*psQN6a(mv#9*RIvB558g0J9ZjoP=WtW7VMjl$L-s)* z_Re8j@$vH^1qIMB2pTOSf)IuygoFh6MflK2xCH`<78JJNgY!cyU{DZ}PXs9<022@p zu;7CVh(Lspf@mQD2vP_wA|N1$Mxs#yP?(SaiVrP}=0gftK;a_7!Z1O!02<0?Ap}JM zD@IlDf4@fM_whmH0?*?^Ioo%kdQl&z&u~-QFAmkLwc)r6Zwb`yG+yWx7;UJfnB2;q zT>Aa9fj%_9Fa&pju)+P;rjHGcUwS9?*n8Ot!)ZNH4uRrQF;?rOp1VZhc1XVsNEJP)>|^&%G}Z4DH&3Y;XKY@)s!|*6-<^# z%SXYg^||;>e8>uIPvk`EVisc^*-No6g!H$^)t>8!Vka10hHnmAYG%2~f9|-`oP9K! ze-_tusLI<{>qFK(R4E%juDc%d9^DMZL8xrpu5d(pUfcOPS639TDgYXcbS29Q- z;{*9B{T℘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 GWkh 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%0Dm+V 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 %v", err) + c.JSON(400, map[string]any{ + "code": constResult.CODE_ERROR, + "msg": "decrypted data could not be parsed", + }) + c.Abort() // 停止执行后续的处理函数 + return + } + + // 分配回请求体 + if method == "GET" { + var urlParams map[string]any + json.Unmarshal([]byte(dataBodyStr), &urlParams) + rawQuery := []string{} + for k, v := range urlParams { + rawQuery = append(rawQuery, fmt.Sprintf("%s=%v", k, v)) + } + c.Request.URL.RawQuery = strings.Join(rawQuery, "&") + } else if contentType == gin.MIMEJSON { + c.Request.Body = io.NopCloser(bytes.NewBuffer([]byte(dataBodyStr))) + } + } + + // 响应加密时替换原有的响应体 + var rbw *replaceBodyWriter + if responseEncrypt { + rbw = &replaceBodyWriter{ + body: &bytes.Buffer{}, + ResponseWriter: c.Writer, + } + c.Writer = rbw + } + + // 调用下一个处理程序 + c.Next() + + // 响应加密时对响应data数据进行加密 + if responseEncrypt { + // 满足成功并带数据的响应进行加密 + if c.Writer.Status() == 200 { + var resBody map[string]any + json.Unmarshal(rbw.body.Bytes(), &resBody) + codeV, codeOk := resBody["code"] + dataV, dataOk := resBody["data"] + if codeOk && dataOk { + if parse.Number(codeV) == constResult.CODE_SUCCESS { + byteBodyData, _ := json.Marshal(dataV) + // 加密-原数据头加入标记16位长度iv终止符 + apiKey := config.Get("aes.apiKey").(string) + contentEn, err := crypto.AESEncryptBase64("=:)"+string(byteBodyData), apiKey) + if err != nil { + logger.Errorf("CryptoApi encrypt err => %v", err) + rbw.ReplaceWrite([]byte(fmt.Sprintf(`{"code":"%d","msg":"encrypt err"}`, constResult.CODE_ERROR))) + } else { + // 响应加密 + byteBody, _ := json.Marshal(map[string]any{ + "code": constResult.CODE_ENCRYPT, + "msg": constResult.MSG_ENCRYPT, + "data": contentEn, + }) + rbw.ReplaceWrite(byteBody) + } + } + } else { + rbw.ReplaceWrite(nil) + } + } else { + rbw.ReplaceWrite(nil) + } + } + // + } +} + +// replaceBodyWriter 替换默认的响应体 +type replaceBodyWriter struct { + gin.ResponseWriter + body *bytes.Buffer +} + +// Write 写入响应体 +func (r replaceBodyWriter) Write(b []byte) (int, error) { + return r.body.Write(b) +} + +// ReplaceWrite 替换响应体 +func (r *replaceBodyWriter) ReplaceWrite(b []byte) (int, error) { + if b == nil { + return r.ResponseWriter.Write(r.body.Bytes()) + } + r.body = &bytes.Buffer{} + r.body.Write(b) + return r.ResponseWriter.Write(r.body.Bytes()) +} diff --git a/src/framework/middleware/report.go b/src/framework/middleware/report.go index 680e7bad..3a6522ba 100644 --- a/src/framework/middleware/report.go +++ b/src/framework/middleware/report.go @@ -1,6 +1,7 @@ package middleware import ( + "runtime" "time" "be.ems/src/framework/logger" @@ -18,6 +19,10 @@ func Report() gin.HandlerFunc { // 计算请求处理时间,并打印日志 duration := time.Since(start) - logger.Infof("%s %s report end=> %v", c.Request.Method, c.Request.RequestURI, duration) + // logger.Infof("%s %s report end=> %v", c.Request.Method, c.Request.RequestURI, duration) + // 获取当前活跃的goroutine数量 + num := runtime.NumGoroutine() + // logger.Infof("当前活跃的goroutine数量 %d\n", num) + logger.Infof("\n访问接口 %s %s\n总耗时 %v\n当前活跃的goroutine数量 %d\n", c.Request.Method, c.Request.RequestURI, duration, num) } } diff --git a/src/framework/redis/conn.go b/src/framework/redis/conn.go new file mode 100644 index 00000000..e31ec90a --- /dev/null +++ b/src/framework/redis/conn.go @@ -0,0 +1,79 @@ +package redis + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/redis/go-redis/v9" +) + +// ConnRedis 连接redis对象 +type ConnRedis struct { + Addr string `json:"addr"` // 地址 + Port int64 `json:"port"` // 端口 + User string `json:"user"` // 用户名 + Password string `json:"password"` // 认证密码 + Database int `json:"database"` // 数据库名称 + + DialTimeOut time.Duration `json:"dialTimeOut"` // 连接超时断开 + + Client *redis.Client `json:"client"` +} + +// NewClient 创建Redis客户端 +func (c *ConnRedis) NewClient() (*ConnRedis, error) { + // IPV6地址协议 + if strings.Contains(c.Addr, ":") { + c.Addr = fmt.Sprintf("[%s]", c.Addr) + } + addr := fmt.Sprintf("%s:%d", c.Addr, c.Port) + + // 默认等待5s + if c.DialTimeOut == 0 { + c.DialTimeOut = 5 * time.Second + } + + // 连接 + rdb := redis.NewClient(&redis.Options{ + Addr: addr, + // Username: c.User, + Password: c.Password, + DB: c.Database, + DialTimeout: c.DialTimeOut, + }) + + // 测试数据库连接 + if _, err := rdb.Ping(context.Background()).Result(); err != nil { + return nil, err + } + + c.Client = rdb + return c, nil +} + +// Close 关闭当前Redis客户端 +func (c *ConnRedis) Close() { + if c.Client != nil { + c.Client.Close() + } +} + +// RunCMD 执行单次命令 "GET key" +func (c *ConnRedis) RunCMD(cmd string) (any, error) { + if c.Client == nil { + return "", fmt.Errorf("redis client not connected") + } + // 写入命令 + cmdArr := strings.Fields(cmd) + if len(cmdArr) == 0 { + return "", fmt.Errorf("redis command is empty") + } + conn := *c.Client + args := make([]any, 0) + for _, v := range cmdArr { + args = append(args, v) + } + return conn.Do(context.Background(), args...).Result() +} diff --git a/src/framework/redis/redis.go b/src/framework/redis/redis.go index 32576f36..eb361217 100644 --- a/src/framework/redis/redis.go +++ b/src/framework/redis/redis.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "sync" "time" "be.ems/src/framework/config" @@ -30,6 +31,15 @@ if tonumber(current) == 1 then end return tonumber(current);`) +// 连接Redis实例 +func ConnectPush(source string, rdb *redis.Client) { + if rdb == nil { + delete(rdbMap, source) + return + } + rdbMap[source] = rdb +} + // 连接Redis实例 func Connect() { ctx := context.Background() @@ -170,31 +180,22 @@ func GetExpire(source string, key string) (float64, error) { } // 获得缓存数据的key列表 -func GetKeys(source string, pattern string) ([]string, error) { +func GetKeys(source string, match string) ([]string, error) { // 数据源 rdb := DefaultRDB() if source != "" { rdb = RDB(source) } - // 初始化变量 - var keys []string - var cursor uint64 = 0 + keys := make([]string, 0) ctx := context.Background() - // 循环遍历获取匹配的键 - for { - // 使用 SCAN 命令获取匹配的键 - batchKeys, nextCursor, err := rdb.Scan(ctx, cursor, pattern, 100).Result() - if err != nil { - logger.Errorf("Failed to scan keys: %v", err) - return keys, err - } - cursor = nextCursor - keys = append(keys, batchKeys...) - // 当 cursor 为 0,表示遍历完成 - if cursor == 0 { - break - } + iter := rdb.Scan(ctx, 0, match, 1000).Iterator() + if err := iter.Err(); err != nil { + logger.Errorf("Failed to scan keys: %v", err) + return keys, err + } + for iter.Next(ctx) { + keys = append(keys, iter.Val()) } return keys, nil } @@ -252,6 +253,84 @@ func GetHash(source, key string) (map[string]string, error) { return value, nil } +// 批量获得缓存数据 [key]result +func GetHashBatch(source string, keys []string) (map[string]map[string]string, error) { + result := make(map[string]map[string]string, 0) + if len(keys) == 0 { + return result, fmt.Errorf("not keys") + } + + // 数据源 + rdb := DefaultRDB() + if source != "" { + rdb = RDB(source) + } + + // 创建一个有限的并发控制信号通道 + sem := make(chan struct{}, 10) + var wg sync.WaitGroup + var mt sync.Mutex + batchSize := 1000 + total := len(keys) + if total < batchSize { + batchSize = total + } + + for i := 0; i < total; i += batchSize { + wg.Add(1) + go func(start int) { + ctx := context.Background() + // 并发控制,限制同时执行的 Goroutine 数量 + sem <- struct{}{} + defer func() { + <-sem + ctx.Done() + wg.Done() + }() + + pipe := rdb.Pipeline() + for _, key := range keys[start : start+batchSize] { + pipe.HGetAll(ctx, key) + } + + cmds, err := pipe.Exec(ctx) + if err != nil { + logger.Errorf("Failed to get hash batch exec err: %v", err) + return + } + + // 将结果添加到 result map 并发访问 + mt.Lock() + defer mt.Unlock() + + // 处理命令结果 + for _, cmd := range cmds { + if cmd.Err() != nil { + logger.Errorf("Failed to get hash batch cmds err: %v", cmd.Err()) + continue + } + // 将结果转换为 *redis.StringStringMapCmd 类型 + rcmd, ok := cmd.(*redis.MapStringStringCmd) + if !ok { + logger.Errorf("Failed to get hash batch type err: %v", cmd.Err()) + continue + } + + key := "-" + args := rcmd.Args() + if len(args) > 0 { + key = fmt.Sprint(args[1]) + } + + result[key] = rcmd.Val() + } + }(i) + } + + wg.Wait() + return result, nil +} + // 判断是否存在 func Has(source string, keys ...string) (bool, error) { // 数据源 diff --git a/src/framework/socket/tcp_client.go b/src/framework/socket/tcp_client.go new file mode 100644 index 00000000..fb8899b5 --- /dev/null +++ b/src/framework/socket/tcp_client.go @@ -0,0 +1,96 @@ +package socket + +import ( + "bytes" + "fmt" + "net" + "strings" + "time" +) + +// ConnTCP 连接TCP客户端 +type ConnTCP struct { + Addr string `json:"addr"` // 主机地址 + Port int64 `json:"port"` // 端口 + + DialTimeOut time.Duration `json:"dialTimeOut"` // 连接超时断开 + + Client *net.Conn `json:"client"` + LastResult string `json:"lastResult"` // 记最后一次发送消息的结果 +} + +// New 创建TCP客户端 +func (c *ConnTCP) New() (*ConnTCP, error) { + // IPV6地址协议 + proto := "tcp" + if strings.Contains(c.Addr, ":") { + proto = "tcp6" + c.Addr = fmt.Sprintf("[%s]", c.Addr) + } + address := fmt.Sprintf("%s:%d", c.Addr, c.Port) + + // 默认等待5s + if c.DialTimeOut == 0 { + c.DialTimeOut = 5 * time.Second + } + + // 连接到服务端 + client, err := net.DialTimeout(proto, address, c.DialTimeOut) + if err != nil { + return nil, err + } + + c.Client = &client + return c, nil +} + +// Close 关闭当前TCP客户端 +func (c *ConnTCP) Close() { + if c.Client != nil { + (*c.Client).Close() + } +} + +// Send 发送消息 +func (c *ConnTCP) Send(msg []byte, timer time.Duration) (string, error) { + if c.Client == nil { + return "", fmt.Errorf("tcp client not connected") + } + conn := *c.Client + + // 写入信息 + if len(msg) > 0 { + if _, err := conn.Write(msg); err != nil { + return "", err + } + } + + var buf bytes.Buffer + defer buf.Reset() + + tmp := make([]byte, 1024) + for { + select { + case <-time.After(timer): + c.LastResult = buf.String() + return c.LastResult, fmt.Errorf("timeout") + default: + // 读取命令消息 + n, err := conn.Read(tmp) + if n == 0 || err != nil { + tmp = nil + break + } + + tmpStr := string(tmp[:n]) + buf.WriteString(tmpStr) + + // 是否有终止符 + if strings.HasSuffix(tmpStr, ">") || strings.HasSuffix(tmpStr, "> ") || strings.HasSuffix(tmpStr, "# ") { + tmp = nil + c.LastResult = buf.String() + return c.LastResult, nil + } + } + } +} diff --git a/src/framework/socket/tcp_server.go b/src/framework/socket/tcp_server.go new file mode 100644 index 00000000..0957438f --- /dev/null +++ b/src/framework/socket/tcp_server.go @@ -0,0 +1,77 @@ +package socket + +import ( + "fmt" + "net" + "strings" + + "be.ems/src/framework/logger" +) + +// SocketTCP TCP服务端 +type SocketTCP struct { + Addr string `json:"addr"` // 主机地址 + Port int64 `json:"port"` // 端口 + Listener *net.TCPListener `json:"listener"` + StopChan chan struct{} `json:"stop"` // 停止信号 +} + +// New 创建TCP服务端 +func (s *SocketTCP) New() (*SocketTCP, error) { + // IPV6地址协议 + proto := "tcp" + if strings.Contains(s.Addr, ":") { + proto = "tcp6" + s.Addr = fmt.Sprintf("[%s]", s.Addr) + } + address := fmt.Sprintf("%s:%d", s.Addr, s.Port) + + // 解析 TCP 地址 + tcpAddr, err := net.ResolveTCPAddr(proto, address) + if err != nil { + return nil, err + } + + // 监听 TCP 地址 + listener, err := net.ListenTCP(proto, tcpAddr) + if err != nil { + return nil, err + } + + s.Listener = listener + s.StopChan = make(chan struct{}, 1) + return s, nil +} + +// Close 关闭当前TCP服务端 +func (s *SocketTCP) Close() { + if s.Listener != nil { + s.StopChan <- struct{}{} + (*s.Listener).Close() + } +} + +// Resolve 处理消息 +func (s *SocketTCP) Resolve(callback func(conn *net.Conn, err error)) { + if s.Listener == nil { + callback(nil, fmt.Errorf("tcp service not created")) + return + } + listener := *s.Listener + + for { + select { + case <-s.StopChan: + callback(nil, fmt.Errorf("udp service stop")) + return + default: + conn, err := listener.Accept() + if err != nil { + logger.Errorf("Error accepting connection: %v ", err) + continue + } + defer conn.Close() + callback(&conn, nil) + } + } +} diff --git a/src/framework/socket/udp_client.go b/src/framework/socket/udp_client.go new file mode 100644 index 00000000..ecaa4864 --- /dev/null +++ b/src/framework/socket/udp_client.go @@ -0,0 +1,96 @@ +package socket + +import ( + "bytes" + "fmt" + "net" + "strings" + "time" +) + +// ConnUDP 连接UDP客户端 +type ConnUDP struct { + Addr string `json:"addr"` // 主机地址 + Port int64 `json:"port"` // 端口 + + DialTimeOut time.Duration `json:"dialTimeOut"` // 连接超时断开 + + Client *net.Conn `json:"client"` + LastResult string `json:"lastResult"` // 记最后一次发送消息的结果 +} + +// New 创建UDP客户端 +func (c *ConnUDP) New() (*ConnUDP, error) { + // IPV6地址协议 + proto := "udp" + if strings.Contains(c.Addr, ":") { + proto = "udp6" + c.Addr = fmt.Sprintf("[%s]", c.Addr) + } + address := fmt.Sprintf("%s:%d", c.Addr, c.Port) + + // 默认等待5s + if c.DialTimeOut == 0 { + c.DialTimeOut = 5 * time.Second + } + + // 连接到服务端 + client, err := net.DialTimeout(proto, address, c.DialTimeOut) + if err != nil { + return nil, err + } + + c.Client = &client + return c, nil +} + +// Close 关闭当前UDP客户端 +func (c *ConnUDP) Close() { + if c.Client != nil { + (*c.Client).Close() + } +} + +// Send 发送消息 +func (c *ConnUDP) Send(msg []byte, ms int) (string, error) { + if c.Client == nil { + return "", fmt.Errorf("udp client not connected") + } + conn := *c.Client + + // 写入信息 + if len(msg) > 0 { + if _, err := conn.Write(msg); err != nil { + return "", err + } + } + + var buf bytes.Buffer + defer buf.Reset() + + tmp := make([]byte, 1024) + for { + select { + case <-time.After(time.Duration(time.Duration(ms).Milliseconds())): + c.LastResult = buf.String() + return c.LastResult, fmt.Errorf("timeout") + default: + // 读取命令消息 + n, err := conn.Read(tmp) + if n == 0 || err != nil { + tmp = nil + break + } + + tmpStr := string(tmp[:n]) + buf.WriteString(tmpStr) + + // 是否有终止符 + if strings.HasSuffix(tmpStr, ">") || strings.HasSuffix(tmpStr, "> ") || strings.HasSuffix(tmpStr, "# ") { + tmp = nil + c.LastResult = buf.String() + return c.LastResult, nil + } + } + } +} diff --git a/src/framework/socket/udp_server.go b/src/framework/socket/udp_server.go new file mode 100644 index 00000000..d93001b0 --- /dev/null +++ b/src/framework/socket/udp_server.go @@ -0,0 +1,67 @@ +package socket + +import ( + "fmt" + "net" + "strings" +) + +// SocketUDP UDP服务端 +type SocketUDP struct { + Addr string `json:"addr"` // 主机地址 + Port int64 `json:"port"` // 端口 + Conn *net.UDPConn `json:"conn"` + StopChan chan struct{} `json:"stop"` // 停止信号 +} + +// New 创建UDP服务端 +func (s *SocketUDP) New() (*SocketUDP, error) { + // IPV6地址协议 + proto := "udp" + if strings.Contains(s.Addr, ":") { + proto = "udp6" + s.Addr = fmt.Sprintf("[%s]", s.Addr) + } + address := fmt.Sprintf("%s:%d", s.Addr, s.Port) + + // 解析 UDP 地址 + udpAddr, err := net.ResolveUDPAddr(proto, address) + if err != nil { + return nil, err + } + + // 监听 UDP 地址 + conn, err := net.ListenUDP("udp", udpAddr) + if err != nil { + return nil, err + } + + s.Conn = conn + s.StopChan = make(chan struct{}, 1) + return s, nil +} + +// CloseService 关闭当前UDP服务端 +func (s *SocketUDP) Close() { + if s.Conn != nil { + s.StopChan <- struct{}{} + (*s.Conn).Close() + } +} + +// Resolve 处理消息 +func (s *SocketUDP) Resolve(callback func(*net.UDPConn, error)) { + if s.Conn == nil { + callback(nil, fmt.Errorf("udp service not created")) + return + } + + for { + select { + case <-s.StopChan: + callback(nil, fmt.Errorf("udp service not created")) + default: + callback(s.Conn, nil) + } + } +} diff --git a/src/framework/utils/telnet/parse.go b/src/framework/telnet/parse.go similarity index 100% rename from src/framework/utils/telnet/parse.go rename to src/framework/telnet/parse.go diff --git a/src/framework/utils/telnet/telnet.go b/src/framework/telnet/telnet.go similarity index 81% rename from src/framework/utils/telnet/telnet.go rename to src/framework/telnet/telnet.go index bcafd082..8b2c2ec4 100644 --- a/src/framework/utils/telnet/telnet.go +++ b/src/framework/telnet/telnet.go @@ -52,9 +52,6 @@ func (c *ConnTelnet) NewClient() (*ConnTelnet, error) { c.Client = &client - // 调整窗口大小 (120 列 x 128 行) - requestPty(c.Client, 120, 128) - // 排空连接登录的信息 c.RunCMD("") return c, nil @@ -111,20 +108,9 @@ func (c *ConnTelnet) NewClientSession(cols, rows int) (*TelnetClientSession, err if c.Client == nil { return nil, fmt.Errorf("telnet client not connected") } - requestPty(c.Client, cols, rows) - return &TelnetClientSession{ + s := &TelnetClientSession{ Client: *c.Client, - }, nil -} - -// requestPty 调整终端窗口大小 -func requestPty(client *net.Conn, cols, rows int) error { - if client == nil { - return fmt.Errorf("telnet client not connected") } - conn := *client - // 需要确保接收方理解并正确处理发送窗口大小设置命令 - conn.Write([]byte{255, 251, 31}) - conn.Write([]byte{255, 250, 31, byte(cols >> 8), byte(cols & 0xFF), byte(rows >> 8), byte(rows & 0xFF), 255, 240}) - return nil + // s.WindowChange(cols, rows) + return s, nil } diff --git a/src/framework/utils/telnet/telnet_session.go b/src/framework/telnet/telnet_session.go similarity index 73% rename from src/framework/utils/telnet/telnet_session.go rename to src/framework/telnet/telnet_session.go index 9289eebf..33994a56 100644 --- a/src/framework/utils/telnet/telnet_session.go +++ b/src/framework/telnet/telnet_session.go @@ -19,6 +19,17 @@ func (s *TelnetClientSession) Close() { } } +// WindowChange informs the remote host about a terminal window dimension change to h rows and w columns. +func (s *TelnetClientSession) WindowChange(h, w int) error { + if s.Client == nil { + return fmt.Errorf("client is nil to content write failed") + } + // 需要确保接收方理解并正确处理发送窗口大小设置命令 + s.Client.Write([]byte{255, 251, 31}) + s.Client.Write([]byte{255, 250, 31, byte(w >> 8), byte(w & 0xFF), byte(h >> 8), byte(h & 0xFF), 255, 240}) + return nil +} + // Write 写入命令 不带回车(\n)也会执行根据客户端情况 func (s *TelnetClientSession) Write(cmd string) (int, error) { if s.Client == nil { @@ -36,11 +47,11 @@ func (s *TelnetClientSession) Read() []byte { buf := make([]byte, 1024) // 设置读取超时时间为100毫秒 s.Client.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) - _, err := s.Client.Read(buf) + n, err := s.Client.Read(buf) if err != nil { return []byte{} } - return buf + return buf[:n] } // CombinedOutput 发送命令带结果返回 diff --git a/src/framework/utils/crypto/aes.go b/src/framework/utils/crypto/aes.go index 290eb718..fd39516b 100644 --- a/src/framework/utils/crypto/aes.go +++ b/src/framework/utils/crypto/aes.go @@ -10,12 +10,12 @@ import ( "io" ) -// StringEncryptByAES 字符串AES加密 -func StringEncryptByAES(text string) (string, error) { +// AESEncryptBase64 AES加密转Base64字符串 +func AESEncryptBase64(text, key string) (string, error) { if len(text) == 0 { return "", nil } - xpass, err := aesEncryptWithSalt([]byte(text)) + xpass, err := AESEncrypt([]byte(text), []byte(key)) if err != nil { return "", err } @@ -23,8 +23,8 @@ func StringEncryptByAES(text string) (string, error) { return pass64, nil } -// StringDecryptByAES 字符串AES解密 -func StringDecryptByAES(text string) (string, error) { +// AESDecryptBase64 AES解密解Base64字符串 +func AESDecryptBase64(text, key string) (string, error) { if len(text) == 0 { return "", nil } @@ -32,21 +32,16 @@ func StringDecryptByAES(text string) (string, error) { if err != nil { return "", err } - - tpass, err := aesDecryptWithSalt(bytesPass) + tpass, err := AESDecrypt(bytesPass, []byte(key)) if err != nil { return "", err } return string(tpass), nil } -// aesKey 字符串AES加解密密钥 -const aesKey = "AGT66VfY4SMaiT97a7df0aef1704d5c5" - -// const aesKey = "AGT66VfY4SMaiT97" -// aesEncryptWithSalt AES加密 -func aesEncryptWithSalt(plaintext []byte) ([]byte, error) { - block, err := aes.NewCipher([]byte(aesKey)) +// AESEncrypt AES加密 +func AESEncrypt(plaintext, aeskey []byte) ([]byte, error) { + block, err := aes.NewCipher(aeskey) if err != nil { return nil, err } @@ -68,8 +63,8 @@ func aesEncryptWithSalt(plaintext []byte) ([]byte, error) { return ciphertext, nil } -// aesDecryptWithSalt AES解密 -func aesDecryptWithSalt(ciphertext []byte) ([]byte, error) { +// AESDecrypt AES解密 +func AESDecrypt(ciphertext, aeskey []byte) ([]byte, error) { blockSize := aes.BlockSize if len(ciphertext) < blockSize { return nil, fmt.Errorf("ciphertext too short") @@ -77,12 +72,14 @@ func aesDecryptWithSalt(ciphertext []byte) ([]byte, error) { iv := ciphertext[:blockSize] ciphertext = ciphertext[blockSize:] + block, err := aes.NewCipher([]byte(aeskey)) - block, err := aes.NewCipher([]byte(aesKey)) if err != nil { return nil, err } - + if len(ciphertext) == 0 { + return nil, fmt.Errorf("ciphertext is invalid") + } if len(ciphertext)%blockSize != 0 { return nil, fmt.Errorf("ciphertext is not a multiple of the block size") } diff --git a/src/framework/utils/ctx/ctx.go b/src/framework/utils/ctx/ctx.go index cc17a092..b9f2f1e6 100644 --- a/src/framework/utils/ctx/ctx.go +++ b/src/framework/utils/ctx/ctx.go @@ -14,7 +14,6 @@ import ( "golang.org/x/text/language" "github.com/gin-gonic/gin" - "github.com/gin-gonic/gin/binding" ) // QueryMap 查询参数转换Map @@ -30,7 +29,7 @@ func QueryMap(c *gin.Context) map[string]any { // BodyJSONMap JSON参数转换Map func BodyJSONMap(c *gin.Context) map[string]any { params := make(map[string]any) - c.ShouldBindBodyWith(¶ms, binding.JSON) + c.ShouldBindBodyWithJSON(¶ms) return params } @@ -39,7 +38,7 @@ func RequestParamsMap(c *gin.Context) map[string]any { params := make(map[string]any) // json if strings.HasPrefix(c.ContentType(), "application/json") { - c.ShouldBindBodyWith(¶ms, binding.JSON) + c.ShouldBindBodyWithJSON(¶ms) } // 表单 diff --git a/src/framework/utils/fetch/fetch.go b/src/framework/utils/fetch/fetch.go index 008b557a..be7cc07c 100644 --- a/src/framework/utils/fetch/fetch.go +++ b/src/framework/utils/fetch/fetch.go @@ -87,7 +87,7 @@ func Post(url string, data url.Values, headers map[string]string) ([]byte, error // PostJSON 发送 POST 请求,并将请求体序列化为 JSON 格式 func PostJSON(url string, data any, headers map[string]string) ([]byte, error) { client := &http.Client{ - Timeout: 3 * time.Second, // 超时时间 + Timeout: 10 * time.Second, // 超时时间 } jsonData, err := json.Marshal(data) @@ -180,7 +180,7 @@ func PostUploadFile(url string, params map[string]string, file *os.File) ([]byte // PutJSON 发送 PUT 请求,并将请求体序列化为 JSON 格式 func PutJSON(url string, data any, headers map[string]string) ([]byte, error) { client := &http.Client{ - Timeout: 3 * time.Second, // 超时时间 + Timeout: 10 * time.Second, // 超时时间 } jsonData, err := json.Marshal(data) diff --git a/src/framework/utils/file/file.go b/src/framework/utils/file/file.go index d1f90f01..25ad2585 100644 --- a/src/framework/utils/file/file.go +++ b/src/framework/utils/file/file.go @@ -351,5 +351,6 @@ func ParseUploadFileDir(subPath string) string { // filePath 上传文件路径 func ParseUploadFilePath(filePath string) string { prefix, dir := resourceUpload() - return strings.Replace(filePath, prefix, dir, 1) + absPath := strings.Replace(filePath, prefix, dir, 1) + return filepath.ToSlash(absPath) } diff --git a/src/framework/utils/file/tar.go b/src/framework/utils/file/tar.go new file mode 100644 index 00000000..22c3fb67 --- /dev/null +++ b/src/framework/utils/file/tar.go @@ -0,0 +1,76 @@ +package file + +import ( + "archive/tar" + "compress/gzip" + "io" + "os" + "path/filepath" +) + +// CompressTarGZByDir 将目录下文件添加到 tar.gz 压缩文件 +func CompressTarGZByDir(zipFilePath, dirPath string) error { + // 创建本地输出目录 + if err := os.MkdirAll(filepath.Dir(zipFilePath), 0775); err != nil { + return err + } + + // 创建输出文件 + tarFile, err := os.Create(zipFilePath) + if err != nil { + return err + } + defer tarFile.Close() + + gw := gzip.NewWriter(tarFile) + defer gw.Close() + + tw := tar.NewWriter(gw) + defer tw.Close() + + // 遍历目录下的所有文件和子目录 + err = filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // 忽略目录 + if info.IsDir() { + return nil + } + + // 创建文件条目 + header, err := tar.FileInfoHeader(info, "") + if err != nil { + return err + } + relPath, err := filepath.Rel(dirPath, path) + if err != nil { + return err + } + header.Name = relPath + if err := tw.WriteHeader(header); err != nil { + return err + } + if !info.Mode().IsRegular() { + return nil + } + + // 打开文件 + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + // 写入文件内容 + _, err = io.Copy(tw, file) + if err != nil { + return err + } + + return nil + }) + + return err +} diff --git a/src/framework/utils/machine/launch.go b/src/framework/utils/machine/launch.go index 3839d146..60d3aa84 100644 --- a/src/framework/utils/machine/launch.go +++ b/src/framework/utils/machine/launch.go @@ -8,6 +8,7 @@ import ( "runtime" "time" + "be.ems/src/framework/config" "be.ems/src/framework/constants/common" "be.ems/src/framework/logger" "be.ems/src/framework/utils/cmd" @@ -68,7 +69,8 @@ func codeFileRead() (map[string]any, error) { } content := string(bytes) // 解密 - contentDe, err := crypto.StringDecryptByAES(content) + hostKey := config.Get("aes.hostKey").(string) + contentDe, err := crypto.AESDecryptBase64(content, hostKey) if err != nil { logger.Errorf("CodeFileRead decrypt: %v", err.Error()) return mapData, fmt.Errorf("decrypt fail") @@ -86,7 +88,8 @@ func codeFileRead() (map[string]any, error) { func codeFileWrite(data map[string]any) error { jsonByte, _ := json.Marshal(data) // 加密 - contentEn, err := crypto.StringEncryptByAES(string(jsonByte)) + hostKey := config.Get("aes.hostKey").(string) + contentEn, err := crypto.AESEncryptBase64(string(jsonByte), hostKey) if err != nil { logger.Errorf("insert encrypt: %v", err.Error()) return fmt.Errorf("encrypt fail") diff --git a/src/framework/utils/parse/parse.go b/src/framework/utils/parse/parse.go index 67fb8317..a51e313f 100644 --- a/src/framework/utils/parse/parse.go +++ b/src/framework/utils/parse/parse.go @@ -17,46 +17,53 @@ import ( ) // Number 解析数值型 -func Number(str any) int64 { - switch str := str.(type) { +func Number(value any) int64 { + switch v := value.(type) { case string: - if str == "" { + if v == "" { return 0 } - num, err := strconv.ParseInt(str, 10, 64) + num, err := strconv.ParseInt(v, 10, 64) if err != nil { return 0 } return num - case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: - return reflect.ValueOf(str).Int() + case int, int8, int16, int32, int64: + return reflect.ValueOf(v).Int() + case uint, uint8, uint16, uint32, uint64: + return int64(reflect.ValueOf(v).Uint()) case float32, float64: - return int64(reflect.ValueOf(str).Float()) + return int64(reflect.ValueOf(v).Float()) + case bool: + if v { + return 1 + } + return 0 default: return 0 } } // Boolean 解析布尔型 -func Boolean(str any) bool { - switch str := str.(type) { +func Boolean(value any) bool { + switch v := value.(type) { case string: - if str == "" || str == "false" || str == "0" { + b, err := strconv.ParseBool(v) + if err != nil { return false } - // 尝试将字符串解析为数字 - if num, err := strconv.ParseFloat(str, 64); err == nil { - return num != 0 - } - return true - case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: - num := reflect.ValueOf(str).Int() + return b + case int, int8, int16, int32, int64: + num := reflect.ValueOf(v).Int() + return num != 0 + case uint, uint8, uint16, uint32, uint64: + num := int64(reflect.ValueOf(v).Uint()) return num != 0 case float32, float64: - num := reflect.ValueOf(str).Float() + num := reflect.ValueOf(v).Float() return num != 0 case bool: - return str + return v default: return false } diff --git a/src/framework/utils/ssh/files.go b/src/framework/utils/ssh/files.go index d9d5f425..187d9c86 100644 --- a/src/framework/utils/ssh/files.go +++ b/src/framework/utils/ssh/files.go @@ -24,9 +24,8 @@ type FileListRow struct { // 文件列表 // search 文件名后模糊* // -// return 目录大小,行记录,异常 -func FileList(sshClient *ConnSSH, path, search string) (string, []FileListRow, error) { - totalSize := "" +// return 行记录,异常 +func FileList(sshClient *ConnSSH, path, search string) ([]FileListRow, error) { var rows []FileListRow rowStr := "" @@ -35,40 +34,37 @@ func FileList(sshClient *ConnSSH, path, search string) (string, []FileListRow, e if search != "" { searchStr = search + searchStr } - cmdStr := fmt.Sprintf("cd %s && ls -lthd --time-style=+%%s %s", path, searchStr) + // cd /var/log && find. -maxdepth 1 -name'mme*' -exec ls -lthd --time-style=+%s {} + + cmdStr := fmt.Sprintf("cd %s && find . -maxdepth 1 -name '%s' -exec ls -lthd --time-style=+%%s {} +", path, searchStr) + // cd /var/log && ls -lthd --time-style=+%s mme* + // cmdStr := fmt.Sprintf("cd %s && ls -lthd --time-style=+%%s %s", path, searchStr) // 是否远程客户端读取 if sshClient == nil { resultStr, err := cmd.Execf(cmdStr) if err != nil { logger.Errorf("Ne FileList Path: %s, Search: %s, Error:%s", path, search, err.Error()) - return totalSize, rows, err + return rows, err } rowStr = resultStr } else { resultStr, err := sshClient.RunCMD(cmdStr) if err != nil { logger.Errorf("Ne FileList Path: %s, Search: %s, Error:%s", path, search, err.Error()) - return totalSize, rows, err + return rows, err } rowStr = resultStr } // 遍历组装 rowStrList := strings.Split(rowStr, "\n") - for i, rowStr := range rowStrList { + for _, rowStr := range rowStrList { if rowStr == "" { continue } // 使用空格对字符串进行切割 fields := strings.Fields(rowStr) - // 无查询过滤会有total总计 - if i == 0 && searchStr == "" { - totalSize = fields[1] - continue - } - // 拆分不足7位跳过 if len(fields) != 7 { continue @@ -83,6 +79,14 @@ func FileList(sshClient *ConnSSH, path, search string) (string, []FileListRow, e fileType = "symlink" } + // 文件名 + fileName := fields[6] + if fileName == "." { + continue + } else if strings.HasPrefix(fileName, "./") { + fileName = strings.TrimPrefix(fileName, "./") + } + // 提取各个字段的值 rows = append(rows, FileListRow{ FileMode: fileMode, @@ -92,8 +96,8 @@ func FileList(sshClient *ConnSSH, path, search string) (string, []FileListRow, e Group: fields[3], Size: fields[4], ModifiedTime: parse.Number(fields[5]), - FileName: fields[6], + FileName: fileName, }) } - return totalSize, rows, nil + return rows, nil } diff --git a/src/framework/utils/ssh/sftp.go b/src/framework/utils/ssh/sftp.go index f5c103f7..45ef9af6 100644 --- a/src/framework/utils/ssh/sftp.go +++ b/src/framework/utils/ssh/sftp.go @@ -60,7 +60,7 @@ func (s *SSHClientSFTP) CopyDirRemoteToLocal(remoteDir, localDir string) error { return nil } -// CopyDirRemoteToLocal 复制目录-本地到远程 +// CopyDirLocalToRemote 复制目录-本地到远程 func (s *SSHClientSFTP) CopyDirLocalToRemote(localDir, remoteDir string) error { // 遍历本地目录中的文件和子目录并复制到远程 err := filepath.Walk(localDir, func(localPath string, info os.FileInfo, err error) error { @@ -94,7 +94,7 @@ func (s *SSHClientSFTP) CopyDirLocalToRemote(localDir, remoteDir string) error { return nil } -// CopyDirRemoteToLocal 复制文件-远程到本地 +// CopyFileRemoteToLocal 复制文件-远程到本地 func (s *SSHClientSFTP) CopyFileRemoteToLocal(remotePath, localPath string) error { if err := os.MkdirAll(filepath.Dir(localPath), 0775); err != nil { return err @@ -124,7 +124,7 @@ func (s *SSHClientSFTP) CopyFileRemoteToLocal(remotePath, localPath string) erro return nil } -// CopyDirRemoteToLocal 复制文件-本地到远程 +// CopyFileLocalToRemote 复制文件-本地到远程 func (s *SSHClientSFTP) CopyFileLocalToRemote(localPath, remotePath string) error { // 打开本地文件 localFile, err := os.Open(localPath) diff --git a/src/framework/utils/ssh/ssh.go b/src/framework/utils/ssh/ssh.go index d70991e7..6a4a526a 100644 --- a/src/framework/utils/ssh/ssh.go +++ b/src/framework/utils/ssh/ssh.go @@ -212,11 +212,14 @@ func (c *ConnSSH) SendToAuthorizedKeys() error { if err != nil { return err } + // "sudo mkdir -p ~/.ssh && sudo chown omcuser:omcuser ~/.ssh && sudo chmod 700 ~/.ssh" + // "sudo touch ~/.ssh/authorized_keys && sudo chown omcuser:omcuser ~/.ssh/authorized_keys && sudo chmod 600 ~/.ssh/authorized_keys" + // "echo 'ssh-rsa AAAAB3= pc-host\n' | sudo tee -a ~/.ssh/authorized_keys" authorizedKeysEntry := fmt.Sprintln(strings.TrimSpace(publicKey)) cmdStrArr := []string{ - fmt.Sprintf("sudo mkdir -p /home/%s/.ssh && sudo chown %s:%s /home/%s/.ssh && sudo chmod 700 /home/%s/.ssh", c.User, c.User, c.User, c.User, c.User), - fmt.Sprintf("sudo touch /home/%s/.ssh/authorized_keys && sudo chown %s:%s /home/%s/.ssh/authorized_keys && sudo chmod 600 /home/%s/.ssh/authorized_keys", c.User, c.User, c.User, c.User, c.User), - fmt.Sprintf("echo '%s' | sudo tee -a /home/%s/.ssh/authorized_keys", authorizedKeysEntry, c.User), + fmt.Sprintf("sudo mkdir -p ~/.ssh && sudo chown %s:%s ~/.ssh && sudo chmod 700 ~/.ssh", c.User, c.User), + fmt.Sprintf("sudo touch ~/.ssh/authorized_keys && sudo chown %s:%s ~/.ssh/authorized_keys && sudo chmod 600 ~/.ssh/authorized_keys", c.User, c.User), + fmt.Sprintf("echo '%s' | sudo tee -a ~/.ssh/authorized_keys", authorizedKeysEntry), } _, err = c.RunCMD(strings.Join(cmdStrArr, " && ")) if err != nil { diff --git a/src/framework/utils/ua/ua.go b/src/framework/utils/ua/ua.go index 1a125315..d1115beb 100644 --- a/src/framework/utils/ua/ua.go +++ b/src/framework/utils/ua/ua.go @@ -1,8 +1,8 @@ package ua -import "github.com/mssola/user_agent" +import "github.com/mssola/useragent" // 获取user-agent信息 -func Info(userAgent string) *user_agent.UserAgent { - return user_agent.New(userAgent) +func Info(userAgent string) *useragent.UserAgent { + return useragent.New(userAgent) } diff --git a/src/framework/vo/result/result.go b/src/framework/vo/result/result.go index 7ff154ff..192e6461 100644 --- a/src/framework/vo/result/result.go +++ b/src/framework/vo/result/result.go @@ -1,7 +1,7 @@ package result import ( - "be.ems/src/framework/constants/result" + constResult "be.ems/src/framework/constants/result" ) // CodeMsg 响应结果 @@ -15,8 +15,8 @@ func CodeMsg(code int, msg string) map[string]any { // 响应成功结果 map[string]any{} func Ok(v map[string]any) map[string]any { args := make(map[string]any) - args["code"] = result.CODE_SUCCESS - args["msg"] = result.MSG_SUCCESS + args["code"] = constResult.CODE_SUCCESS + args["msg"] = constResult.MSG_SUCCESS // v合并到args for key, value := range v { args[key] = value @@ -27,7 +27,7 @@ func Ok(v map[string]any) map[string]any { // 响应成功结果信息 func OkMsg(msg string) map[string]any { args := make(map[string]any) - args["code"] = result.CODE_SUCCESS + args["code"] = constResult.CODE_SUCCESS args["msg"] = msg return args } @@ -35,8 +35,8 @@ func OkMsg(msg string) map[string]any { // 响应成功结果数据 func OkData(data any) map[string]any { args := make(map[string]any) - args["code"] = result.CODE_SUCCESS - args["msg"] = result.MSG_SUCCESS + args["code"] = constResult.CODE_SUCCESS + args["msg"] = constResult.MSG_SUCCESS args["data"] = data return args } @@ -44,8 +44,8 @@ func OkData(data any) map[string]any { // 响应失败结果 map[string]any{} func Err(v map[string]any) map[string]any { args := make(map[string]any) - args["code"] = result.CODE_ERROR - args["msg"] = result.MSG_ERROR + args["code"] = constResult.CODE_ERROR + args["msg"] = constResult.MSG_ERROR // v合并到args for key, value := range v { args[key] = value @@ -56,7 +56,7 @@ func Err(v map[string]any) map[string]any { // 响应失败结果信息 func ErrMsg(msg string) map[string]any { args := make(map[string]any) - args["code"] = result.CODE_ERROR + args["code"] = constResult.CODE_ERROR args["msg"] = msg return args } @@ -64,8 +64,8 @@ func ErrMsg(msg string) map[string]any { // 响应失败结果数据 func ErrData(data any) map[string]any { args := make(map[string]any) - args["code"] = result.CODE_ERROR - args["msg"] = result.MSG_ERROR + args["code"] = constResult.CODE_ERROR + args["msg"] = constResult.MSG_ERROR args["data"] = data return args } diff --git a/src/modules/chart/chart.go b/src/modules/chart/chart.go index d8079ceb..83455e2b 100644 --- a/src/modules/chart/chart.go +++ b/src/modules/chart/chart.go @@ -13,10 +13,8 @@ import ( func Setup(router *gin.Engine) { logger.Infof("开始加载 ====> chart 模块路由") - chartGroup := router.Group("/chart") - - // 关系图 - chartGraphGroup := chartGroup.Group("/graph") + // G6关系图 + chartGraphGroup := router.Group("/chart/graph") { chartGraphGroup.GET("", middleware.PreAuthorize(nil), diff --git a/src/modules/chart/controller/chart_graph.go b/src/modules/chart/controller/chart_graph.go index 89f6a4be..acad8af3 100644 --- a/src/modules/chart/controller/chart_graph.go +++ b/src/modules/chart/controller/chart_graph.go @@ -4,14 +4,14 @@ import ( "be.ems/src/framework/i18n" "be.ems/src/framework/utils/ctx" "be.ems/src/framework/vo/result" - chartService "be.ems/src/modules/chart/service" + "be.ems/src/modules/chart/service" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" ) // 实例化控制层 ChartGraphController 结构体 var NewChartGraph = &ChartGraphController{ - chartGraphService: chartService.NewChartGraphImpl, + chartGraphService: service.NewChartGraph, } // G6关系图 @@ -19,7 +19,7 @@ var NewChartGraph = &ChartGraphController{ // PATH /graph type ChartGraphController struct { // G6关系图数据表服务 - chartGraphService chartService.IChartGraph + chartGraphService *service.ChartGraph } // 获取关系图组名 diff --git a/src/modules/chart/model/chart_graph.go b/src/modules/chart/model/chart_graph.go index 26dd8231..c087d3e2 100644 --- a/src/modules/chart/model/chart_graph.go +++ b/src/modules/chart/model/chart_graph.go @@ -1,31 +1,32 @@ package model -// ChartGraph G6关系图数据对象 chart_graph +// ChartGraph 图表-G6关系图数据对象 chart_graph type ChartGraph struct { RowID int64 `json:"rowId,omitempty" gorm:"column:row_id;primaryKey;autoIncrement"` // 记录ID - RowType string `json:"rowType,omitempty" gorm:"column:row_type"` // 记录类型(node/edge/combo) - RowGroup string `json:"rowGroup,omitempty" gorm:"column:row_group"` // 记录组名 - ID string `json:"id,omitempty" gorm:"column:id"` // 元素ID - Type string `json:"type,omitempty" gorm:"column:type"` // node/combo 类型 - Depth int `json:"depth,omitempty" gorm:"column:depth"` // node/combo 深度 - X float64 `json:"x,omitempty" gorm:"column:x"` // node/combo 横向坐标 - Y float64 `json:"y,omitempty" gorm:"column:y"` // node/combo 纵向坐标 - Size string `json:"size,omitempty" gorm:"column:size"` // node/combo 大小-JSON数组 - Icon string `json:"icon,omitempty" gorm:"column:icon"` // node-部分类型支持图标JSON配置 - Img string `json:"img,omitempty" gorm:"column:img"` // node-img 图片 - ClipCfg string `json:"clipCfg,omitempty" gorm:"column:clip_cfg"` // node-img 图片裁剪JSON配置 - Direction string `json:"direction,omitempty" gorm:"column:direction"` // node-triangle 三角形的方向(up/down/left/right) - Source string `json:"source,omitempty" gorm:"column:source"` // edge-边起始 - Target string `json:"target,omitempty" gorm:"column:target"` // edge-边目标 - ComboID string `json:"combo_id,omitempty" gorm:"column:combo_id"` // combo-分组 - Padding string `json:"padding,omitempty" gorm:"column:padding"` // combo-JSON分组内边距 - ParentID string `json:"parentId,omitempty" gorm:"column:parent_id"` // combo-父级分组 - Children string `json:"children,omitempty" gorm:"column:children"` // combo-JSON分组内含元素 - Style string `json:"style,omitempty" gorm:"column:style"` // 元素样式-JONS配置 - Label string `json:"label,omitempty" gorm:"column:label"` // 标签文本 - LabelCfg string `json:"labelCfg,omitempty" gorm:"column:label_cfg"` // 标签文本-JSON配置 + RowType string `json:"rowType" gorm:"row_type"` // 记录类型 + RowGroup string `json:"rowGroup" gorm:"row_group"` // 记录组名 + ID string `json:"id" gorm:"id"` // 元素ID + Type string `json:"type" gorm:"type"` // node/combo 类型 + Depth int64 `json:"depth" gorm:"depth"` // node/combo 深度 + X float64 `json:"x" gorm:"x"` // node/combo 横向坐标 + Y float64 `json:"y" gorm:"y"` // node/combo 纵向坐标 + Size string `json:"size" gorm:"size"` // node/combo 大小-JSON数组 + Icon string `json:"icon" gorm:"icon"` // node-部分类型支持图标JSON配置 + Img string `json:"img" gorm:"img"` // node-img 图片 + ClipCfg string `json:"clipCfg" gorm:"clip_cfg"` // node-img 图片裁剪JSON配置 + Direction string `json:"direction" gorm:"direction"` // node-triangle 三角形的方向 + Source string `json:"source" gorm:"source"` // edge-边起始 + Target string `json:"target" gorm:"target"` // edge-边目标 + ComboId string `json:"comboId" gorm:"combo_id"` // combo-分组 + Padding string `json:"padding" gorm:"padding"` // combo-JSON分组内边距 + ParentId string `json:"parentId" gorm:"parent_id"` // combo-父级分组 + Children string `json:"children" gorm:"children"` // combo-JSON分组内含元素 + Style string `json:"style" gorm:"style"` // 元素样式-JONS配置 + Label string `json:"label" gorm:"label"` // 标签文本 + LabelCfg string `json:"labelCfg" gorm:"label_cfg"` // 标签文本-JSON配置 } -func (ChartGraph) TableName() string { +// TableName 表名称 +func (*ChartGraph) TableName() string { return "chart_graph" } diff --git a/src/modules/chart/repository/chart_graph.go b/src/modules/chart/repository/chart_graph.go index 9d42eaa8..4d917e3c 100644 --- a/src/modules/chart/repository/chart_graph.go +++ b/src/modules/chart/repository/chart_graph.go @@ -1,21 +1,194 @@ package repository -import "be.ems/src/modules/chart/model" +import ( + "strings" -// G6关系图数据 数据层接口 -type IChartGraph interface { - // SelectPage 根据条件分页查询字典类型 - SelectPage(query map[string]any) map[string]any + "be.ems/src/framework/datasource" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/parse" + "be.ems/src/framework/utils/repo" + "be.ems/src/modules/chart/model" +) - // SelectList 根据实体查询 - SelectList(graph model.ChartGraph) []model.ChartGraph +// 实例化数据层 ChartGraph 结构体 +var NewChartGraph = &ChartGraph{ + selectSql: `select + row_id, row_type, row_group, + id, type, depth, x, y, size, icon, img, + clip_cfg, direction, + source, target, combo_id, + padding, parent_id, children, + style, label, label_cfg + from chart_graph`, - // SelectGroup 查询组名 - SelectGroup() []string - - // Insert 批量添加 - Inserts(graphs []model.ChartGraph) int64 - - // Delete 删除组数据 - DeleteGroup(rowGroup string) int64 + resultMap: map[string]string{ + "row_id": "RowID", + "row_type": "RowType", + "row_group": "RowGroup", + "id": "ID", + "type": "Type", + "depth": "Depth", + "x": "X", + "y": "Y", + "size": "Size", + "icon": "Icon", + "img": "Img", + "clip_cfg": "ClipCfg", + "direction": "Direction", + "source": "Source", + "target": "Target", + "combo_id": "ComboID", + "padding": "Padding", + "parent_id": "ParentID", + "children": "Children", + "style": "Style", + "label": "Label", + "label_cfg": "LabelCfg", + }, +} + +// ChartGraph G6关系图数据表 数据层处理 +type ChartGraph struct { + // 查询视图对象SQL + selectSql string + // 结果字段与实体映射 + resultMap map[string]string +} + +// convertResultRows 将结果记录转实体结果组 +func (r *ChartGraph) convertResultRows(rows []map[string]any) []model.ChartGraph { + arr := make([]model.ChartGraph, 0) + for _, row := range rows { + item := model.ChartGraph{} + for key, value := range row { + if keyMapper, ok := r.resultMap[key]; ok { + repo.SetFieldValue(&item, keyMapper, value) + } + } + arr = append(arr, item) + } + return arr +} + +// SelectPage 根据条件分页查询字典类型 +func (r *ChartGraph) SelectPage(query map[string]any) map[string]any { + // 查询条件拼接 + var conditions []string + var params []any + if v, ok := query["rowType"]; ok && v != "" { + conditions = append(conditions, "row_type = ?") + params = append(params, strings.Trim(v.(string), " ")) + } + if v, ok := query["rowGroup"]; ok && v != "" { + conditions = append(conditions, "row_group = ?") + params = append(params, strings.Trim(v.(string), " ")) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + result := map[string]any{ + "total": 0, + "rows": []model.ChartGraph{}, + } + + // 查询数量 长度为0直接返回 + totalSql := "select count(1) as 'total' from chart_graph" + totalRows, err := datasource.RawDB("", totalSql+whereSql, params) + if err != nil { + logger.Errorf("total err => %v", err) + return result + } + total := parse.Number(totalRows[0]["total"]) + if total == 0 { + return result + } else { + result["total"] = total + } + + // 分页 + pageNum, pageSize := repo.PageNumSize(query["pageNum"], query["pageSize"]) + pageSql := " limit ?,? " + params = append(params, pageNum*pageSize) + params = append(params, pageSize) + + // 查询数据 + querySql := r.selectSql + whereSql + pageSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + return result + } + + // 转换实体 + result["rows"] = r.convertResultRows(results) + return result +} + +// SelectList 根据实体查询 +func (r *ChartGraph) SelectList(graph model.ChartGraph) []model.ChartGraph { + // 查询条件拼接 + var conditions []string + var params []any + if graph.RowType != "" { + conditions = append(conditions, "row_type = ?") + params = append(params, graph.RowType) + } + if graph.RowGroup != "" { + conditions = append(conditions, "row_group = ?") + params = append(params, graph.RowGroup) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询数据 + querySql := r.selectSql + whereSql + " order by depth asc " + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + } + + // 转换实体 + return r.convertResultRows(results) +} + +// SelectGroup 查询组名 +func (r *ChartGraph) SelectGroup() []string { + rows := []string{} + // 查询数量 长度为0直接返回 + querySql := "select row_group as 'str' from chart_graph GROUP BY row_group" + strRows, err := datasource.RawDB("", querySql, nil) + if err != nil { + logger.Errorf("Query err => %v", err) + return rows + } + for _, v := range strRows { + rows = append(rows, v["str"].(string)) + } + return rows +} + +// Insert 批量添加 +func (r *ChartGraph) Inserts(graphs []model.ChartGraph) int64 { + tx := datasource.DefaultDB().CreateInBatches(graphs, 2000) + if err := tx.Error; err != nil { + logger.Errorf("CreateInBatches err => %v", err) + } + return tx.RowsAffected +} + +// Delete 删除组数据 +func (r *ChartGraph) DeleteGroup(rowGroup string) int64 { + tx := datasource.DefaultDB().Where("row_group = ?", rowGroup).Delete(&model.ChartGraph{}) + if err := tx.Error; err != nil { + logger.Errorf("Delete err => %v", err) + } + return tx.RowsAffected } diff --git a/src/modules/chart/repository/chart_graph.impl.go b/src/modules/chart/repository/chart_graph.impl.go deleted file mode 100644 index c3a26d5c..00000000 --- a/src/modules/chart/repository/chart_graph.impl.go +++ /dev/null @@ -1,194 +0,0 @@ -package repository - -import ( - "strings" - - "be.ems/src/framework/datasource" - "be.ems/src/framework/logger" - "be.ems/src/framework/utils/parse" - "be.ems/src/framework/utils/repo" - "be.ems/src/modules/chart/model" -) - -// 实例化数据层 NewChartGraphImpl 结构体 -var NewChartGraphImpl = &ChartGraphImpl{ - selectSql: `select - row_id, row_type, row_group, - id, type, depth, x, y, size, icon, img, - clip_cfg, direction, - source, target, combo_id, - padding, parent_id, children, - style, label, label_cfg - from chart_graph`, - - resultMap: map[string]string{ - "row_id": "RowID", - "row_type": "RowType", - "row_group": "RowGroup", - "id": "ID", - "type": "Type", - "depth": "Depth", - "x": "X", - "y": "Y", - "size": "Size", - "icon": "Icon", - "img": "Img", - "clip_cfg": "ClipCfg", - "direction": "Direction", - "source": "Source", - "target": "Target", - "combo_id": "ComboID", - "padding": "Padding", - "parent_id": "ParentID", - "children": "Children", - "style": "Style", - "label": "Label", - "label_cfg": "LabelCfg", - }, -} - -// ChartGraphImpl G6关系图数据表 数据层处理 -type ChartGraphImpl struct { - // 查询视图对象SQL - selectSql string - // 结果字段与实体映射 - resultMap map[string]string -} - -// convertResultRows 将结果记录转实体结果组 -func (r *ChartGraphImpl) convertResultRows(rows []map[string]any) []model.ChartGraph { - arr := make([]model.ChartGraph, 0) - for _, row := range rows { - item := model.ChartGraph{} - for key, value := range row { - if keyMapper, ok := r.resultMap[key]; ok { - repo.SetFieldValue(&item, keyMapper, value) - } - } - arr = append(arr, item) - } - return arr -} - -// SelectPage 根据条件分页查询字典类型 -func (r *ChartGraphImpl) SelectPage(query map[string]any) map[string]any { - // 查询条件拼接 - var conditions []string - var params []any - if v, ok := query["rowType"]; ok && v != "" { - conditions = append(conditions, "row_type = ?") - params = append(params, strings.Trim(v.(string), " ")) - } - if v, ok := query["rowGroup"]; ok && v != "" { - conditions = append(conditions, "row_group = ?") - params = append(params, strings.Trim(v.(string), " ")) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } - - result := map[string]any{ - "total": 0, - "rows": []model.ChartGraph{}, - } - - // 查询数量 长度为0直接返回 - totalSql := "select count(1) as 'total' from chart_graph" - totalRows, err := datasource.RawDB("", totalSql+whereSql, params) - if err != nil { - logger.Errorf("total err => %v", err) - return result - } - total := parse.Number(totalRows[0]["total"]) - if total == 0 { - return result - } else { - result["total"] = total - } - - // 分页 - pageNum, pageSize := repo.PageNumSize(query["pageNum"], query["pageSize"]) - pageSql := " limit ?,? " - params = append(params, pageNum*pageSize) - params = append(params, pageSize) - - // 查询数据 - querySql := r.selectSql + whereSql + pageSql - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err => %v", err) - return result - } - - // 转换实体 - result["rows"] = r.convertResultRows(results) - return result -} - -// SelectList 根据实体查询 -func (r *ChartGraphImpl) SelectList(graph model.ChartGraph) []model.ChartGraph { - // 查询条件拼接 - var conditions []string - var params []any - if graph.RowType != "" { - conditions = append(conditions, "row_type = ?") - params = append(params, graph.RowType) - } - if graph.RowGroup != "" { - conditions = append(conditions, "row_group = ?") - params = append(params, graph.RowGroup) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } - - // 查询数据 - querySql := r.selectSql + whereSql + " order by depth asc " - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err => %v", err) - } - - // 转换实体 - return r.convertResultRows(results) -} - -// SelectGroup 查询组名 -func (r *ChartGraphImpl) SelectGroup() []string { - rows := []string{} - // 查询数量 长度为0直接返回 - querySql := "select row_group as 'str' from chart_graph GROUP BY row_group" - strRows, err := datasource.RawDB("", querySql, nil) - if err != nil { - logger.Errorf("Query err => %v", err) - return rows - } - for _, v := range strRows { - rows = append(rows, v["str"].(string)) - } - return rows -} - -// Insert 批量添加 -func (r *ChartGraphImpl) Inserts(graphs []model.ChartGraph) int64 { - tx := datasource.DefaultDB().CreateInBatches(graphs, 2000) - if err := tx.Error; err != nil { - logger.Errorf("CreateInBatches err => %v", err) - } - return tx.RowsAffected -} - -// Delete 删除组数据 -func (r *ChartGraphImpl) DeleteGroup(rowGroup string) int64 { - tx := datasource.DefaultDB().Where("row_group = ?", rowGroup).Delete(&model.ChartGraph{}) - if err := tx.Error; err != nil { - logger.Errorf("Delete err => %v", err) - } - return tx.RowsAffected -} diff --git a/src/modules/chart/service/chart_graph.go b/src/modules/chart/service/chart_graph.go index 470183d3..6eaf3cbd 100644 --- a/src/modules/chart/service/chart_graph.go +++ b/src/modules/chart/service/chart_graph.go @@ -1,16 +1,359 @@ package service -// G6关系图数据 服务层接口 -type IChartGraph interface { - // SelectGroup 查询组名 - SelectGroup() []string +import ( + "encoding/json" + "strings" - // LoadData 查询所组图数据 - LoadData(rowGroup, rowType string) map[string]any + "be.ems/src/framework/utils/parse" + "be.ems/src/modules/chart/model" + "be.ems/src/modules/chart/repository" +) - // SaveData 添加组图数据 - SaveData(rowGroup string, data map[string]any) int64 - - // DeleteGroup 删除所组图数据 - DeleteGroup(rowGroup string) int64 +// 实例化服务层 ChartGraph 结构体 +var NewChartGraph = &ChartGraph{ + graphRepository: repository.NewChartGraph, +} + +// ChartGraph G6关系图数据表 服务层处理 +type ChartGraph struct { + // G6关系图数据服务 + graphRepository *repository.ChartGraph +} + +// SelectGroup 查询组名 +func (s *ChartGraph) SelectGroup() []string { + return s.graphRepository.SelectGroup() +} + +// LoadData 查询所组图数据 +func (s *ChartGraph) LoadData(rowGroup, rowType string) map[string]any { + // 查询数据 + graph := model.ChartGraph{ + RowGroup: rowGroup, + } + if rowType != "" { + graph.RowType = rowType + } + data := s.graphRepository.SelectList(graph) + + // 数据项 + nodes := []map[string]any{} + edges := []map[string]any{} + combos := []map[string]any{} + + for _, v := range data { + if v.RowType == "node" { + nodes = append(nodes, s.loadNode(v)) + } + if v.RowType == "edge" { + edges = append(edges, s.loadEdge(v)) + } + if v.RowType == "combo" { + combos = append(combos, s.loadCombo(v)) + } + } + + return map[string]any{ + "nodes": nodes, + "edges": edges, + "combos": combos, + } +} + +// loadNode 图数据Node +func (s *ChartGraph) loadNode(v model.ChartGraph) map[string]any { + node := map[string]any{ + "id": v.ID, + "comboId": v.ComboId, + "x": v.X, + "y": v.Y, + "type": v.Type, + "depth": v.Depth, + } + + // 元素样式 + style := map[string]any{} + if len(v.Style) > 7 { + json.Unmarshal([]byte(v.Style), &style) + } + node["style"] = style + + // 元素大小 + if strings.Contains(v.Size, "[") { + sizeArr := []int64{} + json.Unmarshal([]byte(v.Size), &sizeArr) + node["size"] = sizeArr + } else { + node["size"] = parse.Number(v.Size) + } + + // 标签文本 + node["label"] = v.Label + labelCfg := map[string]any{} + if len(v.LabelCfg) > 7 { + json.Unmarshal([]byte(v.LabelCfg), &labelCfg) + } + node["labelCfg"] = labelCfg + + // 三角形属性 + if v.Type == "triangle" { + node["direction"] = v.Direction + } + + // 图片属性 + if strings.Index(v.Type, "image") == 0 { + node["img"] = v.Img + clipCfg := map[string]any{} + if len(v.ClipCfg) > 7 { + json.Unmarshal([]byte(v.ClipCfg), &clipCfg) + } + node["clipCfg"] = clipCfg + } + + // 图标属性 + if v.Icon != "" { + icon := map[string]any{} + if len(v.Icon) > 7 { + json.Unmarshal([]byte(v.Icon), &icon) + } + node["icon"] = icon + } + + return node +} + +// loadEdge 图数据Edge +func (s *ChartGraph) loadEdge(v model.ChartGraph) map[string]any { + edge := map[string]any{ + "id": v.ID, + "source": v.Source, + "target": v.Target, + "type": v.Type, + } + + // 元素样式 + style := map[string]any{} + if len(v.Style) > 7 { + json.Unmarshal([]byte(v.Style), &style) + } + edge["style"] = style + + // 标签文本 + edge["label"] = v.Label + labelCfg := map[string]any{} + if len(v.LabelCfg) > 7 { + json.Unmarshal([]byte(v.LabelCfg), &labelCfg) + } + edge["labelCfg"] = labelCfg + + return edge +} + +// loadCombo 图数据Combo +func (s *ChartGraph) loadCombo(v model.ChartGraph) map[string]any { + combo := map[string]any{ + "id": v.ID, + "x": v.X, + "y": v.Y, + "type": v.Type, + "depth": v.Depth, + } + + // 元素样式 + style := map[string]any{} + if len(v.Style) > 7 { + json.Unmarshal([]byte(v.Style), &style) + } + combo["style"] = style + + // 元素大小 + if strings.Contains(v.Size, "[") { + sizeArr := []int64{} + json.Unmarshal([]byte(v.Size), &sizeArr) + combo["size"] = sizeArr + } else { + combo["size"] = parse.Number(v.Size) + } + + // 元素内边距 + if strings.Contains(v.Padding, "[") { + paddingArr := []int64{} + json.Unmarshal([]byte(v.Padding), &paddingArr) + combo["padding"] = paddingArr + } else { + combo["padding"] = parse.Number(v.Padding) + } + + // 标签文本 + combo["label"] = v.Label + labelCfg := map[string]any{} + if len(v.LabelCfg) > 7 { + json.Unmarshal([]byte(v.LabelCfg), &labelCfg) + } + combo["labelCfg"] = labelCfg + + // 分组内元素 + if v.Children != "" { + children := []map[string]any{} + if len(v.Children) > 7 { + json.Unmarshal([]byte(v.Children), &children) + } + combo["children"] = children + } + + return combo +} + +// SaveData 添加组图数据 +func (s *ChartGraph) SaveData(rowGroup string, data map[string]any) int64 { + graphs := []model.ChartGraph{} + nodes := data["nodes"].([]map[string]any) + graphNodes := s.saveNode(rowGroup, nodes) + graphs = append(graphs, graphNodes...) + edges := data["edges"].([]map[string]any) + graphEdges := s.saveEdge(rowGroup, edges) + graphs = append(graphs, graphEdges...) + combos := data["combos"].([]map[string]any) + graphCombos := s.saveCombo(rowGroup, combos) + graphs = append(graphs, graphCombos...) + // 删除组数据后插入 + if len(graphs) > 0 { + s.graphRepository.DeleteGroup(rowGroup) + } + return s.graphRepository.Inserts(graphs) +} + +// saveNode 图数据Node +func (s *ChartGraph) saveNode(rowGroup string, nodes []map[string]any) []model.ChartGraph { + var graphs []model.ChartGraph + for _, v := range nodes { + node := model.ChartGraph{ + RowType: "node", + RowGroup: rowGroup, + ID: v["id"].(string), + X: v["x"].(float64), + Y: v["y"].(float64), + Type: v["type"].(string), + } + if comboId, ok := v["comboId"]; ok && comboId != nil { + node.ComboId = comboId.(string) + } + if depth, ok := v["depth"]; ok && depth != nil { + node.Depth = int64(depth.(float64)) + } + if styleByte, err := json.Marshal(v["style"]); err == nil { + node.Style = string(styleByte) + } + + // 元素大小 + if sizeByte, err := json.Marshal(v["size"]); err == nil { + node.Size = string(sizeByte) + } + + // 标签文本 + if label, ok := v["label"]; ok && label != nil { + node.Label = label.(string) + } + if labelCfgByte, err := json.Marshal(v["labelCfg"]); err == nil { + node.LabelCfg = string(labelCfgByte) + } + // 三角形属性 + if direction, ok := v["direction"]; ok && direction != nil && node.Type == "triangle" { + node.Direction = direction.(string) + } + // 图片属性 + if img, ok := v["img"]; ok && img != nil { + node.Img = img.(string) + if clipCfgByte, err := json.Marshal(v["clipCfg"]); err == nil { + node.ClipCfg = string(clipCfgByte) + } + } + // 图标属性 + if icon, ok := v["icon"]; ok && icon != nil { + if iconByte, err := json.Marshal(icon); err == nil { + node.Icon = string(iconByte) + } + } + + graphs = append(graphs, node) + } + return graphs +} + +// saveEdge 图数据Edge +func (s *ChartGraph) saveEdge(rowGroup string, edges []map[string]any) []model.ChartGraph { + var graphs []model.ChartGraph + for _, v := range edges { + edge := model.ChartGraph{ + RowType: "edge", + RowGroup: rowGroup, + ID: v["id"].(string), + Source: v["source"].(string), + Target: v["target"].(string), + Type: v["type"].(string), + } + + if styleByte, err := json.Marshal(v["style"]); err == nil { + edge.Style = string(styleByte) + } + + // 标签文本 + if label, ok := v["label"]; ok && label != nil { + edge.Label = label.(string) + } + if labelCfgByte, err := json.Marshal(v["labelCfg"]); err == nil { + edge.LabelCfg = string(labelCfgByte) + } + + graphs = append(graphs, edge) + } + return graphs +} + +// saveCombo 图数据Combo +func (s *ChartGraph) saveCombo(rowGroup string, combos []map[string]any) []model.ChartGraph { + var graphs []model.ChartGraph + for _, v := range combos { + combo := model.ChartGraph{ + RowType: "combo", + RowGroup: rowGroup, + ID: v["id"].(string), + X: v["x"].(float64), + Y: v["y"].(float64), + Type: v["type"].(string), + } + if depth, ok := v["depth"]; ok && depth != nil { + combo.Depth = int64(depth.(float64)) + } + if styleByte, err := json.Marshal(v["style"]); err == nil { + combo.Style = string(styleByte) + } + if paddingByte, err := json.Marshal(v["padding"]); err == nil { + combo.Padding = string(paddingByte) + } + if childrenByte, err := json.Marshal(v["children"]); err == nil { + combo.Children = string(childrenByte) + } + + // 元素大小 + if sizeByte, err := json.Marshal(v["size"]); err == nil { + combo.Size = string(sizeByte) + } + + // 标签文本 + if label, ok := v["label"]; ok && label != nil { + combo.Label = label.(string) + } + if labelCfgByte, err := json.Marshal(v["labelCfg"]); err == nil { + combo.LabelCfg = string(labelCfgByte) + } + + graphs = append(graphs, combo) + } + return graphs +} + +// Delete 删除所组图数据 +func (s *ChartGraph) DeleteGroup(rowGroup string) int64 { + return s.graphRepository.DeleteGroup(rowGroup) } diff --git a/src/modules/chart/service/chart_graph.impl.go b/src/modules/chart/service/chart_graph.impl.go deleted file mode 100644 index 8244f752..00000000 --- a/src/modules/chart/service/chart_graph.impl.go +++ /dev/null @@ -1,359 +0,0 @@ -package service - -import ( - "strings" - - "be.ems/src/framework/utils/parse" - "be.ems/src/modules/chart/model" - chartRepository "be.ems/src/modules/chart/repository" - "github.com/goccy/go-json" -) - -// 实例化服务层 ChartGraphImpl 结构体 -var NewChartGraphImpl = &ChartGraphImpl{ - graphRepository: chartRepository.NewChartGraphImpl, -} - -// ChartGraphImpl G6关系图数据表 服务层处理 -type ChartGraphImpl struct { - // G6关系图数据服务 - graphRepository chartRepository.IChartGraph -} - -// SelectGroup 查询组名 -func (s *ChartGraphImpl) SelectGroup() []string { - return s.graphRepository.SelectGroup() -} - -// LoadData 查询所组图数据 -func (s *ChartGraphImpl) LoadData(rowGroup, rowType string) map[string]any { - // 查询数据 - graph := model.ChartGraph{ - RowGroup: rowGroup, - } - if rowType != "" { - graph.RowType = rowType - } - data := s.graphRepository.SelectList(graph) - - // 数据项 - nodes := []map[string]any{} - edges := []map[string]any{} - combos := []map[string]any{} - - for _, v := range data { - if v.RowType == "node" { - nodes = append(nodes, s.loadNode(v)) - } - if v.RowType == "edge" { - edges = append(edges, s.loadEdge(v)) - } - if v.RowType == "combo" { - combos = append(combos, s.loadCombo(v)) - } - } - - return map[string]any{ - "nodes": nodes, - "edges": edges, - "combos": combos, - } -} - -// loadNode 图数据Node -func (s *ChartGraphImpl) loadNode(v model.ChartGraph) map[string]any { - node := map[string]any{ - "id": v.ID, - "comboId": v.ComboID, - "x": v.X, - "y": v.Y, - "type": v.Type, - "depth": v.Depth, - } - - // 元素样式 - style := map[string]any{} - if len(v.Style) > 7 { - json.Unmarshal([]byte(v.Style), &style) - } - node["style"] = style - - // 元素大小 - if strings.Contains(v.Size, "[") { - sizeArr := []int64{} - json.Unmarshal([]byte(v.Size), &sizeArr) - node["size"] = sizeArr - } else { - node["size"] = parse.Number(v.Size) - } - - // 标签文本 - node["label"] = v.Label - labelCfg := map[string]any{} - if len(v.LabelCfg) > 7 { - json.Unmarshal([]byte(v.LabelCfg), &labelCfg) - } - node["labelCfg"] = labelCfg - - // 三角形属性 - if v.Type == "triangle" { - node["direction"] = v.Direction - } - - // 图片属性 - if strings.Index(v.Type, "image") == 0 { - node["img"] = v.Img - clipCfg := map[string]any{} - if len(v.ClipCfg) > 7 { - json.Unmarshal([]byte(v.ClipCfg), &clipCfg) - } - node["clipCfg"] = clipCfg - } - - // 图标属性 - if v.Icon != "" { - icon := map[string]any{} - if len(v.Icon) > 7 { - json.Unmarshal([]byte(v.Icon), &icon) - } - node["icon"] = icon - } - - return node -} - -// loadEdge 图数据Edge -func (s *ChartGraphImpl) loadEdge(v model.ChartGraph) map[string]any { - edge := map[string]any{ - "id": v.ID, - "source": v.Source, - "target": v.Target, - "type": v.Type, - } - - // 元素样式 - style := map[string]any{} - if len(v.Style) > 7 { - json.Unmarshal([]byte(v.Style), &style) - } - edge["style"] = style - - // 标签文本 - edge["label"] = v.Label - labelCfg := map[string]any{} - if len(v.LabelCfg) > 7 { - json.Unmarshal([]byte(v.LabelCfg), &labelCfg) - } - edge["labelCfg"] = labelCfg - - return edge -} - -// loadCombo 图数据Combo -func (s *ChartGraphImpl) loadCombo(v model.ChartGraph) map[string]any { - combo := map[string]any{ - "id": v.ID, - "x": v.X, - "y": v.Y, - "type": v.Type, - "depth": v.Depth, - } - - // 元素样式 - style := map[string]any{} - if len(v.Style) > 7 { - json.Unmarshal([]byte(v.Style), &style) - } - combo["style"] = style - - // 元素大小 - if strings.Contains(v.Size, "[") { - sizeArr := []int64{} - json.Unmarshal([]byte(v.Size), &sizeArr) - combo["size"] = sizeArr - } else { - combo["size"] = parse.Number(v.Size) - } - - // 元素内边距 - if strings.Contains(v.Padding, "[") { - paddingArr := []int64{} - json.Unmarshal([]byte(v.Padding), &paddingArr) - combo["padding"] = paddingArr - } else { - combo["padding"] = parse.Number(v.Padding) - } - - // 标签文本 - combo["label"] = v.Label - labelCfg := map[string]any{} - if len(v.LabelCfg) > 7 { - json.Unmarshal([]byte(v.LabelCfg), &labelCfg) - } - combo["labelCfg"] = labelCfg - - // 分组内元素 - if v.Children != "" { - children := []map[string]any{} - if len(v.Children) > 7 { - json.Unmarshal([]byte(v.Children), &children) - } - combo["children"] = children - } - - return combo -} - -// SaveData 添加组图数据 -func (s *ChartGraphImpl) SaveData(rowGroup string, data map[string]any) int64 { - graphs := []model.ChartGraph{} - nodes := data["nodes"].([]map[string]any) - graphNodes := s.saveNode(rowGroup, nodes) - graphs = append(graphs, graphNodes...) - edges := data["edges"].([]map[string]any) - graphEdges := s.saveEdge(rowGroup, edges) - graphs = append(graphs, graphEdges...) - combos := data["combos"].([]map[string]any) - graphCombos := s.saveCombo(rowGroup, combos) - graphs = append(graphs, graphCombos...) - // 删除组数据后插入 - if len(graphs) > 0 { - s.graphRepository.DeleteGroup(rowGroup) - } - return s.graphRepository.Inserts(graphs) -} - -// saveNode 图数据Node -func (s *ChartGraphImpl) saveNode(rowGroup string, nodes []map[string]any) []model.ChartGraph { - var graphs []model.ChartGraph - for _, v := range nodes { - node := model.ChartGraph{ - RowType: "node", - RowGroup: rowGroup, - ID: v["id"].(string), - X: v["x"].(float64), - Y: v["y"].(float64), - Type: v["type"].(string), - } - if comboId, ok := v["comboId"]; ok && comboId != nil { - node.ComboID = comboId.(string) - } - if depth, ok := v["depth"]; ok && depth != nil { - node.Depth = int(depth.(float64)) - } - if styleByte, err := json.Marshal(v["style"]); err == nil { - node.Style = string(styleByte) - } - - // 元素大小 - if sizeByte, err := json.Marshal(v["size"]); err == nil { - node.Size = string(sizeByte) - } - - // 标签文本 - if label, ok := v["label"]; ok && label != nil { - node.Label = label.(string) - } - if labelCfgByte, err := json.Marshal(v["labelCfg"]); err == nil { - node.LabelCfg = string(labelCfgByte) - } - // 三角形属性 - if direction, ok := v["direction"]; ok && direction != nil && node.Type == "triangle" { - node.Direction = direction.(string) - } - // 图片属性 - if img, ok := v["img"]; ok && img != nil { - node.Img = img.(string) - if clipCfgByte, err := json.Marshal(v["clipCfg"]); err == nil { - node.ClipCfg = string(clipCfgByte) - } - } - // 图标属性 - if icon, ok := v["icon"]; ok && icon != nil { - if iconByte, err := json.Marshal(icon); err == nil { - node.Icon = string(iconByte) - } - } - - graphs = append(graphs, node) - } - return graphs -} - -// saveEdge 图数据Edge -func (s *ChartGraphImpl) saveEdge(rowGroup string, edges []map[string]any) []model.ChartGraph { - var graphs []model.ChartGraph - for _, v := range edges { - edge := model.ChartGraph{ - RowType: "edge", - RowGroup: rowGroup, - ID: v["id"].(string), - Source: v["source"].(string), - Target: v["target"].(string), - Type: v["type"].(string), - } - - if styleByte, err := json.Marshal(v["style"]); err == nil { - edge.Style = string(styleByte) - } - - // 标签文本 - if label, ok := v["label"]; ok && label != nil { - edge.Label = label.(string) - } - if labelCfgByte, err := json.Marshal(v["labelCfg"]); err == nil { - edge.LabelCfg = string(labelCfgByte) - } - - graphs = append(graphs, edge) - } - return graphs -} - -// saveCombo 图数据Combo -func (s *ChartGraphImpl) saveCombo(rowGroup string, combos []map[string]any) []model.ChartGraph { - var graphs []model.ChartGraph - for _, v := range combos { - combo := model.ChartGraph{ - RowType: "combo", - RowGroup: rowGroup, - ID: v["id"].(string), - X: v["x"].(float64), - Y: v["y"].(float64), - Type: v["type"].(string), - } - if depth, ok := v["depth"]; ok && depth != nil { - combo.Depth = int(depth.(float64)) - } - if styleByte, err := json.Marshal(v["style"]); err == nil { - combo.Style = string(styleByte) - } - if paddingByte, err := json.Marshal(v["padding"]); err == nil { - combo.Padding = string(paddingByte) - } - if childrenByte, err := json.Marshal(v["children"]); err == nil { - combo.Children = string(childrenByte) - } - - // 元素大小 - if sizeByte, err := json.Marshal(v["size"]); err == nil { - combo.Size = string(sizeByte) - } - - // 标签文本 - if label, ok := v["label"]; ok && label != nil { - combo.Label = label.(string) - } - if labelCfgByte, err := json.Marshal(v["labelCfg"]); err == nil { - combo.LabelCfg = string(labelCfgByte) - } - - graphs = append(graphs, combo) - } - return graphs -} - -// Delete 删除所组图数据 -func (s *ChartGraphImpl) DeleteGroup(rowGroup string) int64 { - return s.graphRepository.DeleteGroup(rowGroup) -} diff --git a/src/modules/common/common.go b/src/modules/common/common.go index bb6c68b0..3bbbd97a 100644 --- a/src/modules/common/common.go +++ b/src/modules/common/common.go @@ -52,6 +52,7 @@ func Setup(router *gin.Engine) { // Count: 10, // Type: middleware.LIMIT_IP, // }), + middleware.CryptoApi(true, true), controller.NewAccount.Login, ) indexGroup.GET("/getInfo", middleware.PreAuthorize(nil), controller.NewAccount.Info) @@ -74,6 +75,7 @@ func Setup(router *gin.Engine) { // Count: 10, // Type: middleware.LIMIT_IP, // }), + middleware.CryptoApi(true, true), controller.NewRegister.Register, ) } diff --git a/src/modules/common/controller/account.go b/src/modules/common/controller/account.go index abfba11c..f41d38d6 100644 --- a/src/modules/common/controller/account.go +++ b/src/modules/common/controller/account.go @@ -17,7 +17,7 @@ import ( // 实例化控制层 AccountController 结构体 var NewAccount = &AccountController{ - accountService: commonService.NewAccountImpl, + accountService: commonService.NewAccount, sysLogLoginService: systemService.NewSysLogLoginImpl, } @@ -25,8 +25,7 @@ var NewAccount = &AccountController{ // // PATH / type AccountController struct { - // 账号身份操作服务 - accountService commonService.IAccount + accountService *commonService.Account // 账号身份操作服务 // 系统登录访问 sysLogLoginService systemService.ISysLogLogin } diff --git a/src/modules/common/controller/bootloader.go b/src/modules/common/controller/bootloader.go index daa12daa..0c9f5205 100644 --- a/src/modules/common/controller/bootloader.go +++ b/src/modules/common/controller/bootloader.go @@ -18,7 +18,7 @@ import ( // 实例化控制层 BootloaderController 结构体 var NewBootloader = &BootloaderController{ - accountService: commonService.NewAccountImpl, + accountService: commonService.NewAccount, sysUserService: systemService.NewSysUserImpl, } @@ -26,8 +26,7 @@ var NewBootloader = &BootloaderController{ // // PATH /bootloader type BootloaderController struct { - // 账号身份操作服务 - accountService commonService.IAccount + accountService *commonService.Account // 账号身份操作服务 // 用户信息服务 sysUserService systemService.ISysUser } diff --git a/src/modules/common/service/account.go b/src/modules/common/service/account.go index 48443e3b..dce06a02 100644 --- a/src/modules/common/service/account.go +++ b/src/modules/common/service/account.go @@ -1,24 +1,194 @@ package service -import "be.ems/src/framework/vo" +import ( + "fmt" + "time" -// 账号身份操作服务 服务层接口 -type IAccount interface { - // ValidateCaptcha 校验验证码 - ValidateCaptcha(code, uuid string) error + "be.ems/src/framework/config" + adminConstants "be.ems/src/framework/constants/admin" + "be.ems/src/framework/constants/cachekey" + "be.ems/src/framework/constants/common" + "be.ems/src/framework/redis" + "be.ems/src/framework/utils/crypto" + "be.ems/src/framework/utils/parse" + "be.ems/src/framework/vo" + "be.ems/src/modules/system/model" + systemService "be.ems/src/modules/system/service" +) - // LoginByUsername 登录生成token - LoginByUsername(username, password string) (vo.LoginUser, error) - - // UpdateLoginDateAndIP 更新登录时间和IP - UpdateLoginDateAndIP(loginUser *vo.LoginUser) bool - - // ClearLoginRecordCache 清除错误记录次数 - ClearLoginRecordCache(username string) bool - - // RoleAndMenuPerms 角色和菜单数据权限 - RoleAndMenuPerms(userId string, isAdmin bool) ([]string, []string) - - // RouteMenus 前端路由所需要的菜单 - RouteMenus(userId string, isAdmin bool) []vo.Router +// 实例化服务层 Account 结构体 +var NewAccount = &Account{ + sysUserService: systemService.NewSysUserImpl, + sysConfigService: systemService.NewSysConfigImpl, + sysRoleService: systemService.NewSysRoleImpl, + sysMenuService: systemService.NewSysMenuImpl, +} + +// 账号身份操作服务 服务层处理 +type Account struct { + // 用户信息服务 + sysUserService systemService.ISysUser + // 参数配置服务 + sysConfigService systemService.ISysConfig + // 角色服务 + sysRoleService systemService.ISysRole + // 菜单服务 + sysMenuService systemService.ISysMenu +} + +// ValidateCaptcha 校验验证码 +func (s *Account) ValidateCaptcha(code, uuid string) error { + // 验证码检查,从数据库配置获取验证码开关 true开启,false关闭 + captchaEnabledStr := s.sysConfigService.SelectConfigValueByKey("sys.account.captchaEnabled") + if !parse.Boolean(captchaEnabledStr) { + return nil + } + if code == "" || uuid == "" { + // 验证码信息错误 + return fmt.Errorf("captcha.err") + } + verifyKey := cachekey.CAPTCHA_CODE_KEY + uuid + captcha, _ := redis.Get("", verifyKey) + if captcha == "" { + // 验证码已失效 + return fmt.Errorf("captcha.errValid") + } + redis.Del("", verifyKey) + if captcha != code { + // 验证码错误 + return fmt.Errorf("captcha.err") + } + return nil +} + +// LoginByUsername 登录创建用户信息 +func (s *Account) LoginByUsername(username, password string) (vo.LoginUser, error) { + loginUser := vo.LoginUser{} + + // 检查密码重试次数 + retrykey, retryCount, lockTime, err := s.passwordRetryCount(username) + if err != nil { + return loginUser, err + } + + // 查询用户登录账号 + sysUser := s.sysUserService.SelectUserByUserName(username) + if sysUser.UserName != username { + return loginUser, fmt.Errorf("login.errNameOrPasswd") + } + if sysUser.DelFlag == common.STATUS_YES { + // 对不起,您的账号已被删除 + return loginUser, fmt.Errorf("login.errDelFlag") + } + if sysUser.Status == common.STATUS_NO { + return loginUser, fmt.Errorf("login.errStatus") + } + + // 检验用户密码 + compareBool := crypto.BcryptCompare(password, sysUser.Password) + if !compareBool { + redis.SetByExpire("", retrykey, retryCount+1, lockTime) + // 用户不存在或密码错误 + return loginUser, fmt.Errorf("login.errNameOrPasswd") + } else { + // 清除错误记录次数 + s.ClearLoginRecordCache(username) + } + + // 登录用户信息 + loginUser.UserID = sysUser.UserID + loginUser.DeptID = sysUser.DeptID + loginUser.User = sysUser + // 用户权限组标识 + isAdmin := config.IsAdmin(sysUser.UserID) + if isAdmin { + loginUser.Permissions = []string{adminConstants.PERMISSION} + } else { + perms := s.sysMenuService.SelectMenuPermsByUserId(sysUser.UserID) + loginUser.Permissions = parse.RemoveDuplicates(perms) + } + return loginUser, nil +} + +// UpdateLoginDateAndIP 更新登录时间和IP +func (s *Account) UpdateLoginDateAndIP(loginUser *vo.LoginUser) bool { + sysUser := loginUser.User + userInfo := model.SysUser{ + UserID: sysUser.UserID, + LoginIP: sysUser.LoginIP, + LoginDate: sysUser.LoginDate, + UpdateBy: sysUser.UserName, + Sex: sysUser.Sex, + PhoneNumber: sysUser.PhoneNumber, + Email: sysUser.Email, + Remark: sysUser.Remark, + } + rows := s.sysUserService.UpdateUser(userInfo) + return rows > 0 +} + +// ClearLoginRecordCache 清除错误记录次数 +func (s *Account) ClearLoginRecordCache(username string) bool { + cacheKey := cachekey.PWD_ERR_CNT_KEY + username + hasKey, _ := redis.Has("", cacheKey) + if hasKey { + delOk, _ := redis.Del("", cacheKey) + return delOk + } + return false +} + +// passwordRetryCount 密码重试次数 +func (s *Account) passwordRetryCount(username string) (string, int64, time.Duration, error) { + // 从数据库配置获取登录次数和错误锁定时间 + maxRetryCountStr := s.sysConfigService.SelectConfigValueByKey("sys.user.maxRetryCount") + lockTimeStr := s.sysConfigService.SelectConfigValueByKey("sys.user.lockTime") + + // 验证登录次数和错误锁定时间 + maxRetryCount := parse.Number(maxRetryCountStr) + lockTime := parse.Number(lockTimeStr) + // 验证缓存记录次数 + retrykey := cachekey.PWD_ERR_CNT_KEY + username + retryCount, err := redis.Get("", retrykey) + if retryCount == "" || err != nil { + retryCount = "0" + } + // 是否超过错误值 + retryCountInt64 := parse.Number(retryCount) + if retryCountInt64 >= maxRetryCount { + // 密码输入错误多次,帐户已被锁定 + errorMsg := fmt.Errorf("login.errRetryPasswd") + return retrykey, retryCountInt64, time.Duration(lockTime) * time.Minute, errorMsg + } + return retrykey, retryCountInt64, time.Duration(lockTime) * time.Minute, nil +} + +// RoleAndMenuPerms 角色和菜单数据权限 +func (s *Account) RoleAndMenuPerms(userId string, isAdmin bool) ([]string, []string) { + if isAdmin { + return []string{adminConstants.ROLE_KEY}, []string{adminConstants.PERMISSION} + } else { + // 角色key + roleGroup := []string{} + roles := s.sysRoleService.SelectRoleListByUserId(userId) + for _, role := range roles { + roleGroup = append(roleGroup, role.RoleKey) + } + // 菜单权限key + perms := s.sysMenuService.SelectMenuPermsByUserId(userId) + return parse.RemoveDuplicates(roleGroup), parse.RemoveDuplicates(perms) + } +} + +// RouteMenus 前端路由所需要的菜单 +func (s *Account) RouteMenus(userId string, isAdmin bool) []vo.Router { + var buildMenus []vo.Router + if isAdmin { + menus := s.sysMenuService.SelectMenuTreeByUserId("*") + buildMenus = s.sysMenuService.BuildRouteMenus(menus, "") + } else { + menus := s.sysMenuService.SelectMenuTreeByUserId(userId) + buildMenus = s.sysMenuService.BuildRouteMenus(menus, "") + } + return buildMenus } diff --git a/src/modules/common/service/account.impl.go b/src/modules/common/service/account.impl.go deleted file mode 100644 index b91c8f14..00000000 --- a/src/modules/common/service/account.impl.go +++ /dev/null @@ -1,190 +0,0 @@ -package service - -import ( - "fmt" - "time" - - "be.ems/src/framework/config" - adminConstants "be.ems/src/framework/constants/admin" - "be.ems/src/framework/constants/cachekey" - "be.ems/src/framework/constants/common" - "be.ems/src/framework/redis" - "be.ems/src/framework/utils/crypto" - "be.ems/src/framework/utils/parse" - "be.ems/src/framework/vo" - "be.ems/src/modules/system/model" - systemService "be.ems/src/modules/system/service" -) - -// 实例化服务层 AccountImpl 结构体 -var NewAccountImpl = &AccountImpl{ - sysUserService: systemService.NewSysUserImpl, - sysConfigService: systemService.NewSysConfigImpl, - sysRoleService: systemService.NewSysRoleImpl, - sysMenuService: systemService.NewSysMenuImpl, -} - -// 账号身份操作服务 服务层处理 -type AccountImpl struct { - // 用户信息服务 - sysUserService systemService.ISysUser - // 参数配置服务 - sysConfigService systemService.ISysConfig - // 角色服务 - sysRoleService systemService.ISysRole - // 菜单服务 - sysMenuService systemService.ISysMenu -} - -// ValidateCaptcha 校验验证码 -func (s *AccountImpl) ValidateCaptcha(code, uuid string) error { - // 验证码检查,从数据库配置获取验证码开关 true开启,false关闭 - captchaEnabledStr := s.sysConfigService.SelectConfigValueByKey("sys.account.captchaEnabled") - if !parse.Boolean(captchaEnabledStr) { - return nil - } - if code == "" || uuid == "" { - // 验证码信息错误 - return fmt.Errorf("captcha.err") - } - verifyKey := cachekey.CAPTCHA_CODE_KEY + uuid - captcha, _ := redis.Get("", verifyKey) - if captcha == "" { - // 验证码已失效 - return fmt.Errorf("captcha.errValid") - } - redis.Del("", verifyKey) - if captcha != code { - // 验证码错误 - return fmt.Errorf("captcha.err") - } - return nil -} - -// LoginByUsername 登录创建用户信息 -func (s *AccountImpl) LoginByUsername(username, password string) (vo.LoginUser, error) { - loginUser := vo.LoginUser{} - - // 检查密码重试次数 - retrykey, retryCount, lockTime, err := s.passwordRetryCount(username) - if err != nil { - return loginUser, err - } - - // 查询用户登录账号 - sysUser := s.sysUserService.SelectUserByUserName(username) - if sysUser.UserName != username { - return loginUser, fmt.Errorf("login.errNameOrPasswd") - } - if sysUser.DelFlag == common.STATUS_YES { - // 对不起,您的账号已被删除 - return loginUser, fmt.Errorf("login.errDelFlag") - } - if sysUser.Status == common.STATUS_NO { - return loginUser, fmt.Errorf("login.errStatus") - } - - // 检验用户密码 - compareBool := crypto.BcryptCompare(password, sysUser.Password) - if !compareBool { - redis.SetByExpire("", retrykey, retryCount+1, lockTime) - // 用户不存在或密码错误 - return loginUser, fmt.Errorf("login.errNameOrPasswd") - } else { - // 清除错误记录次数 - s.ClearLoginRecordCache(username) - } - - // 登录用户信息 - loginUser.UserID = sysUser.UserID - loginUser.DeptID = sysUser.DeptID - loginUser.User = sysUser - // 用户权限组标识 - isAdmin := config.IsAdmin(sysUser.UserID) - if isAdmin { - loginUser.Permissions = []string{adminConstants.PERMISSION} - } else { - perms := s.sysMenuService.SelectMenuPermsByUserId(sysUser.UserID) - loginUser.Permissions = parse.RemoveDuplicates(perms) - } - return loginUser, nil -} - -// UpdateLoginDateAndIP 更新登录时间和IP -func (s *AccountImpl) UpdateLoginDateAndIP(loginUser *vo.LoginUser) bool { - sysUser := loginUser.User - userInfo := model.SysUser{ - UserID: sysUser.UserID, - LoginIP: sysUser.LoginIP, - LoginDate: sysUser.LoginDate, - UpdateBy: sysUser.UserName, - } - rows := s.sysUserService.UpdateUser(userInfo) - return rows > 0 -} - -// ClearLoginRecordCache 清除错误记录次数 -func (s *AccountImpl) ClearLoginRecordCache(username string) bool { - cacheKey := cachekey.PWD_ERR_CNT_KEY + username - hasKey, _ := redis.Has("", cacheKey) - if hasKey { - delOk, _ := redis.Del("", cacheKey) - return delOk - } - return false -} - -// passwordRetryCount 密码重试次数 -func (s *AccountImpl) passwordRetryCount(username string) (string, int64, time.Duration, error) { - // 从数据库配置获取登录次数和错误锁定时间 - maxRetryCountStr := s.sysConfigService.SelectConfigValueByKey("sys.user.maxRetryCount") - lockTimeStr := s.sysConfigService.SelectConfigValueByKey("sys.user.lockTime") - - // 验证登录次数和错误锁定时间 - maxRetryCount := parse.Number(maxRetryCountStr) - lockTime := parse.Number(lockTimeStr) - // 验证缓存记录次数 - retrykey := cachekey.PWD_ERR_CNT_KEY + username - retryCount, err := redis.Get("", retrykey) - if retryCount == "" || err != nil { - retryCount = "0" - } - // 是否超过错误值 - retryCountInt64 := parse.Number(retryCount) - if retryCountInt64 >= maxRetryCount { - // 密码输入错误多次,帐户已被锁定 - errorMsg := fmt.Errorf("login.errRetryPasswd") - return retrykey, retryCountInt64, time.Duration(lockTime) * time.Minute, errorMsg - } - return retrykey, retryCountInt64, time.Duration(lockTime) * time.Minute, nil -} - -// RoleAndMenuPerms 角色和菜单数据权限 -func (s *AccountImpl) RoleAndMenuPerms(userId string, isAdmin bool) ([]string, []string) { - if isAdmin { - return []string{adminConstants.ROLE_KEY}, []string{adminConstants.PERMISSION} - } else { - // 角色key - roleGroup := []string{} - roles := s.sysRoleService.SelectRoleListByUserId(userId) - for _, role := range roles { - roleGroup = append(roleGroup, role.RoleKey) - } - // 菜单权限key - perms := s.sysMenuService.SelectMenuPermsByUserId(userId) - return parse.RemoveDuplicates(roleGroup), parse.RemoveDuplicates(perms) - } -} - -// RouteMenus 前端路由所需要的菜单 -func (s *AccountImpl) RouteMenus(userId string, isAdmin bool) []vo.Router { - var buildMenus []vo.Router - if isAdmin { - menus := s.sysMenuService.SelectMenuTreeByUserId("*") - buildMenus = s.sysMenuService.BuildRouteMenus(menus, "") - } else { - menus := s.sysMenuService.SelectMenuTreeByUserId(userId) - buildMenus = s.sysMenuService.BuildRouteMenus(menus, "") - } - return buildMenus -} diff --git a/src/modules/crontask/processor/exportTable/exportTable.go b/src/modules/crontask/processor/exportTable/exportTable.go new file mode 100644 index 00000000..e0f8a986 --- /dev/null +++ b/src/modules/crontask/processor/exportTable/exportTable.go @@ -0,0 +1,160 @@ +package exportTable + +import ( + "database/sql" + "encoding/csv" + "encoding/json" + "fmt" + "os" + "time" + + "be.ems/lib/dborm" + "be.ems/lib/log" + "be.ems/src/framework/cron" +) + +var NewProcessor = &BarProcessor{ + progress: 0, + count: 0, +} + +// bar 队列任务处理 +type BarProcessor struct { + // 任务进度 + progress int + // 执行次数 + count int +} + +type BarParams struct { + Duration int `json:"duration"` + TableName string `json:"tableName"` + Columns string `json:"columns"` // exported column name of time string + TimeCol string `json:"timeCol"` // time stamp of column name + TimeUnit string `json:"timeUnit"` // timestamp unit: second/micro/milli + Extras string `json:"extras"` // extras condition for where + FilePath string `json:"filePath"` // file path +} + +func (s *BarProcessor) Execute(data any) (any, error) { + s.count++ + + options := data.(cron.JobData) + sysJob := options.SysJob + var params BarParams + + err := json.Unmarshal([]byte(sysJob.TargetParams), ¶ms) + if err != nil { + return nil, err + } + + // mkdir if not exist + if _, err = os.Stat(params.FilePath); os.IsNotExist(err) { + err = os.MkdirAll(params.FilePath, os.ModePerm) + if err != nil { + log.Error("Failed to Mkdir:", err) + return nil, err + } + } + //duration = params.Duration + + now := time.Now() + end := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location()) + start := end.Add(-time.Duration(params.Duration) * time.Hour) + + var startTime, endTime int64 + switch params.TimeUnit { + case "second": + // 格式化时间戳为秒级 + startTime = start.Unix() + endTime = end.Unix() + case "milli": + // 格式化时间戳为毫秒级 + startTime = start.UnixMilli() + endTime = end.UnixMilli() + case "micro": + // 格式化时间戳为微妙级 + startTime = start.UnixMicro() + endTime = end.UnixMicro() + default: + return nil, fmt.Errorf("error input parameter") + } + var query string + if params.Extras != "" { + query = fmt.Sprintf("SELECT %s FROM `%s` WHERE `%s` >= %d AND `%s` < %d AND %s", + params.Columns, params.TableName, params.TimeCol, startTime, params.TimeCol, endTime, params.Extras) + } else { + query = fmt.Sprintf("SELECT %s FROM `%s` WHERE `%s` >= %d AND `%s` < %d", + params.Columns, params.TableName, params.TimeCol, startTime, params.TimeCol, endTime) + } + log.Trace("query:", query) + filePath := fmt.Sprintf("%s/%s_export_%s.csv", params.FilePath, params.TableName, time.Now().Format("20060102150405")) + affected, err := s.exportData(query, filePath) + if err != nil { + return nil, err + } + + // 返回结果,用于记录执行结果 + return map[string]any{ + "msg": "sucess", + "filePath": filePath, + "affected": affected, + }, nil +} + +func (s *BarProcessor) exportData(query, filePath string) (int64, error) { + rows, err := dborm.XCoreDB().Query(query) + if err != nil { + return 0, err + } + defer rows.Close() + + // 创建 CSV 文件 + file, err := os.Create(filePath) + if err != nil { + return 0, err + } + defer file.Close() + + writer := csv.NewWriter(file) + defer writer.Flush() + + // 写入表头 + columns, _ := rows.ColumnTypes() + header := make([]string, len(columns)) + for i, col := range columns { + header[i] = col.Name() + } + if err := writer.Write(header); err != nil { + return 0, err + } + + // 写入数据 + var affected int64 = 0 + for rows.Next() { + values := make([]sql.RawBytes, len(columns)) + scanArgs := make([]interface{}, len(columns)) + for i := range values { + scanArgs[i] = &values[i] + } + + if err := rows.Scan(scanArgs...); err != nil { + return 0, err + } + + record := make([]string, len(columns)) + for i, val := range values { + if val == nil { + record[i] = "" + } else { + record[i] = string(val) + } + } + affected++ + if err := writer.Write(record); err != nil { + return affected, err + } + } + + return affected, nil +} diff --git a/src/modules/crontask/processor/monitor_sys_resource/monitor_sys_resource.go b/src/modules/crontask/processor/monitor_sys_resource/monitor_sys_resource.go index 9b1187bf..401d0233 100644 --- a/src/modules/crontask/processor/monitor_sys_resource/monitor_sys_resource.go +++ b/src/modules/crontask/processor/monitor_sys_resource/monitor_sys_resource.go @@ -10,7 +10,7 @@ import ( ) var NewProcessor = &MonitorSysResourceProcessor{ - monitorService: monitorService.NewMonitorImpl, + monitorService: monitorService.NewMonitor, count: 0, openDataCancel: false, } @@ -18,7 +18,7 @@ var NewProcessor = &MonitorSysResourceProcessor{ // MonitorSysResourceProcessor 系统资源CPU/IO/Netword收集 type MonitorSysResourceProcessor struct { // 服务器系统相关信息服务 - monitorService monitorService.IMonitor + monitorService *monitorService.Monitor // 执行次数 count int // 是否已经开启数据通道 diff --git a/src/modules/crontask/processor/ne_config_backup/ne_config_backup.go b/src/modules/crontask/processor/ne_config_backup/ne_config_backup.go index 8263e19d..bb36135c 100644 --- a/src/modules/crontask/processor/ne_config_backup/ne_config_backup.go +++ b/src/modules/crontask/processor/ne_config_backup/ne_config_backup.go @@ -11,19 +11,16 @@ import ( ) var NewProcessor = &NeConfigBackupProcessor{ - neConfigBackupService: neService.NewNeConfigBackupImpl, - neInfoService: neService.NewNeInfoImpl, + neConfigBackupService: neService.NewNeConfigBackup, + neInfoService: neService.NewNeInfo, count: 0, } // NeConfigBackupProcessor 网元配置文件定期备份 type NeConfigBackupProcessor struct { - // 网元配置文件备份记录服务 - neConfigBackupService neService.INeConfigBackup - // 网元信息服务 - neInfoService neService.INeInfo - // 执行次数 - count int + neConfigBackupService *neService.NeConfigBackup // 网元配置文件备份记录服务 + neInfoService *neService.NeInfo // 网元信息服务 + count int // 执行次数 } func (s *NeConfigBackupProcessor) Execute(data any) (any, error) { diff --git a/src/modules/crontask/processor/ne_data_udm/ne_data_udm.go b/src/modules/crontask/processor/ne_data_udm/ne_data_udm.go new file mode 100644 index 00000000..ae583ae9 --- /dev/null +++ b/src/modules/crontask/processor/ne_data_udm/ne_data_udm.go @@ -0,0 +1,45 @@ +package ne_data_udm + +import ( + "fmt" + + "be.ems/src/framework/cron" + "be.ems/src/framework/logger" + neDataService "be.ems/src/modules/network_data/service" + neModel "be.ems/src/modules/network_element/model" + neService "be.ems/src/modules/network_element/service" +) + +var NewProcessor = &NeDataUDM{ + udmAuthService: neDataService.NewUDMAuthUser, + udmSubService: neDataService.NewUDMSubUser, + neInfoService: neService.NewNeInfo, + count: 0, +} + +// NeDataUDM 网元配置文件定期备份 +type NeDataUDM struct { + udmAuthService *neDataService.UDMAuthUser // UDM鉴权信息 + udmSubService *neDataService.UDMSubUser // UDM签约信息 + neInfoService *neService.NeInfo // 网元信息服务 + count int // 执行次数 +} + +func (s *NeDataUDM) Execute(data any) (any, error) { + s.count++ // 执行次数加一 + options := data.(cron.JobData) + sysJob := options.SysJob + logger.Infof("重复 %v 任务ID %s", options.Repeat, sysJob.JobID) + // 返回结果,用于记录执行结果 + result := map[string]any{ + "count": s.count, + } + + neList := s.neInfoService.SelectList(neModel.NeInfo{NeType: "UDM"}, false, false) + for _, neInfo := range neList { + result[fmt.Sprintf("AuthNumber_%s", neInfo.NeId)] = s.udmAuthService.ResetData(neInfo.NeId) + result[fmt.Sprintf("SubNumber_%s", neInfo.NeId)] = s.udmSubService.ResetData(neInfo.NeId) + } + + return result, nil +} diff --git a/src/modules/crontask/processor/processor.go b/src/modules/crontask/processor/processor.go index 81ce9de1..9e41b045 100644 --- a/src/modules/crontask/processor/processor.go +++ b/src/modules/crontask/processor/processor.go @@ -5,10 +5,13 @@ import ( "be.ems/src/modules/crontask/processor/backupEtcFromNE" "be.ems/src/modules/crontask/processor/delExpiredNeBackup" "be.ems/src/modules/crontask/processor/deleteExpiredRecord" + "be.ems/src/modules/crontask/processor/exportTable" "be.ems/src/modules/crontask/processor/genNeStateAlarm" "be.ems/src/modules/crontask/processor/getStateFromNE" processorMonitorSysResource "be.ems/src/modules/crontask/processor/monitor_sys_resource" processorNeConfigBackup "be.ems/src/modules/crontask/processor/ne_config_backup" + processorNeDataUDM "be.ems/src/modules/crontask/processor/ne_data_udm" + "be.ems/src/modules/crontask/processor/removeFile" ) // InitCronQueue 初始定时任务队列 @@ -17,10 +20,14 @@ func InitCronQueue() { cron.CreateQueue("monitor_sys_resource", processorMonitorSysResource.NewProcessor) // 网元-网元配置文件定期备份 cron.CreateQueue("ne_config_backup", processorNeConfigBackup.NewProcessor) + // 网元数据-UDM数据刷新同步 + cron.CreateQueue("ne_data_udm", processorNeDataUDM.NewProcessor) // delete expired NE backup file cron.CreateQueue("delExpiredNeBackup", delExpiredNeBackup.NewProcessor) cron.CreateQueue("deleteExpiredRecord", deleteExpiredRecord.NewProcessor) cron.CreateQueue("backupEtcFromNE", backupEtcFromNE.NewProcessor) cron.CreateQueue("getStateFromNE", getStateFromNE.NewProcessor) cron.CreateQueue("genNeStateAlarm", genNeStateAlarm.NewProcessor) + cron.CreateQueue("exportTable", exportTable.NewProcessor) + cron.CreateQueue("removeFile", removeFile.NewProcessor) } diff --git a/src/modules/crontask/processor/removeFile/removeFile.go b/src/modules/crontask/processor/removeFile/removeFile.go new file mode 100644 index 00000000..5420b40b --- /dev/null +++ b/src/modules/crontask/processor/removeFile/removeFile.go @@ -0,0 +1,159 @@ +package removeFile + +import ( + "encoding/json" + "os" + "path/filepath" + "sort" + "time" + + "be.ems/lib/log" + "be.ems/src/framework/cron" +) + +var NewProcessor = &BarProcessor{ + progress: 0, + count: 0, +} + +// bar 队列任务处理 +type BarProcessor struct { + // 任务进度 + progress int + // 执行次数 + count int +} + +type BarParams struct { + FilePath string `json:"filePath"` // file path + MaxDays int `json:"maxDays"` + MaxFiles *int `json:"maxFiles"` // keep max files + MaxSize *int64 `json:"maxSize"` + Extras string `json:"extras"` // extras condition for where +} + +type FileInfo struct { + Path string + Info os.FileInfo +} + +func (s *BarProcessor) Execute(data any) (any, error) { + s.count++ + + options := data.(cron.JobData) + sysJob := options.SysJob + var params []BarParams + + err := json.Unmarshal([]byte(sysJob.TargetParams), ¶ms) + if err != nil { + return nil, err + } + result := []map[string]any{} + for _, param := range params { + res, _ := s.ExecuteOne(param) + result = append(result, res) + } + + // 返回结果,用于记录执行结果 + return map[string]any{ + "result": result, + }, nil +} + +func (s *BarProcessor) ExecuteOne(params BarParams) (map[string]any, error) { + var maxFiles int = 0 + var maxSize int64 = 0 + if params.MaxFiles != nil { + maxFiles = *params.MaxFiles + } + if params.MaxSize != nil { + maxSize = int64(*params.MaxSize * 1024 * 1024) + } + files, err := getFiles(params.FilePath) + if err != nil { + return map[string]any{ + "msg": "failed", + "err": err.Error(), + }, err + } + + // 获取本地时区 + loc, err := time.LoadLocation("Local") + if err != nil { + return map[string]any{ + "msg": "failed", + "err": err.Error(), + }, err + } + cutoff := time.Now().In(loc).AddDate(0, 0, -params.MaxDays) + + var oldFiles []FileInfo + for _, file := range files { + if file.Info.ModTime().Before(cutoff) { + oldFiles = append(oldFiles, file) + } + } + + // 按修改时间排序文件(最旧的在前) + sort.Slice(oldFiles, func(i, j int) bool { + return oldFiles[i].Info.ModTime().Before(oldFiles[j].Info.ModTime()) + }) + + deleted, errorDel := 0, 0 + + // 删除文件,直到满足文件总数不超过maxFiles个且总大小不超过maxSize的条件 + var totalSize int64 + for i, file := range oldFiles { + if (maxFiles > 0 && i >= maxFiles) || (maxSize > 0 && totalSize+file.Info.Size() > maxSize) { + break + } + err := os.Remove(file.Path) + if err != nil { + log.Error("Error deleting file:", file.Path, err) + errorDel++ + continue + } + totalSize += file.Info.Size() + deleted++ + } + + // 如果仍然有超过maxFiles个文件或总大小超过maxSize,继续删除最旧的文件 + remainingFiles := files + sort.Slice(remainingFiles, func(i, j int) bool { + return remainingFiles[i].Info.ModTime().Before(remainingFiles[j].Info.ModTime()) + }) + + for (maxFiles > 0 && len(remainingFiles) > maxFiles) || (maxSize > 0 && totalSize > maxSize) { + file := remainingFiles[0] + err := os.Remove(file.Path) + if err != nil { + log.Error("Error deleting file:", file.Path, err) + remainingFiles = remainingFiles[1:] + continue + } + totalSize -= file.Info.Size() + remainingFiles = remainingFiles[1:] + } + + // 返回结果,用于记录执行结果 + return map[string]any{ + "msg": "successed", + "filePath": params.FilePath, + "deleted": deleted, + "errorDel": errorDel, + }, nil +} + +func getFiles(dir string) ([]FileInfo, error) { + var files []FileInfo + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + files = append(files, FileInfo{Path: path, Info: info}) + } + return nil + }) + return files, err +} diff --git a/src/modules/monitor/controller/monitor.go b/src/modules/monitor/controller/monitor.go index 52e90373..17f44dd5 100644 --- a/src/modules/monitor/controller/monitor.go +++ b/src/modules/monitor/controller/monitor.go @@ -11,15 +11,14 @@ import ( // 实例化控制层 MonitorInfoController 结构体 var NewMonitor = &MonitorController{ - monitorService: service.NewMonitorImpl, + monitorService: service.NewMonitor, } // 服务器资源监控信息 // // PATH /monitor type MonitorController struct { - // 服务器系统相关信息服务 - monitorService service.IMonitor + monitorService *service.Monitor // 服务器系统相关信息服务 } // 资源监控信息加载 @@ -28,21 +27,14 @@ type MonitorController struct { func (s *MonitorController) Load(c *gin.Context) { language := ctx.AcceptLanguage(c) var querys struct { - // 数据类型all/load/cpu/memory/io/network - Type string `form:"type" binding:"required,oneof=all load cpu memory io network"` - // 开始时间 - StartTime int64 `form:"startTime" binding:"required"` - // 结束时间 - EndTime int64 `form:"endTime" binding:"required"` - // 网元类型 - NeType string `form:"neType"` - // 网元ID - NeID string `form:"neId"` - // 名称,networ和iok时有效 - Name string `form:"name"` + Type string `form:"type" binding:"required,oneof=all load cpu memory io network"` // 数据类型all/load/cpu/memory/io/network + StartTime int64 `form:"startTime" binding:"required"` // 开始时间 + EndTime int64 `form:"endTime" binding:"required"` // 结束时间 + NeType string `form:"neType"` // 网元类型 + NeID string `form:"neId"` // 网元ID + Name string `form:"name"` // 名称,networ和io时有效 } - err := c.ShouldBindQuery(&querys) - if err != nil { + if err := c.ShouldBindQuery(&querys); err != nil { c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) return } diff --git a/src/modules/monitor/controller/sys_cache.go b/src/modules/monitor/controller/sys_cache.go index 4c544efd..4baa442b 100644 --- a/src/modules/monitor/controller/sys_cache.go +++ b/src/modules/monitor/controller/sys_cache.go @@ -141,10 +141,8 @@ func (s *SysCacheController) ClearCacheKey(c *gin.Context) { func (s *SysCacheController) ClearCacheSafe(c *gin.Context) { language := ctx.AcceptLanguage(c) caches := []model.SysCache{ - model.NewSysCacheNames(i18n.TKey(language, "cache.name.user"), cachekey.LOGIN_TOKEN_KEY), model.NewSysCacheNames(i18n.TKey(language, "cache.name.sys_config"), cachekey.SYS_CONFIG_KEY), model.NewSysCacheNames(i18n.TKey(language, "cache.name.sys_dict"), cachekey.SYS_DICT_KEY), - model.NewSysCacheNames(i18n.TKey(language, "cache.name.captcha_codes"), cachekey.CAPTCHA_CODE_KEY), model.NewSysCacheNames(i18n.TKey(language, "cache.name.repeat_submit"), cachekey.REPEAT_SUBMIT_KEY), model.NewSysCacheNames(i18n.TKey(language, "cache.name.rate_limit"), cachekey.RATE_LIMIT_KEY), model.NewSysCacheNames(i18n.TKey(language, "cache.name.pwd_err_cnt"), cachekey.PWD_ERR_CNT_KEY), diff --git a/src/modules/monitor/controller/sys_job.go b/src/modules/monitor/controller/sys_job.go index 2420e553..07868c8b 100644 --- a/src/modules/monitor/controller/sys_job.go +++ b/src/modules/monitor/controller/sys_job.go @@ -22,18 +22,16 @@ import ( // 实例化控制层 SysJobLogController 结构体 var NewSysJob = &SysJobController{ - sysJobService: service.NewSysJobImpl, - sysDictDataService: systemService.NewSysDictDataImpl, + sysJobService: service.NewSysJob, + sysDictDataService: systemService.NewSysDictData, } // 调度任务信息 // // PATH /monitor/job type SysJobController struct { - // 调度任务服务 - sysJobService service.ISysJob - // 字典数据服务 - sysDictDataService systemService.ISysDictData + sysJobService *service.SysJob // 调度任务服务 + sysDictDataService *systemService.SysDictData // 字典数据服务 } // 调度任务列表 diff --git a/src/modules/monitor/controller/sys_job_log.go b/src/modules/monitor/controller/sys_job_log.go index 4705700e..9401a28d 100644 --- a/src/modules/monitor/controller/sys_job_log.go +++ b/src/modules/monitor/controller/sys_job_log.go @@ -21,18 +21,16 @@ import ( // 实例化控制层 SysJobLogController 结构体 var NewSysJobLog = &SysJobLogController{ - sysJobLogService: service.NewSysJobLogImpl, - sysDictDataService: systemService.NewSysDictDataImpl, + sysJobLogService: service.NewSysJobLog, + sysDictDataService: systemService.NewSysDictData, } // 调度任务日志信息 // // PATH /monitor/jobLog type SysJobLogController struct { - // 调度任务日志服务 - sysJobLogService service.ISysJobLog - // 字典数据服务 - sysDictDataService systemService.ISysDictData + sysJobLogService *service.SysJobLog // 调度任务日志服务 + sysDictDataService *systemService.SysDictData // 字典数据服务 } // 调度任务日志列表 @@ -44,7 +42,7 @@ func (s *SysJobLogController) List(c *gin.Context) { querys := ctx.QueryMap(c) // 任务ID优先级更高 if v, ok := querys["jobId"]; ok && v != nil { - jobInfo := service.NewSysJobImpl.SelectJobById(v.(string)) + jobInfo := service.NewSysJob.SelectJobById(v.(string)) querys["jobName"] = jobInfo.JobName querys["jobGroup"] = jobInfo.JobGroup } diff --git a/src/modules/monitor/controller/sys_user_online.go b/src/modules/monitor/controller/sys_user_online.go index 1212b7eb..072cad32 100644 --- a/src/modules/monitor/controller/sys_user_online.go +++ b/src/modules/monitor/controller/sys_user_online.go @@ -6,7 +6,6 @@ import ( "strings" "be.ems/src/framework/constants/cachekey" - "be.ems/src/framework/constants/roledatascope" "be.ems/src/framework/i18n" "be.ems/src/framework/redis" "be.ems/src/framework/utils/ctx" @@ -20,15 +19,14 @@ import ( // 实例化控制层 SysUserOnlineController 结构体 var NewSysUserOnline = &SysUserOnlineController{ - sysUserOnlineService: service.NewSysUserOnlineImpl, + sysUserOnlineService: service.NewSysUserOnline, } // 在线用户监控 // // PATH /monitor/online type SysUserOnlineController struct { - // 在线用户服务 - sysUserOnlineService service.ISysUserOnline + sysUserOnlineService *service.SysUserOnline // 在线用户服务 } // 在线用户列表 @@ -38,43 +36,6 @@ func (s *SysUserOnlineController) List(c *gin.Context) { language := ctx.AcceptLanguage(c) ipaddr := c.Query("ipaddr") userName := c.Query("userName") - data := map[string]any{ - "total": 0, - "rows": []model.SysUserOnline{}, - } - - // 当前登录用户 - currentUser, _ := ctx.LoginUser(c) - currentRoleKey := "" - currentRoleDataScope := "" - if len(currentUser.User.Roles) > 0 { - role := currentUser.User.Roles[0] - currentRoleKey = role.RoleKey - currentRoleDataScope = role.DataScope - } - - if currentRoleKey == "" { - c.JSON(200, result.Ok(data)) - return - } - hasRoleDataScope := func(loginUser vo.LoginUser) bool { - if currentRoleDataScope == roledatascope.ALL { - return true - } - if currentRoleDataScope == roledatascope.CUSTOM { - return false - } - if currentRoleDataScope == roledatascope.DEPT && loginUser.DeptID == currentUser.DeptID { - return true - } - if currentRoleDataScope == roledatascope.DEPT_AND_CHILD && (strings.Contains(loginUser.User.Dept.Ancestors, currentUser.DeptID) || currentUser.DeptID == loginUser.DeptID) { - return true - } - if currentRoleDataScope == roledatascope.SELF && loginUser.UserID == currentUser.UserID { - return true - } - return false - } // 获取所有在线用户key keys, _ := redis.GetKeys("", cachekey.LOGIN_TOKEN_KEY+"*") @@ -106,10 +67,6 @@ func (s *SysUserOnlineController) List(c *gin.Context) { continue } - if !hasRoleDataScope(loginUser) { - continue - } - onlineUser := s.sysUserOnlineService.LoginUserToUserOnline(loginUser) if onlineUser.TokenID != "" { onlineUser.LoginLocation = i18n.TKey(language, onlineUser.LoginLocation) @@ -146,9 +103,10 @@ func (s *SysUserOnlineController) List(c *gin.Context) { return filteredUserOnlines[j].LoginTime > filteredUserOnlines[i].LoginTime }) - data["total"] = len(filteredUserOnlines) - data["rows"] = filteredUserOnlines - c.JSON(200, result.Ok(data)) + c.JSON(200, result.Ok(map[string]any{ + "total": len(filteredUserOnlines), + "rows": filteredUserOnlines, + })) } // 在线用户强制退出 diff --git a/src/modules/monitor/controller/system_info.go b/src/modules/monitor/controller/system_info.go index a6d47764..14437dda 100644 --- a/src/modules/monitor/controller/system_info.go +++ b/src/modules/monitor/controller/system_info.go @@ -9,27 +9,27 @@ import ( // 实例化控制层 SystemInfoController 结构体 var NewSystemInfo = &SystemInfoController{ - systemInfogService: service.NewSystemInfoImpl, + systemInfogService: service.NewSystemInfo, } // 服务器监控信息 // // PATH /monitor/system-info type SystemInfoController struct { - // 服务器系统相关信息服务 - systemInfogService service.ISystemInfo + systemInfogService *service.SystemInfo // 服务器系统相关信息服务 } // 服务器信息 // // GET / func (s *SystemInfoController) Info(c *gin.Context) { - c.JSON(200, result.OkData(map[string]any{ + data := map[string]any{ "cpu": s.systemInfogService.CPUInfo(), "memory": s.systemInfogService.MemoryInfo(), "network": s.systemInfogService.NetworkInfo(), "time": s.systemInfogService.TimeInfo(), "system": s.systemInfogService.SystemInfo(), "disk": s.systemInfogService.DiskInfo(), - })) + } + c.JSON(200, result.OkData(data)) } diff --git a/src/modules/monitor/model/monitor_base.go b/src/modules/monitor/model/monitor_base.go index 7669afa2..34e056fa 100644 --- a/src/modules/monitor/model/monitor_base.go +++ b/src/modules/monitor/model/monitor_base.go @@ -2,28 +2,19 @@ package model // MonitorBase 监控_基本信息 monitor_base type MonitorBase struct { - // id - ID int64 `json:"id" gorm:"primaryKey"` - // 创建时间 - CreateTime int64 `json:"createTime"` - // cpu使用率 - CPU float64 `json:"cpu"` - // cpu平均使用率 - LoadUsage float64 `json:"loadUsage"` - // cpu使用1分钟 - CPULoad1 float64 `json:"cpuLoad1"` - // cpu使用5分钟 - CPULoad5 float64 `json:"cpuLoad5"` - // cpu使用15分钟 - CPULoad15 float64 `json:"cpuLoad15"` - // 内存使用率 - Memory float64 `json:"memory"` - // 网元ID - NeType string `json:"neType"` - // 网元类型 - NeID string `json:"neId"` + ID int64 `json:"id" gorm:"column:id;primaryKey;autoIncrement"` + CreateTime int64 `json:"createTime" gorm:"create_time"` // 创建时间 + CPU float64 `json:"cpu" gorm:"cpu"` // cpu使用率 + LoadUsage float64 `json:"loadUsage" gorm:"load_usage"` // cpu平均使用率 + CPULoad1 float64 `json:"cpuLoad1" gorm:"cpu_load1"` // cpu使用1分钟 + CPULoad5 float64 `json:"cpuLoad5" gorm:"cpu_load5"` // cpu使用5分钟 + CPULoad15 float64 `json:"cpuLoad15" gorm:"cpu_load15"` // cpu使用15分钟 + Memory float64 `json:"memory" gorm:"memory"` // 内存使用率 + NeType string `json:"neType" gorm:"ne_type"` // 网元类型 + NeID string `json:"neId" gorm:"ne_id"` // 网元ID } -func (MonitorBase) TableName() string { +// TableName 表名称 +func (*MonitorBase) TableName() string { return "monitor_base" } diff --git a/src/modules/monitor/model/monitor_io.go b/src/modules/monitor/model/monitor_io.go index cc52b728..962bfc86 100644 --- a/src/modules/monitor/model/monitor_io.go +++ b/src/modules/monitor/model/monitor_io.go @@ -2,26 +2,18 @@ package model // MonitorIO 监控_磁盘IO monitor_io type MonitorIO struct { - // id - ID int64 `json:"id" gorm:"primaryKey"` - // 创建时间 - CreateTime int64 `json:"createTime"` - // 磁盘名 - Name string `json:"name"` - // 读取K - Read int64 `json:"read"` - // 写入K - Write int64 `json:"write"` - // 次数 - Count int64 `json:"count"` - // 耗时 - Time int64 `json:"time"` - // 网元ID - NeType string `json:"neType"` - // 网元类型 - NeID string `json:"neId"` + ID int64 `json:"id" gorm:"column:id;primaryKey;autoIncrement"` + CreateTime int64 `json:"createTime" gorm:"create_time"` // 创建时间 + Name string `json:"name" gorm:"name"` // 磁盘名 + Read int64 `json:"read" gorm:"read"` // 读取K + Write int64 `json:"write" gorm:"write"` // 写入K + Count int64 `json:"count" gorm:"count"` // 读写次数 + Time int64 `json:"time" gorm:"time"` // 读写延迟 + NeType string `json:"neType" gorm:"ne_type"` // 网元类型 + NeID string `json:"neId" gorm:"ne_id"` // 网元ID } -func (MonitorIO) TableName() string { +// TableName 表名称 +func (*MonitorIO) TableName() string { return "monitor_io" } diff --git a/src/modules/monitor/model/monitor_network.go b/src/modules/monitor/model/monitor_network.go index a2063f18..feed747e 100644 --- a/src/modules/monitor/model/monitor_network.go +++ b/src/modules/monitor/model/monitor_network.go @@ -2,22 +2,16 @@ package model // MonitorNetwork 监控_网络IO monitor_network type MonitorNetwork struct { - // id - ID int64 `json:"id" gorm:"primaryKey"` - // 创建时间 - CreateTime int64 `json:"createTime"` - // 网卡名 - Name string `json:"name"` - // 上行 - Up float64 `json:"up"` - // 下行 - Down float64 `json:"down"` - // 网元ID 本机#号 - NeType string `json:"neType"` - // 网元类型 本机#号 - NeID string `json:"neId"` + ID int64 `json:"id" gorm:"column:id;primaryKey;autoIncrement"` + CreateTime int64 `json:"createTime" gorm:"create_time"` // 创建时间 + Name string `json:"name" gorm:"name"` // 网卡名 + Up float64 `json:"up" gorm:"up"` // 上行 + Down float64 `json:"down" gorm:"down"` // 下行 + NeType string `json:"neType" gorm:"ne_type"` // 网元类型 + NeID string `json:"neId" gorm:"ne_id"` // 网元ID } -func (MonitorNetwork) TableName() string { +// TableName 表名称 +func (*MonitorNetwork) TableName() string { return "monitor_network" } diff --git a/src/modules/monitor/monitor.go b/src/modules/monitor/monitor.go index 67fa63e2..b71bbfa4 100644 --- a/src/modules/monitor/monitor.go +++ b/src/modules/monitor/monitor.go @@ -165,5 +165,5 @@ func InitLoad() { // 初始化定时任务处理 processor.InitCronQueue() // 启动时,初始化调度任务 - service.NewSysJobImpl.ResetQueueJob() + service.NewSysJob.ResetQueueJob() } diff --git a/src/modules/monitor/service/monitor.go b/src/modules/monitor/service/monitor.go index 5e836cc8..285a265b 100644 --- a/src/modules/monitor/service/monitor.go +++ b/src/modules/monitor/service/monitor.go @@ -1,14 +1,274 @@ package service -// IMonitor 服务器系统相关信息 服务层接口 -type IMonitor interface { - // RunMonitor 执行资源监控 - RunMonitor() +import ( + "context" + "strconv" + "time" - // RunMonitorDataCancel 启动资源监控数据存储io/network通道 移除之前的chan上下文后在设置新的均值 - // interval 采集的平均值(分钟) - RunMonitorDataCancel(removeBefore bool, interval float64) + "be.ems/src/framework/logger" + "be.ems/src/modules/monitor/model" + "be.ems/src/modules/monitor/repository" + systemService "be.ems/src/modules/system/service" + "github.com/shirou/gopsutil/v4/cpu" + "github.com/shirou/gopsutil/v4/disk" + "github.com/shirou/gopsutil/v4/load" + "github.com/shirou/gopsutil/v4/mem" + "github.com/shirou/gopsutil/v4/net" +) - // SelectMonitorInfo 查询监控资源信息 - SelectMonitorInfo(query map[string]any) map[string]any +// 实例化服务层 Monitor 结构体 +var NewMonitor = &Monitor{ + sysConfigService: systemService.NewSysConfigImpl, + monitorRepository: repository.NewMonitorImpl, + diskIO: make(chan []disk.IOCountersStat, 2), + netIO: make(chan []net.IOCountersStat, 2), +} + +// Monitor 服务器系统相关信息 服务层处理 +type Monitor struct { + // 参数配置服务 + sysConfigService systemService.ISysConfig + // 监控服务资源数据信息 + monitorRepository repository.IMonitor + // 磁盘网络IO 数据通道 + diskIO chan ([]disk.IOCountersStat) + netIO chan ([]net.IOCountersStat) +} + +// RunMonitor 执行资源监控 +func (s *Monitor) RunMonitor() { + var itemBase model.MonitorBase + itemBase.CreateTime = time.Now().UnixMilli() + itemBase.NeType = "#" + itemBase.NeID = "#" + loadInfo, _ := load.Avg() + itemBase.CPULoad1 = loadInfo.Load1 + itemBase.CPULoad5 = loadInfo.Load5 + itemBase.CPULoad15 = loadInfo.Load15 + totalPercent, _ := cpu.Percent(3*time.Second, false) + if len(totalPercent) > 0 { + itemBase.CPU = totalPercent[0] + } + cpuCount, _ := cpu.Counts(false) + cpuAvg := (float64(cpuCount*2) * 0.75) * 100 + itemBase.LoadUsage = 0 + if cpuAvg > 0 { + itemBase.LoadUsage = loadInfo.Load1 / cpuAvg + } + + memoryInfo, _ := mem.VirtualMemory() + itemBase.Memory = memoryInfo.UsedPercent + + if err := s.monitorRepository.CreateMonitorBase(itemBase); err != nil { + logger.Errorf("CreateMonitorBase err: %v", err) + } + + // 将当前资源发送到chan中处理保存 + s.loadDiskIO() + s.loadNetIO() + + // 监控系统资源-保留天数 + storeDays := s.sysConfigService.SelectConfigValueByKey("monitor.sysResource.storeDays") + if storeDays != "" { + storeDays, _ := strconv.Atoi(storeDays) + ltTime := time.Now().AddDate(0, 0, -storeDays).UnixMilli() + _ = s.monitorRepository.DelMonitorBase(ltTime) + _ = s.monitorRepository.DelMonitorIO(ltTime) + _ = s.monitorRepository.DelMonitorNet(ltTime) + } +} + +func (s *Monitor) loadDiskIO() { + ioStat, _ := disk.IOCounters() + var diskIOList []disk.IOCountersStat + for _, io := range ioStat { + diskIOList = append(diskIOList, io) + } + s.diskIO <- diskIOList +} + +func (s *Monitor) loadNetIO() { + netStat, _ := net.IOCounters(true) + netStatAll, _ := net.IOCounters(false) + var netList []net.IOCountersStat + netList = append(netList, netStat...) + netList = append(netList, netStatAll...) + s.netIO <- netList +} + +// monitorCancel 监控搜集IO/Network上下文 +var monitorCancel context.CancelFunc + +// RunMonitorDataCancel 启动资源监控数据存储io/network通道 移除之前的chan上下文后在设置新的均值 +// interval 采集的平均值(分钟) +func (s *Monitor) RunMonitorDataCancel(removeBefore bool, interval float64) { + // 是否取消之前的 + if removeBefore { + monitorCancel() + } + + // 上下文控制 + ctx, cancel := context.WithCancel(context.Background()) + monitorCancel = cancel + + // chanl 通道进行存储数据 + go s.saveIODataToDB(ctx, interval) + go s.saveNetDataToDB(ctx, interval) +} + +func (s *Monitor) saveIODataToDB(ctx context.Context, interval float64) { + defer close(s.diskIO) + for { + select { + case <-ctx.Done(): + return + case ioStat := <-s.diskIO: + select { + case <-ctx.Done(): + return + case ioStat2 := <-s.diskIO: + var ioList []model.MonitorIO + timeMilli := time.Now().UnixMilli() + for _, io2 := range ioStat2 { + for _, io1 := range ioStat { + if io2.Name == io1.Name { + var itemIO model.MonitorIO + itemIO.CreateTime = timeMilli + itemIO.NeType = "#" + itemIO.NeID = "#" + itemIO.Name = io1.Name + + if io2.ReadBytes != 0 && io1.ReadBytes != 0 && io2.ReadBytes > io1.ReadBytes { + itemIO.Read = int64(float64(io2.ReadBytes-io1.ReadBytes) / interval / 60) + } + if io2.WriteBytes != 0 && io1.WriteBytes != 0 && io2.WriteBytes > io1.WriteBytes { + itemIO.Write = int64(float64(io2.WriteBytes-io1.WriteBytes) / interval / 60) + } + + if io2.ReadCount != 0 && io1.ReadCount != 0 && io2.ReadCount > io1.ReadCount { + itemIO.Count = int64(float64(io2.ReadCount-io1.ReadCount) / interval / 60) + } + writeCount := int64(0) + if io2.WriteCount != 0 && io1.WriteCount != 0 && io2.WriteCount > io1.WriteCount { + writeCount = int64(float64(io2.WriteCount-io1.WriteCount) / interval * 60) + } + if writeCount > itemIO.Count { + itemIO.Count = writeCount + } + + if io2.ReadTime != 0 && io1.ReadTime != 0 && io2.ReadTime > io1.ReadTime { + itemIO.Time = int64(float64(io2.ReadTime-io1.ReadTime) / interval / 60) + } + writeTime := int64(0) + if io2.WriteTime != 0 && io1.WriteTime != 0 && io2.WriteTime > io1.WriteTime { + writeTime = int64(float64(io2.WriteTime-io1.WriteTime) / interval / 60) + } + if writeTime > itemIO.Time { + itemIO.Time = writeTime + } + ioList = append(ioList, itemIO) + break + } + } + } + if err := s.monitorRepository.BatchCreateMonitorIO(ioList); err != nil { + logger.Errorf("BatchCreateMonitorIO err: %v", err) + } + s.diskIO <- ioStat2 + } + } + } +} + +func (s *Monitor) saveNetDataToDB(ctx context.Context, interval float64) { + defer close(s.netIO) + for { + select { + case <-ctx.Done(): + return + case netStat := <-s.netIO: + select { + case <-ctx.Done(): + return + case netStat2 := <-s.netIO: + var netList []model.MonitorNetwork + timeMilli := time.Now().UnixMilli() + for _, net2 := range netStat2 { + for _, net1 := range netStat { + if net2.Name == net1.Name { + var itemNet model.MonitorNetwork + itemNet.CreateTime = timeMilli + itemNet.NeType = "#" + itemNet.NeID = "#" + itemNet.Name = net1.Name + + if net2.BytesSent != 0 && net1.BytesSent != 0 && net2.BytesSent > net1.BytesSent { + itemNet.Up = float64(net2.BytesSent-net1.BytesSent) / 1024 / interval / 60 + } + if net2.BytesRecv != 0 && net1.BytesRecv != 0 && net2.BytesRecv > net1.BytesRecv { + itemNet.Down = float64(net2.BytesRecv-net1.BytesRecv) / 1024 / interval / 60 + } + netList = append(netList, itemNet) + break + } + } + } + + if err := s.monitorRepository.BatchCreateMonitorNet(netList); err != nil { + logger.Errorf("BatchCreateMonitorNet err: %v", err) + } + s.netIO <- netStat2 + } + } + } +} + +// SelectMonitorInfo 查询监控资源信息 +func (s *Monitor) SelectMonitorInfo(query map[string]any) map[string]any { + infoType := query["type"] + startTimeMilli := query["startTime"] + endTimeMilli := query["endTime"] + neType := query["neType"] + neId := query["neId"] + name := query["name"] + + // 返回数据 + backDatas := map[string]any{} + + // 基本信息 + if infoType == "all" || infoType == "load" || infoType == "cpu" || infoType == "memory" { + rows := s.monitorRepository.SelectMonitorBase(map[string]any{ + "startTime": startTimeMilli, + "endTime": endTimeMilli, + "neType": neType, + "neId": neId, + }) + backDatas["base"] = rows + } + + // 磁盘IO + if infoType == "all" || infoType == "io" { + rows := s.monitorRepository.SelectMonitorIO(map[string]any{ + "startTime": startTimeMilli, + "endTime": endTimeMilli, + "neType": neType, + "neId": neId, + "name": name, + }) + backDatas["io"] = rows + } + + // 网络 + if infoType == "all" || infoType == "network" { + rows := s.monitorRepository.SelectMonitorNetwork(map[string]any{ + "startTime": startTimeMilli, + "endTime": endTimeMilli, + "neType": neType, + "neId": neId, + "name": name, + }) + backDatas["network"] = rows + } + + return backDatas } diff --git a/src/modules/monitor/service/monitor.impl.go b/src/modules/monitor/service/monitor.impl.go deleted file mode 100644 index e9de38cb..00000000 --- a/src/modules/monitor/service/monitor.impl.go +++ /dev/null @@ -1,271 +0,0 @@ -package service - -import ( - "context" - "strconv" - "time" - - "be.ems/src/framework/logger" - "be.ems/src/modules/monitor/model" - "be.ems/src/modules/monitor/repository" - systemService "be.ems/src/modules/system/service" - "github.com/shirou/gopsutil/v3/cpu" - "github.com/shirou/gopsutil/v3/disk" - "github.com/shirou/gopsutil/v3/load" - "github.com/shirou/gopsutil/v3/mem" - "github.com/shirou/gopsutil/v3/net" -) - -// 实例化服务层 MonitorImpl 结构体 -var NewMonitorImpl = &MonitorImpl{ - sysConfigService: systemService.NewSysConfigImpl, - monitorRepository: repository.NewMonitorImpl, - diskIO: make(chan []disk.IOCountersStat, 2), - netIO: make(chan []net.IOCountersStat, 2), -} - -// MonitorImpl 服务器系统相关信息 服务层处理 -type MonitorImpl struct { - // 参数配置服务 - sysConfigService systemService.ISysConfig - // 监控服务资源数据信息 - monitorRepository repository.IMonitor - // 磁盘网络IO 数据通道 - diskIO chan ([]disk.IOCountersStat) - netIO chan ([]net.IOCountersStat) -} - -// RunMonitor 执行资源监控 -func (s *MonitorImpl) RunMonitor() { - var itemBase model.MonitorBase - itemBase.CreateTime = time.Now().UnixMilli() - itemBase.NeType = "#" - itemBase.NeID = "#" - totalPercent, _ := cpu.Percent(3*time.Second, false) - if len(totalPercent) == 1 { - itemBase.CPU = totalPercent[0] - } - cpuCount, _ := cpu.Counts(false) - - loadInfo, _ := load.Avg() - itemBase.CPULoad1 = loadInfo.Load1 - itemBase.CPULoad5 = loadInfo.Load5 - itemBase.CPULoad15 = loadInfo.Load15 - itemBase.LoadUsage = loadInfo.Load1 / (float64(cpuCount*2) * 0.75) * 100 - - memoryInfo, _ := mem.VirtualMemory() - itemBase.Memory = memoryInfo.UsedPercent - - if err := s.monitorRepository.CreateMonitorBase(itemBase); err != nil { - logger.Errorf("CreateMonitorBase err: %v", err) - } - - // 将当前资源发送到chan中处理保存 - s.loadDiskIO() - s.loadNetIO() - - // 监控系统资源-保留天数 - storeDays := s.sysConfigService.SelectConfigValueByKey("monitor.sysResource.storeDays") - if storeDays != "" { - storeDays, _ := strconv.Atoi(storeDays) - ltTime := time.Now().AddDate(0, 0, -storeDays).UnixMilli() - _ = s.monitorRepository.DelMonitorBase(ltTime) - _ = s.monitorRepository.DelMonitorIO(ltTime) - _ = s.monitorRepository.DelMonitorNet(ltTime) - } -} - -func (s *MonitorImpl) loadDiskIO() { - ioStat, _ := disk.IOCounters() - var diskIOList []disk.IOCountersStat - for _, io := range ioStat { - diskIOList = append(diskIOList, io) - } - s.diskIO <- diskIOList -} - -func (s *MonitorImpl) loadNetIO() { - netStat, _ := net.IOCounters(true) - netStatAll, _ := net.IOCounters(false) - var netList []net.IOCountersStat - netList = append(netList, netStat...) - netList = append(netList, netStatAll...) - s.netIO <- netList -} - -// monitorCancel 监控搜集IO/Network上下文 -var monitorCancel context.CancelFunc - -// RunMonitorDataCancel 启动资源监控数据存储io/network通道 移除之前的chan上下文后在设置新的均值 -// interval 采集的平均值(分钟) -func (s *MonitorImpl) RunMonitorDataCancel(removeBefore bool, interval float64) { - // 是否取消之前的 - if removeBefore { - monitorCancel() - } - - // 上下文控制 - ctx, cancel := context.WithCancel(context.Background()) - monitorCancel = cancel - - // chanl 通道进行存储数据 - go s.saveIODataToDB(ctx, interval) - go s.saveNetDataToDB(ctx, interval) -} - -func (s *MonitorImpl) saveIODataToDB(ctx context.Context, interval float64) { - defer close(s.diskIO) - for { - select { - case <-ctx.Done(): - return - case ioStat := <-s.diskIO: - select { - case <-ctx.Done(): - return - case ioStat2 := <-s.diskIO: - var ioList []model.MonitorIO - timeMilli := time.Now().UnixMilli() - for _, io2 := range ioStat2 { - for _, io1 := range ioStat { - if io2.Name == io1.Name { - var itemIO model.MonitorIO - itemIO.CreateTime = timeMilli - itemIO.NeType = "#" - itemIO.NeID = "#" - itemIO.Name = io1.Name - - if io2.ReadBytes != 0 && io1.ReadBytes != 0 && io2.ReadBytes > io1.ReadBytes { - itemIO.Read = int64(float64(io2.ReadBytes-io1.ReadBytes) / interval / 60) - } - if io2.WriteBytes != 0 && io1.WriteBytes != 0 && io2.WriteBytes > io1.WriteBytes { - itemIO.Write = int64(float64(io2.WriteBytes-io1.WriteBytes) / interval / 60) - } - - if io2.ReadCount != 0 && io1.ReadCount != 0 && io2.ReadCount > io1.ReadCount { - itemIO.Count = int64(float64(io2.ReadCount-io1.ReadCount) / interval / 60) - } - writeCount := int64(0) - if io2.WriteCount != 0 && io1.WriteCount != 0 && io2.WriteCount > io1.WriteCount { - writeCount = int64(float64(io2.WriteCount-io1.WriteCount) / interval * 60) - } - if writeCount > itemIO.Count { - itemIO.Count = writeCount - } - - if io2.ReadTime != 0 && io1.ReadTime != 0 && io2.ReadTime > io1.ReadTime { - itemIO.Time = int64(float64(io2.ReadTime-io1.ReadTime) / interval / 60) - } - writeTime := int64(0) - if io2.WriteTime != 0 && io1.WriteTime != 0 && io2.WriteTime > io1.WriteTime { - writeTime = int64(float64(io2.WriteTime-io1.WriteTime) / interval / 60) - } - if writeTime > itemIO.Time { - itemIO.Time = writeTime - } - ioList = append(ioList, itemIO) - break - } - } - } - if err := s.monitorRepository.BatchCreateMonitorIO(ioList); err != nil { - logger.Errorf("BatchCreateMonitorIO err: %v", err) - } - s.diskIO <- ioStat2 - } - } - } -} - -func (s *MonitorImpl) saveNetDataToDB(ctx context.Context, interval float64) { - defer close(s.netIO) - for { - select { - case <-ctx.Done(): - return - case netStat := <-s.netIO: - select { - case <-ctx.Done(): - return - case netStat2 := <-s.netIO: - var netList []model.MonitorNetwork - timeMilli := time.Now().UnixMilli() - for _, net2 := range netStat2 { - for _, net1 := range netStat { - if net2.Name == net1.Name { - var itemNet model.MonitorNetwork - itemNet.CreateTime = timeMilli - itemNet.NeType = "#" - itemNet.NeID = "#" - itemNet.Name = net1.Name - - if net2.BytesSent != 0 && net1.BytesSent != 0 && net2.BytesSent > net1.BytesSent { - itemNet.Up = float64(net2.BytesSent-net1.BytesSent) / 1024 / interval / 60 - } - if net2.BytesRecv != 0 && net1.BytesRecv != 0 && net2.BytesRecv > net1.BytesRecv { - itemNet.Down = float64(net2.BytesRecv-net1.BytesRecv) / 1024 / interval / 60 - } - netList = append(netList, itemNet) - break - } - } - } - - if err := s.monitorRepository.BatchCreateMonitorNet(netList); err != nil { - logger.Errorf("BatchCreateMonitorNet err: %v", err) - } - s.netIO <- netStat2 - } - } - } -} - -// SelectMonitorInfo 查询监控资源信息 -func (s *MonitorImpl) SelectMonitorInfo(query map[string]any) map[string]any { - infoType := query["type"] - startTimeMilli := query["startTime"] - endTimeMilli := query["endTime"] - neType := query["neType"] - neId := query["neId"] - name := query["name"] - - // 返回数据 - backDatas := map[string]any{} - - // 基本信息 - if infoType == "all" || infoType == "load" || infoType == "cpu" || infoType == "memory" { - rows := s.monitorRepository.SelectMonitorBase(map[string]any{ - "startTime": startTimeMilli, - "endTime": endTimeMilli, - "neType": neType, - "neId": neId, - }) - backDatas["base"] = rows - } - - // 磁盘IO - if infoType == "all" || infoType == "io" { - rows := s.monitorRepository.SelectMonitorIO(map[string]any{ - "startTime": startTimeMilli, - "endTime": endTimeMilli, - "neType": neType, - "neId": neId, - "name": name, - }) - backDatas["io"] = rows - } - - // 网络 - if infoType == "all" || infoType == "network" { - rows := s.monitorRepository.SelectMonitorNetwork(map[string]any{ - "startTime": startTimeMilli, - "endTime": endTimeMilli, - "neType": neType, - "neId": neId, - "name": name, - }) - backDatas["network"] = rows - } - - return backDatas -} diff --git a/src/modules/monitor/service/monitor_test.go b/src/modules/monitor/service/monitor_test.go index 11a87a8f..44dd2a72 100644 --- a/src/modules/monitor/service/monitor_test.go +++ b/src/modules/monitor/service/monitor_test.go @@ -5,17 +5,13 @@ import ( "testing" "time" - "github.com/shirou/gopsutil/v3/cpu" - "github.com/shirou/gopsutil/v3/disk" - "github.com/shirou/gopsutil/v3/load" - "github.com/shirou/gopsutil/v3/mem" - "github.com/shirou/gopsutil/v3/net" + "github.com/shirou/gopsutil/v4/cpu" + "github.com/shirou/gopsutil/v4/disk" + "github.com/shirou/gopsutil/v4/load" + "github.com/shirou/gopsutil/v4/mem" + "github.com/shirou/gopsutil/v4/net" ) -func init() { - -} - func TestInfo(t *testing.T) { s := MonitorInfo{} s.load(0.5) // 0.5 半分钟 @@ -36,17 +32,21 @@ func (m *MonitorInfo) load(interval float64) { var itemBase MonitorBase itemBase.CreateTime = time.Now().UnixMilli() - totalPercent, _ := cpu.Percent(3*time.Second, false) - if len(totalPercent) == 1 { - itemBase.CPU = totalPercent[0] - } - cpuCount, _ := cpu.Counts(false) - loadInfo, _ := load.Avg() itemBase.CPULoad1 = loadInfo.Load1 itemBase.CPULoad5 = loadInfo.Load5 itemBase.CPULoad15 = loadInfo.Load15 - itemBase.LoadUsage = loadInfo.Load1 / (float64(cpuCount*2) * 0.75) * 100 + + totalPercent, _ := cpu.Percent(3*time.Second, false) + if len(totalPercent) > 0 { + itemBase.CPU = totalPercent[0] + } + cpuCount, _ := cpu.Counts(false) + cpuAvg := (float64(cpuCount*2) * 0.75) * 100 + itemBase.LoadUsage = 0 + if cpuAvg > 0 { + itemBase.LoadUsage = loadInfo.Load1 / cpuAvg + } memoryInfo, _ := mem.VirtualMemory() itemBase.Memory = memoryInfo.UsedPercent diff --git a/src/modules/monitor/service/sys_job.go b/src/modules/monitor/service/sys_job.go index f4beb36c..af1ca91e 100644 --- a/src/modules/monitor/service/sys_job.go +++ b/src/modules/monitor/service/sys_job.go @@ -1,35 +1,169 @@ package service import ( + "fmt" + + "be.ems/src/framework/constants/common" + "be.ems/src/framework/cron" "be.ems/src/modules/monitor/model" + "be.ems/src/modules/monitor/repository" ) -// ISysJob 调度任务信息 服务层接口 -type ISysJob interface { - // SelectJobPage 分页查询调度任务集合 - SelectJobPage(query map[string]any) map[string]any - - // SelectJobList 查询调度任务集合 - SelectJobList(sysJob model.SysJob) []model.SysJob - - // SelectJobById 通过调度ID查询调度任务信息 - SelectJobById(jobId string) model.SysJob - - // CheckUniqueJobName 校验调度任务名称和组是否唯一 - CheckUniqueJobName(jobName, jobGroup, jobId string) bool - - // InsertJob 新增调度任务信息 - InsertJob(sysJob model.SysJob) string - - // UpdateJob 修改调度任务信息 - UpdateJob(sysJob model.SysJob) int64 - - // DeleteJobByIds 批量删除调度任务信息 - DeleteJobByIds(jobIds []string) (int64, error) - - // RunQueueJob 立即运行一次调度任务 - RunQueueJob(sysJob model.SysJob) bool - - // ResetQueueJob 重置初始调度任务 - ResetQueueJob() +// 实例化服务层 SysJob 结构体 +var NewSysJob = &SysJob{ + sysJobRepository: repository.NewSysJobImpl, +} + +// SysJob 调度任务 服务层处理 +type SysJob struct { + // 调度任务数据信息 + sysJobRepository repository.ISysJob +} + +// SelectJobPage 分页查询调度任务集合 +func (r *SysJob) SelectJobPage(query map[string]any) map[string]any { + return r.sysJobRepository.SelectJobPage(query) +} + +// SelectJobList 查询调度任务集合 +func (r *SysJob) SelectJobList(sysJob model.SysJob) []model.SysJob { + return r.sysJobRepository.SelectJobList(sysJob) +} + +// SelectJobById 通过调度ID查询调度任务信息 +func (r *SysJob) SelectJobById(jobId string) model.SysJob { + if jobId == "" { + return model.SysJob{} + } + jobs := r.sysJobRepository.SelectJobByIds([]string{jobId}) + if len(jobs) > 0 { + return jobs[0] + } + return model.SysJob{} +} + +// CheckUniqueJobName 校验调度任务名称和组是否唯一 +func (r *SysJob) CheckUniqueJobName(jobName, jobGroup, jobId string) bool { + uniqueId := r.sysJobRepository.CheckUniqueJob(model.SysJob{ + JobName: jobName, + JobGroup: jobGroup, + }) + if uniqueId == jobId { + return true + } + return uniqueId == "" +} + +// InsertJob 新增调度任务信息 +func (r *SysJob) InsertJob(sysJob model.SysJob) string { + insertId := r.sysJobRepository.InsertJob(sysJob) + if insertId == "" && sysJob.Status == common.STATUS_YES { + sysJob.JobID = insertId + r.insertQueueJob(sysJob, true) + } + return insertId +} + +// UpdateJob 修改调度任务信息 +func (r *SysJob) UpdateJob(sysJob model.SysJob) int64 { + rows := r.sysJobRepository.UpdateJob(sysJob) + if rows > 0 { + //状态正常添加队列任务 + if sysJob.Status == common.STATUS_YES { + r.insertQueueJob(sysJob, true) + } + // 状态禁用删除队列任务 + if sysJob.Status == common.STATUS_NO { + r.deleteQueueJob(sysJob) + } + } + return rows +} + +// DeleteJobByIds 批量删除调度任务信息 +func (r *SysJob) DeleteJobByIds(jobIds []string) (int64, error) { + // 检查是否存在 + jobs := r.sysJobRepository.SelectJobByIds(jobIds) + if len(jobs) <= 0 { + // 没有可访问调度任务数据! + return 0, fmt.Errorf("there is no accessible scheduling task data") + } + if len(jobs) == len(jobIds) { + // 清除任务 + for _, job := range jobs { + r.deleteQueueJob(job) + } + rows := r.sysJobRepository.DeleteJobByIds(jobIds) + return rows, nil + } + // 删除调度任务信息失败! + return 0, fmt.Errorf("failed to delete scheduling task information") +} + +// ResetQueueJob 重置初始调度任务 +func (r *SysJob) ResetQueueJob() { + // 获取注册的队列名称 + queueNames := cron.QueueNames() + if len(queueNames) == 0 { + return + } + // 查询系统中定义状态为正常启用的任务 + sysJobs := r.sysJobRepository.SelectJobList(model.SysJob{ + Status: common.STATUS_YES, + }) + for _, sysJob := range sysJobs { + for _, name := range queueNames { + if name == sysJob.InvokeTarget { + r.insertQueueJob(sysJob, true) + } + } + } +} + +// RunQueueJob 立即运行一次调度任务 +func (r *SysJob) RunQueueJob(sysJob model.SysJob) bool { + return r.insertQueueJob(sysJob, false) +} + +// insertQueueJob 添加调度任务 +func (r *SysJob) insertQueueJob(sysJob model.SysJob, repeat bool) bool { + // 获取队列 Processor + queue := cron.GetQueue(sysJob.InvokeTarget) + if queue.Name != sysJob.InvokeTarget { + return false + } + + // 给执行任务数据参数 + options := cron.JobData{ + Repeat: repeat, + SysJob: sysJob, + } + + // 不是重复任务的情况,立即执行一次 + if !repeat { + // 执行单次任务 + status := queue.RunJob(options, cron.JobOptions{ + JobId: sysJob.JobID, + }) + // 执行中或等待中的都返回正常 + return status == cron.Active || status == cron.Waiting + } + + // 执行重复任务 + queue.RunJob(options, cron.JobOptions{ + JobId: sysJob.JobID, + Cron: sysJob.CronExpression, + }) + + return true +} + +// deleteQueueJob 删除调度任务 +func (r *SysJob) deleteQueueJob(sysJob model.SysJob) bool { + // 获取队列 Processor + queue := cron.GetQueue(sysJob.InvokeTarget) + if queue.Name != sysJob.InvokeTarget { + return false + } + return queue.RemoveJob(sysJob.JobID) } diff --git a/src/modules/monitor/service/sys_job.impl.go b/src/modules/monitor/service/sys_job.impl.go deleted file mode 100644 index 81fd7e7e..00000000 --- a/src/modules/monitor/service/sys_job.impl.go +++ /dev/null @@ -1,169 +0,0 @@ -package service - -import ( - "fmt" - - "be.ems/src/framework/constants/common" - "be.ems/src/framework/cron" - "be.ems/src/modules/monitor/model" - "be.ems/src/modules/monitor/repository" -) - -// 实例化服务层 SysJobImpl 结构体 -var NewSysJobImpl = &SysJobImpl{ - sysJobRepository: repository.NewSysJobImpl, -} - -// SysJobImpl 调度任务 服务层处理 -type SysJobImpl struct { - // 调度任务数据信息 - sysJobRepository repository.ISysJob -} - -// SelectJobPage 分页查询调度任务集合 -func (r *SysJobImpl) SelectJobPage(query map[string]any) map[string]any { - return r.sysJobRepository.SelectJobPage(query) -} - -// SelectJobList 查询调度任务集合 -func (r *SysJobImpl) SelectJobList(sysJob model.SysJob) []model.SysJob { - return r.sysJobRepository.SelectJobList(sysJob) -} - -// SelectJobById 通过调度ID查询调度任务信息 -func (r *SysJobImpl) SelectJobById(jobId string) model.SysJob { - if jobId == "" { - return model.SysJob{} - } - jobs := r.sysJobRepository.SelectJobByIds([]string{jobId}) - if len(jobs) > 0 { - return jobs[0] - } - return model.SysJob{} -} - -// CheckUniqueJobName 校验调度任务名称和组是否唯一 -func (r *SysJobImpl) CheckUniqueJobName(jobName, jobGroup, jobId string) bool { - uniqueId := r.sysJobRepository.CheckUniqueJob(model.SysJob{ - JobName: jobName, - JobGroup: jobGroup, - }) - if uniqueId == jobId { - return true - } - return uniqueId == "" -} - -// InsertJob 新增调度任务信息 -func (r *SysJobImpl) InsertJob(sysJob model.SysJob) string { - insertId := r.sysJobRepository.InsertJob(sysJob) - if insertId == "" && sysJob.Status == common.STATUS_YES { - sysJob.JobID = insertId - r.insertQueueJob(sysJob, true) - } - return insertId -} - -// UpdateJob 修改调度任务信息 -func (r *SysJobImpl) UpdateJob(sysJob model.SysJob) int64 { - rows := r.sysJobRepository.UpdateJob(sysJob) - if rows > 0 { - //状态正常添加队列任务 - if sysJob.Status == common.STATUS_YES { - r.insertQueueJob(sysJob, true) - } - // 状态禁用删除队列任务 - if sysJob.Status == common.STATUS_NO { - r.deleteQueueJob(sysJob) - } - } - return rows -} - -// DeleteJobByIds 批量删除调度任务信息 -func (r *SysJobImpl) DeleteJobByIds(jobIds []string) (int64, error) { - // 检查是否存在 - jobs := r.sysJobRepository.SelectJobByIds(jobIds) - if len(jobs) <= 0 { - // 没有可访问调度任务数据! - return 0, fmt.Errorf("there is no accessible scheduling task data") - } - if len(jobs) == len(jobIds) { - // 清除任务 - for _, job := range jobs { - r.deleteQueueJob(job) - } - rows := r.sysJobRepository.DeleteJobByIds(jobIds) - return rows, nil - } - // 删除调度任务信息失败! - return 0, fmt.Errorf("failed to delete scheduling task information") -} - -// ResetQueueJob 重置初始调度任务 -func (r *SysJobImpl) ResetQueueJob() { - // 获取注册的队列名称 - queueNames := cron.QueueNames() - if len(queueNames) == 0 { - return - } - // 查询系统中定义状态为正常启用的任务 - sysJobs := r.sysJobRepository.SelectJobList(model.SysJob{ - Status: common.STATUS_YES, - }) - for _, sysJob := range sysJobs { - for _, name := range queueNames { - if name == sysJob.InvokeTarget { - r.insertQueueJob(sysJob, true) - } - } - } -} - -// RunQueueJob 立即运行一次调度任务 -func (r *SysJobImpl) RunQueueJob(sysJob model.SysJob) bool { - return r.insertQueueJob(sysJob, false) -} - -// insertQueueJob 添加调度任务 -func (r *SysJobImpl) insertQueueJob(sysJob model.SysJob, repeat bool) bool { - // 获取队列 Processor - queue := cron.GetQueue(sysJob.InvokeTarget) - if queue.Name != sysJob.InvokeTarget { - return false - } - - // 给执行任务数据参数 - options := cron.JobData{ - Repeat: repeat, - SysJob: sysJob, - } - - // 不是重复任务的情况,立即执行一次 - if !repeat { - // 执行单次任务 - status := queue.RunJob(options, cron.JobOptions{ - JobId: sysJob.JobID, - }) - // 执行中或等待中的都返回正常 - return status == cron.Active || status == cron.Waiting - } - - // 执行重复任务 - queue.RunJob(options, cron.JobOptions{ - JobId: sysJob.JobID, - Cron: sysJob.CronExpression, - }) - - return true -} - -// deleteQueueJob 删除调度任务 -func (r *SysJobImpl) deleteQueueJob(sysJob model.SysJob) bool { - // 获取队列 Processor - queue := cron.GetQueue(sysJob.InvokeTarget) - if queue.Name != sysJob.InvokeTarget { - return false - } - return queue.RemoveJob(sysJob.JobID) -} diff --git a/src/modules/monitor/service/sys_job_log.go b/src/modules/monitor/service/sys_job_log.go index b1e227e2..185c17a0 100644 --- a/src/modules/monitor/service/sys_job_log.go +++ b/src/modules/monitor/service/sys_job_log.go @@ -2,22 +2,41 @@ package service import ( "be.ems/src/modules/monitor/model" + "be.ems/src/modules/monitor/repository" ) -// ISysJobLog 调度任务日志 服务层接口 -type ISysJobLog interface { - // SelectJobLogPage 分页查询调度任务日志集合 - SelectJobLogPage(query map[string]any) map[string]any - - // SelectJobLogList 查询调度任务日志集合 - SelectJobLogList(sysJobLog model.SysJobLog) []model.SysJobLog - - // SelectJobLogById 通过调度ID查询调度任务日志信息 - SelectJobLogById(jobLogId string) model.SysJobLog - - // DeleteJobLogByIds 批量删除调度任务日志信息 - DeleteJobLogByIds(jobLogIds []string) int64 - - // CleanJobLog 清空调度任务日志 - CleanJobLog() error +// 实例化服务层 SysJobLog 结构体 +var NewSysJobLog = &SysJobLog{ + sysJobLogRepository: repository.NewSysJobLogImpl, +} + +// SysJobLog 调度任务日志 服务层处理 +type SysJobLog struct { + // 调度任务日志数据信息 + sysJobLogRepository repository.ISysJobLog +} + +// SelectJobLogPage 分页查询调度任务日志集合 +func (s *SysJobLog) SelectJobLogPage(query map[string]any) map[string]any { + return s.sysJobLogRepository.SelectJobLogPage(query) +} + +// SelectJobLogList 查询调度任务日志集合 +func (s *SysJobLog) SelectJobLogList(sysJobLog model.SysJobLog) []model.SysJobLog { + return s.sysJobLogRepository.SelectJobLogList(sysJobLog) +} + +// SelectJobLogById 通过调度ID查询调度任务日志信息 +func (s *SysJobLog) SelectJobLogById(jobLogId string) model.SysJobLog { + return s.sysJobLogRepository.SelectJobLogById(jobLogId) +} + +// DeleteJobLogByIds 批量删除调度任务日志信息 +func (s *SysJobLog) DeleteJobLogByIds(jobLogIds []string) int64 { + return s.sysJobLogRepository.DeleteJobLogByIds(jobLogIds) +} + +// CleanJobLog 清空调度任务日志 +func (s *SysJobLog) CleanJobLog() error { + return s.sysJobLogRepository.CleanJobLog() } diff --git a/src/modules/monitor/service/sys_job_log.impl.go b/src/modules/monitor/service/sys_job_log.impl.go deleted file mode 100644 index 692d40a3..00000000 --- a/src/modules/monitor/service/sys_job_log.impl.go +++ /dev/null @@ -1,42 +0,0 @@ -package service - -import ( - "be.ems/src/modules/monitor/model" - "be.ems/src/modules/monitor/repository" -) - -// 实例化服务层 SysJobLogImpl 结构体 -var NewSysJobLogImpl = &SysJobLogImpl{ - sysJobLogRepository: repository.NewSysJobLogImpl, -} - -// SysJobLogImpl 调度任务日志 服务层处理 -type SysJobLogImpl struct { - // 调度任务日志数据信息 - sysJobLogRepository repository.ISysJobLog -} - -// SelectJobLogPage 分页查询调度任务日志集合 -func (s *SysJobLogImpl) SelectJobLogPage(query map[string]any) map[string]any { - return s.sysJobLogRepository.SelectJobLogPage(query) -} - -// SelectJobLogList 查询调度任务日志集合 -func (s *SysJobLogImpl) SelectJobLogList(sysJobLog model.SysJobLog) []model.SysJobLog { - return s.sysJobLogRepository.SelectJobLogList(sysJobLog) -} - -// SelectJobLogById 通过调度ID查询调度任务日志信息 -func (s *SysJobLogImpl) SelectJobLogById(jobLogId string) model.SysJobLog { - return s.sysJobLogRepository.SelectJobLogById(jobLogId) -} - -// DeleteJobLogByIds 批量删除调度任务日志信息 -func (s *SysJobLogImpl) DeleteJobLogByIds(jobLogIds []string) int64 { - return s.sysJobLogRepository.DeleteJobLogByIds(jobLogIds) -} - -// CleanJobLog 清空调度任务日志 -func (s *SysJobLogImpl) CleanJobLog() error { - return s.sysJobLogRepository.CleanJobLog() -} diff --git a/src/modules/monitor/service/sys_user_online.go b/src/modules/monitor/service/sys_user_online.go index c6aef2cd..1d7115d8 100644 --- a/src/modules/monitor/service/sys_user_online.go +++ b/src/modules/monitor/service/sys_user_online.go @@ -5,8 +5,29 @@ import ( "be.ems/src/modules/monitor/model" ) -// ISysUserOnline 在线用户 服务层接口 -type ISysUserOnline interface { - // LoginUserToUserOnline 设置在线用户信息 - LoginUserToUserOnline(loginUser vo.LoginUser) model.SysUserOnline +// 实例化服务层 SysUserOnline 结构体 +var NewSysUserOnline = &SysUserOnline{} + +// SysUserOnline 在线用户 服务层处理 +type SysUserOnline struct{} + +// LoginUserToUserOnline 设置在线用户信息 +func (r *SysUserOnline) LoginUserToUserOnline(loginUser vo.LoginUser) model.SysUserOnline { + if loginUser.UserID == "" { + return model.SysUserOnline{} + } + + sysUserOnline := model.SysUserOnline{ + TokenID: loginUser.UUID, + UserName: loginUser.User.UserName, + IPAddr: loginUser.IPAddr, + LoginLocation: loginUser.LoginLocation, + Browser: loginUser.Browser, + OS: loginUser.OS, + LoginTime: loginUser.LoginTime, + } + if loginUser.User.DeptID != "" { + sysUserOnline.DeptName = loginUser.User.Dept.DeptName + } + return sysUserOnline } diff --git a/src/modules/monitor/service/sys_user_online.impl.go b/src/modules/monitor/service/sys_user_online.impl.go deleted file mode 100644 index 99c0ec4f..00000000 --- a/src/modules/monitor/service/sys_user_online.impl.go +++ /dev/null @@ -1,33 +0,0 @@ -package service - -import ( - "be.ems/src/framework/vo" - "be.ems/src/modules/monitor/model" -) - -// 实例化服务层 SysUserOnlineImpl 结构体 -var NewSysUserOnlineImpl = &SysUserOnlineImpl{} - -// SysUserOnlineImpl 在线用户 服务层处理 -type SysUserOnlineImpl struct{} - -// LoginUserToUserOnline 设置在线用户信息 -func (r *SysUserOnlineImpl) LoginUserToUserOnline(loginUser vo.LoginUser) model.SysUserOnline { - if loginUser.UserID == "" { - return model.SysUserOnline{} - } - - sysUserOnline := model.SysUserOnline{ - TokenID: loginUser.UUID, - UserName: loginUser.User.UserName, - IPAddr: loginUser.IPAddr, - LoginLocation: loginUser.LoginLocation, - Browser: loginUser.Browser, - OS: loginUser.OS, - LoginTime: loginUser.LoginTime, - } - if loginUser.User.DeptID != "" { - sysUserOnline.DeptName = loginUser.User.Dept.DeptName - } - return sysUserOnline -} diff --git a/src/modules/monitor/service/system_info.go b/src/modules/monitor/service/system_info.go index a6e9a51d..3fda7b7b 100644 --- a/src/modules/monitor/service/system_info.go +++ b/src/modules/monitor/service/system_info.go @@ -1,22 +1,177 @@ package service -// ISystemInfo 服务器系统相关信息 服务层接口 -type ISystemInfo interface { - // SystemInfo 系统信息 - SystemInfo() map[string]any +import ( + "context" + "fmt" + "os" + "runtime" + "strings" + "time" - // TimeInfo 系统时间信息 - TimeInfo() map[string]string + "be.ems/src/framework/config" + "be.ems/src/framework/utils/parse" - // MemoryInfo 内存信息 - MemoryInfo() map[string]any + "github.com/shirou/gopsutil/v4/cpu" + "github.com/shirou/gopsutil/v4/disk" + "github.com/shirou/gopsutil/v4/host" + "github.com/shirou/gopsutil/v4/mem" + "github.com/shirou/gopsutil/v4/net" +) - // CPUInfo CPU信息 - CPUInfo() map[string]any +// 实例化服务层 SystemInfo 结构体 +var NewSystemInfo = &SystemInfo{} - // NetworkInfo 网络信息 - NetworkInfo() map[string]string +// SystemInfo 服务器系统相关信息 服务层处理 +type SystemInfo struct{} - // DiskInfo 磁盘信息 - DiskInfo() []map[string]string +// SystemInfo 系统信息 +func (s *SystemInfo) SystemInfo() map[string]any { + info, err := host.Info() + if err != nil { + info.Platform = err.Error() + } + // 获取主机运行时间 + bootTime := time.Since(time.Unix(int64(info.BootTime), 0)).Seconds() + // 获取程序运行时间 + runTime := time.Since(config.RunTime()).Abs().Seconds() + return map[string]any{ + "platform": info.Platform, + "platformVersion": info.PlatformVersion, + "arch": info.KernelArch, + "archVersion": info.KernelVersion, + "os": info.OS, + "hostname": info.Hostname, + "bootTime": int64(bootTime), + "processId": os.Getpid(), + "runArch": runtime.GOARCH, + "runVersion": runtime.Version(), + "runTime": int64(runTime), + } +} + +// TimeInfo 系统时间信息 +func (s *SystemInfo) TimeInfo() map[string]string { + now := time.Now() + // 获取当前时间 + current := now.Format("2006-01-02 15:04:05") + // 获取时区 + timezone := now.Format("-0700 MST") + // 获取时区名称 + timezoneName := now.Format("MST") + + return map[string]string{ + "current": current, + "timezone": timezone, + "timezoneName": timezoneName, + } +} + +// MemoryInfo 内存信息 +func (s *SystemInfo) MemoryInfo() map[string]any { + memInfo, err := mem.VirtualMemory() + if err != nil { + memInfo.UsedPercent = 0 + memInfo.Available = 0 + memInfo.Total = 0 + } + + var memStats runtime.MemStats + runtime.ReadMemStats(&memStats) + + return map[string]any{ + "usage": fmt.Sprintf("%.2f", memInfo.UsedPercent), // 内存利用率 + "freemem": parse.Bit(float64(memInfo.Available)), // 可用内存大小(GB) + "totalmem": parse.Bit(float64(memInfo.Total)), // 总内存大小(GB) + "rss": parse.Bit(float64(memStats.Sys)), // 常驻内存大小(RSS) + "heapTotal": parse.Bit(float64(memStats.HeapSys)), // 堆总大小 + "heapUsed": parse.Bit(float64(memStats.HeapAlloc)), // 堆已使用大小 + "external": parse.Bit(float64(memStats.Sys - memStats.HeapSys)), // 外部内存大小(非堆) + } +} + +// CPUInfo CPU信息 +func (s *SystemInfo) CPUInfo() map[string]any { + var core int = 0 + var speed string = "未知" + var model string = "未知" + cpuInfo, err := cpu.Info() + if err == nil { + core = runtime.NumCPU() + speed = fmt.Sprintf("%.0fMHz", cpuInfo[0].Mhz) + model = strings.TrimSpace(cpuInfo[0].ModelName) + } + + useds := []string{} + cpuPercent, err := cpu.Percent(0, true) + if err == nil { + for _, v := range cpuPercent { + useds = append(useds, fmt.Sprintf("%.2f", v)) + } + } + + return map[string]any{ + "model": model, + "speed": speed, + "core": core, + "coreUsed": useds, + } +} + +// NetworkInfo 网络信息 +func (s *SystemInfo) NetworkInfo() map[string]string { + ipAddrs := make(map[string]string) + interfaces, err := net.Interfaces() + if err == nil { + for _, iface := range interfaces { + name := iface.Name + if name[len(name)-1] == '0' { + name = name[0 : len(name)-1] + name = strings.Trim(name, "") + } + // ignore localhost + if name == "lo" { + continue + } + var addrs []string + for _, v := range iface.Addrs { + prefix := strings.Split(v.Addr, "/")[0] + if strings.Contains(prefix, "::") { + addrs = append(addrs, fmt.Sprintf("IPv6 %s", prefix)) + } + if strings.Contains(prefix, ".") { + addrs = append(addrs, fmt.Sprintf("IPv4 %s", prefix)) + } + } + ipAddrs[name] = strings.Join(addrs, " / ") + } + } + return ipAddrs +} + +// DiskInfo 磁盘信息 +func (s *SystemInfo) DiskInfo() []map[string]string { + disks := make([]map[string]string, 0) + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, 2*time.Second) + defer cancel() + + partitions, err := disk.PartitionsWithContext(ctx, false) + if err != nil && err != context.DeadlineExceeded { + return disks + } + + for _, partition := range partitions { + usage, err := disk.Usage(partition.Mountpoint) + if err != nil { + continue + } + disks = append(disks, map[string]string{ + "size": parse.Bit(float64(usage.Total)), + "used": parse.Bit(float64(usage.Used)), + "avail": parse.Bit(float64(usage.Free)), + "pcent": fmt.Sprintf("%.1f%%", usage.UsedPercent), + "target": partition.Device, + }) + } + return disks } diff --git a/src/modules/monitor/service/system_info.impl.go b/src/modules/monitor/service/system_info.impl.go deleted file mode 100644 index 663499de..00000000 --- a/src/modules/monitor/service/system_info.impl.go +++ /dev/null @@ -1,173 +0,0 @@ -package service - -import ( - "fmt" - "os" - "runtime" - "strings" - "time" - - "be.ems/src/framework/config" - "be.ems/src/framework/utils/parse" - - "github.com/shirou/gopsutil/v3/cpu" - "github.com/shirou/gopsutil/v3/disk" - "github.com/shirou/gopsutil/v3/host" - "github.com/shirou/gopsutil/v3/mem" - "github.com/shirou/gopsutil/v3/net" -) - -// 实例化服务层 SystemInfoImpl 结构体 -var NewSystemInfoImpl = &SystemInfoImpl{} - -// SystemInfoImpl 服务器系统相关信息 服务层处理 -type SystemInfoImpl struct{} - -// SystemInfo 系统信息 -func (s *SystemInfoImpl) SystemInfo() map[string]any { - info, err := host.Info() - if err != nil { - info.Platform = err.Error() - } - // 获取主机运行时间 - bootTime := time.Since(time.Unix(int64(info.BootTime), 0)).Seconds() - // 获取程序运行时间 - runTime := time.Since(config.RunTime()).Abs().Seconds() - return map[string]any{ - "platform": info.Platform, - "platformVersion": info.PlatformVersion, - "arch": info.KernelArch, - "archVersion": info.KernelVersion, - "os": info.OS, - "hostname": info.Hostname, - "bootTime": int64(bootTime), - "processId": os.Getpid(), - "runArch": runtime.GOARCH, - "runVersion": runtime.Version(), - "runTime": int64(runTime), - } -} - -// TimeInfo 系统时间信息 -func (s *SystemInfoImpl) TimeInfo() map[string]string { - now := time.Now() - // 获取当前时间 - current := now.Format("2006-01-02 15:04:05") - // 获取时区 - timezone := now.Format("-0700 MST") - // 获取时区名称 - timezoneName := now.Format("MST") - - return map[string]string{ - "current": current, - "timezone": timezone, - "timezoneName": timezoneName, - } -} - -// MemoryInfo 内存信息 -func (s *SystemInfoImpl) MemoryInfo() map[string]any { - memInfo, err := mem.VirtualMemory() - if err != nil { - memInfo.UsedPercent = 0 - memInfo.Available = 0 - memInfo.Total = 0 - } - - var memStats runtime.MemStats - runtime.ReadMemStats(&memStats) - - return map[string]any{ - "usage": fmt.Sprintf("%.2f", memInfo.UsedPercent), // 内存利用率 - "freemem": parse.Bit(float64(memInfo.Available)), // 可用内存大小(GB) - "totalmem": parse.Bit(float64(memInfo.Total)), // 总内存大小(GB) - "rss": parse.Bit(float64(memStats.Sys)), // 常驻内存大小(RSS) - "heapTotal": parse.Bit(float64(memStats.HeapSys)), // 堆总大小 - "heapUsed": parse.Bit(float64(memStats.HeapAlloc)), // 堆已使用大小 - "external": parse.Bit(float64(memStats.Sys - memStats.HeapSys)), // 外部内存大小(非堆) - } -} - -// CPUInfo CPU信息 -func (s *SystemInfoImpl) CPUInfo() map[string]any { - var core int = 0 - var speed string = "未知" - var model string = "未知" - cpuInfo, err := cpu.Info() - if err == nil { - core = runtime.NumCPU() - speed = fmt.Sprintf("%.0fMHz", cpuInfo[0].Mhz) - model = strings.TrimSpace(cpuInfo[0].ModelName) - } - - useds := []string{} - cpuPercent, err := cpu.Percent(0, true) - if err == nil { - for _, v := range cpuPercent { - useds = append(useds, fmt.Sprintf("%.2f", v)) - } - } - - return map[string]any{ - "model": model, - "speed": speed, - "core": core, - "coreUsed": useds, - } -} - -// NetworkInfo 网络信息 -func (s *SystemInfoImpl) NetworkInfo() map[string]string { - ipAddrs := make(map[string]string) - interfaces, err := net.Interfaces() - if err == nil { - for _, iface := range interfaces { - name := iface.Name - if name[len(name)-1] == '0' { - name = name[0 : len(name)-1] - name = strings.Trim(name, "") - } - // ignore localhost - if name == "lo" { - continue - } - var addrs []string - for _, v := range iface.Addrs { - prefix := strings.Split(v.Addr, "/")[0] - if strings.Contains(prefix, "::") { - addrs = append(addrs, fmt.Sprintf("IPv6 %s", prefix)) - } - if strings.Contains(prefix, ".") { - addrs = append(addrs, fmt.Sprintf("IPv4 %s", prefix)) - } - } - ipAddrs[name] = strings.Join(addrs, " / ") - } - } - return ipAddrs -} - -// DiskInfo 磁盘信息 -func (s *SystemInfoImpl) DiskInfo() []map[string]string { - disks := make([]map[string]string, 0) - - partitions, err := disk.Partitions(false) - if err != nil { - return disks - } - - for _, partition := range partitions { - usage, err := disk.Usage(partition.Mountpoint) - if err != nil { - continue - } - disks = append(disks, map[string]string{ - "size": parse.Bit(float64(usage.Total)), - "used": parse.Bit(float64(usage.Used)), - "avail": parse.Bit(float64(usage.Free)), - "pcent": fmt.Sprintf("%.1f%%", usage.UsedPercent), - "target": partition.Device, - }) - } - return disks -} diff --git a/src/modules/network_data/controller/all_alarm.go b/src/modules/network_data/controller/all_alarm.go index da67adaf..2372de98 100644 --- a/src/modules/network_data/controller/all_alarm.go +++ b/src/modules/network_data/controller/all_alarm.go @@ -15,19 +15,17 @@ import ( ) // 实例化控制层 AlarmController 结构体 -var NewAlarmController = &AlarmController{ - neInfoService: neService.NewNeInfoImpl, - alarmService: neDataService.NewAlarmImpl, +var NewAlarm = &AlarmController{ + neInfoService: neService.NewNeInfo, + alarmService: neDataService.NewAlarm, } // 告警数据 // // PATH /alarm type AlarmController struct { - // 网元信息服务 - neInfoService neService.INeInfo - // 告警信息服务 - alarmService neDataService.IAlarm + neInfoService *neService.NeInfo // 网元信息服务 + alarmService *neDataService.Alarm // 告警信息服务 } // 告警列表 diff --git a/src/modules/network_data/controller/all_kpi.go b/src/modules/network_data/controller/all_kpi.go index 221eb655..e3dbc720 100644 --- a/src/modules/network_data/controller/all_kpi.go +++ b/src/modules/network_data/controller/all_kpi.go @@ -11,19 +11,17 @@ import ( ) // 实例化控制层 PerfKPIController 结构体 -var NewPerfKPIController = &PerfKPIController{ - neInfoService: neService.NewNeInfoImpl, - perfKPIService: neDataService.NewPerfKPIImpl, +var NewPerfKPI = &PerfKPIController{ + neInfoService: neService.NewNeInfo, + perfKPIService: neDataService.NewPerfKPI, } // 性能统计 // // PATH /kpi type PerfKPIController struct { - // 网元信息服务 - neInfoService neService.INeInfo - // 统计信息服务 - perfKPIService neDataService.IPerfKPI + neInfoService *neService.NeInfo // 网元信息服务 + perfKPIService *neDataService.PerfKPI // 统计信息服务 } // 获取统计数据 diff --git a/src/modules/network_data/controller/amf.go b/src/modules/network_data/controller/amf.go index fbc7142f..cc78b1e7 100644 --- a/src/modules/network_data/controller/amf.go +++ b/src/modules/network_data/controller/amf.go @@ -22,19 +22,17 @@ import ( ) // 实例化控制层 AMFController 结构体 -var NewAMFController = &AMFController{ - neInfoService: neService.NewNeInfoImpl, - ueEventService: neDataService.NewUEEventAMFImpl, +var NewAMF = &AMFController{ + neInfoService: neService.NewNeInfo, + ueEventService: neDataService.NewUEEventAMF, } // 网元AMF // // PATH /amf type AMFController struct { - // 网元信息服务 - neInfoService neService.INeInfo - // UE会话事件服务 - ueEventService neDataService.IUEEventAMF + neInfoService *neService.NeInfo // 网元信息服务 + ueEventService *neDataService.UEEventAMF // UE会话事件服务 } // UE会话列表 @@ -121,11 +119,11 @@ func (s *AMFController) UEExport(c *gin.Context) { "E1": "Time", } // 读取字典数据 UE 事件类型 - dictUEEventType := sysService.NewSysDictDataImpl.SelectDictDataByType("ue_event_type") + dictUEEventType := sysService.NewSysDictData.SelectDictDataByType("ue_event_type") // 读取字典数据 UE 事件认证代码类型 - dictUEAauthCode := sysService.NewSysDictDataImpl.SelectDictDataByType("ue_auth_code") + dictUEAauthCode := sysService.NewSysDictData.SelectDictDataByType("ue_auth_code") // 读取字典数据 UE 事件CM状态 - dictUEEventCmState := sysService.NewSysDictDataImpl.SelectDictDataByType("ue_event_cm_state") + dictUEEventCmState := sysService.NewSysDictData.SelectDictDataByType("ue_event_cm_state") // 从第二行开始的数据 dataCells := make([]map[string]any, 0) for i, row := range rows { @@ -180,7 +178,7 @@ func (s *AMFController) UEExport(c *gin.Context) { timeStr = v.(string) } if v, ok := eventJSON["status"]; ok && v != nil { - eventResult = v.(string) + eventResult = fmt.Sprint(v) for _, v := range dictUEEventCmState { if eventResult == v.DictValue { eventResult = i18n.TKey(language, v.DictLabel) diff --git a/src/modules/network_data/controller/ims.go b/src/modules/network_data/controller/ims.go index feef3813..60aff2bb 100644 --- a/src/modules/network_data/controller/ims.go +++ b/src/modules/network_data/controller/ims.go @@ -23,19 +23,17 @@ import ( ) // 实例化控制层 IMSController 结构体 -var NewIMSController = &IMSController{ - neInfoService: neService.NewNeInfoImpl, - cdrEventService: neDataService.NewCDREventIMSImpl, +var NewIMS = &IMSController{ + neInfoService: neService.NewNeInfo, + cdrEventService: neDataService.NewCDREventIMS, } // 网元IMS // // PATH /ims type IMSController struct { - // 网元信息服务 - neInfoService neService.INeInfo - // CDR会话事件服务 - cdrEventService neDataService.ICDREventIMS + neInfoService *neService.NeInfo // 网元信息服务 + cdrEventService *neDataService.CDREventIMS // CDR会话事件服务 } // CDR会话列表 @@ -49,13 +47,13 @@ func (s *IMSController) CDRList(c *gin.Context) { return } - // 查询网元获取IP - // neInfo := s.neInfoService.SelectNeInfoByNeTypeAndNeID(querys.NeType, querys.NeID) - // if neInfo.NeId != querys.NeID || neInfo.IP == "" { - // c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) - // return - // } - // querys.RmUID = neInfo.RmUID + // 查询网元信息 rmUID + neInfo := s.neInfoService.SelectNeInfoByNeTypeAndNeID(querys.NeType, querys.NeID) + if neInfo.NeId != querys.NeID || neInfo.IP == "" { + c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) + return + } + querys.RmUID = neInfo.RmUID // 查询数据 data := s.cdrEventService.SelectPage(querys) @@ -103,6 +101,13 @@ func (s *IMSController) CDRExport(c *gin.Context) { if querys.PageSize > 10000 { querys.PageSize = 10000 } + // 查询网元信息 rmUID + neInfo := s.neInfoService.SelectNeInfoByNeTypeAndNeID(querys.NeType, querys.NeID) + if neInfo.NeId != querys.NeID || neInfo.IP == "" { + c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) + return + } + querys.RmUID = neInfo.RmUID data := s.cdrEventService.SelectPage(querys) if parse.Number(data["total"]) == 0 { // 导出数据记录为空 @@ -118,16 +123,16 @@ func (s *IMSController) CDRExport(c *gin.Context) { "A1": "ID", "B1": "Record Behavior", "C1": "Type", - "D1": "Called", - "E1": "Caller", + "D1": "Caller", + "E1": "Called", "F1": "Duration", "G1": "Result", "H1": "Time", } // 读取字典数据 CDR SIP响应代码类别类型 - dictCDRSipCode := sysService.NewSysDictDataImpl.SelectDictDataByType("cdr_sip_code") + dictCDRSipCode := sysService.NewSysDictData.SelectDictDataByType("cdr_sip_code") // 读取字典数据 CDR 呼叫类型 - dictCDRCallType := sysService.NewSysDictDataImpl.SelectDictDataByType("cdr_call_type") + dictCDRCallType := sysService.NewSysDictData.SelectDictDataByType("cdr_call_type") // 从第二行开始的数据 dataCells := make([]map[string]any, 0) for i, row := range rows { @@ -193,8 +198,8 @@ func (s *IMSController) CDRExport(c *gin.Context) { "A" + idx: row.ID, "B" + idx: recordType, "C" + idx: callTypeLable, - "D" + idx: called, - "E" + idx: caller, + "D" + idx: caller, + "E" + idx: called, "F" + idx: duration, "G" + idx: callResult, "H" + idx: timeStr, diff --git a/src/modules/network_data/controller/mme.go b/src/modules/network_data/controller/mme.go index a7707a18..ac26d60b 100644 --- a/src/modules/network_data/controller/mme.go +++ b/src/modules/network_data/controller/mme.go @@ -23,19 +23,17 @@ import ( ) // 实例化控制层 MMEController 结构体 -var NewMMEController = &MMEController{ - neInfoService: neService.NewNeInfoImpl, - ueEventService: neDataService.NewUEEventMMEImpl, +var NewMME = &MMEController{ + neInfoService: neService.NewNeInfo, + ueEventService: neDataService.NewUEEventMME, } // 网元MME // // PATH /mme type MMEController struct { - // 网元信息服务 - neInfoService neService.INeInfo - // UE会话事件服务 - ueEventService neDataService.IUEEventMME + neInfoService *neService.NeInfo // 网元信息服务 + ueEventService *neDataService.UEEventMME // UE会话事件服务 } // UE会话列表 @@ -122,11 +120,11 @@ func (s *MMEController) UEExport(c *gin.Context) { "E1": "Time", } // 读取字典数据 UE 事件类型 - dictUEEventType := sysService.NewSysDictDataImpl.SelectDictDataByType("ue_event_type") + dictUEEventType := sysService.NewSysDictData.SelectDictDataByType("ue_event_type") // 读取字典数据 UE 事件认证代码类型 - dictUEAauthCode := sysService.NewSysDictDataImpl.SelectDictDataByType("ue_auth_code") + dictUEAauthCode := sysService.NewSysDictData.SelectDictDataByType("ue_auth_code") // 读取字典数据 UE 事件CM状态 - dictUEEventCmState := sysService.NewSysDictDataImpl.SelectDictDataByType("ue_event_cm_state") + dictUEEventCmState := sysService.NewSysDictData.SelectDictDataByType("ue_event_cm_state") // 从第二行开始的数据 dataCells := make([]map[string]any, 0) for i, row := range rows { diff --git a/src/modules/network_data/controller/smf.go b/src/modules/network_data/controller/smf.go index 2e033386..094b4843 100644 --- a/src/modules/network_data/controller/smf.go +++ b/src/modules/network_data/controller/smf.go @@ -15,25 +15,26 @@ import ( "be.ems/src/framework/vo/result" "be.ems/src/modules/network_data/model" neDataService "be.ems/src/modules/network_data/service" + neFetchlink "be.ems/src/modules/network_element/fetch_link" neService "be.ems/src/modules/network_element/service" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" ) // 实例化控制层 SMFController 结构体 -var NewSMFController = &SMFController{ - neInfoService: neService.NewNeInfoImpl, - cdrEventService: neDataService.NewCDREventSMFImpl, +var NewSMF = &SMFController{ + neInfoService: neService.NewNeInfo, + cdrEventService: neDataService.NewCDREventSMF, + udmUserInfoService: neDataService.NewUDMUserInfo, } // 网元SMF // // PATH /smf type SMFController struct { - // 网元信息服务 - neInfoService neService.INeInfo - // CDR会话事件服务 - cdrEventService neDataService.ICDREventSMF + neInfoService *neService.NeInfo // 网元信息服务 + cdrEventService *neDataService.CDREventSMF // CDR会话事件服务 + udmUserInfoService *neDataService.UDMUserInfo // UDM用户信息服务 } // CDR会话列表 @@ -47,13 +48,13 @@ func (s *SMFController) CDRList(c *gin.Context) { return } - // 查询网元获取IP - // neInfo := s.neInfoService.SelectNeInfoByNeTypeAndNeID(querys.NeType, querys.NeID) - // if neInfo.NeId != querys.NeID || neInfo.IP == "" { - // c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) - // return - // } - // querys.RmUID = neInfo.RmUID + // 查询网元信息 rmUID + neInfo := s.neInfoService.SelectNeInfoByNeTypeAndNeID(querys.NeType, querys.NeID) + if neInfo.NeId != querys.NeID || neInfo.IP == "" { + c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) + return + } + querys.RmUID = neInfo.RmUID // 查询数据 data := s.cdrEventService.SelectPage(querys) @@ -101,6 +102,13 @@ func (s *SMFController) CDRExport(c *gin.Context) { if querys.PageSize > 10000 { querys.PageSize = 10000 } + // 查询网元信息 rmUID + neInfo := s.neInfoService.SelectNeInfoByNeTypeAndNeID(querys.NeType, querys.NeID) + if neInfo.NeId != querys.NeID || neInfo.IP == "" { + c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) + return + } + querys.RmUID = neInfo.RmUID data := s.cdrEventService.SelectPage(querys) if parse.Number(data["total"]) == 0 { // 导出数据记录为空 @@ -115,17 +123,30 @@ func (s *SMFController) CDRExport(c *gin.Context) { headerCells := map[string]string{ "A1": "ID", "B1": "Charging ID", - "C1": "Subscriber ID Data", - "D1": "Subscriber ID Type", - "E1": "Data Volume Uplink", - "F1": "Data Volume Downlink", - "G1": "Data Total Volume", - "H1": "Duration", - "I1": "Invocation Time", - "J1": "PDU Session Charging Information", + "C1": "NE Name", + "D1": "Resource Unique ID", + "E1": "Subscriber ID Data", + "F1": "Subscriber ID Type", + "G1": "Data Volume Uplink", + "H1": "Data Volume Downlink", + "I1": "Data Total Volume", + "J1": "Duration", + "K1": "Invocation Time", + "L1": "User Identifier", + "M1": "SSC Mode", + "N1": "DNN ID", + "O1": "PDU Type", + "P1": "RAT Type", + "Q1": "PDU IPv4 Address", + "R1": "Network Function IPv4", + "S1": "PDU IPv6 Address Swith Prefix", + "T1": "Record Network Function ID", + "U1": "Record Type", + "V1": "Record Opening Time", } // 从第二行开始的数据 dataCells := make([]map[string]any, 0) + for i, row := range rows { idx := strconv.Itoa(i + 2) // 解析 JSON 字符串为 map @@ -150,12 +171,22 @@ func (s *SMFController) CDRExport(c *gin.Context) { subscriptionIDData = sub["subscriptionIDData"].(string) } } + + // 网络功能 IPv4 地址 + networkFunctionIPv4Address := "" + if v, ok := cdrJSON["nFunctionConsumerInformation"]; ok && v != nil { + if conInfo, conInfoOk := v.(map[string]any); conInfoOk && conInfo != nil { + networkFunctionIPv4Address = conInfo["networkFunctionIPv4Address"].(string) + } + } + // 数据量上行链路 dataVolumeUplink := []string{} // 数据量下行链路 dataVolumeDownlink := []string{} // 数据总量 dataTotalVolume := []string{} + if v, ok := cdrJSON["listOfMultipleUnitUsage"]; ok && v != nil { usageList := v.([]any) if len(usageList) > 0 { @@ -190,32 +221,31 @@ func (s *SMFController) CDRExport(c *gin.Context) { invocationTimestamp = v.(string) } // 记录打开时间 - pduSessionChargingInformation := "" + User_Identifier := "" + SSC_Mode := "" + RAT_Type := "" + DNN_ID := "" + PDU_Type := "" + PDU_IPv4 := "" + PDU_IPv6 := "" if v, ok := cdrJSON["pDUSessionChargingInformation"]; ok && v != nil { pduInfo := v.(map[string]any) - User_Identifier := "" if v, ok := pduInfo["userIdentifier"]; ok && v != nil { User_Identifier = v.(string) } - SSC_Mode := "" if v, ok := pduInfo["sSCMode"]; ok && v != nil { SSC_Mode = v.(string) } - RAT_Type := "" if v, ok := pduInfo["rATType"]; ok && v != nil { RAT_Type = v.(string) } - DNN_ID := "" if v, ok := pduInfo["dNNID"]; ok && v != nil { DNN_ID = v.(string) } - PDU_Type := "" if v, ok := pduInfo["pDUType"]; ok && v != nil { PDU_Type = v.(string) } - PDU_IPv4 := "" - PDU_IPv6 := "" if v, ok := pduInfo["pDUAddress"]; ok && v != nil { pDUAddress := v.(map[string]any) if addr, ok := pDUAddress["pDUIPv4Address"]; ok && addr != nil { @@ -226,24 +256,54 @@ func (s *SMFController) CDRExport(c *gin.Context) { } } - pduSessionChargingInformation = fmt.Sprintf(`User Identifier: %s -SSC Mode: %s RAT Type: %s DNN ID: %s -PDU Type: %s -PDU IPv4 Address: %s -PDU IPv6 Addres Swith Prefix: %s`, User_Identifier, SSC_Mode, RAT_Type, DNN_ID, PDU_Type, PDU_IPv4, PDU_IPv6) + // pduSessionChargingInformation = fmt.Sprintf(`User Identifier: %s + // SSC Mode: %s RAT Type: %s DNN ID: %s + // PDU Type: %s + // PDU IPv4 Address: %s + // PDU IPv6 Addres Swith Prefix: %s`, User_Identifier, SSC_Mode, RAT_Type, DNN_ID, PDU_Type, PDU_IPv4, PDU_IPv6) + } + + // 记录网络参数ID + recordNFID := "" + if v, ok := cdrJSON["recordingNetworkFunctionID"]; ok && v != nil { + recordNFID = v.(string) + } + + //记录开始时间 + recordOpeningTime := "" + if v, ok := cdrJSON["recordOpeningTime"]; ok && v != nil { + recordOpeningTime = v.(string) + } + + //记录类型 + recordType := "" + if v, ok := cdrJSON["recordType"]; ok && v != nil { + recordType = v.(string) } dataCells = append(dataCells, map[string]any{ "A" + idx: row.ID, "B" + idx: chargingID, - "C" + idx: subscriptionIDData, - "D" + idx: subscriptionIDType, - "E" + idx: strings.Join(dataVolumeUplink, ","), - "F" + idx: strings.Join(dataVolumeDownlink, ","), - "G" + idx: strings.Join(dataTotalVolume, ","), - "H" + idx: duration, - "I" + idx: invocationTimestamp, - "J" + idx: pduSessionChargingInformation, + "C" + idx: row.NeName, + "D" + idx: row.RmUID, + "E" + idx: subscriptionIDData, + "F" + idx: subscriptionIDType, + "G" + idx: strings.Join(dataVolumeUplink, ","), + "H" + idx: strings.Join(dataVolumeDownlink, ","), + "I" + idx: strings.Join(dataTotalVolume, ","), + "J" + idx: duration, + "K" + idx: invocationTimestamp, + "L" + idx: User_Identifier, + "M" + idx: SSC_Mode, + "N" + idx: DNN_ID, + "O" + idx: PDU_Type, + "P" + idx: RAT_Type, + "Q" + idx: PDU_IPv4, + "R" + idx: networkFunctionIPv4Address, + "S" + idx: PDU_IPv6, + "T" + idx: recordNFID, + "U" + idx: recordType, + "V" + idx: recordOpeningTime, }) } @@ -256,3 +316,62 @@ PDU IPv6 Addres Swith Prefix: %s`, User_Identifier, SSC_Mode, RAT_Type, DNN_ID, c.FileAttachment(saveFilePath, fileName) } + +// 在线订阅用户列表信息 +// +// GET /subscribers +func (s *SMFController) SubUserList(c *gin.Context) { + language := ctx.AcceptLanguage(c) + var query struct { + NeId string `form:"neId" binding:"required"` + IMSI string `form:"imsi"` + MSISDN string `form:"msisdn"` + Upstate string `form:"upstate"` + PageNum string `form:"pageNum"` + } + if err := c.ShouldBindQuery(&query); err != nil { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + + // 查询网元信息 rmUID + neInfo := s.neInfoService.SelectNeInfoByNeTypeAndNeID("SMF", query.NeId) + if neInfo.NeId != query.NeId || neInfo.IP == "" { + c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) + return + } + + // 网元直连 + data, err := neFetchlink.SMFSubInfoList(neInfo, map[string]string{ + "imsi": query.IMSI, + "msisdn": query.MSISDN, + "upstate": query.Upstate, + "pageNum": query.PageNum, + }) + if err != nil { + c.JSON(200, result.ErrMsg(err.Error())) + return + } + + // 对数据进行处理,去掉前缀,并加入imsi拓展信息 + rows := data["rows"].([]any) + if len(rows) > 0 { + arr := &rows + for i := range *arr { + item := (*arr)[i].(map[string]any) + if v, ok := item["imsi"]; ok && v != nil { + imsiStr := v.(string) + imsiStr = strings.TrimPrefix(imsiStr, "imsi-") + item["imsi"] = imsiStr + // 查UDM拓展信息 + info := s.udmUserInfoService.SelectByIMSIAndNeID(imsiStr, "") + item["remark"] = info.Remark + } + if v, ok := item["msisdn"]; ok && v != nil { + item["msisdn"] = strings.TrimPrefix(v.(string), "msisdn-") + } + } + } + + c.JSON(200, result.Ok(data)) +} diff --git a/src/modules/network_data/controller/smsc.go b/src/modules/network_data/controller/smsc.go new file mode 100644 index 00000000..167d097a --- /dev/null +++ b/src/modules/network_data/controller/smsc.go @@ -0,0 +1,195 @@ +package controller + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + "time" + + "be.ems/src/framework/i18n" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/ctx" + "be.ems/src/framework/utils/date" + "be.ems/src/framework/utils/file" + "be.ems/src/framework/utils/parse" + "be.ems/src/framework/vo/result" + "be.ems/src/modules/network_data/model" + neDataService "be.ems/src/modules/network_data/service" + neService "be.ems/src/modules/network_element/service" + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" +) + +// 实例化控制层 SMSCController 结构体 +var NewSMSC = &SMSCController{ + neInfoService: neService.NewNeInfo, + cdrEventService: neDataService.NewCDREventSMSC, +} + +// 网元SMSC +// +// PATH /smsc +type SMSCController struct { + neInfoService *neService.NeInfo // 网元信息服务 + cdrEventService *neDataService.CDREventSMSC // CDR会话事件服务 +} + +// CDR会话列表 +// +// GET /cdr/list +func (s *SMSCController) CDRList(c *gin.Context) { + language := ctx.AcceptLanguage(c) + var querys model.CDREventSMSCQuery + if err := c.ShouldBindQuery(&querys); err != nil { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + + // 查询网元信息 rmUID + neInfo := s.neInfoService.SelectNeInfoByNeTypeAndNeID(querys.NeType, querys.NeID) + if neInfo.NeId != querys.NeID || neInfo.IP == "" { + c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) + return + } + querys.RmUID = neInfo.RmUID + + // 查询数据 + data := s.cdrEventService.SelectPage(querys) + c.JSON(200, result.Ok(data)) +} + +// CDR会话删除 +// +// DELETE /cdr/:cdrIds +func (s *SMSCController) CDRRemove(c *gin.Context) { + language := ctx.AcceptLanguage(c) + cdrIds := c.Param("cdrIds") + if cdrIds == "" { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + // 处理字符转id数组后去重 + ids := strings.Split(cdrIds, ",") + uniqueIDs := parse.RemoveDuplicates(ids) + if len(uniqueIDs) <= 0 { + c.JSON(200, result.Err(nil)) + return + } + rows, err := s.cdrEventService.DeleteByIds(uniqueIDs) + if err != nil { + c.JSON(200, result.ErrMsg(i18n.TKey(language, err.Error()))) + return + } + msg := i18n.TTemplate(language, "app.common.deleteSuccess", map[string]any{"num": rows}) + c.JSON(200, result.OkMsg(msg)) +} + +// CDR会话列表导出 +// +// POST /cdr/export +func (s *SMSCController) CDRExport(c *gin.Context) { + language := ctx.AcceptLanguage(c) + // 查询结果,根据查询条件结果,单页最大值限制 + var querys model.CDREventSMSCQuery + if err := c.ShouldBindBodyWith(&querys, binding.JSON); err != nil { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + // 限制导出数据集 + if querys.PageSize > 10000 { + querys.PageSize = 10000 + } + // 查询网元信息 rmUID + neInfo := s.neInfoService.SelectNeInfoByNeTypeAndNeID(querys.NeType, querys.NeID) + if neInfo.NeId != querys.NeID || neInfo.IP == "" { + c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) + return + } + querys.RmUID = neInfo.RmUID + data := s.cdrEventService.SelectPage(querys) + if parse.Number(data["total"]) == 0 { + // 导出数据记录为空 + c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.exportEmpty"))) + return + } + rows := data["rows"].([]model.CDREventSMSC) + + // 导出文件名称 + fileName := fmt.Sprintf("smsc_cdr_event_export_%d_%d.xlsx", len(rows), time.Now().UnixMilli()) + // 第一行表头标题 + headerCells := map[string]string{ + "A1": "ID", + "B1": "Record Behavior", + "C1": "Service Type", + "D1": "Caller", + "E1": "Called", + "F1": "Result", + "G1": "Time", + } + // 从第二行开始的数据 + dataCells := make([]map[string]any, 0) + for i, row := range rows { + idx := strconv.Itoa(i + 2) + // 解析 JSON 字符串为 map + var cdrJSON map[string]interface{} + err := json.Unmarshal([]byte(row.CDRJSONStr), &cdrJSON) + if err != nil { + logger.Warnf("CDRExport Error parsing JSON: %s", err.Error()) + continue + } + // 记录类型 + recordType := "" + if v, ok := cdrJSON["recordType"]; ok && v != nil { + recordType = v.(string) + } + // 服务类型 + serviceType := "" + if v, ok := cdrJSON["serviceType"]; ok && v != nil { + serviceType = v.(string) + } + // 被叫 + called := "" + if v, ok := cdrJSON["calledParty"]; ok && v != nil { + called = v.(string) + } + // 主叫 + caller := "" + if v, ok := cdrJSON["callerParty"]; ok && v != nil { + caller = v.(string) + } + // 呼叫结果 0失败,1成功 + callResult := "Fail" + if v, ok := cdrJSON["result"]; ok && v != nil { + resultVal := parse.Number(v) + if resultVal == 1 { + callResult = "Success" + } + } + // 取时间 + timeStr := "" + if v, ok := cdrJSON["updateTime"]; ok && v != nil { + releaseTime := parse.Number(v) + timeStr = date.ParseDateToStr(releaseTime, date.YYYY_MM_DDTHH_MM_SSZ) + } + + dataCells = append(dataCells, map[string]any{ + "A" + idx: row.ID, + "B" + idx: recordType, + "C" + idx: serviceType, + "D" + idx: caller, + "E" + idx: called, + "F" + idx: callResult, + "G" + idx: timeStr, + }) + } + + // 导出数据表格 + saveFilePath, err := file.WriteSheet(headerCells, dataCells, fileName, "") + if err != nil { + c.JSON(200, result.ErrMsg(err.Error())) + return + } + + c.FileAttachment(saveFilePath, fileName) +} diff --git a/src/modules/network_data/controller/udm_auth.go b/src/modules/network_data/controller/udm_auth.go index d916957c..90ee8769 100644 --- a/src/modules/network_data/controller/udm_auth.go +++ b/src/modules/network_data/controller/udm_auth.go @@ -8,10 +8,10 @@ import ( "be.ems/src/framework/constants/uploadsubpath" "be.ems/src/framework/i18n" + "be.ems/src/framework/telnet" "be.ems/src/framework/utils/ctx" "be.ems/src/framework/utils/file" "be.ems/src/framework/utils/parse" - "be.ems/src/framework/utils/telnet" "be.ems/src/framework/vo/result" "be.ems/src/modules/network_data/model" neDataService "be.ems/src/modules/network_data/service" @@ -23,18 +23,16 @@ import ( // 实例化控制层 UDMAuthController 结构体 var NewUDMAuth = &UDMAuthController{ - udmAuthService: neDataService.NewUDMAuthImpl, - neInfoService: neService.NewNeInfoImpl, + udmAuthService: neDataService.NewUDMAuthUser, + neInfoService: neService.NewNeInfo, } // UDM鉴权用户 // // PATH /udm/auth type UDMAuthController struct { - // UDM鉴权信息服务 - udmAuthService neDataService.IUDMAuth - // 网元信息服务 - neInfoService neService.INeInfo + udmAuthService *neDataService.UDMAuthUser // UDM鉴权信息服务 + neInfoService *neService.NeInfo // 网元信息服务 } // UDM鉴权用户重载数据 @@ -48,7 +46,6 @@ func (s *UDMAuthController) ResetData(c *gin.Context) { return } - neId = "" data := s.udmAuthService.ResetData(neId) c.JSON(200, result.OkData(data)) } @@ -58,9 +55,8 @@ func (s *UDMAuthController) ResetData(c *gin.Context) { // GET /list func (s *UDMAuthController) List(c *gin.Context) { querys := ctx.QueryMap(c) - querys["neId"] = "" - data := s.udmAuthService.SelectPage(querys) - c.JSON(200, result.Ok(data)) + total, rows := s.udmAuthService.SelectPage(querys) + c.JSON(200, result.Ok(map[string]any{"total": total, "rows": rows})) } // UDM鉴权用户信息 @@ -81,7 +77,7 @@ func (s *UDMAuthController) Info(c *gin.Context) { c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) return } - + // 网元主机的Telnet客户端 telnetClient, err := s.neInfoService.NeRunTelnetClient("UDM", neId, 1) if err != nil { c.JSON(200, result.ErrMsg(err.Error())) @@ -102,27 +98,9 @@ func (s *UDMAuthController) Info(c *gin.Context) { return } - neId = "" - u := model.UDMAuth{ - IMSI: imsi, - Amf: data["amf"], - Status: "1", - Ki: data["ki"], - AlgoIndex: data["algo"], - Opc: data["opc"], - NeId: neId, - } - - // 查询imsi存在赋予id用于更新 - list := s.udmAuthService.SelectList(u) - if len(list) > 0 { - item := list[0] - if item.ID != "" { - u.ID = item.ID - } - } - go s.udmAuthService.Insert(neId, u) - + // 解析返回的数据 + u := s.udmAuthService.ParseInfo(imsi, neId, data) + s.udmAuthService.Insert(neId, u) c.JSON(200, result.OkData(u)) } @@ -137,7 +115,7 @@ func (s *UDMAuthController) Add(c *gin.Context) { return } - var body model.UDMAuth + var body model.UDMAuthUser err := c.ShouldBindBodyWith(&body, binding.JSON) if err != nil || body.IMSI == "" { c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) @@ -150,7 +128,7 @@ func (s *UDMAuthController) Add(c *gin.Context) { c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) return } - + // 网元主机的Telnet客户端 telnetClient, err := s.neInfoService.NeRunTelnetClient("UDM", neId, 1) if err != nil { c.JSON(200, result.ErrMsg(err.Error())) @@ -159,7 +137,8 @@ func (s *UDMAuthController) Add(c *gin.Context) { defer telnetClient.Close() // 发送MML - cmd := fmt.Sprintf("add authdat:imsi=%s,ki=%s,amf=%s,algo=%s,opc=%s", body.IMSI, body.Ki, body.Amf, body.AlgoIndex, body.Opc) + cmd := fmt.Sprintf("add authdat:imsi=%s,", body.IMSI) + cmd += s.udmAuthService.ParseCommandParams(body) data, err := telnet.ConvertToStr(telnetClient, cmd) if err != nil { c.JSON(200, result.ErrMsg(err.Error())) @@ -168,8 +147,7 @@ func (s *UDMAuthController) Add(c *gin.Context) { // 命令ok时 if strings.Contains(data, "ok") { - neId = "" - go s.udmAuthService.Insert(neId, body) + s.udmAuthService.Insert(neId, body) } c.JSON(200, result.OkData(data)) } @@ -186,7 +164,7 @@ func (s *UDMAuthController) Adds(c *gin.Context) { return } - var body model.UDMAuth + var body model.UDMAuthUser err := c.ShouldBindBodyWith(&body, binding.JSON) if err != nil || body.IMSI == "" { c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) @@ -199,7 +177,7 @@ func (s *UDMAuthController) Adds(c *gin.Context) { c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) return } - + // 网元主机的Telnet客户端 telnetClient, err := s.neInfoService.NeRunTelnetClient("UDM", neId, 1) if err != nil { c.JSON(200, result.ErrMsg(err.Error())) @@ -208,7 +186,8 @@ func (s *UDMAuthController) Adds(c *gin.Context) { defer telnetClient.Close() // 发送MML - cmd := fmt.Sprintf("baa authdat:start_imsi=%s,sub_num=%s,ki=%s,amf=%s,algo=%s,opc=%s", body.IMSI, num, body.Ki, body.Amf, body.AlgoIndex, body.Opc) + cmd := fmt.Sprintf("baa authdat:start_imsi=%s,sub_num=%s,", body.IMSI, num) + cmd += s.udmAuthService.ParseCommandParams(body) data, err := telnet.ConvertToStr(telnetClient, cmd) if err != nil { c.JSON(200, result.ErrMsg(err.Error())) @@ -217,8 +196,7 @@ func (s *UDMAuthController) Adds(c *gin.Context) { // 命令ok时 if strings.Contains(data, "ok") { - neId = "" - go s.udmAuthService.LoadData(neId, body.IMSI, num) + s.udmAuthService.LoadData(neId, body.IMSI, num) } c.JSON(200, result.OkData(data)) } @@ -234,7 +212,7 @@ func (s *UDMAuthController) Edit(c *gin.Context) { return } - var body model.UDMAuth + var body model.UDMAuthUser err := c.ShouldBindBodyWith(&body, binding.JSON) if err != nil || body.IMSI == "" { c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) @@ -247,7 +225,7 @@ func (s *UDMAuthController) Edit(c *gin.Context) { c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) return } - + // 网元主机的Telnet客户端 telnetClient, err := s.neInfoService.NeRunTelnetClient("UDM", neId, 1) if err != nil { c.JSON(200, result.ErrMsg(err.Error())) @@ -256,20 +234,8 @@ func (s *UDMAuthController) Edit(c *gin.Context) { defer telnetClient.Close() // 发送MML - cmd := fmt.Sprintf("mod authdata:imsi=%s", body.IMSI) - // 修改的参数名称 - if body.Ki != "" { - cmd += fmt.Sprintf(",ki=%s", body.Ki) - } - if body.Amf != "" { - cmd += fmt.Sprintf(",amf=%s", body.Amf) - } - if body.AlgoIndex != "" { - cmd += fmt.Sprintf(",algo=%s", body.AlgoIndex) - } - if body.Opc != "" { - cmd += fmt.Sprintf(",opc=%s", body.Opc) - } + cmd := fmt.Sprintf("mod authdata:imsi=%s,", body.IMSI) + cmd += s.udmAuthService.ParseCommandParams(body) data, err := telnet.ConvertToStr(telnetClient, cmd) if err != nil { c.JSON(200, result.ErrMsg(err.Error())) @@ -278,8 +244,7 @@ func (s *UDMAuthController) Edit(c *gin.Context) { // 命令ok时 if strings.Contains(data, "ok") { - neId = "" - go s.udmAuthService.Insert(neId, body) + s.udmAuthService.Insert(neId, body) } c.JSON(200, result.OkData(data)) } @@ -310,7 +275,7 @@ func (s *UDMAuthController) Remove(c *gin.Context) { c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) return } - + // 网元主机的Telnet客户端 telnetClient, err := s.neInfoService.NeRunTelnetClient("UDM", neId, 1) if err != nil { c.JSON(200, result.ErrMsg(err.Error())) @@ -329,8 +294,7 @@ func (s *UDMAuthController) Remove(c *gin.Context) { } // 命令ok时 if strings.Contains(data, "ok") { - neId = "" - go s.udmAuthService.Delete(neId, imsi) + s.udmAuthService.Delete(imsi, neId) } resultData[imsi] = data } @@ -357,7 +321,7 @@ func (s *UDMAuthController) Removes(c *gin.Context) { c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) return } - + // 网元主机的Telnet客户端 telnetClient, err := s.neInfoService.NeRunTelnetClient("UDM", neId, 1) if err != nil { c.JSON(200, result.ErrMsg(err.Error())) @@ -375,8 +339,7 @@ func (s *UDMAuthController) Removes(c *gin.Context) { // 命令ok时 if strings.Contains(data, "ok") { - neId = "" - go s.udmAuthService.LoadData(neId, imsi, num) + s.udmAuthService.LoadData(neId, imsi, num) } c.JSON(200, result.OkData(data)) } @@ -386,32 +349,44 @@ func (s *UDMAuthController) Removes(c *gin.Context) { // POST /export func (s *UDMAuthController) Export(c *gin.Context) { language := ctx.AcceptLanguage(c) - var body struct { - NeId string `json:"neId" binding:"required"` - Type string `json:"type" binding:"required"` - } - err := c.ShouldBindBodyWith(&body, binding.JSON) - if err != nil { + // 查询结果,根据查询条件结果,单页最大值限制 + querys := ctx.BodyJSONMap(c) + neId := querys["neId"].(string) + fileType := querys["type"].(string) + if neId == "" || fileType == "" { c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) return } - - if !(body.Type == "csv" || body.Type == "txt") { - c.JSON(200, result.ErrMsg(i18n.TKey(language, "ne.udm.errExportType"))) + if !(fileType == "csv" || fileType == "txt") { + c.JSON(200, result.ErrMsg(i18n.TKey(language, "ne.udm.errImportUserSubFileFormat"))) return } - neId := "" - list := s.udmAuthService.SelectList(model.UDMAuth{NeId: neId}) - // 文件名 - fileName := fmt.Sprintf("udm_auth_user_export_%s_%d.%s", neId, time.Now().UnixMilli(), body.Type) - filePath := fmt.Sprintf("%s/%s", file.ParseUploadFileDir(uploadsubpath.EXPORT), fileName) + querys["pageNum"] = 1 + querys["pageSize"] = 10000 + total, rows := s.udmAuthService.SelectPage(querys) + if total == 0 { + // 导出数据记录为空 + c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.exportEmpty"))) + return + } - if body.Type == "csv" { + // rows := s.udmAuthService.SelectList(model.UDMAuthUser{NeId: neId}) + if len(rows) <= 0 { + // 导出数据记录为空 + c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.exportEmpty"))) + return + } + + // 文件名 + fileName := fmt.Sprintf("udm_auth_user_export_%s_%d.%s", neId, time.Now().UnixMilli(), fileType) + filePath := filepath.Join(file.ParseUploadFileDir(uploadsubpath.EXPORT), fileName) + + if fileType == "csv" { // 转换数据 data := [][]string{} data = append(data, []string{"imsi", "ki", "algo", "amf", "opc"}) - for _, v := range list { + for _, v := range rows { opc := v.Opc if opc == "-" { opc = "" @@ -426,10 +401,10 @@ func (s *UDMAuthController) Export(c *gin.Context) { } } - if body.Type == "txt" { + if fileType == "txt" { // 转换数据 data := [][]string{} - for _, v := range list { + for _, v := range rows { opc := v.Opc if opc == "-" { opc = "" @@ -437,8 +412,8 @@ func (s *UDMAuthController) Export(c *gin.Context) { data = append(data, []string{v.IMSI, v.Ki, v.AlgoIndex, v.Amf, opc}) } // 输出到文件 - err = file.WriterFileTXT(data, ",", filePath) - if err != nil { + + if err := file.WriterFileTXT(data, ",", filePath); err != nil { c.JSON(200, result.ErrMsg(err.Error())) return } @@ -534,13 +509,11 @@ func (s *UDMAuthController) Import(c *gin.Context) { if strings.Contains(resultMsg, "ok") { if strings.HasSuffix(body.UploadPath, ".csv") { data := file.ReadFileCSV(localFilePath) - neId := "" - go s.udmAuthService.InsertData(neId, "csv", data) + go s.udmAuthService.InsertData(neInfo.NeId, "csv", data) } if strings.HasSuffix(body.UploadPath, ".txt") { data := file.ReadFileTXT(",", localFilePath) - neId := "" - go s.udmAuthService.InsertData(neId, "txt", data) + go s.udmAuthService.InsertData(neInfo.NeId, "txt", data) } } c.JSON(200, result.OkMsg(resultMsg)) diff --git a/src/modules/network_data/controller/udm_sub.go b/src/modules/network_data/controller/udm_sub.go index 62f57ac6..1c611178 100644 --- a/src/modules/network_data/controller/udm_sub.go +++ b/src/modules/network_data/controller/udm_sub.go @@ -3,16 +3,15 @@ package controller import ( "fmt" "path/filepath" - "strconv" "strings" "time" "be.ems/src/framework/constants/uploadsubpath" "be.ems/src/framework/i18n" + "be.ems/src/framework/telnet" "be.ems/src/framework/utils/ctx" "be.ems/src/framework/utils/file" "be.ems/src/framework/utils/parse" - "be.ems/src/framework/utils/telnet" "be.ems/src/framework/vo/result" "be.ems/src/modules/network_data/model" neDataService "be.ems/src/modules/network_data/service" @@ -23,18 +22,16 @@ import ( // 实例化控制层 UDMSubController 结构体 var NewUDMSub = &UDMSubController{ - udmSubService: neDataService.NewUDMSubImpl, - neInfoService: neService.NewNeInfoImpl, + udmSubService: neDataService.NewUDMSubUser, + neInfoService: neService.NewNeInfo, } // UDM签约用户 // // PATH /udm/sub type UDMSubController struct { - // UDM签约信息服务 - udmSubService neDataService.IUDMSub - // 网元信息服务 - neInfoService neService.INeInfo + udmSubService *neDataService.UDMSubUser // UDM签约信息服务 + neInfoService *neService.NeInfo // 网元信息服务 } // UDM签约用户重载数据 @@ -48,7 +45,6 @@ func (s *UDMSubController) ResetData(c *gin.Context) { return } - neId = "" data := s.udmSubService.ResetData(neId) c.JSON(200, result.OkData(data)) } @@ -58,9 +54,8 @@ func (s *UDMSubController) ResetData(c *gin.Context) { // GET /list func (s *UDMSubController) List(c *gin.Context) { querys := ctx.QueryMap(c) - querys["neId"] = "" - data := s.udmSubService.SelectPage(querys) - c.JSON(200, result.Ok(data)) + total, rows := s.udmSubService.SelectPage(querys) + c.JSON(200, result.Ok(map[string]any{"total": total, "rows": rows})) } // UDM签约用户信息 @@ -81,7 +76,7 @@ func (s *UDMSubController) Info(c *gin.Context) { c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) return } - + // 网元主机的Telnet客户端 telnetClient, err := s.neInfoService.NeRunTelnetClient("UDM", neId, 1) if err != nil { c.JSON(200, result.ErrMsg(err.Error())) @@ -103,52 +98,8 @@ func (s *UDMSubController) Info(c *gin.Context) { } // 解析返回的数据 - cnType, _ := strconv.ParseInt(data["CNType"][:4], 0, 64) - rat, _ := strconv.ParseInt(data["RAT"][:4], 0, 64) - msisdn := data["MSISDN"] - imsMsisdnLen := strings.Index(msisdn, ",") - if imsMsisdnLen != -1 { - msisdn = msisdn[:imsMsisdnLen] - } - neId = "" - u := model.UDMSub{ - IMSI: imsi, - Msisdn: msisdn, - Ambr: data["AMBR"], - Arfb: data["AreaForbidden"], - Cn: fmt.Sprint(cnType), - SmData: data["SM-Data(snssai+dnn[1..n])"], - Sar: data["ServiceAreaRestriction"], - Nssai: data["NSSAI"], - SmfSel: data["Smf-Selection"], - Rat: fmt.Sprint(rat), - NeId: neId, - } - // 1,64,24,65,def_eps,1,2,010200000000,- - if v, ok := data["EPS-Data"]; ok { - u.EpsDat = v - arr := strings.Split(v, ",") - u.EpsFlag = arr[0] - u.EpsOdb = arr[1] - u.HplmnOdb = arr[2] - u.Ard = arr[3] - u.Epstpl = arr[4] - u.ContextId = arr[5] - u.ApnContext = arr[7] - // [6] 是不要的,导入和导出不用 - u.StaticIp = arr[8] - } - - // 查询imsi存在赋予id用于更新 - list := s.udmSubService.SelectList(u) - if len(list) > 0 { - item := list[0] - if item.ID != "" { - u.ID = item.ID - } - } - go s.udmSubService.Insert(neId, u) - + u := s.udmSubService.ParseInfo(imsi, neId, data) + s.udmSubService.Insert(neId, u) c.JSON(200, result.OkData(u)) } @@ -163,9 +114,9 @@ func (s *UDMSubController) Add(c *gin.Context) { return } - var body model.UDMSub + var body model.UDMSubUser err := c.ShouldBindBodyWith(&body, binding.JSON) - if err != nil || body.IMSI == "" { + if err != nil || len(body.IMSI) < 15 { c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) return } @@ -176,7 +127,7 @@ func (s *UDMSubController) Add(c *gin.Context) { c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) return } - + // 网元主机的Telnet客户端 telnetClient, err := s.neInfoService.NeRunTelnetClient("UDM", neId, 1) if err != nil { c.JSON(200, result.ErrMsg(err.Error())) @@ -185,12 +136,8 @@ func (s *UDMSubController) Add(c *gin.Context) { defer telnetClient.Close() // 发送MML - cmd := fmt.Sprintf("add udmuser:imsi=%s,msisdn=%s,ambr=%s,nssai=%s,arfb=%s,sar=%s,rat=%s,cn=%s,smf_sel=%s,sm_data=%s,eps_flag=%s,eps_odb=%s,hplmn_odb=%s,ard=%s,epstpl=%s,context_id=%s,apn_context=%s", - body.IMSI, body.Msisdn, body.Ambr, body.Nssai, body.Arfb, body.Sar, body.Rat, body.Cn, body.SmfSel, body.SmData, body.EpsFlag, body.EpsOdb, body.HplmnOdb, body.Ard, body.Epstpl, body.ContextId, body.ApnContext) - // static_ip指给4G UE分配的静态IP,没有可不带此字段名,批量添加IP会自动递增 - if body.StaticIp != "" { - cmd += fmt.Sprintf(",static_ip=%s", body.StaticIp) - } + cmd := fmt.Sprintf("add udmuser:imsi=%s,", body.IMSI) + cmd += s.udmSubService.ParseCommandParams(body) data, err := telnet.ConvertToStr(telnetClient, cmd) if err != nil { c.JSON(200, result.ErrMsg(err.Error())) @@ -199,8 +146,8 @@ func (s *UDMSubController) Add(c *gin.Context) { // 命令ok时 if strings.Contains(data, "ok") { - neId = "" - go s.udmSubService.Insert(neId, body) + body.NeId = neId + s.udmSubService.Insert(neId, body) } c.JSON(200, result.OkData(data)) } @@ -217,9 +164,9 @@ func (s *UDMSubController) Adds(c *gin.Context) { return } - var body model.UDMSub + var body model.UDMSubUser err := c.ShouldBindBodyWith(&body, binding.JSON) - if err != nil || body.IMSI == "" { + if err != nil || len(body.IMSI) < 15 { c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) return } @@ -230,7 +177,7 @@ func (s *UDMSubController) Adds(c *gin.Context) { c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) return } - + // 网元主机的Telnet客户端 telnetClient, err := s.neInfoService.NeRunTelnetClient("UDM", neId, 1) if err != nil { c.JSON(200, result.ErrMsg(err.Error())) @@ -239,8 +186,8 @@ func (s *UDMSubController) Adds(c *gin.Context) { defer telnetClient.Close() // 发送MML - cmd := fmt.Sprintf("baa udmuser:start_imsi=%s,start_msisdn=%s,sub_num=%s,ambr=%s,nssai=%s,arfb=%s,sar=%s,rat=%s,cn=%s,smf_sel=%s,sm_data=%s,eps_flag=%s,eps_odb=%s,hplmn_odb=%s,ard=%s,epstpl=%s,context_id=%s,apn_context=%s", - body.IMSI, body.Msisdn, num, body.Ambr, body.Nssai, body.Arfb, body.Sar, body.Rat, body.Cn, body.SmfSel, body.SmData, body.EpsFlag, body.EpsOdb, body.HplmnOdb, body.Ard, body.Epstpl, body.ContextId, body.ApnContext) + cmd := fmt.Sprintf("baa udmuser:start_imsi=%s,start_msisdn=%s,sub_num=%s,", body.IMSI, body.MSISDN, num) + cmd += s.udmSubService.ParseCommandParams(body) // static_ip指给4G UE分配的静态IP,没有可不带此字段名,批量添加IP会自动递增 if body.StaticIp != "" { cmd += fmt.Sprintf(",static_ip=%s", body.StaticIp) @@ -253,8 +200,7 @@ func (s *UDMSubController) Adds(c *gin.Context) { // 命令ok时 if strings.Contains(data, "ok") { - neId = "" - go s.udmSubService.LoadData(neId, body.IMSI, num) + s.udmSubService.LoadData(neId, body.IMSI, num, body.Remark) } c.JSON(200, result.OkData(data)) } @@ -270,9 +216,9 @@ func (s *UDMSubController) Edit(c *gin.Context) { return } - var body model.UDMSub + var body model.UDMSubUser err := c.ShouldBindBodyWith(&body, binding.JSON) - if err != nil || body.IMSI == "" { + if err != nil || len(body.IMSI) < 15 { c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) return } @@ -283,7 +229,7 @@ func (s *UDMSubController) Edit(c *gin.Context) { c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) return } - + // 网元主机的Telnet客户端 telnetClient, err := s.neInfoService.NeRunTelnetClient("UDM", neId, 1) if err != nil { c.JSON(200, result.ErrMsg(err.Error())) @@ -292,62 +238,8 @@ func (s *UDMSubController) Edit(c *gin.Context) { defer telnetClient.Close() // 发送MML - cmd := fmt.Sprintf("mod udmuser:imsi=%s", body.IMSI) - // 修改的参数名称 - if body.Msisdn != "" { - cmd += fmt.Sprintf(",msisdn=%s", body.Msisdn) - } - if body.Ambr != "" { - cmd += fmt.Sprintf(",ambr=%s", body.Ambr) - } - if body.Nssai != "" { - cmd += fmt.Sprintf(",nssai=%s", body.Nssai) - } - if body.Arfb != "" { - cmd += fmt.Sprintf(",arfb=%s", body.Arfb) - } - if body.Sar != "" { - cmd += fmt.Sprintf(",sar=%s", body.Sar) - } - if body.Rat != "" { - cmd += fmt.Sprintf(",rat=%s", body.Rat) - } - if body.Cn != "" { - cmd += fmt.Sprintf(",cn=%s", body.Cn) - } - if body.SmfSel != "" { - cmd += fmt.Sprintf(",smf_sel=%s", body.SmfSel) - } - if body.SmData != "" { - cmd += fmt.Sprintf(",sm_data=%s", body.SmData) - } - if body.EpsDat != "" { - cmd += fmt.Sprintf(",eps_dat=%s", body.EpsDat) - } - if body.EpsFlag != "" { - cmd += fmt.Sprintf(",eps_flag=%s", body.EpsFlag) - } - if body.EpsOdb != "" { - cmd += fmt.Sprintf(",eps_odb=%s", body.EpsOdb) - } - if body.HplmnOdb != "" { - cmd += fmt.Sprintf(",hplmn_odb=%s", body.HplmnOdb) - } - if body.Epstpl != "" { - cmd += fmt.Sprintf(",epstpl=%s", body.Epstpl) - } - if body.Ard != "" { - cmd += fmt.Sprintf(",ard=%s", body.Ard) - } - if body.ContextId != "" { - cmd += fmt.Sprintf(",context_id=%s", body.ContextId) - } - if body.ApnContext != "" { - cmd += fmt.Sprintf(",apn_context=%s", body.ApnContext) - } - if body.StaticIp != "" { - cmd += fmt.Sprintf(",static_ip=%s", body.StaticIp) - } + cmd := fmt.Sprintf("mod udmuser:imsi=%s,", body.IMSI) + cmd += s.udmSubService.ParseCommandParams(body) data, err := telnet.ConvertToStr(telnetClient, cmd) if err != nil { c.JSON(200, result.ErrMsg(err.Error())) @@ -356,8 +248,8 @@ func (s *UDMSubController) Edit(c *gin.Context) { // 命令ok时 if strings.Contains(data, "ok") { - neId = "" - go s.udmSubService.Insert(neId, body) + body.NeId = neId + s.udmSubService.Insert(neId, body) } c.JSON(200, result.OkData(data)) } @@ -369,7 +261,7 @@ func (s *UDMSubController) Remove(c *gin.Context) { language := ctx.AcceptLanguage(c) neId := c.Param("neId") imsi := c.Param("imsi") - if neId == "" || imsi == "" { + if neId == "" || len(imsi) < 15 { c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) return } @@ -388,7 +280,7 @@ func (s *UDMSubController) Remove(c *gin.Context) { c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) return } - + // 网元主机的Telnet客户端 telnetClient, err := s.neInfoService.NeRunTelnetClient("UDM", neId, 1) if err != nil { c.JSON(200, result.ErrMsg(err.Error())) @@ -407,8 +299,7 @@ func (s *UDMSubController) Remove(c *gin.Context) { } // 命令ok时 if strings.Contains(data, "ok") { - neId = "" - go s.udmSubService.Delete(neId, imsi) + s.udmSubService.Delete(neId, imsi) } resultData[imsi] = data } @@ -424,7 +315,7 @@ func (s *UDMSubController) Removes(c *gin.Context) { neId := c.Param("neId") imsi := c.Param("imsi") num := c.Param("num") - if neId == "" || imsi == "" || num == "" { + if neId == "" || len(imsi) < 15 || num == "" { c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) return } @@ -435,7 +326,7 @@ func (s *UDMSubController) Removes(c *gin.Context) { c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) return } - + // 网元主机的Telnet客户端 telnetClient, err := s.neInfoService.NeRunTelnetClient("UDM", neId, 1) if err != nil { c.JSON(200, result.ErrMsg(err.Error())) @@ -453,8 +344,7 @@ func (s *UDMSubController) Removes(c *gin.Context) { // 命令ok时 if strings.Contains(data, "ok") { - neId = "" - go s.udmSubService.LoadData(neId, imsi, num) + s.udmSubService.LoadData(neId, imsi, num, "-(Deleted)-") } c.JSON(200, result.OkData(data)) } @@ -464,53 +354,63 @@ func (s *UDMSubController) Removes(c *gin.Context) { // POST /export func (s *UDMSubController) Export(c *gin.Context) { language := ctx.AcceptLanguage(c) - var body struct { - NeId string `json:"neId" binding:"required"` - Type string `json:"type" binding:"required"` - } - err := c.ShouldBindBodyWith(&body, binding.JSON) - if err != nil { + // 查询结果,根据查询条件结果,单页最大值限制 + querys := ctx.BodyJSONMap(c) + neId := querys["neId"].(string) + fileType := querys["type"].(string) + if neId == "" || fileType == "" { c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) return } - - if !(body.Type == "csv" || body.Type == "txt") { + if !(fileType == "csv" || fileType == "txt") { c.JSON(200, result.ErrMsg(i18n.TKey(language, "ne.udm.errImportUserSubFileFormat"))) return } - neId := "" - list := s.udmSubService.SelectList(model.UDMSub{NeId: neId}) - // 文件名 - fileName := fmt.Sprintf("udm_sub_user_export_%s_%d.%s", neId, time.Now().UnixMilli(), body.Type) - filePath := fmt.Sprintf("%s/%s", file.ParseUploadFileDir(uploadsubpath.EXPORT), fileName) + querys["pageNum"] = 1 + querys["pageSize"] = 10000 + total, rows := s.udmSubService.SelectPage(querys) + if total == 0 { + // 导出数据记录为空 + c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.exportEmpty"))) + return + } - if body.Type == "csv" { + // rows := s.udmSubService.SelectList(model.UDMSubUser{NeId: neId}) + if len(rows) <= 0 { + // 导出数据记录为空 + c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.exportEmpty"))) + return + } + + // 文件名 + fileName := fmt.Sprintf("udm_sub_user_export_%s_%d.%s", neId, time.Now().UnixMilli(), fileType) + filePath := filepath.Join(file.ParseUploadFileDir(uploadsubpath.EXPORT), fileName) + + if fileType == "csv" { // 转换数据 data := [][]string{} - data = append(data, []string{"imsi", "msisdn", "ambr", "nssai", "arfb", "sar", "rat", "cn", "smf_sel", "sm_dat", "eps_dat"}) - for _, v := range list { + data = append(data, []string{"IMSI", "MSISDN", "UeAmbrTpl", "NssaiTpl", "AreaForbiddenTpl", "ServiceAreaRestrictionTpl", "RatRestrictions", "CnTypeRestrictions", "SmfSel", "SmData", "EPSDat"}) + for _, v := range rows { epsDat := fmt.Sprintf("%s,%s,%s,%s,%s,%s,%s,%s", v.EpsFlag, v.EpsOdb, v.HplmnOdb, v.Ard, v.Epstpl, v.ContextId, v.ApnContext, v.StaticIp) - data = append(data, []string{v.IMSI, v.Msisdn, v.Ambr, v.Nssai, v.Arfb, v.Sar, v.Rat, v.Cn, v.SmfSel, v.SmData, epsDat}) + data = append(data, []string{v.IMSI, v.MSISDN, v.UeAmbrTpl, v.NssaiTpl, v.AreaForbiddenTpl, v.ServiceAreaRestrictionTpl, v.RatRestrictions, v.CnTypeRestrictions, v.SmfSel, v.SmData, epsDat}) } // 输出到文件 - err = file.WriterFileCSV(data, filePath) - if err != nil { + if err := file.WriterFileCSV(data, filePath); err != nil { c.JSON(200, result.ErrMsg(err.Error())) return } } - if body.Type == "txt" { + if fileType == "txt" { // 转换数据 data := [][]string{} - for _, v := range list { + for _, v := range rows { epsDat := fmt.Sprintf("%s,%s,%s,%s,%s,%s,%s,%s", v.EpsFlag, v.EpsOdb, v.HplmnOdb, v.Ard, v.Epstpl, v.ContextId, v.ApnContext, v.StaticIp) - data = append(data, []string{v.IMSI, v.Msisdn, v.Ambr, v.Nssai, v.Arfb, v.Sar, v.Rat, v.Cn, v.SmfSel, v.SmData, epsDat}) + data = append(data, []string{v.IMSI, v.MSISDN, v.UeAmbrTpl, v.NssaiTpl, v.AreaForbiddenTpl, v.ServiceAreaRestrictionTpl, v.RatRestrictions, v.CnTypeRestrictions, v.SmfSel, v.SmData, epsDat}) } // 输出到文件 - err = file.WriterFileTXT(data, ",", filePath) - if err != nil { + if err := file.WriterFileTXT(data, ",", filePath); err != nil { c.JSON(200, result.ErrMsg(err.Error())) return } @@ -590,13 +490,11 @@ func (s *UDMSubController) Import(c *gin.Context) { if strings.Contains(data, "ok") { if strings.HasSuffix(body.UploadPath, ".csv") { data := file.ReadFileCSV(localFilePath) - neId := "" - go s.udmSubService.InsertData(neId, "csv", data) + go s.udmSubService.InsertData(neInfo.NeId, "csv", data) } if strings.HasSuffix(body.UploadPath, ".txt") { data := file.ReadFileTXT(",", localFilePath) - neId := "" - go s.udmSubService.InsertData(neId, "txt", data) + go s.udmSubService.InsertData(neInfo.NeId, "txt", data) } } c.JSON(200, result.OkMsg(data)) diff --git a/src/modules/network_data/controller/upf.go b/src/modules/network_data/controller/upf.go index aa3ef337..0d0acd5e 100644 --- a/src/modules/network_data/controller/upf.go +++ b/src/modules/network_data/controller/upf.go @@ -10,19 +10,17 @@ import ( ) // 实例化控制层 UPFController 结构体 -var NewUPFController = &UPFController{ - neInfoService: neService.NewNeInfoImpl, - perfKPIService: neDataService.NewPerfKPIImpl, +var NewUPF = &UPFController{ + neInfoService: neService.NewNeInfo, + perfKPIService: neDataService.NewPerfKPI, } // 网元UPF // // PATH /upf type UPFController struct { - // 网元信息服务 - neInfoService neService.INeInfo - // 统计信息服务 - perfKPIService neDataService.IPerfKPI + neInfoService *neService.NeInfo // 网元信息服务 + perfKPIService *neDataService.PerfKPI // 统计信息服务 } // 总流量数 N3上行 N6下行 @@ -32,9 +30,8 @@ type UPFController struct { func (s *UPFController) TotalFlow(c *gin.Context) { language := ctx.AcceptLanguage(c) var querys struct { - NeType string `json:"neType" form:"neType" binding:"required"` - NeID string `form:"neId" binding:"required"` - Day int `form:"day" binding:"required"` + NeID string `form:"neId" binding:"required"` + Day int `form:"day" binding:"required"` } if err := c.ShouldBindQuery(&querys); querys.Day < 0 || err != nil { c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) @@ -42,7 +39,7 @@ func (s *UPFController) TotalFlow(c *gin.Context) { } // 查询网元获取IP - neInfo := s.neInfoService.SelectNeInfoByNeTypeAndNeID(querys.NeType, querys.NeID) + neInfo := s.neInfoService.SelectNeInfoByNeTypeAndNeID("UPF", querys.NeID) if neInfo.NeId != querys.NeID || neInfo.IP == "" { c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) return diff --git a/src/modules/network_data/model/alarm.go b/src/modules/network_data/model/alarm.go index 11ef4d73..4e4bc9a9 100644 --- a/src/modules/network_data/model/alarm.go +++ b/src/modules/network_data/model/alarm.go @@ -4,7 +4,7 @@ import "time" // Alarm 告警数据对象 alarm type Alarm struct { - ID string `json:"id" gorm:"column:id;primaryKey;autoIncrement"` + ID string `json:"id" gorm:"id"` AlarmSeq string `json:"alarmSeq" gorm:"alarm_seq"` AlarmId string `json:"alarmId" gorm:"alarm_id"` AlarmTitle string `json:"alarmTitle" gorm:"alarm_title"` diff --git a/src/modules/network_data/model/cdr_event_ims.go b/src/modules/network_data/model/cdr_event_ims.go index ffe280c5..30343173 100644 --- a/src/modules/network_data/model/cdr_event_ims.go +++ b/src/modules/network_data/model/cdr_event_ims.go @@ -18,7 +18,7 @@ type CDREventIMSQuery struct { NeType string `json:"neType" form:"neType" binding:"required"` // 网元类型IMS NeID string `json:"neId" form:"neId" binding:"required"` RmUID string `json:"rmUID" form:"rmUID"` - RecordType string `json:"recordType" form:"recordType"` // 记录行为 MOC MTC MOSM MTSM + RecordType string `json:"recordType" form:"recordType"` // 记录行为 MOC MTC CallerParty string `json:"callerParty" form:"callerParty"` // 主叫号码 CalledParty string `json:"calledParty" form:"calledParty"` // 被叫号码 StartTime string `json:"startTime" form:"startTime"` diff --git a/src/modules/network_data/model/cdr_event_smsc.go b/src/modules/network_data/model/cdr_event_smsc.go new file mode 100644 index 00000000..6a50e597 --- /dev/null +++ b/src/modules/network_data/model/cdr_event_smsc.go @@ -0,0 +1,30 @@ +package model + +import "time" + +// CDREventSMSC CDR会话对象SMSC cdr_event_smsc +type CDREventSMSC struct { + ID string `json:"id" gorm:"column:id;primaryKey;autoIncrement"` + NeType string `json:"neType" gorm:"column:ne_type"` + NeName string `json:"neName" gorm:"column:ne_name"` + RmUID string `json:"rmUID" gorm:"column:rm_uid"` // 可能没有 + Timestamp int64 `json:"timestamp" gorm:"column:timestamp"` + CDRJSONStr string `json:"cdrJSON" gorm:"column:cdr_json"` + CreatedAt time.Time `json:"createdAt" gorm:"column:created_at;default:CURRENT_TIMESTAMP"` +} + +// CDREventSMSCQuery CDR会话对象SMSC查询参数结构体 +type CDREventSMSCQuery struct { + NeType string `json:"neType" form:"neType" binding:"required"` // 网元类型SMSC + NeID string `json:"neId" form:"neId" binding:"required"` + RmUID string `json:"rmUID" form:"rmUID"` + RecordType string `json:"recordType" form:"recordType"` // 记录行为 MOSM MTSM + CallerParty string `json:"callerParty" form:"callerParty"` // 主叫号码 + CalledParty string `json:"calledParty" form:"calledParty"` // 被叫号码 + StartTime string `json:"startTime" form:"startTime"` + EndTime string `json:"endTime" form:"endTime"` + SortField string `json:"sortField" form:"sortField" binding:"omitempty,oneof=timestamp"` // 排序字段,填写结果字段 + SortOrder string `json:"sortOrder" form:"sortOrder" binding:"omitempty,oneof=asc desc"` // 排序升降序,asc desc + PageNum int64 `json:"pageNum" form:"pageNum" binding:"required"` + PageSize int64 `json:"pageSize" form:"pageSize" binding:"required"` +} diff --git a/src/modules/network_data/model/udm_auth.go b/src/modules/network_data/model/udm_auth.go index 6f7c1d17..f22e52b0 100644 --- a/src/modules/network_data/model/udm_auth.go +++ b/src/modules/network_data/model/udm_auth.go @@ -1,17 +1,18 @@ package model -// UDMAuth UDM鉴权用户对象 u_auth_user -type UDMAuth struct { +// UDMAuthUser UDM鉴权用户 u_auth_user +type UDMAuthUser struct { ID string `json:"id" gorm:"column:id;primaryKey;autoIncrement"` // 默认ID - IMSI string `json:"imsi" gorm:"column:imsi"` // SIM卡号 - Amf string `json:"amf" gorm:"column:amf"` // ANF - Status string `json:"status" gorm:"column:status"` // 状态 默认给1 + IMSI string `json:"imsi" gorm:"column:imsi"` // SIM卡/USIM卡ID + NeId string `json:"neId" gorm:"column:ne_id"` // UDM网元标识 + Amf string `json:"amf" gorm:"column:amf"` // AMF + Status string `json:"status" gorm:"column:status"` // 状态 Ki string `json:"ki" gorm:"column:ki"` // ki - AlgoIndex string `json:"algoIndex" gorm:"column:algo_index"` // AlgoIndex - Opc string `json:"opc" gorm:"column:opc"` // opc - NeId string `json:"neId" gorm:"column:ne_id"` // UDM网元标识-子系统 + AlgoIndex string `json:"algoIndex" gorm:"column:algo_index"` // algoIndex + Opc string `json:"opc" gorm:"column:opc"` // OPC } -func (UDMAuth) TableName() string { +// TableName 表名称 +func (*UDMAuthUser) TableName() string { return "u_auth_user" } diff --git a/src/modules/network_data/model/udm_sub.go b/src/modules/network_data/model/udm_sub.go index a4f3acd7..96342f9b 100644 --- a/src/modules/network_data/model/udm_sub.go +++ b/src/modules/network_data/model/udm_sub.go @@ -1,34 +1,48 @@ package model -// UDMSub UDM签约用户对象 u_sub_user -type UDMSub struct { - ID string `json:"id" gorm:"column:id;primaryKey;autoIncrement"` - Msisdn string `json:"msisdn" gorm:"column:msisdn"` // 相当手机号 - IMSI string `json:"imsi" gorm:"column:imsi"` // SIM卡号 - Ambr string `json:"ambr" gorm:"column:ambr"` - Nssai string `json:"nssai" gorm:"column:nssai"` - Rat string `json:"rat" gorm:"column:rat"` - Arfb string `json:"arfb" gorm:"column:arfb"` - Sar string `json:"sar" gorm:"column:sar"` - Cn string `json:"cn" gorm:"column:cn"` - SmData string `json:"smData" gorm:"column:sm_data"` - SmfSel string `json:"smfSel" gorm:"column:smf_sel"` - EpsDat string `json:"epsDat" gorm:"column:eps_dat"` - NeId string `json:"neId" gorm:"column:ne_id"` // UDM网元标识-子系统 +// UDMSubUser UDM签约用户 u_sub_user +type UDMSubUser struct { + ID string `json:"id" gorm:"column:id;primaryKey;autoIncrement"` // 主键 + IMSI string `json:"imsi" gorm:"column:imsi"` // SIM卡/USIM卡ID + MSISDN string `json:"msisdn" gorm:"column:msisdn"` // 用户电话号码 + NeId string `json:"neId" gorm:"column:ne_id"` // UDM网元标识 - EpsFlag string `json:"epsFlag" gorm:"column:eps_flag"` - EpsOdb string `json:"epsOdb" gorm:"column:eps_odb"` - HplmnOdb string `json:"hplmnOdb" gorm:"column:hplmn_odb"` - Ard string `json:"ard" gorm:"column:ard"` - Epstpl string `json:"epstpl" gorm:"column:epstpl"` - ContextId string `json:"contextId" gorm:"column:context_id"` - ApnContext string `json:"apnContext" gorm:"column:apn_context"` - StaticIp string `json:"staticIp" gorm:"column:static_ip"` + AmDat string `json:"amDat" gorm:"column:am_dat"` // AmData + UeAmbrTpl string `json:"ambr" gorm:"column:ambr"` // AmData SubUeAMBRTemp + NssaiTpl string `json:"nssai" gorm:"column:nssai"` // AmData SubSNSSAITemp + RatRestrictions string `json:"rat" gorm:"column:rat"` // AmData RAT 0x00:VIRTUAL 0x01:WLAN 0x02:EUTRA 0x03:NR + AreaForbiddenTpl string `json:"arfb" gorm:"column:arfb"` // AmData AreaForbidden + ServiceAreaRestrictionTpl string `json:"sar" gorm:"column:sar"` // AmData serviceAreaRestrictTemp + CnTypeRestrictions string `json:"cnType" gorm:"column:cn_type"` // AmData CNType 0x00:EPC和5GC 0x01:5GC 0x02:EPC 0x03:EPC+5GC + RfspIndex string `json:"rfspIndex" gorm:"column:rfsp_index"` // AmData RfspIndex + SubsRegTime string `json:"regTimer" gorm:"column:reg_timer"` // AmData RegTimer + UeUsageType string `json:"ueUsageType" gorm:"column:ue_usage_type"` // AmData UEUsageType + ActiveTime string `json:"activeTime" gorm:"column:active_time"` // AmData ActiveTime + MicoAllowed string `json:"mico" gorm:"column:mico"` // AmData MICO + OdbPs string `json:"odbPs" gorm:"column:odb_ps"` // AmData ODB_PS 0-all,1-hplmn,2-vplmn + GroupId string `json:"groupId" gorm:"column:group_id"` // AmData GroupId + + EpsDat string `json:"epsDat" gorm:"column:eps_dat"` // EpsDat + EpsFlag string `json:"epsFlag" gorm:"column:eps_flag"` // EpsDat epsFlag + EpsOdb string `json:"epsOdb" gorm:"column:eps_odb"` // EpsDat epsOdb + HplmnOdb string `json:"hplmnOdb" gorm:"column:hplmn_odb"` // EpsDat hplmnOdb + Ard string `json:"ard" gorm:"column:ard"` // EpsDat Ard + Epstpl string `json:"epstpl" gorm:"column:epstpl"` // EpsDat Epstpl + ContextId string `json:"contextId" gorm:"column:context_id"` // EpsDat ContextId + ApnNum string `json:"apnNum" gorm:"column:apn_mum"` // EpsDat apnNum + ApnContext string `json:"apnContext" gorm:"column:apn_context"` // EpsDat apnContext + StaticIp string `json:"staticIp" gorm:"column:static_ip"` // EpsDat staticIp 指给4G UE分配的静态IP,没有可不带此字段名 + + SmData string `json:"smData" gorm:"column:sm_data"` // smData + SmfSel string `json:"smfSel" gorm:"column:smf_sel"` // smfSel + Cag string `json:"cag" gorm:"column:cag"` // CAG // ====== 非数据库字段属性 ====== + Remark string `json:"remark,omitempty" gorm:"-"` // 备注 } -func (UDMSub) TableName() string { +// TableName 表名称 +func (*UDMSubUser) TableName() string { return "u_sub_user" } diff --git a/src/modules/network_data/model/udm_user_info.go b/src/modules/network_data/model/udm_user_info.go new file mode 100644 index 00000000..470a9abc --- /dev/null +++ b/src/modules/network_data/model/udm_user_info.go @@ -0,0 +1,15 @@ +package model + +// UDMUserInfo UDM用户IMSI扩展信息 u_user_info +type UDMUserInfo struct { + ID string `json:"id" gorm:"column:id;primaryKey;autoIncrement"` // 默认ID + IMSI string `json:"imsi" gorm:"column:imsi"` // SIM卡/USIM卡ID + MSISDN string `json:"msisdn" gorm:"column:msisdn"` // 用户电话号码 + NeId string `json:"neId" gorm:"column:ne_id"` // UDM网元标识-子系统 + Remark string `json:"remark" gorm:"remark"` // 备注 +} + +// TableName 表名称 +func (*UDMUserInfo) TableName() string { + return "u_user_info" +} diff --git a/src/modules/network_data/network_data.go b/src/modules/network_data/network_data.go index 1f484641..a114f75b 100644 --- a/src/modules/network_data/network_data.go +++ b/src/modules/network_data/network_data.go @@ -21,11 +21,11 @@ func Setup(router *gin.Engine) { { kpiGroup.GET("/title", middleware.PreAuthorize(nil), - controller.NewPerfKPIController.Title, + controller.NewPerfKPI.Title, ) kpiGroup.GET("/data", middleware.PreAuthorize(nil), - controller.NewPerfKPIController.GoldKPI, + controller.NewPerfKPI.GoldKPI, ) } @@ -34,11 +34,11 @@ func Setup(router *gin.Engine) { { alarmGroup.GET("/list", middleware.PreAuthorize(nil), - controller.NewAlarmController.List, + controller.NewAlarm.List, ) alarmGroup.DELETE("/:alarmIds", middleware.PreAuthorize(nil), - controller.NewAlarmController.Remove, + controller.NewAlarm.Remove, ) } @@ -47,17 +47,36 @@ func Setup(router *gin.Engine) { { imsGroup.GET("/cdr/list", middleware.PreAuthorize(nil), - controller.NewIMSController.CDRList, + controller.NewIMS.CDRList, ) imsGroup.DELETE("/cdr/:cdrIds", middleware.PreAuthorize(nil), collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.imsCDR", collectlogs.BUSINESS_TYPE_DELETE)), - controller.NewIMSController.CDRRemove, + controller.NewIMS.CDRRemove, ) imsGroup.POST("/cdr/export", middleware.PreAuthorize(nil), collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.imsCDR", collectlogs.BUSINESS_TYPE_EXPORT)), - controller.NewIMSController.CDRExport, + controller.NewIMS.CDRExport, + ) + } + + // 网元SMSC + smscGroup := neDataGroup.Group("/smsc") + { + smscGroup.GET("/cdr/list", + middleware.PreAuthorize(nil), + controller.NewSMSC.CDRList, + ) + smscGroup.DELETE("/cdr/:cdrIds", + middleware.PreAuthorize(nil), + collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.smscCDR", collectlogs.BUSINESS_TYPE_DELETE)), + controller.NewSMSC.CDRRemove, + ) + smscGroup.POST("/cdr/export", + middleware.PreAuthorize(nil), + collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.smscCDR", collectlogs.BUSINESS_TYPE_EXPORT)), + controller.NewSMSC.CDRExport, ) } @@ -66,17 +85,21 @@ func Setup(router *gin.Engine) { { smfGroup.GET("/cdr/list", middleware.PreAuthorize(nil), - controller.NewSMFController.CDRList, + controller.NewSMF.CDRList, ) smfGroup.DELETE("/cdr/:cdrIds", middleware.PreAuthorize(nil), collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.smfCDR", collectlogs.BUSINESS_TYPE_DELETE)), - controller.NewSMFController.CDRRemove, + controller.NewSMF.CDRRemove, ) smfGroup.POST("/cdr/export", middleware.PreAuthorize(nil), collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.smfCDR", collectlogs.BUSINESS_TYPE_EXPORT)), - controller.NewSMFController.CDRExport, + controller.NewSMF.CDRExport, + ) + smfGroup.GET("/subscribers", + middleware.PreAuthorize(nil), + controller.NewSMF.SubUserList, ) } @@ -85,17 +108,17 @@ func Setup(router *gin.Engine) { { amfGroup.GET("/ue/list", middleware.PreAuthorize(nil), - controller.NewAMFController.UEList, + controller.NewAMF.UEList, ) amfGroup.DELETE("/ue/:ueIds", middleware.PreAuthorize(nil), collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.amfUE", collectlogs.BUSINESS_TYPE_DELETE)), - controller.NewAMFController.UERemove, + controller.NewAMF.UERemove, ) amfGroup.POST("/ue/export", middleware.PreAuthorize(nil), collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.amfUE", collectlogs.BUSINESS_TYPE_EXPORT)), - controller.NewAMFController.UEExport, + controller.NewAMF.UEExport, ) } @@ -104,7 +127,7 @@ func Setup(router *gin.Engine) { { upfGroup.GET("/totalFlow", middleware.PreAuthorize(nil), - controller.NewUPFController.TotalFlow, + controller.NewUPF.TotalFlow, ) } @@ -221,17 +244,17 @@ func Setup(router *gin.Engine) { { mmeGroup.GET("/ue/list", middleware.PreAuthorize(nil), - controller.NewMMEController.UEList, + controller.NewMME.UEList, ) mmeGroup.DELETE("/ue/:ueIds", middleware.PreAuthorize(nil), collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.mmeUE", collectlogs.BUSINESS_TYPE_DELETE)), - controller.NewMMEController.UERemove, + controller.NewMME.UERemove, ) mmeGroup.POST("/ue/export", middleware.PreAuthorize(nil), collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.mmeUE", collectlogs.BUSINESS_TYPE_EXPORT)), - controller.NewMMEController.UEExport, + controller.NewMME.UEExport, ) } } diff --git a/src/modules/network_data/repository/alarm.go b/src/modules/network_data/repository/alarm.go index 0ea43bb0..41d50325 100644 --- a/src/modules/network_data/repository/alarm.go +++ b/src/modules/network_data/repository/alarm.go @@ -1,15 +1,194 @@ package repository -import "be.ems/src/modules/network_data/model" +import ( + "fmt" + "strings" -// 告警 数据层接口 -type IAlarm interface { - // SelectPage 根据条件分页查询 - SelectPage(querys model.AlarmQuery) map[string]any + "be.ems/src/framework/datasource" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/parse" + "be.ems/src/framework/utils/repo" + "be.ems/src/modules/network_data/model" +) - // SelectByIds 通过ID查询 - SelectByIds(ids []string) []model.Alarm +// 实例化数据层 Alarm 结构体 +var NewAlarm = &Alarm{ + selectSql: `select + id, alarm_seq, alarm_id, alarm_title, ne_type, ne_id, alarm_code, event_time, + alarm_type, orig_severity, perceived_severity, pv_flag, ne_name, object_uid, object_name, + object_type, location_info, province, alarm_status, specific_problem, specific_problem_id, + add_info, counter, latest_event_time, ack_state, ack_time, ack_user, clear_type, + clear_time, clear_user, timestamp + from alarm`, - // DeleteByIds 批量删除信息 - DeleteByIds(ids []string) int64 + resultMap: map[string]string{ + "id": "ID", + "alarm_seq": "AlarmSeq", + "alarm_id": "AlarmId", + "alarm_title": "AlarmTitle", + "ne_type": "NeType", + "ne_id": "NeId", + "alarm_code": "AlarmCode", + "event_time": "EventTime", + "alarm_type": "AlarmType", + "orig_severity": "OrigSeverity", + "perceived_severity": "PerceivedSeverity", + "pv_flag": "PvFlag", + "ne_name": "NeName", + "object_uid": "ObjectUid", + "object_name": "ObjectName", + "object_type": "ObjectType", + "location_info": "LocationInfo", + "province": "Province", + "alarm_status": "AlarmStatus", + "specific_problem": "SpecificProblem", + "specific_problem_id": "SpecificProblemId", + "add_info": "AddInfo", + "counter": "Counter", + "latest_event_time": "LatestEventTime", + "ack_state": "AckState", + "ack_time": "AckTime", + "ack_user": "AckUser", + "clear_type": "ClearType", + "clear_time": "ClearTime", + "clear_user": "ClearUser", + "timestamp": "Timestamp", + }, +} + +// Alarm 告警 数据层处理 +type Alarm struct { + // 查询视图对象SQL + selectSql string + // 结果字段与实体映射 + resultMap map[string]string +} + +// convertResultRows 将结果记录转实体结果组 +func (r *Alarm) convertResultRows(rows []map[string]any) []model.Alarm { + arr := make([]model.Alarm, 0) + for _, row := range rows { + item := model.Alarm{} + for key, value := range row { + if keyMapper, ok := r.resultMap[key]; ok { + repo.SetFieldValue(&item, keyMapper, value) + } + } + arr = append(arr, item) + } + return arr +} + +// SelectPage 根据条件分页查询 +func (r *Alarm) SelectPage(querys model.AlarmQuery) map[string]any { + // 查询条件拼接 + var conditions []string + var params []any + if querys.NeType != "" { + conditions = append(conditions, "ne_type = ?") + params = append(params, querys.NeType) + } + if querys.RmUID != "" { + conditions = append(conditions, "object_uid = ?") + params = append(params, querys.RmUID) + } + if querys.StartTime != "" { + conditions = append(conditions, "timestamp >= ?") + params = append(params, querys.StartTime) + } + if querys.EndTime != "" { + conditions = append(conditions, "timestamp <= ?") + params = append(params, querys.EndTime) + } + if querys.OrigSeverity != "" { + eventTypes := strings.Split(querys.OrigSeverity, ",") + placeholder := repo.KeyPlaceholderByQuery(len(eventTypes)) + conditions = append(conditions, fmt.Sprintf("orig_severity in (%s)", placeholder)) + for _, eventType := range eventTypes { + params = append(params, eventType) + } + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + result := map[string]any{ + "total": 0, + "rows": []model.Alarm{}, + } + + // 查询数量 长度为0直接返回 + totalSql := "select count(1) as 'total' from alarm" + totalRows, err := datasource.RawDB("", totalSql+whereSql, params) + if err != nil { + logger.Errorf("total err => %v", err) + return result + } + total := parse.Number(totalRows[0]["total"]) + if total == 0 { + return result + } else { + result["total"] = total + } + + // 分页 + pageNum, pageSize := repo.PageNumSize(querys.PageNum, querys.PageSize) + pageSql := " limit ?,? " + params = append(params, pageNum*pageSize) + params = append(params, pageSize) + + // 排序 + orderSql := "" + if querys.SortField != "" { + sortSql := querys.SortField + if querys.SortOrder != "" { + if querys.SortOrder == "desc" { + sortSql += " desc " + } else { + sortSql += " asc " + } + } + orderSql = fmt.Sprintf(" order by %s ", sortSql) + } + + // 查询数据 + querySql := r.selectSql + whereSql + orderSql + pageSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + } + + // 转换实体 + result["rows"] = r.convertResultRows(results) + return result +} + +// SelectByIds 通过ID查询 +func (r *Alarm) SelectByIds(ids []string) []model.Alarm { + placeholder := repo.KeyPlaceholderByQuery(len(ids)) + querySql := r.selectSql + " where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(ids) + results, err := datasource.RawDB("", querySql, parameters) + if err != nil { + logger.Errorf("query err => %v", err) + return []model.Alarm{} + } + // 转换实体 + return r.convertResultRows(results) +} + +// DeleteByIds 批量删除信息 +func (r *Alarm) DeleteByIds(ids []string) int64 { + placeholder := repo.KeyPlaceholderByQuery(len(ids)) + sql := "delete from alarm where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(ids) + results, err := datasource.ExecDB("", sql, parameters) + if err != nil { + logger.Errorf("delete err => %v", err) + return 0 + } + return results } diff --git a/src/modules/network_data/repository/alarm.impl.go b/src/modules/network_data/repository/alarm.impl.go deleted file mode 100644 index 6a97dbf8..00000000 --- a/src/modules/network_data/repository/alarm.impl.go +++ /dev/null @@ -1,194 +0,0 @@ -package repository - -import ( - "fmt" - "strings" - - "be.ems/src/framework/datasource" - "be.ems/src/framework/logger" - "be.ems/src/framework/utils/parse" - "be.ems/src/framework/utils/repo" - "be.ems/src/modules/network_data/model" -) - -// 实例化数据层 AlarmImpl 结构体 -var NewAlarmImpl = &AlarmImpl{ - selectSql: `select - id, alarm_seq, alarm_id, alarm_title, ne_type, ne_id, alarm_code, event_time, - alarm_type, orig_severity, perceived_severity, pv_flag, ne_name, object_uid, object_name, - object_type, location_info, province, alarm_status, specific_problem, specific_problem_id, - add_info, counter, latest_event_time, ack_state, ack_time, ack_user, clear_type, - clear_time, clear_user, timestamp - from alarm`, - - resultMap: map[string]string{ - "id": "ID", - "alarm_seq": "AlarmSeq", - "alarm_id": "AlarmId", - "alarm_title": "AlarmTitle", - "ne_type": "NeType", - "ne_id": "NeId", - "alarm_code": "AlarmCode", - "event_time": "EventTime", - "alarm_type": "AlarmType", - "orig_severity": "OrigSeverity", - "perceived_severity": "PerceivedSeverity", - "pv_flag": "PvFlag", - "ne_name": "NeName", - "object_uid": "ObjectUid", - "object_name": "ObjectName", - "object_type": "ObjectType", - "location_info": "LocationInfo", - "province": "Province", - "alarm_status": "AlarmStatus", - "specific_problem": "SpecificProblem", - "specific_problem_id": "SpecificProblemId", - "add_info": "AddInfo", - "counter": "Counter", - "latest_event_time": "LatestEventTime", - "ack_state": "AckState", - "ack_time": "AckTime", - "ack_user": "AckUser", - "clear_type": "ClearType", - "clear_time": "ClearTime", - "clear_user": "ClearUser", - "timestamp": "Timestamp", - }, -} - -// AlarmImpl 告警 数据层处理 -type AlarmImpl struct { - // 查询视图对象SQL - selectSql string - // 结果字段与实体映射 - resultMap map[string]string -} - -// convertResultRows 将结果记录转实体结果组 -func (r *AlarmImpl) convertResultRows(rows []map[string]any) []model.Alarm { - arr := make([]model.Alarm, 0) - for _, row := range rows { - item := model.Alarm{} - for key, value := range row { - if keyMapper, ok := r.resultMap[key]; ok { - repo.SetFieldValue(&item, keyMapper, value) - } - } - arr = append(arr, item) - } - return arr -} - -// SelectPage 根据条件分页查询 -func (r *AlarmImpl) SelectPage(querys model.AlarmQuery) map[string]any { - // 查询条件拼接 - var conditions []string - var params []any - if querys.NeType != "" { - conditions = append(conditions, "ne_type = ?") - params = append(params, querys.NeType) - } - if querys.RmUID != "" { - conditions = append(conditions, "object_uid = ?") - params = append(params, querys.RmUID) - } - if querys.StartTime != "" { - conditions = append(conditions, "timestamp >= ?") - params = append(params, querys.StartTime) - } - if querys.EndTime != "" { - conditions = append(conditions, "timestamp <= ?") - params = append(params, querys.EndTime) - } - if querys.OrigSeverity != "" { - eventTypes := strings.Split(querys.OrigSeverity, ",") - placeholder := repo.KeyPlaceholderByQuery(len(eventTypes)) - conditions = append(conditions, fmt.Sprintf("orig_severity in (%s)", placeholder)) - for _, eventType := range eventTypes { - params = append(params, eventType) - } - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } - - result := map[string]any{ - "total": 0, - "rows": []model.Alarm{}, - } - - // 查询数量 长度为0直接返回 - totalSql := "select count(1) as 'total' from alarm" - totalRows, err := datasource.RawDB("", totalSql+whereSql, params) - if err != nil { - logger.Errorf("total err => %v", err) - return result - } - total := parse.Number(totalRows[0]["total"]) - if total == 0 { - return result - } else { - result["total"] = total - } - - // 分页 - pageNum, pageSize := repo.PageNumSize(querys.PageNum, querys.PageSize) - pageSql := " limit ?,? " - params = append(params, pageNum*pageSize) - params = append(params, pageSize) - - // 排序 - orderSql := "" - if querys.SortField != "" { - sortSql := querys.SortField - if querys.SortOrder != "" { - if querys.SortOrder == "desc" { - sortSql += " desc " - } else { - sortSql += " asc " - } - } - orderSql = fmt.Sprintf(" order by %s ", sortSql) - } - - // 查询数据 - querySql := r.selectSql + whereSql + orderSql + pageSql - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err => %v", err) - } - - // 转换实体 - result["rows"] = r.convertResultRows(results) - return result -} - -// SelectByIds 通过ID查询 -func (r *AlarmImpl) SelectByIds(ids []string) []model.Alarm { - placeholder := repo.KeyPlaceholderByQuery(len(ids)) - querySql := r.selectSql + " where id in (" + placeholder + ")" - parameters := repo.ConvertIdsSlice(ids) - results, err := datasource.RawDB("", querySql, parameters) - if err != nil { - logger.Errorf("query err => %v", err) - return []model.Alarm{} - } - // 转换实体 - return r.convertResultRows(results) -} - -// DeleteByIds 批量删除信息 -func (r *AlarmImpl) DeleteByIds(ids []string) int64 { - placeholder := repo.KeyPlaceholderByQuery(len(ids)) - sql := "delete from alarm where id in (" + placeholder + ")" - parameters := repo.ConvertIdsSlice(ids) - results, err := datasource.ExecDB("", sql, parameters) - if err != nil { - logger.Errorf("delete err => %v", err) - return 0 - } - return results -} diff --git a/src/modules/network_data/repository/cdr_event_ims.go b/src/modules/network_data/repository/cdr_event_ims.go index fcc9a7bb..598e848c 100644 --- a/src/modules/network_data/repository/cdr_event_ims.go +++ b/src/modules/network_data/repository/cdr_event_ims.go @@ -1,15 +1,189 @@ package repository -import "be.ems/src/modules/network_data/model" +import ( + "fmt" + "strings" -// CDR会话事件IMS 数据层接口 -type ICDREventIMS interface { - // SelectPage 根据条件分页查询 - SelectPage(querys model.CDREventIMSQuery) map[string]any + "be.ems/src/framework/datasource" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/parse" + "be.ems/src/framework/utils/repo" + "be.ems/src/modules/network_data/model" +) - // SelectByIds 通过ID查询 - SelectByIds(cdrIds []string) []model.CDREventIMS +// 实例化数据层 CDREventIMS 结构体 +var NewCDREventIMS = &CDREventIMS{ + selectSql: `select id, ne_type, ne_name, rm_uid, timestamp, cdr_json, created_at from cdr_event_ims`, - // DeleteByIds 批量删除信息 - DeleteByIds(cdrIds []string) int64 + resultMap: map[string]string{ + "id": "ID", + "ne_type": "NeType", + "ne_name": "NeName", + "rm_uid": "RmUID", + "timestamp": "Timestamp", + "cdr_json": "CDRJSONStr", + "created_at": "CreatedAt", + }, +} + +// CDREventIMS CDR会话事件IMS 数据层处理 +type CDREventIMS struct { + // 查询视图对象SQL + selectSql string + // 结果字段与实体映射 + resultMap map[string]string +} + +// convertResultRows 将结果记录转实体结果组 +func (r *CDREventIMS) convertResultRows(rows []map[string]any) []model.CDREventIMS { + arr := make([]model.CDREventIMS, 0) + for _, row := range rows { + item := model.CDREventIMS{} + for key, value := range row { + if keyMapper, ok := r.resultMap[key]; ok { + repo.SetFieldValue(&item, keyMapper, value) + } + } + arr = append(arr, item) + } + return arr +} + +// SelectPage 根据条件分页查询 +func (r *CDREventIMS) SelectPage(querys model.CDREventIMSQuery) map[string]any { + // 查询条件拼接 + var conditions []string + var params []any + if querys.NeType != "" { + conditions = append(conditions, "ne_type = ?") + params = append(params, querys.NeType) + } + if querys.RmUID != "" { + conditions = append(conditions, "rm_uid = ?") + params = append(params, querys.RmUID) + } + if querys.StartTime != "" { + conditions = append(conditions, "timestamp >= ?") + if len(querys.StartTime) == 13 { + querys.StartTime = querys.StartTime[:10] + } + params = append(params, querys.StartTime) + } + if querys.EndTime != "" { + conditions = append(conditions, "timestamp <= ?") + if len(querys.EndTime) == 13 { + querys.EndTime = querys.EndTime[:10] + } + params = append(params, querys.EndTime) + } + if querys.CallerParty != "" { + conditions = append(conditions, "JSON_EXTRACT(cdr_json, '$.callerParty') = ?") + params = append(params, querys.CallerParty) + } + if querys.CalledParty != "" { + conditions = append(conditions, "JSON_EXTRACT(cdr_json, '$.calledParty') = ?") + params = append(params, querys.CalledParty) + } + // MySQL8支持的 + // if querys.RecordType != "" { + // recordTypes := strings.Split(querys.RecordType, ",") + // placeholder := repo.KeyPlaceholderByQuery(len(recordTypes)) + // conditions = append(conditions, fmt.Sprintf("JSON_EXTRACT(cdr_json, '$.recordType') in (%s)", placeholder)) + // for _, recordType := range recordTypes { + // params = append(params, recordType) + // } + // } + // Mariadb不支持json in查询改or + if querys.RecordType != "" { + recordTypes := strings.Split(querys.RecordType, ",") + var queryStrArr []string + for _, recordType := range recordTypes { + queryStrArr = append(queryStrArr, "JSON_EXTRACT(cdr_json, '$.recordType') = ?") + params = append(params, recordType) + } + conditions = append(conditions, fmt.Sprintf("( %s )", strings.Join(queryStrArr, " OR "))) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + result := map[string]any{ + "total": 0, + "rows": []model.CDREventIMS{}, + } + + // 查询数量 长度为0直接返回 + totalSql := "select count(1) as 'total' from cdr_event_ims" + totalRows, err := datasource.RawDB("", totalSql+whereSql, params) + if err != nil { + logger.Errorf("total err => %v", err) + return result + } + total := parse.Number(totalRows[0]["total"]) + if total == 0 { + return result + } else { + result["total"] = total + } + + // 分页 + pageNum, pageSize := repo.PageNumSize(querys.PageNum, querys.PageSize) + pageSql := " limit ?,? " + params = append(params, pageNum*pageSize) + params = append(params, pageSize) + + // 排序 + orderSql := "" + if querys.SortField != "" { + sortSql := querys.SortField + if querys.SortOrder != "" { + if querys.SortOrder == "desc" { + sortSql += " desc " + } else { + sortSql += " asc " + } + } + orderSql = fmt.Sprintf(" order by id desc, %s ", sortSql) + } + + // 查询数据 + querySql := r.selectSql + whereSql + orderSql + pageSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + } + + // 转换实体 + result["rows"] = r.convertResultRows(results) + return result +} + +// SelectByIds 通过ID查询 +func (r *CDREventIMS) SelectByIds(cdrIds []string) []model.CDREventIMS { + placeholder := repo.KeyPlaceholderByQuery(len(cdrIds)) + querySql := r.selectSql + " where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(cdrIds) + results, err := datasource.RawDB("", querySql, parameters) + if err != nil { + logger.Errorf("query err => %v", err) + return []model.CDREventIMS{} + } + // 转换实体 + return r.convertResultRows(results) +} + +// DeleteByIds 批量删除信息 +func (r *CDREventIMS) DeleteByIds(cdrIds []string) int64 { + placeholder := repo.KeyPlaceholderByQuery(len(cdrIds)) + sql := "delete from cdr_event_ims where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(cdrIds) + results, err := datasource.ExecDB("", sql, parameters) + if err != nil { + logger.Errorf("delete err => %v", err) + return 0 + } + return results } diff --git a/src/modules/network_data/repository/cdr_event_smf.go b/src/modules/network_data/repository/cdr_event_smf.go index 1a950a29..14f49cfc 100644 --- a/src/modules/network_data/repository/cdr_event_smf.go +++ b/src/modules/network_data/repository/cdr_event_smf.go @@ -1,15 +1,170 @@ package repository -import "be.ems/src/modules/network_data/model" +import ( + "fmt" + "strings" -// CDR会话事件SMF 数据层接口 -type ICDREventSMF interface { - // SelectPage 根据条件分页查询 - SelectPage(querys model.CDREventSMFQuery) map[string]any + "be.ems/src/framework/datasource" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/parse" + "be.ems/src/framework/utils/repo" + "be.ems/src/modules/network_data/model" +) - // SelectByIds 通过ID查询 - SelectByIds(cdrIds []string) []model.CDREventSMF +// 实例化数据层 CDREventSMF 结构体 +var NewCDREventSMF = &CDREventSMF{ + selectSql: `select id, ne_type, ne_name, rm_uid, timestamp, cdr_json, created_at from cdr_event_smf`, - // DeleteByIds 批量删除信息 - DeleteByIds(cdrIds []string) int64 + resultMap: map[string]string{ + "id": "ID", + "ne_type": "NeType", + "ne_name": "NeName", + "rm_uid": "RmUID", + "timestamp": "Timestamp", + "cdr_json": "CDRJSONStr", + "created_at": "CreatedAt", + }, +} + +// CDREventSMF CDR会话事件 数据层处理 +type CDREventSMF struct { + // 查询视图对象SQL + selectSql string + // 结果字段与实体映射 + resultMap map[string]string +} + +// convertResultRows 将结果记录转实体结果组 +func (r *CDREventSMF) convertResultRows(rows []map[string]any) []model.CDREventSMF { + arr := make([]model.CDREventSMF, 0) + for _, row := range rows { + item := model.CDREventSMF{} + for key, value := range row { + if keyMapper, ok := r.resultMap[key]; ok { + repo.SetFieldValue(&item, keyMapper, value) + } + } + arr = append(arr, item) + } + return arr +} + +// SelectPage 根据条件分页查询 +func (r *CDREventSMF) SelectPage(querys model.CDREventSMFQuery) map[string]any { + // 查询条件拼接 + var conditions []string + var params []any + if querys.NeType != "" { + conditions = append(conditions, "ne_type = ?") + params = append(params, querys.NeType) + } + if querys.RmUID != "" { + conditions = append(conditions, "rm_uid = ?") + params = append(params, querys.RmUID) + } + if querys.StartTime != "" { + conditions = append(conditions, "timestamp >= ?") + if len(querys.StartTime) == 13 { + querys.StartTime = querys.StartTime[:10] + } + params = append(params, querys.StartTime) + } + if querys.EndTime != "" { + conditions = append(conditions, "timestamp <= ?") + if len(querys.EndTime) == 13 { + querys.EndTime = querys.EndTime[:10] + } + params = append(params, querys.EndTime) + } + if querys.RecordType != "" { + conditions = append(conditions, "JSON_EXTRACT(cdr_json, '$.recordType') = ?") + params = append(params, querys.RecordType) + } + if querys.SubscriberID != "" { + conditions = append(conditions, "JSON_EXTRACT(cdr_json, '$.subscriberIdentifier.subscriptionIDData') = ?") + params = append(params, querys.SubscriberID) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + result := map[string]any{ + "total": 0, + "rows": []model.CDREventSMF{}, + } + + // 查询数量 长度为0直接返回 + totalSql := "select count(1) as 'total' from cdr_event_smf" + totalRows, err := datasource.RawDB("", totalSql+whereSql, params) + if err != nil { + logger.Errorf("total err => %v", err) + return result + } + total := parse.Number(totalRows[0]["total"]) + if total == 0 { + return result + } else { + result["total"] = total + } + + // 分页 + pageNum, pageSize := repo.PageNumSize(querys.PageNum, querys.PageSize) + pageSql := " limit ?,? " + params = append(params, pageNum*pageSize) + params = append(params, pageSize) + + // 排序 + orderSql := "" + if querys.SortField != "" { + sortSql := querys.SortField + if querys.SortOrder != "" { + if querys.SortOrder == "desc" { + sortSql += " desc " + } else { + sortSql += " asc " + } + } + orderSql = fmt.Sprintf(" order by id desc, %s ", sortSql) + } + + // 查询数据 + querySql := r.selectSql + whereSql + orderSql + pageSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + } + + // 转换实体 + result["rows"] = r.convertResultRows(results) + return result +} + +// SelectByIds 通过ID查询 +func (r *CDREventSMF) SelectByIds(cdrIds []string) []model.CDREventSMF { + placeholder := repo.KeyPlaceholderByQuery(len(cdrIds)) + querySql := r.selectSql + " where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(cdrIds) + results, err := datasource.RawDB("", querySql, parameters) + if err != nil { + logger.Errorf("query err => %v", err) + return []model.CDREventSMF{} + } + // 转换实体 + return r.convertResultRows(results) +} + +// DeleteByIds 批量删除信息 +func (r *CDREventSMF) DeleteByIds(cdrIds []string) int64 { + placeholder := repo.KeyPlaceholderByQuery(len(cdrIds)) + sql := "delete from cdr_event_smf where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(cdrIds) + results, err := datasource.ExecDB("", sql, parameters) + if err != nil { + logger.Errorf("delete err => %v", err) + return 0 + } + return results } diff --git a/src/modules/network_data/repository/cdr_event_smf.impl.go b/src/modules/network_data/repository/cdr_event_smf.impl.go deleted file mode 100644 index 97c98aa7..00000000 --- a/src/modules/network_data/repository/cdr_event_smf.impl.go +++ /dev/null @@ -1,170 +0,0 @@ -package repository - -import ( - "fmt" - "strings" - - "be.ems/src/framework/datasource" - "be.ems/src/framework/logger" - "be.ems/src/framework/utils/parse" - "be.ems/src/framework/utils/repo" - "be.ems/src/modules/network_data/model" -) - -// 实例化数据层 CDREventSMFImpl 结构体 -var NewCDREventSMFImpl = &CDREventSMFImpl{ - selectSql: `select id, ne_type, ne_name, rm_uid, timestamp, cdr_json, created_at from cdr_event_smf`, - - resultMap: map[string]string{ - "id": "ID", - "ne_type": "NeType", - "ne_name": "NeName", - "rm_uid": "RmUID", - "timestamp": "Timestamp", - "cdr_json": "CDRJSONStr", - "created_at": "CreatedAt", - }, -} - -// CDREventSMFImpl CDR会话事件 数据层处理 -type CDREventSMFImpl struct { - // 查询视图对象SQL - selectSql string - // 结果字段与实体映射 - resultMap map[string]string -} - -// convertResultRows 将结果记录转实体结果组 -func (r *CDREventSMFImpl) convertResultRows(rows []map[string]any) []model.CDREventSMF { - arr := make([]model.CDREventSMF, 0) - for _, row := range rows { - item := model.CDREventSMF{} - for key, value := range row { - if keyMapper, ok := r.resultMap[key]; ok { - repo.SetFieldValue(&item, keyMapper, value) - } - } - arr = append(arr, item) - } - return arr -} - -// SelectPage 根据条件分页查询 -func (r *CDREventSMFImpl) SelectPage(querys model.CDREventSMFQuery) map[string]any { - // 查询条件拼接 - var conditions []string - var params []any - if querys.NeType != "" { - conditions = append(conditions, "ne_type = ?") - params = append(params, querys.NeType) - } - if querys.RmUID != "" { - conditions = append(conditions, "rm_uid = ?") - params = append(params, querys.RmUID) - } - if querys.StartTime != "" { - conditions = append(conditions, "timestamp >= ?") - if len(querys.StartTime) == 13 { - querys.StartTime = querys.StartTime[:10] - } - params = append(params, querys.StartTime) - } - if querys.EndTime != "" { - conditions = append(conditions, "timestamp <= ?") - if len(querys.EndTime) == 13 { - querys.EndTime = querys.EndTime[:10] - } - params = append(params, querys.EndTime) - } - if querys.RecordType != "" { - conditions = append(conditions, "JSON_EXTRACT(cdr_json, '$.recordType') = ?") - params = append(params, querys.RecordType) - } - if querys.SubscriberID != "" { - conditions = append(conditions, "JSON_EXTRACT(cdr_json, '$.subscriberIdentifier.subscriptionIDData') = ?") - params = append(params, querys.SubscriberID) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } - - result := map[string]any{ - "total": 0, - "rows": []model.CDREventSMF{}, - } - - // 查询数量 长度为0直接返回 - totalSql := "select count(1) as 'total' from cdr_event_smf" - totalRows, err := datasource.RawDB("", totalSql+whereSql, params) - if err != nil { - logger.Errorf("total err => %v", err) - return result - } - total := parse.Number(totalRows[0]["total"]) - if total == 0 { - return result - } else { - result["total"] = total - } - - // 分页 - pageNum, pageSize := repo.PageNumSize(querys.PageNum, querys.PageSize) - pageSql := " limit ?,? " - params = append(params, pageNum*pageSize) - params = append(params, pageSize) - - // 排序 - orderSql := "" - if querys.SortField != "" { - sortSql := querys.SortField - if querys.SortOrder != "" { - if querys.SortOrder == "desc" { - sortSql += " desc " - } else { - sortSql += " asc " - } - } - orderSql = fmt.Sprintf(" order by id desc, %s ", sortSql) - } - - // 查询数据 - querySql := r.selectSql + whereSql + orderSql + pageSql - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err => %v", err) - } - - // 转换实体 - result["rows"] = r.convertResultRows(results) - return result -} - -// SelectByIds 通过ID查询 -func (r *CDREventSMFImpl) SelectByIds(cdrIds []string) []model.CDREventSMF { - placeholder := repo.KeyPlaceholderByQuery(len(cdrIds)) - querySql := r.selectSql + " where id in (" + placeholder + ")" - parameters := repo.ConvertIdsSlice(cdrIds) - results, err := datasource.RawDB("", querySql, parameters) - if err != nil { - logger.Errorf("query err => %v", err) - return []model.CDREventSMF{} - } - // 转换实体 - return r.convertResultRows(results) -} - -// DeleteByIds 批量删除信息 -func (r *CDREventSMFImpl) DeleteByIds(cdrIds []string) int64 { - placeholder := repo.KeyPlaceholderByQuery(len(cdrIds)) - sql := "delete from cdr_event_smf where id in (" + placeholder + ")" - parameters := repo.ConvertIdsSlice(cdrIds) - results, err := datasource.ExecDB("", sql, parameters) - if err != nil { - logger.Errorf("delete err => %v", err) - return 0 - } - return results -} diff --git a/src/modules/network_data/repository/cdr_event_ims.impl.go b/src/modules/network_data/repository/cdr_event_smsc.go similarity index 79% rename from src/modules/network_data/repository/cdr_event_ims.impl.go rename to src/modules/network_data/repository/cdr_event_smsc.go index 6d67b6ca..7673e7bf 100644 --- a/src/modules/network_data/repository/cdr_event_ims.impl.go +++ b/src/modules/network_data/repository/cdr_event_smsc.go @@ -11,9 +11,9 @@ import ( "be.ems/src/modules/network_data/model" ) -// 实例化数据层 CDREventImpl 结构体 -var NewCDREventIMSImpl = &CDREventIMSImpl{ - selectSql: `select id, ne_type, ne_name, rm_uid, timestamp, cdr_json, created_at from cdr_event_ims`, +// 实例化数据层 CDREventSMSC 结构体 +var NewCDREventSMSC = &CDREventSMSC{ + selectSql: `select id, ne_type, ne_name, rm_uid, timestamp, cdr_json, created_at from cdr_event_smsc`, resultMap: map[string]string{ "id": "ID", @@ -26,8 +26,8 @@ var NewCDREventIMSImpl = &CDREventIMSImpl{ }, } -// CDREventIMSImpl CDR会话事件IMS 数据层处理 -type CDREventIMSImpl struct { +// CDREventSMSC CDR会话事件 数据层处理 +type CDREventSMSC struct { // 查询视图对象SQL selectSql string // 结果字段与实体映射 @@ -35,10 +35,10 @@ type CDREventIMSImpl struct { } // convertResultRows 将结果记录转实体结果组 -func (r *CDREventIMSImpl) convertResultRows(rows []map[string]any) []model.CDREventIMS { - arr := make([]model.CDREventIMS, 0) +func (r *CDREventSMSC) convertResultRows(rows []map[string]any) []model.CDREventSMSC { + arr := make([]model.CDREventSMSC, 0) for _, row := range rows { - item := model.CDREventIMS{} + item := model.CDREventSMSC{} for key, value := range row { if keyMapper, ok := r.resultMap[key]; ok { repo.SetFieldValue(&item, keyMapper, value) @@ -50,7 +50,7 @@ func (r *CDREventIMSImpl) convertResultRows(rows []map[string]any) []model.CDREv } // SelectPage 根据条件分页查询 -func (r *CDREventIMSImpl) SelectPage(querys model.CDREventIMSQuery) map[string]any { +func (r *CDREventSMSC) SelectPage(querys model.CDREventSMSCQuery) map[string]any { // 查询条件拼接 var conditions []string var params []any @@ -76,14 +76,6 @@ func (r *CDREventIMSImpl) SelectPage(querys model.CDREventIMSQuery) map[string]a } params = append(params, querys.EndTime) } - if querys.CallerParty != "" { - conditions = append(conditions, "JSON_EXTRACT(cdr_json, '$.callerParty') = ?") - params = append(params, querys.CallerParty) - } - if querys.CalledParty != "" { - conditions = append(conditions, "JSON_EXTRACT(cdr_json, '$.calledParty') = ?") - params = append(params, querys.CalledParty) - } // MySQL8支持的 // if querys.RecordType != "" { // recordTypes := strings.Split(querys.RecordType, ",") @@ -112,11 +104,11 @@ func (r *CDREventIMSImpl) SelectPage(querys model.CDREventIMSQuery) map[string]a result := map[string]any{ "total": 0, - "rows": []model.CDREventIMS{}, + "rows": []model.CDREventSMSC{}, } // 查询数量 长度为0直接返回 - totalSql := "select count(1) as 'total' from cdr_event_ims" + totalSql := "select count(1) as 'total' from cdr_event_smsc" totalRows, err := datasource.RawDB("", totalSql+whereSql, params) if err != nil { logger.Errorf("total err => %v", err) @@ -162,23 +154,23 @@ func (r *CDREventIMSImpl) SelectPage(querys model.CDREventIMSQuery) map[string]a } // SelectByIds 通过ID查询 -func (r *CDREventIMSImpl) SelectByIds(cdrIds []string) []model.CDREventIMS { +func (r *CDREventSMSC) SelectByIds(cdrIds []string) []model.CDREventSMSC { placeholder := repo.KeyPlaceholderByQuery(len(cdrIds)) querySql := r.selectSql + " where id in (" + placeholder + ")" parameters := repo.ConvertIdsSlice(cdrIds) results, err := datasource.RawDB("", querySql, parameters) if err != nil { logger.Errorf("query err => %v", err) - return []model.CDREventIMS{} + return []model.CDREventSMSC{} } // 转换实体 return r.convertResultRows(results) } // DeleteByIds 批量删除信息 -func (r *CDREventIMSImpl) DeleteByIds(cdrIds []string) int64 { +func (r *CDREventSMSC) DeleteByIds(cdrIds []string) int64 { placeholder := repo.KeyPlaceholderByQuery(len(cdrIds)) - sql := "delete from cdr_event_ims where id in (" + placeholder + ")" + sql := "delete from cdr_event_smsc where id in (" + placeholder + ")" parameters := repo.ConvertIdsSlice(cdrIds) results, err := datasource.ExecDB("", sql, parameters) if err != nil { diff --git a/src/modules/network_data/repository/perf_kpi.go b/src/modules/network_data/repository/perf_kpi.go index 1a00957f..e757fe01 100644 --- a/src/modules/network_data/repository/perf_kpi.go +++ b/src/modules/network_data/repository/perf_kpi.go @@ -1,15 +1,131 @@ package repository -import "be.ems/src/modules/network_data/model" +import ( + "fmt" + "strings" -// 性能统计 数据层接口 -type IPerfKPI interface { - // SelectGoldKPI 通过网元指标数据信息 - SelectGoldKPI(query model.GoldKPIQuery, kpiIds []string) []map[string]any + "be.ems/src/framework/datasource" + "be.ems/src/framework/logger" + "be.ems/src/modules/network_data/model" +) - // SelectGoldKPITitle 网元对应的指标名称 - SelectGoldKPITitle(neType string) []model.GoldKPITitle +// 实例化数据层 PerfKPI 结构体 +var NewPerfKPI = &PerfKPI{} - // SelectUPFTotalFlow 查询UPF总流量 N3上行 N6下行 - SelectUPFTotalFlow(neType, rmUID, startDate, endDate string) map[string]any +// PerfKPI 性能统计 数据层处理 +type PerfKPI struct{} + +// SelectGoldKPI 通过网元指标数据信息 +func (r *PerfKPI) SelectGoldKPI(query model.GoldKPIQuery, kpiIds []string) []map[string]any { + // 查询条件拼接 + var conditions []string + var params []any + var tableName string = "kpi_report_" + if query.RmUID != "" { + conditions = append(conditions, "gk.rm_uid = ?") + params = append(params, query.RmUID) + } + if query.NeType != "" { + //conditions = append(conditions, "gk.ne_type = ?") + // params = append(params, query.NeType) + tableName += strings.ToLower(query.NeType) + } + if query.StartTime != "" { + conditions = append(conditions, "gk.created_at >= ?") + params = append(params, query.StartTime) + } + if query.EndTime != "" { + conditions = append(conditions, "gk.created_at <= ?") + params = append(params, query.EndTime) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询字段列 + var fields = []string{ + // fmt.Sprintf("FROM_UNIXTIME(FLOOR(gk.created_at / (%d * 1000)) * %d) AS timeGroup", query.Interval, query.Interval), + fmt.Sprintf("CONCAT(FLOOR(gk.created_at / (%d * 1000)) * (%d * 1000)) AS timeGroup", query.Interval, query.Interval), // 时间戳毫秒 + "min(CASE WHEN gk.index != '' THEN gk.index ELSE 0 END) AS startIndex", + "min(CASE WHEN gk.ne_type != '' THEN gk.ne_type ELSE 0 END) AS neType", + "min(CASE WHEN gk.ne_name != '' THEN gk.ne_name ELSE 0 END) AS neName", + } + for i, kid := range kpiIds { + // 特殊字段,只取最后一次收到的非0值 + if kid == "AMF.01" || kid == "UDM.01" || kid == "UDM.02" || kid == "UDM.03" || kid == "SMF.01" { + str := fmt.Sprintf("IFNULL(SUBSTRING_INDEX(GROUP_CONCAT( CASE WHEN JSON_EXTRACT(gk.kpi_values, '$[%d].kpi_id') = '%s' THEN JSON_EXTRACT(gk.kpi_values, '$[%d].value') END ), ',', 1), 0) AS '%s'", i, kid, i, kid) + fields = append(fields, str) + } else { + str := fmt.Sprintf("sum(CASE WHEN JSON_EXTRACT(gk.kpi_values, '$[%d].kpi_id') = '%s' THEN JSON_EXTRACT(gk.kpi_values, '$[%d].value') ELSE 0 END) AS '%s'", i, kid, i, kid) + fields = append(fields, str) + } + } + fieldsSql := strings.Join(fields, ",") + + // 查询数据 + if query.SortField == "" { + query.SortField = "timeGroup" + } + if query.SortOrder == "" { + query.SortOrder = "desc" + } + orderSql := fmt.Sprintf(" order by %s %s", query.SortField, query.SortOrder) + querySql := fmt.Sprintf("SELECT %s FROM %s gk %s GROUP BY timeGroup %s", fieldsSql, tableName, whereSql, orderSql) + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + } + return results +} + +// SelectGoldKPITitle 网元对应的指标名称 +func (r *PerfKPI) SelectGoldKPITitle(neType string) []model.GoldKPITitle { + result := []model.GoldKPITitle{} + tx := datasource.DefaultDB().Table("kpi_title").Where("ne_type = ?", neType).Find(&result) + if err := tx.Error; err != nil { + logger.Errorf("Find err => %v", err) + } + return result +} + +// SelectUPFTotalFlow 查询UPF总流量 N3上行 N6下行 +func (r *PerfKPI) SelectUPFTotalFlow(neType, rmUID, startDate, endDate string) map[string]any { + // 查询条件拼接 + var conditions []string + var params []any + if neType != "" { + conditions = append(conditions, "kupf.ne_type = ?") + params = append(params, neType) + } + if rmUID != "" { + conditions = append(conditions, "kupf.rm_uid = ?") + params = append(params, rmUID) + } + if startDate != "" { + conditions = append(conditions, "kupf.created_at >= ?") + params = append(params, startDate) + } + if endDate != "" { + conditions = append(conditions, "kupf.created_at <= ?") + params = append(params, endDate) + } + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询数据 + querySql := `SELECT + sum( CASE WHEN JSON_EXTRACT(kupf.kpi_values, '$[2].kpi_id') = 'UPF.03' THEN JSON_EXTRACT(kupf.kpi_values, '$[2].value') ELSE 0 END ) AS 'up', + sum( CASE WHEN JSON_EXTRACT(kupf.kpi_values, '$[5].kpi_id') = 'UPF.06' THEN JSON_EXTRACT(kupf.kpi_values, '$[5].value') ELSE 0 END ) AS 'down' + FROM kpi_report_upf kupf` + results, err := datasource.RawDB("", querySql+whereSql, params) + if err != nil { + logger.Errorf("query err => %v", err) + } + return results[0] } diff --git a/src/modules/network_data/repository/perf_kpi.impl.go b/src/modules/network_data/repository/perf_kpi.impl.go deleted file mode 100644 index b7ac5bb8..00000000 --- a/src/modules/network_data/repository/perf_kpi.impl.go +++ /dev/null @@ -1,131 +0,0 @@ -package repository - -import ( - "fmt" - "strings" - - "be.ems/src/framework/datasource" - "be.ems/src/framework/logger" - "be.ems/src/modules/network_data/model" -) - -// 实例化数据层 PerfKPIImpl 结构体 -var NewPerfKPIImpl = &PerfKPIImpl{} - -// PerfKPIImpl 性能统计 数据层处理 -type PerfKPIImpl struct{} - -// SelectGoldKPI 通过网元指标数据信息 -func (r *PerfKPIImpl) SelectGoldKPI(query model.GoldKPIQuery, kpiIds []string) []map[string]any { - // 查询条件拼接 - var conditions []string - var params []any - var tableName string = "kpi_report_" - if query.RmUID != "" { - conditions = append(conditions, "gk.rm_uid = ?") - params = append(params, query.RmUID) - } - if query.NeType != "" { - //conditions = append(conditions, "gk.ne_type = ?") - // params = append(params, query.NeType) - tableName += strings.ToLower(query.NeType) - } - if query.StartTime != "" { - conditions = append(conditions, "gk.created_at >= ?") - params = append(params, query.StartTime) - } - if query.EndTime != "" { - conditions = append(conditions, "gk.created_at <= ?") - params = append(params, query.EndTime) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } - - // 查询字段列 - var fields = []string{ - // fmt.Sprintf("FROM_UNIXTIME(FLOOR(gk.created_at / (%d * 1000)) * %d) AS timeGroup", query.Interval, query.Interval), - fmt.Sprintf("CONCAT(FLOOR(gk.created_at / (%d * 1000)) * (%d * 1000)) AS timeGroup", query.Interval, query.Interval), // 时间戳毫秒 - "min(CASE WHEN gk.index != '' THEN gk.index ELSE 0 END) AS startIndex", - "min(CASE WHEN gk.ne_type != '' THEN gk.ne_type ELSE 0 END) AS neType", - "min(CASE WHEN gk.ne_name != '' THEN gk.ne_name ELSE 0 END) AS neName", - } - for i, kid := range kpiIds { - // 特殊字段,只取最后一次收到的非0值 - if kid == "AMF.01" || kid == "UDM.01" || kid == "UDM.02" || kid == "UDM.03" || kid == "SMF.01" { - str := fmt.Sprintf("IFNULL(SUBSTRING_INDEX(GROUP_CONCAT( CASE WHEN JSON_EXTRACT(gk.kpi_values, '$[%d].kpi_id') = '%s' THEN JSON_EXTRACT(gk.kpi_values, '$[%d].value') END ), ',', 1), 0) AS '%s'", i, kid, i, kid) - fields = append(fields, str) - } else { - str := fmt.Sprintf("sum(CASE WHEN JSON_EXTRACT(gk.kpi_values, '$[%d].kpi_id') = '%s' THEN JSON_EXTRACT(gk.kpi_values, '$[%d].value') ELSE 0 END) AS '%s'", i, kid, i, kid) - fields = append(fields, str) - } - } - fieldsSql := strings.Join(fields, ",") - - // 查询数据 - if query.SortField == "" { - query.SortField = "timeGroup" - } - if query.SortOrder == "" { - query.SortOrder = "desc" - } - orderSql := fmt.Sprintf(" order by %s %s", query.SortField, query.SortOrder) - querySql := fmt.Sprintf("SELECT %s FROM %s gk %s GROUP BY timeGroup %s", fieldsSql, tableName, whereSql, orderSql) - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err => %v", err) - } - return results -} - -// SelectGoldKPITitle 网元对应的指标名称 -func (r *PerfKPIImpl) SelectGoldKPITitle(neType string) []model.GoldKPITitle { - result := []model.GoldKPITitle{} - tx := datasource.DefaultDB().Table("kpi_title").Where("ne_type = ?", neType).Find(&result) - if err := tx.Error; err != nil { - logger.Errorf("Find err => %v", err) - } - return result -} - -// SelectUPFTotalFlow 查询UPF总流量 N3上行 N6下行 -func (r *PerfKPIImpl) SelectUPFTotalFlow(neType, rmUID, startDate, endDate string) map[string]any { - // 查询条件拼接 - var conditions []string - var params []any - if neType != "" { - conditions = append(conditions, "kupf.ne_type = ?") - params = append(params, neType) - } - if rmUID != "" { - conditions = append(conditions, "kupf.rm_uid = ?") - params = append(params, rmUID) - } - if startDate != "" { - conditions = append(conditions, "kupf.created_at >= ?") - params = append(params, startDate) - } - if endDate != "" { - conditions = append(conditions, "kupf.created_at <= ?") - params = append(params, endDate) - } - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } - - // 查询数据 - querySql := `SELECT - sum( CASE WHEN JSON_EXTRACT(kupf.kpi_values, '$[2].kpi_id') = 'UPF.03' THEN JSON_EXTRACT(kupf.kpi_values, '$[2].value') ELSE 0 END ) AS 'up', - sum( CASE WHEN JSON_EXTRACT(kupf.kpi_values, '$[5].kpi_id') = 'UPF.06' THEN JSON_EXTRACT(kupf.kpi_values, '$[5].value') ELSE 0 END ) AS 'down' - FROM kpi_report_upf kupf` - results, err := datasource.RawDB("", querySql+whereSql, params) - if err != nil { - logger.Errorf("query err => %v", err) - } - return results[0] -} diff --git a/src/modules/network_data/repository/udm_auth.go b/src/modules/network_data/repository/udm_auth.go index 90a3bea8..980ef003 100644 --- a/src/modules/network_data/repository/udm_auth.go +++ b/src/modules/network_data/repository/udm_auth.go @@ -1,26 +1,132 @@ package repository import ( + "be.ems/src/framework/datasource" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/repo" "be.ems/src/modules/network_data/model" ) -// UDM鉴权信息 数据层接口 -type IUDMAuth interface { - // ClearAndInsert 清空ne_id后新增实体 - ClearAndInsert(neId string, uArr []model.UDMAuth) int64 +// 实例化数据层 UDMAuthUser 结构体 +var NewUDMAuthUser = &UDMAuthUser{} - // SelectPage 根据条件分页查询 - SelectPage(query map[string]any) map[string]any +// UDMAuthUser UDM鉴权信息表 数据层处理 +type UDMAuthUser struct{} - // SelectList 根据实体查询 - SelectList(u model.UDMAuth) []model.UDMAuth - - // Insert 批量添加 - Inserts(uArr []model.UDMAuth) int64 - - // Delete 删除实体 - Delete(neId, imsi string) int64 - - // DeletePrefixByIMSI 删除前缀匹配的实体 - DeletePrefixByIMSI(neId, imsi string) int64 +// ClearAndInsert 清空ne_id后新增实体 +func (r *UDMAuthUser) ClearAndInsert(neId string, uArr []model.UDMAuthUser) int64 { + // 不指定neID时,用 TRUNCATE 清空表快 + // _, err := datasource.ExecDB("", "TRUNCATE TABLE u_auth_user", nil) + result := datasource.DB("").Where("ne_id = ?", neId).Unscoped().Delete(&model.UDMAuthUser{}) + if result.Error != nil { + logger.Errorf("Delete err => %v", result.Error) + } + return r.Inserts(uArr) +} + +// SelectPage 根据条件分页查询 +func (r *UDMAuthUser) SelectPage(query map[string]any) (int64, []model.UDMAuthUser) { + tx := datasource.DB("").Model(&model.UDMAuthUser{}) + // 查询条件拼接 + if v, ok := query["imsi"]; ok && v != "" { + tx = tx.Where("imsi like concat(concat('%',?), '%')", v) + } + if v, ok := query["neId"]; ok && v != "" { + tx = tx.Where("ne_id =?", v) + } + if v, ok := query["imsis"]; ok && v != "" { + tx = tx.Where("imsi in ?", v) + } + + var total int64 = 0 + rows := []model.UDMAuthUser{} + + // 查询数量 长度为0直接返回 + if err := tx.Count(&total).Error; err != nil || total <= 0 { + logger.Errorf("total err => %v", err) + return total, rows + } + + // 分页 + pageNum, pageSize := repo.PageNumSize(query["pageNum"], query["pageSize"]) + tx = tx.Offset(int(pageNum * pageSize)).Limit(int(pageSize)) + + // 排序 + if v, ok := query["sortField"]; ok && v != "" { + sortSql := v.(string) + if o, ok := query["sortOrder"]; ok && o != nil && v != "" { + if o == "desc" { + sortSql += " desc " + } else { + sortSql += " asc " + } + } + tx = tx.Order(sortSql) + } + + // 查询数据 + if err := tx.Find(&rows).Error; err != nil { + logger.Errorf("query err => %v", err) + } + + return total, rows +} + +// SelectList 根据实体查询 +func (r *UDMAuthUser) SelectList(u model.UDMAuthUser) []model.UDMAuthUser { + tx := datasource.DB("").Model(&model.UDMAuthUser{}) + // 查询条件拼接 + if u.IMSI != "" { + tx = tx.Where("imsi = ?", u.IMSI) + } + if u.NeId != "" { + tx = tx.Where("ne_id = ?", u.NeId) + } + + // 查询数据 + arr := []model.UDMAuthUser{} + if err := tx.Order("imsi asc").Find(&arr).Error; err != nil { + logger.Errorf("query err => %v", err) + } + return arr +} + +// SelectByIMSIAndNeID 通过imsi和ne_id查询 +func (r *UDMAuthUser) SelectByIMSIAndNeID(imsi, neId string) model.UDMAuthUser { + tx := datasource.DB("").Model(&model.UDMAuthUser{}) + item := model.UDMAuthUser{} + // 查询条件拼接 + tx = tx.Where("imsi = ? and ne_id = ?", imsi, neId) + // 查询数据 + if err := tx.Order("imsi asc").Limit(1).Find(&item).Error; err != nil { + logger.Errorf("query err => %v", err) + } + return item +} + +// Insert 批量添加 +func (r *UDMAuthUser) Inserts(uArr []model.UDMAuthUser) int64 { + tx := datasource.DB("").CreateInBatches(uArr, 3000) + if err := tx.Error; err != nil { + logger.Errorf("CreateInBatches err => %v", err) + } + return tx.RowsAffected +} + +// Delete 删除实体 +func (r *UDMAuthUser) Delete(imsi, neId string) int64 { + tx := datasource.DefaultDB().Where("imsi = ? and ne_id = ?", imsi, neId).Delete(&model.UDMAuthUser{}) + if err := tx.Error; err != nil { + logger.Errorf("Delete err => %v", err) + } + return tx.RowsAffected +} + +// DeletePrefixByIMSI 删除前缀匹配的实体 +func (r *UDMAuthUser) DeletePrefixByIMSI(neId, imsi string) int64 { + tx := datasource.DefaultDB().Where("imsi like concat(?, '%') and ne_id = ?", imsi, neId).Delete(&model.UDMAuthUser{}) + if err := tx.Error; err != nil { + logger.Errorf("DeletePrefixByIMSI err => %v", err) + } + return tx.RowsAffected } diff --git a/src/modules/network_data/repository/udm_sub.go b/src/modules/network_data/repository/udm_sub.go index db8db0f8..7f57dc50 100644 --- a/src/modules/network_data/repository/udm_sub.go +++ b/src/modules/network_data/repository/udm_sub.go @@ -1,26 +1,135 @@ package repository import ( + "be.ems/src/framework/datasource" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/repo" "be.ems/src/modules/network_data/model" ) -// UDM签约信息 数据层接口 -type IUDMSub interface { - // ClearAndInsert 清空ne_id后新增实体 - ClearAndInsert(neId string, uArr []model.UDMSub) int64 +// 实例化数据层 UDMSubUser 结构体 +var NewUDMSub = &UDMSubUser{} - // SelectPage 根据条件分页查询 - SelectPage(query map[string]any) map[string]any +// UDMSubUser UDM签约信息表 数据层处理 +type UDMSubUser struct{} - // SelectList 根据实体查询 - SelectList(u model.UDMSub) []model.UDMSub - - // Insert 批量添加 - Inserts(uArr []model.UDMSub) int64 - - // Delete 删除实体 - Delete(neId, imsi string) int64 - - // DeletePrefixByIMSI 删除前缀匹配的实体 - DeletePrefixByIMSI(neId, imsi string) int64 +// ClearAndInsert 清空ne_id后新增实体 +func (r *UDMSubUser) ClearAndInsert(neId string, u []model.UDMSubUser) int64 { + // 不指定neID时,用 TRUNCATE 清空表快 + // _, err := datasource.ExecDB("", "TRUNCATE TABLE u_sub_user", nil) + result := datasource.DB("").Where("ne_id = ?", neId).Unscoped().Delete(&model.UDMSubUser{}) + if result.Error != nil { + logger.Errorf("Delete err => %v", result.Error) + } + return r.Inserts(u) +} + +// SelectPage 根据条件分页查询字典类型 +func (r *UDMSubUser) SelectPage(query map[string]any) (int64, []model.UDMSubUser) { + tx := datasource.DB("").Model(&model.UDMSubUser{}) + // 查询条件拼接 + if v, ok := query["imsi"]; ok && v != "" { + tx = tx.Where("imsi like concat(concat('%', ?), '%')", v) + } + if v, ok := query["msisdn"]; ok && v != "" { + tx = tx.Where("msisdn like concat(concat('%', ?), '%')", v) + } + if v, ok := query["neId"]; ok && v != "" { + tx = tx.Where("ne_id =?", v) + } + if v, ok := query["imsis"]; ok && v != "" { + tx = tx.Where("imsi in ?", v) + } + + var total int64 = 0 + rows := []model.UDMSubUser{} + + // 查询数量 长度为0直接返回 + if err := tx.Count(&total).Error; err != nil || total <= 0 { + logger.Errorf("total err => %v", err) + return total, rows + } + + // 分页 + pageNum, pageSize := repo.PageNumSize(query["pageNum"], query["pageSize"]) + tx = tx.Offset(int(pageNum * pageSize)).Limit(int(pageSize)) + + // 排序 + if v, ok := query["sortField"]; ok && v != "" { + sortSql := v.(string) + if o, ok := query["sortOrder"]; ok && o != nil && v != "" { + if o == "desc" { + sortSql += " desc " + } else { + sortSql += " asc " + } + } + tx = tx.Order(sortSql) + } + + // 查询数据 + if err := tx.Find(&rows).Error; err != nil { + logger.Errorf("query err => %v", err) + } + + return total, rows +} + +// SelectList 根据实体查询 +func (r *UDMSubUser) SelectList(u model.UDMSubUser) []model.UDMSubUser { + tx := datasource.DB("").Model(&model.UDMSubUser{}) + // 查询条件拼接 + if u.IMSI != "" { + tx = tx.Where("imsi = ?", u.IMSI) + } + if u.NeId != "" { + tx = tx.Where("ne_id = ?", u.NeId) + } + + // 查询数据 + arr := []model.UDMSubUser{} + if err := tx.Order("imsi asc").Find(&arr).Error; err != nil { + logger.Errorf("query err => %v", err) + } + return arr +} + +// SelectByIMSIAndNeID 通过imsi和ne_id查询 +func (r *UDMSubUser) SelectByIMSIAndNeID(imsi, neId string) model.UDMSubUser { + tx := datasource.DB("").Model(&model.UDMSubUser{}) + item := model.UDMSubUser{} + // 查询条件拼接 + tx = tx.Where("imsi = ? and ne_id = ?", imsi, neId) + // 查询数据 + if err := tx.Order("imsi asc").Limit(1).Find(&item).Error; err != nil { + logger.Errorf("query err => %v", err) + } + return item +} + +// Insert 批量添加 +func (r *UDMSubUser) Inserts(uArr []model.UDMSubUser) int64 { + tx := datasource.DB("").CreateInBatches(uArr, 2000) + if err := tx.Error; err != nil { + logger.Errorf("CreateInBatches err => %v", err) + } + return tx.RowsAffected +} + +// Delete 删除实体 +func (r *UDMSubUser) Delete(imsi, neId string) int64 { + tx := datasource.DefaultDB().Where("imsi = ? and ne_id = ?", imsi, neId).Delete(&model.UDMSubUser{}) + if err := tx.Error; err != nil { + logger.Errorf("Delete err => %v", err) + } + return tx.RowsAffected +} + +// DeletePrefixByIMSI 删除前缀匹配的实体 +func (r *UDMSubUser) DeletePrefixByIMSI(imsiPrefix, neId string) int64 { + tx := datasource.DefaultDB().Where("imsi like concat(?, '%') and ne_id = ?", imsiPrefix, neId).Delete(&model.UDMSubUser{}) + if err := tx.Error; err != nil { + logger.Errorf("DeletePrefixByIMSI err => %v", err) + } + return tx.RowsAffected } diff --git a/src/modules/network_data/repository/udm_sub.impl.go b/src/modules/network_data/repository/udm_sub.impl.go deleted file mode 100644 index c5ddda82..00000000 --- a/src/modules/network_data/repository/udm_sub.impl.go +++ /dev/null @@ -1,211 +0,0 @@ -package repository - -import ( - "fmt" - "strings" - - "be.ems/src/framework/datasource" - "be.ems/src/framework/logger" - "be.ems/src/framework/utils/parse" - "be.ems/src/framework/utils/repo" - "be.ems/src/modules/network_data/model" -) - -// 实例化数据层 UDMSubImpl 结构体 -var NewUDMSubImpl = &UDMSubImpl{ - selectSql: `select - id, msisdn, imsi, ambr, nssai, rat, arfb, sar, cn, sm_data, smf_sel, eps_dat, ne_id, eps_flag, eps_odb, hplmn_odb, ard, epstpl, context_id, apn_context, static_ip - from u_sub_user`, - - resultMap: map[string]string{ - "id": "ID", - "msisdn": "Msisdn", - "imsi": "IMSI", - "ambr": "Ambr", - "nssai": "Nssai", - "rat": "Rat", - "arfb": "Arfb", - "sar": "Sar", - "cn": "Cn", - "sm_data": "SmData", - "smf_sel": "SmfSel", - "eps_dat": "EpsDat", - "ne_id": "NeId", - "eps_flag": "EpsFlag", - "eps_odb": "EpsOdb", - "hplmn_odb": "HplmnOdb", - "ard": "Ard", - "epstpl": "Epstpl", - "context_id": "ContextId", - "apn_context": "ApnContext", - "static_ip": "StaticIp", - }, -} - -// UDMSubImpl UDM签约信息表 数据层处理 -type UDMSubImpl struct { - // 查询视图对象SQL - selectSql string - // 结果字段与实体映射 - resultMap map[string]string -} - -// convertResultRows 将结果记录转实体结果组 -func (r *UDMSubImpl) convertResultRows(rows []map[string]any) []model.UDMSub { - arr := make([]model.UDMSub, 0) - for _, row := range rows { - item := model.UDMSub{} - for key, value := range row { - if keyMapper, ok := r.resultMap[key]; ok { - repo.SetFieldValue(&item, keyMapper, value) - } - } - arr = append(arr, item) - } - return arr -} - -// ClearAndInsert 清空ne_id后新增实体 -func (r *UDMSubImpl) ClearAndInsert(neID string, u []model.UDMSub) int64 { - // 不指定neID时,用 TRUNCATE 清空表快 - _, err := datasource.ExecDB("", "TRUNCATE TABLE u_sub_user", nil) - if err != nil { - logger.Errorf("TRUNCATE err => %v", err) - } - - return r.Inserts(u) -} - -// SelectPage 根据条件分页查询字典类型 -func (r *UDMSubImpl) SelectPage(query map[string]any) map[string]any { - // 查询条件拼接 - var conditions []string - var params []any - if v, ok := query["msisdn"]; ok && v != "" { - conditions = append(conditions, "msisdn like concat(concat('%', ?), '%')") - params = append(params, strings.Trim(v.(string), " ")) - } - if v, ok := query["imsi"]; ok && v != "" { - conditions = append(conditions, "imsi like concat(concat('%', ?), '%')") - params = append(params, strings.Trim(v.(string), " ")) - } - if v, ok := query["neId"]; ok && v != "" { - conditions = append(conditions, "ne_id = ?") - params = append(params, v) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } - - result := map[string]any{ - "total": 0, - "rows": []model.UDMSub{}, - } - - // 查询数量 长度为0直接返回 - totalSql := "select count(1) as 'total' from u_sub_user" - totalRows, err := datasource.RawDB("", totalSql+whereSql, params) - if err != nil { - logger.Errorf("total err => %v", err) - return result - } - total := parse.Number(totalRows[0]["total"]) - if total == 0 { - return result - } else { - result["total"] = total - } - - // 分页 - pageNum, pageSize := repo.PageNumSize(query["pageNum"], query["pageSize"]) - pageSql := " limit ?,? " - params = append(params, pageNum*pageSize) - params = append(params, pageSize) - - // 排序 - orderSql := "" - if v, ok := query["sortField"]; ok && v != "" { - sortSql := v.(string) - if o, ok := query["sortOrder"]; ok && o != nil && v != "" { - if o == "desc" { - sortSql += " desc " - } else { - sortSql += " asc " - } - } - orderSql = fmt.Sprintf(" order by %s ", sortSql) - } - - // 查询数据 - querySql := r.selectSql + whereSql + orderSql + pageSql - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err => %v", err) - return result - } - - // 转换实体 - result["rows"] = r.convertResultRows(results) - return result -} - -// SelectList 根据实体查询 -func (r *UDMSubImpl) SelectList(u model.UDMSub) []model.UDMSub { - // 查询条件拼接 - var conditions []string - var params []any - if u.IMSI != "" { - conditions = append(conditions, "imsi = ?") - params = append(params, u.IMSI) - } - if u.NeId != "" { - conditions = append(conditions, "ne_id = ?") - params = append(params, u.NeId) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } - - // 查询数据 - querySql := r.selectSql + whereSql + " order by imsi asc " - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err => %v", err) - } - - // 转换实体 - return r.convertResultRows(results) -} - -// Insert 批量添加 -func (r *UDMSubImpl) Inserts(uArr []model.UDMSub) int64 { - tx := datasource.DefaultDB().CreateInBatches(uArr, 2000) - if err := tx.Error; err != nil { - logger.Errorf("CreateInBatches err => %v", err) - } - return tx.RowsAffected -} - -// Delete 删除实体 -func (r *UDMSubImpl) Delete(neId, imsi string) int64 { - tx := datasource.DefaultDB().Where("imsi = ? and ne_id = ?", imsi, neId).Delete(&model.UDMSub{}) - if err := tx.Error; err != nil { - logger.Errorf("Delete err => %v", err) - } - return tx.RowsAffected -} - -// DeletePrefixByIMSI 删除前缀匹配的实体 -func (r *UDMSubImpl) DeletePrefixByIMSI(neId, imsi string) int64 { - tx := datasource.DefaultDB().Where("imsi like concat(?, '%') and ne_id = ?", imsi, neId).Delete(&model.UDMSub{}) - if err := tx.Error; err != nil { - logger.Errorf("DeletePrefixByIMSI err => %v", err) - } - return tx.RowsAffected -} diff --git a/src/modules/network_data/repository/udm_auth.impl.go b/src/modules/network_data/repository/udm_user_info.go similarity index 71% rename from src/modules/network_data/repository/udm_auth.impl.go rename to src/modules/network_data/repository/udm_user_info.go index 5fee2f0d..c7aead43 100644 --- a/src/modules/network_data/repository/udm_auth.impl.go +++ b/src/modules/network_data/repository/udm_user_info.go @@ -11,24 +11,21 @@ import ( "be.ems/src/modules/network_data/model" ) -// 实例化数据层 UDMAuthImpl 结构体 -var NewUDMAuthImpl = &UDMAuthImpl{ - selectSql: `select id, imsi, amf, status, ki, algo_index, opc, ne_id from u_auth_user`, +// 实例化数据层 UDMUserInfo 结构体 +var NewUDMUserInfo = &UDMUserInfo{ + selectSql: `select id, imsi, msisdn, ne_id, remark from u_user_info`, resultMap: map[string]string{ - "id": "ID", - "imsi": "IMSI", - "amf": "Amf", - "status": "Status", - "ki": "Ki", - "algo_index": "AlgoIndex", - "opc": "Opc", - "ne_id": "NeId", + "id": "ID", + "imsi": "IMSI", + "msisdn": "MSISDN", + "ne_id": "NeId", + "remark": "Remark", }, } -// UDMAuthImpl UDM鉴权信息表 数据层处理 -type UDMAuthImpl struct { +// UDMUserInfo UDM鉴权信息表 数据层处理 +type UDMUserInfo struct { // 查询视图对象SQL selectSql string // 结果字段与实体映射 @@ -36,10 +33,10 @@ type UDMAuthImpl struct { } // convertResultRows 将结果记录转实体结果组 -func (r *UDMAuthImpl) convertResultRows(rows []map[string]any) []model.UDMAuth { - arr := make([]model.UDMAuth, 0) +func (r *UDMUserInfo) convertResultRows(rows []map[string]any) []model.UDMUserInfo { + arr := make([]model.UDMUserInfo, 0) for _, row := range rows { - item := model.UDMAuth{} + item := model.UDMUserInfo{} for key, value := range row { if keyMapper, ok := r.resultMap[key]; ok { repo.SetFieldValue(&item, keyMapper, value) @@ -50,18 +47,8 @@ func (r *UDMAuthImpl) convertResultRows(rows []map[string]any) []model.UDMAuth { return arr } -// ClearAndInsert 清空ne_id后新增实体 -func (r *UDMAuthImpl) ClearAndInsert(neId string, uArr []model.UDMAuth) int64 { - // 不指定neID时,用 TRUNCATE 清空表快 - _, err := datasource.ExecDB("", "TRUNCATE TABLE u_auth_user", nil) - if err != nil { - logger.Errorf("TRUNCATE err => %v", err) - } - return r.Inserts(uArr) -} - // SelectPage 根据条件分页查询 -func (r *UDMAuthImpl) SelectPage(query map[string]any) map[string]any { +func (r *UDMUserInfo) SelectPage(query map[string]any) map[string]any { // 查询条件拼接 var conditions []string var params []any @@ -82,11 +69,11 @@ func (r *UDMAuthImpl) SelectPage(query map[string]any) map[string]any { result := map[string]any{ "total": 0, - "rows": []model.UDMAuth{}, + "rows": []model.UDMUserInfo{}, } // 查询数量 长度为0直接返回 - totalSql := "select count(1) as 'total' from u_auth_user" + totalSql := "select count(1) as 'total' from u_user_info" totalRows, err := datasource.RawDB("", totalSql+whereSql, params) if err != nil { logger.Errorf("total err => %v", err) @@ -132,7 +119,7 @@ func (r *UDMAuthImpl) SelectPage(query map[string]any) map[string]any { } // SelectList 根据实体查询 -func (r *UDMAuthImpl) SelectList(u model.UDMAuth) []model.UDMAuth { +func (r *UDMUserInfo) SelectList(u model.UDMUserInfo) []model.UDMUserInfo { // 查询条件拼接 var conditions []string var params []any @@ -162,8 +149,24 @@ func (r *UDMAuthImpl) SelectList(u model.UDMAuth) []model.UDMAuth { return r.convertResultRows(results) } +// SelectByIMSIAndNeID 通过imsi和ne_id查询 +func (r *UDMUserInfo) SelectByIMSIAndNeID(imsi, neId string) model.UDMUserInfo { + querySql := r.selectSql + " where imsi = ? and ne_id = ?" + results, err := datasource.RawDB("", querySql, []any{imsi, neId}) + if err != nil { + logger.Errorf("query err => %v", err) + return model.UDMUserInfo{} + } + // 转换实体 + rows := r.convertResultRows(results) + if len(rows) > 0 { + return rows[0] + } + return model.UDMUserInfo{} +} + // Insert 批量添加 -func (r *UDMAuthImpl) Inserts(uArr []model.UDMAuth) int64 { +func (r *UDMUserInfo) Inserts(uArr []model.UDMUserInfo) int64 { tx := datasource.DefaultDB().CreateInBatches(uArr, 3000) if err := tx.Error; err != nil { logger.Errorf("CreateInBatches err => %v", err) @@ -172,8 +175,8 @@ func (r *UDMAuthImpl) Inserts(uArr []model.UDMAuth) int64 { } // Delete 删除实体 -func (r *UDMAuthImpl) Delete(neId, imsi string) int64 { - tx := datasource.DefaultDB().Where("imsi = ? and ne_id = ?", imsi, neId).Delete(&model.UDMAuth{}) +func (r *UDMUserInfo) Delete(imsi, neId string) int64 { + tx := datasource.DefaultDB().Where("imsi = ? and ne_id = ?", imsi, neId).Delete(&model.UDMUserInfo{}) if err := tx.Error; err != nil { logger.Errorf("Delete err => %v", err) } @@ -181,8 +184,8 @@ func (r *UDMAuthImpl) Delete(neId, imsi string) int64 { } // DeletePrefixByIMSI 删除前缀匹配的实体 -func (r *UDMAuthImpl) DeletePrefixByIMSI(neId, imsi string) int64 { - tx := datasource.DefaultDB().Where("imsi like concat(?, '%') and ne_id = ?", imsi, neId).Delete(&model.UDMAuth{}) +func (r *UDMUserInfo) DeletePrefixByIMSI(imsiPrefix, neId string) int64 { + tx := datasource.DefaultDB().Where("imsi like concat(?, '%') and ne_id = ?", imsiPrefix, neId).Delete(&model.UDMUserInfo{}) if err := tx.Error; err != nil { logger.Errorf("DeletePrefixByIMSI err => %v", err) } diff --git a/src/modules/network_data/repository/ue_event_amf.go b/src/modules/network_data/repository/ue_event_amf.go index 4e053a01..76442b8c 100644 --- a/src/modules/network_data/repository/ue_event_amf.go +++ b/src/modules/network_data/repository/ue_event_amf.go @@ -1,15 +1,175 @@ package repository -import "be.ems/src/modules/network_data/model" +import ( + "fmt" + "strings" -// UE会话事件AMF 数据层接口 -type IUEEventAMF interface { - // SelectPage 根据条件分页查询 - SelectPage(querys model.UEEventAMFQuery) map[string]any + "be.ems/src/framework/datasource" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/parse" + "be.ems/src/framework/utils/repo" + "be.ems/src/modules/network_data/model" +) - // SelectByIds 通过ID查询 - SelectByIds(ueIds []string) []model.UEEventAMF +// 实例化数据层 UEEventAMF 结构体 +var NewUEEventAMF = &UEEventAMF{ + selectSql: `select id, ne_type, ne_name, rm_uid, timestamp, event_type, event_json, created_at from ue_event_amf`, - // DeleteByIds 批量删除信息 - DeleteByIds(ueIds []string) int64 + resultMap: map[string]string{ + "id": "ID", + "ne_type": "NeType", + "ne_name": "NeName", + "rm_uid": "RmUID", + "timestamp": "Timestamp", + "event_type": "EventType", + "event_json": "EventJSONStr", + "created_at": "CreatedAt", + }, +} + +// UEEventAMF UE会话事件 数据层处理 +type UEEventAMF struct { + // 查询视图对象SQL + selectSql string + // 结果字段与实体映射 + resultMap map[string]string +} + +// convertResultRows 将结果记录转实体结果组 +func (r *UEEventAMF) convertResultRows(rows []map[string]any) []model.UEEventAMF { + arr := make([]model.UEEventAMF, 0) + for _, row := range rows { + item := model.UEEventAMF{} + for key, value := range row { + if keyMapper, ok := r.resultMap[key]; ok { + repo.SetFieldValue(&item, keyMapper, value) + } + } + arr = append(arr, item) + } + return arr +} + +// SelectPage 根据条件分页查询 +func (r *UEEventAMF) SelectPage(querys model.UEEventAMFQuery) map[string]any { + // 查询条件拼接 + var conditions []string + var params []any + if querys.NeType != "" { + conditions = append(conditions, "ne_type = ?") + params = append(params, querys.NeType) + } + if querys.RmUID != "" { + conditions = append(conditions, "rm_uid = ?") + params = append(params, querys.RmUID) + } + if querys.StartTime != "" { + conditions = append(conditions, "timestamp >= ?") + if len(querys.StartTime) == 13 { + querys.StartTime = querys.StartTime[:10] + } + params = append(params, querys.StartTime) + } + if querys.EndTime != "" { + conditions = append(conditions, "timestamp <= ?") + if len(querys.EndTime) == 13 { + querys.EndTime = querys.EndTime[:10] + } + params = append(params, querys.EndTime) + } + if querys.EventType != "" { + eventTypes := strings.Split(querys.EventType, ",") + placeholder := repo.KeyPlaceholderByQuery(len(eventTypes)) + conditions = append(conditions, fmt.Sprintf("event_type in (%s)", placeholder)) + for _, eventType := range eventTypes { + params = append(params, eventType) + } + } + if querys.IMSI != "" { + conditions = append(conditions, "JSON_EXTRACT(event_json, '$.imsi') = ?") + params = append(params, querys.IMSI) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + result := map[string]any{ + "total": 0, + "rows": []model.UEEventAMF{}, + } + + // 查询数量 长度为0直接返回 + totalSql := "select count(1) as 'total' from ue_event_amf" + totalRows, err := datasource.RawDB("", totalSql+whereSql, params) + if err != nil { + logger.Errorf("total err => %v", err) + return result + } + total := parse.Number(totalRows[0]["total"]) + if total == 0 { + return result + } else { + result["total"] = total + } + + // 分页 + pageNum, pageSize := repo.PageNumSize(querys.PageNum, querys.PageSize) + pageSql := " limit ?,? " + params = append(params, pageNum*pageSize) + params = append(params, pageSize) + + // 排序 + orderSql := "" + if querys.SortField != "" { + sortSql := querys.SortField + if querys.SortOrder != "" { + if querys.SortOrder == "desc" { + sortSql += " desc " + } else { + sortSql += " asc " + } + } + orderSql = fmt.Sprintf(" order by id desc, %s ", sortSql) + } + + // 查询数据 + querySql := r.selectSql + whereSql + orderSql + pageSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + } + + // 转换实体 + result["rows"] = r.convertResultRows(results) + return result +} + +// SelectByIds 通过ID查询 +func (r *UEEventAMF) SelectByIds(ueIds []string) []model.UEEventAMF { + placeholder := repo.KeyPlaceholderByQuery(len(ueIds)) + querySql := r.selectSql + " where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(ueIds) + results, err := datasource.RawDB("", querySql, parameters) + if err != nil { + logger.Errorf("query err => %v", err) + return []model.UEEventAMF{} + } + // 转换实体 + return r.convertResultRows(results) +} + +// DeleteByIds 批量删除信息 +func (r *UEEventAMF) DeleteByIds(ueIds []string) int64 { + placeholder := repo.KeyPlaceholderByQuery(len(ueIds)) + sql := "delete from ue_event_amf where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(ueIds) + results, err := datasource.ExecDB("", sql, parameters) + if err != nil { + logger.Errorf("delete err => %v", err) + return 0 + } + return results } diff --git a/src/modules/network_data/repository/ue_event_amf.impl.go b/src/modules/network_data/repository/ue_event_amf.impl.go deleted file mode 100644 index b442207c..00000000 --- a/src/modules/network_data/repository/ue_event_amf.impl.go +++ /dev/null @@ -1,175 +0,0 @@ -package repository - -import ( - "fmt" - "strings" - - "be.ems/src/framework/datasource" - "be.ems/src/framework/logger" - "be.ems/src/framework/utils/parse" - "be.ems/src/framework/utils/repo" - "be.ems/src/modules/network_data/model" -) - -// 实例化数据层 UEEventAMFImpl 结构体 -var NewUEEventAMFImpl = &UEEventAMFImpl{ - selectSql: `select id, ne_type, ne_name, rm_uid, timestamp, event_type, event_json, created_at from ue_event_amf`, - - resultMap: map[string]string{ - "id": "ID", - "ne_type": "NeType", - "ne_name": "NeName", - "rm_uid": "RmUID", - "timestamp": "Timestamp", - "event_type": "EventType", - "event_json": "EventJSONStr", - "created_at": "CreatedAt", - }, -} - -// UEEventAMFImpl UE会话事件 数据层处理 -type UEEventAMFImpl struct { - // 查询视图对象SQL - selectSql string - // 结果字段与实体映射 - resultMap map[string]string -} - -// convertResultRows 将结果记录转实体结果组 -func (r *UEEventAMFImpl) convertResultRows(rows []map[string]any) []model.UEEventAMF { - arr := make([]model.UEEventAMF, 0) - for _, row := range rows { - item := model.UEEventAMF{} - for key, value := range row { - if keyMapper, ok := r.resultMap[key]; ok { - repo.SetFieldValue(&item, keyMapper, value) - } - } - arr = append(arr, item) - } - return arr -} - -// SelectPage 根据条件分页查询 -func (r *UEEventAMFImpl) SelectPage(querys model.UEEventAMFQuery) map[string]any { - // 查询条件拼接 - var conditions []string - var params []any - if querys.NeType != "" { - conditions = append(conditions, "ne_type = ?") - params = append(params, querys.NeType) - } - if querys.RmUID != "" { - conditions = append(conditions, "rm_uid = ?") - params = append(params, querys.RmUID) - } - if querys.StartTime != "" { - conditions = append(conditions, "timestamp >= ?") - if len(querys.StartTime) == 13 { - querys.StartTime = querys.StartTime[:10] - } - params = append(params, querys.StartTime) - } - if querys.EndTime != "" { - conditions = append(conditions, "timestamp <= ?") - if len(querys.EndTime) == 13 { - querys.EndTime = querys.EndTime[:10] - } - params = append(params, querys.EndTime) - } - if querys.EventType != "" { - eventTypes := strings.Split(querys.EventType, ",") - placeholder := repo.KeyPlaceholderByQuery(len(eventTypes)) - conditions = append(conditions, fmt.Sprintf("event_type in (%s)", placeholder)) - for _, eventType := range eventTypes { - params = append(params, eventType) - } - } - if querys.IMSI != "" { - conditions = append(conditions, "JSON_EXTRACT(event_json, '$.imsi') = ?") - params = append(params, querys.IMSI) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } - - result := map[string]any{ - "total": 0, - "rows": []model.UEEventAMF{}, - } - - // 查询数量 长度为0直接返回 - totalSql := "select count(1) as 'total' from ue_event_amf" - totalRows, err := datasource.RawDB("", totalSql+whereSql, params) - if err != nil { - logger.Errorf("total err => %v", err) - return result - } - total := parse.Number(totalRows[0]["total"]) - if total == 0 { - return result - } else { - result["total"] = total - } - - // 分页 - pageNum, pageSize := repo.PageNumSize(querys.PageNum, querys.PageSize) - pageSql := " limit ?,? " - params = append(params, pageNum*pageSize) - params = append(params, pageSize) - - // 排序 - orderSql := "" - if querys.SortField != "" { - sortSql := querys.SortField - if querys.SortOrder != "" { - if querys.SortOrder == "desc" { - sortSql += " desc " - } else { - sortSql += " asc " - } - } - orderSql = fmt.Sprintf(" order by id desc, %s ", sortSql) - } - - // 查询数据 - querySql := r.selectSql + whereSql + orderSql + pageSql - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err => %v", err) - } - - // 转换实体 - result["rows"] = r.convertResultRows(results) - return result -} - -// SelectByIds 通过ID查询 -func (r *UEEventAMFImpl) SelectByIds(ueIds []string) []model.UEEventAMF { - placeholder := repo.KeyPlaceholderByQuery(len(ueIds)) - querySql := r.selectSql + " where id in (" + placeholder + ")" - parameters := repo.ConvertIdsSlice(ueIds) - results, err := datasource.RawDB("", querySql, parameters) - if err != nil { - logger.Errorf("query err => %v", err) - return []model.UEEventAMF{} - } - // 转换实体 - return r.convertResultRows(results) -} - -// DeleteByIds 批量删除信息 -func (r *UEEventAMFImpl) DeleteByIds(ueIds []string) int64 { - placeholder := repo.KeyPlaceholderByQuery(len(ueIds)) - sql := "delete from ue_event_amf where id in (" + placeholder + ")" - parameters := repo.ConvertIdsSlice(ueIds) - results, err := datasource.ExecDB("", sql, parameters) - if err != nil { - logger.Errorf("delete err => %v", err) - return 0 - } - return results -} diff --git a/src/modules/network_data/repository/ue_event_mme.go b/src/modules/network_data/repository/ue_event_mme.go index 7a77fb18..a035f5aa 100644 --- a/src/modules/network_data/repository/ue_event_mme.go +++ b/src/modules/network_data/repository/ue_event_mme.go @@ -1,15 +1,175 @@ package repository -import "be.ems/src/modules/network_data/model" +import ( + "fmt" + "strings" -// UE会话事件MME 数据层接口 -type IUEEventMME interface { - // SelectPage 根据条件分页查询 - SelectPage(querys model.UEEventMMEQuery) map[string]any + "be.ems/src/framework/datasource" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/parse" + "be.ems/src/framework/utils/repo" + "be.ems/src/modules/network_data/model" +) - // SelectByIds 通过ID查询 - SelectByIds(ueIds []string) []model.UEEventMME +// 实例化数据层 UEEventMME 结构体 +var NewUEEventMME = &UEEventMME{ + selectSql: `select id, ne_type, ne_name, rm_uid, timestamp, event_type, event_json, created_at from ue_event_mme`, - // DeleteByIds 批量删除信息 - DeleteByIds(ueIds []string) int64 + resultMap: map[string]string{ + "id": "ID", + "ne_type": "NeType", + "ne_name": "NeName", + "rm_uid": "RmUID", + "timestamp": "Timestamp", + "event_type": "EventType", + "event_json": "EventJSONStr", + "created_at": "CreatedAt", + }, +} + +// UEEventMME UE会话事件 数据层处理 +type UEEventMME struct { + // 查询视图对象SQL + selectSql string + // 结果字段与实体映射 + resultMap map[string]string +} + +// convertResultRows 将结果记录转实体结果组 +func (r *UEEventMME) convertResultRows(rows []map[string]any) []model.UEEventMME { + arr := make([]model.UEEventMME, 0) + for _, row := range rows { + item := model.UEEventMME{} + for key, value := range row { + if keyMapper, ok := r.resultMap[key]; ok { + repo.SetFieldValue(&item, keyMapper, value) + } + } + arr = append(arr, item) + } + return arr +} + +// SelectPage 根据条件分页查询 +func (r *UEEventMME) SelectPage(querys model.UEEventMMEQuery) map[string]any { + // 查询条件拼接 + var conditions []string + var params []any + if querys.NeType != "" { + conditions = append(conditions, "ne_type = ?") + params = append(params, querys.NeType) + } + if querys.RmUID != "" { + conditions = append(conditions, "rm_uid = ?") + params = append(params, querys.RmUID) + } + if querys.StartTime != "" { + conditions = append(conditions, "timestamp >= ?") + if len(querys.StartTime) == 13 { + querys.StartTime = querys.StartTime[:10] + } + params = append(params, querys.StartTime) + } + if querys.EndTime != "" { + conditions = append(conditions, "timestamp <= ?") + if len(querys.EndTime) == 13 { + querys.EndTime = querys.EndTime[:10] + } + params = append(params, querys.EndTime) + } + if querys.EventType != "" { + eventTypes := strings.Split(querys.EventType, ",") + placeholder := repo.KeyPlaceholderByQuery(len(eventTypes)) + conditions = append(conditions, fmt.Sprintf("event_type in (%s)", placeholder)) + for _, eventType := range eventTypes { + params = append(params, eventType) + } + } + if querys.IMSI != "" { + conditions = append(conditions, "JSON_EXTRACT(event_json, '$.imsi') = ?") + params = append(params, querys.IMSI) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + result := map[string]any{ + "total": 0, + "rows": []model.UEEventMME{}, + } + + // 查询数量 长度为0直接返回 + totalSql := "select count(1) as 'total' from ue_event_mme" + totalRows, err := datasource.RawDB("", totalSql+whereSql, params) + if err != nil { + logger.Errorf("total err => %v", err) + return result + } + total := parse.Number(totalRows[0]["total"]) + if total == 0 { + return result + } else { + result["total"] = total + } + + // 分页 + pageNum, pageSize := repo.PageNumSize(querys.PageNum, querys.PageSize) + pageSql := " limit ?,? " + params = append(params, pageNum*pageSize) + params = append(params, pageSize) + + // 排序 + orderSql := "" + if querys.SortField != "" { + sortSql := querys.SortField + if querys.SortOrder != "" { + if querys.SortOrder == "desc" { + sortSql += " desc " + } else { + sortSql += " asc " + } + } + orderSql = fmt.Sprintf(" order by id desc, %s ", sortSql) + } + + // 查询数据 + querySql := r.selectSql + whereSql + orderSql + pageSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + } + + // 转换实体 + result["rows"] = r.convertResultRows(results) + return result +} + +// SelectByIds 通过ID查询 +func (r *UEEventMME) SelectByIds(ueIds []string) []model.UEEventMME { + placeholder := repo.KeyPlaceholderByQuery(len(ueIds)) + querySql := r.selectSql + " where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(ueIds) + results, err := datasource.RawDB("", querySql, parameters) + if err != nil { + logger.Errorf("query err => %v", err) + return []model.UEEventMME{} + } + // 转换实体 + return r.convertResultRows(results) +} + +// DeleteByIds 批量删除信息 +func (r *UEEventMME) DeleteByIds(ueIds []string) int64 { + placeholder := repo.KeyPlaceholderByQuery(len(ueIds)) + sql := "delete from ue_event_mme where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(ueIds) + results, err := datasource.ExecDB("", sql, parameters) + if err != nil { + logger.Errorf("delete err => %v", err) + return 0 + } + return results } diff --git a/src/modules/network_data/repository/ue_event_mme.impl.go b/src/modules/network_data/repository/ue_event_mme.impl.go deleted file mode 100644 index 180bc9df..00000000 --- a/src/modules/network_data/repository/ue_event_mme.impl.go +++ /dev/null @@ -1,175 +0,0 @@ -package repository - -import ( - "fmt" - "strings" - - "be.ems/src/framework/datasource" - "be.ems/src/framework/logger" - "be.ems/src/framework/utils/parse" - "be.ems/src/framework/utils/repo" - "be.ems/src/modules/network_data/model" -) - -// 实例化数据层 UEEventMMEImpl 结构体 -var NewUEEventMMEImpl = &UEEventMMEImpl{ - selectSql: `select id, ne_type, ne_name, rm_uid, timestamp, event_type, event_json, created_at from ue_event_mme`, - - resultMap: map[string]string{ - "id": "ID", - "ne_type": "NeType", - "ne_name": "NeName", - "rm_uid": "RmUID", - "timestamp": "Timestamp", - "event_type": "EventType", - "event_json": "EventJSONStr", - "created_at": "CreatedAt", - }, -} - -// UEEventMMEImpl UE会话事件 数据层处理 -type UEEventMMEImpl struct { - // 查询视图对象SQL - selectSql string - // 结果字段与实体映射 - resultMap map[string]string -} - -// convertResultRows 将结果记录转实体结果组 -func (r *UEEventMMEImpl) convertResultRows(rows []map[string]any) []model.UEEventMME { - arr := make([]model.UEEventMME, 0) - for _, row := range rows { - item := model.UEEventMME{} - for key, value := range row { - if keyMapper, ok := r.resultMap[key]; ok { - repo.SetFieldValue(&item, keyMapper, value) - } - } - arr = append(arr, item) - } - return arr -} - -// SelectPage 根据条件分页查询 -func (r *UEEventMMEImpl) SelectPage(querys model.UEEventMMEQuery) map[string]any { - // 查询条件拼接 - var conditions []string - var params []any - if querys.NeType != "" { - conditions = append(conditions, "ne_type = ?") - params = append(params, querys.NeType) - } - if querys.RmUID != "" { - conditions = append(conditions, "rm_uid = ?") - params = append(params, querys.RmUID) - } - if querys.StartTime != "" { - conditions = append(conditions, "timestamp >= ?") - if len(querys.StartTime) == 13 { - querys.StartTime = querys.StartTime[:10] - } - params = append(params, querys.StartTime) - } - if querys.EndTime != "" { - conditions = append(conditions, "timestamp <= ?") - if len(querys.EndTime) == 13 { - querys.EndTime = querys.EndTime[:10] - } - params = append(params, querys.EndTime) - } - if querys.EventType != "" { - eventTypes := strings.Split(querys.EventType, ",") - placeholder := repo.KeyPlaceholderByQuery(len(eventTypes)) - conditions = append(conditions, fmt.Sprintf("event_type in (%s)", placeholder)) - for _, eventType := range eventTypes { - params = append(params, eventType) - } - } - if querys.IMSI != "" { - conditions = append(conditions, "JSON_EXTRACT(event_json, '$.imsi') = ?") - params = append(params, querys.IMSI) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } - - result := map[string]any{ - "total": 0, - "rows": []model.UEEventMME{}, - } - - // 查询数量 长度为0直接返回 - totalSql := "select count(1) as 'total' from ue_event_mme" - totalRows, err := datasource.RawDB("", totalSql+whereSql, params) - if err != nil { - logger.Errorf("total err => %v", err) - return result - } - total := parse.Number(totalRows[0]["total"]) - if total == 0 { - return result - } else { - result["total"] = total - } - - // 分页 - pageNum, pageSize := repo.PageNumSize(querys.PageNum, querys.PageSize) - pageSql := " limit ?,? " - params = append(params, pageNum*pageSize) - params = append(params, pageSize) - - // 排序 - orderSql := "" - if querys.SortField != "" { - sortSql := querys.SortField - if querys.SortOrder != "" { - if querys.SortOrder == "desc" { - sortSql += " desc " - } else { - sortSql += " asc " - } - } - orderSql = fmt.Sprintf(" order by id desc, %s ", sortSql) - } - - // 查询数据 - querySql := r.selectSql + whereSql + orderSql + pageSql - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err => %v", err) - } - - // 转换实体 - result["rows"] = r.convertResultRows(results) - return result -} - -// SelectByIds 通过ID查询 -func (r *UEEventMMEImpl) SelectByIds(ueIds []string) []model.UEEventMME { - placeholder := repo.KeyPlaceholderByQuery(len(ueIds)) - querySql := r.selectSql + " where id in (" + placeholder + ")" - parameters := repo.ConvertIdsSlice(ueIds) - results, err := datasource.RawDB("", querySql, parameters) - if err != nil { - logger.Errorf("query err => %v", err) - return []model.UEEventMME{} - } - // 转换实体 - return r.convertResultRows(results) -} - -// DeleteByIds 批量删除信息 -func (r *UEEventMMEImpl) DeleteByIds(ueIds []string) int64 { - placeholder := repo.KeyPlaceholderByQuery(len(ueIds)) - sql := "delete from ue_event_mme where id in (" + placeholder + ")" - parameters := repo.ConvertIdsSlice(ueIds) - results, err := datasource.ExecDB("", sql, parameters) - if err != nil { - logger.Errorf("delete err => %v", err) - return 0 - } - return results -} diff --git a/src/modules/network_data/service/alarm.go b/src/modules/network_data/service/alarm.go index 3c466ee4..3df04ead 100644 --- a/src/modules/network_data/service/alarm.go +++ b/src/modules/network_data/service/alarm.go @@ -1,12 +1,39 @@ package service -import "be.ems/src/modules/network_data/model" +import ( + "fmt" -// 告警 服务层接口 -type IAlarm interface { - // SelectPage 根据条件分页查询 - SelectPage(querys model.AlarmQuery) map[string]any + "be.ems/src/modules/network_data/model" + "be.ems/src/modules/network_data/repository" +) - // DeleteByIds 批量删除信息 - DeleteByIds(ids []string) (int64, error) +// 实例化数据层 Alarm 结构体 +var NewAlarm = &Alarm{ + alarmRepository: repository.NewAlarm, +} + +// Alarm 告警 服务层处理 +type Alarm struct { + alarmRepository *repository.Alarm // 告警数据信息 +} + +// SelectPage 根据条件分页查询 +func (r *Alarm) SelectPage(querys model.AlarmQuery) map[string]any { + return r.alarmRepository.SelectPage(querys) +} + +// DeleteByIds 批量删除信息 +func (r *Alarm) DeleteByIds(ids []string) (int64, error) { + // 检查是否存在 + data := r.alarmRepository.SelectByIds(ids) + if len(data) <= 0 { + return 0, fmt.Errorf("no data") + } + + if len(data) == len(ids) { + rows := r.alarmRepository.DeleteByIds(ids) + return rows, nil + } + // 删除信息失败! + return 0, fmt.Errorf("delete fail") } diff --git a/src/modules/network_data/service/alarm.impl.go b/src/modules/network_data/service/alarm.impl.go deleted file mode 100644 index e988ed13..00000000 --- a/src/modules/network_data/service/alarm.impl.go +++ /dev/null @@ -1,40 +0,0 @@ -package service - -import ( - "fmt" - - "be.ems/src/modules/network_data/model" - "be.ems/src/modules/network_data/repository" -) - -// 实例化数据层 AlarmImpl 结构体 -var NewAlarmImpl = &AlarmImpl{ - alarmRepository: repository.NewAlarmImpl, -} - -// AlarmImpl 告警 服务层处理 -type AlarmImpl struct { - // 告警数据信息 - alarmRepository repository.IAlarm -} - -// SelectPage 根据条件分页查询 -func (r *AlarmImpl) SelectPage(querys model.AlarmQuery) map[string]any { - return r.alarmRepository.SelectPage(querys) -} - -// DeleteByIds 批量删除信息 -func (r *AlarmImpl) DeleteByIds(ids []string) (int64, error) { - // 检查是否存在 - data := r.alarmRepository.SelectByIds(ids) - if len(data) <= 0 { - return 0, fmt.Errorf("no data") - } - - if len(data) == len(ids) { - rows := r.alarmRepository.DeleteByIds(ids) - return rows, nil - } - // 删除信息失败! - return 0, fmt.Errorf("delete fail") -} diff --git a/src/modules/network_data/service/cdr_event_ims.go b/src/modules/network_data/service/cdr_event_ims.go index 2f5be53e..922fb024 100644 --- a/src/modules/network_data/service/cdr_event_ims.go +++ b/src/modules/network_data/service/cdr_event_ims.go @@ -1,12 +1,39 @@ package service -import "be.ems/src/modules/network_data/model" +import ( + "fmt" -// CDR会话事件IMS 服务层接口 -type ICDREventIMS interface { - // SelectPage 根据条件分页查询 - SelectPage(querys model.CDREventIMSQuery) map[string]any + "be.ems/src/modules/network_data/model" + "be.ems/src/modules/network_data/repository" +) - // DeleteByIds 批量删除信息 - DeleteByIds(cdrIds []string) (int64, error) +// 实例化数据层 CDREventIMS 结构体 +var NewCDREventIMS = &CDREventIMS{ + cdrEventIMSRepository: repository.NewCDREventIMS, +} + +// CDREventImpl CDR会话事件IMS 服务层处理 +type CDREventIMS struct { + cdrEventIMSRepository *repository.CDREventIMS // CDR会话事件数据信息 +} + +// SelectPage 根据条件分页查询 +func (r *CDREventIMS) SelectPage(querys model.CDREventIMSQuery) map[string]any { + return r.cdrEventIMSRepository.SelectPage(querys) +} + +// DeleteByIds 批量删除信息 +func (r *CDREventIMS) DeleteByIds(cdrIds []string) (int64, error) { + // 检查是否存在 + ids := r.cdrEventIMSRepository.SelectByIds(cdrIds) + if len(ids) <= 0 { + return 0, fmt.Errorf("not data") + } + + if len(ids) == len(cdrIds) { + rows := r.cdrEventIMSRepository.DeleteByIds(cdrIds) + return rows, nil + } + // 删除信息失败! + return 0, fmt.Errorf("delete fail") } diff --git a/src/modules/network_data/service/cdr_event_ims.impl.go b/src/modules/network_data/service/cdr_event_ims.impl.go deleted file mode 100644 index 15dfbd81..00000000 --- a/src/modules/network_data/service/cdr_event_ims.impl.go +++ /dev/null @@ -1,40 +0,0 @@ -package service - -import ( - "fmt" - - "be.ems/src/modules/network_data/model" - "be.ems/src/modules/network_data/repository" -) - -// 实例化数据层 NewCDREventIMSImpl 结构体 -var NewCDREventIMSImpl = &CDREventIMSImpl{ - cdrEventIMSRepository: repository.NewCDREventIMSImpl, -} - -// CDREventImpl CDR会话事件IMS 服务层处理 -type CDREventIMSImpl struct { - // CDR会话事件数据信息 - cdrEventIMSRepository repository.ICDREventIMS -} - -// SelectPage 根据条件分页查询 -func (r *CDREventIMSImpl) SelectPage(querys model.CDREventIMSQuery) map[string]any { - return r.cdrEventIMSRepository.SelectPage(querys) -} - -// DeleteByIds 批量删除信息 -func (r *CDREventIMSImpl) DeleteByIds(cdrIds []string) (int64, error) { - // 检查是否存在 - ids := r.cdrEventIMSRepository.SelectByIds(cdrIds) - if len(ids) <= 0 { - return 0, fmt.Errorf("not data") - } - - if len(ids) == len(cdrIds) { - rows := r.cdrEventIMSRepository.DeleteByIds(cdrIds) - return rows, nil - } - // 删除信息失败! - return 0, fmt.Errorf("delete fail") -} diff --git a/src/modules/network_data/service/cdr_event_smf.go b/src/modules/network_data/service/cdr_event_smf.go index 1e55a690..abd30a85 100644 --- a/src/modules/network_data/service/cdr_event_smf.go +++ b/src/modules/network_data/service/cdr_event_smf.go @@ -1,12 +1,39 @@ package service -import "be.ems/src/modules/network_data/model" +import ( + "fmt" -// CDR会话事件SMF 服务层接口 -type ICDREventSMF interface { - // SelectPage 根据条件分页查询 - SelectPage(querys model.CDREventSMFQuery) map[string]any + "be.ems/src/modules/network_data/model" + "be.ems/src/modules/network_data/repository" +) - // DeleteByIds 批量删除信息 - DeleteByIds(cdrIds []string) (int64, error) +// 实例化数据层 CDREventSMF 结构体 +var NewCDREventSMF = &CDREventSMF{ + cdrEventRepository: repository.NewCDREventSMF, +} + +// CDREventSMF CDR会话事件SMF 服务层处理 +type CDREventSMF struct { + cdrEventRepository *repository.CDREventSMF // CDR会话事件数据信息 +} + +// SelectPage 根据条件分页查询 +func (r *CDREventSMF) SelectPage(querys model.CDREventSMFQuery) map[string]any { + return r.cdrEventRepository.SelectPage(querys) +} + +// DeleteByIds 批量删除信息 +func (r *CDREventSMF) DeleteByIds(cdrIds []string) (int64, error) { + // 检查是否存在 + ids := r.cdrEventRepository.SelectByIds(cdrIds) + if len(ids) <= 0 { + return 0, fmt.Errorf("not data") + } + + if len(ids) == len(cdrIds) { + rows := r.cdrEventRepository.DeleteByIds(cdrIds) + return rows, nil + } + // 删除信息失败! + return 0, fmt.Errorf("delete fail") } diff --git a/src/modules/network_data/service/cdr_event_smf.impl.go b/src/modules/network_data/service/cdr_event_smsc.go similarity index 51% rename from src/modules/network_data/service/cdr_event_smf.impl.go rename to src/modules/network_data/service/cdr_event_smsc.go index 09d2e48a..39448874 100644 --- a/src/modules/network_data/service/cdr_event_smf.impl.go +++ b/src/modules/network_data/service/cdr_event_smsc.go @@ -7,21 +7,23 @@ import ( "be.ems/src/modules/network_data/repository" ) -var NewCDREventSMFImpl = &CDREventSMFImpl{ - cdrEventRepository: repository.NewCDREventSMFImpl, +// 实例化数据层 CDREventSMSC 结构体 +var NewCDREventSMSC = &CDREventSMSC{ + cdrEventRepository: repository.NewCDREventSMSC, } -type CDREventSMFImpl struct { - // CDR会话事件数据信息 - cdrEventRepository repository.ICDREventSMF +// CDREventSMSC CDR会话事件SMSC 服务层处理 +type CDREventSMSC struct { + cdrEventRepository *repository.CDREventSMSC // CDR会话事件数据信息 } -func (r *CDREventSMFImpl) SelectPage(querys model.CDREventSMFQuery) map[string]any { +// SelectPage 根据条件分页查询 +func (r *CDREventSMSC) SelectPage(querys model.CDREventSMSCQuery) map[string]any { return r.cdrEventRepository.SelectPage(querys) } // DeleteByIds 批量删除信息 -func (r *CDREventSMFImpl) DeleteByIds(cdrIds []string) (int64, error) { +func (r *CDREventSMSC) DeleteByIds(cdrIds []string) (int64, error) { // 检查是否存在 ids := r.cdrEventRepository.SelectByIds(cdrIds) if len(ids) <= 0 { diff --git a/src/modules/network_data/service/perf_kpi.go b/src/modules/network_data/service/perf_kpi.go index fcd7d768..b4aef0c4 100644 --- a/src/modules/network_data/service/perf_kpi.go +++ b/src/modules/network_data/service/perf_kpi.go @@ -1,15 +1,79 @@ package service -import "be.ems/src/modules/network_data/model" +import ( + "encoding/json" + "fmt" + "time" -// 性能统计 服务层接口 -type IPerfKPI interface { - // SelectGoldKPI 通过网元指标数据信息 - SelectGoldKPI(query model.GoldKPIQuery) []map[string]any + "be.ems/src/framework/constants/cachekey" + "be.ems/src/framework/redis" + "be.ems/src/modules/network_data/model" + "be.ems/src/modules/network_data/repository" +) - // SelectGoldKPITitle 网元对应的指标名称 - SelectGoldKPITitle(neType string) []model.GoldKPITitle - - // SelectUPFTotalFlow 查询UPF总流量 N3上行 N6下行 - SelectUPFTotalFlow(neType, rmUID string, day int) map[string]any +// 实例化数据层 PerfKPI 结构体 +var NewPerfKPI = &PerfKPI{ + perfKPIRepository: repository.NewPerfKPI, +} + +// PerfKPI 性能统计 服务层处理 +type PerfKPI struct { + perfKPIRepository *repository.PerfKPI // 性能统计数据信息 +} + +// SelectGoldKPI 通过网元指标数据信息 +func (r *PerfKPI) SelectGoldKPI(query model.GoldKPIQuery) []map[string]any { + // 获取数据指标id + var kpiIds []string + kpiTitles := r.perfKPIRepository.SelectGoldKPITitle(query.NeType) + for _, kpiId := range kpiTitles { + kpiIds = append(kpiIds, kpiId.KPIID) + } + + data := r.perfKPIRepository.SelectGoldKPI(query, kpiIds) + if data == nil { + return []map[string]any{} + } + return data +} + +// SelectGoldKPITitle 网元对应的指标名称 +func (r *PerfKPI) SelectGoldKPITitle(neType string) []model.GoldKPITitle { + return r.perfKPIRepository.SelectGoldKPITitle(neType) +} + +// SelectUPFTotalFlow 查询UPF总流量 N3上行 N6下行 +func (r *PerfKPI) SelectUPFTotalFlow(neType, rmUID string, day int) map[string]any { + now := time.Now() + // 获取当前日期 + endDate := fmt.Sprint(now.UnixMilli()) + // 将当前日期前几天数 + startDate := fmt.Sprint(now.AddDate(0, 0, -day).Truncate(24 * time.Hour).UnixMilli()) + + var info map[string]any + + // 读取缓存数据 小于2分钟重新缓存 + key := fmt.Sprintf("%sUPF:totalFlow:%s_%d", cachekey.NE_DATA_KEY, rmUID, day) + infoStr, _ := redis.Get("", key) + if infoStr != "" { + json.Unmarshal([]byte(infoStr), &info) + expireSecond, _ := redis.GetExpire("", key) + if expireSecond > 120 { + return info + } + } + // down * 8 / 1000 / 1000 单位M + info = r.perfKPIRepository.SelectUPFTotalFlow(neType, rmUID, startDate, endDate) + if v, ok := info["up"]; ok && v == nil { + info["up"] = 0 + } + if v, ok := info["down"]; ok && v == nil { + info["down"] = 0 + } + + // 保存到缓存 + infoJSON, _ := json.Marshal(info) + redis.SetByExpire("", key, string(infoJSON), time.Duration(10)*time.Minute) + + return info } diff --git a/src/modules/network_data/service/perf_kpi.impl.go b/src/modules/network_data/service/perf_kpi.impl.go deleted file mode 100644 index a7174faf..00000000 --- a/src/modules/network_data/service/perf_kpi.impl.go +++ /dev/null @@ -1,80 +0,0 @@ -package service - -import ( - "encoding/json" - "fmt" - "time" - - "be.ems/src/framework/constants/cachekey" - "be.ems/src/framework/redis" - "be.ems/src/modules/network_data/model" - "be.ems/src/modules/network_data/repository" -) - -// 实例化数据层 PerfKPIImpl 结构体 -var NewPerfKPIImpl = &PerfKPIImpl{ - perfKPIRepository: repository.NewPerfKPIImpl, -} - -// PerfKPIImpl 性能统计 服务层处理 -type PerfKPIImpl struct { - // 性能统计数据信息 - perfKPIRepository repository.IPerfKPI -} - -// SelectGoldKPI 通过网元指标数据信息 -func (r *PerfKPIImpl) SelectGoldKPI(query model.GoldKPIQuery) []map[string]any { - // 获取数据指标id - var kpiIds []string - kpiTitles := r.perfKPIRepository.SelectGoldKPITitle(query.NeType) - for _, kpiId := range kpiTitles { - kpiIds = append(kpiIds, kpiId.KPIID) - } - - data := r.perfKPIRepository.SelectGoldKPI(query, kpiIds) - if data == nil { - return []map[string]any{} - } - return data -} - -// SelectGoldKPITitle 网元对应的指标名称 -func (r *PerfKPIImpl) SelectGoldKPITitle(neType string) []model.GoldKPITitle { - return r.perfKPIRepository.SelectGoldKPITitle(neType) -} - -// SelectUPFTotalFlow 查询UPF总流量 N3上行 N6下行 -func (r *PerfKPIImpl) SelectUPFTotalFlow(neType, rmUID string, day int) map[string]any { - now := time.Now() - // 获取当前日期 - endDate := fmt.Sprint(now.UnixMilli()) - // 将当前日期前几天数 - startDate := fmt.Sprint(now.AddDate(0, 0, -day).Truncate(24 * time.Hour).UnixMilli()) - - var info map[string]any - - // 读取缓存数据 小于2分钟重新缓存 - key := fmt.Sprintf("%sUPF:totalFlow:%s_%d", cachekey.NE_DATA_KEY, rmUID, day) - infoStr, _ := redis.Get("", key) - if infoStr != "" { - json.Unmarshal([]byte(infoStr), &info) - expireSecond, _ := redis.GetExpire("", key) - if expireSecond > 120 { - return info - } - } - - info = r.perfKPIRepository.SelectUPFTotalFlow(neType, rmUID, startDate, endDate) - if v, ok := info["up"]; ok && v == nil { - info["up"] = 0 - } - if v, ok := info["down"]; ok && v == nil { - info["down"] = 0 - } - - // 保存到缓存 - infoJSON, _ := json.Marshal(info) - redis.SetByExpire("", key, string(infoJSON), time.Duration(10)*time.Minute) - - return info -} diff --git a/src/modules/network_data/service/udm_auth.go b/src/modules/network_data/service/udm_auth.go index 6e2b14e1..807b8be2 100644 --- a/src/modules/network_data/service/udm_auth.go +++ b/src/modules/network_data/service/udm_auth.go @@ -1,28 +1,204 @@ package service -import "be.ems/src/modules/network_data/model" +import ( + "fmt" + "strconv" + "strings" -// UDM鉴权信息 服务层接口 -type IUDMAuth interface { - // ResetData 重置鉴权用户数据,清空数据库重新同步Redis数据 - ResetData(neId string) int64 + "be.ems/src/framework/redis" + "be.ems/src/modules/network_data/model" + "be.ems/src/modules/network_data/repository" + neService "be.ems/src/modules/network_element/service" +) - // SelectPage 分页查询数据库 - SelectPage(query map[string]any) map[string]any - - // SelectList 查询数据库 - SelectList(u model.UDMAuth) []model.UDMAuth - - // Insert 从数据中读取后删除imsi再存入数据库 - // imsi长度15,ki长度32,opc长度0或者32 - Insert(neId string, u model.UDMAuth) int64 - - // InsertData 导入文件数据 dataType目前两种:txt/csv - InsertData(neId, dataType string, data any) int64 - - // Delete 删除单个不重新加载 - Delete(neID, imsi string) int64 - - // LoadData 删除范围后重新加载 num表示imsi后几位 - LoadData(neID, imsi, num string) int64 +// 实例化服务层 UDMAuthUser 结构体 +var NewUDMAuthUser = &UDMAuthUser{ + udmAuthRepository: repository.NewUDMAuthUser, +} + +// UDM鉴权信息 服务层处理 +type UDMAuthUser struct { + // UDM鉴权信息数据信息 + udmAuthRepository *repository.UDMAuthUser +} + +// dataByRedis UDM鉴权用户 db:0 中 ausf:* +func (r *UDMAuthUser) dataByRedis(imsi, neId string) []model.UDMAuthUser { + arr := []model.UDMAuthUser{} + key := fmt.Sprintf("ausf:%s", imsi) + source := fmt.Sprintf("UDM_%s", neId) + + // 网元主机的Redis客户端 + redisClient, err := neService.NewNeInfo.NeRunRedisClient("UDM", neId) + if err != nil { + return arr + } + defer func() { + redisClient.Close() + redis.ConnectPush(source, nil) + }() + redis.ConnectPush(source, redisClient.Client) + + ausfArr, err := redis.GetKeys(source, key) + if err != nil { + return arr + } + mkv, err := redis.GetHashBatch(source, ausfArr) + if err != nil { + return arr + } + + for k, m := range mkv { + if k == "-" { + continue + } + + // 跳过-号数据 ausf:360000100000130 + imsi := k[5:] + if strings.Contains(imsi, "-") { + continue + } + + amf := "" + if v, ok := m["amf"]; ok { + amf = strings.Replace(v, "\r\n", "", 1) + } + a := model.UDMAuthUser{ + IMSI: imsi, + Amf: amf, + Status: "1", // 默认给1 + Ki: m["ki"], + AlgoIndex: m["algo"], + Opc: m["opc"], + NeId: neId, + } + arr = append(arr, a) + } + return arr +} + +// ResetData 重置鉴权用户数据,清空数据库重新同步Redis数据 +func (r *UDMAuthUser) ResetData(neId string) int64 { + authArr := r.dataByRedis("*", neId) + // 数据清空后添加 + go r.udmAuthRepository.ClearAndInsert(neId, authArr) + return int64(len(authArr)) +} + +// ParseInfo 解析单个用户imsi鉴权信息 data从命令MML得到的结果 +func (r *UDMAuthUser) ParseInfo(imsi, neId string, data map[string]string) model.UDMAuthUser { + u := r.udmAuthRepository.SelectByIMSIAndNeID(imsi, neId) + + // 用于更新 + u.IMSI = imsi + u.NeId = neId + u.Amf = data["amf"] + u.Ki = data["ki"] + u.AlgoIndex = data["algo"] + u.Opc = data["opc"] + u.Status = "1" + return u +} + +// SelectPage 分页查询数据库 +func (r *UDMAuthUser) SelectPage(query map[string]any) (int64, []model.UDMAuthUser) { + return r.udmAuthRepository.SelectPage(query) +} + +// SelectList 查询数据库 +func (r *UDMAuthUser) SelectList(u model.UDMAuthUser) []model.UDMAuthUser { + return r.udmAuthRepository.SelectList(u) +} + +// Insert 从数据中读取后删除imsi再存入数据库 +// imsi长度15,ki长度32,opc长度0或者32 +func (r *UDMAuthUser) Insert(neId string, u model.UDMAuthUser) int64 { + uArr := r.dataByRedis(u.IMSI, neId) + if len(uArr) > 0 { + r.udmAuthRepository.Delete(u.IMSI, neId) + return r.udmAuthRepository.Inserts(uArr) + } + return 0 +} + +// InsertData 导入文件数据 dataType目前两种:txt/csv +func (r *UDMAuthUser) InsertData(neId, dataType string, data any) int64 { + // imsi截取前缀,重新获取部分数据 + prefixes := make(map[string]struct{}) + + if dataType == "csv" { + for _, v := range data.([]map[string]string) { + imsi := v["imsi"] + if len(imsi) < 6 { + continue + } + prefix := imsi[:len(imsi)-4] + prefixes[prefix] = struct{}{} + } + } + if dataType == "txt" { + for _, v := range data.([][]string) { + imsi := v[0] + if len(imsi) < 6 { + continue + } + prefix := imsi[:len(imsi)-4] + prefixes[prefix] = struct{}{} + } + } + + // 根据前缀重新加载插入 + var num int64 = 0 + for prefix := range prefixes { + // 直接删除前缀的记录 + r.udmAuthRepository.DeletePrefixByIMSI(neId, prefix) + // keys ausf:4600001000004* + arr := r.dataByRedis(prefix+"*", neId) + if len(arr) > 0 { + num += r.udmAuthRepository.Inserts(arr) + } + } + return num +} + +// Delete 删除单个不重新加载 +func (r *UDMAuthUser) Delete(imsi, neId string) int64 { + return r.udmAuthRepository.Delete(imsi, neId) +} + +// LoadData 重新加载从imsi开始num的数据 +func (r *UDMAuthUser) LoadData(neId, imsi, num string) { + startIMSI, _ := strconv.ParseInt(imsi, 10, 64) + subNum, _ := strconv.ParseInt(num, 10, 64) + var i int64 + for i = 0; i < subNum; i++ { + keyIMSI := fmt.Sprintf("%015d", startIMSI+i) + // 删除原数据 + r.udmAuthRepository.Delete(keyIMSI, neId) + // 加载数据 + arr := r.dataByRedis(keyIMSI, neId) + if len(arr) < 1 { + continue + } + r.udmAuthRepository.Inserts(arr) + } +} + +// ParseCommandParams 解析数据组成命令参数 ki=xx,xx=xx,... +func (r *UDMAuthUser) ParseCommandParams(item model.UDMAuthUser) string { + var conditions []string + if item.Ki != "" { + conditions = append(conditions, fmt.Sprintf("ki=%s", item.Ki)) + } + + if item.Amf != "" { + conditions = append(conditions, fmt.Sprintf("amf=%s", item.Amf)) + } + if item.AlgoIndex != "" { + conditions = append(conditions, fmt.Sprintf("algo=%s", item.AlgoIndex)) + } + if item.Opc != "" { + conditions = append(conditions, fmt.Sprintf("opc=%s", item.Opc)) + } + return strings.Join(conditions, ",") } diff --git a/src/modules/network_data/service/udm_auth.impl.go b/src/modules/network_data/service/udm_auth.impl.go deleted file mode 100644 index d7998a10..00000000 --- a/src/modules/network_data/service/udm_auth.impl.go +++ /dev/null @@ -1,146 +0,0 @@ -package service - -import ( - "fmt" - "strings" - - "be.ems/src/framework/redis" - "be.ems/src/modules/network_data/model" - "be.ems/src/modules/network_data/repository" -) - -// 实例化服务层 UDMAuthImpl 结构体 -var NewUDMAuthImpl = &UDMAuthImpl{ - udmAuthRepository: repository.NewUDMAuthImpl, -} - -// UDM鉴权信息 服务层处理 -type UDMAuthImpl struct { - // UDM鉴权信息数据信息 - udmAuthRepository repository.IUDMAuth -} - -// dataByRedis UDM鉴权用户 db:0 中 ausf:* -func (r *UDMAuthImpl) dataByRedis(imsi, neId string) []model.UDMAuth { - arr := []model.UDMAuth{} - key := fmt.Sprintf("ausf:%s", imsi) - ausfArr, err := redis.GetKeys("udmuser", key) - if err != nil { - return arr - } - for _, key := range ausfArr { - m, err := redis.GetHash("udmuser", key) - if err != nil { - continue - } - - // 跳过-号数据 - imsi := key[5:] - if strings.Contains(imsi, "-") { - continue - } - - amf := "" - if v, ok := m["amf"]; ok { - amf = strings.Replace(v, "\r\n", "", 1) - } - a := model.UDMAuth{ - IMSI: imsi, - Amf: amf, - Status: "1", // 默认给1 - Ki: m["ki"], - AlgoIndex: m["algo"], - Opc: m["opc"], - NeId: neId, - } - arr = append(arr, a) - } - return arr -} - -// ResetData 重置鉴权用户数据,清空数据库重新同步Redis数据 -func (r *UDMAuthImpl) ResetData(neId string) int64 { - authArr := r.dataByRedis("*", neId) - // 数据清空后添加 - go r.udmAuthRepository.ClearAndInsert(neId, authArr) - return int64(len(authArr)) -} - -// SelectPage 分页查询数据库 -func (r *UDMAuthImpl) SelectPage(query map[string]any) map[string]any { - return r.udmAuthRepository.SelectPage(query) -} - -// SelectList 查询数据库 -func (r *UDMAuthImpl) SelectList(u model.UDMAuth) []model.UDMAuth { - return r.udmAuthRepository.SelectList(u) -} - -// Insert 从数据中读取后删除imsi再存入数据库 -// imsi长度15,ki长度32,opc长度0或者32 -func (r *UDMAuthImpl) Insert(neId string, u model.UDMAuth) int64 { - uArr := r.dataByRedis(u.IMSI, neId) - if len(uArr) > 0 { - r.udmAuthRepository.Delete(neId, u.IMSI) - return r.udmAuthRepository.Inserts(uArr) - } - return 0 -} - -// InsertData 导入文件数据 dataType目前两种:txt/csv -func (r *UDMAuthImpl) InsertData(neId, dataType string, data any) int64 { - // imsi截取前缀,重新获取部分数据 - prefixes := make(map[string]struct{}) - - if dataType == "csv" { - for _, v := range data.([]map[string]string) { - imsi := v["imsi"] - if len(imsi) < 6 { - continue - } - prefix := imsi[:len(imsi)-4] - prefixes[prefix] = struct{}{} - } - } - if dataType == "txt" { - for _, v := range data.([][]string) { - imsi := v[0] - if len(imsi) < 6 { - continue - } - prefix := imsi[:len(imsi)-4] - prefixes[prefix] = struct{}{} - } - } - - // 根据前缀重新加载插入 - var num int64 = 0 - for prefix := range prefixes { - // 直接删除前缀的记录 - r.udmAuthRepository.DeletePrefixByIMSI(neId, prefix) - // keys ausf:4600001000004* - authArr := r.dataByRedis(prefix+"*", neId) - if len(authArr) > 0 { - num += r.udmAuthRepository.Inserts(authArr) - } - } - return num -} - -// Delete 删除单个不重新加载 -func (r *UDMAuthImpl) Delete(neId, imsi string) int64 { - return r.udmAuthRepository.Delete(neId, imsi) -} - -// LoadData 删除范围后重新加载 num表示imsi后几位 -func (r *UDMAuthImpl) LoadData(neId, imsi, num string) int64 { - prefix := imsi[:len(imsi)-len(num)-1] - // 直接删除前缀的记录 - delNum := r.udmAuthRepository.DeletePrefixByIMSI(neId, prefix) - // keys ausf:4600001000004* - authArr := r.dataByRedis(prefix+"*", neId) - if len(authArr) > 0 { - return r.udmAuthRepository.Inserts(authArr) - } - return delNum -} diff --git a/src/modules/network_data/service/udm_sub.go b/src/modules/network_data/service/udm_sub.go index cb03afe7..7288c108 100644 --- a/src/modules/network_data/service/udm_sub.go +++ b/src/modules/network_data/service/udm_sub.go @@ -1,28 +1,365 @@ package service -import "be.ems/src/modules/network_data/model" +import ( + "fmt" + "strconv" + "strings" -// UDM签约用户信息 服务层接口 -type IUDMSub interface { - // ResetData 重置鉴权用户数据,清空数据库重新同步Redis数据 - ResetData(neId string) int64 + "be.ems/src/framework/redis" + "be.ems/src/modules/network_data/model" + "be.ems/src/modules/network_data/repository" + neService "be.ems/src/modules/network_element/service" +) - // SelectPage 分页查询数据库 - SelectPage(query map[string]any) map[string]any - - // SelectList 查询数据库 - SelectList(u model.UDMSub) []model.UDMSub - - // Insert 从数据中读取后删除imsi再存入数据库 - // imsi长度15,ki长度32,opc长度0或者32 - Insert(neId string, u model.UDMSub) int64 - - // InsertData 导入文件数据 dataType目前两种:txt/csv - InsertData(neId, dataType string, data any) int64 - - // Delete 删除单个不重新加载 - Delete(neId, imsi string) int64 - - // LoadData 删除范围后重新加载 num表示imsi后几位 - LoadData(neId, imsi, num string) int64 +// 实例化服务层 UDMSubUser 结构体 +var NewUDMSubUser = &UDMSubUser{ + udmSubRepository: repository.NewUDMSub, + udmUserInfoRepository: repository.NewUDMUserInfo, +} + +// UDM签约信息 服务层处理 +type UDMSubUser struct { + udmSubRepository *repository.UDMSubUser // UDM签约信息数据信息 + udmUserInfoRepository *repository.UDMUserInfo // UDM用户IMSI信息数据信息 +} + +// dataByRedis UDM签约用户 db:0 中 udm-sd:* +func (r *UDMSubUser) dataByRedis(imsi, neId string) []model.UDMSubUser { + arr := []model.UDMSubUser{} + key := fmt.Sprintf("udm-sd:%s", imsi) + source := fmt.Sprintf("UDM_%s", neId) + + // 网元主机的Redis客户端 + redisClient, err := neService.NewNeInfo.NeRunRedisClient("UDM", neId) + if err != nil { + return arr + } + defer func() { + redisClient.Close() + redis.ConnectPush(source, nil) + }() + redis.ConnectPush(source, redisClient.Client) + + udmsdArr, err := redis.GetKeys(source, key) + if err != nil { + return arr + } + mkv, err := redis.GetHashBatch(source, udmsdArr) + if err != nil { + return arr + } + + for k, m := range mkv { + if k == "-" { + continue + } + + // 跳过-号数据 udm-sd:360000100000130 + imsi := k[7:] + if strings.Contains(imsi, "-") { + continue + } + + a := model.UDMSubUser{ + IMSI: imsi, // udm-sd:360000100000130 + MSISDN: m["gpsi"], // 8612300000130 + NeId: neId, + SmfSel: m["smf-sel"], // def_snssai + SmData: m["sm-dat"], // 1-000001&cmnet&ims&3gnet + Cag: m["cag"], // def_cag + } + + // def_ambr,def_nssai,0,def_arfb,def_sar,3,1,12000,1,1000,0,1,- + if v, ok := m["am-dat"]; ok { + arr := strings.Split(v, ",") + a.AmDat = v + a.UeAmbrTpl = arr[0] + a.NssaiTpl = arr[1] + a.RatRestrictions = arr[2] + a.AreaForbiddenTpl = arr[3] + a.ServiceAreaRestrictionTpl = arr[4] + a.CnTypeRestrictions = arr[5] + a.RfspIndex = arr[6] + a.SubsRegTime = arr[7] + a.UeUsageType = arr[8] + a.ActiveTime = arr[9] + a.MicoAllowed = "0" // arr[10] + a.OdbPs = "1" // arr[11] + a.GroupId = "-" // arr[12] + if len(arr) > 10 { + a.MicoAllowed = arr[10] + } + if len(arr) > 11 { + a.OdbPs = arr[11] + } + if len(arr) > 12 && arr[12] != "-" { + a.GroupId = arr[12] + } + } + // 1,64,24,65,def_eps,1,2,010200000000,- + if v, ok := m["eps-dat"]; ok { + arr := strings.Split(v, ",") + // 跳过非常规数据 + if len(arr) > 9 { + continue + } + a.EpsDat = v + a.EpsFlag = arr[0] + a.EpsOdb = arr[1] + a.HplmnOdb = arr[2] + a.Ard = arr[3] + a.Epstpl = arr[4] + a.ContextId = arr[5] + a.ApnNum = arr[6] // 导入和导出不用 + a.ApnContext = arr[7] + if len(arr) >= 9 { + a.StaticIp = arr[8] + } + } + + arr = append(arr, a) + } + return arr +} + +// ResetData 重置鉴权用户数据,清空数据库重新同步Redis数据 +func (r *UDMSubUser) ResetData(neId string) int64 { + subArr := r.dataByRedis("*", neId) + // 数据清空后添加 + go r.udmSubRepository.ClearAndInsert(neId, subArr) + return int64(len(subArr)) +} + +// ParseInfo 解析单个用户imsi签约信息 data从命令MML得到的结果 +func (r *UDMSubUser) ParseInfo(imsi, neId string, data map[string]string) model.UDMSubUser { + u := r.udmSubRepository.SelectByIMSIAndNeID(imsi, neId) + + cnType, _ := strconv.ParseInt(data["CNType"][:4], 0, 64) // 0x03(EPC|5GC) + rat, _ := strconv.ParseInt(data["RAT"][:4], 0, 64) // 0x00(VIRTUAL|WLAN|EUTRA|NR) + msisdn := data["MSISDN"] + if imsMsisdnLen := strings.Index(msisdn, ","); imsMsisdnLen != -1 { + msisdn = msisdn[:imsMsisdnLen] + } + + // 用于更新 + u.IMSI = imsi + u.MSISDN = msisdn + u.NeId = neId + u.UeAmbrTpl = data["AMBR"] + u.NssaiTpl = data["NSSAI"] + u.AreaForbiddenTpl = data["AreaForbidden"] + u.ServiceAreaRestrictionTpl = data["ServiceAreaRestriction"] + u.CnTypeRestrictions = fmt.Sprint(cnType) + u.RatRestrictions = fmt.Sprint(rat) + u.MicoAllowed = data["MICO"] + u.SmData = data["SM-Data(snssai+dnn[1..n])"] + u.SmfSel = data["Smf-Selection"] + u.Cag = data["cag"] + + // 1,64,24,65,def_eps,1,2,010200000000,- + if v, ok := data["EPS-Data"]; ok { + u.EpsDat = v + arr := strings.Split(v, ",") + u.EpsFlag = arr[0] + u.EpsOdb = arr[1] + u.HplmnOdb = arr[2] + u.Ard = arr[3] + u.Epstpl = arr[4] + u.ContextId = arr[5] + u.ApnNum = arr[6] // 导入和导出不用 + u.ApnContext = arr[7] + u.StaticIp = arr[8] + } + + // 补充用户拓展信息 + info := r.udmUserInfoRepository.SelectByIMSIAndNeID(imsi, neId) + if info.IMSI == imsi { + u.Remark = info.Remark + } + return u +} + +// SelectPage 分页查询数据库 +func (r *UDMSubUser) SelectPage(query map[string]any) (int64, []model.UDMSubUser) { + return r.udmSubRepository.SelectPage(query) +} + +// SelectList 查询数据库 +func (r *UDMSubUser) SelectList(u model.UDMSubUser) []model.UDMSubUser { + return r.udmSubRepository.SelectList(u) +} + +// Insert 从数据中读取后删除imsi再存入数据库 +// imsi长度15,ki长度32,opc长度0或者32 +func (r *UDMSubUser) Insert(neId string, u model.UDMSubUser) int64 { + uArr := r.dataByRedis(u.IMSI, neId) + if len(uArr) > 0 { + r.udmSubRepository.Delete(u.IMSI, neId) + // 新增到拓展信息 + if u.Remark != "" { + r.udmUserInfoRepository.Delete(u.IMSI, neId) + r.udmUserInfoRepository.Inserts([]model.UDMUserInfo{{ + IMSI: u.IMSI, + MSISDN: u.MSISDN, + NeId: u.NeId, + Remark: u.Remark, + }}) + } + return r.udmSubRepository.Inserts(uArr) + } + return 0 +} + +// InsertData 导入文件数据 dataType目前两种:txt/csv +func (r *UDMSubUser) InsertData(neId, dataType string, data any) int64 { + // imsi截取前缀,重新获取部分数据 + prefixes := make(map[string]struct{}) + + if dataType == "csv" { + for _, v := range data.([]map[string]string) { + imsi := v["imsi"] + if len(imsi) < 6 { + continue + } + prefix := imsi[:len(imsi)-4] + prefixes[prefix] = struct{}{} + } + } + if dataType == "txt" { + for _, v := range data.([][]string) { + imsi := v[0] + if len(imsi) < 6 { + continue + } + prefix := imsi[:len(imsi)-4] + prefixes[prefix] = struct{}{} + } + } + + // 根据前缀重新加载插入 + var num int64 = 0 + for prefix := range prefixes { + // keys udm-sd:4600001000004* + arr := r.dataByRedis(prefix+"*", neId) + if len(arr) > 0 { + r.udmSubRepository.DeletePrefixByIMSI(prefix, neId) + num += r.udmSubRepository.Inserts(arr) + } + } + return num +} + +// Delete 删除单个不重新加载 +func (r *UDMSubUser) Delete(neId, imsi string) int64 { + // 删除拓展信息 + r.udmUserInfoRepository.Delete(imsi, neId) + return r.udmSubRepository.Delete(imsi, neId) +} + +// LoadData 重新加载从imsi开始num的数据 +// remark不为空,则新增到拓展信息,删除标记为-(Deleted)- +func (r *UDMSubUser) LoadData(neId, imsi, num, remark string) { + startIMSI, _ := strconv.ParseInt(imsi, 10, 64) + subNum, _ := strconv.ParseInt(num, 10, 64) + var i int64 + for i = 0; i < subNum; i++ { + keyIMSI := fmt.Sprintf("%015d", startIMSI+i) + // 删除原数据 + r.udmSubRepository.Delete(keyIMSI, neId) + if remark == "-(Deleted)-" { + r.udmUserInfoRepository.Delete(keyIMSI, neId) + } + // 加载数据 + arr := r.dataByRedis(keyIMSI, neId) + if len(arr) < 1 { + continue + } + r.udmSubRepository.Inserts(arr) + // 拓展信息 + if remark != "" { + uarr := make([]model.UDMUserInfo, 0, len(arr)) + for _, v := range arr { + uarr = append(uarr, model.UDMUserInfo{ + IMSI: v.IMSI, + MSISDN: v.MSISDN, + NeId: v.NeId, + Remark: remark, + }) + } + r.udmUserInfoRepository.Delete(keyIMSI, neId) + r.udmUserInfoRepository.Inserts(uarr) + } + } +} + +// ParseCommandParams 解析数据组成命令参数 msisdn=xx,xx=xx,... +func (r *UDMSubUser) ParseCommandParams(item model.UDMSubUser) string { + var conditions []string + if item.MSISDN != "" { + conditions = append(conditions, fmt.Sprintf("msisdn=%s", item.MSISDN)) + } + + // AmData + if item.UeAmbrTpl != "" { + conditions = append(conditions, fmt.Sprintf("ambr=%s", item.UeAmbrTpl)) + } + if item.NssaiTpl != "" { + conditions = append(conditions, fmt.Sprintf("nssai=%s", item.NssaiTpl)) + } + if item.AreaForbiddenTpl != "" { + conditions = append(conditions, fmt.Sprintf("arfb=%s", item.AreaForbiddenTpl)) + } + if item.ServiceAreaRestrictionTpl != "" { + conditions = append(conditions, fmt.Sprintf("sar=%s", item.ServiceAreaRestrictionTpl)) + } + if item.RatRestrictions != "" { + conditions = append(conditions, fmt.Sprintf("rat=%s", item.RatRestrictions)) + } + if item.CnTypeRestrictions != "" { + conditions = append(conditions, fmt.Sprintf("cn=%s", item.CnTypeRestrictions)) + } + if item.MicoAllowed != "" { + conditions = append(conditions, fmt.Sprintf("mico=%s", item.MicoAllowed)) + } + + // EpsDat + // if item.EpsDat != "" { + // conditions = append(conditions, fmt.Sprintf("eps_dat=%s", item.EpsDat)) + // } + if item.EpsFlag != "" { + conditions = append(conditions, fmt.Sprintf("eps_flag=%s", item.EpsFlag)) + } + if item.EpsOdb != "" { + conditions = append(conditions, fmt.Sprintf("eps_odb=%s", item.EpsOdb)) + } + if item.HplmnOdb != "" { + conditions = append(conditions, fmt.Sprintf("hplmn_odb=%s", item.HplmnOdb)) + } + if item.Epstpl != "" { + conditions = append(conditions, fmt.Sprintf("epstpl=%s", item.Epstpl)) + } + if item.Ard != "" { + conditions = append(conditions, fmt.Sprintf("ard=%s", item.Ard)) + } + if item.ContextId != "" { + conditions = append(conditions, fmt.Sprintf("context_id=%s", item.ContextId)) + } + if item.ApnContext != "" { + conditions = append(conditions, fmt.Sprintf("apn_context=%s", item.ApnContext)) + } + if item.StaticIp != "" { + conditions = append(conditions, fmt.Sprintf("static_ip=%s", item.StaticIp)) + } + + // 其他 + if item.SmfSel != "" { + conditions = append(conditions, fmt.Sprintf("smf_sel=%s", item.SmfSel)) + } + if item.SmData != "" { + conditions = append(conditions, fmt.Sprintf("sm_data=%s", item.SmData)) + } + if item.Cag != "" { + conditions = append(conditions, fmt.Sprintf("cag=%s", item.Cag)) + } + return strings.Join(conditions, ",") } diff --git a/src/modules/network_data/service/udm_sub.impl.go b/src/modules/network_data/service/udm_sub.impl.go deleted file mode 100644 index 768e48a8..00000000 --- a/src/modules/network_data/service/udm_sub.impl.go +++ /dev/null @@ -1,164 +0,0 @@ -package service - -import ( - "fmt" - "strings" - - "be.ems/src/framework/redis" - "be.ems/src/modules/network_data/model" - "be.ems/src/modules/network_data/repository" -) - -// 实例化服务层 UDMSubImpl 结构体 -var NewUDMSubImpl = &UDMSubImpl{ - udmSubRepository: repository.NewUDMSubImpl, -} - -// UDM签约信息 服务层处理 -type UDMSubImpl struct { - // UDM签约信息数据信息 - udmSubRepository repository.IUDMSub -} - -// dataByRedis UDM签约用户 db:0 中 udm-sd:* -func (r *UDMSubImpl) dataByRedis(imsi, neId string) []model.UDMSub { - arr := []model.UDMSub{} - key := fmt.Sprintf("udm-sd:%s", imsi) - udmsdArr, err := redis.GetKeys("udmuser", key) - if err != nil { - return arr - } - for _, key := range udmsdArr { - m, err := redis.GetHash("udmuser", key) - if err != nil { - continue - } - - a := model.UDMSub{ - IMSI: key[7:], - Msisdn: m["gpsi"], // 46003550072 - SmfSel: m["smf-sel"], - SmData: m["sm-dat"], // 1-000001&cmnet&ims&3gnet - NeId: neId, - } - - // def_ambr,def_nssai,0,def_arfb,def_sar,3,1,12000,1,1000,0,1,- - if v, ok := m["am-dat"]; ok { - arr := strings.Split(v, ",") - a.Ambr = arr[0] - a.Nssai = arr[1] - a.Rat = arr[2] - a.Arfb = arr[3] - a.Sar = arr[4] - a.Cn = arr[5] - } - // 1,64,24,65,def_eps,1,2,010200000000,- - if v, ok := m["eps-dat"]; ok { - arr := strings.Split(v, ",") - // 跳过非常规数据 - if len(arr) > 9 { - continue - } - a.EpsDat = v - a.EpsFlag = arr[0] - a.EpsOdb = arr[1] - a.HplmnOdb = arr[2] - a.Ard = arr[3] - a.Epstpl = arr[4] - a.ContextId = arr[5] - a.ApnContext = arr[7] - // [6] 是不要的,导入和导出不用 - a.StaticIp = arr[8] - } - - arr = append(arr, a) - } - return arr -} - -// ResetData 重置鉴权用户数据,清空数据库重新同步Redis数据 -func (r *UDMSubImpl) ResetData(neId string) int64 { - subArr := r.dataByRedis("*", neId) - // 数据清空后添加 - go r.udmSubRepository.ClearAndInsert(neId, subArr) - return int64(len(subArr)) -} - -// SelectPage 分页查询数据库 -func (r *UDMSubImpl) SelectPage(query map[string]any) map[string]any { - return r.udmSubRepository.SelectPage(query) -} - -// SelectList 查询数据库 -func (r *UDMSubImpl) SelectList(u model.UDMSub) []model.UDMSub { - return r.udmSubRepository.SelectList(u) -} - -// Insert 从数据中读取后删除imsi再存入数据库 -// imsi长度15,ki长度32,opc长度0或者32 -func (r *UDMSubImpl) Insert(neId string, u model.UDMSub) int64 { - uArr := r.dataByRedis(u.IMSI, neId) - if len(uArr) > 0 { - r.udmSubRepository.Delete(neId, u.IMSI) - return r.udmSubRepository.Inserts(uArr) - } - return 0 -} - -// InsertData 导入文件数据 dataType目前两种:txt/csv -func (r *UDMSubImpl) InsertData(neId, dataType string, data any) int64 { - // imsi截取前缀,重新获取部分数据 - prefixes := make(map[string]struct{}) - - if dataType == "csv" { - for _, v := range data.([]map[string]string) { - imsi := v["imsi"] - if len(imsi) < 6 { - continue - } - prefix := imsi[:len(imsi)-4] - prefixes[prefix] = struct{}{} - } - } - if dataType == "txt" { - for _, v := range data.([][]string) { - imsi := v[0] - if len(imsi) < 6 { - continue - } - prefix := imsi[:len(imsi)-4] - prefixes[prefix] = struct{}{} - } - } - - // 根据前缀重新加载插入 - var num int64 = 0 - for prefix := range prefixes { - // 直接删除前缀的记录 - r.udmSubRepository.DeletePrefixByIMSI(neId, prefix) - // keys udm-sd:4600001000004* - subArr := r.dataByRedis(prefix+"*", neId) - if len(subArr) > 0 { - num += r.udmSubRepository.Inserts(subArr) - } - } - return num -} - -// Delete 删除单个不重新加载 -func (r *UDMSubImpl) Delete(neId, imsi string) int64 { - return r.udmSubRepository.Delete(neId, imsi) -} - -// LoadData 删除范围后重新加载 num表示imsi后几位 -func (r *UDMSubImpl) LoadData(neId, imsi, num string) int64 { - prefix := imsi[:len(imsi)-len(num)-1] - // 直接删除前缀的记录 - delNum := r.udmSubRepository.DeletePrefixByIMSI(neId, prefix) - // keys udm-sd:4600001000004* - authArr := r.dataByRedis(prefix+"*", neId) - if len(authArr) > 0 { - return r.udmSubRepository.Inserts(authArr) - } - return delNum -} diff --git a/src/modules/network_data/service/udm_user_info.go b/src/modules/network_data/service/udm_user_info.go new file mode 100644 index 00000000..da610724 --- /dev/null +++ b/src/modules/network_data/service/udm_user_info.go @@ -0,0 +1,33 @@ +package service + +import ( + "be.ems/src/modules/network_data/model" + "be.ems/src/modules/network_data/repository" +) + +// 实例化服务层 UDMUserInfo 结构体 +var NewUDMUserInfo = &UDMUserInfo{ + udmUserInfoRepository: repository.NewUDMUserInfo, +} + +// UDM用户IMSI拓展信息 服务层处理 +type UDMUserInfo struct { + // UDM用户IMSI信息数据信息 + udmUserInfoRepository *repository.UDMUserInfo +} + +// SelectByIMSIAndNeID 通过IMSI和网元标识查询信息 +func (r *UDMUserInfo) SelectByIMSIAndNeID(imsi, neId string) model.UDMUserInfo { + return r.udmUserInfoRepository.SelectByIMSIAndNeID(imsi, neId) +} + +// Save 新增或修改信息 +func (r *UDMUserInfo) Save(u model.UDMUserInfo) bool { + r.udmUserInfoRepository.Delete(u.IMSI, u.NeId) + return r.udmUserInfoRepository.Inserts([]model.UDMUserInfo{u}) > 0 +} + +// Delete 删除信息 +func (r *UDMUserInfo) Delete(imsi, neId string) int64 { + return r.udmUserInfoRepository.Delete(imsi, neId) +} diff --git a/src/modules/network_data/service/ue_event_amf.go b/src/modules/network_data/service/ue_event_amf.go index e5f77a63..29fdec2d 100644 --- a/src/modules/network_data/service/ue_event_amf.go +++ b/src/modules/network_data/service/ue_event_amf.go @@ -1,12 +1,40 @@ package service -import "be.ems/src/modules/network_data/model" +import ( + "fmt" -// UE会话事件AMF 服务层接口 -type IUEEventAMF interface { - // SelectPage 根据条件分页查询 - SelectPage(querys model.UEEventAMFQuery) map[string]any + "be.ems/src/modules/network_data/model" + "be.ems/src/modules/network_data/repository" +) - // DeleteByIds 批量删除信息 - DeleteByIds(ueIds []string) (int64, error) +// 实例化数据层 UEEventAMF 结构体 +var NewUEEventAMF = &UEEventAMF{ + ueEventRepository: repository.NewUEEventAMF, +} + +// UEEventAMF UE会话事件AMF 服务层处理 +type UEEventAMF struct { + // UE会话事件数据信息 + ueEventRepository *repository.UEEventAMF +} + +// SelectPage 根据条件分页查询 +func (r *UEEventAMF) SelectPage(querys model.UEEventAMFQuery) map[string]any { + return r.ueEventRepository.SelectPage(querys) +} + +// DeleteByIds 批量删除信息 +func (r *UEEventAMF) DeleteByIds(ueIds []string) (int64, error) { + // 检查是否存在 + ids := r.ueEventRepository.SelectByIds(ueIds) + if len(ids) <= 0 { + return 0, fmt.Errorf("no data") + } + + if len(ids) == len(ueIds) { + rows := r.ueEventRepository.DeleteByIds(ueIds) + return rows, nil + } + // 删除信息失败! + return 0, fmt.Errorf("delete fail") } diff --git a/src/modules/network_data/service/ue_event_amf.impl.go b/src/modules/network_data/service/ue_event_amf.impl.go deleted file mode 100644 index 8d5f61c9..00000000 --- a/src/modules/network_data/service/ue_event_amf.impl.go +++ /dev/null @@ -1,40 +0,0 @@ -package service - -import ( - "fmt" - - "be.ems/src/modules/network_data/model" - "be.ems/src/modules/network_data/repository" -) - -// 实例化数据层 UEEventMMEImpl 结构体 -var NewUEEventMMEImpl = &UEEventMMEImpl{ - ueEventRepository: repository.NewUEEventMMEImpl, -} - -// UEEventMMEImpl UE会话事件MME 服务层处理 -type UEEventMMEImpl struct { - // UE会话事件数据信息 - ueEventRepository repository.IUEEventMME -} - -// SelectPage 根据条件分页查询 -func (r *UEEventMMEImpl) SelectPage(querys model.UEEventMMEQuery) map[string]any { - return r.ueEventRepository.SelectPage(querys) -} - -// DeleteByIds 批量删除信息 -func (r *UEEventMMEImpl) DeleteByIds(ueIds []string) (int64, error) { - // 检查是否存在 - ids := r.ueEventRepository.SelectByIds(ueIds) - if len(ids) <= 0 { - return 0, fmt.Errorf("no data") - } - - if len(ids) == len(ueIds) { - rows := r.ueEventRepository.DeleteByIds(ueIds) - return rows, nil - } - // 删除信息失败! - return 0, fmt.Errorf("delete fail") -} diff --git a/src/modules/network_data/service/ue_event_mme.go b/src/modules/network_data/service/ue_event_mme.go index d4b5dd86..dd034b25 100644 --- a/src/modules/network_data/service/ue_event_mme.go +++ b/src/modules/network_data/service/ue_event_mme.go @@ -1,12 +1,39 @@ package service -import "be.ems/src/modules/network_data/model" +import ( + "fmt" -// UE会话事件MME 服务层接口 -type IUEEventMME interface { - // SelectPage 根据条件分页查询 - SelectPage(querys model.UEEventMMEQuery) map[string]any + "be.ems/src/modules/network_data/model" + "be.ems/src/modules/network_data/repository" +) - // DeleteByIds 批量删除信息 - DeleteByIds(ueIds []string) (int64, error) +// 实例化数据层 UEEventMME 结构体 +var NewUEEventMME = &UEEventMME{ + ueEventRepository: repository.NewUEEventMME, +} + +// UEEventMME UE会话事件MME 服务层处理 +type UEEventMME struct { + ueEventRepository *repository.UEEventMME // UE会话事件数据信息 +} + +// SelectPage 根据条件分页查询 +func (r *UEEventMME) SelectPage(querys model.UEEventMMEQuery) map[string]any { + return r.ueEventRepository.SelectPage(querys) +} + +// DeleteByIds 批量删除信息 +func (r *UEEventMME) DeleteByIds(ueIds []string) (int64, error) { + // 检查是否存在 + ids := r.ueEventRepository.SelectByIds(ueIds) + if len(ids) <= 0 { + return 0, fmt.Errorf("no data") + } + + if len(ids) == len(ueIds) { + rows := r.ueEventRepository.DeleteByIds(ueIds) + return rows, nil + } + // 删除信息失败! + return 0, fmt.Errorf("delete fail") } diff --git a/src/modules/network_data/service/ue_event_mme.impl.go b/src/modules/network_data/service/ue_event_mme.impl.go deleted file mode 100644 index 9c7e0710..00000000 --- a/src/modules/network_data/service/ue_event_mme.impl.go +++ /dev/null @@ -1,40 +0,0 @@ -package service - -import ( - "fmt" - - "be.ems/src/modules/network_data/model" - "be.ems/src/modules/network_data/repository" -) - -// 实例化数据层 UEEventAMFImpl 结构体 -var NewUEEventAMFImpl = &UEEventAMFImpl{ - ueEventRepository: repository.NewUEEventAMFImpl, -} - -// UEEventAMFImpl UE会话事件AMF 服务层处理 -type UEEventAMFImpl struct { - // UE会话事件数据信息 - ueEventRepository repository.IUEEventAMF -} - -// SelectPage 根据条件分页查询 -func (r *UEEventAMFImpl) SelectPage(querys model.UEEventAMFQuery) map[string]any { - return r.ueEventRepository.SelectPage(querys) -} - -// DeleteByIds 批量删除信息 -func (r *UEEventAMFImpl) DeleteByIds(ueIds []string) (int64, error) { - // 检查是否存在 - ids := r.ueEventRepository.SelectByIds(ueIds) - if len(ids) <= 0 { - return 0, fmt.Errorf("no data") - } - - if len(ids) == len(ueIds) { - rows := r.ueEventRepository.DeleteByIds(ueIds) - return rows, nil - } - // 删除信息失败! - return 0, fmt.Errorf("delete fail") -} diff --git a/src/modules/network_element/controller/action.go b/src/modules/network_element/controller/action.go index 4d597356..6e24c7a5 100644 --- a/src/modules/network_element/controller/action.go +++ b/src/modules/network_element/controller/action.go @@ -2,6 +2,7 @@ package controller import ( "fmt" + "os" "path/filepath" "runtime" "strings" @@ -9,6 +10,7 @@ import ( "be.ems/src/framework/i18n" "be.ems/src/framework/utils/ctx" "be.ems/src/framework/utils/file" + "be.ems/src/framework/utils/generate" "be.ems/src/framework/utils/ssh" "be.ems/src/framework/vo/result" neService "be.ems/src/modules/network_element/service" @@ -18,7 +20,7 @@ import ( // 实例化控制层 NeActionController 结构体 var NewNeAction = &NeActionController{ - neInfoService: neService.NewNeInfoImpl, + neInfoService: neService.NewNeInfo, } // 网元处理请求 @@ -26,10 +28,10 @@ var NewNeAction = &NeActionController{ // PATH /action type NeActionController struct { // 网元信息服务 - neInfoService neService.INeInfo + neInfoService *neService.NeInfo } -// 发送文件到网元端 +// 从本地到网元发送文件 // // POST /pushFile func (s *NeActionController) PushFile(c *gin.Context) { @@ -38,6 +40,7 @@ func (s *NeActionController) PushFile(c *gin.Context) { NeType string `json:"neType" binding:"required"` NeID string `json:"neId" binding:"required"` UploadPath string `json:"uploadPath" binding:"required"` + DelTemp bool `json:"delTemp"` // 删除本地临时文件 } if err := c.ShouldBindBodyWith(&body, binding.JSON); err != nil { c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) @@ -68,17 +71,24 @@ func (s *NeActionController) PushFile(c *gin.Context) { // 本地文件 localFilePath := file.ParseUploadFilePath(body.UploadPath) - neFilePath := fmt.Sprintf("/tmp/%s", filepath.Base(localFilePath)) + // 网元端临时目录 + sshClient.RunCMD("mkdir -p /tmp/omc/push && sudo chmod 777 -R /tmp/omc") + neFilePath := filepath.ToSlash(filepath.Join("/tmp/omc/push", filepath.Base(localFilePath))) // 复制到远程 if err = sftpClient.CopyFileLocalToRemote(localFilePath, neFilePath); err != nil { - c.JSON(200, result.ErrMsg(fmt.Sprintf("%s : please check if scp remote copy is allowed", neInfo.NeType))) + c.JSON(200, result.ErrMsg("Please check if the file exists or if scp is allowed to copy remotely")) return } + defer func() { + if body.DelTemp { + _ = os.Remove(localFilePath) + } + }() c.JSON(200, result.OkData(filepath.ToSlash(neFilePath))) } -// 获取文件从网元到本地 +// 从网元到本地获取文件 // // GET /pullFile func (s *NeActionController) PullFile(c *gin.Context) { @@ -88,6 +98,7 @@ func (s *NeActionController) PullFile(c *gin.Context) { NeID string `form:"neId" binding:"required"` Path string `form:"path" binding:"required"` FileName string `form:"fileName" binding:"required"` + DelTemp bool `form:"delTemp"` // 删除本地临时文件 } if err := c.ShouldBindQuery(&querys); err != nil { c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) @@ -116,8 +127,9 @@ func (s *NeActionController) PullFile(c *gin.Context) { } defer sftpClient.Close() - nePath := fmt.Sprintf("%s/%s", querys.Path, querys.FileName) - localFilePath := fmt.Sprintf("/tmp/omc/pullFile%s", nePath) + nePath := filepath.ToSlash(filepath.Join(querys.Path, querys.FileName)) + fileName := generate.Code(6) + "_" + querys.FileName + localFilePath := filepath.Join("/tmp/omc/pull", fileName) if runtime.GOOS == "windows" { localFilePath = fmt.Sprintf("C:%s", localFilePath) } @@ -126,7 +138,123 @@ func (s *NeActionController) PullFile(c *gin.Context) { c.JSON(200, result.ErrMsg(err.Error())) return } - c.FileAttachment(localFilePath, querys.FileName) + + defer func() { + if querys.DelTemp { + _ = os.Remove(localFilePath) + } + }() + c.FileAttachment(localFilePath, fileName) +} + +// 从网元到本地获取目录压缩为ZIP +// +// GET /pullDirZip +func (s *NeActionController) PullDirZip(c *gin.Context) { + language := ctx.AcceptLanguage(c) + var querys struct { + NeType string `form:"neType" binding:"required"` + NeID string `form:"neId" binding:"required"` + Path string `form:"path" binding:"required"` + DelTemp bool `form:"delTemp"` // 删除本地临时文件 + } + if err := c.ShouldBindQuery(&querys); err != nil { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + + // 查询网元获取IP + neInfo := s.neInfoService.SelectNeInfoByNeTypeAndNeID(querys.NeType, querys.NeID) + if neInfo.NeId != querys.NeID || neInfo.IP == "" { + c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) + return + } + + // 网元主机的SSH客户端 + sshClient, err := s.neInfoService.NeRunSSHClient(neInfo.NeType, neInfo.NeId) + if err != nil { + c.JSON(200, result.ErrMsg(err.Error())) + return + } + defer sshClient.Close() + // 网元主机的SSH客户端进行文件传输 + sftpClient, err := sshClient.NewClientSFTP() + if err != nil { + c.JSON(200, result.ErrMsg(err.Error())) + return + } + defer sftpClient.Close() + + nePath := querys.Path + dirName := generate.Code(6) + localFilePath := filepath.Join("/tmp/omc/pull/", dirName) + if runtime.GOOS == "windows" { + localFilePath = fmt.Sprintf("C:%s", localFilePath) + } + // 复制到本地 + localDirFilePath := filepath.Join(localFilePath, "zip") + if err = sftpClient.CopyDirRemoteToLocal(nePath, localDirFilePath); err != nil { + c.JSON(200, result.ErrMsg(err.Error())) + return + } + + // 压缩zip文件名 + zipFileName := fmt.Sprintf("%s-%s-%s.zip", neInfo.NeType, neInfo.NeId, dirName) + zipFilePath := filepath.Join(localFilePath, zipFileName) + if err := file.CompressZipByDir(zipFilePath, localDirFilePath); err != nil { + c.JSON(200, result.ErrMsg(err.Error())) + return + } + + defer func() { + if querys.DelTemp { + _ = os.RemoveAll(localFilePath) + } + }() + c.FileAttachment(zipFilePath, zipFileName) +} + +// 查看网元端文件内容 +// +// GET /viewFile +func (s *NeActionController) ViewFile(c *gin.Context) { + language := ctx.AcceptLanguage(c) + var querys struct { + NeType string `form:"neType" binding:"required"` + NeID string `form:"neId" binding:"required"` + Path string `form:"path" binding:"required"` + FileName string `form:"fileName" binding:"required"` + } + if err := c.ShouldBindQuery(&querys); err != nil { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + + // 查询网元获取IP + neInfo := s.neInfoService.SelectNeInfoByNeTypeAndNeID(querys.NeType, querys.NeID) + if neInfo.NeId != querys.NeID || neInfo.IP == "" { + c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) + return + } + + // 网元主机的SSH客户端 + sshClient, err := s.neInfoService.NeRunSSHClient(neInfo.NeType, neInfo.NeId) + if err != nil { + c.JSON(200, result.ErrMsg(err.Error())) + return + } + defer sshClient.Close() + + // 网元端文件 + nePath := filepath.ToSlash(filepath.Join(querys.Path, querys.FileName)) + // 网元端临时目录 + output, err := sshClient.RunCMD(fmt.Sprintf("cat %s", nePath)) + output = strings.TrimSpace(output) + if err != nil || strings.HasPrefix(output, "ls: ") { + c.JSON(200, result.ErrMsg(i18n.TKey(language, "file view cat error"))) + return + } + c.JSON(200, result.OkData(output)) } // 网元端文件列表 @@ -163,13 +291,12 @@ func (s *NeActionController) Files(c *gin.Context) { defer sshClient.Close() // 获取文件列表 - totalSize, rows, err := ssh.FileList(sshClient, querys.Path, querys.Search) + rows, err := ssh.FileList(sshClient, querys.Path, querys.Search) if err != nil { c.JSON(200, result.Ok(map[string]any{ - "path": querys.Path, - "totalSize": totalSize, - "total": len(rows), - "rows": []ssh.FileListRow{}, + "path": querys.Path, + "total": len(rows), + "rows": []ssh.FileListRow{}, })) return } @@ -188,10 +315,9 @@ func (s *NeActionController) Files(c *gin.Context) { } c.JSON(200, result.Ok(map[string]any{ - "path": querys.Path, - "totalSize": totalSize, - "total": lenNum, - "rows": splitRows, + "path": querys.Path, + "total": lenNum, + "rows": splitRows, })) } diff --git a/src/modules/network_element/controller/ne_config.go b/src/modules/network_element/controller/ne_config.go index 0c74b33c..c6c25be6 100644 --- a/src/modules/network_element/controller/ne_config.go +++ b/src/modules/network_element/controller/ne_config.go @@ -4,6 +4,7 @@ import ( "encoding/json" "strings" + cm_omc "be.ems/features/cm/omc" "be.ems/src/framework/i18n" "be.ems/src/framework/utils/ctx" "be.ems/src/framework/utils/parse" @@ -17,18 +18,16 @@ import ( // NewNeConfig 网元参数配置 实例化控制层 var NewNeConfig = &NeConfigController{ - neConfigService: neService.NewNeConfigImpl, - neInfoService: neService.NewNeInfoImpl, + neConfigService: neService.NewNeConfig, + neInfoService: neService.NewNeInfo, } // 网元参数配置 // // PATH /config type NeConfigController struct { - // 网元参数配置可用属性值服务 - neConfigService neService.INeConfig - // 网元信息服务 - neInfoService neService.INeInfo + neConfigService *neService.NeConfig // 网元参数配置可用属性值服务 + neInfoService *neService.NeInfo // 网元信息服务 } // 网元参数配置可用属性值列表 @@ -191,14 +190,25 @@ func (s *NeConfigController) DataInfo(c *gin.Context) { return } - // 网元直连 - resData, err := neFetchlink.NeConfigInfo(neInfo, query.ParamName) - if err != nil { - c.JSON(200, result.ErrMsg(err.Error())) + if query.NeType == "OMC" { + var o *cm_omc.ConfigOMC + resData, err := o.Query(query.ParamName) + if err != nil { + c.JSON(200, result.ErrMsg(err.Error())) + return + } + c.JSON(200, result.OkData(resData)) return - } + } else { + // 网元直连 + resData, err := neFetchlink.NeConfigInfo(neInfo, query.ParamName) + if err != nil { + c.JSON(200, result.ErrMsg(err.Error())) + return + } - c.JSON(200, result.Ok(resData)) + c.JSON(200, result.Ok(resData)) + } } // 网元参数配置数据修改 @@ -223,14 +233,24 @@ func (s *NeConfigController) DataEdit(c *gin.Context) { c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) return } - - // 网元直连 - resData, err := neFetchlink.NeConfigUpdate(neInfo, body.ParamName, body.Loc, body.ParamData) - if err != nil { - c.JSON(200, result.ErrMsg(err.Error())) + if body.NeType == "OMC" { + var o *cm_omc.ConfigOMC + resData, err := o.Modify(body.ParamName, body.ParamData) + if err != nil { + c.JSON(200, result.ErrMsg(err.Error())) + return + } + c.JSON(200, result.OkData(resData)) return + } else { + // 网元直连 + resData, err := neFetchlink.NeConfigUpdate(neInfo, body.ParamName, body.Loc, body.ParamData) + if err != nil { + c.JSON(200, result.ErrMsg(err.Error())) + return + } + c.JSON(200, result.OkData(resData)) } - c.JSON(200, result.OkData(resData)) } // 网元参数配置数据新增(array) diff --git a/src/modules/network_element/controller/ne_config_backup.go b/src/modules/network_element/controller/ne_config_backup.go index 1ba1870c..90937dc3 100644 --- a/src/modules/network_element/controller/ne_config_backup.go +++ b/src/modules/network_element/controller/ne_config_backup.go @@ -18,18 +18,16 @@ import ( // NewNeConfigBackup 实例化控制层 NeConfigBackupController 结构体 var NewNeConfigBackup = &NeConfigBackupController{ - neConfigBackupService: neService.NewNeConfigBackupImpl, - neInfoService: neService.NewNeInfoImpl, + neConfigBackupService: neService.NewNeConfigBackup, + neInfoService: neService.NewNeInfo, } // 网元配置文件备份记录 // // PATH /config/backup type NeConfigBackupController struct { - // 网元配置文件备份记录服务 - neConfigBackupService neService.INeConfigBackup - // 网元信息服务 - neInfoService neService.INeInfo + neConfigBackupService *neService.NeConfigBackup // 网元配置文件备份记录服务 + neInfoService *neService.NeInfo // 网元信息服务 } // 网元配置文件备份记录列表 diff --git a/src/modules/network_element/controller/ne_host.go b/src/modules/network_element/controller/ne_host.go index 57685740..e4719bc2 100644 --- a/src/modules/network_element/controller/ne_host.go +++ b/src/modules/network_element/controller/ne_host.go @@ -4,10 +4,11 @@ import ( "strings" "be.ems/src/framework/i18n" + "be.ems/src/framework/redis" + "be.ems/src/framework/telnet" "be.ems/src/framework/utils/ctx" "be.ems/src/framework/utils/parse" "be.ems/src/framework/utils/ssh" - "be.ems/src/framework/utils/telnet" "be.ems/src/framework/vo/result" "be.ems/src/modules/network_element/model" neService "be.ems/src/modules/network_element/service" @@ -17,15 +18,14 @@ import ( // 实例化控制层 NeHostController 结构体 var NewNeHost = &NeHostController{ - neHostService: neService.NewNeHostImpl, + neHostService: neService.NewNeHost, } // 网元主机连接请求 // // PATH /host type NeHostController struct { - // 网元主机连接服务 - neHostService neService.INeHost + neHostService *neService.NeHost // 网元主机连接服务 } // 网元主机列表 @@ -79,6 +79,13 @@ func (s *NeHostController) Add(c *gin.Context) { return } + if body.GroupID == "1" { + // 主机信息操作【%s】失败,禁止操作网元 + msg := i18n.TKey(language, "neHost.banNE") + c.JSON(200, result.ErrMsg(msg)) + return + } + // 检查属性值唯一 uniqueHost := s.neHostService.CheckUniqueHostTitle(body.GroupID, body.Title, body.HostType, "") if !uniqueHost { @@ -212,6 +219,21 @@ func (s *NeHostController) Test(c *gin.Context) { } return } + + if body.HostType == "redis" { + var connRedis redis.ConnRedis + body.CopyTo(&connRedis) + + client, err := connRedis.NewClient() + if err != nil { + // 连接主机失败,请检查连接参数后重试 + c.JSON(200, result.ErrMsg(i18n.TKey(language, "neHost.errByHostInfo"))) + return + } + defer client.Close() + c.JSON(200, result.Ok(nil)) + return + } } // 网元主机发送命令 diff --git a/src/modules/network_element/controller/ne_host_cmd.go b/src/modules/network_element/controller/ne_host_cmd.go index f817a59a..31031da5 100644 --- a/src/modules/network_element/controller/ne_host_cmd.go +++ b/src/modules/network_element/controller/ne_host_cmd.go @@ -15,15 +15,14 @@ import ( // 实例化控制层 NeHostCmdController 结构体 var NewNeHostCmd = &NeHostCmdController{ - neHostCmdService: neService.NewNeHostCmdImpl, + neHostCmdService: neService.NewNeHostCmd, } // 网元主机命令请求 // // PATH /hostCmd type NeHostCmdController struct { - // 网元主机命令服务 - neHostCmdService neService.INeHostCmd + neHostCmdService *neService.NeHostCmd // 网元主机命令服务 } // 网元主机命令列表 diff --git a/src/modules/network_element/controller/ne_info.go b/src/modules/network_element/controller/ne_info.go index ea0ded6e..c2ce7ab3 100644 --- a/src/modules/network_element/controller/ne_info.go +++ b/src/modules/network_element/controller/ne_info.go @@ -18,21 +18,18 @@ import ( // 实例化控制层 NeInfoController 结构体 var NewNeInfo = &NeInfoController{ - neInfoService: neService.NewNeInfoImpl, - neLicenseService: neService.NewNeLicenseImpl, - neVersionService: neService.NewNeVersionImpl, + neInfoService: neService.NewNeInfo, + neLicenseService: neService.NewNeLicense, + neVersionService: neService.NewNeVersion, } // 网元信息请求 // // PATH /info type NeInfoController struct { - // 网元信息服务 - neInfoService neService.INeInfo - // 网元授权激活信息服务 - neLicenseService neService.INeLicense - // 网元版本信息服务 - neVersionService neService.INeVersion + neInfoService *neService.NeInfo // 网元信息服务 + neLicenseService *neService.NeLicense // 网元授权激活信息服务 + neVersionService *neService.NeVersion // 网元版本信息服务 } // neStateCacheMap 网元状态缓存最后一次成功的信息 @@ -193,7 +190,7 @@ func (s *NeInfoController) OAMFileRead(c *gin.Context) { return } - data, err := s.neInfoService.NeConfOAMRead(querys.NeType, querys.NeID) + data, err := s.neInfoService.NeConfOAMReadSync(querys.NeType, querys.NeID) if err != nil { c.JSON(200, result.ErrMsg(err.Error())) return @@ -224,7 +221,7 @@ func (s *NeInfoController) OAMFileWrite(c *gin.Context) { return } - err := s.neInfoService.NeConfOAMSync(neInfo, body.Content, body.Sync) + err := s.neInfoService.NeConfOAMWirteSync(neInfo, body.Content, body.Sync) if err != nil { c.JSON(200, result.ErrMsg(err.Error())) return @@ -318,6 +315,8 @@ func (s *NeInfoController) Add(c *gin.Context) { // 已有网元可获取的信息 if body.ServerState != nil { if v, ok := body.ServerState["version"]; ok && v != nil { + neVersion.Name = "-" + neVersion.Path = "-" neVersion.Version = v.(string) } if v, ok := body.ServerState["sn"]; ok && v != nil { @@ -397,6 +396,8 @@ func (s *NeInfoController) Edit(c *gin.Context) { // 已有网元可获取的信息 if body.ServerState != nil { if v, ok := body.ServerState["version"]; ok && v != nil { + neVersion.Name = "-" + neVersion.Path = "-" neVersion.Version = v.(string) neVersion.UpdateBy = loginUserName } diff --git a/src/modules/network_element/controller/ne_license.go b/src/modules/network_element/controller/ne_license.go index 3b4dd4ce..4ddf1ed5 100644 --- a/src/modules/network_element/controller/ne_license.go +++ b/src/modules/network_element/controller/ne_license.go @@ -15,18 +15,16 @@ import ( // 实例化控制层 NeLicenseController 结构体 var NewNeLicense = &NeLicenseController{ - neLicenseService: neService.NewNeLicenseImpl, - neInfoService: neService.NewNeInfoImpl, + neLicenseService: neService.NewNeLicense, + neInfoService: neService.NewNeInfo, } // 网元授权激活请求 // // PATH /license type NeLicenseController struct { - // 网元授权激活服务 - neLicenseService neService.INeLicense - // 网元信息服务 - neInfoService neService.INeInfo + neLicenseService *neService.NeLicense // 网元授权激活服务 + neInfoService *neService.NeInfo // 网元信息服务 } // 网元授权激活列表 diff --git a/src/modules/network_element/controller/ne_software.go b/src/modules/network_element/controller/ne_software.go index fb15beb8..25c05803 100644 --- a/src/modules/network_element/controller/ne_software.go +++ b/src/modules/network_element/controller/ne_software.go @@ -15,15 +15,14 @@ import ( // 实例化控制层 NeSoftwareController 结构体 var NewNeSoftware = &NeSoftwareController{ - neSoftwareService: neService.NewNeSoftwareImpl, + neSoftwareService: neService.NewNeSoftware, } // 网元软件包请求 // // PATH /software type NeSoftwareController struct { - // 网元软件包服务 - neSoftwareService neService.INeSoftware + neSoftwareService *neService.NeSoftware // 网元软件包服务 } // 网元软件包列表 diff --git a/src/modules/network_element/controller/ne_version.go b/src/modules/network_element/controller/ne_version.go index 8df9b017..366c9891 100644 --- a/src/modules/network_element/controller/ne_version.go +++ b/src/modules/network_element/controller/ne_version.go @@ -11,15 +11,14 @@ import ( // 实例化控制层 NeVersionController 结构体 var NewNeVersion = &NeVersionController{ - neVersionService: neService.NewNeVersionImpl, + neVersionService: neService.NewNeVersion, } // 网元版本请求 // // PATH /version type NeVersionController struct { - // 网元版本服务 - neVersionService neService.INeVersion + neVersionService *neService.NeVersion // 网元版本服务 } // 网元版本列表 @@ -27,7 +26,7 @@ type NeVersionController struct { // GET /list func (s *NeVersionController) List(c *gin.Context) { querys := ctx.QueryMap(c) - data := s.neVersionService.SelectPage(querys) + data := s.neVersionService.SelectPage(querys, true) c.JSON(200, result.Ok(data)) } diff --git a/src/modules/network_element/fetch_link/hlr.go b/src/modules/network_element/fetch_link/hlr.go new file mode 100644 index 00000000..8a4c1198 --- /dev/null +++ b/src/modules/network_element/fetch_link/hlr.go @@ -0,0 +1,68 @@ +// 网元HLR服务8080端口。 +// 融合到UDM网元,也许是UDM的HLR服务。 + +package fetchlink + +import ( + "encoding/json" + "fmt" + "strings" + + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/fetch" + "be.ems/src/modules/network_element/model" +) + +// HLRTraceStart HLR跟踪任务开始 +// +// data参数 {traceID:"跟踪任务ID", imsi:"IMSI和MSISDN必填一个,都带的话以IMSI为准", msisdn:""} +func HLRTraceStart(neInfo model.NeInfo, data map[string]any) (string, error) { + // 网元参数配置新增(array) + neUrl := fmt.Sprintf("http://%s:%d/trace-manage/v1/add-task", neInfo.IP, neInfo.Port) + resBytes, err := fetch.PostJSON(neUrl, data, nil) + var resData map[string]string + if err != nil { + errStr := err.Error() + logger.Warnf("HLRTraceStart Post \"%s\"", neUrl) + logger.Errorf("HLRTraceStart %s", errStr) + return "", fmt.Errorf("NeService HLR API Error") + } + + // 序列化结果 + err = json.Unmarshal(resBytes, &resData) + if err != nil { + logger.Errorf("HLRTraceStart Unmarshal %s", err.Error()) + return "", err + } + if v, ok := resData["code"]; ok && v == "0" { + return strings.TrimSpace(strings.ToLower(resData["message"])), nil + } + return "", fmt.Errorf(resData["message"]) +} + +// HLRTraceStop HLR跟踪任务停止 +// +// data参数 {traceIDArray: ["跟踪任务ID数组"]} +func HLRTraceStop(neInfo model.NeInfo, data map[string]any) (string, error) { + // 网元参数配置新增(array) + neUrl := fmt.Sprintf("http://%s:%d/trace-manage/v1/delete-task", neInfo.IP, neInfo.Port) + resBytes, err := fetch.PostJSON(neUrl, data, nil) + var resData map[string]string + if err != nil { + errStr := err.Error() + logger.Warnf("HLRTraceStop Post \"%s\"", neUrl) + logger.Errorf("HLRTraceStop %s", errStr) + return "", fmt.Errorf("NeService HLR API Error") + } + + // 序列化结果 + err = json.Unmarshal(resBytes, &resData) + if err != nil { + logger.Errorf("HLRTraceStop Unmarshal %s", err.Error()) + return "", err + } + if v, ok := resData["code"]; ok && v == "0" { + return strings.TrimSpace(strings.ToLower(resData["message"])), nil + } + return "", fmt.Errorf(resData["message"]) +} diff --git a/src/modules/network_element/fetch_link/ne_config.go b/src/modules/network_element/fetch_link/ne_config.go index 3641cda9..4695351a 100644 --- a/src/modules/network_element/fetch_link/ne_config.go +++ b/src/modules/network_element/fetch_link/ne_config.go @@ -52,9 +52,9 @@ func NeConfigOMC(neInfo model.NeInfo) (map[string]any, error) { // NeConfigInfo 网元配置信息 func NeConfigInfo(neInfo model.NeInfo, paramName string) (map[string]any, error) { - // 网元配置对端网管信息 + // 网元参数配置信息 neUrl := fmt.Sprintf("http://%s:%d/api/rest/systemManagement/v1/elementType/%s/objectType/config/%s", neInfo.IP, neInfo.Port, strings.ToLower(neInfo.NeType), paramName) - resBytes, err := fetch.Get(neUrl, nil, 1000) + resBytes, err := fetch.Get(neUrl, nil, 30_000) if err != nil { logger.Warnf("NeConfigInfo Get \"%s\"", neUrl) logger.Errorf("NeConfigInfo %s", err.Error()) diff --git a/src/modules/network_element/fetch_link/ne_state.go b/src/modules/network_element/fetch_link/ne_state.go index 9a5feb39..9da0547d 100644 --- a/src/modules/network_element/fetch_link/ne_state.go +++ b/src/modules/network_element/fetch_link/ne_state.go @@ -49,6 +49,8 @@ func NeState(neInfo model.NeInfo) (map[string]any, error) { "capability": resData["capability"], "sn": resData["serialNum"], "expire": resData["expiryDate"], + "hostname": resData["hostName"], + "os": resData["osInfo"], "cpu": resData["cpuUsage"], "mem": resData["memUsage"], "disk": resData["diskSpace"], diff --git a/src/modules/network_element/fetch_link/ne_trace.go b/src/modules/network_element/fetch_link/ne_trace.go new file mode 100644 index 00000000..387633fa --- /dev/null +++ b/src/modules/network_element/fetch_link/ne_trace.go @@ -0,0 +1,121 @@ +package fetchlink + +import ( + "encoding/json" + "fmt" + "strings" + + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/fetch" + "be.ems/src/modules/network_element/model" +) + +// NeTraceInfo 网元跟踪任务信息 +func NeTraceInfo(neInfo model.NeInfo, traceId string) (map[string]any, error) { + // 跟踪任务信息 + neUrl := fmt.Sprintf("http://%s:%d/api/rest/traceManagement/v1/subscriptions?id=%s", neInfo.IP, neInfo.Port, traceId) + resBytes, err := fetch.Get(neUrl, nil, 30_000) + if err != nil { + logger.Warnf("NeTraceInfo Get \"%s\"", neUrl) + logger.Errorf("NeTraceInfo %s", err.Error()) + return nil, fmt.Errorf("NeService Trace Info API Error") + } + + // 序列化结果 + var resData map[string]any + err = json.Unmarshal(resBytes, &resData) + if err != nil { + logger.Errorf("NeTraceInfo Unmarshal %s", err.Error()) + return nil, err + } + return resData, nil +} + +// NeTraceAdd 网元跟踪任务新增 +func NeTraceAdd(neInfo model.NeInfo, data map[string]any) (map[string]any, error) { + // 跟踪任务创建 + neUrl := fmt.Sprintf("http://%s:%d/api/rest/traceManagement/v1/subscriptions", neInfo.IP, neInfo.Port) + resBytes, err := fetch.PostJSON(neUrl, data, nil) + var resData map[string]any + if err != nil { + errStr := err.Error() + logger.Warnf("NeTraceAdd POST \"%s\"", neUrl) + if !(strings.HasPrefix(errStr, "201") || strings.HasPrefix(errStr, "400")) { + logger.Errorf("NeTraceAdd %s", errStr) + return nil, fmt.Errorf("NeService Trace Add API Error") + } + } + + // 200 成功无数据时 + if len(resBytes) == 0 { + return resData, nil + } + + // 序列化结果 + err = json.Unmarshal(resBytes, &resData) + if err != nil { + logger.Errorf("NeTraceAdd Unmarshal %s", err.Error()) + return nil, err + } + return resData, nil +} + +// NeTraceEdit 网元跟踪任务编辑 +func NeTraceEdit(neInfo model.NeInfo, data map[string]any) (map[string]any, error) { + // 网元参数配置新增(array) + neUrl := fmt.Sprintf("http://%s:%d/api/rest/traceManagement/v1/subscriptions", neInfo.IP, neInfo.Port) + resBytes, err := fetch.PutJSON(neUrl, data, nil) + var resData map[string]any + if err != nil { + errStr := err.Error() + logger.Warnf("NeTraceEdit PUT \"%s\"", neUrl) + if strings.HasPrefix(errStr, "201") || strings.HasPrefix(errStr, "204") { + return resData, nil + } + logger.Errorf("NeTraceEdit %s", errStr) + return nil, fmt.Errorf("NeService Trace Edit API Error") + } + + // 200 成功无数据时 + if len(resBytes) == 0 { + return resData, nil + } + + // 序列化结果 + err = json.Unmarshal(resBytes, &resData) + if err != nil { + logger.Errorf("NeTraceEdit Unmarshal %s", err.Error()) + return nil, err + } + return resData, nil +} + +// NeTraceDelete 网元跟踪任务删除 +func NeTraceDelete(neInfo model.NeInfo, traceId string) (map[string]any, error) { + // 网元参数配置删除(array) + neUrl := fmt.Sprintf("http://%s:%d/api/rest/traceManagement/v1/subscriptions?id=%s", neInfo.IP, neInfo.Port, traceId) + resBytes, err := fetch.Delete(neUrl, nil) + var resData map[string]any + if err != nil { + errStr := err.Error() + logger.Warnf("NeTraceDelete Delete \"%s\"", neUrl) + if strings.HasPrefix(errStr, "201") || strings.HasPrefix(errStr, "204") { + return resData, nil + } + logger.Errorf("NeTraceDelete %s", errStr) + return nil, fmt.Errorf("NeService Trace Delete API Error") + } + + // 200 成功无数据时 + if len(resBytes) == 0 { + return resData, nil + } + + // 序列化结果 + err = json.Unmarshal(resBytes, &resData) + if err != nil { + logger.Errorf("NeTraceDelete Unmarshal %s", err.Error()) + return nil, err + } + return resData, nil +} diff --git a/src/modules/network_element/fetch_link/smf.go b/src/modules/network_element/fetch_link/smf.go new file mode 100644 index 00000000..bbbb0ee3 --- /dev/null +++ b/src/modules/network_element/fetch_link/smf.go @@ -0,0 +1,66 @@ +package fetchlink + +import ( + "encoding/json" + "fmt" + "strings" + + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/fetch" + "be.ems/src/modules/network_element/model" +) + +// SMFSubInfoList SMF在线订阅用户列表信息 +// +// 查询参数 {"imsi":"360000100000130","msisdn":"8612300000130","upstate":"Inactive","pageNum":"1"} +// +// 返回结果 {"rows":[],"total":0} +func SMFSubInfoList(neInfo model.NeInfo, data map[string]string) (map[string]any, error) { + neUrl := fmt.Sprintf("http://%s:%d/api/rest/ueManagement/v1/elementType/smf/objectType/ueInfo", neInfo.IP, neInfo.Port) + // 查询参数拼接 + query := []string{} + if v, ok := data["imsi"]; ok && v != "" { + query = append(query, fmt.Sprintf("imsi=%s", v)) + } + if v, ok := data["msisdn"]; ok && v != "" { + query = append(query, fmt.Sprintf("msisdn=%s", v)) + } + if v, ok := data["upstate"]; ok && v != "" { + query = append(query, fmt.Sprintf("upstate=%s", v)) + } + // 固定页数量50条 + if v, ok := data["pageNum"]; ok && v != "" { + query = append(query, fmt.Sprintf("pageNum=%s", v)) + } + + if len(query) > 0 { + neUrl = fmt.Sprintf("%s?%s", neUrl, strings.Join(query, "&")) + } + + resBytes, err := fetch.Get(neUrl, nil, 60_000) + if err != nil { + logger.Warnf("SMFSubInfo Get \"%s\"", neUrl) + logger.Errorf("SMFSubInfo %s", err.Error()) + return nil, fmt.Errorf("NeService SMF API Error") + } + + // 序列化结果 {"data":[],"total":0} + var resData map[string]any + err = json.Unmarshal(resBytes, &resData) + if err != nil { + logger.Errorf("SMFSubInfo Unmarshal %s", err.Error()) + return nil, err + } + + // 固定返回字段,方便前端解析 + if v, ok := resData["data"]; ok && v != nil { + resData["rows"] = v.([]any) + delete(resData, "data") + } else { + resData["rows"] = []any{} + } + if v, ok := resData["total"]; !ok || v == nil { + resData["total"] = 0 + } + return resData, nil +} diff --git a/src/modules/network_element/fetch_link/udm.go b/src/modules/network_element/fetch_link/udm.go index 2f1119c3..d027f9f0 100644 --- a/src/modules/network_element/fetch_link/udm.go +++ b/src/modules/network_element/fetch_link/udm.go @@ -1,3 +1,6 @@ +// 网元UDM服务,可能是8080、33030端口服务 +// 融合的UDM网元视情况调整。 + package fetchlink import ( @@ -25,8 +28,7 @@ func UDMImportAuth(udmIP string, data map[string]any) (string, error) { } // 序列化结果 - err = json.Unmarshal(resBytes, &resData) - if err != nil { + if err = json.Unmarshal(resBytes, &resData); err != nil { logger.Errorf("UDMImportAuth Unmarshal %s", err.Error()) return "", err } diff --git a/src/modules/network_element/model/ne_config.go b/src/modules/network_element/model/ne_config.go index 0b694401..99ae9941 100644 --- a/src/modules/network_element/model/ne_config.go +++ b/src/modules/network_element/model/ne_config.go @@ -11,6 +11,7 @@ type NeConfig struct { ParamSort int64 `json:"paramSort" gorm:"param_sort"` // 参数排序 ParamPerms string `json:"paramPerms" gorm:"param_perms"` // 操作权限 get只读 put可编辑 delete可删除 post可新增 UpdateTime int64 `json:"updateTime" gorm:"update_time"` // 更新时间 + // ====== 非数据库字段属性 ====== ParamData []map[string]any `json:"paramData,omitempty" binding:"required" gorm:"-"` // 与ParamJSONStr配合转换 diff --git a/src/modules/network_element/model/ne_host.go b/src/modules/network_element/model/ne_host.go index ba7362b2..1ff3db06 100644 --- a/src/modules/network_element/model/ne_host.go +++ b/src/modules/network_element/model/ne_host.go @@ -5,16 +5,17 @@ import "encoding/json" // NeHost 网元主机表 ne_host type NeHost struct { HostID string `json:"hostId" gorm:"column:host_id"` // 主机主键 - HostType string `json:"hostType" gorm:"column:host_type" binding:"oneof=ssh telnet"` // 主机类型 ssh telnet + HostType string `json:"hostType" gorm:"column:host_type" binding:"oneof=ssh telnet redis"` // 连接类型 ssh telnet redis GroupID string `json:"groupId" gorm:"column:group_id"` // 分组(0默认 1网元 2系统) Title string `json:"title" gorm:"column:title"` // 标题名称 Addr string `json:"addr" gorm:"column:addr" binding:"required"` // 主机地址 - Port int64 `json:"port" gorm:"column:port" binding:"required,number,max=65535,min=1"` // SSH端口 - User string `json:"user" gorm:"column:user" binding:"required"` // 主机用户名 + Port int64 `json:"port" gorm:"column:port" binding:"required,number,max=65535,min=1"` // 端口 22 4100 6379 + User string `json:"user" gorm:"column:user" binding:"required"` // 认证用户名 AuthMode string `json:"authMode" gorm:"column:auth_mode" binding:"oneof=0 1 2"` // 认证模式(0密码 1主机私钥 2已免密) Password string `json:"password" gorm:"column:password"` // 认证密码 PrivateKey string `json:"privateKey" gorm:"column:private_key"` // 认证私钥 PassPhrase string `json:"passPhrase" gorm:"column:pass_phrase"` // 认证私钥密码 + DBName string `json:"dbName" gorm:"column:db_name"` // 数据库名称 Remark string `json:"remark" gorm:"column:remark"` // 备注 CreateBy string `json:"createBy" gorm:"column:create_by"` // 创建者 CreateTime int64 `json:"createTime" gorm:"column:create_time"` // 创建时间 @@ -23,7 +24,7 @@ type NeHost struct { } // TableName 表名称 -func (NeHost) TableName() string { +func (*NeHost) TableName() string { return "ne_host" } diff --git a/src/modules/network_element/model/ne_info.go b/src/modules/network_element/model/ne_info.go index 31cbedec..71a1a756 100644 --- a/src/modules/network_element/model/ne_info.go +++ b/src/modules/network_element/model/ne_info.go @@ -2,7 +2,7 @@ package model // NeInfo 网元信息对象 ne_info type NeInfo struct { - ID string `json:"id" gorm:"column:id;primaryKey;autoIncrement"` + ID string `json:"id" gorm:"id"` NeType string `json:"neType" gorm:"ne_type" binding:"required"` NeId string `json:"neId" gorm:"ne_id" binding:"required"` RmUID string `json:"rmUid" gorm:"rm_uid"` @@ -14,7 +14,7 @@ type NeInfo struct { VendorName string `json:"vendorName" gorm:"vendor_name"` Dn string `json:"dn" gorm:"dn"` NeAddress string `json:"neAddress" gorm:"ne_address"` // MAC地址 - HostIDs string `json:"hostIds" gorm:"host_ids"` // 网元主机ID组 数据格式(ssh,telnet,telnet) + HostIDs string `json:"hostIds" gorm:"host_ids"` // 网元主机ID组 数据格式(ssh,telnet) UDM(ssh,telnet,redis) UPF(ssh,telnet,telnet) Status string `json:"status" gorm:"status"` // 0离线 1在线 2配置待下发 Remark string `json:"remark" gorm:"remark"` // 备注 CreateBy string `json:"createBy" gorm:"create_by"` // 创建者 diff --git a/src/modules/network_element/model/ne_license.go b/src/modules/network_element/model/ne_license.go index e2156ff8..0a08802c 100644 --- a/src/modules/network_element/model/ne_license.go +++ b/src/modules/network_element/model/ne_license.go @@ -2,7 +2,7 @@ package model // NeLicense 网元授权激活信息 ne_license type NeLicense struct { - ID string `json:"id" gorm:"column:id;primaryKey;autoIncrement"` + ID string `json:"id" gorm:"id"` NeType string `json:"neType" gorm:"ne_type" binding:"required"` // 网元类型 NeId string `json:"neId" gorm:"ne_id" binding:"required"` // 网元ID ActivationRequestCode string `json:"activationRequestCode" gorm:"activation_request_code"` // 激活申请代码 diff --git a/src/modules/network_element/model/ne_software.go b/src/modules/network_element/model/ne_software.go index 00d10d7c..53a5321d 100644 --- a/src/modules/network_element/model/ne_software.go +++ b/src/modules/network_element/model/ne_software.go @@ -2,7 +2,7 @@ package model // NeSoftware 网元软件包 ne_software type NeSoftware struct { - ID string `json:"id" gorm:"column:id;primaryKey;autoIncrement"` + ID string `json:"id" gorm:"id"` NeType string `json:"neType" gorm:"ne_type" binding:"required"` // 网元类型 Name string `json:"name" gorm:"name" binding:"required"` // 包名称 Path string `json:"path" gorm:"path"` // 包路径 diff --git a/src/modules/network_element/model/ne_version.go b/src/modules/network_element/model/ne_version.go index 49479b16..ddde0af0 100644 --- a/src/modules/network_element/model/ne_version.go +++ b/src/modules/network_element/model/ne_version.go @@ -2,7 +2,7 @@ package model // NeVersion 网元版本信息 ne_version type NeVersion struct { - ID string `json:"id" gorm:"column:id;primaryKey;autoIncrement"` + ID string `json:"id" gorm:"id"` NeType string `json:"neType" gorm:"ne_type" binding:"required"` // 网元类型 NeId string `json:"neId" gorm:"ne_id" binding:"required"` // 网元ID Name string `json:"name" gorm:"name"` // 当前包名 diff --git a/src/modules/network_element/ne_config_test.go b/src/modules/network_element/ne_config_test.go index 1678ddf7..6e80c9fc 100644 --- a/src/modules/network_element/ne_config_test.go +++ b/src/modules/network_element/ne_config_test.go @@ -21,18 +21,18 @@ import ( const ( // 数据库 - DbHost = "192.168.8.58" - DbPort = 33066 + DbHost = "192.168.9.58" + DbPort = 13306 DbUser = "root" DbPassswd = "1000omc@kp!" DbName = "omc_db" // 配置文件路径 configParamDir = "../../../config/param" // configParamFile = "*" // 目录下全部更新 - configParamFile = "upf_param_config.yaml" // 单文件更新 + configParamFile = "smf_param_config.yaml" // 单文件更新 ) -func TestEncrypt(t *testing.T) { +func TestConfig(t *testing.T) { fileNameList, err := getDirFileNameList(configParamDir) if err != nil { log.Fatal(err) @@ -154,9 +154,9 @@ func saveDB(s model.NeConfig) string { s.UpdateTime = time.Now().UnixMilli() if id != "" { s.ID = id - db.Save(s) + db.Save(&s) } else { - db.Create(s) + db.Create(&s) } return s.ID } @@ -217,7 +217,7 @@ func parseParamConfig(data map[string]any) ([]map[string]string, error) { itemMap["paramSort"] = fmt.Sprint(iiv) case "perms", "method": itemMap["paramPerms"] = iiv.(string) - case "data", "list", "array": + case "list", "array": // 参数类型为数组 itemMap["paramType"] = iik strByte, _ := json.Marshal(iiv) itemMap["paramJson"] = string(strByte) diff --git a/src/modules/network_element/network_element.go b/src/modules/network_element/network_element.go index bc068e48..88e28069 100644 --- a/src/modules/network_element/network_element.go +++ b/src/modules/network_element/network_element.go @@ -17,10 +17,8 @@ func Setup(router *gin.Engine) { // 启动时需要的初始参数 InitLoad() - neGroup := router.Group("/ne") - // 网元操作处理 - neActionGroup := neGroup.Group("/action") + neActionGroup := router.Group("/ne/action") { neActionGroup.GET("/files", middleware.PreAuthorize(nil), @@ -35,6 +33,14 @@ func Setup(router *gin.Engine) { collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.neAction", collectlogs.BUSINESS_TYPE_IMPORT)), controller.NewNeAction.PushFile, ) + neActionGroup.GET("/pullDirZip", + middleware.PreAuthorize(nil), + controller.NewNeAction.PullDirZip, + ) + neActionGroup.GET("/viewFile", + middleware.PreAuthorize(nil), + controller.NewNeAction.ViewFile, + ) neActionGroup.PUT("/service", middleware.PreAuthorize(nil), collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.neAction", collectlogs.BUSINESS_TYPE_OTHER)), @@ -43,7 +49,7 @@ func Setup(router *gin.Engine) { } // 网元信息 - neInfoGroup := neGroup.Group("/info") + neInfoGroup := router.Group("/ne/info") { neInfoGroup.GET("/state", middleware.PreAuthorize(nil), @@ -80,15 +86,18 @@ func Setup(router *gin.Engine) { controller.NewNeInfo.List, ) neInfoGroup.GET("/:infoId", + middleware.CryptoApi(false, true), middleware.PreAuthorize(nil), controller.NewNeInfo.Info, ) neInfoGroup.POST("", + middleware.CryptoApi(true, true), middleware.PreAuthorize(nil), collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.neInfo", collectlogs.BUSINESS_TYPE_INSERT)), controller.NewNeInfo.Add, ) neInfoGroup.PUT("", + middleware.CryptoApi(true, true), middleware.PreAuthorize(nil), collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.neInfo", collectlogs.BUSINESS_TYPE_UPDATE)), controller.NewNeInfo.Edit, @@ -101,13 +110,14 @@ func Setup(router *gin.Engine) { } // 网元主机 - neHostGroup := neGroup.Group("/host") + neHostGroup := router.Group("/ne/host") { neHostGroup.GET("/list", middleware.PreAuthorize(nil), controller.NewNeHost.List, ) neHostGroup.GET("/:hostId", + middleware.CryptoApi(false, true), middleware.PreAuthorize(nil), controller.NewNeHost.Info, ) @@ -149,7 +159,7 @@ func Setup(router *gin.Engine) { } // 网元主机命令 - neHostCmdGroup := neGroup.Group("/hostCmd") + neHostCmdGroup := router.Group("/ne/hostCmd") { neHostCmdGroup.GET("/list", middleware.PreAuthorize(nil), @@ -177,7 +187,7 @@ func Setup(router *gin.Engine) { } // 网元版本信息 - neVersionGroup := neGroup.Group("/version") + neVersionGroup := router.Group("/ne/version") { neVersionGroup.GET("/list", middleware.PreAuthorize(nil), @@ -195,7 +205,7 @@ func Setup(router *gin.Engine) { } // 网元软件包信息 - neSoftwareGroup := neGroup.Group("/software") + neSoftwareGroup := router.Group("/ne/software") { neSoftwareGroup.GET("/list", middleware.PreAuthorize(nil), @@ -228,7 +238,7 @@ func Setup(router *gin.Engine) { } // 网元授权激活信息 - neLicenseGroup := neGroup.Group("/license") + neLicenseGroup := router.Group("/ne/license") { neLicenseGroup.GET("/list", middleware.PreAuthorize(nil), @@ -258,7 +268,7 @@ func Setup(router *gin.Engine) { } // 网元参数配置 - neConfigGroup := neGroup.Group("/config") + neConfigGroup := router.Group("/ne/config") { // 网元参数配置可用属性值 neConfigGroup.GET("/list", @@ -311,7 +321,7 @@ func Setup(router *gin.Engine) { } // 网元配置文件备份记录 - neConfigBackupGroup := neGroup.Group("/config/backup") + neConfigBackupGroup := router.Group("/ne/config/backup") { neConfigBackupGroup.GET("/list", middleware.PreAuthorize(nil), @@ -347,13 +357,13 @@ func Setup(router *gin.Engine) { // InitLoad 初始参数 func InitLoad() { // 启动时,清除缓存-网元类型 - service.NewNeInfoImpl.ClearNeCacheByNeType("*") - service.NewNeInfoImpl.SelectNeInfoByRmuid("") + service.NewNeInfo.ClearNeCacheByNeType("*") + service.NewNeInfo.SelectNeInfoByRmuid("") // 启动时,网元公共参数数据记录到全局变量 - if para5GMap, err := service.NewNeInfoImpl.NeConfPara5GRead(); para5GMap != nil && err == nil { - service.NewNeInfoImpl.NeConfPara5GWirte(para5GMap, nil) + if para5GMap, err := service.NewNeInfo.NeConfPara5GRead(); para5GMap != nil && err == nil { + service.NewNeInfo.NeConfPara5GWirte(para5GMap, nil) } // 启动时,清除缓存-网元参数配置可用属性值 - service.NewNeConfigImpl.ClearNeCacheByNeType("*") - service.NewNeConfigImpl.RefreshByNeTypeAndNeID("*") + service.NewNeConfig.ClearNeCacheByNeType("*") + service.NewNeConfig.RefreshByNeTypeAndNeID("*") } diff --git a/src/modules/network_element/repository/ne_config.go b/src/modules/network_element/repository/ne_config.go index 5f555b3e..29ea68f5 100644 --- a/src/modules/network_element/repository/ne_config.go +++ b/src/modules/network_element/repository/ne_config.go @@ -1,24 +1,259 @@ package repository -import "be.ems/src/modules/network_element/model" +import ( + "strings" + "time" -// INeConfig 网元参数配置可用属性值 数据层接口 -type INeConfig interface { - // SelectPage 根据条件分页查询字典类型 - SelectPage(query map[string]any) map[string]any + "be.ems/src/framework/datasource" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/parse" + "be.ems/src/framework/utils/repo" + "be.ems/src/modules/network_element/model" +) - // SelectList 根据实体查询 - SelectList(param model.NeConfig) []model.NeConfig +// 实例化数据层 NeConfig 结构体 +var NewNeConfig = &NeConfig{ + selectSql: `select id, ne_type, param_name, param_display, param_type, param_json, param_sort, param_perms, update_time from ne_config`, - // SelectByIds 通过ID查询 - SelectByIds(ids []string) []model.NeConfig - - // Insert 新增信息 - Insert(param model.NeConfig) string - - // Update 修改信息 - Update(param model.NeConfig) int64 - - // DeleteByIds 批量删除信息 - DeleteByIds(ids []string) int64 + resultMap: map[string]string{ + "id": "ID", + "ne_type": "NeType", + "param_name": "ParamName", + "param_display": "ParamDisplay", + "param_type": "ParamType", + "param_json": "ParamJson", + "param_sort": "ParamSort", + "param_perms": "ParamPerms", + "update_time": "UpdateTime", + }, +} + +// NeConfig 网元参数配置可用属性值 数据层处理 +type NeConfig struct { + // 查询视图对象SQL + selectSql string + // 结果字段与实体映射 + resultMap map[string]string +} + +// convertResultRows 将结果记录转实体结果组 +func (r *NeConfig) convertResultRows(rows []map[string]any) []model.NeConfig { + arr := make([]model.NeConfig, 0) + for _, row := range rows { + item := model.NeConfig{} + for key, value := range row { + if keyMapper, ok := r.resultMap[key]; ok { + repo.SetFieldValue(&item, keyMapper, value) + } + } + arr = append(arr, item) + } + return arr +} + +// SelectPage 根据条件分页查询字典类型 +func (r *NeConfig) SelectPage(query map[string]any) map[string]any { + // 查询条件拼接 + var conditions []string + var params []any + if v, ok := query["neType"]; ok && v != "" { + conditions = append(conditions, "ne_type = ?") + params = append(params, v) + } + if v, ok := query["paramName"]; ok && v != "" { + conditions = append(conditions, "param_name = ?") + params = append(params, v) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + result := map[string]any{ + "total": 0, + "rows": []model.NeHost{}, + } + + // 查询数量 长度为0直接返回 + totalSql := "select count(id) as 'total' from ne_config" + totalRows, err := datasource.RawDB("", totalSql+whereSql, params) + if err != nil { + logger.Errorf("total err => %v", err) + return result + } + total := parse.Number(totalRows[0]["total"]) + if total == 0 { + return result + } else { + result["total"] = total + } + + // 分页 + pageNum, pageSize := repo.PageNumSize(query["pageNum"], query["pageSize"]) + pageSql := " limit ?,? " + params = append(params, pageNum*pageSize) + params = append(params, pageSize) + + // 查询数据 + querySql := r.selectSql + whereSql + pageSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + return result + } + + // 转换实体 + result["rows"] = r.convertResultRows(results) + return result +} + +// SelectList 根据实体查询 +func (r *NeConfig) SelectList(param model.NeConfig) []model.NeConfig { + // 查询条件拼接 + var conditions []string + var params []any + if param.NeType != "" { + conditions = append(conditions, "ne_type = ?") + params = append(params, param.NeType) + } + if param.ParamName != "" { + conditions = append(conditions, "param_name = ?") + params = append(params, param.ParamName) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询数据 + querySql := r.selectSql + whereSql + " order by param_sort asc " + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + } + + // 转换实体 + return r.convertResultRows(results) +} + +// SelectByIds 通过ID查询 +func (r *NeConfig) SelectByIds(ids []string) []model.NeConfig { + placeholder := repo.KeyPlaceholderByQuery(len(ids)) + querySql := r.selectSql + " where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(ids) + results, err := datasource.RawDB("", querySql, parameters) + if err != nil { + logger.Errorf("query err => %v", err) + return []model.NeConfig{} + } + // 转换实体 + return r.convertResultRows(results) +} + +// Insert 新增信息 +func (r *NeConfig) Insert(param model.NeConfig) string { + // 参数拼接 + params := make(map[string]any) + if param.NeType != "" { + params["ne_type"] = param.NeType + } + if param.ParamName != "" { + params["param_name"] = param.ParamName + } + if param.ParamDisplay != "" { + params["param_display"] = param.ParamDisplay + } + if param.ParamType != "" { + params["param_type"] = param.ParamType + } + if param.ParamJson != "" { + params["param_json"] = param.ParamJson + } + params["param_sort"] = param.ParamSort + if param.ParamPerms != "" { + params["param_perms"] = param.ParamPerms + } + params["update_time"] = time.Now().UnixMilli() + + // 构建执行语句 + keys, placeholder, values := repo.KeyPlaceholderValueByInsert(params) + sql := "insert into ne_config (" + strings.Join(keys, ",") + ")values(" + placeholder + ")" + + db := datasource.DefaultDB() + // 开启事务 + tx := db.Begin() + // 执行插入 + err := tx.Exec(sql, values...).Error + if err != nil { + logger.Errorf("insert row : %v", err.Error()) + tx.Rollback() + return "" + } + // 获取生成的自增 ID + var insertedID string + err = tx.Raw("select last_insert_id()").Row().Scan(&insertedID) + if err != nil { + logger.Errorf("insert last id : %v", err.Error()) + tx.Rollback() + return "" + } + // 提交事务 + tx.Commit() + return insertedID +} + +// Update 修改信息 +func (r *NeConfig) Update(param model.NeConfig) int64 { + // 参数拼接 + params := make(map[string]any) + if param.NeType != "" { + params["ne_type"] = param.NeType + } + if param.ParamName != "" { + params["param_name"] = param.ParamName + } + if param.ParamDisplay != "" { + params["param_display"] = param.ParamDisplay + } + if param.ParamType != "" { + params["param_type"] = param.ParamType + } + if param.ParamJson != "" { + params["param_json"] = param.ParamJson + } + params["param_sort"] = param.ParamSort + if param.ParamPerms != "" { + params["param_perms"] = param.ParamPerms + } + params["update_time"] = time.Now().UnixMilli() + + // 构建执行语句 + keys, values := repo.KeyValueByUpdate(params) + sql := "update ne_config set " + strings.Join(keys, ",") + " where id = ?" + + // 执行更新 + values = append(values, param.ID) + rows, err := datasource.ExecDB("", sql, values) + if err != nil { + logger.Errorf("update row : %v", err.Error()) + return 0 + } + return rows +} + +// DeleteByIds 批量删除信息 +func (r *NeConfig) DeleteByIds(ids []string) int64 { + placeholder := repo.KeyPlaceholderByQuery(len(ids)) + sql := "delete from ne_config where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(ids) + results, err := datasource.ExecDB("", sql, parameters) + if err != nil { + logger.Errorf("delete err => %v", err) + return 0 + } + return results } diff --git a/src/modules/network_element/repository/ne_config.impl.go b/src/modules/network_element/repository/ne_config.impl.go deleted file mode 100644 index d7a845ef..00000000 --- a/src/modules/network_element/repository/ne_config.impl.go +++ /dev/null @@ -1,259 +0,0 @@ -package repository - -import ( - "strings" - "time" - - "be.ems/src/framework/datasource" - "be.ems/src/framework/logger" - "be.ems/src/framework/utils/parse" - "be.ems/src/framework/utils/repo" - "be.ems/src/modules/network_element/model" -) - -// NewNeConfigImpl 网元参数配置可用属性值 实例化数据层 -var NewNeConfigImpl = &NeConfigImpl{ - selectSql: `select id, ne_type, param_name, param_display, param_type, param_json, param_sort, param_perms, update_time from ne_config`, - - resultMap: map[string]string{ - "id": "ID", - "ne_type": "NeType", - "param_name": "ParamName", - "param_display": "ParamDisplay", - "param_type": "ParamType", - "param_json": "ParamJson", - "param_sort": "ParamSort", - "param_perms": "ParamPerms", - "update_time": "UpdateTime", - }, -} - -// NeConfigImpl 网元参数配置可用属性值 数据层处理 -type NeConfigImpl struct { - // 查询视图对象SQL - selectSql string - // 结果字段与实体映射 - resultMap map[string]string -} - -// convertResultRows 将结果记录转实体结果组 -func (r *NeConfigImpl) convertResultRows(rows []map[string]any) []model.NeConfig { - arr := make([]model.NeConfig, 0) - for _, row := range rows { - item := model.NeConfig{} - for key, value := range row { - if keyMapper, ok := r.resultMap[key]; ok { - repo.SetFieldValue(&item, keyMapper, value) - } - } - arr = append(arr, item) - } - return arr -} - -// SelectPage 根据条件分页查询字典类型 -func (r *NeConfigImpl) SelectPage(query map[string]any) map[string]any { - // 查询条件拼接 - var conditions []string - var params []any - if v, ok := query["neType"]; ok && v != "" { - conditions = append(conditions, "ne_type = ?") - params = append(params, v) - } - if v, ok := query["paramName"]; ok && v != "" { - conditions = append(conditions, "param_name = ?") - params = append(params, v) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } - - result := map[string]any{ - "total": 0, - "rows": []model.NeHost{}, - } - - // 查询数量 长度为0直接返回 - totalSql := "select count(id) as 'total' from ne_config" - totalRows, err := datasource.RawDB("", totalSql+whereSql, params) - if err != nil { - logger.Errorf("total err => %v", err) - return result - } - total := parse.Number(totalRows[0]["total"]) - if total == 0 { - return result - } else { - result["total"] = total - } - - // 分页 - pageNum, pageSize := repo.PageNumSize(query["pageNum"], query["pageSize"]) - pageSql := " limit ?,? " - params = append(params, pageNum*pageSize) - params = append(params, pageSize) - - // 查询数据 - querySql := r.selectSql + whereSql + pageSql - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err => %v", err) - return result - } - - // 转换实体 - result["rows"] = r.convertResultRows(results) - return result -} - -// SelectList 根据实体查询 -func (r *NeConfigImpl) SelectList(param model.NeConfig) []model.NeConfig { - // 查询条件拼接 - var conditions []string - var params []any - if param.NeType != "" { - conditions = append(conditions, "ne_type = ?") - params = append(params, param.NeType) - } - if param.ParamName != "" { - conditions = append(conditions, "param_name = ?") - params = append(params, param.ParamName) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } - - // 查询数据 - querySql := r.selectSql + whereSql + " order by param_sort asc " - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err => %v", err) - } - - // 转换实体 - return r.convertResultRows(results) -} - -// SelectByIds 通过ID查询 -func (r *NeConfigImpl) SelectByIds(ids []string) []model.NeConfig { - placeholder := repo.KeyPlaceholderByQuery(len(ids)) - querySql := r.selectSql + " where id in (" + placeholder + ")" - parameters := repo.ConvertIdsSlice(ids) - results, err := datasource.RawDB("", querySql, parameters) - if err != nil { - logger.Errorf("query err => %v", err) - return []model.NeConfig{} - } - // 转换实体 - return r.convertResultRows(results) -} - -// Insert 新增信息 -func (r *NeConfigImpl) Insert(param model.NeConfig) string { - // 参数拼接 - params := make(map[string]any) - if param.NeType != "" { - params["ne_type"] = param.NeType - } - if param.ParamName != "" { - params["param_name"] = param.ParamName - } - if param.ParamDisplay != "" { - params["param_display"] = param.ParamDisplay - } - if param.ParamType != "" { - params["param_type"] = param.ParamType - } - if param.ParamJson != "" { - params["param_json"] = param.ParamJson - } - params["param_sort"] = param.ParamSort - if param.ParamPerms != "" { - params["param_perms"] = param.ParamPerms - } - params["update_time"] = time.Now().UnixMilli() - - // 构建执行语句 - keys, placeholder, values := repo.KeyPlaceholderValueByInsert(params) - sql := "insert into ne_config (" + strings.Join(keys, ",") + ")values(" + placeholder + ")" - - db := datasource.DefaultDB() - // 开启事务 - tx := db.Begin() - // 执行插入 - err := tx.Exec(sql, values...).Error - if err != nil { - logger.Errorf("insert row : %v", err.Error()) - tx.Rollback() - return "" - } - // 获取生成的自增 ID - var insertedID string - err = tx.Raw("select last_insert_id()").Row().Scan(&insertedID) - if err != nil { - logger.Errorf("insert last id : %v", err.Error()) - tx.Rollback() - return "" - } - // 提交事务 - tx.Commit() - return insertedID -} - -// Update 修改信息 -func (r *NeConfigImpl) Update(param model.NeConfig) int64 { - // 参数拼接 - params := make(map[string]any) - if param.NeType != "" { - params["ne_type"] = param.NeType - } - if param.ParamName != "" { - params["param_name"] = param.ParamName - } - if param.ParamDisplay != "" { - params["param_display"] = param.ParamDisplay - } - if param.ParamType != "" { - params["param_type"] = param.ParamType - } - if param.ParamJson != "" { - params["param_json"] = param.ParamJson - } - params["param_sort"] = param.ParamSort - if param.ParamPerms != "" { - params["param_perms"] = param.ParamPerms - } - params["update_time"] = time.Now().UnixMilli() - - // 构建执行语句 - keys, values := repo.KeyValueByUpdate(params) - sql := "update ne_config set " + strings.Join(keys, ",") + " where id = ?" - - // 执行更新 - values = append(values, param.ID) - rows, err := datasource.ExecDB("", sql, values) - if err != nil { - logger.Errorf("update row : %v", err.Error()) - return 0 - } - return rows -} - -// DeleteByIds 批量删除信息 -func (r *NeConfigImpl) DeleteByIds(ids []string) int64 { - placeholder := repo.KeyPlaceholderByQuery(len(ids)) - sql := "delete from ne_config where id in (" + placeholder + ")" - parameters := repo.ConvertIdsSlice(ids) - results, err := datasource.ExecDB("", sql, parameters) - if err != nil { - logger.Errorf("delete err => %v", err) - return 0 - } - return results -} diff --git a/src/modules/network_element/repository/ne_config_backup.go b/src/modules/network_element/repository/ne_config_backup.go index 131f363d..3354034d 100644 --- a/src/modules/network_element/repository/ne_config_backup.go +++ b/src/modules/network_element/repository/ne_config_backup.go @@ -1,24 +1,262 @@ package repository -import "be.ems/src/modules/network_element/model" +import ( + "strings" + "time" -// INeConfigBackup 网元配置文件备份记录 数据层接口 -type INeConfigBackup interface { - // SelectPage 根据条件分页查询字典类型 - SelectPage(query map[string]any) map[string]any + "be.ems/src/framework/datasource" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/parse" + "be.ems/src/framework/utils/repo" + "be.ems/src/modules/network_element/model" +) - // SelectList 根据实体查询 - SelectList(item model.NeConfigBackup) []model.NeConfigBackup +// 实例化数据层 NeConfigBackup 结构体 +var NewNeConfigBackup = &NeConfigBackup{ + selectSql: `select + id, ne_type, ne_id, name, path, remark, create_by, create_time, update_by, update_time + from ne_config_backup`, - // SelectByIds 通过ID查询 - SelectByIds(ids []string) []model.NeConfigBackup - - // Insert 新增信息 - Insert(item model.NeConfigBackup) string - - // Update 修改信息 - Update(item model.NeConfigBackup) int64 - - // DeleteByIds 批量删除信息 - DeleteByIds(ids []string) int64 + resultMap: map[string]string{ + "id": "ID", + "ne_type": "NeType", + "ne_id": "NeId", + "name": "Name", + "path": "Path", + "remark": "Remark", + "create_by": "CreateBy", + "create_time": "CreateTime", + "update_by": "UpdateBy", + "update_time": "UpdateTime", + }, +} + +// NeConfigBackup 网元配置文件备份记录 数据层处理 +type NeConfigBackup struct { + // 查询视图对象SQL + selectSql string + // 结果字段与实体映射 + resultMap map[string]string +} + +// convertResultRows 将结果记录转实体结果组 +func (r *NeConfigBackup) convertResultRows(rows []map[string]any) []model.NeConfigBackup { + arr := make([]model.NeConfigBackup, 0) + for _, row := range rows { + item := model.NeConfigBackup{} + for key, value := range row { + if keyMapper, ok := r.resultMap[key]; ok { + repo.SetFieldValue(&item, keyMapper, value) + } + } + arr = append(arr, item) + } + return arr +} + +// SelectPage 根据条件分页查询字典类型 +func (r *NeConfigBackup) SelectPage(query map[string]any) map[string]any { + // 查询条件拼接 + var conditions []string + var params []any + if v, ok := query["neType"]; ok && v != "" { + conditions = append(conditions, "ne_type = ?") + params = append(params, strings.Trim(v.(string), " ")) + } + if v, ok := query["neId"]; ok && v != "" { + conditions = append(conditions, "ne_id = ?") + params = append(params, strings.Trim(v.(string), " ")) + } + if v, ok := query["name"]; ok && v != "" { + conditions = append(conditions, "name like concat(concat('%', ?), '%')") + params = append(params, strings.Trim(v.(string), " ")) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + result := map[string]any{ + "total": 0, + "rows": []model.NeHost{}, + } + + // 查询数量 长度为0直接返回 + totalSql := "select count(1) as 'total' from ne_config_backup" + totalRows, err := datasource.RawDB("", totalSql+whereSql, params) + if err != nil { + logger.Errorf("total err => %v", err) + return result + } + total := parse.Number(totalRows[0]["total"]) + if total == 0 { + return result + } else { + result["total"] = total + } + + // 分页 + pageNum, pageSize := repo.PageNumSize(query["pageNum"], query["pageSize"]) + pageSql := " order by id desc limit ?,? " + params = append(params, pageNum*pageSize) + params = append(params, pageSize) + + // 查询数据 + querySql := r.selectSql + whereSql + pageSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + return result + } + + // 转换实体 + result["rows"] = r.convertResultRows(results) + return result +} + +// SelectList 根据实体查询 +func (r *NeConfigBackup) SelectList(item model.NeConfigBackup) []model.NeConfigBackup { + // 查询条件拼接 + var conditions []string + var params []any + if item.NeType != "" { + conditions = append(conditions, "ne_type = ?") + params = append(params, item.NeType) + } + if item.NeId != "" { + conditions = append(conditions, "ne_id = ?") + params = append(params, item.NeId) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询数据 + querySql := r.selectSql + whereSql + " order by id desc " + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + } + + // 转换实体 + return r.convertResultRows(results) +} + +// SelectByIds 通过ID查询 +func (r *NeConfigBackup) SelectByIds(cmdIds []string) []model.NeConfigBackup { + placeholder := repo.KeyPlaceholderByQuery(len(cmdIds)) + querySql := r.selectSql + " where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(cmdIds) + results, err := datasource.RawDB("", querySql, parameters) + if err != nil { + logger.Errorf("query err => %v", err) + return []model.NeConfigBackup{} + } + // 转换实体 + return r.convertResultRows(results) +} + +// Insert 新增信息 +func (r *NeConfigBackup) Insert(item model.NeConfigBackup) string { + // 参数拼接 + params := make(map[string]any) + if item.NeType != "" { + params["ne_type"] = item.NeType + } + if item.NeId != "" { + params["ne_id"] = item.NeId + } + if item.Name != "" { + params["name"] = item.Name + } + if item.Path != "" { + params["path"] = item.Path + } + if item.Remark != "" { + params["remark"] = item.Remark + } + if item.CreateBy != "" { + params["create_by"] = item.CreateBy + params["create_time"] = time.Now().UnixMilli() + } + + // 构建执行语句 + keys, placeholder, values := repo.KeyPlaceholderValueByInsert(params) + sql := "insert into ne_config_backup (" + strings.Join(keys, ",") + ")values(" + placeholder + ")" + + db := datasource.DefaultDB() + // 开启事务 + tx := db.Begin() + // 执行插入 + err := tx.Exec(sql, values...).Error + if err != nil { + logger.Errorf("insert row : %v", err.Error()) + tx.Rollback() + return "" + } + // 获取生成的自增 ID + var insertedID string + err = tx.Raw("select last_insert_id()").Row().Scan(&insertedID) + if err != nil { + logger.Errorf("insert last id : %v", err.Error()) + tx.Rollback() + return "" + } + // 提交事务 + tx.Commit() + return insertedID +} + +// Update 修改信息 +func (r *NeConfigBackup) Update(item model.NeConfigBackup) int64 { + // 参数拼接 + params := make(map[string]any) + if item.NeType != "" { + params["ne_type"] = item.NeType + } + if item.NeId != "" { + params["ne_id"] = item.NeId + } + if item.Name != "" { + params["name"] = item.Name + } + if item.Path != "" { + params["path"] = item.Path + } + params["remark"] = item.Remark + if item.UpdateBy != "" { + params["update_by"] = item.UpdateBy + params["update_time"] = time.Now().UnixMilli() + } + + // 构建执行语句 + keys, values := repo.KeyValueByUpdate(params) + sql := "update ne_config_backup set " + strings.Join(keys, ",") + " where id = ?" + + // 执行更新 + values = append(values, item.ID) + rows, err := datasource.ExecDB("", sql, values) + if err != nil { + logger.Errorf("update row : %v", err.Error()) + return 0 + } + return rows +} + +// DeleteByIds 批量删除信息 +func (r *NeConfigBackup) DeleteByIds(ids []string) int64 { + placeholder := repo.KeyPlaceholderByQuery(len(ids)) + sql := "delete from ne_config_backup where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(ids) + results, err := datasource.ExecDB("", sql, parameters) + if err != nil { + logger.Errorf("delete err => %v", err) + return 0 + } + return results } diff --git a/src/modules/network_element/repository/ne_config_backup.impl.go b/src/modules/network_element/repository/ne_config_backup.impl.go deleted file mode 100644 index 17331822..00000000 --- a/src/modules/network_element/repository/ne_config_backup.impl.go +++ /dev/null @@ -1,262 +0,0 @@ -package repository - -import ( - "strings" - "time" - - "be.ems/src/framework/datasource" - "be.ems/src/framework/logger" - "be.ems/src/framework/utils/parse" - "be.ems/src/framework/utils/repo" - "be.ems/src/modules/network_element/model" -) - -// 实例化数据层 NewNeConfigBackupImpl 结构体 -var NewNeConfigBackupImpl = &NeConfigBackupImpl{ - selectSql: `select - id, ne_type, ne_id, name, path, remark, create_by, create_time, update_by, update_time - from ne_config_backup`, - - resultMap: map[string]string{ - "id": "ID", - "ne_type": "NeType", - "ne_id": "NeId", - "name": "Name", - "path": "Path", - "remark": "Remark", - "create_by": "CreateBy", - "create_time": "CreateTime", - "update_by": "UpdateBy", - "update_time": "UpdateTime", - }, -} - -// NeConfigBackupImpl 网元配置文件备份记录 数据层处理 -type NeConfigBackupImpl struct { - // 查询视图对象SQL - selectSql string - // 结果字段与实体映射 - resultMap map[string]string -} - -// convertResultRows 将结果记录转实体结果组 -func (r *NeConfigBackupImpl) convertResultRows(rows []map[string]any) []model.NeConfigBackup { - arr := make([]model.NeConfigBackup, 0) - for _, row := range rows { - item := model.NeConfigBackup{} - for key, value := range row { - if keyMapper, ok := r.resultMap[key]; ok { - repo.SetFieldValue(&item, keyMapper, value) - } - } - arr = append(arr, item) - } - return arr -} - -// SelectPage 根据条件分页查询字典类型 -func (r *NeConfigBackupImpl) SelectPage(query map[string]any) map[string]any { - // 查询条件拼接 - var conditions []string - var params []any - if v, ok := query["neType"]; ok && v != "" { - conditions = append(conditions, "ne_type = ?") - params = append(params, strings.Trim(v.(string), " ")) - } - if v, ok := query["neId"]; ok && v != "" { - conditions = append(conditions, "ne_id = ?") - params = append(params, strings.Trim(v.(string), " ")) - } - if v, ok := query["name"]; ok && v != "" { - conditions = append(conditions, "name like concat(concat('%', ?), '%')") - params = append(params, strings.Trim(v.(string), " ")) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } - - result := map[string]any{ - "total": 0, - "rows": []model.NeHost{}, - } - - // 查询数量 长度为0直接返回 - totalSql := "select count(1) as 'total' from ne_config_backup" - totalRows, err := datasource.RawDB("", totalSql+whereSql, params) - if err != nil { - logger.Errorf("total err => %v", err) - return result - } - total := parse.Number(totalRows[0]["total"]) - if total == 0 { - return result - } else { - result["total"] = total - } - - // 分页 - pageNum, pageSize := repo.PageNumSize(query["pageNum"], query["pageSize"]) - pageSql := " order by id desc limit ?,? " - params = append(params, pageNum*pageSize) - params = append(params, pageSize) - - // 查询数据 - querySql := r.selectSql + whereSql + pageSql - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err => %v", err) - return result - } - - // 转换实体 - result["rows"] = r.convertResultRows(results) - return result -} - -// SelectList 根据实体查询 -func (r *NeConfigBackupImpl) SelectList(item model.NeConfigBackup) []model.NeConfigBackup { - // 查询条件拼接 - var conditions []string - var params []any - if item.NeType != "" { - conditions = append(conditions, "ne_type = ?") - params = append(params, item.NeType) - } - if item.NeId != "" { - conditions = append(conditions, "ne_id = ?") - params = append(params, item.NeId) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } - - // 查询数据 - querySql := r.selectSql + whereSql + " order by id desc " - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err => %v", err) - } - - // 转换实体 - return r.convertResultRows(results) -} - -// SelectByIds 通过ID查询 -func (r *NeConfigBackupImpl) SelectByIds(cmdIds []string) []model.NeConfigBackup { - placeholder := repo.KeyPlaceholderByQuery(len(cmdIds)) - querySql := r.selectSql + " where id in (" + placeholder + ")" - parameters := repo.ConvertIdsSlice(cmdIds) - results, err := datasource.RawDB("", querySql, parameters) - if err != nil { - logger.Errorf("query err => %v", err) - return []model.NeConfigBackup{} - } - // 转换实体 - return r.convertResultRows(results) -} - -// Insert 新增信息 -func (r *NeConfigBackupImpl) Insert(item model.NeConfigBackup) string { - // 参数拼接 - params := make(map[string]any) - if item.NeType != "" { - params["ne_type"] = item.NeType - } - if item.NeId != "" { - params["ne_id"] = item.NeId - } - if item.Name != "" { - params["name"] = item.Name - } - if item.Path != "" { - params["path"] = item.Path - } - if item.Remark != "" { - params["remark"] = item.Remark - } - if item.CreateBy != "" { - params["create_by"] = item.CreateBy - params["create_time"] = time.Now().UnixMilli() - } - - // 构建执行语句 - keys, placeholder, values := repo.KeyPlaceholderValueByInsert(params) - sql := "insert into ne_config_backup (" + strings.Join(keys, ",") + ")values(" + placeholder + ")" - - db := datasource.DefaultDB() - // 开启事务 - tx := db.Begin() - // 执行插入 - err := tx.Exec(sql, values...).Error - if err != nil { - logger.Errorf("insert row : %v", err.Error()) - tx.Rollback() - return "" - } - // 获取生成的自增 ID - var insertedID string - err = tx.Raw("select last_insert_id()").Row().Scan(&insertedID) - if err != nil { - logger.Errorf("insert last id : %v", err.Error()) - tx.Rollback() - return "" - } - // 提交事务 - tx.Commit() - return insertedID -} - -// Update 修改信息 -func (r *NeConfigBackupImpl) Update(item model.NeConfigBackup) int64 { - // 参数拼接 - params := make(map[string]any) - if item.NeType != "" { - params["ne_type"] = item.NeType - } - if item.NeId != "" { - params["ne_id"] = item.NeId - } - if item.Name != "" { - params["name"] = item.Name - } - if item.Path != "" { - params["path"] = item.Path - } - params["remark"] = item.Remark - if item.UpdateBy != "" { - params["update_by"] = item.UpdateBy - params["update_time"] = time.Now().UnixMilli() - } - - // 构建执行语句 - keys, values := repo.KeyValueByUpdate(params) - sql := "update ne_config_backup set " + strings.Join(keys, ",") + " where id = ?" - - // 执行更新 - values = append(values, item.ID) - rows, err := datasource.ExecDB("", sql, values) - if err != nil { - logger.Errorf("update row : %v", err.Error()) - return 0 - } - return rows -} - -// DeleteByIds 批量删除信息 -func (r *NeConfigBackupImpl) DeleteByIds(ids []string) int64 { - placeholder := repo.KeyPlaceholderByQuery(len(ids)) - sql := "delete from ne_config_backup where id in (" + placeholder + ")" - parameters := repo.ConvertIdsSlice(ids) - results, err := datasource.ExecDB("", sql, parameters) - if err != nil { - logger.Errorf("delete err => %v", err) - return 0 - } - return results -} diff --git a/src/modules/network_element/repository/ne_host.go b/src/modules/network_element/repository/ne_host.go index a61fb968..70794663 100644 --- a/src/modules/network_element/repository/ne_host.go +++ b/src/modules/network_element/repository/ne_host.go @@ -1,27 +1,403 @@ package repository -import "be.ems/src/modules/network_element/model" +import ( + "fmt" + "strings" + "time" -// INeHost 网元主机连接 数据层接口 -type INeHost interface { - // SelectPage 根据条件分页查询字典类型 - SelectPage(query map[string]any) map[string]any + "be.ems/src/framework/datasource" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/parse" + "be.ems/src/framework/utils/repo" + "be.ems/src/modules/network_element/model" +) - // SelectList 根据实体查询 - SelectList(neHost model.NeHost) []model.NeHost +// 实例化数据层 NeHost 结构体 +var NewNeHost = &NeHost{ + selectSql: `select + host_id, host_type, group_id, title, addr, port, user, auth_mode, password, private_key, pass_phrase, db_name, remark, create_by, create_time, update_by, update_time + from ne_host`, - // SelectByIds 通过ID查询 - SelectByIds(hostIds []string) []model.NeHost - - // Insert 新增信息 - Insert(neHost model.NeHost) string - - // Update 修改信息 - Update(neHost model.NeHost) int64 - - // DeleteByIds 批量删除网元主机连接信息 - DeleteByIds(hostIds []string) int64 - - // CheckUniqueNeHost 校验主机是否唯一 - CheckUniqueNeHost(neHost model.NeHost) string + resultMap: map[string]string{ + "host_id": "HostID", + "host_type": "HostType", + "group_id": "GroupID", + "title": "Title", + "addr": "Addr", + "port": "Port", + "user": "User", + "auth_mode": "AuthMode", + "password": "Password", + "private_key": "PrivateKey", + "private_password": "PassPhrase", + "db_name": "DBName", + "remark": "Remark", + "create_by": "CreateBy", + "create_time": "CreateTime", + "update_by": "UpdateBy", + "update_time": "UpdateTime", + }, +} + +// NeHost 网元主机连接 数据层处理 +type NeHost struct { + // 查询视图对象SQL + selectSql string + // 结果字段与实体映射 + resultMap map[string]string +} + +// convertResultRows 将结果记录转实体结果组 +func (r *NeHost) convertResultRows(rows []map[string]any) []model.NeHost { + arr := make([]model.NeHost, 0) + for _, row := range rows { + item := model.NeHost{} + for key, value := range row { + if keyMapper, ok := r.resultMap[key]; ok { + repo.SetFieldValue(&item, keyMapper, value) + } + } + arr = append(arr, item) + } + return arr +} + +// SelectPage 根据条件分页查询字典类型 +func (r *NeHost) SelectPage(query map[string]any) map[string]any { + // 查询条件拼接 + var conditions []string + var params []any + if v, ok := query["hostType"]; ok && v != "" { + conditions = append(conditions, "host_type = ?") + params = append(params, strings.Trim(v.(string), " ")) + } + if v, ok := query["groupId"]; ok && v != "" { + conditions = append(conditions, "group_id = ?") + params = append(params, strings.Trim(v.(string), " ")) + } + if v, ok := query["title"]; ok && v != "" { + conditions = append(conditions, "title like concat(?, '%')") + params = append(params, strings.Trim(v.(string), " ")) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + result := map[string]any{ + "total": 0, + "rows": []model.NeHost{}, + } + + // 查询数量 长度为0直接返回 + totalSql := "select count(1) as 'total' from ne_host" + totalRows, err := datasource.RawDB("", totalSql+whereSql, params) + if err != nil { + logger.Errorf("total err => %v", err) + return result + } + total := parse.Number(totalRows[0]["total"]) + if total == 0 { + return result + } else { + result["total"] = total + } + + // 分页 + pageNum, pageSize := repo.PageNumSize(query["pageNum"], query["pageSize"]) + pageSql := " limit ?,? " + params = append(params, pageNum*pageSize) + params = append(params, pageSize) + + // 排序 + orderSql := "" + if sv, ok := query["sortField"]; ok && sv != "" { + sortSql := fmt.Sprint(sv) + if sortSql == "updateTime" { + sortSql = "update_time" + } + if sortSql == "createTime" { + sortSql = "create_time" + } + if ov, ok := query["sortOrder"]; ok && ov != "" { + if fmt.Sprint(ov) == "desc" { + sortSql += " desc " + } else { + sortSql += " asc " + } + } + orderSql = fmt.Sprintf(" order by %s ", sortSql) + } + + // 查询数据 + querySql := r.selectSql + whereSql + orderSql + pageSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + return result + } + + // 转换实体 + result["rows"] = r.convertResultRows(results) + return result +} + +// SelectList 根据实体查询 +func (r *NeHost) SelectList(neHost model.NeHost) []model.NeHost { + // 查询条件拼接 + var conditions []string + var params []any + if neHost.HostType != "" { + conditions = append(conditions, "host_type = ?") + params = append(params, neHost.HostType) + } + if neHost.GroupID != "" { + conditions = append(conditions, "group_id = ?") + params = append(params, neHost.GroupID) + } + if neHost.Title != "" { + conditions = append(conditions, "title like concat(?, '%')") + params = append(params, neHost.Title) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询数据 + querySql := r.selectSql + whereSql + " order by update_time asc " + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + } + + // 转换实体 + return r.convertResultRows(results) +} + +// SelectByIds 通过ID查询 +func (r *NeHost) SelectByIds(hostIds []string) []model.NeHost { + placeholder := repo.KeyPlaceholderByQuery(len(hostIds)) + querySql := r.selectSql + " where host_id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(hostIds) + results, err := datasource.RawDB("", querySql, parameters) + if err != nil { + logger.Errorf("query err => %v", err) + return []model.NeHost{} + } + // 转换实体 + return r.convertResultRows(results) +} + +// CheckUniqueNeHost 校验主机是否唯一 +func (r *NeHost) CheckUniqueNeHost(neHost model.NeHost) string { + // 查询条件拼接 + var conditions []string + var params []any + if neHost.HostType != "" { + conditions = append(conditions, "host_type = ?") + params = append(params, neHost.HostType) + } + if neHost.GroupID != "" { + conditions = append(conditions, "group_id = ?") + params = append(params, neHost.GroupID) + } + if neHost.Title != "" { + conditions = append(conditions, "title = ?") + params = append(params, neHost.Title) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } else { + return "" + } + + // 查询数据 + querySql := "select host_id as 'str' from ne_host " + whereSql + " limit 1" + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err %v", err) + return "" + } + if len(results) > 0 { + return fmt.Sprint(results[0]["str"]) + } + return "" +} + +// Insert 新增信息 +func (r *NeHost) Insert(neHost model.NeHost) string { + // 参数拼接 + params := make(map[string]any) + if neHost.HostType != "" { + params["host_type"] = neHost.HostType + } + if neHost.GroupID != "" { + params["group_id"] = neHost.GroupID + } + if neHost.Title != "" { + params["title"] = neHost.Title + } + if neHost.Addr != "" { + params["addr"] = neHost.Addr + } + if neHost.Port > 0 { + params["port"] = neHost.Port + } + if neHost.User != "" { + params["user"] = neHost.User + } + if neHost.AuthMode != "" { + params["auth_mode"] = neHost.AuthMode + } + if neHost.Password != "" { + params["password"] = neHost.Password + } + if neHost.PrivateKey != "" { + params["private_key"] = neHost.PrivateKey + } + if neHost.PassPhrase != "" { + params["pass_phrase"] = neHost.PassPhrase + } + if neHost.DBName != "" { + params["db_name"] = neHost.DBName + } + if neHost.Remark != "" { + params["remark"] = neHost.Remark + } + if neHost.CreateBy != "" { + params["create_by"] = neHost.CreateBy + params["create_time"] = time.Now().UnixMilli() + } + + // 根据认证模式清除不必要的信息 + if neHost.AuthMode == "0" { + params["private_key"] = "" + params["pass_phrase"] = "" + } + if neHost.AuthMode == "1" { + params["password"] = "" + } + if neHost.AuthMode == "2" { + params["password"] = "" + params["private_key"] = "" + params["pass_phrase"] = "" + } + + // 构建执行语句 + keys, placeholder, values := repo.KeyPlaceholderValueByInsert(params) + sql := "insert into ne_host (" + strings.Join(keys, ",") + ")values(" + placeholder + ")" + + db := datasource.DefaultDB() + // 开启事务 + tx := db.Begin() + // 执行插入 + err := tx.Exec(sql, values...).Error + if err != nil { + logger.Errorf("insert row : %v", err.Error()) + tx.Rollback() + return "" + } + // 获取生成的自增 ID + var insertedID string + err = tx.Raw("select last_insert_id()").Row().Scan(&insertedID) + if err != nil { + logger.Errorf("insert last id : %v", err.Error()) + tx.Rollback() + return "" + } + // 提交事务 + tx.Commit() + return insertedID +} + +// Update 修改信息 +func (r *NeHost) Update(neHost model.NeHost) int64 { + // 参数拼接 + params := make(map[string]any) + if neHost.HostType != "" { + params["host_type"] = neHost.HostType + } + if neHost.GroupID != "" { + params["group_id"] = neHost.GroupID + } + if neHost.Title != "" { + params["title"] = neHost.Title + } + if neHost.Addr != "" { + params["addr"] = neHost.Addr + } + if neHost.Port > 0 { + params["port"] = neHost.Port + } + if neHost.User != "" { + params["user"] = neHost.User + } + if neHost.AuthMode != "" { + params["auth_mode"] = neHost.AuthMode + } + if neHost.Password != "" { + params["password"] = neHost.Password + } + if neHost.PrivateKey != "" { + params["private_key"] = neHost.PrivateKey + } + if neHost.PassPhrase != "" { + params["pass_phrase"] = neHost.PassPhrase + } + if neHost.DBName != "" { + params["db_name"] = neHost.DBName + } + params["remark"] = neHost.Remark + if neHost.UpdateBy != "" { + params["update_by"] = neHost.UpdateBy + params["update_time"] = time.Now().UnixMilli() + } + + // 根据认证模式清除不必要的信息 + if neHost.AuthMode == "0" { + params["private_key"] = "" + params["pass_phrase"] = "" + } + if neHost.AuthMode == "1" { + params["password"] = "" + } + if neHost.AuthMode == "2" { + params["password"] = "" + params["private_key"] = "" + params["pass_phrase"] = "" + } + + // 构建执行语句 + keys, values := repo.KeyValueByUpdate(params) + sql := "update ne_host set " + strings.Join(keys, ",") + " where host_id = ?" + + // 执行更新 + values = append(values, neHost.HostID) + rows, err := datasource.ExecDB("", sql, values) + if err != nil { + logger.Errorf("update row : %v", err.Error()) + return 0 + } + return rows +} + +// DeleteByIds 批量删除网元主机连接信息 +func (r *NeHost) DeleteByIds(hostIds []string) int64 { + placeholder := repo.KeyPlaceholderByQuery(len(hostIds)) + sql := "delete from ne_host where host_id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(hostIds) + results, err := datasource.ExecDB("", sql, parameters) + if err != nil { + logger.Errorf("delete err => %v", err) + return 0 + } + return results } diff --git a/src/modules/network_element/repository/ne_host.impl.go b/src/modules/network_element/repository/ne_host.impl.go deleted file mode 100644 index fdc9f030..00000000 --- a/src/modules/network_element/repository/ne_host.impl.go +++ /dev/null @@ -1,432 +0,0 @@ -package repository - -import ( - "fmt" - "strings" - "time" - - "be.ems/src/framework/datasource" - "be.ems/src/framework/logger" - "be.ems/src/framework/utils/crypto" - "be.ems/src/framework/utils/parse" - "be.ems/src/framework/utils/repo" - "be.ems/src/modules/network_element/model" -) - -// 实例化数据层 NewNeHostImpl 结构体 -var NewNeHostImpl = &NeHostImpl{ - selectSql: `select - host_id, host_type, group_id, title, addr, port, user, auth_mode, password, private_key, pass_phrase, remark, create_by, create_time, update_by, update_time - from ne_host`, - - resultMap: map[string]string{ - "host_id": "HostID", - "host_type": "HostType", - "group_id": "GroupID", - "title": "Title", - "addr": "Addr", - "port": "Port", - "user": "User", - "auth_mode": "AuthMode", - "password": "Password", - "private_key": "PrivateKey", - "private_password": "PassPhrase", - "remark": "Remark", - "create_by": "CreateBy", - "create_time": "CreateTime", - "update_by": "UpdateBy", - "update_time": "UpdateTime", - }, -} - -// NeHostImpl 网元主机连接 数据层处理 -type NeHostImpl struct { - // 查询视图对象SQL - selectSql string - // 结果字段与实体映射 - resultMap map[string]string -} - -// convertResultRows 将结果记录转实体结果组 -func (r *NeHostImpl) convertResultRows(rows []map[string]any) []model.NeHost { - arr := make([]model.NeHost, 0) - for _, row := range rows { - item := model.NeHost{} - for key, value := range row { - if keyMapper, ok := r.resultMap[key]; ok { - repo.SetFieldValue(&item, keyMapper, value) - } - } - arr = append(arr, item) - } - return arr -} - -// SelectPage 根据条件分页查询字典类型 -func (r *NeHostImpl) SelectPage(query map[string]any) map[string]any { - // 查询条件拼接 - var conditions []string - var params []any - if v, ok := query["hostType"]; ok && v != "" { - conditions = append(conditions, "host_type = ?") - params = append(params, strings.Trim(v.(string), " ")) - } - if v, ok := query["groupId"]; ok && v != "" { - conditions = append(conditions, "group_id = ?") - params = append(params, strings.Trim(v.(string), " ")) - } - if v, ok := query["title"]; ok && v != "" { - conditions = append(conditions, "title like concat(?, '%')") - params = append(params, strings.Trim(v.(string), " ")) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } - - result := map[string]any{ - "total": 0, - "rows": []model.NeHost{}, - } - - // 查询数量 长度为0直接返回 - totalSql := "select count(1) as 'total' from ne_host" - totalRows, err := datasource.RawDB("", totalSql+whereSql, params) - if err != nil { - logger.Errorf("total err => %v", err) - return result - } - total := parse.Number(totalRows[0]["total"]) - if total == 0 { - return result - } else { - result["total"] = total - } - - // 分页 - pageNum, pageSize := repo.PageNumSize(query["pageNum"], query["pageSize"]) - pageSql := " limit ?,? " - params = append(params, pageNum*pageSize) - params = append(params, pageSize) - - // 查询数据 - querySql := r.selectSql + whereSql + pageSql - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err => %v", err) - return result - } - - // 转换实体 - result["rows"] = r.convertResultRows(results) - return result -} - -// SelectList 根据实体查询 -func (r *NeHostImpl) SelectList(neHost model.NeHost) []model.NeHost { - // 查询条件拼接 - var conditions []string - var params []any - if neHost.HostType != "" { - conditions = append(conditions, "host_type = ?") - params = append(params, neHost.HostType) - } - if neHost.GroupID != "" { - conditions = append(conditions, "group_id = ?") - params = append(params, neHost.GroupID) - } - if neHost.Title != "" { - conditions = append(conditions, "title like concat(?, '%')") - params = append(params, neHost.Title) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } - - // 查询数据 - querySql := r.selectSql + whereSql + " order by update_time asc " - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err => %v", err) - } - - // 转换实体 - return r.convertResultRows(results) -} - -// SelectByIds 通过ID查询 -func (r *NeHostImpl) SelectByIds(hostIds []string) []model.NeHost { - placeholder := repo.KeyPlaceholderByQuery(len(hostIds)) - querySql := r.selectSql + " where host_id in (" + placeholder + ")" - parameters := repo.ConvertIdsSlice(hostIds) - results, err := datasource.RawDB("", querySql, parameters) - if err != nil { - logger.Errorf("query err => %v", err) - return []model.NeHost{} - } - // 转换实体 - rows := r.convertResultRows(results) - arr := &rows - for i := range *arr { - passwordDe, err := crypto.StringDecryptByAES((*arr)[i].Password) - if err != nil { - logger.Errorf("selectById %s decrypt: %v", (*arr)[i].HostID, err.Error()) - (*arr)[i].Password = "" - } else { - (*arr)[i].Password = passwordDe - } - privateKeyDe, err := crypto.StringDecryptByAES((*arr)[i].PrivateKey) - if err != nil { - logger.Errorf("selectById %s decrypt: %v", (*arr)[i].HostID, err.Error()) - (*arr)[i].PrivateKey = "" - } else { - (*arr)[i].PrivateKey = privateKeyDe - } - passPhraseDe, err := crypto.StringDecryptByAES((*arr)[i].PassPhrase) - if err != nil { - logger.Errorf("selectById %s decrypt: %v", (*arr)[i].HostID, err.Error()) - (*arr)[i].PassPhrase = "" - } else { - (*arr)[i].PassPhrase = passPhraseDe - } - } - return rows -} - -// CheckUniqueNeHost 校验主机是否唯一 -func (r *NeHostImpl) CheckUniqueNeHost(neHost model.NeHost) string { - // 查询条件拼接 - var conditions []string - var params []any - if neHost.HostType != "" { - conditions = append(conditions, "host_type = ?") - params = append(params, neHost.HostType) - } - if neHost.GroupID != "" { - conditions = append(conditions, "group_id = ?") - params = append(params, neHost.GroupID) - } - if neHost.Title != "" { - conditions = append(conditions, "title = ?") - params = append(params, neHost.Title) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } else { - return "" - } - - // 查询数据 - querySql := "select host_id as 'str' from ne_host " + whereSql + " limit 1" - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err %v", err) - return "" - } - if len(results) > 0 { - return fmt.Sprint(results[0]["str"]) - } - return "" -} - -// Insert 新增信息 -func (r *NeHostImpl) Insert(neHost model.NeHost) string { - // 参数拼接 - params := make(map[string]any) - if neHost.HostType != "" { - params["host_type"] = neHost.HostType - } - if neHost.GroupID != "" { - params["group_id"] = neHost.GroupID - } - if neHost.Title != "" { - params["title"] = neHost.Title - } - if neHost.Addr != "" { - params["addr"] = neHost.Addr - } - if neHost.Port > 0 { - params["port"] = neHost.Port - } - if neHost.User != "" { - params["user"] = neHost.User - } - if neHost.AuthMode != "" { - params["auth_mode"] = neHost.AuthMode - } - if neHost.Password != "" { - passwordEn, err := crypto.StringEncryptByAES(neHost.Password) - if err != nil { - logger.Errorf("insert encrypt: %v", err.Error()) - return "" - } - params["password"] = passwordEn - } - if neHost.PrivateKey != "" { - privateKeyEn, err := crypto.StringEncryptByAES(neHost.PrivateKey) - if err != nil { - logger.Errorf("insert encrypt: %v", err.Error()) - return "" - } - params["private_key"] = privateKeyEn - } - if neHost.PassPhrase != "" { - passPhraseEn, err := crypto.StringEncryptByAES(neHost.PassPhrase) - if err != nil { - logger.Errorf("insert encrypt: %v", err.Error()) - return "" - } - params["pass_phrase"] = passPhraseEn - } - if neHost.Remark != "" { - params["remark"] = neHost.Remark - } - if neHost.CreateBy != "" { - params["create_by"] = neHost.CreateBy - params["create_time"] = time.Now().UnixMilli() - } - - // 根据认证模式清除不必要的信息 - if neHost.AuthMode == "0" { - params["private_key"] = "" - params["pass_phrase"] = "" - } - if neHost.AuthMode == "1" { - params["password"] = "" - } - if neHost.AuthMode == "2" { - params["password"] = "" - params["private_key"] = "" - params["pass_phrase"] = "" - } - - // 构建执行语句 - keys, placeholder, values := repo.KeyPlaceholderValueByInsert(params) - sql := "insert into ne_host (" + strings.Join(keys, ",") + ")values(" + placeholder + ")" - - db := datasource.DefaultDB() - // 开启事务 - tx := db.Begin() - // 执行插入 - err := tx.Exec(sql, values...).Error - if err != nil { - logger.Errorf("insert row : %v", err.Error()) - tx.Rollback() - return "" - } - // 获取生成的自增 ID - var insertedID string - err = tx.Raw("select last_insert_id()").Row().Scan(&insertedID) - if err != nil { - logger.Errorf("insert last id : %v", err.Error()) - tx.Rollback() - return "" - } - // 提交事务 - tx.Commit() - return insertedID -} - -// Update 修改信息 -func (r *NeHostImpl) Update(neHost model.NeHost) int64 { - // 参数拼接 - params := make(map[string]any) - if neHost.HostType != "" { - params["host_type"] = neHost.HostType - } - if neHost.GroupID != "" { - params["group_id"] = neHost.GroupID - } - if neHost.Title != "" { - params["title"] = neHost.Title - } - if neHost.Addr != "" { - params["addr"] = neHost.Addr - } - if neHost.Port > 0 { - params["port"] = neHost.Port - } - if neHost.User != "" { - params["user"] = neHost.User - } - if neHost.AuthMode != "" { - params["auth_mode"] = neHost.AuthMode - } - if neHost.Password != "" { - passwordEn, err := crypto.StringEncryptByAES(neHost.Password) - if err != nil { - logger.Errorf("update encrypt: %v", err.Error()) - return 0 - } - params["password"] = passwordEn - } - if neHost.PrivateKey != "" { - privateKeyEn, err := crypto.StringEncryptByAES(neHost.PrivateKey) - if err != nil { - logger.Errorf("update encrypt: %v", err.Error()) - return 0 - } - params["private_key"] = privateKeyEn - } - if neHost.PassPhrase != "" { - passPhraseEn, err := crypto.StringEncryptByAES(neHost.PassPhrase) - if err != nil { - logger.Errorf("update encrypt: %v", err.Error()) - return 0 - } - params["pass_phrase"] = passPhraseEn - } - params["remark"] = neHost.Remark - if neHost.UpdateBy != "" { - params["update_by"] = neHost.UpdateBy - params["update_time"] = time.Now().UnixMilli() - } - - // 根据认证模式清除不必要的信息 - if neHost.AuthMode == "0" { - params["private_key"] = "" - params["pass_phrase"] = "" - } - if neHost.AuthMode == "1" { - params["password"] = "" - } - if neHost.AuthMode == "2" { - params["password"] = "" - params["private_key"] = "" - params["pass_phrase"] = "" - } - - // 构建执行语句 - keys, values := repo.KeyValueByUpdate(params) - sql := "update ne_host set " + strings.Join(keys, ",") + " where host_id = ?" - - // 执行更新 - values = append(values, neHost.HostID) - rows, err := datasource.ExecDB("", sql, values) - if err != nil { - logger.Errorf("update row : %v", err.Error()) - return 0 - } - return rows -} - -// DeleteByIds 批量删除网元主机连接信息 -func (r *NeHostImpl) DeleteByIds(hostIds []string) int64 { - placeholder := repo.KeyPlaceholderByQuery(len(hostIds)) - sql := "delete from ne_host where host_id in (" + placeholder + ")" - parameters := repo.ConvertIdsSlice(hostIds) - results, err := datasource.ExecDB("", sql, parameters) - if err != nil { - logger.Errorf("delete err => %v", err) - return 0 - } - return results -} diff --git a/src/modules/network_element/repository/ne_host_cmd.go b/src/modules/network_element/repository/ne_host_cmd.go index 170441bc..4ef57073 100644 --- a/src/modules/network_element/repository/ne_host_cmd.go +++ b/src/modules/network_element/repository/ne_host_cmd.go @@ -1,27 +1,306 @@ package repository -import "be.ems/src/modules/network_element/model" +import ( + "fmt" + "strings" + "time" -// INeHostCmd 网元主机命令 数据层接口 -type INeHostCmd interface { - // SelectPage 根据条件分页查询字典类型 - SelectPage(query map[string]any) map[string]any + "be.ems/src/framework/datasource" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/parse" + "be.ems/src/framework/utils/repo" + "be.ems/src/modules/network_element/model" +) - // SelectList 根据实体查询 - SelectList(neHostCmd model.NeHostCmd) []model.NeHostCmd +// 实例化数据层 NeHostCmd 结构体 +var NewNeHostCmd = &NeHostCmd{ + selectSql: `select + cmd_id, cmd_type, group_id, title, command, remark, create_by, create_time, update_by, update_time + from ne_host_cmd`, - // SelectByIds 通过ID查询 - SelectByIds(cmdIds []string) []model.NeHostCmd - - // Insert 新增信息 - Insert(neHostCmd model.NeHostCmd) string - - // Update 修改信息 - Update(neHostCmd model.NeHostCmd) int64 - - // DeleteByIds 批量删除信息 - DeleteByIds(cmdIds []string) int64 - - // CheckUniqueGroupTitle 校验同类型组内是否唯一 - CheckUniqueGroupTitle(neHostCmd model.NeHostCmd) string + resultMap: map[string]string{ + "cmd_id": "CmdID", + "cmd_type": "CmdType", + "group_id": "GroupID", + "title": "Title", + "command": "Command", + "remark": "Remark", + "create_by": "CreateBy", + "create_time": "CreateTime", + "update_by": "UpdateBy", + "update_time": "UpdateTime", + }, +} + +// NeHostCmd 网元主机连接 数据层处理 +type NeHostCmd struct { + // 查询视图对象SQL + selectSql string + // 结果字段与实体映射 + resultMap map[string]string +} + +// convertResultRows 将结果记录转实体结果组 +func (r *NeHostCmd) convertResultRows(rows []map[string]any) []model.NeHostCmd { + arr := make([]model.NeHostCmd, 0) + for _, row := range rows { + item := model.NeHostCmd{} + for key, value := range row { + if keyMapper, ok := r.resultMap[key]; ok { + repo.SetFieldValue(&item, keyMapper, value) + } + } + arr = append(arr, item) + } + return arr +} + +// SelectPage 根据条件分页查询字典类型 +func (r *NeHostCmd) SelectPage(query map[string]any) map[string]any { + // 查询条件拼接 + var conditions []string + var params []any + if v, ok := query["cmdType"]; ok && v != "" { + conditions = append(conditions, "cmd_type = ?") + params = append(params, strings.Trim(v.(string), " ")) + } + if v, ok := query["groupId"]; ok && v != "" { + conditions = append(conditions, "group_id = ?") + params = append(params, strings.Trim(v.(string), " ")) + } + if v, ok := query["title"]; ok && v != "" { + conditions = append(conditions, "title like concat(?, '%')") + params = append(params, strings.Trim(v.(string), " ")) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + result := map[string]any{ + "total": 0, + "rows": []model.NeHost{}, + } + + // 查询数量 长度为0直接返回 + totalSql := "select count(1) as 'total' from ne_host_cmd" + totalRows, err := datasource.RawDB("", totalSql+whereSql, params) + if err != nil { + logger.Errorf("total err => %v", err) + return result + } + total := parse.Number(totalRows[0]["total"]) + if total == 0 { + return result + } else { + result["total"] = total + } + + // 分页 + pageNum, pageSize := repo.PageNumSize(query["pageNum"], query["pageSize"]) + pageSql := " limit ?,? " + params = append(params, pageNum*pageSize) + params = append(params, pageSize) + + // 查询数据 + querySql := r.selectSql + whereSql + pageSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + return result + } + + // 转换实体 + result["rows"] = r.convertResultRows(results) + return result +} + +// SelectList 根据实体查询 +func (r *NeHostCmd) SelectList(neHostCmd model.NeHostCmd) []model.NeHostCmd { + // 查询条件拼接 + var conditions []string + var params []any + if neHostCmd.CmdType != "" { + conditions = append(conditions, "cmd_type = ?") + params = append(params, neHostCmd.CmdType) + } + if neHostCmd.GroupID != "" { + conditions = append(conditions, "group_id = ?") + params = append(params, neHostCmd.GroupID) + } + if neHostCmd.Title != "" { + conditions = append(conditions, "title like concat(?, '%')") + params = append(params, neHostCmd.Title) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询数据 + querySql := r.selectSql + whereSql + " order by update_time asc " + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + } + + // 转换实体 + return r.convertResultRows(results) +} + +// SelectByIds 通过ID查询 +func (r *NeHostCmd) SelectByIds(cmdIds []string) []model.NeHostCmd { + placeholder := repo.KeyPlaceholderByQuery(len(cmdIds)) + querySql := r.selectSql + " where cmd_id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(cmdIds) + results, err := datasource.RawDB("", querySql, parameters) + if err != nil { + logger.Errorf("query err => %v", err) + return []model.NeHostCmd{} + } + // 转换实体 + return r.convertResultRows(results) +} + +// CheckUniqueGroupTitle 校验同类型组内是否唯一 +func (r *NeHostCmd) CheckUniqueGroupTitle(neHostCmd model.NeHostCmd) string { + // 查询条件拼接 + var conditions []string + var params []any + if neHostCmd.CmdType != "" { + conditions = append(conditions, "cmd_type = ?") + params = append(params, neHostCmd.CmdType) + } + if neHostCmd.GroupID != "" { + conditions = append(conditions, "group_id = ?") + params = append(params, neHostCmd.GroupID) + } + if neHostCmd.Title != "" { + conditions = append(conditions, "title = ?") + params = append(params, neHostCmd.Title) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } else { + return "" + } + + // 查询数据 + querySql := "select host_id as 'str' from ne_host " + whereSql + " limit 1" + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err %v", err) + return "" + } + if len(results) > 0 { + return fmt.Sprint(results[0]["str"]) + } + return "" +} + +// Insert 新增信息 +func (r *NeHostCmd) Insert(neHostCmd model.NeHostCmd) string { + // 参数拼接 + params := make(map[string]any) + if neHostCmd.CmdType != "" { + params["cmd_type"] = neHostCmd.CmdType + } + if neHostCmd.GroupID != "" { + params["group_id"] = neHostCmd.GroupID + } + if neHostCmd.Title != "" { + params["title"] = neHostCmd.Title + } + if neHostCmd.Command != "" { + params["command"] = neHostCmd.Command + } + if neHostCmd.Remark != "" { + params["remark"] = neHostCmd.Remark + } + if neHostCmd.CreateBy != "" { + params["create_by"] = neHostCmd.CreateBy + params["create_time"] = time.Now().UnixMilli() + } + + // 构建执行语句 + keys, placeholder, values := repo.KeyPlaceholderValueByInsert(params) + sql := "insert into ne_host_cmd (" + strings.Join(keys, ",") + ")values(" + placeholder + ")" + + db := datasource.DefaultDB() + // 开启事务 + tx := db.Begin() + // 执行插入 + err := tx.Exec(sql, values...).Error + if err != nil { + logger.Errorf("insert row : %v", err.Error()) + tx.Rollback() + return "" + } + // 获取生成的自增 ID + var insertedID string + err = tx.Raw("select last_insert_id()").Row().Scan(&insertedID) + if err != nil { + logger.Errorf("insert last id : %v", err.Error()) + tx.Rollback() + return "" + } + // 提交事务 + tx.Commit() + return insertedID +} + +// Update 修改信息 +func (r *NeHostCmd) Update(neHostCmd model.NeHostCmd) int64 { + // 参数拼接 + params := make(map[string]any) + if neHostCmd.CmdType != "" { + params["cmd_type"] = neHostCmd.CmdType + } + if neHostCmd.GroupID != "" { + params["group_id"] = neHostCmd.GroupID + } + if neHostCmd.Title != "" { + params["title"] = neHostCmd.Title + } + if neHostCmd.Command != "" { + params["command"] = neHostCmd.Command + } + params["remark"] = neHostCmd.Remark + if neHostCmd.UpdateBy != "" { + params["update_by"] = neHostCmd.UpdateBy + params["update_time"] = time.Now().UnixMilli() + } + + // 构建执行语句 + keys, values := repo.KeyValueByUpdate(params) + sql := "update ne_host_cmd set " + strings.Join(keys, ",") + " where cmd_id = ?" + + // 执行更新 + values = append(values, neHostCmd.CmdID) + rows, err := datasource.ExecDB("", sql, values) + if err != nil { + logger.Errorf("update row : %v", err.Error()) + return 0 + } + return rows +} + +// DeleteByIds 批量删除信息 +func (r *NeHostCmd) DeleteByIds(cmdIds []string) int64 { + placeholder := repo.KeyPlaceholderByQuery(len(cmdIds)) + sql := "delete from ne_host_cmd where cmd_id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(cmdIds) + results, err := datasource.ExecDB("", sql, parameters) + if err != nil { + logger.Errorf("delete err => %v", err) + return 0 + } + return results } diff --git a/src/modules/network_element/repository/ne_host_cmd.impl.go b/src/modules/network_element/repository/ne_host_cmd.impl.go deleted file mode 100644 index b4bb279e..00000000 --- a/src/modules/network_element/repository/ne_host_cmd.impl.go +++ /dev/null @@ -1,306 +0,0 @@ -package repository - -import ( - "fmt" - "strings" - "time" - - "be.ems/src/framework/datasource" - "be.ems/src/framework/logger" - "be.ems/src/framework/utils/parse" - "be.ems/src/framework/utils/repo" - "be.ems/src/modules/network_element/model" -) - -// 实例化数据层 NewNeHostCmdImpl 结构体 -var NewNeHostCmdImpl = &NeHostCmdImpl{ - selectSql: `select - cmd_id, cmd_type, group_id, title, command, remark, create_by, create_time, update_by, update_time - from ne_host_cmd`, - - resultMap: map[string]string{ - "cmd_id": "CmdID", - "cmd_type": "CmdType", - "group_id": "GroupID", - "title": "Title", - "command": "Command", - "remark": "Remark", - "create_by": "CreateBy", - "create_time": "CreateTime", - "update_by": "UpdateBy", - "update_time": "UpdateTime", - }, -} - -// NeHostCmdImpl 网元主机连接 数据层处理 -type NeHostCmdImpl struct { - // 查询视图对象SQL - selectSql string - // 结果字段与实体映射 - resultMap map[string]string -} - -// convertResultRows 将结果记录转实体结果组 -func (r *NeHostCmdImpl) convertResultRows(rows []map[string]any) []model.NeHostCmd { - arr := make([]model.NeHostCmd, 0) - for _, row := range rows { - item := model.NeHostCmd{} - for key, value := range row { - if keyMapper, ok := r.resultMap[key]; ok { - repo.SetFieldValue(&item, keyMapper, value) - } - } - arr = append(arr, item) - } - return arr -} - -// SelectPage 根据条件分页查询字典类型 -func (r *NeHostCmdImpl) SelectPage(query map[string]any) map[string]any { - // 查询条件拼接 - var conditions []string - var params []any - if v, ok := query["cmdType"]; ok && v != "" { - conditions = append(conditions, "cmd_type = ?") - params = append(params, strings.Trim(v.(string), " ")) - } - if v, ok := query["groupId"]; ok && v != "" { - conditions = append(conditions, "group_id = ?") - params = append(params, strings.Trim(v.(string), " ")) - } - if v, ok := query["title"]; ok && v != "" { - conditions = append(conditions, "title like concat(?, '%')") - params = append(params, strings.Trim(v.(string), " ")) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } - - result := map[string]any{ - "total": 0, - "rows": []model.NeHost{}, - } - - // 查询数量 长度为0直接返回 - totalSql := "select count(1) as 'total' from ne_host_cmd" - totalRows, err := datasource.RawDB("", totalSql+whereSql, params) - if err != nil { - logger.Errorf("total err => %v", err) - return result - } - total := parse.Number(totalRows[0]["total"]) - if total == 0 { - return result - } else { - result["total"] = total - } - - // 分页 - pageNum, pageSize := repo.PageNumSize(query["pageNum"], query["pageSize"]) - pageSql := " limit ?,? " - params = append(params, pageNum*pageSize) - params = append(params, pageSize) - - // 查询数据 - querySql := r.selectSql + whereSql + pageSql - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err => %v", err) - return result - } - - // 转换实体 - result["rows"] = r.convertResultRows(results) - return result -} - -// SelectList 根据实体查询 -func (r *NeHostCmdImpl) SelectList(neHostCmd model.NeHostCmd) []model.NeHostCmd { - // 查询条件拼接 - var conditions []string - var params []any - if neHostCmd.CmdType != "" { - conditions = append(conditions, "cmd_type = ?") - params = append(params, neHostCmd.CmdType) - } - if neHostCmd.GroupID != "" { - conditions = append(conditions, "group_id = ?") - params = append(params, neHostCmd.GroupID) - } - if neHostCmd.Title != "" { - conditions = append(conditions, "title like concat(?, '%')") - params = append(params, neHostCmd.Title) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } - - // 查询数据 - querySql := r.selectSql + whereSql + " order by update_time asc " - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err => %v", err) - } - - // 转换实体 - return r.convertResultRows(results) -} - -// SelectByIds 通过ID查询 -func (r *NeHostCmdImpl) SelectByIds(cmdIds []string) []model.NeHostCmd { - placeholder := repo.KeyPlaceholderByQuery(len(cmdIds)) - querySql := r.selectSql + " where cmd_id in (" + placeholder + ")" - parameters := repo.ConvertIdsSlice(cmdIds) - results, err := datasource.RawDB("", querySql, parameters) - if err != nil { - logger.Errorf("query err => %v", err) - return []model.NeHostCmd{} - } - // 转换实体 - return r.convertResultRows(results) -} - -// CheckUniqueGroupTitle 校验同类型组内是否唯一 -func (r *NeHostCmdImpl) CheckUniqueGroupTitle(neHostCmd model.NeHostCmd) string { - // 查询条件拼接 - var conditions []string - var params []any - if neHostCmd.CmdType != "" { - conditions = append(conditions, "cmd_type = ?") - params = append(params, neHostCmd.CmdType) - } - if neHostCmd.GroupID != "" { - conditions = append(conditions, "group_id = ?") - params = append(params, neHostCmd.GroupID) - } - if neHostCmd.Title != "" { - conditions = append(conditions, "title = ?") - params = append(params, neHostCmd.Title) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } else { - return "" - } - - // 查询数据 - querySql := "select host_id as 'str' from ne_host " + whereSql + " limit 1" - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err %v", err) - return "" - } - if len(results) > 0 { - return fmt.Sprint(results[0]["str"]) - } - return "" -} - -// Insert 新增信息 -func (r *NeHostCmdImpl) Insert(neHostCmd model.NeHostCmd) string { - // 参数拼接 - params := make(map[string]any) - if neHostCmd.CmdType != "" { - params["cmd_type"] = neHostCmd.CmdType - } - if neHostCmd.GroupID != "" { - params["group_id"] = neHostCmd.GroupID - } - if neHostCmd.Title != "" { - params["title"] = neHostCmd.Title - } - if neHostCmd.Command != "" { - params["command"] = neHostCmd.Command - } - if neHostCmd.Remark != "" { - params["remark"] = neHostCmd.Remark - } - if neHostCmd.CreateBy != "" { - params["create_by"] = neHostCmd.CreateBy - params["create_time"] = time.Now().UnixMilli() - } - - // 构建执行语句 - keys, placeholder, values := repo.KeyPlaceholderValueByInsert(params) - sql := "insert into ne_host_cmd (" + strings.Join(keys, ",") + ")values(" + placeholder + ")" - - db := datasource.DefaultDB() - // 开启事务 - tx := db.Begin() - // 执行插入 - err := tx.Exec(sql, values...).Error - if err != nil { - logger.Errorf("insert row : %v", err.Error()) - tx.Rollback() - return "" - } - // 获取生成的自增 ID - var insertedID string - err = tx.Raw("select last_insert_id()").Row().Scan(&insertedID) - if err != nil { - logger.Errorf("insert last id : %v", err.Error()) - tx.Rollback() - return "" - } - // 提交事务 - tx.Commit() - return insertedID -} - -// Update 修改信息 -func (r *NeHostCmdImpl) Update(neHostCmd model.NeHostCmd) int64 { - // 参数拼接 - params := make(map[string]any) - if neHostCmd.CmdType != "" { - params["cmd_type"] = neHostCmd.CmdType - } - if neHostCmd.GroupID != "" { - params["group_id"] = neHostCmd.GroupID - } - if neHostCmd.Title != "" { - params["title"] = neHostCmd.Title - } - if neHostCmd.Command != "" { - params["command"] = neHostCmd.Command - } - params["remark"] = neHostCmd.Remark - if neHostCmd.UpdateBy != "" { - params["update_by"] = neHostCmd.UpdateBy - params["update_time"] = time.Now().UnixMilli() - } - - // 构建执行语句 - keys, values := repo.KeyValueByUpdate(params) - sql := "update ne_host_cmd set " + strings.Join(keys, ",") + " where cmd_id = ?" - - // 执行更新 - values = append(values, neHostCmd.CmdID) - rows, err := datasource.ExecDB("", sql, values) - if err != nil { - logger.Errorf("update row : %v", err.Error()) - return 0 - } - return rows -} - -// DeleteByIds 批量删除信息 -func (r *NeHostCmdImpl) DeleteByIds(cmdIds []string) int64 { - placeholder := repo.KeyPlaceholderByQuery(len(cmdIds)) - sql := "delete from ne_host_cmd where cmd_id in (" + placeholder + ")" - parameters := repo.ConvertIdsSlice(cmdIds) - results, err := datasource.ExecDB("", sql, parameters) - if err != nil { - logger.Errorf("delete err => %v", err) - return 0 - } - return results -} diff --git a/src/modules/network_element/repository/ne_info.go b/src/modules/network_element/repository/ne_info.go index c09d034a..6e2a5ccd 100644 --- a/src/modules/network_element/repository/ne_info.go +++ b/src/modules/network_element/repository/ne_info.go @@ -1,32 +1,408 @@ package repository import ( + "fmt" + "strings" + "time" + + "be.ems/src/framework/datasource" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/parse" + "be.ems/src/framework/utils/repo" "be.ems/src/modules/network_element/model" ) -// 网元信息 数据层接口 -type INeInfo interface { - // SelectNeInfoByNeTypeAndNeID 通过ne_type和ne_id查询网元信息 - SelectNeInfoByNeTypeAndNeID(neType, neID string) model.NeInfo - - // SelectPage 根据条件分页查询 - SelectPage(query map[string]any) map[string]any - - // SelectList 查询列表 - SelectList(neInfo model.NeInfo) []model.NeInfo - - // SelectByIds 通过ID查询 - SelectByIds(infoIds []string) []model.NeInfo - - // Insert 新增信息 - Insert(neInfo model.NeInfo) string - - // Update 修改信息 - Update(neInfo model.NeInfo) int64 - - // DeleteByIds 批量删除网元信息 - DeleteByIds(infoIds []string) int64 - - // CheckUniqueNeTypeAndNeId 校验同类型下标识是否唯一 - CheckUniqueNeTypeAndNeId(neInfo model.NeInfo) string +// neListSort 网元列表预设排序 +var neListSort = []string{ + "OMC", + "IMS", + "AMF", + "AUSF", + "UDR", + "UDM", + "SMF", + "PCF", + "NSSF", + "NRF", + "UPF", + "LMF", + "NEF", + "MME", + "N3IWF", + "MOCNGW", + "SMSC", + "SMSF", + "CBC", + "CHF", +} + +// 实例化数据层 NeInfo 结构体 +var NewNeInfo = &NeInfo{ + selectSql: `select id, ne_type, ne_id, rm_uid, ne_name, ip, port, pv_flag, province, vendor_name, dn, ne_address, host_ids, status, remark, create_by, create_time, update_by, update_time from ne_info`, + + resultMap: map[string]string{ + "id": "ID", + "ne_type": "NeType", + "ne_id": "NeId", + "rm_uid": "RmUID", + "ne_name": "NeName", + "ip": "IP", + "port": "Port", + "pv_flag": "PvFlag", + "province": "Province", + "vendor_name": "VendorName", + "dn": "Dn", + "ne_address": "NeAddress", + "host_ids": "HostIDs", + "status": "Status", + "remark": "Remark", + "create_by": "CreateBy", + "create_time": "CreateTime", + "update_by": "UpdateBy", + "update_time": "UpdateTime", + }, +} + +// NeInfo 网元信息表 数据层处理 +type NeInfo struct { + // 查询视图对象SQL + selectSql string + // 结果字段与实体映射 + resultMap map[string]string +} + +// convertResultRows 将结果记录转实体结果组 +func (r *NeInfo) convertResultRows(rows []map[string]any) []model.NeInfo { + arr := make([]model.NeInfo, 0) + for _, row := range rows { + item := model.NeInfo{} + for key, value := range row { + if keyMapper, ok := r.resultMap[key]; ok { + repo.SetFieldValue(&item, keyMapper, value) + } + } + arr = append(arr, item) + } + + // 创建优先级映射 + priority := make(map[string]int) + for i, v := range neListSort { + priority[v] = i + } + // 冒泡排序 + n := len(arr) + for i := 0; i < n-1; i++ { + for j := 0; j < n-i-1; j++ { + if priority[arr[j].NeType] > priority[arr[j+1].NeType] { + // 交换元素 + arr[j], arr[j+1] = arr[j+1], arr[j] + } + } + } + + return arr +} + +// SelectNeInfoByNeTypeAndNeID 通过ne_type和ne_id查询网元信息 +func (r *NeInfo) SelectNeInfoByNeTypeAndNeID(neType, neID string) model.NeInfo { + querySql := r.selectSql + " where ne_type = ? and ne_id = ?" + results, err := datasource.RawDB("", querySql, []any{neType, neID}) + if err != nil { + logger.Errorf("query err => %v", err) + return model.NeInfo{} + } + // 转换实体 + rows := r.convertResultRows(results) + if len(rows) > 0 { + return rows[0] + } + return model.NeInfo{} +} + +// SelectPage 根据条件分页查询 +func (r *NeInfo) SelectPage(query map[string]any) map[string]any { + // 查询条件拼接 + var conditions []string + var params []any + if v, ok := query["neType"]; ok && v != "" { + conditions = append(conditions, "ne_type = ?") + params = append(params, strings.Trim(v.(string), " ")) + } + if v, ok := query["neId"]; ok && v != "" { + conditions = append(conditions, "ne_id = ?") + params = append(params, strings.Trim(v.(string), " ")) + } + if v, ok := query["rmUid"]; ok && v != "" { + conditions = append(conditions, "rmUid like concat(?, '%')") + params = append(params, strings.Trim(v.(string), " ")) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + result := map[string]any{ + "total": 0, + "rows": []model.NeInfo{}, + } + + // 查询数量 长度为0直接返回 + totalSql := "select count(1) as 'total' from ne_info" + totalRows, err := datasource.RawDB("", totalSql+whereSql, params) + if err != nil { + logger.Errorf("total err => %v", err) + return result + } + total := parse.Number(totalRows[0]["total"]) + if total == 0 { + return result + } else { + result["total"] = total + } + + // 分页 + pageNum, pageSize := repo.PageNumSize(query["pageNum"], query["pageSize"]) + pageSql := " limit ?,? " + params = append(params, pageNum*pageSize) + params = append(params, pageSize) + + // 查询数据 + querySql := r.selectSql + whereSql + " order by ne_type asc, ne_id asc " + pageSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + return result + } + + // 转换实体 + result["rows"] = r.convertResultRows(results) + return result +} + +// SelectList 查询列表 +func (r *NeInfo) SelectList(neInfo model.NeInfo) []model.NeInfo { + // 查询条件拼接 + var conditions []string + var params []any + if neInfo.NeType != "" { + conditions = append(conditions, "ne_type = ?") + params = append(params, neInfo.NeType) + } + if neInfo.NeId != "" { + conditions = append(conditions, "ne_id = ?") + params = append(params, neInfo.NeId) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询数据 + querySql := r.selectSql + whereSql + " order by ne_type asc, ne_id asc " + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + } + + // 转换实体 + return r.convertResultRows(results) +} + +// SelectByIds 通过ID查询 +func (r *NeInfo) SelectByIds(infoIds []string) []model.NeInfo { + placeholder := repo.KeyPlaceholderByQuery(len(infoIds)) + querySql := r.selectSql + " where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(infoIds) + results, err := datasource.RawDB("", querySql, parameters) + if err != nil { + logger.Errorf("query err => %v", err) + return []model.NeInfo{} + } + // 转换实体 + return r.convertResultRows(results) +} + +// CheckUniqueNeTypeAndNeId 校验同类型下标识是否唯一 +func (r *NeInfo) CheckUniqueNeTypeAndNeId(neInfo model.NeInfo) string { + // 查询条件拼接 + var conditions []string + var params []any + if neInfo.NeType != "" { + conditions = append(conditions, "ne_type = ?") + params = append(params, neInfo.NeType) + } + if neInfo.NeId != "" { + conditions = append(conditions, "ne_id = ?") + params = append(params, neInfo.NeId) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } else { + return "" + } + + // 查询数据 + querySql := "select id as 'str' from ne_info " + whereSql + " limit 1" + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err %v", err) + return "" + } + if len(results) > 0 { + return fmt.Sprint(results[0]["str"]) + } + return "" +} + +// Insert 新增信息 +func (r *NeInfo) Insert(neInfo model.NeInfo) string { + // 参数拼接 + params := make(map[string]any) + if neInfo.NeType != "" { + params["ne_type"] = neInfo.NeType + } + if neInfo.NeId != "" { + params["ne_id"] = neInfo.NeId + } + if neInfo.RmUID != "" { + params["rm_uid"] = neInfo.RmUID + } + if neInfo.NeName != "" { + params["ne_name"] = neInfo.NeName + } + if neInfo.IP != "" { + params["ip"] = neInfo.IP + } + if neInfo.Port > 0 { + params["port"] = neInfo.Port + } + if neInfo.PvFlag != "" { + params["pv_flag"] = neInfo.PvFlag + } + if neInfo.Province != "" { + params["province"] = neInfo.Province + } + if neInfo.VendorName != "" { + params["vendor_name"] = neInfo.VendorName + } + if neInfo.Dn != "" { + params["dn"] = neInfo.Dn + } + if neInfo.NeAddress != "" { + params["ne_address"] = neInfo.NeAddress + } + if neInfo.HostIDs != "" { + params["host_ids"] = neInfo.HostIDs + } + if neInfo.Status != "" { + params["status"] = neInfo.Status + } + if neInfo.Remark != "" { + params["remark"] = neInfo.Remark + } + if neInfo.CreateBy != "" { + params["create_by"] = neInfo.CreateBy + params["create_time"] = time.Now().UnixMilli() + } + + // 构建执行语句 + keys, placeholder, values := repo.KeyPlaceholderValueByInsert(params) + sql := "insert into ne_info (" + strings.Join(keys, ",") + ")values(" + placeholder + ")" + + db := datasource.DefaultDB() + // 开启事务 + tx := db.Begin() + // 执行插入 + err := tx.Exec(sql, values...).Error + if err != nil { + logger.Errorf("insert row : %v", err.Error()) + tx.Rollback() + return "" + } + // 获取生成的自增 ID + var insertedID string + err = tx.Raw("select last_insert_id()").Row().Scan(&insertedID) + if err != nil { + logger.Errorf("insert last id : %v", err.Error()) + tx.Rollback() + return "" + } + // 提交事务 + tx.Commit() + return insertedID +} + +// Update 修改信息 +func (r *NeInfo) Update(neInfo model.NeInfo) int64 { + // 参数拼接 + params := make(map[string]any) + if neInfo.NeType != "" { + params["ne_type"] = neInfo.NeType + } + if neInfo.NeId != "" { + params["ne_id"] = neInfo.NeId + } + if neInfo.RmUID != "" { + params["rm_uid"] = neInfo.RmUID + } + if neInfo.NeName != "" { + params["ne_name"] = neInfo.NeName + } + if neInfo.IP != "" { + params["ip"] = neInfo.IP + } + if neInfo.Port > 0 { + params["port"] = neInfo.Port + } + if neInfo.PvFlag != "" { + params["pv_flag"] = neInfo.PvFlag + } + params["province"] = neInfo.Province + params["vendor_name"] = neInfo.VendorName + params["dn"] = neInfo.Dn + params["ne_address"] = neInfo.NeAddress + if neInfo.HostIDs != "" { + params["host_ids"] = neInfo.HostIDs + } + params["remark"] = neInfo.Remark + if neInfo.Status != "" { + params["status"] = neInfo.Status + } + if neInfo.UpdateBy != "" { + params["update_by"] = neInfo.UpdateBy + params["update_time"] = time.Now().UnixMilli() + } + + // 构建执行语句 + keys, values := repo.KeyValueByUpdate(params) + sql := "update ne_info set " + strings.Join(keys, ",") + " where id = ?" + + // 执行更新 + values = append(values, neInfo.ID) + rows, err := datasource.ExecDB("", sql, values) + if err != nil { + logger.Errorf("update row : %v", err.Error()) + return 0 + } + return rows +} + +// DeleteByIds 批量删除网元信息 +func (r *NeInfo) DeleteByIds(infoIds []string) int64 { + placeholder := repo.KeyPlaceholderByQuery(len(infoIds)) + sql := "delete from ne_info where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(infoIds) + results, err := datasource.ExecDB("", sql, parameters) + if err != nil { + logger.Errorf("delete err => %v", err) + return 0 + } + return results } diff --git a/src/modules/network_element/repository/ne_info.impl.go b/src/modules/network_element/repository/ne_info.impl.go deleted file mode 100644 index 631a7e6a..00000000 --- a/src/modules/network_element/repository/ne_info.impl.go +++ /dev/null @@ -1,413 +0,0 @@ -package repository - -import ( - "fmt" - "sort" - "strings" - "time" - - "be.ems/src/framework/datasource" - "be.ems/src/framework/logger" - "be.ems/src/framework/utils/parse" - "be.ems/src/framework/utils/repo" - "be.ems/src/modules/network_element/model" -) - -// neListSort 网元列表预设排序 -var neListSort = []string{ - "OMC", - "IMS", - "AMF", - "AUSF", - "UDM", - "SMF", - "PCF", - "NSSF", - "NRF", - "UPF", - "LMF", - "NEF", - "MME", - "N3IWF", - "MOCNGW", - "SMSC", -} - -// 实例化数据层 NeInfoImpl 结构体 -var NewNeInfoImpl = &NeInfoImpl{ - selectSql: `select id, ne_type, ne_id, rm_uid, ne_name, ip, port, pv_flag, province, vendor_name, dn, ne_address, host_ids, status, remark, create_by, create_time, update_by, update_time from ne_info`, - - resultMap: map[string]string{ - "id": "ID", - "ne_type": "NeType", - "ne_id": "NeId", - "rm_uid": "RmUID", - "ne_name": "NeName", - "ip": "IP", - "port": "Port", - "pv_flag": "PvFlag", - "province": "Province", - "vendor_name": "VendorName", - "dn": "Dn", - "ne_address": "NeAddress", - "host_ids": "HostIDs", - "status": "Status", - "remark": "Remark", - "create_by": "CreateBy", - "create_time": "CreateTime", - "update_by": "UpdateBy", - "update_time": "UpdateTime", - }, -} - -// NeInfoImpl 网元信息表 数据层处理 -type NeInfoImpl struct { - // 查询视图对象SQL - selectSql string - // 结果字段与实体映射 - resultMap map[string]string -} - -// convertResultRows 将结果记录转实体结果组 -func (r *NeInfoImpl) convertResultRows(rows []map[string]any) []model.NeInfo { - arr := make([]model.NeInfo, 0) - for _, row := range rows { - item := model.NeInfo{} - for key, value := range row { - if keyMapper, ok := r.resultMap[key]; ok { - repo.SetFieldValue(&item, keyMapper, value) - } - } - arr = append(arr, item) - } - - // 排序 - sort.Slice(arr, func(i, j int) bool { - // 前一个 - after := arr[i] - afterIndex := 0 - for i, v := range neListSort { - if after.NeType == v { - afterIndex = i - break - } - } - // 后一个 - befter := arr[j] - befterIndex := 0 - for i, v := range neListSort { - if befter.NeType == v { - befterIndex = i - break - } - } - // 升序 - return afterIndex < befterIndex - }) - - return arr -} - -// SelectNeInfoByNeTypeAndNeID 通过ne_type和ne_id查询网元信息 -func (r *NeInfoImpl) SelectNeInfoByNeTypeAndNeID(neType, neID string) model.NeInfo { - querySql := r.selectSql + " where ne_type = ? and ne_id = ?" - results, err := datasource.RawDB("", querySql, []any{neType, neID}) - if err != nil { - logger.Errorf("query err => %v", err) - return model.NeInfo{} - } - // 转换实体 - rows := r.convertResultRows(results) - if len(rows) > 0 { - return rows[0] - } - return model.NeInfo{} -} - -// SelectPage 根据条件分页查询 -func (r *NeInfoImpl) SelectPage(query map[string]any) map[string]any { - // 查询条件拼接 - var conditions []string - var params []any - if v, ok := query["neType"]; ok && v != "" { - conditions = append(conditions, "ne_type = ?") - params = append(params, strings.Trim(v.(string), " ")) - } - if v, ok := query["neId"]; ok && v != "" { - conditions = append(conditions, "ne_id = ?") - params = append(params, strings.Trim(v.(string), " ")) - } - if v, ok := query["rmUid"]; ok && v != "" { - conditions = append(conditions, "rmUid like concat(?, '%')") - params = append(params, strings.Trim(v.(string), " ")) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } - - result := map[string]any{ - "total": 0, - "rows": []model.NeInfo{}, - } - - // 查询数量 长度为0直接返回 - totalSql := "select count(1) as 'total' from ne_info" - totalRows, err := datasource.RawDB("", totalSql+whereSql, params) - if err != nil { - logger.Errorf("total err => %v", err) - return result - } - total := parse.Number(totalRows[0]["total"]) - if total == 0 { - return result - } else { - result["total"] = total - } - - // 分页 - pageNum, pageSize := repo.PageNumSize(query["pageNum"], query["pageSize"]) - pageSql := " limit ?,? " - params = append(params, pageNum*pageSize) - params = append(params, pageSize) - - // 查询数据 - querySql := r.selectSql + whereSql + " order by ne_type asc, ne_id desc " + pageSql - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err => %v", err) - return result - } - - // 转换实体 - result["rows"] = r.convertResultRows(results) - return result -} - -// SelectList 查询列表 -func (r *NeInfoImpl) SelectList(neInfo model.NeInfo) []model.NeInfo { - // 查询条件拼接 - var conditions []string - var params []any - if neInfo.NeType != "" { - conditions = append(conditions, "ne_type = ?") - params = append(params, neInfo.NeType) - } - if neInfo.NeId != "" { - conditions = append(conditions, "ne_id = ?") - params = append(params, neInfo.NeId) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } - - // 查询数据 - querySql := r.selectSql + whereSql + " order by ne_type asc, ne_id desc " - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err => %v", err) - } - - // 转换实体 - return r.convertResultRows(results) -} - -// SelectByIds 通过ID查询 -func (r *NeInfoImpl) SelectByIds(infoIds []string) []model.NeInfo { - placeholder := repo.KeyPlaceholderByQuery(len(infoIds)) - querySql := r.selectSql + " where id in (" + placeholder + ")" - parameters := repo.ConvertIdsSlice(infoIds) - results, err := datasource.RawDB("", querySql, parameters) - if err != nil { - logger.Errorf("query err => %v", err) - return []model.NeInfo{} - } - // 转换实体 - return r.convertResultRows(results) -} - -// CheckUniqueNeTypeAndNeId 校验同类型下标识是否唯一 -func (r *NeInfoImpl) CheckUniqueNeTypeAndNeId(neInfo model.NeInfo) string { - // 查询条件拼接 - var conditions []string - var params []any - if neInfo.NeType != "" { - conditions = append(conditions, "ne_type = ?") - params = append(params, neInfo.NeType) - } - if neInfo.NeId != "" { - conditions = append(conditions, "ne_id = ?") - params = append(params, neInfo.NeId) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } else { - return "" - } - - // 查询数据 - querySql := "select id as 'str' from ne_info " + whereSql + " limit 1" - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err %v", err) - return "" - } - if len(results) > 0 { - return fmt.Sprint(results[0]["str"]) - } - return "" -} - -// Insert 新增信息 -func (r *NeInfoImpl) Insert(neInfo model.NeInfo) string { - // 参数拼接 - params := make(map[string]any) - if neInfo.NeType != "" { - params["ne_type"] = neInfo.NeType - } - if neInfo.NeId != "" { - params["ne_id"] = neInfo.NeId - } - if neInfo.RmUID != "" { - params["rm_uid"] = neInfo.RmUID - } - if neInfo.NeName != "" { - params["ne_name"] = neInfo.NeName - } - if neInfo.IP != "" { - params["ip"] = neInfo.IP - } - if neInfo.Port > 0 { - params["port"] = neInfo.Port - } - if neInfo.PvFlag != "" { - params["pv_flag"] = neInfo.PvFlag - } - if neInfo.Province != "" { - params["province"] = neInfo.Province - } - if neInfo.VendorName != "" { - params["vendor_name"] = neInfo.VendorName - } - if neInfo.Dn != "" { - params["dn"] = neInfo.Dn - } - if neInfo.NeAddress != "" { - params["ne_address"] = neInfo.NeAddress - } - if neInfo.HostIDs != "" { - params["host_ids"] = neInfo.HostIDs - } - if neInfo.Status != "" { - params["status"] = neInfo.Status - } - if neInfo.Remark != "" { - params["remark"] = neInfo.Remark - } - if neInfo.CreateBy != "" { - params["create_by"] = neInfo.CreateBy - params["create_time"] = time.Now().UnixMilli() - } - - // 构建执行语句 - keys, placeholder, values := repo.KeyPlaceholderValueByInsert(params) - sql := "insert into ne_info (" + strings.Join(keys, ",") + ")values(" + placeholder + ")" - - db := datasource.DefaultDB() - // 开启事务 - tx := db.Begin() - // 执行插入 - err := tx.Exec(sql, values...).Error - if err != nil { - logger.Errorf("insert row : %v", err.Error()) - tx.Rollback() - return "" - } - // 获取生成的自增 ID - var insertedID string - err = tx.Raw("select last_insert_id()").Row().Scan(&insertedID) - if err != nil { - logger.Errorf("insert last id : %v", err.Error()) - tx.Rollback() - return "" - } - // 提交事务 - tx.Commit() - return insertedID -} - -// Update 修改信息 -func (r *NeInfoImpl) Update(neInfo model.NeInfo) int64 { - // 参数拼接 - params := make(map[string]any) - if neInfo.NeType != "" { - params["ne_type"] = neInfo.NeType - } - if neInfo.NeId != "" { - params["ne_id"] = neInfo.NeId - } - if neInfo.RmUID != "" { - params["rm_uid"] = neInfo.RmUID - } - if neInfo.NeName != "" { - params["ne_name"] = neInfo.NeName - } - if neInfo.IP != "" { - params["ip"] = neInfo.IP - } - if neInfo.Port > 0 { - params["port"] = neInfo.Port - } - if neInfo.PvFlag != "" { - params["pv_flag"] = neInfo.PvFlag - } - params["province"] = neInfo.Province - params["vendor_name"] = neInfo.VendorName - params["dn"] = neInfo.Dn - params["ne_address"] = neInfo.NeAddress - if neInfo.HostIDs != "" { - params["host_ids"] = neInfo.HostIDs - } - params["remark"] = neInfo.Remark - if neInfo.Status != "" { - params["status"] = neInfo.Status - } - if neInfo.UpdateBy != "" { - params["update_by"] = neInfo.UpdateBy - params["update_time"] = time.Now().UnixMilli() - } - - // 构建执行语句 - keys, values := repo.KeyValueByUpdate(params) - sql := "update ne_info set " + strings.Join(keys, ",") + " where id = ?" - - // 执行更新 - values = append(values, neInfo.ID) - rows, err := datasource.ExecDB("", sql, values) - if err != nil { - logger.Errorf("update row : %v", err.Error()) - return 0 - } - return rows -} - -// DeleteByIds 批量删除网元信息 -func (r *NeInfoImpl) DeleteByIds(infoIds []string) int64 { - placeholder := repo.KeyPlaceholderByQuery(len(infoIds)) - sql := "delete from ne_info where id in (" + placeholder + ")" - parameters := repo.ConvertIdsSlice(infoIds) - results, err := datasource.ExecDB("", sql, parameters) - if err != nil { - logger.Errorf("delete err => %v", err) - return 0 - } - return results -} diff --git a/src/modules/network_element/repository/ne_license.go b/src/modules/network_element/repository/ne_license.go index 83cc42ac..5aad6b32 100644 --- a/src/modules/network_element/repository/ne_license.go +++ b/src/modules/network_element/repository/ne_license.go @@ -1,24 +1,297 @@ package repository -import "be.ems/src/modules/network_element/model" +import ( + "strings" + "time" -// INeLicense 网元授权激活信息 数据层接口 -type INeLicense interface { - // SelectPage 根据条件分页查询字典类型 - SelectPage(query map[string]any) map[string]any + "be.ems/src/framework/datasource" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/parse" + "be.ems/src/framework/utils/repo" + "be.ems/src/modules/network_element/model" +) - // SelectList 根据实体查询 - SelectList(neLicense model.NeLicense) []model.NeLicense +// 实例化数据层 NeLicense 结构体 +var NewNeLicense = &NeLicense{ + selectSql: `select + id, ne_type, ne_id, activation_request_code, license_path, serial_num, expiry_date, status, remark, create_by, create_time, update_by, update_time + from ne_license`, - // SelectByIds 通过ID查询 - SelectByIds(ids []string) []model.NeLicense - - // Insert 新增信息 - Insert(neLicense model.NeLicense) string - - // Update 修改信息 - Update(neLicense model.NeLicense) int64 - - // DeleteByIds 批量删除信息 - DeleteByIds(ids []string) int64 + resultMap: map[string]string{ + "id": "ID", + "ne_type": "NeType", + "ne_id": "NeId", + "activation_request_code": "ActivationRequestCode", + "license_path": "LicensePath", + "serial_num": "SerialNum", + "expiry_date": "ExpiryDate", + "status": "Status", + "remark": "Remark", + "create_by": "CreateBy", + "create_time": "CreateTime", + "update_by": "UpdateBy", + "update_time": "UpdateTime", + }, +} + +// NeLicense 网元授权激活信息 数据层处理 +type NeLicense struct { + // 查询视图对象SQL + selectSql string + // 结果字段与实体映射 + resultMap map[string]string +} + +// convertResultRows 将结果记录转实体结果组 +func (r *NeLicense) convertResultRows(rows []map[string]any) []model.NeLicense { + arr := make([]model.NeLicense, 0) + for _, row := range rows { + item := model.NeLicense{} + for key, value := range row { + if keyMapper, ok := r.resultMap[key]; ok { + repo.SetFieldValue(&item, keyMapper, value) + } + } + arr = append(arr, item) + } + return arr +} + +// SelectPage 根据条件分页查询字典类型 +func (r *NeLicense) SelectPage(query map[string]any) map[string]any { + // 查询条件拼接 + var conditions []string + var params []any + if v, ok := query["neType"]; ok && v != "" { + conditions = append(conditions, "ne_type = ?") + params = append(params, strings.Trim(v.(string), " ")) + } + if v, ok := query["neId"]; ok && v != "" { + conditions = append(conditions, "ne_id = ?") + params = append(params, strings.Trim(v.(string), " ")) + } + if v, ok := query["expiryDate"]; ok && v != "" { + conditions = append(conditions, "expiry_date = ?") + params = append(params, strings.Trim(v.(string), " ")) + } + if v, ok := query["createBy"]; ok && v != "" { + conditions = append(conditions, "create_by like concat(?, '%')") + params = append(params, strings.Trim(v.(string), " ")) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + result := map[string]any{ + "total": 0, + "rows": []model.NeHost{}, + } + + // 查询数量 长度为0直接返回 + totalSql := "select count(1) as 'total' from ne_license" + totalRows, err := datasource.RawDB("", totalSql+whereSql, params) + if err != nil { + logger.Errorf("total err => %v", err) + return result + } + total := parse.Number(totalRows[0]["total"]) + if total == 0 { + return result + } else { + result["total"] = total + } + + // 分页 + pageNum, pageSize := repo.PageNumSize(query["pageNum"], query["pageSize"]) + pageSql := " limit ?,? " + params = append(params, pageNum*pageSize) + params = append(params, pageSize) + + // 查询数据 + querySql := r.selectSql + whereSql + pageSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + return result + } + + // 转换实体 + result["rows"] = r.convertResultRows(results) + return result +} + +// SelectList 根据实体查询 +func (r *NeLicense) SelectList(neLicense model.NeLicense) []model.NeLicense { + // 查询条件拼接 + var conditions []string + var params []any + if neLicense.NeType != "" { + conditions = append(conditions, "ne_type = ?") + params = append(params, neLicense.NeType) + } + if neLicense.NeId != "" { + conditions = append(conditions, "ne_id = ?") + params = append(params, neLicense.NeId) + } + if neLicense.ExpiryDate != "" { + conditions = append(conditions, "expiry_date = ?") + params = append(params, neLicense.ExpiryDate) + } + if neLicense.CreateBy != "" { + conditions = append(conditions, "create_by like concat(?, '%')") + params = append(params, neLicense.CreateBy) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询数据 + querySql := r.selectSql + whereSql + " order by id asc " + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + } + + // 转换实体 + return r.convertResultRows(results) +} + +// SelectByIds 通过ID查询 +func (r *NeLicense) SelectByIds(cmdIds []string) []model.NeLicense { + placeholder := repo.KeyPlaceholderByQuery(len(cmdIds)) + querySql := r.selectSql + " where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(cmdIds) + results, err := datasource.RawDB("", querySql, parameters) + if err != nil { + logger.Errorf("query err => %v", err) + return []model.NeLicense{} + } + // 转换实体 + return r.convertResultRows(results) +} + +// Insert 新增信息 +func (r *NeLicense) Insert(neLicense model.NeLicense) string { + // 参数拼接 + params := make(map[string]any) + if neLicense.NeType != "" { + params["ne_type"] = neLicense.NeType + } + if neLicense.NeId != "" { + params["ne_id"] = neLicense.NeId + } + if neLicense.ActivationRequestCode != "" { + params["activation_request_code"] = neLicense.ActivationRequestCode + } + if neLicense.LicensePath != "" { + params["license_path"] = neLicense.LicensePath + } + if neLicense.SerialNum != "" { + params["serial_num"] = neLicense.SerialNum + } + if neLicense.ExpiryDate != "" { + params["expiry_date"] = neLicense.ExpiryDate + } + if neLicense.Status != "" { + params["status"] = neLicense.Status + } + if neLicense.Remark != "" { + params["remark"] = neLicense.Remark + } + if neLicense.CreateBy != "" { + params["create_by"] = neLicense.CreateBy + params["create_time"] = time.Now().UnixMilli() + } + + // 构建执行语句 + keys, placeholder, values := repo.KeyPlaceholderValueByInsert(params) + sql := "insert into ne_license (" + strings.Join(keys, ",") + ")values(" + placeholder + ")" + + db := datasource.DefaultDB() + // 开启事务 + tx := db.Begin() + // 执行插入 + err := tx.Exec(sql, values...).Error + if err != nil { + logger.Errorf("insert row : %v", err.Error()) + tx.Rollback() + return "" + } + // 获取生成的自增 ID + var insertedID string + err = tx.Raw("select last_insert_id()").Row().Scan(&insertedID) + if err != nil { + logger.Errorf("insert last id : %v", err.Error()) + tx.Rollback() + return "" + } + // 提交事务 + tx.Commit() + return insertedID +} + +// Update 修改信息 +func (r *NeLicense) Update(neLicense model.NeLicense) int64 { + // 参数拼接 + params := make(map[string]any) + if neLicense.NeType != "" { + params["ne_type"] = neLicense.NeType + } + if neLicense.NeId != "" { + params["ne_id"] = neLicense.NeId + } + if neLicense.ActivationRequestCode != "" { + params["activation_request_code"] = neLicense.ActivationRequestCode + } + if neLicense.LicensePath != "" { + params["license_path"] = neLicense.LicensePath + } + if neLicense.SerialNum != "" { + params["serial_num"] = neLicense.SerialNum + } + if neLicense.ExpiryDate != "" { + params["expiry_date"] = neLicense.ExpiryDate + } + if neLicense.Status != "" { + params["status"] = neLicense.Status + } + if neLicense.Remark != "" { + params["remark"] = neLicense.Remark + } + if neLicense.UpdateBy != "" { + params["update_by"] = neLicense.UpdateBy + params["update_time"] = time.Now().UnixMilli() + } + + // 构建执行语句 + keys, values := repo.KeyValueByUpdate(params) + sql := "update ne_license set " + strings.Join(keys, ",") + " where id = ?" + + // 执行更新 + values = append(values, neLicense.ID) + rows, err := datasource.ExecDB("", sql, values) + if err != nil { + logger.Errorf("update row : %v", err.Error()) + return 0 + } + return rows +} + +// DeleteByIds 批量删除信息 +func (r *NeLicense) DeleteByIds(cmdIds []string) int64 { + placeholder := repo.KeyPlaceholderByQuery(len(cmdIds)) + sql := "delete from ne_license where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(cmdIds) + results, err := datasource.ExecDB("", sql, parameters) + if err != nil { + logger.Errorf("delete err => %v", err) + return 0 + } + return results } diff --git a/src/modules/network_element/repository/ne_license.impl.go b/src/modules/network_element/repository/ne_license.impl.go deleted file mode 100644 index 0daee946..00000000 --- a/src/modules/network_element/repository/ne_license.impl.go +++ /dev/null @@ -1,297 +0,0 @@ -package repository - -import ( - "strings" - "time" - - "be.ems/src/framework/datasource" - "be.ems/src/framework/logger" - "be.ems/src/framework/utils/parse" - "be.ems/src/framework/utils/repo" - "be.ems/src/modules/network_element/model" -) - -// 实例化数据层 NewNeLicense 结构体 -var NewNeLicenseImpl = &NeLicenseImpl{ - selectSql: `select - id, ne_type, ne_id, activation_request_code, license_path, serial_num, expiry_date, status, remark, create_by, create_time, update_by, update_time - from ne_license`, - - resultMap: map[string]string{ - "id": "ID", - "ne_type": "NeType", - "ne_id": "NeId", - "activation_request_code": "ActivationRequestCode", - "license_path": "LicensePath", - "serial_num": "SerialNum", - "expiry_date": "ExpiryDate", - "status": "Status", - "remark": "Remark", - "create_by": "CreateBy", - "create_time": "CreateTime", - "update_by": "UpdateBy", - "update_time": "UpdateTime", - }, -} - -// NeLicenseImpl 网元授权激活信息 数据层处理 -type NeLicenseImpl struct { - // 查询视图对象SQL - selectSql string - // 结果字段与实体映射 - resultMap map[string]string -} - -// convertResultRows 将结果记录转实体结果组 -func (r *NeLicenseImpl) convertResultRows(rows []map[string]any) []model.NeLicense { - arr := make([]model.NeLicense, 0) - for _, row := range rows { - item := model.NeLicense{} - for key, value := range row { - if keyMapper, ok := r.resultMap[key]; ok { - repo.SetFieldValue(&item, keyMapper, value) - } - } - arr = append(arr, item) - } - return arr -} - -// SelectPage 根据条件分页查询字典类型 -func (r *NeLicenseImpl) SelectPage(query map[string]any) map[string]any { - // 查询条件拼接 - var conditions []string - var params []any - if v, ok := query["neType"]; ok && v != "" { - conditions = append(conditions, "ne_type = ?") - params = append(params, strings.Trim(v.(string), " ")) - } - if v, ok := query["neId"]; ok && v != "" { - conditions = append(conditions, "ne_id = ?") - params = append(params, strings.Trim(v.(string), " ")) - } - if v, ok := query["expiryDate"]; ok && v != "" { - conditions = append(conditions, "expiry_date = ?") - params = append(params, strings.Trim(v.(string), " ")) - } - if v, ok := query["createBy"]; ok && v != "" { - conditions = append(conditions, "create_by like concat(?, '%')") - params = append(params, strings.Trim(v.(string), " ")) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } - - result := map[string]any{ - "total": 0, - "rows": []model.NeHost{}, - } - - // 查询数量 长度为0直接返回 - totalSql := "select count(1) as 'total' from ne_license" - totalRows, err := datasource.RawDB("", totalSql+whereSql, params) - if err != nil { - logger.Errorf("total err => %v", err) - return result - } - total := parse.Number(totalRows[0]["total"]) - if total == 0 { - return result - } else { - result["total"] = total - } - - // 分页 - pageNum, pageSize := repo.PageNumSize(query["pageNum"], query["pageSize"]) - pageSql := " limit ?,? " - params = append(params, pageNum*pageSize) - params = append(params, pageSize) - - // 查询数据 - querySql := r.selectSql + whereSql + pageSql - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err => %v", err) - return result - } - - // 转换实体 - result["rows"] = r.convertResultRows(results) - return result -} - -// SelectList 根据实体查询 -func (r *NeLicenseImpl) SelectList(neLicense model.NeLicense) []model.NeLicense { - // 查询条件拼接 - var conditions []string - var params []any - if neLicense.NeType != "" { - conditions = append(conditions, "ne_type = ?") - params = append(params, neLicense.NeType) - } - if neLicense.NeId != "" { - conditions = append(conditions, "ne_id = ?") - params = append(params, neLicense.NeId) - } - if neLicense.ExpiryDate != "" { - conditions = append(conditions, "expiry_date = ?") - params = append(params, neLicense.ExpiryDate) - } - if neLicense.CreateBy != "" { - conditions = append(conditions, "create_by like concat(?, '%')") - params = append(params, neLicense.CreateBy) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } - - // 查询数据 - querySql := r.selectSql + whereSql + " order by id asc " - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err => %v", err) - } - - // 转换实体 - return r.convertResultRows(results) -} - -// SelectByIds 通过ID查询 -func (r *NeLicenseImpl) SelectByIds(cmdIds []string) []model.NeLicense { - placeholder := repo.KeyPlaceholderByQuery(len(cmdIds)) - querySql := r.selectSql + " where id in (" + placeholder + ")" - parameters := repo.ConvertIdsSlice(cmdIds) - results, err := datasource.RawDB("", querySql, parameters) - if err != nil { - logger.Errorf("query err => %v", err) - return []model.NeLicense{} - } - // 转换实体 - return r.convertResultRows(results) -} - -// Insert 新增信息 -func (r *NeLicenseImpl) Insert(neLicense model.NeLicense) string { - // 参数拼接 - params := make(map[string]any) - if neLicense.NeType != "" { - params["ne_type"] = neLicense.NeType - } - if neLicense.NeId != "" { - params["ne_id"] = neLicense.NeId - } - if neLicense.ActivationRequestCode != "" { - params["activation_request_code"] = neLicense.ActivationRequestCode - } - if neLicense.LicensePath != "" { - params["license_path"] = neLicense.LicensePath - } - if neLicense.SerialNum != "" { - params["serial_num"] = neLicense.SerialNum - } - if neLicense.ExpiryDate != "" { - params["expiry_date"] = neLicense.ExpiryDate - } - if neLicense.Status != "" { - params["status"] = neLicense.Status - } - if neLicense.Remark != "" { - params["remark"] = neLicense.Remark - } - if neLicense.CreateBy != "" { - params["create_by"] = neLicense.CreateBy - params["create_time"] = time.Now().UnixMilli() - } - - // 构建执行语句 - keys, placeholder, values := repo.KeyPlaceholderValueByInsert(params) - sql := "insert into ne_license (" + strings.Join(keys, ",") + ")values(" + placeholder + ")" - - db := datasource.DefaultDB() - // 开启事务 - tx := db.Begin() - // 执行插入 - err := tx.Exec(sql, values...).Error - if err != nil { - logger.Errorf("insert row : %v", err.Error()) - tx.Rollback() - return "" - } - // 获取生成的自增 ID - var insertedID string - err = tx.Raw("select last_insert_id()").Row().Scan(&insertedID) - if err != nil { - logger.Errorf("insert last id : %v", err.Error()) - tx.Rollback() - return "" - } - // 提交事务 - tx.Commit() - return insertedID -} - -// Update 修改信息 -func (r *NeLicenseImpl) Update(neLicense model.NeLicense) int64 { - // 参数拼接 - params := make(map[string]any) - if neLicense.NeType != "" { - params["ne_type"] = neLicense.NeType - } - if neLicense.NeId != "" { - params["ne_id"] = neLicense.NeId - } - if neLicense.ActivationRequestCode != "" { - params["activation_request_code"] = neLicense.ActivationRequestCode - } - if neLicense.LicensePath != "" { - params["license_path"] = neLicense.LicensePath - } - if neLicense.SerialNum != "" { - params["serial_num"] = neLicense.SerialNum - } - if neLicense.ExpiryDate != "" { - params["expiry_date"] = neLicense.ExpiryDate - } - if neLicense.Status != "" { - params["status"] = neLicense.Status - } - if neLicense.Remark != "" { - params["remark"] = neLicense.Remark - } - if neLicense.UpdateBy != "" { - params["update_by"] = neLicense.UpdateBy - params["update_time"] = time.Now().UnixMilli() - } - - // 构建执行语句 - keys, values := repo.KeyValueByUpdate(params) - sql := "update ne_license set " + strings.Join(keys, ",") + " where id = ?" - - // 执行更新 - values = append(values, neLicense.ID) - rows, err := datasource.ExecDB("", sql, values) - if err != nil { - logger.Errorf("update row : %v", err.Error()) - return 0 - } - return rows -} - -// DeleteByIds 批量删除信息 -func (r *NeLicenseImpl) DeleteByIds(cmdIds []string) int64 { - placeholder := repo.KeyPlaceholderByQuery(len(cmdIds)) - sql := "delete from ne_license where id in (" + placeholder + ")" - parameters := repo.ConvertIdsSlice(cmdIds) - results, err := datasource.ExecDB("", sql, parameters) - if err != nil { - logger.Errorf("delete err => %v", err) - return 0 - } - return results -} diff --git a/src/modules/network_element/repository/ne_software.go b/src/modules/network_element/repository/ne_software.go index f6d0f23e..d039ec6d 100644 --- a/src/modules/network_element/repository/ne_software.go +++ b/src/modules/network_element/repository/ne_software.go @@ -1,27 +1,317 @@ package repository -import "be.ems/src/modules/network_element/model" +import ( + "fmt" + "strings" + "time" -// INeSoftware 网元软件包信息 数据层接口 -type INeSoftware interface { - // SelectPage 根据条件分页查询字典类型 - SelectPage(query map[string]any) map[string]any + "be.ems/src/framework/datasource" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/parse" + "be.ems/src/framework/utils/repo" + "be.ems/src/modules/network_element/model" +) - // SelectList 根据实体查询 - SelectList(neSoftware model.NeSoftware) []model.NeSoftware +// 实例化数据层 NeSoftware 结构体 +var NewNeSoftware = &NeSoftware{ + selectSql: `select + id, ne_type, name, path, version, description, create_by, create_time, update_by, update_time + from ne_software`, - // SelectByIds 通过ID查询 - SelectByIds(ids []string) []model.NeSoftware - - // Insert 新增信息 - Insert(neSoftware model.NeSoftware) string - - // Update 修改信息 - Update(neSoftware model.NeSoftware) int64 - - // DeleteByIds 批量删除信息 - DeleteByIds(ids []string) int64 - - // CheckUniqueTypeAndNameAndVersion 校验网元类型和文件名版本是否唯一 - CheckUniqueTypeAndNameAndVersion(neSoftware model.NeSoftware) string + resultMap: map[string]string{ + "id": "ID", + "ne_type": "NeType", + "name": "Name", + "path": "Path", + "version": "Version", + "description": "Description", + "create_by": "CreateBy", + "create_time": "CreateTime", + "update_by": "UpdateBy", + "update_time": "UpdateTime", + }, +} + +// NeSoftware 网元软件包信息 数据层处理 +type NeSoftware struct { + // 查询视图对象SQL + selectSql string + // 结果字段与实体映射 + resultMap map[string]string +} + +// convertResultRows 将结果记录转实体结果组 +func (r *NeSoftware) convertResultRows(rows []map[string]any) []model.NeSoftware { + arr := make([]model.NeSoftware, 0) + for _, row := range rows { + item := model.NeSoftware{} + for key, value := range row { + if keyMapper, ok := r.resultMap[key]; ok { + repo.SetFieldValue(&item, keyMapper, value) + } + } + arr = append(arr, item) + } + return arr +} + +// SelectPage 根据条件分页查询字典类型 +func (r *NeSoftware) SelectPage(query map[string]any) map[string]any { + // 查询条件拼接 + var conditions []string + var params []any + if v, ok := query["neType"]; ok && v != "" { + softwareType := v.(string) + if strings.Contains(softwareType, ",") { + softwareTypeArr := strings.Split(softwareType, ",") + placeholder := repo.KeyPlaceholderByQuery(len(softwareTypeArr)) + conditions = append(conditions, "ne_type in ("+placeholder+")") + parameters := repo.ConvertIdsSlice(softwareTypeArr) + params = append(params, parameters...) + } else { + conditions = append(conditions, "ne_type = ?") + params = append(params, strings.Trim(softwareType, " ")) + } + } + if v, ok := query["name"]; ok && v != "" { + conditions = append(conditions, "name like concat(?, '%')") + params = append(params, strings.Trim(v.(string), " ")) + } + if v, ok := query["version"]; ok && v != "" { + conditions = append(conditions, "version like concat(?, '%')") + params = append(params, strings.Trim(v.(string), " ")) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + result := map[string]any{ + "total": 0, + "rows": []model.NeHost{}, + } + + // 查询数量 长度为0直接返回 + totalSql := "select count(1) as 'total' from ne_software" + totalRows, err := datasource.RawDB("", totalSql+whereSql, params) + if err != nil { + logger.Errorf("total err => %v", err) + return result + } + total := parse.Number(totalRows[0]["total"]) + if total == 0 { + return result + } else { + result["total"] = total + } + + // 分页 + pageNum, pageSize := repo.PageNumSize(query["pageNum"], query["pageSize"]) + pageSql := " order by id desc limit ?,? " + params = append(params, pageNum*pageSize) + params = append(params, pageSize) + + // 查询数据 + querySql := r.selectSql + whereSql + pageSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + return result + } + + // 转换实体 + result["rows"] = r.convertResultRows(results) + return result +} + +// SelectList 根据实体查询 +func (r *NeSoftware) SelectList(neSoftware model.NeSoftware) []model.NeSoftware { + // 查询条件拼接 + var conditions []string + var params []any + if neSoftware.NeType != "" { + conditions = append(conditions, "ne_type = ?") + params = append(params, neSoftware.NeType) + } + if neSoftware.Path != "" { + conditions = append(conditions, "path = ?") + params = append(params, neSoftware.Path) + } + if neSoftware.Version != "" { + conditions = append(conditions, "version = ?") + params = append(params, neSoftware.Version) + } + if neSoftware.Name != "" { + conditions = append(conditions, "name like concat(?, '%')") + params = append(params, neSoftware.Name) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询数据 + querySql := r.selectSql + whereSql + " order by id desc " + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + } + + // 转换实体 + return r.convertResultRows(results) +} + +// SelectByIds 通过ID查询 +func (r *NeSoftware) SelectByIds(cmdIds []string) []model.NeSoftware { + placeholder := repo.KeyPlaceholderByQuery(len(cmdIds)) + querySql := r.selectSql + " where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(cmdIds) + results, err := datasource.RawDB("", querySql, parameters) + if err != nil { + logger.Errorf("query err => %v", err) + return []model.NeSoftware{} + } + // 转换实体 + return r.convertResultRows(results) +} + +// CheckUniqueTypeAndNameAndVersion 校验网元类型和文件名版本是否唯一 +func (r *NeSoftware) CheckUniqueTypeAndNameAndVersion(neSoftware model.NeSoftware) string { + // 查询条件拼接 + var conditions []string + var params []any + if neSoftware.NeType != "" { + conditions = append(conditions, "ne_type = ?") + params = append(params, neSoftware.NeType) + } + if neSoftware.Version != "" { + conditions = append(conditions, "version = ?") + params = append(params, neSoftware.Version) + } + if neSoftware.Name != "" { + conditions = append(conditions, "name = ?") + params = append(params, neSoftware.Name) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } else { + return "" + } + + // 查询数据 + querySql := "select id as 'str' from ne_software " + whereSql + " limit 1" + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err %v", err) + return "" + } + if len(results) > 0 { + return fmt.Sprint(results[0]["str"]) + } + return "" +} + +// Insert 新增信息 +func (r *NeSoftware) Insert(neSoftware model.NeSoftware) string { + // 参数拼接 + params := make(map[string]any) + if neSoftware.NeType != "" { + params["ne_type"] = neSoftware.NeType + } + if neSoftware.Name != "" { + params["name"] = neSoftware.Name + } + if neSoftware.Path != "" { + params["path"] = neSoftware.Path + } + if neSoftware.Version != "" { + params["version"] = neSoftware.Version + } + params["description"] = neSoftware.Description + if neSoftware.CreateBy != "" { + params["create_by"] = neSoftware.CreateBy + params["create_time"] = time.Now().UnixMilli() + } + + // 构建执行语句 + keys, placeholder, values := repo.KeyPlaceholderValueByInsert(params) + sql := "insert into ne_software (" + strings.Join(keys, ",") + ")values(" + placeholder + ")" + + db := datasource.DefaultDB() + // 开启事务 + tx := db.Begin() + // 执行插入 + err := tx.Exec(sql, values...).Error + if err != nil { + logger.Errorf("insert row : %v", err.Error()) + tx.Rollback() + return "" + } + // 获取生成的自增 ID + var insertedID string + err = tx.Raw("select last_insert_id()").Row().Scan(&insertedID) + if err != nil { + logger.Errorf("insert last id : %v", err.Error()) + tx.Rollback() + return "" + } + // 提交事务 + tx.Commit() + return insertedID +} + +// Update 修改信息 +func (r *NeSoftware) Update(neSoftware model.NeSoftware) int64 { + // 参数拼接 + params := make(map[string]any) + if neSoftware.NeType != "" { + params["ne_type"] = neSoftware.NeType + } + if neSoftware.Name != "" { + params["name"] = neSoftware.Name + } + if neSoftware.Path != "" { + params["path"] = neSoftware.Path + } + if neSoftware.Version != "" { + params["version"] = neSoftware.Version + } + params["description"] = neSoftware.Description + if neSoftware.UpdateBy != "" { + params["update_by"] = neSoftware.UpdateBy + params["update_time"] = time.Now().UnixMilli() + } + + // 构建执行语句 + keys, values := repo.KeyValueByUpdate(params) + sql := "update ne_software set " + strings.Join(keys, ",") + " where id = ?" + + // 执行更新 + values = append(values, neSoftware.ID) + rows, err := datasource.ExecDB("", sql, values) + if err != nil { + logger.Errorf("update row : %v", err.Error()) + return 0 + } + return rows +} + +// DeleteByIds 批量删除信息 +func (r *NeSoftware) DeleteByIds(cmdIds []string) int64 { + placeholder := repo.KeyPlaceholderByQuery(len(cmdIds)) + sql := "delete from ne_software where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(cmdIds) + results, err := datasource.ExecDB("", sql, parameters) + if err != nil { + logger.Errorf("delete err => %v", err) + return 0 + } + return results } diff --git a/src/modules/network_element/repository/ne_software.impl.go b/src/modules/network_element/repository/ne_software.impl.go deleted file mode 100644 index 5aa8cda6..00000000 --- a/src/modules/network_element/repository/ne_software.impl.go +++ /dev/null @@ -1,317 +0,0 @@ -package repository - -import ( - "fmt" - "strings" - "time" - - "be.ems/src/framework/datasource" - "be.ems/src/framework/logger" - "be.ems/src/framework/utils/parse" - "be.ems/src/framework/utils/repo" - "be.ems/src/modules/network_element/model" -) - -// 实例化数据层 NewNeSoftware 结构体 -var NewNeSoftwareImpl = &NeSoftwareImpl{ - selectSql: `select - id, ne_type, name, path, version, description, create_by, create_time, update_by, update_time - from ne_software`, - - resultMap: map[string]string{ - "id": "ID", - "ne_type": "NeType", - "name": "Name", - "path": "Path", - "version": "Version", - "description": "Description", - "create_by": "CreateBy", - "create_time": "CreateTime", - "update_by": "UpdateBy", - "update_time": "UpdateTime", - }, -} - -// NeSoftwareImpl 网元软件包信息 数据层处理 -type NeSoftwareImpl struct { - // 查询视图对象SQL - selectSql string - // 结果字段与实体映射 - resultMap map[string]string -} - -// convertResultRows 将结果记录转实体结果组 -func (r *NeSoftwareImpl) convertResultRows(rows []map[string]any) []model.NeSoftware { - arr := make([]model.NeSoftware, 0) - for _, row := range rows { - item := model.NeSoftware{} - for key, value := range row { - if keyMapper, ok := r.resultMap[key]; ok { - repo.SetFieldValue(&item, keyMapper, value) - } - } - arr = append(arr, item) - } - return arr -} - -// SelectPage 根据条件分页查询字典类型 -func (r *NeSoftwareImpl) SelectPage(query map[string]any) map[string]any { - // 查询条件拼接 - var conditions []string - var params []any - if v, ok := query["neType"]; ok && v != "" { - softwareType := v.(string) - if strings.Contains(softwareType, ",") { - softwareTypeArr := strings.Split(softwareType, ",") - placeholder := repo.KeyPlaceholderByQuery(len(softwareTypeArr)) - conditions = append(conditions, "ne_type in ("+placeholder+")") - parameters := repo.ConvertIdsSlice(softwareTypeArr) - params = append(params, parameters...) - } else { - conditions = append(conditions, "ne_type = ?") - params = append(params, strings.Trim(softwareType, " ")) - } - } - if v, ok := query["name"]; ok && v != "" { - conditions = append(conditions, "name like concat(?, '%')") - params = append(params, strings.Trim(v.(string), " ")) - } - if v, ok := query["version"]; ok && v != "" { - conditions = append(conditions, "version like concat(?, '%')") - params = append(params, strings.Trim(v.(string), " ")) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } - - result := map[string]any{ - "total": 0, - "rows": []model.NeHost{}, - } - - // 查询数量 长度为0直接返回 - totalSql := "select count(1) as 'total' from ne_software" - totalRows, err := datasource.RawDB("", totalSql+whereSql, params) - if err != nil { - logger.Errorf("total err => %v", err) - return result - } - total := parse.Number(totalRows[0]["total"]) - if total == 0 { - return result - } else { - result["total"] = total - } - - // 分页 - pageNum, pageSize := repo.PageNumSize(query["pageNum"], query["pageSize"]) - pageSql := " order by id desc limit ?,? " - params = append(params, pageNum*pageSize) - params = append(params, pageSize) - - // 查询数据 - querySql := r.selectSql + whereSql + pageSql - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err => %v", err) - return result - } - - // 转换实体 - result["rows"] = r.convertResultRows(results) - return result -} - -// SelectList 根据实体查询 -func (r *NeSoftwareImpl) SelectList(neSoftware model.NeSoftware) []model.NeSoftware { - // 查询条件拼接 - var conditions []string - var params []any - if neSoftware.NeType != "" { - conditions = append(conditions, "ne_type = ?") - params = append(params, neSoftware.NeType) - } - if neSoftware.Path != "" { - conditions = append(conditions, "path = ?") - params = append(params, neSoftware.Path) - } - if neSoftware.Version != "" { - conditions = append(conditions, "version = ?") - params = append(params, neSoftware.Version) - } - if neSoftware.Name != "" { - conditions = append(conditions, "name like concat(?, '%')") - params = append(params, neSoftware.Name) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } - - // 查询数据 - querySql := r.selectSql + whereSql + " order by id desc " - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err => %v", err) - } - - // 转换实体 - return r.convertResultRows(results) -} - -// SelectByIds 通过ID查询 -func (r *NeSoftwareImpl) SelectByIds(cmdIds []string) []model.NeSoftware { - placeholder := repo.KeyPlaceholderByQuery(len(cmdIds)) - querySql := r.selectSql + " where id in (" + placeholder + ")" - parameters := repo.ConvertIdsSlice(cmdIds) - results, err := datasource.RawDB("", querySql, parameters) - if err != nil { - logger.Errorf("query err => %v", err) - return []model.NeSoftware{} - } - // 转换实体 - return r.convertResultRows(results) -} - -// CheckUniqueTypeAndNameAndVersion 校验网元类型和文件名版本是否唯一 -func (r *NeSoftwareImpl) CheckUniqueTypeAndNameAndVersion(neSoftware model.NeSoftware) string { - // 查询条件拼接 - var conditions []string - var params []any - if neSoftware.NeType != "" { - conditions = append(conditions, "ne_type = ?") - params = append(params, neSoftware.NeType) - } - if neSoftware.Version != "" { - conditions = append(conditions, "version = ?") - params = append(params, neSoftware.Version) - } - if neSoftware.Name != "" { - conditions = append(conditions, "name = ?") - params = append(params, neSoftware.Name) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } else { - return "" - } - - // 查询数据 - querySql := "select id as 'str' from ne_software " + whereSql + " limit 1" - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err %v", err) - return "" - } - if len(results) > 0 { - return fmt.Sprint(results[0]["str"]) - } - return "" -} - -// Insert 新增信息 -func (r *NeSoftwareImpl) Insert(neSoftware model.NeSoftware) string { - // 参数拼接 - params := make(map[string]any) - if neSoftware.NeType != "" { - params["ne_type"] = neSoftware.NeType - } - if neSoftware.Name != "" { - params["name"] = neSoftware.Name - } - if neSoftware.Path != "" { - params["path"] = neSoftware.Path - } - if neSoftware.Version != "" { - params["version"] = neSoftware.Version - } - params["description"] = neSoftware.Description - if neSoftware.CreateBy != "" { - params["create_by"] = neSoftware.CreateBy - params["create_time"] = time.Now().UnixMilli() - } - - // 构建执行语句 - keys, placeholder, values := repo.KeyPlaceholderValueByInsert(params) - sql := "insert into ne_software (" + strings.Join(keys, ",") + ")values(" + placeholder + ")" - - db := datasource.DefaultDB() - // 开启事务 - tx := db.Begin() - // 执行插入 - err := tx.Exec(sql, values...).Error - if err != nil { - logger.Errorf("insert row : %v", err.Error()) - tx.Rollback() - return "" - } - // 获取生成的自增 ID - var insertedID string - err = tx.Raw("select last_insert_id()").Row().Scan(&insertedID) - if err != nil { - logger.Errorf("insert last id : %v", err.Error()) - tx.Rollback() - return "" - } - // 提交事务 - tx.Commit() - return insertedID -} - -// Update 修改信息 -func (r *NeSoftwareImpl) Update(neSoftware model.NeSoftware) int64 { - // 参数拼接 - params := make(map[string]any) - if neSoftware.NeType != "" { - params["ne_type"] = neSoftware.NeType - } - if neSoftware.Name != "" { - params["name"] = neSoftware.Name - } - if neSoftware.Path != "" { - params["path"] = neSoftware.Path - } - if neSoftware.Version != "" { - params["version"] = neSoftware.Version - } - params["description"] = neSoftware.Description - if neSoftware.UpdateBy != "" { - params["update_by"] = neSoftware.UpdateBy - params["update_time"] = time.Now().UnixMilli() - } - - // 构建执行语句 - keys, values := repo.KeyValueByUpdate(params) - sql := "update ne_software set " + strings.Join(keys, ",") + " where id = ?" - - // 执行更新 - values = append(values, neSoftware.ID) - rows, err := datasource.ExecDB("", sql, values) - if err != nil { - logger.Errorf("update row : %v", err.Error()) - return 0 - } - return rows -} - -// DeleteByIds 批量删除信息 -func (r *NeSoftwareImpl) DeleteByIds(cmdIds []string) int64 { - placeholder := repo.KeyPlaceholderByQuery(len(cmdIds)) - sql := "delete from ne_software where id in (" + placeholder + ")" - parameters := repo.ConvertIdsSlice(cmdIds) - results, err := datasource.ExecDB("", sql, parameters) - if err != nil { - logger.Errorf("delete err => %v", err) - return 0 - } - return results -} diff --git a/src/modules/network_element/repository/ne_version.go b/src/modules/network_element/repository/ne_version.go index 6da08cc8..e02466b2 100644 --- a/src/modules/network_element/repository/ne_version.go +++ b/src/modules/network_element/repository/ne_version.go @@ -1,24 +1,329 @@ package repository -import "be.ems/src/modules/network_element/model" +import ( + "strings" + "time" -// INeVersion 网元版本信息 数据层接口 -type INeVersion interface { - // SelectPage 根据条件分页查询字典类型 - SelectPage(query map[string]any) map[string]any + "be.ems/src/framework/datasource" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/parse" + "be.ems/src/framework/utils/repo" + "be.ems/src/modules/network_element/model" +) - // SelectList 根据实体查询 - SelectList(neVersion model.NeVersion) []model.NeVersion +// 实例化数据层 NeVersion 结构体 +var NewNeVersion = &NeVersion{ + selectSql: `select + id, ne_type, ne_id, name, version, path, pre_name, pre_version, pre_path, new_name, new_version, new_path, status, create_by, create_time, update_by, update_time + from ne_version`, - // SelectByIds 通过ID查询 - SelectByIds(ids []string) []model.NeVersion - - // Insert 新增信息 - Insert(neVersion model.NeVersion) string - - // Update 修改信息 - Update(neVersion model.NeVersion) int64 - - // DeleteByIds 批量删除信息 - DeleteByIds(ids []string) int64 + resultMap: map[string]string{ + "id": "ID", + "ne_type": "NeType", + "ne_id": "NeId", + "name": "name", + "version": "Version", + "path": "Path", + "pre_name": "preName", + "pre_version": "PreVersion", + "pre_path": "PrePath", + "new_name": "NewName", + "new_version": "NewVersion", + "new_path": "NewPath", + "status": "Status", + "create_by": "CreateBy", + "create_time": "CreateTime", + "update_by": "UpdateBy", + "update_time": "UpdateTime", + }, +} + +// NeVersion 网元版本信息 数据层处理 +type NeVersion struct { + // 查询视图对象SQL + selectSql string + // 结果字段与实体映射 + resultMap map[string]string +} + +// convertResultRows 将结果记录转实体结果组 +func (r *NeVersion) convertResultRows(rows []map[string]any) []model.NeVersion { + arr := make([]model.NeVersion, 0) + for _, row := range rows { + item := model.NeVersion{} + for key, value := range row { + if keyMapper, ok := r.resultMap[key]; ok { + repo.SetFieldValue(&item, keyMapper, value) + } + } + arr = append(arr, item) + } + return arr +} + +// SelectPage 根据条件分页查询字典类型 +func (r *NeVersion) SelectPage(query map[string]any) map[string]any { + // 查询条件拼接 + var conditions []string + var params []any + if v, ok := query["neType"]; ok && v != "" { + conditions = append(conditions, "ne_type = ?") + params = append(params, strings.Trim(v.(string), " ")) + } + if v, ok := query["neId"]; ok && v != "" { + conditions = append(conditions, "ne_id = ?") + params = append(params, strings.Trim(v.(string), " ")) + } + if v, ok := query["version"]; ok && v != "" { + conditions = append(conditions, "version like concat(?, '%')") + params = append(params, strings.Trim(v.(string), " ")) + } + if v, ok := query["path"]; ok && v != "" { + conditions = append(conditions, "path like concat(?, '%')") + params = append(params, strings.Trim(v.(string), " ")) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + result := map[string]any{ + "total": 0, + "rows": []model.NeHost{}, + } + + // 查询数量 长度为0直接返回 + totalSql := "select count(1) as 'total' from ne_version" + totalRows, err := datasource.RawDB("", totalSql+whereSql, params) + if err != nil { + logger.Errorf("total err => %v", err) + return result + } + total := parse.Number(totalRows[0]["total"]) + if total == 0 { + return result + } else { + result["total"] = total + } + + // 分页 + pageNum, pageSize := repo.PageNumSize(query["pageNum"], query["pageSize"]) + pageSql := " order by update_time desc limit ?,? " + params = append(params, pageNum*pageSize) + params = append(params, pageSize) + + // 查询数据 + querySql := r.selectSql + whereSql + pageSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + return result + } + + // 转换实体 + result["rows"] = r.convertResultRows(results) + return result +} + +// SelectList 根据实体查询 +func (r *NeVersion) SelectList(neVersion model.NeVersion) []model.NeVersion { + // 查询条件拼接 + var conditions []string + var params []any + if neVersion.NeType != "" { + conditions = append(conditions, "ne_type = ?") + params = append(params, neVersion.NeType) + } + if neVersion.NeId != "" { + conditions = append(conditions, "ne_id = ?") + params = append(params, neVersion.NeId) + } + if neVersion.Version != "" { + conditions = append(conditions, "version like concat(?, '%')") + params = append(params, neVersion.Version) + } + if neVersion.Path != "" { + conditions = append(conditions, "path like concat(?, '%')") + params = append(params, neVersion.Path) + } + if neVersion.Status != "" { + conditions = append(conditions, "status = ?") + params = append(params, neVersion.Status) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询数据 + querySql := r.selectSql + whereSql + " order by id asc " + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + } + + // 转换实体 + return r.convertResultRows(results) +} + +// SelectByIds 通过ID查询 +func (r *NeVersion) SelectByIds(cmdIds []string) []model.NeVersion { + placeholder := repo.KeyPlaceholderByQuery(len(cmdIds)) + querySql := r.selectSql + " where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(cmdIds) + results, err := datasource.RawDB("", querySql, parameters) + if err != nil { + logger.Errorf("query err => %v", err) + return []model.NeVersion{} + } + // 转换实体 + return r.convertResultRows(results) +} + +// Insert 新增信息 +func (r *NeVersion) Insert(neVersion model.NeVersion) string { + // 参数拼接 + params := make(map[string]any) + if neVersion.NeType != "" { + params["ne_type"] = neVersion.NeType + } + if neVersion.NeId != "" { + params["ne_id"] = neVersion.NeId + } + if neVersion.Name != "" { + params["name"] = neVersion.Name + } + if neVersion.Version != "" { + params["version"] = neVersion.Version + } + if neVersion.Path != "" { + params["path"] = neVersion.Path + } + if neVersion.PreName != "" { + params["pre_name"] = neVersion.PreName + } + if neVersion.PreVersion != "" { + params["pre_version"] = neVersion.PreVersion + } + if neVersion.PrePath != "" { + params["pre_path"] = neVersion.PrePath + } + if neVersion.NewName != "" { + params["new_name"] = neVersion.NewName + } + if neVersion.NewVersion != "" { + params["new_version"] = neVersion.NewVersion + } + if neVersion.NewPath != "" { + params["new_path"] = neVersion.NewPath + } + if neVersion.Status != "" { + params["status"] = neVersion.Status + } + if neVersion.CreateBy != "" { + params["create_by"] = neVersion.CreateBy + params["create_time"] = time.Now().UnixMilli() + } + + // 构建执行语句 + keys, placeholder, values := repo.KeyPlaceholderValueByInsert(params) + sql := "insert into ne_version (" + strings.Join(keys, ",") + ")values(" + placeholder + ")" + + db := datasource.DefaultDB() + // 开启事务 + tx := db.Begin() + // 执行插入 + err := tx.Exec(sql, values...).Error + if err != nil { + logger.Errorf("insert row : %v", err.Error()) + tx.Rollback() + return "" + } + // 获取生成的自增 ID + var insertedID string + err = tx.Raw("select last_insert_id()").Row().Scan(&insertedID) + if err != nil { + logger.Errorf("insert last id : %v", err.Error()) + tx.Rollback() + return "" + } + // 提交事务 + tx.Commit() + return insertedID +} + +// Update 修改信息 +func (r *NeVersion) Update(neVersion model.NeVersion) int64 { + // 参数拼接 + params := make(map[string]any) + if neVersion.NeType != "" { + params["ne_type"] = neVersion.NeType + } + if neVersion.NeId != "" { + params["ne_id"] = neVersion.NeId + } + if neVersion.Name != "" { + params["name"] = neVersion.Name + } + if neVersion.Version != "" { + params["version"] = neVersion.Version + } + if neVersion.Path != "" { + params["path"] = neVersion.Path + } + if neVersion.PreName != "" { + params["pre_name"] = neVersion.PreName + } + if neVersion.PreVersion != "" { + params["pre_version"] = neVersion.PreVersion + } + if neVersion.PrePath != "" { + params["pre_path"] = neVersion.PrePath + } + if neVersion.NewName != "" { + params["new_name"] = neVersion.NewName + } + if neVersion.NewVersion != "" { + params["new_version"] = neVersion.NewVersion + } + if neVersion.NewPath != "" { + params["new_path"] = neVersion.NewPath + } + if neVersion.Status != "" { + params["status"] = neVersion.Status + } + if neVersion.UpdateBy != "" { + params["update_by"] = neVersion.UpdateBy + params["update_time"] = time.Now().UnixMilli() + } + + // 构建执行语句 + keys, values := repo.KeyValueByUpdate(params) + sql := "update ne_version set " + strings.Join(keys, ",") + " where id = ?" + + // 执行更新 + values = append(values, neVersion.ID) + rows, err := datasource.ExecDB("", sql, values) + if err != nil { + logger.Errorf("update row : %v", err.Error()) + return 0 + } + return rows +} + +// DeleteByIds 批量删除信息 +func (r *NeVersion) DeleteByIds(cmdIds []string) int64 { + placeholder := repo.KeyPlaceholderByQuery(len(cmdIds)) + sql := "delete from ne_version where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(cmdIds) + results, err := datasource.ExecDB("", sql, parameters) + if err != nil { + logger.Errorf("delete err => %v", err) + return 0 + } + return results } diff --git a/src/modules/network_element/repository/ne_version.impl.go b/src/modules/network_element/repository/ne_version.impl.go deleted file mode 100644 index ad2c1440..00000000 --- a/src/modules/network_element/repository/ne_version.impl.go +++ /dev/null @@ -1,329 +0,0 @@ -package repository - -import ( - "strings" - "time" - - "be.ems/src/framework/datasource" - "be.ems/src/framework/logger" - "be.ems/src/framework/utils/parse" - "be.ems/src/framework/utils/repo" - "be.ems/src/modules/network_element/model" -) - -// 实例化数据层 NewNeVersion 结构体 -var NewNeVersionImpl = &NeVersionImpl{ - selectSql: `select - id, ne_type, ne_id, name, version, path, pre_name, pre_version, pre_path, new_name, new_version, new_path, status, create_by, create_time, update_by, update_time - from ne_version`, - - resultMap: map[string]string{ - "id": "ID", - "ne_type": "NeType", - "ne_id": "NeId", - "name": "name", - "version": "Version", - "path": "Path", - "pre_name": "preName", - "pre_version": "PreVersion", - "pre_path": "PrePath", - "new_name": "NewName", - "new_version": "NewVersion", - "new_path": "NewPath", - "status": "Status", - "create_by": "CreateBy", - "create_time": "CreateTime", - "update_by": "UpdateBy", - "update_time": "UpdateTime", - }, -} - -// NeVersionImpl 网元版本信息 数据层处理 -type NeVersionImpl struct { - // 查询视图对象SQL - selectSql string - // 结果字段与实体映射 - resultMap map[string]string -} - -// convertResultRows 将结果记录转实体结果组 -func (r *NeVersionImpl) convertResultRows(rows []map[string]any) []model.NeVersion { - arr := make([]model.NeVersion, 0) - for _, row := range rows { - item := model.NeVersion{} - for key, value := range row { - if keyMapper, ok := r.resultMap[key]; ok { - repo.SetFieldValue(&item, keyMapper, value) - } - } - arr = append(arr, item) - } - return arr -} - -// SelectPage 根据条件分页查询字典类型 -func (r *NeVersionImpl) SelectPage(query map[string]any) map[string]any { - // 查询条件拼接 - var conditions []string - var params []any - if v, ok := query["neType"]; ok && v != "" { - conditions = append(conditions, "ne_type = ?") - params = append(params, strings.Trim(v.(string), " ")) - } - if v, ok := query["neId"]; ok && v != "" { - conditions = append(conditions, "ne_id = ?") - params = append(params, strings.Trim(v.(string), " ")) - } - if v, ok := query["version"]; ok && v != "" { - conditions = append(conditions, "version like concat(?, '%')") - params = append(params, strings.Trim(v.(string), " ")) - } - if v, ok := query["path"]; ok && v != "" { - conditions = append(conditions, "path like concat(?, '%')") - params = append(params, strings.Trim(v.(string), " ")) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } - - result := map[string]any{ - "total": 0, - "rows": []model.NeHost{}, - } - - // 查询数量 长度为0直接返回 - totalSql := "select count(1) as 'total' from ne_version" - totalRows, err := datasource.RawDB("", totalSql+whereSql, params) - if err != nil { - logger.Errorf("total err => %v", err) - return result - } - total := parse.Number(totalRows[0]["total"]) - if total == 0 { - return result - } else { - result["total"] = total - } - - // 分页 - pageNum, pageSize := repo.PageNumSize(query["pageNum"], query["pageSize"]) - pageSql := " order by update_time desc limit ?,? " - params = append(params, pageNum*pageSize) - params = append(params, pageSize) - - // 查询数据 - querySql := r.selectSql + whereSql + pageSql - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err => %v", err) - return result - } - - // 转换实体 - result["rows"] = r.convertResultRows(results) - return result -} - -// SelectList 根据实体查询 -func (r *NeVersionImpl) SelectList(neVersion model.NeVersion) []model.NeVersion { - // 查询条件拼接 - var conditions []string - var params []any - if neVersion.NeType != "" { - conditions = append(conditions, "ne_type = ?") - params = append(params, neVersion.NeType) - } - if neVersion.NeId != "" { - conditions = append(conditions, "ne_id = ?") - params = append(params, neVersion.NeId) - } - if neVersion.Version != "" { - conditions = append(conditions, "version like concat(?, '%')") - params = append(params, neVersion.Version) - } - if neVersion.Path != "" { - conditions = append(conditions, "path like concat(?, '%')") - params = append(params, neVersion.Path) - } - if neVersion.Status != "" { - conditions = append(conditions, "status = ?") - params = append(params, neVersion.Status) - } - - // 构建查询条件语句 - whereSql := "" - if len(conditions) > 0 { - whereSql += " where " + strings.Join(conditions, " and ") - } - - // 查询数据 - querySql := r.selectSql + whereSql + " order by id asc " - results, err := datasource.RawDB("", querySql, params) - if err != nil { - logger.Errorf("query err => %v", err) - } - - // 转换实体 - return r.convertResultRows(results) -} - -// SelectByIds 通过ID查询 -func (r *NeVersionImpl) SelectByIds(cmdIds []string) []model.NeVersion { - placeholder := repo.KeyPlaceholderByQuery(len(cmdIds)) - querySql := r.selectSql + " where id in (" + placeholder + ")" - parameters := repo.ConvertIdsSlice(cmdIds) - results, err := datasource.RawDB("", querySql, parameters) - if err != nil { - logger.Errorf("query err => %v", err) - return []model.NeVersion{} - } - // 转换实体 - return r.convertResultRows(results) -} - -// Insert 新增信息 -func (r *NeVersionImpl) Insert(neVersion model.NeVersion) string { - // 参数拼接 - params := make(map[string]any) - if neVersion.NeType != "" { - params["ne_type"] = neVersion.NeType - } - if neVersion.NeId != "" { - params["ne_id"] = neVersion.NeId - } - if neVersion.Name != "" { - params["name"] = neVersion.Name - } - if neVersion.Version != "" { - params["version"] = neVersion.Version - } - if neVersion.Path != "" { - params["path"] = neVersion.Path - } - if neVersion.PreName != "" { - params["pre_name"] = neVersion.PreName - } - if neVersion.PreVersion != "" { - params["pre_version"] = neVersion.PreVersion - } - if neVersion.PrePath != "" { - params["pre_path"] = neVersion.PrePath - } - if neVersion.NewName != "" { - params["new_name"] = neVersion.NewName - } - if neVersion.NewVersion != "" { - params["new_version"] = neVersion.NewVersion - } - if neVersion.NewPath != "" { - params["new_path"] = neVersion.NewPath - } - if neVersion.Status != "" { - params["status"] = neVersion.Status - } - if neVersion.CreateBy != "" { - params["create_by"] = neVersion.CreateBy - params["create_time"] = time.Now().UnixMilli() - } - - // 构建执行语句 - keys, placeholder, values := repo.KeyPlaceholderValueByInsert(params) - sql := "insert into ne_version (" + strings.Join(keys, ",") + ")values(" + placeholder + ")" - - db := datasource.DefaultDB() - // 开启事务 - tx := db.Begin() - // 执行插入 - err := tx.Exec(sql, values...).Error - if err != nil { - logger.Errorf("insert row : %v", err.Error()) - tx.Rollback() - return "" - } - // 获取生成的自增 ID - var insertedID string - err = tx.Raw("select last_insert_id()").Row().Scan(&insertedID) - if err != nil { - logger.Errorf("insert last id : %v", err.Error()) - tx.Rollback() - return "" - } - // 提交事务 - tx.Commit() - return insertedID -} - -// Update 修改信息 -func (r *NeVersionImpl) Update(neVersion model.NeVersion) int64 { - // 参数拼接 - params := make(map[string]any) - if neVersion.NeType != "" { - params["ne_type"] = neVersion.NeType - } - if neVersion.NeId != "" { - params["ne_id"] = neVersion.NeId - } - if neVersion.Name != "" { - params["name"] = neVersion.Name - } - if neVersion.Version != "" { - params["version"] = neVersion.Version - } - if neVersion.Path != "" { - params["path"] = neVersion.Path - } - if neVersion.PreName != "" { - params["pre_name"] = neVersion.PreName - } - if neVersion.PreVersion != "" { - params["pre_version"] = neVersion.PreVersion - } - if neVersion.PrePath != "" { - params["pre_path"] = neVersion.PrePath - } - if neVersion.NewName != "" { - params["new_name"] = neVersion.NewName - } - if neVersion.NewVersion != "" { - params["new_version"] = neVersion.NewVersion - } - if neVersion.NewPath != "" { - params["new_path"] = neVersion.NewPath - } - if neVersion.Status != "" { - params["status"] = neVersion.Status - } - if neVersion.UpdateBy != "" { - params["update_by"] = neVersion.UpdateBy - params["update_time"] = time.Now().UnixMilli() - } - - // 构建执行语句 - keys, values := repo.KeyValueByUpdate(params) - sql := "update ne_version set " + strings.Join(keys, ",") + " where id = ?" - - // 执行更新 - values = append(values, neVersion.ID) - rows, err := datasource.ExecDB("", sql, values) - if err != nil { - logger.Errorf("update row : %v", err.Error()) - return 0 - } - return rows -} - -// DeleteByIds 批量删除信息 -func (r *NeVersionImpl) DeleteByIds(cmdIds []string) int64 { - placeholder := repo.KeyPlaceholderByQuery(len(cmdIds)) - sql := "delete from ne_version where id in (" + placeholder + ")" - parameters := repo.ConvertIdsSlice(cmdIds) - results, err := datasource.ExecDB("", sql, parameters) - if err != nil { - logger.Errorf("delete err => %v", err) - return 0 - } - return results -} diff --git a/src/modules/network_element/service/ne_config.go b/src/modules/network_element/service/ne_config.go index e63c0b51..f607c0ab 100644 --- a/src/modules/network_element/service/ne_config.go +++ b/src/modules/network_element/service/ne_config.go @@ -1,36 +1,163 @@ package service -import "be.ems/src/modules/network_element/model" +import ( + "encoding/json" + "fmt" + "strings" -// INeConfig 网元参数配置可用属性值 服务层接口 -type INeConfig interface { - // RefreshByNeType 通过ne_type刷新redis中的缓存 - RefreshByNeTypeAndNeID(neType string) []model.NeConfig + "be.ems/src/framework/constants/cachekey" + "be.ems/src/framework/redis" + "be.ems/src/modules/network_element/model" + "be.ems/src/modules/network_element/repository" +) - // ClearNeCacheByNeType 清除网元类型参数配置缓存 - ClearNeCacheByNeType(neType string) bool - - // SelectNeConfigByNeType 查询网元类型参数配置 - SelectNeConfigByNeType(neType string) []model.NeConfig - - // SelectNeConfigByNeTypeAndParamName 查询网元类型参数配置By参数名 - SelectNeConfigByNeTypeAndParamName(neType, paramName string) model.NeConfig - - // SelectNeHostPage 分页查询列表数据 - SelectPage(query map[string]any) map[string]any - - // SelectList 根据实体查询 - SelectList(param model.NeConfig) []model.NeConfig - - // SelectByIds 通过ID查询 - SelectById(id string) model.NeConfig - - // Insert 新增信息 - Insert(param model.NeConfig) string - - // Update 修改信息 - Update(param model.NeConfig) int64 - - // DeleteByIds 批量删除信息 - DeleteByIds(ids []string) (int64, error) +// 实例化服务层 NeConfig 结构体 +var NewNeConfig = &NeConfig{ + neConfigRepository: repository.NewNeConfig, +} + +// NeConfig 网元参数配置可用属性值 服务层处理 +type NeConfig struct { + neConfigRepository *repository.NeConfig // 网元参数配置可用属性值表 +} + +// RefreshByNeType 通过ne_type刷新redis中的缓存 +func (r *NeConfig) RefreshByNeTypeAndNeID(neType string) []model.NeConfig { + // 多个 + if neType == "" || neType == "*" { + neConfigList := r.neConfigRepository.SelectList(model.NeConfig{}) + if len(neConfigList) > 0 { + neConfigGroup := map[string][]model.NeConfig{} + for _, v := range neConfigList { + if item, ok := neConfigGroup[v.NeType]; ok { + neConfigGroup[v.NeType] = append(item, v) + } else { + neConfigGroup[v.NeType] = []model.NeConfig{v} + } + } + for k, v := range neConfigGroup { + key := fmt.Sprintf("%sNeConfig:%s", cachekey.NE_DATA_KEY, strings.ToUpper(k)) + redis.Del("", key) + if len(v) > 0 { + for i, item := range v { + if err := json.Unmarshal([]byte(item.ParamJson), &item.ParamData); err != nil { + continue + } + v[i] = item + } + values, _ := json.Marshal(v) + redis.Set("", key, string(values)) + } + } + } + return neConfigList + } + // 单个 + key := fmt.Sprintf("%sNeConfig:%s", cachekey.NE_DATA_KEY, strings.ToUpper(neType)) + redis.Del("", key) + neConfigList := r.neConfigRepository.SelectList(model.NeConfig{ + NeType: neType, + }) + if len(neConfigList) > 0 { + for i, v := range neConfigList { + if err := json.Unmarshal([]byte(v.ParamJson), &v.ParamData); err != nil { + continue + } + neConfigList[i] = v + } + values, _ := json.Marshal(neConfigList) + redis.Set("", key, string(values)) + } + return neConfigList +} + +// ClearNeCacheByNeType 清除网元类型参数配置缓存 +func (r *NeConfig) ClearNeCacheByNeType(neType string) bool { + key := fmt.Sprintf("%sNeConfig:%s", cachekey.NE_DATA_KEY, neType) + if neType == "*" { + key = fmt.Sprintf("%sNeConfig:*", cachekey.NE_DATA_KEY) + } + keys, err := redis.GetKeys("", key) + if err != nil { + return false + } + delOk, _ := redis.DelKeys("", keys) + return delOk +} + +// SelectNeConfigByNeType 查询网元类型参数配置 +func (r *NeConfig) SelectNeConfigByNeType(neType string) []model.NeConfig { + var neConfigList []model.NeConfig + key := fmt.Sprintf("%sNeConfig:%s", cachekey.NE_DATA_KEY, strings.ToUpper(neType)) + jsonStr, _ := redis.Get("", key) + if len(jsonStr) > 7 { + err := json.Unmarshal([]byte(jsonStr), &neConfigList) + if err != nil { + neConfigList = []model.NeConfig{} + } + } else { + neConfigList = r.RefreshByNeTypeAndNeID(neType) + } + return neConfigList +} + +// SelectNeConfigByNeTypeAndParamName 查询网元类型参数配置By参数名 +func (r *NeConfig) SelectNeConfigByNeTypeAndParamName(neType, paramName string) model.NeConfig { + neConfigList := r.SelectNeConfigByNeType(neType) + var neConfig model.NeConfig + for _, v := range neConfigList { + if v.ParamName == paramName { + neConfig = v + break + } + } + return neConfig +} + +// SelectNeHostPage 分页查询列表数据 +func (r *NeConfig) SelectPage(query map[string]any) map[string]any { + return r.neConfigRepository.SelectPage(query) +} + +// SelectConfigList 查询列表 +func (r *NeConfig) SelectList(param model.NeConfig) []model.NeConfig { + return r.neConfigRepository.SelectList(param) +} + +// SelectByIds 通过ID查询 +func (r *NeConfig) SelectById(id string) model.NeConfig { + if id == "" { + return model.NeConfig{} + } + neHosts := r.neConfigRepository.SelectByIds([]string{id}) + if len(neHosts) > 0 { + return neHosts[0] + } + return model.NeConfig{} +} + +// Insert 新增信息 +func (r *NeConfig) Insert(param model.NeConfig) string { + return r.neConfigRepository.Insert(param) +} + +// Update 修改信息 +func (r *NeConfig) Update(param model.NeConfig) int64 { + return r.neConfigRepository.Update(param) +} + +// DeleteByIds 批量删除信息 +func (r *NeConfig) DeleteByIds(ids []string) (int64, error) { + // 检查是否存在 + data := r.neConfigRepository.SelectByIds(ids) + if len(data) <= 0 { + return 0, fmt.Errorf("param.noData") + } + + if len(data) == len(ids) { + rows := r.neConfigRepository.DeleteByIds(ids) + return rows, nil + } + // 删除信息失败! + return 0, fmt.Errorf("delete fail") } diff --git a/src/modules/network_element/service/ne_config.impl.go b/src/modules/network_element/service/ne_config.impl.go deleted file mode 100644 index 120f5bf4..00000000 --- a/src/modules/network_element/service/ne_config.impl.go +++ /dev/null @@ -1,164 +0,0 @@ -package service - -import ( - "encoding/json" - "fmt" - "strings" - - "be.ems/src/framework/constants/cachekey" - "be.ems/src/framework/redis" - "be.ems/src/modules/network_element/model" - "be.ems/src/modules/network_element/repository" -) - -// NewNeConfigImpl 网元参数配置可用属性值 实例化服务层 -var NewNeConfigImpl = &NeConfigImpl{ - neConfigRepository: repository.NewNeConfigImpl, -} - -// NeConfigImpl 网元参数配置可用属性值 服务层处理 -type NeConfigImpl struct { - // 网元参数配置可用属性值表 - neConfigRepository repository.INeConfig -} - -// RefreshByNeType 通过ne_type刷新redis中的缓存 -func (r *NeConfigImpl) RefreshByNeTypeAndNeID(neType string) []model.NeConfig { - // 多个 - if neType == "" || neType == "*" { - neConfigList := r.neConfigRepository.SelectList(model.NeConfig{}) - if len(neConfigList) > 0 { - neConfigGroup := map[string][]model.NeConfig{} - for _, v := range neConfigList { - if item, ok := neConfigGroup[v.NeType]; ok { - neConfigGroup[v.NeType] = append(item, v) - } else { - neConfigGroup[v.NeType] = []model.NeConfig{v} - } - } - for k, v := range neConfigGroup { - key := fmt.Sprintf("%sNeConfig:%s", cachekey.NE_DATA_KEY, strings.ToUpper(k)) - redis.Del("", key) - if len(v) > 0 { - for i, item := range v { - if err := json.Unmarshal([]byte(item.ParamJson), &item.ParamData); err != nil { - continue - } - v[i] = item - } - values, _ := json.Marshal(v) - redis.Set("", key, string(values)) - } - } - } - return neConfigList - } - // 单个 - key := fmt.Sprintf("%sNeConfig:%s", cachekey.NE_DATA_KEY, strings.ToUpper(neType)) - redis.Del("", key) - neConfigList := r.neConfigRepository.SelectList(model.NeConfig{ - NeType: neType, - }) - if len(neConfigList) > 0 { - for i, v := range neConfigList { - if err := json.Unmarshal([]byte(v.ParamJson), &v.ParamData); err != nil { - continue - } - neConfigList[i] = v - } - values, _ := json.Marshal(neConfigList) - redis.Set("", key, string(values)) - } - return neConfigList -} - -// ClearNeCacheByNeType 清除网元类型参数配置缓存 -func (r *NeConfigImpl) ClearNeCacheByNeType(neType string) bool { - key := fmt.Sprintf("%sNeConfig:%s", cachekey.NE_DATA_KEY, neType) - if neType == "*" { - key = fmt.Sprintf("%sNeConfig:*", cachekey.NE_DATA_KEY) - } - keys, err := redis.GetKeys("", key) - if err != nil { - return false - } - delOk, _ := redis.DelKeys("", keys) - return delOk -} - -// SelectNeConfigByNeType 查询网元类型参数配置 -func (r *NeConfigImpl) SelectNeConfigByNeType(neType string) []model.NeConfig { - var neConfigList []model.NeConfig - key := fmt.Sprintf("%sNeConfig:%s", cachekey.NE_DATA_KEY, strings.ToUpper(neType)) - jsonStr, _ := redis.Get("", key) - if len(jsonStr) > 7 { - err := json.Unmarshal([]byte(jsonStr), &neConfigList) - if err != nil { - neConfigList = []model.NeConfig{} - } - } else { - neConfigList = r.RefreshByNeTypeAndNeID(neType) - } - return neConfigList -} - -// SelectNeConfigByNeTypeAndParamName 查询网元类型参数配置By参数名 -func (r *NeConfigImpl) SelectNeConfigByNeTypeAndParamName(neType, paramName string) model.NeConfig { - neConfigList := r.SelectNeConfigByNeType(neType) - var neConfig model.NeConfig - for _, v := range neConfigList { - if v.ParamName == paramName { - neConfig = v - break - } - } - return neConfig -} - -// SelectNeHostPage 分页查询列表数据 -func (r *NeConfigImpl) SelectPage(query map[string]any) map[string]any { - return r.neConfigRepository.SelectPage(query) -} - -// SelectConfigList 查询列表 -func (r *NeConfigImpl) SelectList(param model.NeConfig) []model.NeConfig { - return r.neConfigRepository.SelectList(param) -} - -// SelectByIds 通过ID查询 -func (r *NeConfigImpl) SelectById(id string) model.NeConfig { - if id == "" { - return model.NeConfig{} - } - neHosts := r.neConfigRepository.SelectByIds([]string{id}) - if len(neHosts) > 0 { - return neHosts[0] - } - return model.NeConfig{} -} - -// Insert 新增信息 -func (r *NeConfigImpl) Insert(param model.NeConfig) string { - return r.neConfigRepository.Insert(param) -} - -// Update 修改信息 -func (r *NeConfigImpl) Update(param model.NeConfig) int64 { - return r.neConfigRepository.Update(param) -} - -// DeleteByIds 批量删除信息 -func (r *NeConfigImpl) DeleteByIds(ids []string) (int64, error) { - // 检查是否存在 - data := r.neConfigRepository.SelectByIds(ids) - if len(data) <= 0 { - return 0, fmt.Errorf("param.noData") - } - - if len(data) == len(ids) { - rows := r.neConfigRepository.DeleteByIds(ids) - return rows, nil - } - // 删除信息失败! - return 0, fmt.Errorf("delete fail") -} diff --git a/src/modules/network_element/service/ne_config_backup.go b/src/modules/network_element/service/ne_config_backup.go index 3f8239f6..9c2d6ba8 100644 --- a/src/modules/network_element/service/ne_config_backup.go +++ b/src/modules/network_element/service/ne_config_backup.go @@ -1,30 +1,201 @@ package service -import "be.ems/src/modules/network_element/model" +import ( + "fmt" + "os" + "runtime" + "strings" + "time" -// INeConfigBackup 网元配置文件备份记录 服务层接口 -type INeConfigBackup interface { - // SelectNeHostPage 分页查询列表数据 - SelectPage(query map[string]any) map[string]any + "be.ems/src/framework/utils/date" + "be.ems/src/framework/utils/file" + "be.ems/src/modules/network_element/model" + "be.ems/src/modules/network_element/repository" +) - // SelectList 根据实体查询 - SelectList(item model.NeConfigBackup) []model.NeConfigBackup - - // SelectByIds 通过ID查询 - SelectById(id string) model.NeConfigBackup - - // Insert 新增信息 - Insert(item model.NeConfigBackup) string - - // Update 修改信息 - Update(item model.NeConfigBackup) int64 - - // DeleteByIds 批量删除信息 - DeleteByIds(ids []string) (int64, error) - - // NeConfigLocalToNe 网元配置文件复制到网元端覆盖 - NeConfigLocalToNe(neInfo model.NeInfo, localFile string) error - - // NeConfigNeToLocal 网元备份文件网元端复制到本地 - NeConfigNeToLocal(neInfo model.NeInfo) (string, error) +// 实例化服务层 NeConfigBackup 结构体 +var NewNeConfigBackup = &NeConfigBackup{ + neConfigBackupRepository: repository.NewNeConfigBackup, +} + +// NeConfigBackup 网元配置文件备份记录 服务层处理 +type NeConfigBackup struct { + neConfigBackupRepository *repository.NeConfigBackup // 网元配置文件备份记录 +} + +// SelectNeHostPage 分页查询列表数据 +func (r *NeConfigBackup) SelectPage(query map[string]any) map[string]any { + return r.neConfigBackupRepository.SelectPage(query) +} + +// SelectConfigList 查询列表 +func (r *NeConfigBackup) SelectList(item model.NeConfigBackup) []model.NeConfigBackup { + return r.neConfigBackupRepository.SelectList(item) +} + +// SelectByIds 通过ID查询 +func (r *NeConfigBackup) SelectById(id string) model.NeConfigBackup { + if id == "" { + return model.NeConfigBackup{} + } + arr := r.neConfigBackupRepository.SelectByIds([]string{id}) + if len(arr) > 0 { + return arr[0] + } + return model.NeConfigBackup{} +} + +// Insert 新增信息 +func (r *NeConfigBackup) Insert(item model.NeConfigBackup) string { + return r.neConfigBackupRepository.Insert(item) +} + +// Update 修改信息 +func (r *NeConfigBackup) Update(item model.NeConfigBackup) int64 { + return r.neConfigBackupRepository.Update(item) +} + +// DeleteByIds 批量删除信息 +func (r *NeConfigBackup) DeleteByIds(ids []string) (int64, error) { + // 检查是否存在 + data := r.neConfigBackupRepository.SelectByIds(ids) + if len(data) <= 0 { + return 0, fmt.Errorf("neConfigBackup.noData") + } + + if len(data) == len(ids) { + rows := r.neConfigBackupRepository.DeleteByIds(ids) + return rows, nil + } + // 删除信息失败! + return 0, fmt.Errorf("delete fail") +} + +// NeConfigLocalToNe 网元配置文件复制到网元端覆盖 +func (r *NeConfigBackup) NeConfigLocalToNe(neInfo model.NeInfo, localFile string) error { + neTypeLower := strings.ToLower(neInfo.NeType) + // 网管本地路径 + omcPath := "/usr/local/etc/omc/ne_config" + if runtime.GOOS == "windows" { + omcPath = fmt.Sprintf("C:%s", omcPath) + } + localDirPath := fmt.Sprintf("%s/%s/%s/backup/tmp_import", omcPath, neTypeLower, neInfo.NeId) + if err := file.UnZip(localFile, localDirPath); err != nil { + return fmt.Errorf("unzip err") + } + + // 网元主机的SSH客户端 + sshClient, err := NewNeInfo.NeRunSSHClient(neInfo.NeType, neInfo.NeId) + if err != nil { + return fmt.Errorf("ne info ssh client err") + } + defer sshClient.Close() + // 网元主机的SSH客户端进行文件传输 + sftpClient, err := sshClient.NewClientSFTP() + if err != nil { + return fmt.Errorf("ne info sftp client err") + } + defer sftpClient.Close() + + // 网元配置端上的临时目录 + neDirTemp := fmt.Sprintf("/tmp/omc/ne_config/%s/%s", neTypeLower, neInfo.NeId) + sshClient.RunCMD(fmt.Sprintf("mkdir -p /tmp/omc && sudo chmod 777 -R /tmp/omc && sudo rm -rf %s", neDirTemp)) + // 复制到网元端 + if err = sftpClient.CopyDirLocalToRemote(localDirPath, neDirTemp); err != nil { + return fmt.Errorf("copy config to ne err") + } + + // 配置复制到网元内 + if neTypeLower == "ims" { + // ims目录 + imsDirArr := [...]string{"bgcf", "icscf", "ismc", "mmtel", "mrf", "oam_manager.yaml", "pcscf", "scscf", "vars.cfg", "zlog"} + for _, v := range imsDirArr { + sshClient.RunCMD(fmt.Sprintf("sudo mkdir -p /usr/local/etc/ims && sudo cp -rf %s/ims/%s /usr/local/etc/ims/%v && sudo chmod 755 -R /usr/local/etc/ims/%s", neDirTemp, v, v, v)) + } + // mf目录 + sshClient.RunCMD(fmt.Sprintf("sudo mkdir -p /usr/local/etc/mf && sudo cp -rf %s/mf/* /usr/local/etc/mf && sudo chmod 755 -R /usr/local/etc/mf", neDirTemp)) + // rtproxy目录 + sshClient.RunCMD(fmt.Sprintf("sudo mkdir -p /usr/local/etc/rtproxy && sudo cp -rf %s/rtproxy/* /usr/local/etc/rtproxy && sudo chmod 755 /usr/local/etc/rtproxy/rtproxy.conf", neDirTemp)) + // iwf目录 + sshClient.RunCMD(fmt.Sprintf("sudo mkdir -p /usr/local/etc/iwf && sudo cp -rf %s/iwf/* /usr/local/etc/iwf && sudo chmod 755 /usr/local/etc/iwf/*.yaml", neDirTemp)) + } else if neTypeLower == "omc" { + sshClient.RunCMD(fmt.Sprintf("sudo mkdir -p /usr/local/omc/etc && sudo cp -rf %s/* /usr/local/omc/etc && sudo chmod 755 /usr/local/omc/etc/*.{yaml,conf}", neDirTemp)) + } else { + neEtcPath := fmt.Sprintf("/usr/local/etc/%s", neTypeLower) + chmodFile := fmt.Sprintf("sudo chmod 755 %s/*.yaml", neEtcPath) + if neTypeLower == "mme" { + chmodFile = fmt.Sprintf("sudo chmod 755 %s/*.{yaml,conf}", neEtcPath) + } + sshClient.RunCMD(fmt.Sprintf("sudo cp -rf %s/* %s && %s", neDirTemp, neEtcPath, chmodFile)) + } + + _ = os.RemoveAll(localDirPath) // 删除本地临时目录 + sshClient.RunCMD(fmt.Sprintf("sudo rm -rf %s", neDirTemp)) // 删除临时目录 + return nil +} + +// NeConfigNeToLocal 网元备份文件网元端复制到本地 +func (r *NeConfigBackup) NeConfigNeToLocal(neInfo model.NeInfo) (string, error) { + // 网元主机的SSH客户端 + sshClient, err := NewNeInfo.NeRunSSHClient(neInfo.NeType, neInfo.NeId) + if err != nil { + return "", fmt.Errorf("ne info ssh client err") + } + defer sshClient.Close() + // 网元主机的SSH客户端进行文件传输 + sftpClient, err := sshClient.NewClientSFTP() + if err != nil { + return "", fmt.Errorf("ne info sftp client err") + } + defer sftpClient.Close() + + neTypeLower := strings.ToLower(neInfo.NeType) + // 网管本地路径 + omcPath := "/usr/local/etc/omc/ne_config" + if runtime.GOOS == "windows" { + omcPath = fmt.Sprintf("C:%s", omcPath) + } + localDirPath := fmt.Sprintf("%s/%s/%s/backup/tmp_export", omcPath, neTypeLower, neInfo.NeId) + + // 网元配置文件先复制到临时目录 + sshClient.RunCMD("mkdir -p /tmp/omc && sudo chmod 777 -R /tmp/omc") + neDirTemp := fmt.Sprintf("/tmp/omc/ne_config/%s/%s", neTypeLower, neInfo.NeId) + if neTypeLower == "ims" { + // ims目录 + sshClient.RunCMD(fmt.Sprintf("mkdir -p %s/ims", neDirTemp)) + imsDirArr := [...]string{"bgcf", "icscf", "ismc", "mmtel", "mrf", "oam_manager.yaml", "pcscf", "scscf", "vars.cfg", "zlog"} + for _, v := range imsDirArr { + sshClient.RunCMD(fmt.Sprintf("sudo cp -rf /usr/local/etc/ims/%s %s/ims", v, neDirTemp)) + } + // mf目录 + sshClient.RunCMD(fmt.Sprintf("mkdir -p %s/mf && sudo cp -rf /usr/local/etc/mf %s", neDirTemp, neDirTemp)) + // rtproxy目录 + sshClient.RunCMD(fmt.Sprintf("mkdir -p %s/rtproxy && sudo cp -rf /usr/local/etc/rtproxy/rtproxy.conf %s/rtproxy", neDirTemp, neDirTemp)) + // iwf目录 + sshClient.RunCMD(fmt.Sprintf("mkdir -p %s/iwf && sudo cp -rf /usr/local/etc/iwf/*.yaml %s/iwf", neDirTemp, neDirTemp)) + } else if neTypeLower == "omc" { + sshClient.RunCMD(fmt.Sprintf("mkdir -p %s && sudo cp -rf /usr/local/omc/etc/*.{yaml,conf} %s", neDirTemp, neDirTemp)) + } else { + nePath := fmt.Sprintf("/usr/local/etc/%s/*.yaml", neTypeLower) + if neTypeLower == "mme" { + nePath = fmt.Sprintf("/usr/local/etc/%s/*.{yaml,conf}", neTypeLower) + } + sshClient.RunCMD(fmt.Sprintf("mkdir -p %s && sudo cp -rf %s %s", neDirTemp, nePath, neDirTemp)) + } + + // 网元端复制到本地 + if err = sftpClient.CopyDirRemoteToLocal(neDirTemp, localDirPath); err != nil { + return "", fmt.Errorf("copy config err") + } + + // 压缩zip文件名 + zipFileName := fmt.Sprintf("%s-%s-etc-%s.zip", neTypeLower, neInfo.NeId, date.ParseDateToStr(time.Now(), date.YYYYMMDDHHMMSS)) + zipFilePath := fmt.Sprintf("%s/%s/%s/backup/%s", omcPath, neTypeLower, neInfo.NeId, zipFileName) + if err := file.CompressZipByDir(zipFilePath, localDirPath); err != nil { + return "", fmt.Errorf("compress zip err") + } + + _ = os.RemoveAll(localDirPath) // 删除本地临时目录 + sshClient.RunCMD(fmt.Sprintf("sudo rm -rf %s", neDirTemp)) // 删除临时目录 + return zipFilePath, nil } diff --git a/src/modules/network_element/service/ne_config_backup.impl.go b/src/modules/network_element/service/ne_config_backup.impl.go deleted file mode 100644 index f1eee5cb..00000000 --- a/src/modules/network_element/service/ne_config_backup.impl.go +++ /dev/null @@ -1,202 +0,0 @@ -package service - -import ( - "fmt" - "os" - "runtime" - "strings" - "time" - - "be.ems/src/framework/utils/date" - "be.ems/src/framework/utils/file" - "be.ems/src/modules/network_element/model" - "be.ems/src/modules/network_element/repository" -) - -// NewNeConfigBackupImpl 网元配置文件备份记录 实例化服务层 -var NewNeConfigBackupImpl = &NeConfigBackupImpl{ - neConfigBackupRepository: repository.NewNeConfigBackupImpl, -} - -// NeConfigBackupImpl 网元配置文件备份记录 服务层处理 -type NeConfigBackupImpl struct { - // 网元配置文件备份记录 - neConfigBackupRepository repository.INeConfigBackup -} - -// SelectNeHostPage 分页查询列表数据 -func (r *NeConfigBackupImpl) SelectPage(query map[string]any) map[string]any { - return r.neConfigBackupRepository.SelectPage(query) -} - -// SelectConfigList 查询列表 -func (r *NeConfigBackupImpl) SelectList(item model.NeConfigBackup) []model.NeConfigBackup { - return r.neConfigBackupRepository.SelectList(item) -} - -// SelectByIds 通过ID查询 -func (r *NeConfigBackupImpl) SelectById(id string) model.NeConfigBackup { - if id == "" { - return model.NeConfigBackup{} - } - arr := r.neConfigBackupRepository.SelectByIds([]string{id}) - if len(arr) > 0 { - return arr[0] - } - return model.NeConfigBackup{} -} - -// Insert 新增信息 -func (r *NeConfigBackupImpl) Insert(item model.NeConfigBackup) string { - return r.neConfigBackupRepository.Insert(item) -} - -// Update 修改信息 -func (r *NeConfigBackupImpl) Update(item model.NeConfigBackup) int64 { - return r.neConfigBackupRepository.Update(item) -} - -// DeleteByIds 批量删除信息 -func (r *NeConfigBackupImpl) DeleteByIds(ids []string) (int64, error) { - // 检查是否存在 - data := r.neConfigBackupRepository.SelectByIds(ids) - if len(data) <= 0 { - return 0, fmt.Errorf("neConfigBackup.noData") - } - - if len(data) == len(ids) { - rows := r.neConfigBackupRepository.DeleteByIds(ids) - return rows, nil - } - // 删除信息失败! - return 0, fmt.Errorf("delete fail") -} - -// NeConfigLocalToNe 网元配置文件复制到网元端覆盖 -func (r *NeConfigBackupImpl) NeConfigLocalToNe(neInfo model.NeInfo, localFile string) error { - neTypeLower := strings.ToLower(neInfo.NeType) - // 网管本地路径 - omcPath := "/usr/local/etc/omc/ne_config" - if runtime.GOOS == "windows" { - omcPath = fmt.Sprintf("C:%s", omcPath) - } - localDirPath := fmt.Sprintf("%s/%s/%s/backup/tmp_import", omcPath, neTypeLower, neInfo.NeId) - if err := file.UnZip(localFile, localDirPath); err != nil { - return fmt.Errorf("unzip err") - } - - // 网元主机的SSH客户端 - sshClient, err := NewNeInfoImpl.NeRunSSHClient(neInfo.NeType, neInfo.NeId) - if err != nil { - return fmt.Errorf("ne info ssh client err") - } - defer sshClient.Close() - // 网元主机的SSH客户端进行文件传输 - sftpClient, err := sshClient.NewClientSFTP() - if err != nil { - return fmt.Errorf("ne info sftp client err") - } - defer sftpClient.Close() - - // 网元配置端上的临时目录 - neDirTemp := fmt.Sprintf("/tmp/omc/ne_config/%s/%s", neTypeLower, neInfo.NeId) - sshClient.RunCMD(fmt.Sprintf("mkdir -p /tmp/omc && sudo chmod 777 -R /tmp/omc && sudo rm -rf %s", neDirTemp)) - // 复制到网元端 - if err = sftpClient.CopyDirLocalToRemote(localDirPath, neDirTemp); err != nil { - return fmt.Errorf("copy config to ne err") - } - - // 配置复制到网元内 - if neTypeLower == "ims" { - // ims目录 - imsDirArr := [...]string{"bgcf", "icscf", "ismc", "mmtel", "mrf", "oam_manager.yaml", "pcscf", "scscf", "vars.cfg", "zlog"} - for _, v := range imsDirArr { - sshClient.RunCMD(fmt.Sprintf("sudo mkdir -p /usr/local/etc/ims && sudo cp -rf %s/ims/%s /usr/local/etc/ims/%v && sudo chmod 755 -R /usr/local/etc/ims/%s", neDirTemp, v, v, v)) - } - // mf目录 - sshClient.RunCMD(fmt.Sprintf("sudo mkdir -p /usr/local/etc/mf && sudo cp -rf %s/mf/* /usr/local/etc/mf && sudo chmod 755 -R /usr/local/etc/mf", neDirTemp)) - // rtproxy目录 - sshClient.RunCMD(fmt.Sprintf("sudo mkdir -p /usr/local/etc/rtproxy && sudo cp -rf %s/rtproxy/* /usr/local/etc/rtproxy && sudo chmod 755 /usr/local/etc/rtproxy/rtproxy.conf", neDirTemp)) - // iwf目录 - sshClient.RunCMD(fmt.Sprintf("sudo mkdir -p /usr/local/etc/iwf && sudo cp -rf %s/iwf/* /usr/local/etc/iwf && sudo chmod 755 /usr/local/etc/iwf/*.yaml", neDirTemp)) - } else if neTypeLower == "omc" { - sshClient.RunCMD(fmt.Sprintf("sudo mkdir -p /usr/local/omc/etc && sudo cp -rf %s/* /usr/local/omc/etc && sudo chmod 755 /usr/local/omc/etc/*.{yaml,conf}", neDirTemp)) - } else { - neEtcPath := fmt.Sprintf("/usr/local/etc/%s", neTypeLower) - chmodFile := fmt.Sprintf("sudo chmod 755 %s/*.yaml", neEtcPath) - if neTypeLower == "mme" { - chmodFile = fmt.Sprintf("sudo chmod 755 %s/*.{yaml,conf}", neEtcPath) - } - sshClient.RunCMD(fmt.Sprintf("sudo cp -rf %s/* %s && %s", neDirTemp, neEtcPath, chmodFile)) - } - - _ = os.RemoveAll(localDirPath) // 删除本地临时目录 - sshClient.RunCMD(fmt.Sprintf("sudo rm -rf %s", neDirTemp)) // 删除临时目录 - return nil -} - -// NeConfigNeToLocal 网元备份文件网元端复制到本地 -func (r *NeConfigBackupImpl) NeConfigNeToLocal(neInfo model.NeInfo) (string, error) { - // 网元主机的SSH客户端 - sshClient, err := NewNeInfoImpl.NeRunSSHClient(neInfo.NeType, neInfo.NeId) - if err != nil { - return "", fmt.Errorf("ne info ssh client err") - } - defer sshClient.Close() - // 网元主机的SSH客户端进行文件传输 - sftpClient, err := sshClient.NewClientSFTP() - if err != nil { - return "", fmt.Errorf("ne info sftp client err") - } - defer sftpClient.Close() - - neTypeLower := strings.ToLower(neInfo.NeType) - // 网管本地路径 - omcPath := "/usr/local/etc/omc/ne_config" - if runtime.GOOS == "windows" { - omcPath = fmt.Sprintf("C:%s", omcPath) - } - localDirPath := fmt.Sprintf("%s/%s/%s/backup/tmp_export", omcPath, neTypeLower, neInfo.NeId) - - // 网元配置文件先复制到临时目录 - sshClient.RunCMD("mkdir -p /tmp/omc && sudo chmod 777 -R /tmp/omc") - neDirTemp := fmt.Sprintf("/tmp/omc/ne_config/%s/%s", neTypeLower, neInfo.NeId) - if neTypeLower == "ims" { - // ims目录 - sshClient.RunCMD(fmt.Sprintf("mkdir -p %s/ims", neDirTemp)) - imsDirArr := [...]string{"bgcf", "icscf", "ismc", "mmtel", "mrf", "oam_manager.yaml", "pcscf", "scscf", "vars.cfg", "zlog"} - for _, v := range imsDirArr { - sshClient.RunCMD(fmt.Sprintf("sudo cp -rf /usr/local/etc/ims/%s %s/ims", v, neDirTemp)) - } - // mf目录 - sshClient.RunCMD(fmt.Sprintf("mkdir -p %s/mf && sudo cp -rf /usr/local/etc/mf %s", neDirTemp, neDirTemp)) - // rtproxy目录 - sshClient.RunCMD(fmt.Sprintf("mkdir -p %s/rtproxy && sudo cp -rf /usr/local/etc/rtproxy/rtproxy.conf %s/rtproxy", neDirTemp, neDirTemp)) - // iwf目录 - sshClient.RunCMD(fmt.Sprintf("mkdir -p %s/iwf && sudo cp -rf /usr/local/etc/iwf/*.yaml %s/iwf", neDirTemp, neDirTemp)) - } else if neTypeLower == "omc" { - sshClient.RunCMD(fmt.Sprintf("mkdir -p %s && sudo cp -rf /usr/local/omc/etc/*.{yaml,conf} %s", neDirTemp, neDirTemp)) - } else { - nePath := fmt.Sprintf("/usr/local/etc/%s/*.yaml", neTypeLower) - if neTypeLower == "mme" { - nePath = fmt.Sprintf("/usr/local/etc/%s/*.{yaml,conf}", neTypeLower) - } - sshClient.RunCMD(fmt.Sprintf("mkdir -p %s && sudo cp -rf %s %s", neDirTemp, nePath, neDirTemp)) - } - - // 网元端复制到本地 - if err = sftpClient.CopyDirRemoteToLocal(neDirTemp, localDirPath); err != nil { - return "", fmt.Errorf("copy config err") - } - - // 压缩zip文件名 - zipFileName := fmt.Sprintf("%s-%s-etc-%s.zip", neTypeLower, neInfo.NeId, date.ParseDateToStr(time.Now(), date.YYYYMMDDHHMMSS)) - zipFilePath := fmt.Sprintf("%s/%s/%s/backup/%s", omcPath, neTypeLower, neInfo.NeId, zipFileName) - if err := file.CompressZipByDir(zipFilePath, localDirPath); err != nil { - return "", fmt.Errorf("compress zip err") - } - - _ = os.RemoveAll(localDirPath) // 删除本地临时目录 - sshClient.RunCMD(fmt.Sprintf("sudo rm -rf %s", neDirTemp)) // 删除临时目录 - return zipFilePath, nil -} diff --git a/src/modules/network_element/service/ne_host.go b/src/modules/network_element/service/ne_host.go index 7b6f0c16..fbedaa15 100644 --- a/src/modules/network_element/service/ne_host.go +++ b/src/modules/network_element/service/ne_host.go @@ -1,30 +1,178 @@ package service -import "be.ems/src/modules/network_element/model" +import ( + "fmt" -// INeHost 网元主机连接 服务层接口 -type INeHost interface { - // SelectPage 根据条件分页查询字典类型 - SelectPage(query map[string]any) map[string]any + "be.ems/src/framework/config" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/crypto" + "be.ems/src/modules/network_element/model" + "be.ems/src/modules/network_element/repository" +) - // SelectList 根据实体查询 - SelectList(neHost model.NeHost) []model.NeHost - - // SelectByIds 通过ID查询 - SelectById(hostId string) model.NeHost - - // CheckUniqueHostTitle 校验分组组和主机名称是否唯一 - CheckUniqueHostTitle(groupId, title, hostType, hostId string) bool - - // Insert 新增信息 - Insert(neHost model.NeHost) string - - // Update 修改信息 - Update(neHost model.NeHost) int64 - - // Insert 批量添加 - Inserts(neHosts []model.NeHost) int64 - - // DeleteByIds 批量删除网元主机连接信息 - DeleteByIds(hostIds []string) (int64, error) +// 实例化服务层 NeHost 结构体 +var NewNeHost = &NeHost{ + neHostRepository: repository.NewNeHost, +} + +// NeHost 网元主机连接 服务层处理 +type NeHost struct { + neHostRepository *repository.NeHost // 网元主机连接表 +} + +// SelectNeHostPage 分页查询列表数据 +func (r *NeHost) SelectPage(query map[string]any) map[string]any { + return r.neHostRepository.SelectPage(query) +} + +// SelectConfigList 查询列表 +func (r *NeHost) SelectList(neHost model.NeHost) []model.NeHost { + return r.neHostRepository.SelectList(neHost) +} + +// SelectByIds 通过ID查询 +func (r *NeHost) SelectById(hostId string) model.NeHost { + neHost := model.NeHost{} + if hostId == "" { + return neHost + } + neHosts := r.neHostRepository.SelectByIds([]string{hostId}) + if len(neHosts) > 0 { + neHost := neHosts[0] + hostKey := config.Get("aes.hostKey").(string) + if neHost.Password != "" { + passwordDe, err := crypto.AESDecryptBase64(neHost.Password, hostKey) + if err != nil { + logger.Errorf("select encrypt: %v", err.Error()) + return neHost + } + neHost.Password = passwordDe + } + if neHost.PrivateKey != "" { + privateKeyDe, err := crypto.AESDecryptBase64(neHost.PrivateKey, hostKey) + if err != nil { + logger.Errorf("select encrypt: %v", err.Error()) + return neHost + } + neHost.PrivateKey = privateKeyDe + } + if neHost.PassPhrase != "" { + passPhraseDe, err := crypto.AESDecryptBase64(neHost.PassPhrase, hostKey) + if err != nil { + logger.Errorf("select encrypt: %v", err.Error()) + return neHost + } + neHost.PassPhrase = passPhraseDe + } + return neHost + } + return model.NeHost{} +} + +// Insert 批量添加 +func (r *NeHost) Inserts(neHosts []model.NeHost) int64 { + var num int64 = 0 + for _, v := range neHosts { + hostId := r.neHostRepository.Insert(v) + if hostId != "" { + num += 1 + } + } + return num +} + +// Insert 新增信息 +func (r *NeHost) Insert(neHost model.NeHost) string { + hostKey := config.Get("aes.hostKey").(string) + if neHost.Password != "" { + passwordEn, err := crypto.AESEncryptBase64(neHost.Password, hostKey) + if err != nil { + logger.Errorf("insert encrypt: %v", err.Error()) + return "" + } + neHost.Password = passwordEn + } + if neHost.PrivateKey != "" { + privateKeyEn, err := crypto.AESEncryptBase64(neHost.PrivateKey, hostKey) + if err != nil { + logger.Errorf("insert encrypt: %v", err.Error()) + return "" + } + neHost.PrivateKey = privateKeyEn + } + if neHost.PassPhrase != "" { + passPhraseEn, err := crypto.AESEncryptBase64(neHost.PassPhrase, hostKey) + if err != nil { + logger.Errorf("insert encrypt: %v", err.Error()) + return "" + } + neHost.PassPhrase = passPhraseEn + } + return r.neHostRepository.Insert(neHost) +} + +// Update 修改信息 +func (r *NeHost) Update(neHost model.NeHost) int64 { + hostKey := config.Get("aes.hostKey").(string) + if neHost.Password != "" { + passwordEn, err := crypto.AESEncryptBase64(neHost.Password, hostKey) + if err != nil { + logger.Errorf("update password encrypt: %v", err.Error()) + return 0 + } + neHost.Password = passwordEn + } + if neHost.PrivateKey != "" { + privateKeyEn, err := crypto.AESEncryptBase64(neHost.PrivateKey, hostKey) + if err != nil { + logger.Errorf("update private key encrypt: %v", err.Error()) + return 0 + } + neHost.PrivateKey = privateKeyEn + } + if neHost.PassPhrase != "" { + passPhraseEn, err := crypto.AESEncryptBase64(neHost.PassPhrase, hostKey) + if err != nil { + logger.Errorf("update pass phrase encrypt: %v", err.Error()) + return 0 + } + neHost.PassPhrase = passPhraseEn + } + return r.neHostRepository.Update(neHost) +} + +// DeleteByIds 批量删除网元主机连接信息 +func (r *NeHost) DeleteByIds(hostIds []string) (int64, error) { + // 检查是否存在 + ids := r.neHostRepository.SelectByIds(hostIds) + if len(ids) <= 0 { + return 0, fmt.Errorf("neHost.noData") + } + + for _, v := range ids { + if v.GroupID == "1" { + // 主机信息操作【%s】失败,禁止操作网元 + return 0, fmt.Errorf("neHost.banNE") + } + } + + if len(ids) == len(hostIds) { + rows := r.neHostRepository.DeleteByIds(hostIds) + return rows, nil + } + // 删除信息失败! + return 0, fmt.Errorf("delete fail") +} + +// CheckUniqueHostTitle 校验分组组和主机名称是否唯一 +func (r *NeHost) CheckUniqueHostTitle(groupId, title, hostType, hostId string) bool { + uniqueId := r.neHostRepository.CheckUniqueNeHost(model.NeHost{ + HostType: hostType, + GroupID: groupId, + Title: title, + }) + if uniqueId == hostId { + return true + } + return uniqueId == "" } diff --git a/src/modules/network_element/service/ne_host.impl.go b/src/modules/network_element/service/ne_host.impl.go deleted file mode 100644 index 101c3c10..00000000 --- a/src/modules/network_element/service/ne_host.impl.go +++ /dev/null @@ -1,92 +0,0 @@ -package service - -import ( - "fmt" - - "be.ems/src/modules/network_element/model" - "be.ems/src/modules/network_element/repository" -) - -// 实例化服务层 NeHostImpl 结构体 -var NewNeHostImpl = &NeHostImpl{ - neHostRepository: repository.NewNeHostImpl, -} - -// NeHostImpl 网元主机连接 服务层处理 -type NeHostImpl struct { - // 网元主机连接表 - neHostRepository repository.INeHost -} - -// SelectNeHostPage 分页查询列表数据 -func (r *NeHostImpl) SelectPage(query map[string]any) map[string]any { - return r.neHostRepository.SelectPage(query) -} - -// SelectConfigList 查询列表 -func (r *NeHostImpl) SelectList(neHost model.NeHost) []model.NeHost { - return r.neHostRepository.SelectList(neHost) -} - -// SelectByIds 通过ID查询 -func (r *NeHostImpl) SelectById(hostId string) model.NeHost { - if hostId == "" { - return model.NeHost{} - } - neHosts := r.neHostRepository.SelectByIds([]string{hostId}) - if len(neHosts) > 0 { - return neHosts[0] - } - return model.NeHost{} -} - -// Insert 批量添加 -func (r *NeHostImpl) Inserts(neHosts []model.NeHost) int64 { - var num int64 = 0 - for _, v := range neHosts { - hostId := r.neHostRepository.Insert(v) - if hostId != "" { - num += 1 - } - } - return num -} - -// Insert 新增信息 -func (r *NeHostImpl) Insert(neHost model.NeHost) string { - return r.neHostRepository.Insert(neHost) -} - -// Update 修改信息 -func (r *NeHostImpl) Update(neHost model.NeHost) int64 { - return r.neHostRepository.Update(neHost) -} - -// DeleteByIds 批量删除网元主机连接信息 -func (r *NeHostImpl) DeleteByIds(hostIds []string) (int64, error) { - // 检查是否存在 - ids := r.neHostRepository.SelectByIds(hostIds) - if len(ids) <= 0 { - return 0, fmt.Errorf("neHost.noData") - } - - if len(ids) == len(hostIds) { - rows := r.neHostRepository.DeleteByIds(hostIds) - return rows, nil - } - // 删除信息失败! - return 0, fmt.Errorf("delete fail") -} - -// CheckUniqueHostTitle 校验分组组和主机名称是否唯一 -func (r *NeHostImpl) CheckUniqueHostTitle(groupId, title, hostType, hostId string) bool { - uniqueId := r.neHostRepository.CheckUniqueNeHost(model.NeHost{ - HostType: hostType, - GroupID: groupId, - Title: title, - }) - if uniqueId == hostId { - return true - } - return uniqueId == "" -} diff --git a/src/modules/network_element/service/ne_host_cmd.go b/src/modules/network_element/service/ne_host_cmd.go index eaea5a35..1936848d 100644 --- a/src/modules/network_element/service/ne_host_cmd.go +++ b/src/modules/network_element/service/ne_host_cmd.go @@ -1,27 +1,79 @@ package service -import "be.ems/src/modules/network_element/model" +import ( + "fmt" -// INeHostCmd 网元主机命令 服务层接口 -type INeHostCmd interface { - // SelectPage 根据条件分页查询字典类型 - SelectPage(query map[string]any) map[string]any + "be.ems/src/modules/network_element/model" + "be.ems/src/modules/network_element/repository" +) - // SelectList 根据实体查询 - SelectList(neHostCmd model.NeHostCmd) []model.NeHostCmd - - // SelectByIds 通过ID查询 - SelectById(cmdId string) model.NeHostCmd - - // Insert 新增信息 - Insert(neHostCmd model.NeHostCmd) string - - // Update 修改信息 - Update(neHostCmd model.NeHostCmd) int64 - - // DeleteByIds 批量删除信息 - DeleteByIds(cmdIds []string) (int64, error) - - // CheckUniqueGroupTitle 校验同类型组内是否唯一 - CheckUniqueGroupTitle(groupId, title, cmdType, cmdId string) bool +// 实例化服务层 NeHostCmd 结构体 +var NewNeHostCmd = &NeHostCmd{ + neHostCmdRepository: repository.NewNeHostCmd, +} + +// NeHostCmd 网元主机命令 服务层处理 +type NeHostCmd struct { + neHostCmdRepository *repository.NeHostCmd // 网元主机命令表 +} + +// SelectNeHostPage 分页查询列表数据 +func (r *NeHostCmd) SelectPage(query map[string]any) map[string]any { + return r.neHostCmdRepository.SelectPage(query) +} + +// SelectConfigList 查询列表 +func (r *NeHostCmd) SelectList(neHostCmd model.NeHostCmd) []model.NeHostCmd { + return r.neHostCmdRepository.SelectList(neHostCmd) +} + +// SelectByIds 通过ID查询 +func (r *NeHostCmd) SelectById(cmdId string) model.NeHostCmd { + if cmdId == "" { + return model.NeHostCmd{} + } + neHosts := r.neHostCmdRepository.SelectByIds([]string{cmdId}) + if len(neHosts) > 0 { + return neHosts[0] + } + return model.NeHostCmd{} +} + +// Insert 新增信息 +func (r *NeHostCmd) Insert(neHostCmd model.NeHostCmd) string { + return r.neHostCmdRepository.Insert(neHostCmd) +} + +// Update 修改信息 +func (r *NeHostCmd) Update(neHostCmd model.NeHostCmd) int64 { + return r.neHostCmdRepository.Update(neHostCmd) +} + +// DeleteByIds 批量删除信息 +func (r *NeHostCmd) DeleteByIds(cmdIds []string) (int64, error) { + // 检查是否存在 + ids := r.neHostCmdRepository.SelectByIds(cmdIds) + if len(ids) <= 0 { + return 0, fmt.Errorf("neHostCmd.noData") + } + + if len(ids) == len(cmdIds) { + rows := r.neHostCmdRepository.DeleteByIds(cmdIds) + return rows, nil + } + // 删除信息失败! + return 0, fmt.Errorf("delete fail") +} + +// CheckUniqueGroupTitle 校验同类型组内是否唯一 +func (r *NeHostCmd) CheckUniqueGroupTitle(groupId, title, cmdType, cmdId string) bool { + uniqueId := r.neHostCmdRepository.CheckUniqueGroupTitle(model.NeHostCmd{ + CmdType: cmdType, + GroupID: groupId, + Title: title, + }) + if uniqueId == cmdId { + return true + } + return uniqueId == "" } diff --git a/src/modules/network_element/service/ne_host_cmd.impl.go b/src/modules/network_element/service/ne_host_cmd.impl.go deleted file mode 100644 index a38c9ba7..00000000 --- a/src/modules/network_element/service/ne_host_cmd.impl.go +++ /dev/null @@ -1,80 +0,0 @@ -package service - -import ( - "fmt" - - "be.ems/src/modules/network_element/model" - "be.ems/src/modules/network_element/repository" -) - -// 实例化服务层 NeHostCmdImpl 结构体 -var NewNeHostCmdImpl = &NeHostCmdImpl{ - neHostCmdRepository: repository.NewNeHostCmdImpl, -} - -// NeHostCmdImpl 网元主机命令 服务层处理 -type NeHostCmdImpl struct { - // 网元主机命令表 - neHostCmdRepository repository.INeHostCmd -} - -// SelectNeHostPage 分页查询列表数据 -func (r *NeHostCmdImpl) SelectPage(query map[string]any) map[string]any { - return r.neHostCmdRepository.SelectPage(query) -} - -// SelectConfigList 查询列表 -func (r *NeHostCmdImpl) SelectList(neHostCmd model.NeHostCmd) []model.NeHostCmd { - return r.neHostCmdRepository.SelectList(neHostCmd) -} - -// SelectByIds 通过ID查询 -func (r *NeHostCmdImpl) SelectById(cmdId string) model.NeHostCmd { - if cmdId == "" { - return model.NeHostCmd{} - } - neHosts := r.neHostCmdRepository.SelectByIds([]string{cmdId}) - if len(neHosts) > 0 { - return neHosts[0] - } - return model.NeHostCmd{} -} - -// Insert 新增信息 -func (r *NeHostCmdImpl) Insert(neHostCmd model.NeHostCmd) string { - return r.neHostCmdRepository.Insert(neHostCmd) -} - -// Update 修改信息 -func (r *NeHostCmdImpl) Update(neHostCmd model.NeHostCmd) int64 { - return r.neHostCmdRepository.Update(neHostCmd) -} - -// DeleteByIds 批量删除信息 -func (r *NeHostCmdImpl) DeleteByIds(cmdIds []string) (int64, error) { - // 检查是否存在 - ids := r.neHostCmdRepository.SelectByIds(cmdIds) - if len(ids) <= 0 { - return 0, fmt.Errorf("neHostCmd.noData") - } - - if len(ids) == len(cmdIds) { - rows := r.neHostCmdRepository.DeleteByIds(cmdIds) - return rows, nil - } - // 删除信息失败! - return 0, fmt.Errorf("delete fail") -} - -// CheckUniqueGroupTitle 校验同类型组内是否唯一 -func (r *NeHostCmdImpl) CheckUniqueGroupTitle(groupId, title, cmdType, cmdId string) bool { - uniqueId := r.neHostCmdRepository.CheckUniqueGroupTitle(model.NeHostCmd{ - CmdType: cmdType, - GroupID: groupId, - Title: title, - }) - if uniqueId == cmdId { - return true - } - return uniqueId == "" -} diff --git a/src/modules/network_element/service/ne_info.go b/src/modules/network_element/service/ne_info.go index b3e9838c..9acd7175 100644 --- a/src/modules/network_element/service/ne_info.go +++ b/src/modules/network_element/service/ne_info.go @@ -1,72 +1,957 @@ package service import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + + "be.ems/src/framework/constants/cachekey" + "be.ems/src/framework/logger" + "be.ems/src/framework/redis" + "be.ems/src/framework/telnet" + "be.ems/src/framework/utils/parse" "be.ems/src/framework/utils/ssh" - "be.ems/src/framework/utils/telnet" + neFetchlink "be.ems/src/modules/network_element/fetch_link" "be.ems/src/modules/network_element/model" + "be.ems/src/modules/network_element/repository" ) -// 网元信息 服务层接口 -type INeInfo interface { - // SelectNeInfoByNeTypeAndNeID 通过ne_type和ne_id查询网元信息 - SelectNeInfoByNeTypeAndNeID(neType, neID string) model.NeInfo - - // RefreshByNeTypeAndNeID 通过ne_type和ne_id刷新redis中的缓存 - RefreshByNeTypeAndNeID(neType, neID string) model.NeInfo - - // ClearNeCacheByNeType 清除网元类型缓存 - ClearNeCacheByNeType(neType string) bool - - // SelectNeInfoByRmuid 通过rmUID查询网元信息 - SelectNeInfoByRmuid(rmUid string) model.NeInfo - - // SelectPage 根据条件分页查询 - // - // bandStatus 带状态信息 - SelectPage(query map[string]any, bandStatus bool) map[string]any - - // SelectList 查询列表 - // - // bandStatus 带状态信息 - // bandHost 带主机信息 - SelectList(ne model.NeInfo, bandStatus bool, bandHost bool) []model.NeInfo - - // SelectByIds 通过ID查询 - // - // bandStatus 带主机信息 - SelectById(infoId string, bandHost bool) model.NeInfo - - // Insert 新增信息 - Insert(neInfo model.NeInfo) string - - // Update 修改信息 - Update(neInfo model.NeInfo) int64 - - // DeleteByIds 批量删除信息 - DeleteByIds(infoIds []string) (int64, error) - - // CheckUniqueNeTypeAndNeId 校验同类型下标识是否唯一 - CheckUniqueNeTypeAndNeId(neType, neId, id string) bool - - // NeRunSSHClient 网元主机的SSH客户端-为创建相关连接,注意结束后 Close() - NeRunSSHClient(neType, neId string) (*ssh.ConnSSH, error) - - // NeRunSSHCmd 网元主机的SSH客户端发送cmd命令 - NeRunSSHCmd(neType, neId, cmd string) (string, error) - - // NeRunTelnetClient 网元主机的Telnet客户端-为创建相关连接,注意结束后 Close() - // num 是网元主机telnet 1:4100 2:5200 - NeRunTelnetClient(neType, neId string, num int) (*telnet.ConnTelnet, error) - - // neConfOAMRead 网元OAM配置文件读取 - NeConfOAMRead(neType, neId string) (map[string]any, error) - - // NeConfOAMSync 网元OAM配置文件生成并同步 - NeConfOAMSync(neInfo model.NeInfo, content map[string]any, sync bool) error - - // NeConfPara5GRead 网元公共配置文件读取 - NeConfPara5GRead() (map[string]any, error) - - // NeConfPara5GWirte 网元公共配置文件写入 content内容 syncNE同步到网元端NeType@NeId - NeConfPara5GWirte(content map[string]any, syncNE []string) error +// 实例化服务层 NeInfo 结构体 +var NewNeInfo = &NeInfo{ + neInfoRepository: repository.NewNeInfo, + Para5GData: map[string]string{}, +} + +// 网元信息 服务层处理 +type NeInfo struct { + neInfoRepository *repository.NeInfo // 网元信息数据信息 + Para5GData map[string]string +} + +// SelectNeInfoByNeTypeAndNeID 通过ne_type和ne_id查询网元信息 +func (r *NeInfo) SelectNeInfoByNeTypeAndNeID(neType, neID string) model.NeInfo { + var neInfo model.NeInfo + key := fmt.Sprintf("%s%s:%s", cachekey.NE_KEY, strings.ToUpper(neType), neID) + jsonStr, _ := redis.Get("", key) + if len(jsonStr) > 7 { + err := json.Unmarshal([]byte(jsonStr), &neInfo) + if err != nil { + neInfo = model.NeInfo{} + } + } else { + neInfo = r.neInfoRepository.SelectNeInfoByNeTypeAndNeID(neType, neID) + if neInfo.ID != "" && neInfo.NeId == neID { + redis.Del("", key) + values, _ := json.Marshal(neInfo) + redis.Set("", key, string(values)) + } + } + return neInfo +} + +// RefreshByNeTypeAndNeID 通过ne_type和ne_id刷新redis中的缓存 +func (r *NeInfo) RefreshByNeTypeAndNeID(neType, neID string) model.NeInfo { + var neInfo model.NeInfo + key := fmt.Sprintf("%s%s:%s", cachekey.NE_KEY, strings.ToUpper(neType), neID) + redis.Del("", key) + neInfo = r.neInfoRepository.SelectNeInfoByNeTypeAndNeID(neType, neID) + if neInfo.ID != "" && neInfo.NeId == neID { + values, _ := json.Marshal(neInfo) + redis.Set("", key, string(values)) + } + return neInfo +} + +// ClearNeCacheByNeType 清除网元类型缓存 +func (r *NeInfo) ClearNeCacheByNeType(neType string) bool { + key := fmt.Sprintf("%s*", cachekey.NE_KEY) + if neType != "*" { + key = fmt.Sprintf("%s%s*", cachekey.NE_KEY, neType) + } + keys, err := redis.GetKeys("", key) + if err != nil { + return false + } + delOk, _ := redis.DelKeys("", keys) + return delOk +} + +// SelectNeInfoByRmuid 通过rmUID查询网元信息 +func (r *NeInfo) SelectNeInfoByRmuid(rmUid string) model.NeInfo { + var neInfo model.NeInfo + cacheKeys, _ := redis.GetKeys("", cachekey.NE_KEY+"*") + if len(cacheKeys) > 0 { + for _, key := range cacheKeys { + var v model.NeInfo + jsonStr, _ := redis.Get("", key) + if len(jsonStr) > 7 { + json.Unmarshal([]byte(jsonStr), &v) + } + if v.RmUID == rmUid { + neInfo = v + break + } + } + } else { + neInfos := r.SelectList(neInfo, false, false) + for _, v := range neInfos { + key := fmt.Sprintf("%s%s:%s", cachekey.NE_KEY, strings.ToUpper(v.NeType), v.NeId) + redis.Del("", key) + values, _ := json.Marshal(v) + redis.Set("", key, string(values)) + if v.RmUID == rmUid { + neInfo = v + } + } + } + return neInfo +} + +// SelectPage 根据条件分页查询 +// +// bandStatus 带状态信息 +func (r *NeInfo) SelectPage(query map[string]any, bandStatus bool) map[string]any { + data := r.neInfoRepository.SelectPage(query) + + // 网元直连读取网元服务状态 + if bandStatus { + rows := data["rows"].([]model.NeInfo) + r.bandNeStatus(&rows) + } + + return data +} + +// SelectList 查询列表 +// +// bandStatus 带状态信息 +// bandHost 带主机信息 +func (r *NeInfo) SelectList(ne model.NeInfo, bandStatus bool, bandHost bool) []model.NeInfo { + list := r.neInfoRepository.SelectList(ne) + + // 网元直连读取网元服务状态 + if bandStatus { + r.bandNeStatus(&list) + } + + // 网元主机信息 + if bandHost { + r.bandNeHosts(&list) + } + + return list +} + +// bandNeStatus 网元列表项数据带网元服务状态 +func (r *NeInfo) bandNeStatus(arr *[]model.NeInfo) { + for i := range *arr { + v := (*arr)[i] + result, err := neFetchlink.NeState(v) + if err != nil { + (*arr)[i].ServerState = map[string]any{ + "online": false, + } + // 网元状态设置为离线 + if v.Status != "0" { + v.Status = "0" + (*arr)[i].Status = v.Status + r.neInfoRepository.Update(v) + } + continue + } + result["online"] = true + (*arr)[i].ServerState = result + // 网元状态设置为在线 + if v.Status != "1" { + // 下发网管配置信息给网元 + _, err = neFetchlink.NeConfigOMC(v) + if err == nil { + v.Status = "1" + } else { + v.Status = "2" + } + (*arr)[i].Status = v.Status + r.neInfoRepository.Update(v) + } + } +} + +// bandNeHosts 网元列表项数据带网元主机信息 +func (r *NeInfo) bandNeHosts(arr *[]model.NeInfo) { + for i := range *arr { + v := (*arr)[i] + if v.HostIDs != "" { + hostIds := strings.Split(v.HostIDs, ",") + if len(hostIds) <= 1 { + continue + } + for _, hostId := range hostIds { + neHost := NewNeHost.SelectById(hostId) + if neHost.HostID == "" || neHost.HostID != hostId { + continue + } + (*arr)[i].Hosts = append((*arr)[i].Hosts, neHost) + } + } + } +} + +// SelectByIds 通过ID查询 +// +// bandHost 带主机信息 +func (r *NeInfo) SelectById(infoId string, bandHost bool) model.NeInfo { + if infoId == "" { + return model.NeInfo{} + } + neInfos := r.neInfoRepository.SelectByIds([]string{infoId}) + if len(neInfos) > 0 { + // 带主机信息 + if neInfos[0].HostIDs != "" && bandHost { + r.bandNeHosts(&neInfos) + } + return neInfos[0] + } + return model.NeInfo{} +} + +// Insert 新增信息 +func (r *NeInfo) Insert(neInfo model.NeInfo) string { + // 主机信息新增 + if neInfo.Hosts != nil { + var hostIDs []string + for _, host := range neInfo.Hosts { + host.Title = fmt.Sprintf("%s_%s_%d", strings.ToUpper(neInfo.NeType), neInfo.NeId, host.Port) + host.GroupID = "1" + host.CreateBy = neInfo.CreateBy + hostId := NewNeHost.Insert(host) + if hostId != "" { + hostIDs = append(hostIDs, hostId) + } + } + neInfo.HostIDs = strings.Join(hostIDs, ",") + } + + insertId := r.neInfoRepository.Insert(neInfo) + if insertId != "" { + // 刷新缓存 + r.RefreshByNeTypeAndNeID(neInfo.NeType, neInfo.NeId) + } + return insertId +} + +// Update 修改信息 +func (r *NeInfo) Update(neInfo model.NeInfo) int64 { + // 主机信息更新 + if neInfo.Hosts != nil { + for _, host := range neInfo.Hosts { + if host.HostID != "" { + host.Title = fmt.Sprintf("%s_%s_%d", strings.ToUpper(neInfo.NeType), neInfo.NeId, host.Port) + host.GroupID = "1" + host.UpdateBy = neInfo.UpdateBy + NewNeHost.Update(host) + } + } + } + + num := r.neInfoRepository.Update(neInfo) + if num > 0 { + // 刷新缓存 + r.RefreshByNeTypeAndNeID(neInfo.NeType, neInfo.NeId) + } + return num +} + +// DeleteByIds 批量删除信息 +func (r *NeInfo) DeleteByIds(infoIds []string) (int64, error) { + // 检查是否存在 + infos := r.neInfoRepository.SelectByIds(infoIds) + if len(infos) <= 0 { + return 0, fmt.Errorf("neHostCmd.noData") + } + + if len(infos) == len(infoIds) { + for _, v := range infos { + // 主机信息删除 + if v.HostIDs != "" { + NewNeHost.DeleteByIds(strings.Split(v.HostIDs, ",")) + } + // 删除License + neLicense := NewNeLicense.SelectByNeTypeAndNeID(v.NeType, v.NeId) + if neLicense.NeId == v.NeId { + NewNeLicense.DeleteByIds([]string{neLicense.ID}) + } + // 删除Version + neVersion := NewNeVersion.SelectByNeTypeAndNeID(v.NeType, v.NeId) + if neVersion.NeId == v.NeId { + NewNeVersion.DeleteByIds([]string{neVersion.ID}) + } + // 缓存信息删除 + redis.Del("", fmt.Sprintf("%s%s:%s", cachekey.NE_KEY, v.NeType, v.NeId)) + } + rows := r.neInfoRepository.DeleteByIds(infoIds) + return rows, nil + } + // 删除信息失败! + return 0, fmt.Errorf("delete fail") +} + +// CheckUniqueNeTypeAndNeId 校验同类型下标识是否唯一 +func (r *NeInfo) CheckUniqueNeTypeAndNeId(neType, neId, id string) bool { + uniqueId := r.neInfoRepository.CheckUniqueNeTypeAndNeId(model.NeInfo{ + NeType: neType, + NeId: neId, + }) + if uniqueId == id { + return true + } + return uniqueId == "" +} + +// NeRunSSHClient 网元主机的SSH客户端-为创建相关连接,注意结束后 Close() +func (r *NeInfo) NeRunSSHClient(neType, neId string) (*ssh.ConnSSH, error) { + neInfo := r.SelectNeInfoByNeTypeAndNeID(neType, neId) + if neInfo.NeId != neId { + logger.Errorf("NeRunSSHClient NeType:%s NeID:%s not found", neType, neId) + return nil, fmt.Errorf("neinfo not found") + } + // 取主机信息 + if neInfo.HostIDs == "" { + logger.Errorf("NeRunSSHClient NeType:%s NeID:%s hostId not found", neType, neId) + return nil, fmt.Errorf("neinfo hostId not found") + } + hostIds := strings.Split(neInfo.HostIDs, ",") + if len(hostIds) <= 1 { + logger.Errorf("NeRunTelnetClient hosts id %s not found", neInfo.HostIDs) + return nil, fmt.Errorf("neinfo host id not found") + } + hostId := hostIds[0] // 网元主机ssh 0:22 + neHost := NewNeHost.SelectById(hostId) + if neHost.HostID == "" || neHost.HostID != hostId { + logger.Errorf("NeRunTelnetClient Hosts %s not found", neInfo.HostIDs) + return nil, fmt.Errorf("neinfo host not found") + } + if neHost.HostType != "ssh" { + logger.Errorf("NeRunSSHClient Hosts first HostType %s not ssh", neHost.HostType) + return nil, fmt.Errorf("neinfo host type not ssh") + } + + var connSSH ssh.ConnSSH + neHost.CopyTo(&connSSH) + var client *ssh.ConnSSH + var err error + if neHost.AuthMode == "2" { + client, err = connSSH.NewClientByLocalPrivate() + } else { + client, err = connSSH.NewClient() + } + if err != nil { + logger.Errorf("NeRunSSHClient NewClient err => %s", err.Error()) + return nil, fmt.Errorf("neinfo ssh client new err") + } + return client, nil +} + +// NeRunSSHCmd 网元主机的SSH客户端发送cmd命令 +func (r *NeInfo) NeRunSSHCmd(neType, neId, cmd string) (string, error) { + sshClient, err := r.NeRunSSHClient(neType, neId) + if err != nil { + return "", err + } + defer sshClient.Close() + + // 执行命令 + output, err := sshClient.RunCMD(cmd) + if err != nil { + logger.Errorf("NeRunSSHCmd RunCMD %s err => %s", output, err.Error()) + return "", fmt.Errorf("neinfo ssh run cmd err") + } + return output, nil +} + +// NeRunTelnetClient 网元主机的Telnet客户端-为创建相关连接,注意结束后 Close() +// num 是网元主机telnet 1:4100 2:5200(UPF标准版) +func (r *NeInfo) NeRunTelnetClient(neType, neId string, num int) (*telnet.ConnTelnet, error) { + neInfo := r.SelectNeInfoByNeTypeAndNeID(neType, neId) + if neInfo.NeId != neId { + logger.Errorf("NeRunTelnetClient NeType:%s NeID:%s not found", neType, neId) + return nil, fmt.Errorf("neinfo not found") + } + // 取主机信息 + if neInfo.HostIDs == "" { + logger.Errorf("NeRunTelnetClient NeType:%s NeID:%s hostId not found", neType, neId) + return nil, fmt.Errorf("neinfo hostId not found") + } + hostIds := strings.Split(neInfo.HostIDs, ",") + if len(hostIds) <= 1 { + logger.Errorf("NeRunTelnetClient hosts id %s not found", neInfo.HostIDs) + return nil, fmt.Errorf("neinfo host id not found") + } + hostId := hostIds[num] // 网元主机telnet 1:4100 2:5200 + neHost := NewNeHost.SelectById(hostId) + if neHost.HostID == "" || neHost.HostID != hostId { + logger.Errorf("NeRunTelnetClient Hosts %s not found", neInfo.HostIDs) + return nil, fmt.Errorf("neinfo host not found") + } + + // 创建链接Telnet客户端 + var connTelnet telnet.ConnTelnet + neHost.CopyTo(&connTelnet) + telnetClient, err := connTelnet.NewClient() + if err != nil { + logger.Errorf("NeRunTelnetClient NewClient err => %s", err.Error()) + return nil, fmt.Errorf("neinfo telnet client new err") + } + return telnetClient, nil +} + +// NeRunRedisClient 网元主机的Redis客户端-为创建相关连接,注意结束后 Close() +// 暂时只有UDM有Redis配置项 +func (r *NeInfo) NeRunRedisClient(neType, neId string) (*redis.ConnRedis, error) { + neInfo := r.SelectNeInfoByNeTypeAndNeID(neType, neId) + if neInfo.NeId != neId { + logger.Errorf("NeRunRedisClient NeType:%s NeID:%s not found", neType, neId) + return nil, fmt.Errorf("neinfo not found") + } + // 取主机信息 + if neInfo.HostIDs == "" { + logger.Errorf("NeRunRedisClient NeType:%s NeID:%s hostId not found", neType, neId) + return nil, fmt.Errorf("neinfo hostId not found") + } + hostIds := strings.Split(neInfo.HostIDs, ",") + if len(hostIds) <= 2 { + logger.Errorf("NeRunRedisClient hosts id %s not found", neInfo.HostIDs) + return nil, fmt.Errorf("neinfo host id not found") + } + hostId := hostIds[2] + neHost := NewNeHost.SelectById(hostId) + if neHost.HostID == "" || neHost.HostID != hostId { + logger.Errorf("NeRunRedisClient Hosts %s not found", neInfo.HostIDs) + return nil, fmt.Errorf("neinfo host not found") + } + + // 创建链接Redis客户端 + var connRedis redis.ConnRedis + neHost.CopyTo(&connRedis) + redisClient, err := connRedis.NewClient() + if err != nil { + logger.Errorf("NeRunRedisClient NewClient err => %s", err.Error()) + return nil, fmt.Errorf("neinfo redis client new err") + } + return redisClient, nil +} + +// NeConfOAMReadSync 网元OAM配置文件读取 +func (r *NeInfo) NeConfOAMReadSync(neType, neId string) (map[string]any, error) { + oamData, err := r.neConfOAMRead(neType, neId, true) + if err != nil { + return nil, err + } + + // UPF和SMF 全小写的key + if _, ok := oamData["httpmanagecfg"]; ok { + content := map[string]any{} + // 网元HTTP服务 + // if v, ok := oamData["httpmanagecfg"]; ok { + // item := v.(map[string]any) + // } + // 对网管HTTP配置 + if v, ok := oamData["oamconfig"]; ok { + item := v.(map[string]any) + if v, ok := item["iptype"]; ok && v != "" && v != nil { + ipType := v.(string) + if ipType == "ipv6" { + content["omcIP"] = item["ipv6"] + } + if ipType == "ipv4" { + content["omcIP"] = item["ipv4"] + } + } + content["oamEnable"] = item["enable"] + content["oamPort"] = item["port"] + } + // 对网管SNMP配置 + if v, ok := oamData["snmpconfig"]; ok { + item := v.(map[string]any) + content["snmpEnable"] = item["enable"] + content["snmpPort"] = item["port"] + } + // 对网管KPI上报配置 + if v, ok := oamData["kpiconfig"]; ok { + item := v.(map[string]any) + content["kpiEnable"] = item["enable"] + content["kpiTimer"] = item["timer"] + } + + oamData := r.neConfOAMData() + r.neConfOAMWirte(neType, neId, oamData, false) + r.NeConfOAMWirteSync(model.NeInfo{ + NeType: neType, + NeId: neId, + }, content, false) + return r.neConfOAMRead(neType, neId, false) + } + + // NSSF和MME 配置KPIconfig名不一致时 + if v, ok := oamData["KPIconfig"]; ok && v != nil { + item := v.(map[string]any) + oamData["kpiConfig"] = item + delete(oamData, "KPIconfig") + r.neConfOAMWirte(neType, neId, oamData, false) + } + + return oamData, nil +} + +// neConfOAMData 网元OAM配置文件默认格式数据 +func (r *NeInfo) neConfOAMData() map[string]any { + return map[string]any{ + "httpManageCfg": map[string]any{ + "ipType": "ipv4", + "ipv4": "172.16.5.1", // 必改 + "ipv6": "", + "port": 33030, + "scheme": "http", + }, + "oamConfig": map[string]any{ + "enable": true, + "ipType": "ipv4", + "ipv4": "172.16.5.100", // 必改 + "ipv6": "", + "port": 33030, + "scheme": "http", + // 必改 + "neConfig": map[string]any{ + "neId": "001", + "rmUid": "4400HX1XXX001", + "neName": "XXX_001", + "dn": "-", + "vendorName": "GD", + "province": "-", + "pvFlag": "PNF", + }, + }, + "snmpConfig": map[string]any{ + "enable": false, + "ipType": "ipv4", + "ipv4": "172.16.5.1", // 必改 + "ipv6": "", + "port": 4957, + }, + "kpiConfig": map[string]any{ + "enable": true, + "timer": 60, // 必改 + }, + // "pubConfigPath": "/usr/local/etc/conf/para5G.yaml", // 网元只会读一次后续会置空,建议不放 + } +} + +// neConfOAMRead 网元OAM配置文件读取 sync从网元端同步到本地 +func (r *NeInfo) neConfOAMRead(neType, neId string, sync bool) (map[string]any, error) { + neTypeLower := strings.ToLower(neType) + fileName := "oam_manager.yaml" + // 网管本地路径 + localFilePath := fmt.Sprintf("/usr/local/etc/omc/ne_config/%s/%s/%s", neTypeLower, neId, fileName) + if runtime.GOOS == "windows" { + localFilePath = fmt.Sprintf("C:%s", localFilePath) + } + + // 从网元端同步到本地 + if sync { + // 网元主机的SSH客户端 + sshClient, err := r.NeRunSSHClient(neType, neId) + if err != nil { + return nil, fmt.Errorf("ne info ssh client err") + } + defer sshClient.Close() + // 网元主机的SSH客户端进行文件传输 + sftpClient, err := sshClient.NewClientSFTP() + if err != nil { + return nil, fmt.Errorf("ne info sftp client err") + } + defer sftpClient.Close() + // 网元端文件路径 + neFilePath := fmt.Sprintf("/usr/local/etc/%s/%s", neTypeLower, fileName) + // 修改网元文件权限 + sshClient.RunCMD(fmt.Sprintf("sudo touch %s && sudo chmod o+rw %s", neFilePath, neFilePath)) + // 网元端复制到本地 + if err = sftpClient.CopyFileRemoteToLocal(neFilePath, localFilePath); err != nil { + return nil, fmt.Errorf("copy oam config err") + } + } + + // 读取文件内容 + bytes, err := os.ReadFile(localFilePath) + if err != nil { + // logger.Warnf("NeConfOAMRead ReadFile => %s", err.Error()) + // return nil, fmt.Errorf("read file error") + // 无保留文件时返回默认文件数据 + oamData := r.neConfOAMData() + r.neConfOAMWirte(neType, neId, oamData, false) + return oamData, nil + } + content := string(bytes) + + // 序列化Map + mapData, err := parse.ConvertConfigToMap("yaml", content) + if err != nil { + logger.Warnf("NeConfOAMRead ConvertConfigToMap => %s", err.Error()) + return nil, fmt.Errorf("content convert type error") + } + return mapData, nil +} + +// neConfOAMWirte 网元OAM配置文件写入 content内容 sync同步到网元端 +func (r *NeInfo) neConfOAMWirte(neType, neId string, content any, sync bool) error { + neTypeLower := strings.ToLower(neType) + fileName := "oam_manager.yaml" + // 网管本地路径 + omcPath := "/usr/local/etc/omc/ne_config" + if runtime.GOOS == "windows" { + omcPath = fmt.Sprintf("C:%s", omcPath) + } + localFilePath := fmt.Sprintf("%s/%s/%s/%s", omcPath, neTypeLower, neId, fileName) + + // 写入文件 + if err := parse.ConvertConfigToFile("yaml", localFilePath, content); err != nil { + return fmt.Errorf("please check if the file exists or write permissions") + } + + // 同步到网元端 + if sync { + // 网元主机的SSH客户端 + sshClient, err := r.NeRunSSHClient(neType, neId) + if err != nil { + return err + } + defer sshClient.Close() + // 网元主机的SSH客户端进行文件传输 + sftpClient, err := sshClient.NewClientSFTP() + if err != nil { + return err + } + defer sftpClient.Close() + + // 网元端配置路径 + neFilePath := fmt.Sprintf("/usr/local/etc/%s/%s", neTypeLower, fileName) + neFileDir := filepath.ToSlash(filepath.Dir(neFilePath)) + // 修改网元文件权限 + sshClient.RunCMD(fmt.Sprintf("sudo mkdir -p %s && sudo chmod 775 %s && sudo touch %s && sudo chmod o+rw %s", neFileDir, neFileDir, neFilePath, neFilePath)) + // 复制到网元进行覆盖 + if err = sftpClient.CopyFileLocalToRemote(localFilePath, neFilePath); err != nil { + return fmt.Errorf("please check if scp remote copy is allowed") + } + } + + return nil +} + +// NeConfOAMWirteSync 网元OAM配置文件生成并同步 +func (r *NeInfo) NeConfOAMWirteSync(neInfo model.NeInfo, content map[string]any, sync bool) error { + oamData, err := r.neConfOAMRead(neInfo.NeType, neInfo.NeId, false) + if oamData == nil || err != nil { + return fmt.Errorf("error read OAM file info") + } + // 网元HTTP服务 + if v, ok := oamData["httpManageCfg"]; ok { + item := v.(map[string]any) + item["port"] = neInfo.Port + if strings.Contains(neInfo.IP, ":") { + item["ipType"] = "ipv6" + item["ipv6"] = neInfo.IP + } + if strings.Contains(neInfo.IP, ".") { + item["ipType"] = "ipv4" + item["ipv4"] = neInfo.IP + } + + oamData["httpManageCfg"] = item + } + // 对网管HTTP配置 + if v, ok := oamData["oamConfig"]; ok { + item := v.(map[string]any) + item["neConfig"] = map[string]string{ + "neId": neInfo.NeId, + "rmUid": neInfo.RmUID, + "neName": neInfo.NeName, + "dn": neInfo.Dn, + "vendorName": neInfo.VendorName, + "province": neInfo.Province, + "pvFlag": neInfo.PvFlag, + } + + // 公共参数指定的OMC + if omcIP, ok := r.Para5GData["OMC_IP"]; ok && omcIP != "" { + if strings.Contains(omcIP, ":") { + item["ipType"] = "ipv6" + item["ipv6"] = omcIP + } + if strings.Contains(omcIP, ".") { + item["ipType"] = "ipv4" + item["ipv4"] = omcIP + } + } + + if v, ok := content["omcIP"]; ok && v != "" && v != nil { + omcIP := v.(string) + if strings.Contains(omcIP, ":") { + item["ipType"] = "ipv6" + item["ipv6"] = omcIP + } + if strings.Contains(omcIP, ".") { + item["ipType"] = "ipv4" + item["ipv4"] = omcIP + } + } + if oamEnable, ok := content["oamEnable"]; ok && oamEnable != nil { + item["enable"] = parse.Boolean(oamEnable) + } + if oamPort, ok := content["oamPort"]; ok && oamPort != nil { + item["port"] = parse.Number(oamPort) + } + oamData["oamConfig"] = item + } + // 对网管SNMP配置 + if v, ok := oamData["snmpConfig"]; ok { + item := v.(map[string]any) + if strings.Contains(neInfo.IP, ":") { + item["ipType"] = "ipv6" + item["ipv6"] = neInfo.IP + } + if strings.Contains(neInfo.IP, ".") { + item["ipType"] = "ipv4" + item["ipv4"] = neInfo.IP + } + + if snmpEnable, ok := content["snmpEnable"]; ok && snmpEnable != nil { + item["enable"] = parse.Boolean(snmpEnable) + } + if snmpPort, ok := content["snmpPort"]; ok && snmpPort != nil { + item["port"] = parse.Number(snmpPort) + } + oamData["snmpConfig"] = item + } + // 对网管KPI上报配置 + if v, ok := oamData["kpiConfig"]; ok { + item := v.(map[string]any) + if neInfo.NeType == "UPF" { + item["timer"] = 5 + } else { + item["timer"] = 60 + } + + if kpiEnable, ok := content["kpiEnable"]; ok && kpiEnable != nil { + item["enable"] = parse.Boolean(kpiEnable) + } + if kpiTimer, ok := content["kpiTimer"]; ok && kpiTimer != nil { + item["timer"] = parse.Number(kpiTimer) + } + oamData["kpiConfig"] = item + } + if err := r.neConfOAMWirte(neInfo.NeType, neInfo.NeId, oamData, sync); err != nil { + return fmt.Errorf("error wirte OAM file info") + } + return nil +} + +// NeConfPara5GRead 网元公共配置文件读取 +func (r *NeInfo) NeConfPara5GRead() (map[string]any, error) { + // 网管本地路径 + omcFilePath := "/usr/local/etc/omc/para5G.yaml" + if runtime.GOOS == "windows" { + omcFilePath = fmt.Sprintf("C:%s", omcFilePath) + } + // 读取文件内容 + bytes, err := os.ReadFile(omcFilePath) + if err != nil { + logger.Warnf("NeConfPara5GRead ReadFile => %s", err.Error()) + return nil, fmt.Errorf("read file error") + } + content := string(bytes) + + // 序列化Map + mapData, err := parse.ConvertConfigToMap("yaml", content) + if err != nil { + logger.Warnf("NeConfPara5GRead ConvertConfigToMap => %s", err.Error()) + return nil, fmt.Errorf("content convert type error") + } + return mapData, nil +} + +// NeConfPara5GWirte 网元公共配置文件写入 content内容 syncNE同步到网元端NeType@NeId +func (r *NeInfo) NeConfPara5GWirte(content map[string]any, syncNE []string) error { + // 网管本地路径 + omcFilePath := "/usr/local/etc/omc/para5G.yaml" + if runtime.GOOS == "windows" { + omcFilePath = fmt.Sprintf("C:%s", omcFilePath) + } + + if err := parse.ConvertConfigToFile("yaml", omcFilePath, content); err != nil { + return fmt.Errorf("please check if the file exists or write permissions") + } + + // 同步到网元端 + if len(syncNE) > 0 { + errMsg := []string{} + for _, neTI := range syncNE { + ti := strings.SplitN(neTI, "@", 2) + // 网元主机的SSH客户端 + sshClient, err := r.NeRunSSHClient(ti[0], ti[1]) + if err != nil { + errMsg = append(errMsg, fmt.Sprintf("%s : %s", ti, err.Error())) + continue + } + defer sshClient.Close() + // 网元主机的SSH客户端进行文件传输 + sftpClient, err := sshClient.NewClientSFTP() + if err != nil { + errMsg = append(errMsg, fmt.Sprintf("%s : %s", ti, err.Error())) + continue + } + defer sftpClient.Close() + + // 网元端配置路径 + neFilePath := "/usr/local/etc/conf/para5G.yaml" + neFileDir := filepath.ToSlash(filepath.Dir(neFilePath)) + // 修改网元文件权限 + sshClient.RunCMD(fmt.Sprintf("sudo mkdir -p %s && sudo chmod 775 %s && sudo touch %s && sudo chmod o+rw %s", neFileDir, neFileDir, neFilePath, neFilePath)) + // 复制到网元进行覆盖 + if err = sftpClient.CopyFileLocalToRemote(omcFilePath, neFilePath); err != nil { + errMsg = append(errMsg, fmt.Sprintf("%s : please check if scp remote copy is allowed", ti)) + continue + } + } + if len(errMsg) > 0 { + return fmt.Errorf("%s", strings.Join(errMsg, "\r\n")) + } + } + + // 转换一份数据到全局 + r.Para5GData = r.neConfPara5GDataConvert(content) + return nil +} + +// NeConfPara5GConvert 网元公共配置数据转化 content网元公共配置文件读取内容 +func (r *NeInfo) neConfPara5GDataConvert(content map[string]any) map[string]string { + defer func() { + if err := recover(); err != nil { + logger.Errorf("NeConfPara5GDataConvert panic: %v", err) + // 文件异常就删除配置 + omcFilePath := "/usr/local/etc/omc/para5G.yaml" + if runtime.GOOS == "windows" { + omcFilePath = fmt.Sprintf("C:%s", omcFilePath) + } + os.Remove(omcFilePath) + } + }() + + basic := content["basic"].(map[string]any) + external := content["external"].(map[string]any) + sbi := content["sbi"].(map[string]any) + + mcc := "460" + mnc := "01" + mncDomain := "001" + if plmnId, plmnIdOk := basic["plmnId"].(map[string]any); plmnIdOk { + mcc = plmnId["mcc"].(string) + mnc = plmnId["mnc"].(string) + // If a user input two digit MNC, add a leading zero + if len(mnc) == 2 { + mncDomain = fmt.Sprintf("0%s", mnc) + } else { + mncDomain = mnc + } + } + + sst := "1" + sd := "000001" + if plmnId, plmnIdOk := basic["snssai"].(map[string]any); plmnIdOk { + sst = plmnId["sst"].(string) + sd = plmnId["sd"].(string) + } + + n3IPAmdMask := external["upfn3_ip"].(string) + n3Arr := strings.SplitN(n3IPAmdMask, "/", 2) + n3IP := n3Arr[0] + n3Mask := "255.255.255.0" + if len(n3Arr) > 1 { + n3Mask = parse.ConvertIPMask(parse.Number(n3Arr[1])) + } + + n6IPAmdMask := external["upfn6_ip"].(string) + n6Arr := strings.SplitN(n6IPAmdMask, "/", 2) + n6IP := n6Arr[0] + n6Mask := "255.255.255.0" + if len(n6Arr) > 1 { + n6Mask = parse.ConvertIPMask(parse.Number(n6Arr[1])) + } + + ueIPAmdMask := external["ue_pool"].(string) + ueArr := strings.SplitN(ueIPAmdMask, "/", 2) + ueIP := ueArr[0] + ueCicr := "24" + ueMask := "255.255.255.0" + if len(ueArr) > 1 { + ueCicr = ueArr[1] + ueMask = parse.ConvertIPMask(parse.Number(ueArr[1])) + } + + return map[string]string{ + // basic + "TAC": basic["tac"].(string), + "MCC": mcc, + "MNC": mnc, + "MNC_DOMAIN": mncDomain, + "SST": sst, + "SD": sd, + "DNN_DATA": basic["dnn_data"].(string), + "DNN_IMS": basic["dnn_ims"].(string), + + // external + "N2_IP": external["amfn2_ip"].(string), + "UE_POOL": external["ue_pool"].(string), + "UE_IP": ueIP, + "UE_MASK": ueMask, + "UE_CIDR": ueCicr, + "UPF_TYPE": external["upf_type"].(string), // 类型 StandardUPF LightUPF + "UPF_DRIVER_TYPE": external["upf_driver_type"].(string), // 网卡驱动 vmxnet3 host dpdk + "UPF_NIC_NAME": external["upf_card_name"].(string), // 网卡名 eth0 + "N3_IP": n3IP, + "N3_MASK": n3Mask, + "N3_GW": external["upfn3_gw"].(string), + "N3_PCI": external["upfn3_pci"].(string), + "N3_MAC": external["upfn3_mac"].(string), + "N6_IP": n6IP, + "N6_MASK": n6Mask, + "N6_GW": external["upfn6_gw"].(string), + "N6_PCI": external["upfn6_pci"].(string), + "N6_MAC": external["upfn6_mac"].(string), + + "SIP_IP": external["ims_sip_ip"].(string), + + "S1_MMEIP": external["mmes1_ip"].(string), + "S11_MMEIP": external["mmes11_ip"].(string), + "S10_MMEIP": external["mmes10_ip"].(string), + + // sbi + "OMC_IP": sbi["omc_ip"].(string), + "IMS_IP": sbi["ims_ip"].(string), + "AMF_IP": sbi["amf_ip"].(string), + "AUSF_IP": sbi["ausf_ip"].(string), + "UDM_IP": sbi["udm_ip"].(string), + "SMF_IP": sbi["smf_ip"].(string), + "PCF_IP": sbi["pcf_ip"].(string), + "NSSF_IP": sbi["nssf_ip"].(string), + "NRF_IP": sbi["nrf_ip"].(string), + "UPF_IP": sbi["upf_ip"].(string), + "LMF_IP": sbi["lmf_ip"].(string), + "NEF_IP": sbi["nef_ip"].(string), + "MME_IP": sbi["mme_ip"].(string), + "N3IWF_IP": sbi["n3iwf_ip"].(string), + "SMSC_IP": sbi["smsc_ip"].(string), + + "DB_IP": sbi["db_ip"].(string), + } } diff --git a/src/modules/network_element/service/ne_info.impl.go b/src/modules/network_element/service/ne_info.impl.go deleted file mode 100644 index 51497725..00000000 --- a/src/modules/network_element/service/ne_info.impl.go +++ /dev/null @@ -1,798 +0,0 @@ -package service - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - "runtime" - "strings" - - "be.ems/src/framework/constants/cachekey" - "be.ems/src/framework/logger" - "be.ems/src/framework/redis" - "be.ems/src/framework/utils/parse" - "be.ems/src/framework/utils/ssh" - "be.ems/src/framework/utils/telnet" - neFetchlink "be.ems/src/modules/network_element/fetch_link" - "be.ems/src/modules/network_element/model" - "be.ems/src/modules/network_element/repository" -) - -// 实例化服务层 NeInfoImpl 结构体 -var NewNeInfoImpl = &NeInfoImpl{ - neInfoRepository: repository.NewNeInfoImpl, - Para5GData: map[string]string{}, -} - -// 网元信息 服务层处理 -type NeInfoImpl struct { - // 网元信息数据信息 - neInfoRepository repository.INeInfo - Para5GData map[string]string -} - -// SelectNeInfoByNeTypeAndNeID 通过ne_type和ne_id查询网元信息 -func (r *NeInfoImpl) SelectNeInfoByNeTypeAndNeID(neType, neID string) model.NeInfo { - var neInfo model.NeInfo - key := fmt.Sprintf("%s%s:%s", cachekey.NE_KEY, strings.ToUpper(neType), neID) - jsonStr, _ := redis.Get("", key) - if len(jsonStr) > 7 { - err := json.Unmarshal([]byte(jsonStr), &neInfo) - if err != nil { - neInfo = model.NeInfo{} - } - } else { - neInfo = r.neInfoRepository.SelectNeInfoByNeTypeAndNeID(neType, neID) - if neInfo.ID != "" && neInfo.NeId == neID { - redis.Del("", key) - values, _ := json.Marshal(neInfo) - redis.Set("", key, string(values)) - } - } - return neInfo -} - -// RefreshByNeTypeAndNeID 通过ne_type和ne_id刷新redis中的缓存 -func (r *NeInfoImpl) RefreshByNeTypeAndNeID(neType, neID string) model.NeInfo { - var neInfo model.NeInfo - key := fmt.Sprintf("%s%s:%s", cachekey.NE_KEY, strings.ToUpper(neType), neID) - redis.Del("", key) - neInfo = r.neInfoRepository.SelectNeInfoByNeTypeAndNeID(neType, neID) - if neInfo.ID != "" && neInfo.NeId == neID { - values, _ := json.Marshal(neInfo) - redis.Set("", key, string(values)) - } - return neInfo -} - -// ClearNeCacheByNeType 清除网元类型缓存 -func (r *NeInfoImpl) ClearNeCacheByNeType(neType string) bool { - key := fmt.Sprintf("%s*", cachekey.NE_KEY) - if neType != "*" { - key = fmt.Sprintf("%s%s*", cachekey.NE_KEY, neType) - } - keys, err := redis.GetKeys("", key) - if err != nil { - return false - } - delOk, _ := redis.DelKeys("", keys) - return delOk -} - -// SelectNeInfoByRmuid 通过rmUID查询网元信息 -func (r *NeInfoImpl) SelectNeInfoByRmuid(rmUid string) model.NeInfo { - var neInfo model.NeInfo - cacheKeys, _ := redis.GetKeys("", cachekey.NE_KEY+"*") - if len(cacheKeys) > 0 { - for _, key := range cacheKeys { - var v model.NeInfo - jsonStr, _ := redis.Get("", key) - if len(jsonStr) > 7 { - json.Unmarshal([]byte(jsonStr), &v) - } - if v.RmUID == rmUid { - neInfo = v - break - } - } - } else { - neInfos := r.SelectList(neInfo, false, false) - for _, v := range neInfos { - key := fmt.Sprintf("%s%s:%s", cachekey.NE_KEY, strings.ToUpper(v.NeType), v.NeId) - redis.Del("", key) - values, _ := json.Marshal(v) - redis.Set("", key, string(values)) - if v.RmUID == rmUid { - neInfo = v - } - } - } - return neInfo -} - -// SelectPage 根据条件分页查询 -// -// bandStatus 带状态信息 -func (r *NeInfoImpl) SelectPage(query map[string]any, bandStatus bool) map[string]any { - data := r.neInfoRepository.SelectPage(query) - - // 网元直连读取网元服务状态 - if bandStatus { - rows := data["rows"].([]model.NeInfo) - r.bandNeStatus(&rows) - } - - return data -} - -// SelectList 查询列表 -// -// bandStatus 带状态信息 -// bandHost 带主机信息 -func (r *NeInfoImpl) SelectList(ne model.NeInfo, bandStatus bool, bandHost bool) []model.NeInfo { - list := r.neInfoRepository.SelectList(ne) - - // 网元直连读取网元服务状态 - if bandStatus { - r.bandNeStatus(&list) - } - - // 网元主机信息 - if bandHost { - r.bandNeHosts(&list) - } - - return list -} - -// bandNeStatus 网元列表项数据带网元服务状态 -func (r *NeInfoImpl) bandNeStatus(arr *[]model.NeInfo) { - for i := range *arr { - v := (*arr)[i] - result, err := neFetchlink.NeState(v) - if err != nil { - (*arr)[i].ServerState = map[string]any{ - "online": false, - } - // 网元状态设置为离线 - if v.Status != "0" { - v.Status = "0" - (*arr)[i].Status = v.Status - r.neInfoRepository.Update(v) - } - continue - } - result["online"] = true - (*arr)[i].ServerState = result - // 网元状态设置为在线 - if v.Status != "1" { - // 下发网管配置信息给网元 - _, err = neFetchlink.NeConfigOMC(v) - if err == nil { - v.Status = "1" - } else { - v.Status = "2" - } - (*arr)[i].Status = v.Status - r.neInfoRepository.Update(v) - } - } -} - -// bandNeHosts 网元列表项数据带网元主机信息 -func (r *NeInfoImpl) bandNeHosts(arr *[]model.NeInfo) { - for i := range *arr { - v := (*arr)[i] - if v.HostIDs != "" { - (*arr)[i].Hosts = NewNeHostImpl.neHostRepository.SelectByIds(strings.Split(v.HostIDs, ",")) - } - } -} - -// SelectByIds 通过ID查询 -// -// bandHost 带主机信息 -func (r *NeInfoImpl) SelectById(infoId string, bandHost bool) model.NeInfo { - if infoId == "" { - return model.NeInfo{} - } - neInfos := r.neInfoRepository.SelectByIds([]string{infoId}) - if len(neInfos) > 0 { - neInfo := neInfos[0] - // 带主机信息 - if neInfo.HostIDs != "" && bandHost { - neInfo.Hosts = NewNeHostImpl.neHostRepository.SelectByIds(strings.Split(neInfo.HostIDs, ",")) - } - return neInfo - } - return model.NeInfo{} -} - -// Insert 新增信息 -func (r *NeInfoImpl) Insert(neInfo model.NeInfo) string { - // 主机信息新增 - if neInfo.Hosts != nil { - var hostIDs []string - for _, host := range neInfo.Hosts { - host.Title = fmt.Sprintf("%s_%s_%d", strings.ToUpper(neInfo.NeType), neInfo.NeId, host.Port) - host.GroupID = "1" - hostId := NewNeHostImpl.Insert(host) - if hostId != "" { - hostIDs = append(hostIDs, hostId) - } - } - neInfo.HostIDs = strings.Join(hostIDs, ",") - } - - insertId := r.neInfoRepository.Insert(neInfo) - if insertId != "" { - // 刷新缓存 - r.RefreshByNeTypeAndNeID(neInfo.NeType, neInfo.NeId) - } - return insertId -} - -// Update 修改信息 -func (r *NeInfoImpl) Update(neInfo model.NeInfo) int64 { - // 主机信息更新 - if neInfo.Hosts != nil { - for _, host := range neInfo.Hosts { - if host.HostID != "" { - host.Title = fmt.Sprintf("%s_%s_%d", strings.ToUpper(neInfo.NeType), neInfo.NeId, host.Port) - host.GroupID = "1" - NewNeHostImpl.Update(host) - } - } - } - - num := r.neInfoRepository.Update(neInfo) - if num > 0 { - // 刷新缓存 - r.RefreshByNeTypeAndNeID(neInfo.NeType, neInfo.NeId) - } - return num -} - -// DeleteByIds 批量删除信息 -func (r *NeInfoImpl) DeleteByIds(infoIds []string) (int64, error) { - // 检查是否存在 - infos := r.neInfoRepository.SelectByIds(infoIds) - if len(infos) <= 0 { - return 0, fmt.Errorf("neHostCmd.noData") - } - - if len(infos) == len(infoIds) { - for _, v := range infos { - // 主机信息删除 - if v.HostIDs != "" { - NewNeHostImpl.DeleteByIds(strings.Split(v.HostIDs, ",")) - } - // 删除License - neLicense := NewNeLicenseImpl.SelectByNeTypeAndNeID(v.NeType, v.NeId) - if neLicense.NeId == v.NeId { - NewNeLicenseImpl.DeleteByIds([]string{neLicense.ID}) - } - // 删除Version - neVersion := NewNeVersionImpl.SelectByNeTypeAndNeID(v.NeType, v.NeId) - if neVersion.NeId == v.NeId { - NewNeVersionImpl.DeleteByIds([]string{neVersion.ID}) - } - // 缓存信息删除 - redis.Del("", fmt.Sprintf("%s%s:%s", cachekey.NE_KEY, v.NeType, v.NeId)) - } - rows := r.neInfoRepository.DeleteByIds(infoIds) - return rows, nil - } - // 删除信息失败! - return 0, fmt.Errorf("delete fail") -} - -// CheckUniqueNeTypeAndNeId 校验同类型下标识是否唯一 -func (r *NeInfoImpl) CheckUniqueNeTypeAndNeId(neType, neId, id string) bool { - uniqueId := r.neInfoRepository.CheckUniqueNeTypeAndNeId(model.NeInfo{ - NeType: neType, - NeId: neId, - }) - if uniqueId == id { - return true - } - return uniqueId == "" -} - -// NeRunSSHClient 网元主机的SSH客户端-为创建相关连接,注意结束后 Close() -func (r *NeInfoImpl) NeRunSSHClient(neType, neId string) (*ssh.ConnSSH, error) { - neInfo := r.SelectNeInfoByNeTypeAndNeID(neType, neId) - if neInfo.NeId != neId { - logger.Errorf("NeRunSSHClient NeType:%s NeID:%s not found", neType, neId) - return nil, fmt.Errorf("neinfo not found") - } - // 取主机信息 - if neInfo.HostIDs == "" { - logger.Errorf("NeRunSSHClient NeType:%s NeID:%s hostId not found", neType, neId) - return nil, fmt.Errorf("neinfo hostId not found") - } - neInfo.Hosts = NewNeHostImpl.neHostRepository.SelectByIds(strings.Split(neInfo.HostIDs, ",")) - if len(neInfo.Hosts) <= 0 { - logger.Errorf("NeRunSSHClient Hosts %s not found", neInfo.HostIDs) - return nil, fmt.Errorf("neinfo host not found") - } - neHost := neInfo.Hosts[0] // 网元主机ssh 0:22 - if neHost.HostType != "ssh" { - logger.Errorf("NeRunSSHClient Hosts first HostType %s not ssh", neHost.HostType) - return nil, fmt.Errorf("neinfo host type not ssh") - } - - var connSSH ssh.ConnSSH - neHost.CopyTo(&connSSH) - var client *ssh.ConnSSH - var err error - if neHost.AuthMode == "2" { - client, err = connSSH.NewClientByLocalPrivate() - } else { - client, err = connSSH.NewClient() - } - if err != nil { - logger.Errorf("NeRunSSHClient NewClient err => %s", err.Error()) - return nil, fmt.Errorf("neinfo ssh client new err") - } - return client, nil -} - -// NeRunSSHCmd 网元主机的SSH客户端发送cmd命令 -func (r *NeInfoImpl) NeRunSSHCmd(neType, neId, cmd string) (string, error) { - sshClient, err := r.NeRunSSHClient(neType, neId) - if err != nil { - return "", err - } - defer sshClient.Close() - - // 执行命令 - output, err := sshClient.RunCMD(cmd) - if err != nil { - logger.Errorf("NeRunSSHCmd RunCMD %s err => %s", output, err.Error()) - return "", fmt.Errorf("neinfo ssh run cmd err") - } - return output, nil -} - -// NeRunTelnetClient 网元主机的Telnet客户端-为创建相关连接,注意结束后 Close() -// num 是网元主机telnet 1:4100 2:5200 -func (r *NeInfoImpl) NeRunTelnetClient(neType, neId string, num int) (*telnet.ConnTelnet, error) { - neInfo := r.SelectNeInfoByNeTypeAndNeID(neType, neId) - if neInfo.NeId != neId { - logger.Errorf("NeRunTelnetClient NeType:%s NeID:%s not found", neType, neId) - return nil, fmt.Errorf("neinfo not found") - } - // 取主机信息 - if neInfo.HostIDs == "" { - logger.Errorf("NeRunTelnetClient NeType:%s NeID:%s hostId not found", neType, neId) - return nil, fmt.Errorf("neinfo hostId not found") - } - neInfo.Hosts = NewNeHostImpl.neHostRepository.SelectByIds(strings.Split(neInfo.HostIDs, ",")) - if len(neInfo.Hosts) <= 0 { - logger.Errorf("NeRunTelnetClient Hosts %s not found", neInfo.HostIDs) - return nil, fmt.Errorf("neinfo host not found") - } - neHost := neInfo.Hosts[num] - - // 创建链接Telnet客户端 - var connTelnet telnet.ConnTelnet - neHost.CopyTo(&connTelnet) - telnetClient, err := connTelnet.NewClient() - if err != nil { - logger.Errorf("NeRunTelnetClient NewClient err => %s", err.Error()) - return nil, fmt.Errorf("neinfo telnet client new err") - } - return telnetClient, nil -} - -// neConfOAMData 网元OAM配置文件默认格式数据 -func (r *NeInfoImpl) neConfOAMData() map[string]any { - return map[string]any{ - "httpManageCfg": map[string]any{ - "ipType": "ipv4", - // 必改 - "ipv4": "172.60.5.2", - "ipv6": "", - "port": 33030, - "scheme": "http", - }, - "oamConfig": map[string]any{ - "enable": true, - "ipType": "ipv4", - "ipv4": "172.60.5.1", // 必改 - "ipv6": "", - "port": 33030, - "scheme": "http", - "neConfig": map[string]any{ // 必改 - "neId": "001", - "rmUid": "4400HX1XXX001", - "neName": "XXX_001", - "dn": "-", - "vendorName": "GD", - "province": "-", - "pvFlag": "PNF", - }, - }, - "snmpConfig": map[string]any{ - "enable": false, - "ipType": "ipv4", - "ipv4": "172.60.5.2", // 必改 - "ipv6": "", - "port": 4957, - }, - "kpiConfig": map[string]any{ - "enable": true, - "timer": 60, // 必改 - }, - // "pubConfigPath": "/usr/local/etc/conf/para5G.yaml", // 网元只会读一次后续会置空,建议不放 - } -} - -// neConfOAMRead 网元OAM配置文件读取 -func (r *NeInfoImpl) NeConfOAMRead(neType, neId string) (map[string]any, error) { - neTypeLower := strings.ToLower(neType) - // 网管本地路径 - omcPath := "/usr/local/etc/omc/ne_config" - if runtime.GOOS == "windows" { - omcPath = fmt.Sprintf("C:%s", omcPath) - } - localFilePath := fmt.Sprintf("%s/%s/%s/%s", omcPath, neTypeLower, neId, "oam_manager.yaml") - - // 读取文件内容 - bytes, err := os.ReadFile(localFilePath) - if err != nil { - // logger.Warnf("NeConfOAMRead ReadFile => %s", err.Error()) - // return nil, fmt.Errorf("read file error") - // 无保留文件时返回默认文件数据 - oamData := r.neConfOAMData() - r.neConfOAMWirte(neType, neId, oamData, false) - return oamData, nil - } - content := string(bytes) - - // 序列化Map - mapData, err := parse.ConvertConfigToMap("yaml", content) - if err != nil { - logger.Warnf("NeConfOAMRead ConvertConfigToMap => %s", err.Error()) - return nil, fmt.Errorf("content convert type error") - } - return mapData, nil -} - -// neConfOAMWirte 网元OAM配置文件写入 content内容 sync同步到网元端 -func (r *NeInfoImpl) neConfOAMWirte(neType, neId string, content any, sync bool) error { - neTypeLower := strings.ToLower(neType) - fileName := "oam_manager.yaml" - // 网管本地路径 - omcPath := "/usr/local/etc/omc/ne_config" - if runtime.GOOS == "windows" { - omcPath = fmt.Sprintf("C:%s", omcPath) - } - localFilePath := fmt.Sprintf("%s/%s/%s/%s", omcPath, neTypeLower, neId, fileName) - - // 写入文件 - if err := parse.ConvertConfigToFile("yaml", localFilePath, content); err != nil { - return fmt.Errorf("please check if the file exists or write permissions") - } - - // 同步到网元端 - if sync { - // 网元主机的SSH客户端 - sshClient, err := r.NeRunSSHClient(neType, neId) - if err != nil { - return err - } - defer sshClient.Close() - // 网元主机的SSH客户端进行文件传输 - sftpClient, err := sshClient.NewClientSFTP() - if err != nil { - return err - } - defer sftpClient.Close() - - // 网元端配置路径 - neFilePath := fmt.Sprintf("/usr/local/etc/%s/%s", neTypeLower, fileName) - neFileDir := filepath.ToSlash(filepath.Dir(neFilePath)) - // 修改网元文件权限 - sshClient.RunCMD(fmt.Sprintf("sudo mkdir -p %s && sudo chmod 775 %s && sudo touch %s && sudo chmod o+w %s", neFileDir, neFileDir, neFilePath, neFilePath)) - // 复制到网元进行覆盖 - if err = sftpClient.CopyFileLocalToRemote(localFilePath, neFilePath); err != nil { - return fmt.Errorf("please check if scp remote copy is allowed") - } - } - - return nil -} - -// NeConfOAMSync 网元OAM配置文件生成并同步 -func (r *NeInfoImpl) NeConfOAMSync(neInfo model.NeInfo, content map[string]any, sync bool) error { - oamData, err := r.NeConfOAMRead(neInfo.NeType, neInfo.NeId) - if oamData == nil || err != nil { - return fmt.Errorf("error read OAM file info") - } - // 网元HTTP服务 - if v, ok := oamData["httpManageCfg"]; ok { - item := v.(map[string]any) - item["port"] = neInfo.Port - if strings.Contains(neInfo.IP, ":") { - item["ipType"] = "ipv6" - item["ipv6"] = neInfo.IP - } - if strings.Contains(neInfo.IP, ".") { - item["ipType"] = "ipv4" - item["ipv4"] = neInfo.IP - } - - oamData["httpManageCfg"] = item - } - // 对网管HTTP配置 - if v, ok := oamData["oamConfig"]; ok { - item := v.(map[string]any) - item["neConfig"] = map[string]string{ - "neId": neInfo.NeId, - "rmUid": neInfo.RmUID, - "neName": neInfo.NeName, - "dn": neInfo.Dn, - "vendorName": neInfo.VendorName, - "province": neInfo.Province, - "pvFlag": neInfo.PvFlag, - } - - if omcIP, ok := r.Para5GData["OMC_IP"]; ok && omcIP != "" { - if strings.Contains(omcIP, ":") { - item["ipType"] = "ipv6" - item["ipv6"] = omcIP - } - if strings.Contains(omcIP, ".") { - item["ipType"] = "ipv4" - item["ipv4"] = omcIP - } - } - - if oamEnable, ok := content["oamEnable"]; ok && oamEnable != nil { - item["enable"] = parse.Boolean(oamEnable) - } - if oamPort, ok := content["oamPort"]; ok && oamPort != nil { - item["port"] = parse.Number(oamPort) - } - oamData["oamConfig"] = item - } - // 对网管SNMP配置 - if v, ok := oamData["snmpConfig"]; ok { - item := v.(map[string]any) - if strings.Contains(neInfo.IP, ":") { - item["ipType"] = "ipv6" - item["ipv6"] = neInfo.IP - } - if strings.Contains(neInfo.IP, ".") { - item["ipType"] = "ipv4" - item["ipv4"] = neInfo.IP - } - - if snmpEnable, ok := content["snmpEnable"]; ok && snmpEnable != nil { - item["enable"] = parse.Boolean(snmpEnable) - } - if snmpPort, ok := content["snmpPort"]; ok && snmpPort != nil { - item["port"] = parse.Number(snmpPort) - } - oamData["snmpConfig"] = item - } - // 对网管KPI上报配置 - if v, ok := oamData["kpiConfig"]; ok { - item := v.(map[string]any) - if neInfo.NeType == "UPF" { - item["timer"] = 5 - } - - if kpiEnable, ok := content["kpiEnable"]; ok && kpiEnable != nil { - item["enable"] = parse.Boolean(kpiEnable) - } - if kpiTimer, ok := content["kpiTimer"]; ok && kpiTimer != nil { - item["timer"] = parse.Number(kpiTimer) - } - oamData["kpiConfig"] = item - } - if err := NewNeInfoImpl.neConfOAMWirte(neInfo.NeType, neInfo.NeId, oamData, sync); err != nil { - return fmt.Errorf("error wirte OAM file info") - } - return nil -} - -// NeConfPara5GRead 网元公共配置文件读取 -func (r *NeInfoImpl) NeConfPara5GRead() (map[string]any, error) { - // 网管本地路径 - omcFilePath := "/usr/local/etc/omc/para5G.yaml" - if runtime.GOOS == "windows" { - omcFilePath = fmt.Sprintf("C:%s", omcFilePath) - } - // 读取文件内容 - bytes, err := os.ReadFile(omcFilePath) - if err != nil { - logger.Warnf("NeConfPara5GRead ReadFile => %s", err.Error()) - return nil, fmt.Errorf("read file error") - } - content := string(bytes) - - // 序列化Map - mapData, err := parse.ConvertConfigToMap("yaml", content) - if err != nil { - logger.Warnf("NeConfPara5GRead ConvertConfigToMap => %s", err.Error()) - return nil, fmt.Errorf("content convert type error") - } - return mapData, nil -} - -// NeConfPara5GWirte 网元公共配置文件写入 content内容 syncNE同步到网元端NeType@NeId -func (r *NeInfoImpl) NeConfPara5GWirte(content map[string]any, syncNE []string) error { - // 网管本地路径 - omcFilePath := "/usr/local/etc/omc/para5G.yaml" - if runtime.GOOS == "windows" { - omcFilePath = fmt.Sprintf("C:%s", omcFilePath) - } - - if err := parse.ConvertConfigToFile("yaml", omcFilePath, content); err != nil { - return fmt.Errorf("please check if the file exists or write permissions") - } - - // 同步到网元端 - if len(syncNE) > 0 { - errMsg := []string{} - for _, neTI := range syncNE { - ti := strings.SplitN(neTI, "@", 2) - // 网元主机的SSH客户端 - sshClient, err := r.NeRunSSHClient(ti[0], ti[1]) - if err != nil { - errMsg = append(errMsg, fmt.Sprintf("%s : %s", ti, err.Error())) - continue - } - defer sshClient.Close() - // 网元主机的SSH客户端进行文件传输 - sftpClient, err := sshClient.NewClientSFTP() - if err != nil { - errMsg = append(errMsg, fmt.Sprintf("%s : %s", ti, err.Error())) - continue - } - defer sftpClient.Close() - - // 网元端配置路径 - neFilePath := "/usr/local/etc/conf/para5G.yaml" - neFileDir := filepath.ToSlash(filepath.Dir(neFilePath)) - // 修改网元文件权限 - sshClient.RunCMD(fmt.Sprintf("sudo mkdir -p %s && sudo chmod 775 %s && sudo touch %s && sudo chmod o+rw %s", neFileDir, neFileDir, neFilePath, neFilePath)) - // 复制到网元进行覆盖 - if err = sftpClient.CopyFileLocalToRemote(omcFilePath, neFilePath); err != nil { - errMsg = append(errMsg, fmt.Sprintf("%s : please check if scp remote copy is allowed", ti)) - continue - } - } - if len(errMsg) > 0 { - return fmt.Errorf(strings.Join(errMsg, "\r\n")) - } - } - - // 转换一份数据到全局 - r.Para5GData = r.neConfPara5GDataConvert(content) - return nil -} - -// NeConfPara5GConvert 网元公共配置数据转化 content网元公共配置文件读取内容 -func (r *NeInfoImpl) neConfPara5GDataConvert(content map[string]any) map[string]string { - defer func() { - if err := recover(); err != nil { - logger.Errorf("NeConfPara5GDataConvert panic: %v", err) - // 文件异常就删除配置 - omcFilePath := "/usr/local/etc/omc/para5G.yaml" - if runtime.GOOS == "windows" { - omcFilePath = fmt.Sprintf("C:%s", omcFilePath) - } - os.Remove(omcFilePath) - } - }() - - basic := content["basic"].(map[string]any) - external := content["external"].(map[string]any) - sbi := content["sbi"].(map[string]any) - - mcc := "460" - mnc := "01" - mncDomain := "001" - if plmnId, plmnIdOk := basic["plmnId"].(map[string]any); plmnIdOk { - mcc = plmnId["mcc"].(string) - mnc = plmnId["mnc"].(string) - // If a user input two digit MNC, add a leading zero - if len(mnc) == 2 { - mncDomain = fmt.Sprintf("0%s", mnc) - } else { - mncDomain = mnc - } - } - - sst := "1" - sd := "000001" - if plmnId, plmnIdOk := basic["snssai"].(map[string]any); plmnIdOk { - sst = plmnId["sst"].(string) - sd = plmnId["sd"].(string) - } - - n3IPAmdMask := external["upfn3_ip"].(string) - n3Arr := strings.SplitN(n3IPAmdMask, "/", 2) - n3IP := n3Arr[0] - n3Mask := "255.255.255.0" - if len(n3Arr) > 1 { - n3Mask = parse.ConvertIPMask(parse.Number(n3Arr[1])) - } - - n6IPAmdMask := external["upfn6_ip"].(string) - n6Arr := strings.SplitN(n6IPAmdMask, "/", 2) - n6IP := n6Arr[0] - n6Mask := "255.255.255.0" - if len(n6Arr) > 1 { - n6Mask = parse.ConvertIPMask(parse.Number(n6Arr[1])) - } - - ueIPAmdMask := external["ue_pool"].(string) - ueArr := strings.SplitN(ueIPAmdMask, "/", 2) - ueIP := ueArr[0] - ueCicr := "24" - ueMask := "255.255.255.0" - if len(ueArr) > 1 { - ueCicr = ueArr[1] - ueMask = parse.ConvertIPMask(parse.Number(ueArr[1])) - } - - return map[string]string{ - // basic - "TAC": basic["tac"].(string), - "MCC": mcc, - "MNC": mnc, - "MNC_DOMAIN": mncDomain, - "SST": sst, - "SD": sd, - "DNN_DATA": basic["dnn_data"].(string), - "DNN_IMS": basic["dnn_ims"].(string), - - // external - "N2_IP": external["amfn2_ip"].(string), - "UE_POOL": external["ue_pool"].(string), - "UE_IP": ueIP, - "UE_MASK": ueMask, - "UE_CIDR": ueCicr, - "UPF_TYPE": external["upf_type"].(string), // StandardUPF LightUPF - "N3_IP": n3IP, - "N3_MASK": n3Mask, - "N3_GW": external["upfn3_gw"].(string), - "N3_PCI": external["upfn3_pci"].(string), - "N3_MAC": external["upfn3_mac"].(string), - "N6_IP": n6IP, - "N6_MASK": n6Mask, - "N6_GW": external["upfn6_gw"].(string), - "N6_PCI": external["upfn6_pci"].(string), - "N6_MAC": external["upfn6_mac"].(string), - - "SIP_IP": external["ims_sip_ip"].(string), - - "S1_MMEIP": external["mmes1_ip"].(string), - "S11_MMEIP": external["mmes11_ip"].(string), - "S10_MMEIP": external["mmes10_ip"].(string), - - // sbi - "OMC_IP": sbi["omc_ip"].(string), - "IMS_IP": sbi["ims_ip"].(string), - "AMF_IP": sbi["amf_ip"].(string), - "AUSF_IP": sbi["ausf_ip"].(string), - "UDM_IP": sbi["udm_ip"].(string), - "SMF_IP": sbi["smf_ip"].(string), - "PCF_IP": sbi["pcf_ip"].(string), - "NSSF_IP": sbi["nssf_ip"].(string), - "NRF_IP": sbi["nrf_ip"].(string), - "UPF_IP": sbi["upf_ip"].(string), - "LMF_IP": sbi["lmf_ip"].(string), - "NEF_IP": sbi["nef_ip"].(string), - "MME_IP": sbi["mme_ip"].(string), - "N3IWF_IP": sbi["n3iwf_ip"].(string), - - "DB_IP": sbi["db_ip"].(string), - } -} diff --git a/src/modules/network_element/service/ne_license.go b/src/modules/network_element/service/ne_license.go index ace554d1..fa84b155 100644 --- a/src/modules/network_element/service/ne_license.go +++ b/src/modules/network_element/service/ne_license.go @@ -1,34 +1,190 @@ package service -import "be.ems/src/modules/network_element/model" +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + "time" -// INeLicense 网元授权激活信息 服务层接口 -type INeLicense interface { - // SelectPage 根据条件分页查询字典类型 - SelectPage(query map[string]any) map[string]any + "be.ems/src/framework/utils/file" + "be.ems/src/modules/network_element/model" + "be.ems/src/modules/network_element/repository" +) - // SelectList 根据实体查询 - SelectList(neLicense model.NeLicense) []model.NeLicense - - // SelectById 通过ID查询 - SelectById(id string) model.NeLicense - - // Insert 新增信息 - Insert(neLicense model.NeLicense) string - - // Update 修改信息 - Update(neLicense model.NeLicense) int64 - - // DeleteByIds 批量删除信息 - DeleteByIds(ids []string) (int64, error) - - // SelectByNeTypeAndNeID 通过ne_type和ne_id查询信息 - SelectByNeTypeAndNeID(neType, neId string) model.NeLicense - - // ReadLicenseInfo 读取授权文件信息 - // 返回激活申请码, 激活文件 - ReadLicenseInfo(neLicense model.NeLicense) (string, string) - - // UploadLicense 授权文件上传到网元主机 - UploadLicense(neLicense model.NeLicense) error +// 实例化服务层 NeLicense 结构体 +var NewNeLicense = &NeLicense{ + neLicenseRepository: repository.NewNeLicense, +} + +// NeLicense 网元授权激活信息 服务层处理 +type NeLicense struct { + neLicenseRepository *repository.NeLicense // 网元授权激活信息表 +} + +// SelectNeHostPage 分页查询列表数据 +func (r *NeLicense) SelectPage(query map[string]any) map[string]any { + return r.neLicenseRepository.SelectPage(query) +} + +// SelectConfigList 查询列表 +func (r *NeLicense) SelectList(neLicense model.NeLicense) []model.NeLicense { + return r.neLicenseRepository.SelectList(neLicense) +} + +// SelectByIds 通过ID查询 +func (r *NeLicense) SelectById(id string) model.NeLicense { + if id == "" { + return model.NeLicense{} + } + neLicenses := r.neLicenseRepository.SelectByIds([]string{id}) + if len(neLicenses) > 0 { + return neLicenses[0] + } + return model.NeLicense{} +} + +// Insert 新增信息 +func (r *NeLicense) Insert(neLicense model.NeLicense) string { + return r.neLicenseRepository.Insert(neLicense) +} + +// Update 修改信息 +func (r *NeLicense) Update(neLicense model.NeLicense) int64 { + return r.neLicenseRepository.Update(neLicense) +} + +// DeleteByIds 批量删除信息 +func (r *NeLicense) DeleteByIds(ids []string) (int64, error) { + // 检查是否存在 + rowIds := r.neLicenseRepository.SelectByIds(ids) + if len(rowIds) <= 0 { + return 0, fmt.Errorf("neLicense.noData") + } + + if len(rowIds) == len(ids) { + rows := r.neLicenseRepository.DeleteByIds(ids) + return rows, nil + } + // 删除信息失败! + return 0, fmt.Errorf("delete fail") +} + +// SelectByTypeAndID 通过网元类型和网元ID查询 +func (r *NeLicense) SelectByTypeAndID(neType, neId string) model.NeLicense { + neLicenses := r.neLicenseRepository.SelectList(model.NeLicense{ + NeType: neType, + NeId: neId, + }) + if len(neLicenses) > 0 { + return neLicenses[0] + } + return model.NeLicense{} +} + +// SelectByNeTypeAndNeID 通过ne_type和ne_id查询信息 +func (r *NeLicense) SelectByNeTypeAndNeID(neType, neId string) model.NeLicense { + neLicenses := r.neLicenseRepository.SelectList(model.NeLicense{ + NeType: neType, + NeId: neId, + }) + if len(neLicenses) > 0 { + return neLicenses[0] + } + return model.NeLicense{} +} + +// ReadLicenseInfo 读取授权文件信息 +// 返回激活申请码, 激活文件 +func (r *NeLicense) ReadLicenseInfo(neLicense model.NeLicense) (string, string) { + neTypeLower := strings.ToLower(neLicense.NeType) + // 网管本地路径 + omcPath := "/usr/local/etc/omc/ne_license" + if runtime.GOOS == "windows" { + omcPath = fmt.Sprintf("C:%s", omcPath) + } + omcPath = fmt.Sprintf("%s/%s/%s", omcPath, neTypeLower, neLicense.NeId) + // 网元端授权文件路径 + nePath := fmt.Sprintf("/usr/local/etc/%s/license", neTypeLower) + + // 网元主机的SSH客户端 + sshClient, err := NewNeInfo.NeRunSSHClient(neLicense.NeType, neLicense.NeId) + if err != nil { + return "", "" + } + defer sshClient.Close() + // 网元主机的SSH客户端进行文件传输 + sftpClient, err := sshClient.NewClientSFTP() + if err != nil { + return "", "" + } + defer sftpClient.Close() + + // 复制授权申请码到本地 + if err = sftpClient.CopyFileRemoteToLocal(nePath+"/Activation_request_code.txt", omcPath+"/Activation_request_code.txt"); err != nil { + return "", "" + } + // 读取文件内容 + bytes, err := os.ReadFile(omcPath + "/Activation_request_code.txt") + if err != nil { + return "", "" + } + + // 复制激活文件到本地 + licensePath := "" + if err = sftpClient.CopyFileRemoteToLocal(nePath+"/system.ini", omcPath+"/system.ini"); err == nil { + licensePath = omcPath + "/system.ini" + } + return strings.TrimSpace(string(bytes)), licensePath +} + +// UploadLicense 授权文件上传到网元主机 +func (r *NeLicense) UploadLicense(neLicense model.NeLicense) error { + // 检查文件是否存在 + omcLicensePath := file.ParseUploadFilePath(neLicense.LicensePath) + if _, err := os.Stat(omcLicensePath); err != nil { + return fmt.Errorf("file read failure") + } + + // 网元主机的SSH客户端 + sshClient, err := NewNeInfo.NeRunSSHClient(neLicense.NeType, neLicense.NeId) + if err != nil { + return err + } + defer sshClient.Close() + // 网元主机的SSH客户端进行文件传输 + sftpClient, err := sshClient.NewClientSFTP() + if err != nil { + return err + } + defer sftpClient.Close() + + // 网元端授权文件路径 + neTypeLower := strings.ToLower(neLicense.NeType) + neLicensePath := fmt.Sprintf("/usr/local/etc/%s/license/system.ini", neTypeLower) + neLicenseDir := filepath.ToSlash(filepath.Dir(neLicensePath)) + // 修改网元文件权限 + sshClient.RunCMD(fmt.Sprintf("sudo mkdir -p %s && sudo chmod 775 %s && sudo touch %s && sudo chmod o+rw %s", neLicenseDir, neLicenseDir, neLicensePath, neLicensePath)) + + // 尝试备份授权文件 + neLicensePathBack := fmt.Sprintf("%s/system_%s.ini", neLicensePath, time.Now().Format("20060102_150405")) + sshClient.RunCMD(fmt.Sprintf("sudo cp -rf %s/system.ini %s", neLicensePath, neLicensePathBack)) + + // 上传授权文件去覆盖 + if err := sftpClient.CopyFileLocalToRemote(omcLicensePath, neLicensePath); err != nil { + return fmt.Errorf("please check if scp remote copy is allowed") + } + + // 重启服务 + if neLicense.Reload { + cmdStr := fmt.Sprintf("sudo service %s restart", neTypeLower) + if neTypeLower == "ims" { + cmdStr = "ims-stop || true && ims-start" + } else if neTypeLower == "omc" { + cmdStr = "sudo systemctl restart restagent" + } + sshClient.RunCMD(cmdStr) + } + return nil } diff --git a/src/modules/network_element/service/ne_license.impl.go b/src/modules/network_element/service/ne_license.impl.go deleted file mode 100644 index 9d11804d..00000000 --- a/src/modules/network_element/service/ne_license.impl.go +++ /dev/null @@ -1,191 +0,0 @@ -package service - -import ( - "fmt" - "os" - "path/filepath" - "runtime" - "strings" - "time" - - "be.ems/src/framework/utils/file" - "be.ems/src/modules/network_element/model" - "be.ems/src/modules/network_element/repository" -) - -// 实例化服务层 NeLicenseImpl 结构体 -var NewNeLicenseImpl = &NeLicenseImpl{ - neLicenseRepository: repository.NewNeLicenseImpl, -} - -// NeLicenseImpl 网元授权激活信息 服务层处理 -type NeLicenseImpl struct { - // 网元授权激活信息表 - neLicenseRepository repository.INeLicense -} - -// SelectNeHostPage 分页查询列表数据 -func (r *NeLicenseImpl) SelectPage(query map[string]any) map[string]any { - return r.neLicenseRepository.SelectPage(query) -} - -// SelectConfigList 查询列表 -func (r *NeLicenseImpl) SelectList(neLicense model.NeLicense) []model.NeLicense { - return r.neLicenseRepository.SelectList(neLicense) -} - -// SelectByIds 通过ID查询 -func (r *NeLicenseImpl) SelectById(id string) model.NeLicense { - if id == "" { - return model.NeLicense{} - } - neLicenses := r.neLicenseRepository.SelectByIds([]string{id}) - if len(neLicenses) > 0 { - return neLicenses[0] - } - return model.NeLicense{} -} - -// Insert 新增信息 -func (r *NeLicenseImpl) Insert(neLicense model.NeLicense) string { - return r.neLicenseRepository.Insert(neLicense) -} - -// Update 修改信息 -func (r *NeLicenseImpl) Update(neLicense model.NeLicense) int64 { - return r.neLicenseRepository.Update(neLicense) -} - -// DeleteByIds 批量删除信息 -func (r *NeLicenseImpl) DeleteByIds(ids []string) (int64, error) { - // 检查是否存在 - rowIds := r.neLicenseRepository.SelectByIds(ids) - if len(rowIds) <= 0 { - return 0, fmt.Errorf("neLicense.noData") - } - - if len(rowIds) == len(ids) { - rows := r.neLicenseRepository.DeleteByIds(ids) - return rows, nil - } - // 删除信息失败! - return 0, fmt.Errorf("delete fail") -} - -// SelectByTypeAndID 通过网元类型和网元ID查询 -func (r *NeLicenseImpl) SelectByTypeAndID(neType, neId string) model.NeLicense { - neLicenses := r.neLicenseRepository.SelectList(model.NeLicense{ - NeType: neType, - NeId: neId, - }) - if len(neLicenses) > 0 { - return neLicenses[0] - } - return model.NeLicense{} -} - -// SelectByNeTypeAndNeID 通过ne_type和ne_id查询信息 -func (r *NeLicenseImpl) SelectByNeTypeAndNeID(neType, neId string) model.NeLicense { - neLicenses := r.neLicenseRepository.SelectList(model.NeLicense{ - NeType: neType, - NeId: neId, - }) - if len(neLicenses) > 0 { - return neLicenses[0] - } - return model.NeLicense{} -} - -// ReadLicenseInfo 读取授权文件信息 -// 返回激活申请码, 激活文件 -func (r *NeLicenseImpl) ReadLicenseInfo(neLicense model.NeLicense) (string, string) { - neTypeLower := strings.ToLower(neLicense.NeType) - // 网管本地路径 - omcPath := "/usr/local/etc/omc/ne_license" - if runtime.GOOS == "windows" { - omcPath = fmt.Sprintf("C:%s", omcPath) - } - omcPath = fmt.Sprintf("%s/%s/%s", omcPath, neTypeLower, neLicense.NeId) - // 网元端授权文件路径 - nePath := fmt.Sprintf("/usr/local/etc/%s/license", neTypeLower) - - // 网元主机的SSH客户端 - sshClient, err := NewNeInfoImpl.NeRunSSHClient(neLicense.NeType, neLicense.NeId) - if err != nil { - return "", "" - } - defer sshClient.Close() - // 网元主机的SSH客户端进行文件传输 - sftpClient, err := sshClient.NewClientSFTP() - if err != nil { - return "", "" - } - defer sftpClient.Close() - - // 复制授权申请码到本地 - if err = sftpClient.CopyFileRemoteToLocal(nePath+"/Activation_request_code.txt", omcPath+"/Activation_request_code.txt"); err != nil { - return "", "" - } - // 读取文件内容 - bytes, err := os.ReadFile(omcPath + "/Activation_request_code.txt") - if err != nil { - return "", "" - } - - // 复制激活文件到本地 - licensePath := "" - if err = sftpClient.CopyFileRemoteToLocal(nePath+"/system.ini", omcPath+"/system.ini"); err == nil { - licensePath = omcPath + "/system.ini" - } - return strings.TrimSpace(string(bytes)), licensePath -} - -// UploadLicense 授权文件上传到网元主机 -func (r *NeLicenseImpl) UploadLicense(neLicense model.NeLicense) error { - // 检查文件是否存在 - omcLicensePath := file.ParseUploadFilePath(neLicense.LicensePath) - if _, err := os.Stat(omcLicensePath); err != nil { - return fmt.Errorf("file read failure") - } - - // 网元主机的SSH客户端 - sshClient, err := NewNeInfoImpl.NeRunSSHClient(neLicense.NeType, neLicense.NeId) - if err != nil { - return err - } - defer sshClient.Close() - // 网元主机的SSH客户端进行文件传输 - sftpClient, err := sshClient.NewClientSFTP() - if err != nil { - return err - } - defer sftpClient.Close() - - // 网元端授权文件路径 - neTypeLower := strings.ToLower(neLicense.NeType) - neLicensePath := fmt.Sprintf("/usr/local/etc/%s/license/system.ini", neTypeLower) - neLicenseDir := filepath.ToSlash(filepath.Dir(neLicensePath)) - // 修改网元文件权限 - sshClient.RunCMD(fmt.Sprintf("sudo mkdir -p %s && sudo chmod 775 %s && sudo touch %s && sudo chmod o+rw %s", neLicenseDir, neLicenseDir, neLicensePath, neLicensePath)) - - // 尝试备份授权文件 - neLicensePathBack := fmt.Sprintf("%s/system_%s.ini", neLicensePath, time.Now().Format("20060102_150405")) - sshClient.RunCMD(fmt.Sprintf("sudo cp -rf %s/system.ini %s", neLicensePath, neLicensePathBack)) - - // 上传授权文件去覆盖 - if err := sftpClient.CopyFileLocalToRemote(omcLicensePath, neLicensePath); err != nil { - return fmt.Errorf("please check if scp remote copy is allowed") - } - - // 重启服务 - if neLicense.Reload { - cmdStr := fmt.Sprintf("sudo service %s restart", neTypeLower) - if neTypeLower == "ims" { - cmdStr = "ims-stop || true && ims-start" - } else if neTypeLower == "omc" { - cmdStr = "sudo systemctl restart restagent" - } - sshClient.RunCMD(cmdStr) - } - return nil -} diff --git a/src/modules/network_element/service/ne_software.go b/src/modules/network_element/service/ne_software.go index 37e9ad4a..c4910f05 100644 --- a/src/modules/network_element/service/ne_software.go +++ b/src/modules/network_element/service/ne_software.go @@ -1,30 +1,142 @@ package service -import "be.ems/src/modules/network_element/model" +import ( + "fmt" + "os" -// INeSoftware 网元软件包信息 服务层接口 -type INeSoftware interface { - // SelectPage 根据条件分页查询字典类型 - SelectPage(query map[string]any) map[string]any + "be.ems/src/framework/utils/file" + "be.ems/src/modules/network_element/model" + "be.ems/src/modules/network_element/repository" +) - // SelectList 根据实体查询 - SelectList(neSoftware model.NeSoftware) []model.NeSoftware - - // SelectById 通过ID查询 - SelectById(id string) model.NeSoftware - - // Insert 新增信息 - Insert(neSoftware model.NeSoftware) string - - // Update 修改信息 - Update(neSoftware model.NeSoftware) int64 - - // DeleteByIds 批量删除信息 - DeleteByIds(ids []string) (int64, error) - - // CheckUniqueTypeAndNameAndVersion 校验网元类型和文件名版本是否唯一 - CheckUniqueTypeAndNameAndVersion(neType, name, version, id string) bool - - // UpdateVersions 更新软件包对应网元的新版本 - UpdateVersions(neSoftware model.NeSoftware, neVersion model.NeVersion) int64 +// 实例化服务层 NeSoftware 结构体 +var NewNeSoftware = &NeSoftware{ + neSoftwareRepository: repository.NewNeSoftware, +} + +// NeSoftware 网元软件包信息 服务层处理 +type NeSoftware struct { + neSoftwareRepository *repository.NeSoftware // 网元软件包信息 +} + +// SelectNeHostPage 分页查询列表数据 +func (r *NeSoftware) SelectPage(query map[string]any) map[string]any { + return r.neSoftwareRepository.SelectPage(query) +} + +// SelectConfigList 查询列表 +func (r *NeSoftware) SelectList(neSoftware model.NeSoftware) []model.NeSoftware { + return r.neSoftwareRepository.SelectList(neSoftware) +} + +// SelectByIds 通过ID查询 +func (r *NeSoftware) SelectById(id string) model.NeSoftware { + if id == "" { + return model.NeSoftware{} + } + neHosts := r.neSoftwareRepository.SelectByIds([]string{id}) + if len(neHosts) > 0 { + return neHosts[0] + } + return model.NeSoftware{} +} + +// Insert 新增信息 +func (r *NeSoftware) Insert(neSoftware model.NeSoftware) string { + inserId := r.neSoftwareRepository.Insert(neSoftware) + if inserId != "" { + // 更新同类型的新包版本 + neVersions := NewNeVersion.SelectList(model.NeVersion{NeType: neSoftware.NeType}, false) + if len(neVersions) > 0 { + for _, neVersion := range neVersions { + neVersion.NewName = neSoftware.Name + neVersion.NewVersion = neSoftware.Version + neVersion.NewPath = neSoftware.Path + neVersion.Status = "3" + neVersion.UpdateBy = neSoftware.CreateBy + NewNeVersion.Update(neVersion) + } + } + } + return inserId +} + +// Update 修改信息 +func (r *NeSoftware) Update(neSoftware model.NeSoftware) int64 { + rows := r.neSoftwareRepository.Update(neSoftware) + if rows > 0 { + // 更新同类型的新包版本 + neVersions := NewNeVersion.SelectList(model.NeVersion{ + NeType: neSoftware.NeType, + Status: "3", + }, false) + if len(neVersions) > 0 { + for _, neVersion := range neVersions { + neVersion.NewName = neSoftware.Name + neVersion.NewVersion = neSoftware.Version + neVersion.NewPath = neSoftware.Path + neVersion.Status = "3" + neVersion.UpdateBy = neSoftware.UpdateBy + NewNeVersion.Update(neVersion) + } + } + } + return rows +} + +// DeleteByIds 批量删除信息 +func (r *NeSoftware) DeleteByIds(ids []string) (int64, error) { + // 检查是否存在 + rows := r.neSoftwareRepository.SelectByIds(ids) + if len(rows) <= 0 { + return 0, fmt.Errorf("neSoftware.noData") + } + + if len(rows) == len(ids) { + // 遍历软件包列表进行文件删除 + for _, row := range rows { + // 检查文件是否存在 + filePath := file.ParseUploadFilePath(row.Path) + if _, err := os.Stat(filePath); err != nil { + continue + } + os.Remove(filePath) + } + rows := r.neSoftwareRepository.DeleteByIds(ids) + return rows, nil + } + + // 删除信息失败! + return 0, fmt.Errorf("delete fail") +} + +// CheckUniqueTypeAndNameAndVersion 校验网元类型和文件名版本是否唯一 +func (r *NeSoftware) CheckUniqueTypeAndNameAndVersion(neType, name, version, id string) bool { + uniqueId := r.neSoftwareRepository.CheckUniqueTypeAndNameAndVersion(model.NeSoftware{ + NeType: neType, + Name: name, + Version: version, + }) + if uniqueId == id { + return true + } + return uniqueId == "" +} + +// UpdateVersions 更新软件包对应网元的新版本 +func (r *NeSoftware) UpdateVersions(neSoftware model.NeSoftware, neVersion model.NeVersion) int64 { + var rows int64 = 0 + // 更新同类型的新包版本 + neVersions := NewNeVersion.SelectList(neVersion, false) + if len(neVersions) > 0 { + for _, v := range neVersions { + v.NewName = neSoftware.Name + v.NewVersion = neSoftware.Version + v.NewPath = neSoftware.Path + v.Status = "3" + v.UpdateBy = neVersion.UpdateBy + rows += NewNeVersion.Update(v) + } + } + return rows } diff --git a/src/modules/network_element/service/ne_software.impl.go b/src/modules/network_element/service/ne_software.impl.go deleted file mode 100644 index 8ac52094..00000000 --- a/src/modules/network_element/service/ne_software.impl.go +++ /dev/null @@ -1,143 +0,0 @@ -package service - -import ( - "fmt" - "os" - - "be.ems/src/framework/utils/file" - "be.ems/src/modules/network_element/model" - "be.ems/src/modules/network_element/repository" -) - -// 实例化服务层 NeSoftwareImpl 结构体 -var NewNeSoftwareImpl = &NeSoftwareImpl{ - neSoftwareRepository: repository.NewNeSoftwareImpl, -} - -// NeSoftwareImpl 网元软件包信息 服务层处理 -type NeSoftwareImpl struct { - // 网元软件包信息 - neSoftwareRepository repository.INeSoftware -} - -// SelectNeHostPage 分页查询列表数据 -func (r *NeSoftwareImpl) SelectPage(query map[string]any) map[string]any { - return r.neSoftwareRepository.SelectPage(query) -} - -// SelectConfigList 查询列表 -func (r *NeSoftwareImpl) SelectList(neSoftware model.NeSoftware) []model.NeSoftware { - return r.neSoftwareRepository.SelectList(neSoftware) -} - -// SelectByIds 通过ID查询 -func (r *NeSoftwareImpl) SelectById(id string) model.NeSoftware { - if id == "" { - return model.NeSoftware{} - } - neHosts := r.neSoftwareRepository.SelectByIds([]string{id}) - if len(neHosts) > 0 { - return neHosts[0] - } - return model.NeSoftware{} -} - -// Insert 新增信息 -func (r *NeSoftwareImpl) Insert(neSoftware model.NeSoftware) string { - inserId := r.neSoftwareRepository.Insert(neSoftware) - if inserId != "" { - // 更新同类型的新包版本 - neVersions := NewNeVersionImpl.SelectList(model.NeVersion{NeType: neSoftware.NeType}) - if len(neVersions) > 0 { - for _, neVersion := range neVersions { - neVersion.NewName = neSoftware.Name - neVersion.NewVersion = neSoftware.Version - neVersion.NewPath = neSoftware.Path - neVersion.Status = "3" - neVersion.UpdateBy = neSoftware.CreateBy - NewNeVersionImpl.Update(neVersion) - } - } - } - return inserId -} - -// Update 修改信息 -func (r *NeSoftwareImpl) Update(neSoftware model.NeSoftware) int64 { - rows := r.neSoftwareRepository.Update(neSoftware) - if rows > 0 { - // 更新同类型的新包版本 - neVersions := NewNeVersionImpl.SelectList(model.NeVersion{ - NeType: neSoftware.NeType, - Status: "3", - }) - if len(neVersions) > 0 { - for _, neVersion := range neVersions { - neVersion.NewName = neSoftware.Name - neVersion.NewVersion = neSoftware.Version - neVersion.NewPath = neSoftware.Path - neVersion.Status = "3" - neVersion.UpdateBy = neSoftware.UpdateBy - NewNeVersionImpl.Update(neVersion) - } - } - } - return rows -} - -// DeleteByIds 批量删除信息 -func (r *NeSoftwareImpl) DeleteByIds(ids []string) (int64, error) { - // 检查是否存在 - rows := r.neSoftwareRepository.SelectByIds(ids) - if len(rows) <= 0 { - return 0, fmt.Errorf("neSoftware.noData") - } - - if len(rows) == len(ids) { - // 遍历软件包列表进行文件删除 - for _, row := range rows { - // 检查文件是否存在 - filePath := file.ParseUploadFilePath(row.Path) - if _, err := os.Stat(filePath); err != nil { - continue - } - os.Remove(filePath) - } - rows := r.neSoftwareRepository.DeleteByIds(ids) - return rows, nil - } - - // 删除信息失败! - return 0, fmt.Errorf("delete fail") -} - -// CheckUniqueTypeAndNameAndVersion 校验网元类型和文件名版本是否唯一 -func (r *NeSoftwareImpl) CheckUniqueTypeAndNameAndVersion(neType, name, version, id string) bool { - uniqueId := r.neSoftwareRepository.CheckUniqueTypeAndNameAndVersion(model.NeSoftware{ - NeType: neType, - Name: name, - Version: version, - }) - if uniqueId == id { - return true - } - return uniqueId == "" -} - -// UpdateVersions 更新软件包对应网元的新版本 -func (r *NeSoftwareImpl) UpdateVersions(neSoftware model.NeSoftware, neVersion model.NeVersion) int64 { - var rows int64 = 0 - // 更新同类型的新包版本 - neVersions := NewNeVersionImpl.SelectList(neVersion) - if len(neVersions) > 0 { - for _, v := range neVersions { - v.NewName = neSoftware.Name - v.NewVersion = neSoftware.Version - v.NewPath = neSoftware.Path - v.Status = "3" - v.UpdateBy = neVersion.UpdateBy - rows += NewNeVersionImpl.Update(v) - } - } - return rows -} diff --git a/src/modules/network_element/service/ne_version.go b/src/modules/network_element/service/ne_version.go index f92a81be..8fecd122 100644 --- a/src/modules/network_element/service/ne_version.go +++ b/src/modules/network_element/service/ne_version.go @@ -1,32 +1,744 @@ package service -import "be.ems/src/modules/network_element/model" +import ( + "fmt" + "os" + "path/filepath" + "strings" + "time" -// INeVersion 网元版本信息 服务层接口 -type INeVersion interface { - // SelectPage 根据条件分页查询字典类型 - SelectPage(query map[string]any) map[string]any + "be.ems/src/framework/utils/file" + "be.ems/src/framework/utils/ssh" + neFetchlink "be.ems/src/modules/network_element/fetch_link" + "be.ems/src/modules/network_element/model" + "be.ems/src/modules/network_element/repository" +) - // SelectList 根据实体查询 - SelectList(neVersion model.NeVersion) []model.NeVersion - - // SelectById 通过ID查询 - SelectById(id string) model.NeVersion - - // Insert 新增信息 - Insert(neVersion model.NeVersion) string - - // Update 修改信息 - Update(neVersion model.NeVersion) int64 - - // DeleteByIds 批量删除信息 - DeleteByIds(ids []string) (int64, error) - - // SelectByNeTypeAndNeID 通过网元类型和网元ID查询 - SelectByNeTypeAndNeID(neType, neId string) model.NeVersion - - // Operate 操作版本上传到网元主机执行命令 - // - // action 安装行为:install upgrade rollback - Operate(action string, neVersion model.NeVersion, preinput map[string]string) (string, error) +// 实例化服务层 NeVersion 结构体 +var NewNeVersion = &NeVersion{ + neVersionRepository: repository.NewNeVersion, +} + +// NeVersion 网元版本信息 服务层处理 +type NeVersion struct { + neVersionRepository *repository.NeVersion // 网元版本信息表 +} + +// SelectNeHostPage 分页查询列表数据 +func (r *NeVersion) SelectPage(query map[string]any, checkVersion bool) map[string]any { + data := r.neVersionRepository.SelectPage(query) + + // 网元直连检查更新网元服务版本 + if checkVersion { + rows := data["rows"].([]model.NeVersion) + r.checkNeVersion(&rows) + } + + return data +} + +// SelectConfigList 查询列表 +func (r *NeVersion) SelectList(neVersion model.NeVersion, checkVersion bool) []model.NeVersion { + list := r.neVersionRepository.SelectList(neVersion) + + // 网元直连检查更新网元服务版本 + if checkVersion { + r.checkNeVersion(&list) + } + + return list +} + +// checkNeVersion 网元列表检查更新网元版本 +func (r *NeVersion) checkNeVersion(arr *[]model.NeVersion) { + for i := range *arr { + item := (*arr)[i] + // 查询网元获取IP + neInfo := NewNeInfo.SelectNeInfoByNeTypeAndNeID(item.NeType, item.NeId) + if neInfo.NeId != item.NeId || neInfo.IP == "" { + continue + } + result, err := neFetchlink.NeState(neInfo) + if err != nil { + continue + } + if v, ok := result["version"]; ok && v != nil { + ver := v.(string) + if ver == item.Version { + continue + } + item.Name = "-" + item.Path = "-" + item.Version = ver + } + if item.NeType != neInfo.NeType || item.NeId != neInfo.NeId { + item.NeType = neInfo.NeType + item.NeId = neInfo.NeId + } + r.Update(item) + (*arr)[i] = item + } +} + +// SelectByIds 通过ID查询 +func (r *NeVersion) SelectById(id string) model.NeVersion { + if id == "" { + return model.NeVersion{} + } + neVersions := r.neVersionRepository.SelectByIds([]string{id}) + if len(neVersions) > 0 { + return neVersions[0] + } + return model.NeVersion{} +} + +// Insert 新增信息 +func (r *NeVersion) Insert(neVersion model.NeVersion) string { + return r.neVersionRepository.Insert(neVersion) +} + +// Update 修改信息 +func (r *NeVersion) Update(neVersion model.NeVersion) int64 { + return r.neVersionRepository.Update(neVersion) +} + +// DeleteByIds 批量删除信息 +func (r *NeVersion) DeleteByIds(ids []string) (int64, error) { + // 检查是否存在 + rowIds := r.neVersionRepository.SelectByIds(ids) + if len(rowIds) <= 0 { + return 0, fmt.Errorf("neVersion.noData") + } + + if len(rowIds) == len(ids) { + rows := r.neVersionRepository.DeleteByIds(ids) + return rows, nil + } + // 删除信息失败! + return 0, fmt.Errorf("delete fail") +} + +// SelectByNeTypeAndNeID 通过网元类型和网元ID查询 +func (r *NeVersion) SelectByNeTypeAndNeID(neType, neId string) model.NeVersion { + neVersions := r.neVersionRepository.SelectList(model.NeVersion{ + NeType: neType, + NeId: neId, + }) + if len(neVersions) > 0 { + return neVersions[0] + } + return model.NeVersion{} +} + +// Operate 操作版本上传到网元主机执行命令 +// +// action 安装行为:install upgrade rollback +func (r *NeVersion) Operate(action string, neVersion model.NeVersion, preinput map[string]string) (string, error) { + // 网元主机的SSH客户端 + sshClient, err := NewNeInfo.NeRunSSHClient(neVersion.NeType, neVersion.NeId) + if err != nil { + return "", err + } + defer sshClient.Close() + + // ========= 文件传输阶段 ========= + softwarePath := neVersion.Path + if action == "install" || action == "upgrade" { + softwarePath = neVersion.NewPath + } + if action == "rollback" { + softwarePath = neVersion.PrePath + } + neFilePaths, err := r.operateFile(sshClient, softwarePath) + if err != nil { + return "", err + } + + // ========= 安装时设置 ========= + if action == "install" { + // 网元公共配置文件 + para5GMap, err := NewNeInfo.NeConfPara5GRead() + if para5GMap == nil || err != nil { + return "", fmt.Errorf("error read para5G file info") + } + if err := NewNeInfo.NeConfPara5GWirte(para5GMap, []string{fmt.Sprintf("%s@%s", neVersion.NeType, neVersion.NeId)}); err != nil { + return "", fmt.Errorf("error wirte para5G file info") + } + } + + // ========= 命令生成阶段 ========= + okFlagStr, cmdStrArr, err := r.operateCommand(action, neVersion.NeType, neFilePaths) + if err != nil { + return "", err + } + + // ========= 执行阶段 ========= + commandLine, err := r.operateRun(sshClient, preinput, cmdStrArr, neVersion.NeType, okFlagStr) + if err != nil { + return "", err + } + + // ========= 完成阶段 ========= + if strings.LastIndex(commandLine, okFlagStr) > 5 { + if err := r.operateDome(action, neVersion); err != nil { + return "", err + } + } + return commandLine, nil +} + +// operateFile 操作版本-文件传输阶段 +func (r *NeVersion) operateFile(sshClient *ssh.ConnSSH, softwarePath string) ([]string, error) { + // 网元主机的SSH客户端进行文件传输 + sftpClient, err := sshClient.NewClientSFTP() + if err != nil { + return nil, err + } + defer sftpClient.Close() + + nePath := "/tmp" + copyFileToNeMap := map[string]string{} + + // 统一处理多个文件和单个文件的情况 + var softwarePaths []string + if strings.Contains(softwarePath, ",") { + softwarePaths = strings.Split(softwarePath, ",") + } else { + softwarePaths = []string{softwarePath} + } + + for _, path := range softwarePaths { + // 检查文件是否存在 + localFilePath := file.ParseUploadFilePath(path) + if _, err := os.Stat(localFilePath); err != nil { + return nil, fmt.Errorf("file read failure") + } + + fileName := filepath.Base(path) + neFilePath := fmt.Sprintf("%s/%s", nePath, fileName) + copyFileToNeMap[localFilePath] = neFilePath + } + + // 上传软件包到 /tmp + neFilePaths := []string{} + for k, v := range copyFileToNeMap { + if err = sftpClient.CopyFileLocalToRemote(k, v); err != nil { + return nil, fmt.Errorf("error uploading package") + } + neFilePaths = append(neFilePaths, v) + } + return neFilePaths, nil +} + +// operateCommand 操作版本-命令生成阶段 +func (r *NeVersion) operateCommand(action, neType string, neFilePaths []string) (string, []string, error) { + neTypeLower := strings.ToLower(neType) + // 命令终止结束标记 + okFlagStr := fmt.Sprintf("%s version %s successful!", neTypeLower, action) + // 安装软件包 + pkgCmdStr := fmt.Sprintf("sudo dpkg -i %s", strings.Join(neFilePaths, " ")) + fileExt := filepath.Ext(strings.ToLower(neFilePaths[0])) + if strings.HasSuffix(fileExt, "rpm") { + pkgCmdStr = fmt.Sprintf("sudo rpm -Uvh --reinstall %s", strings.Join(neFilePaths, " ")) + } + + // 组合命令输入 + cmdStrArr := []string{} + if neType == "OMC" { + omcStrArr := []string{} + if action == "install" { + // 安装软件包 + pkgCmdStr = fmt.Sprintf("sudo M_PARAM=install dpkg -i %s", strings.Join(neFilePaths, " ")) + if strings.HasSuffix(fileExt, "rpm") { + pkgCmdStr = fmt.Sprintf("sudo M_PARAM=install rpm -Uvh %s", strings.Join(neFilePaths, " ")) + } + omcStrArr = append(omcStrArr, pkgCmdStr) + } else { + // 升级软件包 + pkgCmdStr = fmt.Sprintf("sudo M_PARAM=upgrade dpkg -i %s", strings.Join(neFilePaths, " ")) + if strings.HasSuffix(fileExt, "rpm") { + pkgCmdStr = fmt.Sprintf("sudo M_PARAM=upgrade rpm -Uvh --reinstall %s", strings.Join(neFilePaths, " ")) + } + omcStrArr = append(omcStrArr, pkgCmdStr) + } + // 删除软件包 + omcStrArr = append(omcStrArr, fmt.Sprintf("sudo rm %s", strings.Join(neFilePaths, " "))) + + // 2s后执行omc相关命令 + cmdStrArr = append(cmdStrArr, fmt.Sprintf("nohup sh -c \"sleep 2s && %s\" > /tmp/omc_%s.out 2>&1 & \n", strings.Join(omcStrArr, " && "), action)) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("echo '%s' \n", okFlagStr)) + return okFlagStr, cmdStrArr, nil + } else if neType == "IMS" { + if action == "install" { + para5GData := NewNeInfo.Para5GData + cmdStrArr = append(cmdStrArr, pkgCmdStr+" \n") + + // 公网 PLMN地址 + cmdStrArr = append(cmdStrArr, fmt.Sprintf("/usr/local/etc/ims/default/tools/modipplmn.sh %s %s %s \n", para5GData["SIP_IP"], para5GData["MCC"], para5GData["MNC"])) + // 内网 服务地址 + cmdStrArr = append(cmdStrArr, fmt.Sprintf("/usr/local/etc/ims/default/tools/modintraip.sh %s \n", para5GData["IMS_IP"])) + // IWF连接PCF服务 + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.160/%s/g\" /usr/local/etc/iwf/iwf_conf.yaml \n", para5GData["PCF_IP"])) + // 设置 HOST + cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s ims' /etc/hosts || echo '%s ims' | sudo tee -a /etc/hosts \n", para5GData["IMS_IP"], para5GData["IMS_IP"])) + mnc_mcc := fmt.Sprintf("mnc%s.mcc%s", para5GData["MNC_DOMAIN"], para5GData["MCC"]) + hssHost := fmt.Sprintf("%s hss.ims.%s.3gppnetwork.org hss", para5GData["UDM_IP"], mnc_mcc) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s' /etc/hosts || echo '%s' | sudo tee -a /etc/hosts \n", hssHost, hssHost)) + pcrfHost := fmt.Sprintf("%s pcrf.epc.%s.3gppnetwork.org pcrf", para5GData["IMS_IP"], mnc_mcc) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s' /etc/hosts || echo '%s' | sudo tee -a /etc/hosts \n", pcrfHost, pcrfHost)) + imsOrgHost := fmt.Sprintf("ims.%s.3gppnetwork.org", mnc_mcc) + imsHost := fmt.Sprintf("%s %s ims", para5GData["SIP_IP"], imsOrgHost) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s' /etc/hosts || echo '%s' | sudo tee -a /etc/hosts \n", imsHost, imsHost)) + pcscfHost := fmt.Sprintf("%s pcscf.%s pcscf", para5GData["SIP_IP"], imsOrgHost) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s' /etc/hosts || echo '%s' | sudo tee -a /etc/hosts \n", pcscfHost, pcscfHost)) + icscfHost := fmt.Sprintf("%s icscf.%s icscf", para5GData["SIP_IP"], imsOrgHost) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s' /etc/hosts || echo '%s' | sudo tee -a /etc/hosts \n", icscfHost, icscfHost)) + scscfHost := fmt.Sprintf("%s scscf.%s scscf", para5GData["SIP_IP"], imsOrgHost) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s' /etc/hosts || echo '%s' | sudo tee -a /etc/hosts \n", scscfHost, scscfHost)) + mmtelHost := fmt.Sprintf("%s mmtel.%s mmtel", para5GData["SIP_IP"], imsOrgHost) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s' /etc/hosts || echo '%s' | sudo tee -a /etc/hosts \n", mmtelHost, mmtelHost)) + mrfcHost := fmt.Sprintf("%s mrfc.%s mrfc", para5GData["SIP_IP"], imsOrgHost) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s' /etc/hosts || echo '%s' | sudo tee -a /etc/hosts \n", mrfcHost, mrfcHost)) + smsHost := fmt.Sprintf("%s smsc.%s smsc", para5GData["SIP_IP"], imsOrgHost) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s' /etc/hosts || echo '%s' | sudo tee -a /etc/hosts \n", smsHost, smsHost)) + + cmdStrArr = append(cmdStrArr, "ims-stop || true && ims-start \n") + // 30s后停止服务 + // cmdStrArr = append(cmdStrArr, "nohup sh -c \"sleep 30s && sudo ims-stop\" > /dev/null 2>&1 & \n") + } else { + cmdStrArr = append(cmdStrArr, "ims-stop \n") + cmdStrArr = append(cmdStrArr, pkgCmdStr+" \n") + cmdStrArr = append(cmdStrArr, "ims-start \n") + } + } else { + if action == "install" { + para5GData := NewNeInfo.Para5GData + cmdStrArr = append(cmdStrArr, pkgCmdStr+" \n") + + // AMF配置修改 + if neTypeLower == "amf" { + cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/amf/default/amfcfg.yaml /usr/local/etc/amf/amfcfg.yaml \n") + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.120/%s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["AMF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.8.120/%s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["N2_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mcc: 001/mcc: %s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["MCC"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mnc: 01/mnc: %s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["MNC"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/sst: 1/sst: %s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["SST"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/sd: 000001/sd: %s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["SD"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/- 4388/- %s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["TAC"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.130/%s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["AUSF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.140/%s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["UDM_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.150/%s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["SMF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.160/%s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["PCF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.180/%s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["NRF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.200/%s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["LMF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.210/%s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["NEF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/- internet/- %s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["DNN_DATA"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s n2' /etc/hosts || echo '%s n2' | sudo tee -a /etc/hosts \n", para5GData["N2_IP"], para5GData["N2_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s amf' /etc/hosts || echo '%s amf' | sudo tee -a /etc/hosts \n", para5GData["AMF_IP"], para5GData["AMF_IP"])) + } + // AUSF配置修改 + if neTypeLower == "ausf" { + cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/ausf/default/ausfcfg.yaml /usr/local/etc/ausf/ausfcfg.yaml \n") + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.130/%s/g\" /usr/local/etc/ausf/ausfcfg.yaml \n", para5GData["AUSF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.140/%s/g\" /usr/local/etc/ausf/ausfcfg.yaml \n", para5GData["UDM_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.180/%s/g\" /usr/local/etc/ausf/ausfcfg.yaml \n", para5GData["NRF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mcc: 001/mcc: %s/g\" /usr/local/etc/ausf/ausfcfg.yaml \n", para5GData["MCC"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mnc: 01/mnc: %s/g\" /usr/local/etc/ausf/ausfcfg.yaml \n", para5GData["MNC"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s ausf' /etc/hosts || echo '%s ausf' | sudo tee -a /etc/hosts \n", para5GData["AUSF_IP"], para5GData["AUSF_IP"])) + } + // UDM配置修改 + if neTypeLower == "udm" { + cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/udm/default/udmcfg.yaml /usr/local/etc/udm/udmcfg.yaml \n") + cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/udm/default/nssai.yaml /usr/local/etc/udm/nssai.yaml \n") + cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/udm/default/snssai.yaml /usr/local/etc/udm/snssai.yaml \n") + cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/udm/default/as.yaml /usr/local/etc/udm/as.yaml \n") + cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/udm/default/dnn.yaml /usr/local/etc/udm/dnn.yaml \n") + cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/udm/default/scscfSet.yaml /usr/local/etc/udm/scscfSet.yaml \n") + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.180/%s/g\" /usr/local/etc/udm/udmcfg.yaml \n", para5GData["NRF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.140/%s/g\" /usr/local/etc/udm/udmcfg.yaml \n", para5GData["UDM_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.130/%s/g\" /usr/local/etc/udm/udmcfg.yaml \n", para5GData["AUSF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.120/%s/g\" /usr/local/etc/udm/udmcfg.yaml \n", para5GData["AMF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mcc: 001/mcc: %s/g\" /usr/local/etc/udm/udmcfg.yaml \n", para5GData["MCC"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mnc: 01/mnc: %s/g\" /usr/local/etc/udm/udmcfg.yaml \n", para5GData["MNC"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mnc001.mcc001/mnc%s.mcc%s/g\" /usr/local/etc/udm/udmcfg.yaml \n", para5GData["MNC_DOMAIN"], para5GData["MCC"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mnc001.mcc001/mnc%s.mcc%s/g\" /usr/local/etc/udm/as.yaml \n", para5GData["MNC_DOMAIN"], para5GData["MCC"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mnc001.mcc001/mnc%s.mcc%s/g\" /usr/local/etc/udm/scscfSet.yaml \n", para5GData["MNC_DOMAIN"], para5GData["MCC"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/sst: 1/sst: %s/g\" /usr/local/etc/udm/nssai.yaml \n", para5GData["SST"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/sst: 1/sst: %s/g\" /usr/local/etc/udm/snssai.yaml \n", para5GData["SST"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/sd: 000001/sd: %s/g\" /usr/local/etc/udm/nssai.yaml \n", para5GData["SD"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/sd: 000001/sd: %s/g\" /usr/local/etc/udm/snssai.yaml \n", para5GData["SD"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/dnn: internet/dnn: %s/g\" /usr/local/etc/udm/snssai.yaml \n", para5GData["DNN_DATA"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/dnn: ims/dnn: %s/g\" /usr/local/etc/udm/snssai.yaml \n", para5GData["DNN_IMS"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/serviceSelection: 'internet'/serviceSelection: '%s'/g\" /usr/local/etc/udm/epsApn.yaml \n", para5GData["DNN_DATA"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/serviceSelection: 'ims'/serviceSelection: '%s'/g\" /usr/local/etc/udm/epsApn.yaml \n", para5GData["DNN_IMS"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/dnn: internet/dnn: %s/g\" /usr/local/etc/udm/dnn.yaml \n", para5GData["DNN_DATA"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/dnn: ims/dnn: %s/g\" /usr/local/etc/udm/dnn.yaml \n", para5GData["DNN_IMS"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.8.110/%s/g\" /usr/local/etc/udm/as.yaml \n", para5GData["SIP_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s udm' /etc/hosts || echo '%s udm' | sudo tee -a /etc/hosts \n", para5GData["UDM_IP"], para5GData["UDM_IP"])) + } + // SMF配置修改 + if neTypeLower == "smf" { + cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/smf/default/smf_conf.yaml /usr/local/etc/smf/smf_conf.yaml \n") + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.8.110/%s/g\" /usr/local/etc/smf/smf_conf.yaml \n", para5GData["SIP_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.120/%s/g\" /usr/local/etc/smf/smf_conf.yaml \n", para5GData["AMF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.140/%s/g\" /usr/local/etc/smf/smf_conf.yaml \n", para5GData["UDM_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.150/%s/g\" /usr/local/etc/smf/smf_conf.yaml \n", para5GData["SMF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.160/%s/g\" /usr/local/etc/smf/smf_conf.yaml \n", para5GData["PCF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.180/%s/g\" /usr/local/etc/smf/smf_conf.yaml \n", para5GData["NRF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.190/%s/g\" /usr/local/etc/smf/smf_conf.yaml \n", para5GData["UPF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s|10.2.1.0/24|%s|g\" /usr/local/etc/smf/smf_conf.yaml \n", para5GData["UE_POOL"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/internet/%s/g\" /usr/local/etc/smf/smf_conf.yaml \n", para5GData["DNN_DATA"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s smf' /etc/hosts || echo '%s smf' | sudo tee -a /etc/hosts \n", para5GData["SMF_IP"], para5GData["SMF_IP"])) + } + // PCF配置修改 + if neTypeLower == "pcf" { + cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/pcf/default/pcfcfg.yaml /usr/local/etc/pcf/pcfcfg.yaml \n") + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.120/%s/g\" /usr/local/etc/pcf/pcfcfg.yaml \n", para5GData["AMF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.140/%s/g\" /usr/local/etc/pcf/pcfcfg.yaml \n", para5GData["UDM_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.160/%s/g\" /usr/local/etc/pcf/pcfcfg.yaml \n", para5GData["PCF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.180/%s/g\" /usr/local/etc/pcf/pcfcfg.yaml \n", para5GData["NRF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.210/%s/g\" /usr/local/etc/pcf/pcfcfg.yaml \n", para5GData["NEF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mcc: 001/mcc: %s/g\" /usr/local/etc/pcf/pcfcfg.yaml \n", para5GData["MCC"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mnc: 01/mnc: %s/g\" /usr/local/etc/pcf/pcfcfg.yaml \n", para5GData["MNC"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mnc001.mcc001/mnc%s.mcc%s/g\" /usr/local/etc/pcf/pcfcfg.yaml \n", para5GData["MNC_DOMAIN"], para5GData["MCC"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s pcf' /etc/hosts || echo '%s pcf' | sudo tee -a /etc/hosts \n", para5GData["PCF_IP"], para5GData["PCF_IP"])) + } + + // NSSF配置修改 + if neTypeLower == "nssf" { + cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/nssf/default/nssfcfg.yaml /usr/local/etc/nssf/nssfcfg.yaml \n") + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.170/%s/g\" /usr/local/etc/nssf/nssfcfg.yaml \n", para5GData["NSSF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.180/%s/g\" /usr/local/etc/nssf/nssfcfg.yaml \n", para5GData["NRF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mcc: 001/mcc: %s/g\" /usr/local/etc/nssf/nssfcfg.yaml \n", para5GData["MCC"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mnc: 01/mnc: %s/g\" /usr/local/etc/nssf/nssfcfg.yaml \n", para5GData["MNC"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s nssf' /etc/hosts || echo '%s nssf' | sudo tee -a /etc/hosts \n", para5GData["NSSF_IP"], para5GData["NSSF_IP"])) + } + // NRF配置修改 + if neTypeLower == "nrf" { + cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/nrf/default/nrfcfg.yaml /usr/local/etc/nrf/nrfcfg.yaml \n") + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.180/%s/g\" /usr/local/etc/nrf/nrfcfg.yaml \n", para5GData["NRF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mcc: 001/mcc: %s/g\" /usr/local/etc/nrf/nrfcfg.yaml \n", para5GData["MCC"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mnc: 01/mnc: %s/g\" /usr/local/etc/nrf/nrfcfg.yaml \n", para5GData["MNC"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s nrf' /etc/hosts || echo '%s nrf' | sudo tee -a /etc/hosts \n", para5GData["NRF_IP"], para5GData["NRF_IP"])) + } + + // UPF配置修改 + if neTypeLower == "upf" { + cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/upf/default/upfcfg.yaml /usr/local/etc/upf/upfcfg.yaml \n") + cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/upf/default/upfForwarder_1.yaml /usr/local/etc/upf/upfForwarder_1.yaml \n") + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.190/%s/g\" /usr/local/etc/upf/upfcfg.yaml \n", para5GData["UPF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/localhost/%s/g\" /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["UPF_IP"])) + // UE + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i '/- interfaceType: \"N6\"/,/ueIpv4: 10.2.1.0/s/ueIpv4: 10.2.1.0/ueIpv4: %s/' /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["UE_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i '/- interfaceType: \"N6\"/,/ueIpv4Mask: 255.255.255.0/s/ueIpv4Mask: 255.255.255.0/ueIpv4Mask: %s/' /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["UE_MASK"])) + // N3 + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.8.190/%s/g\" /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["N3_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i '/- interfaceType: \"N3\"/,/ipv4Mask: 255.255.240.0/s/ipv4Mask: 255.255.240.0/ipv4Mask: %s/' /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["N3_MASK"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i '/- interfaceType: \"N3\"/,/gatewayIpv4: 192.168.1.254/s/gatewayIpv4: 192.168.1.254/gatewayIpv4: %s/' /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["N3_GW"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i '/- interfaceType: \"N3\"/,/interfacePCI: \"0000:00:00.0\"/s/interfacePCI: \"0000:00:00.0\"/interfacePCI: \"%s\"/' /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["N3_PCI"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i '/- interfaceType: \"N3\"/,/macAddr: \"00:00:00:00:00:00\"/s/macAddr: \"00:00:00:00:00:00\"/macAddr: \"%s\"/' /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["N3_MAC"])) + // 标准版 N6 + if para5GData["UPF_TYPE"] == "StandardUPF" { + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i '/- interfaceType: \"N3\"/,/driverType: .*/s/driverType: .*/driverType: \"%s\"/' /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["UPF_DRIVER_TYPE"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i '/- interfaceType: \"N3\"/,/systemNetworkCardName: .*/s/systemNetworkCardName: .*/systemNetworkCardName: \"%s\"/' /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["UPF_NIC_NAME"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i '/- interfaceType: \"N6\"/,/driverType: .*/s/driverType: .*/driverType: \"%s\"/' /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["UPF_DRIVER_TYPE"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i '/- interfaceType: \"N6\"/,/systemNetworkCardName: .*/s/systemNetworkCardName: .*/systemNetworkCardName: \"%s\"/' /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["UPF_NIC_NAME"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.8.191/%s/g\" /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["N6_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i '/- interfaceType: \"N6\"/,/ipv4Mask: 255.255.240.0/s/ipv4Mask: 255.255.240.0/ipv4Mask: %s/' /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["N6_MASK"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i '/- interfaceType: \"N6\"/,/gatewayIpv4: 192.168.1.254/s/gatewayIpv4: 192.168.1.254/gatewayIpv4: %s/' /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["N6_GW"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i '/- interfaceType: \"N6\"/,/interfacePCI: \"0000:00:00.0\"/s/interfacePCI: \"0000:00:00.0\"/interfacePCI: \"%s\"/' /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["N6_PCI"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i '/- interfaceType: \"N6\"/,/macAddr: \"00:00:00:00:00:00\"/s/macAddr: \"00:00:00:00:00:00\"/macAddr: \"%s\"/' /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["N6_MAC"])) + // 路由 + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo ip route add '%s/%s' via '%s' \n", para5GData["UE_IP"], para5GData["UE_CIDR"], para5GData["N6_IP"])) + } + // 轻量版 + if para5GData["UPF_TYPE"] == "LightUPF" { + cmdStrArr = append(cmdStrArr, "sudo sed -i \"s/192.168.8.191/0.0.0.0/g\" /usr/local/etc/upf/upfForwarder_1.yaml \n") + cmdStrArr = append(cmdStrArr, "sudo sed -i \"s/type: upfd/type: tun/g\" /usr/local/etc/upf/upfForwarder_1.yaml \n") + cmdStrArr = append(cmdStrArr, "sudo sed -i 's/driverType: .*/driverType: \"\"/g' /usr/local/etc/upf/upfForwarder_1.yaml \n") + } + cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s upf' /etc/hosts || echo '%s upf' | sudo tee -a /etc/hosts \n", para5GData["UPF_IP"], para5GData["UPF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s upfn3' /etc/hosts || echo '%s upfn3' | sudo tee -a /etc/hosts \n", para5GData["N3_IP"], para5GData["N3_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s upfn6' /etc/hosts || echo '%s upfn6' | sudo tee -a /etc/hosts \n", para5GData["N6_IP"], para5GData["N6_IP"])) + } + + // LMF配置修改 - 已不再维护,导致激活License失败 + // NEF配置修改 - SNMP无需License + if neTypeLower == "nef" { + cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/nef/default/nef_conf.yaml /usr/local/etc/nef/nef_conf.yaml \n") + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.110/%s/g\" /usr/local/etc/nef/nef_conf.yaml \n", para5GData["IMS_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.120/%s/g\" /usr/local/etc/nef/nef_conf.yaml \n", para5GData["AMF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.130/%s/g\" /usr/local/etc/nef/nef_conf.yaml \n", para5GData["AUSF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.140/%s/g\" /usr/local/etc/nef/nef_conf.yaml \n", para5GData["UDM_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.150/%s/g\" /usr/local/etc/nef/nef_conf.yaml \n", para5GData["SMF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.160/%s/g\" /usr/local/etc/nef/nef_conf.yaml \n", para5GData["PCF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.170/%s/g\" /usr/local/etc/nef/nef_conf.yaml \n", para5GData["NSSF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.180/%s/g\" /usr/local/etc/nef/nef_conf.yaml \n", para5GData["NRF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.190/%s/g\" /usr/local/etc/nef/nef_conf.yaml \n", para5GData["UPF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.210/%s/g\" /usr/local/etc/nef/nef_conf.yaml \n", para5GData["NEF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s nef' /etc/hosts || echo '%s nef' | sudo tee -a /etc/hosts \n", para5GData["NEF_IP"], para5GData["NEF_IP"])) + } + + // MME配置修改 - 4G + if neTypeLower == "mme" { + cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/mme/default/mme.conf /usr/local/etc/mme/mme.conf \n") + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.120/%s/g\" /usr/local/etc/mme/mme.conf \n", para5GData["AMF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.150/%s/g\" /usr/local/etc/mme/mme.conf \n", para5GData["SMF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s|192.168.8.220/20|%s|g\" /usr/local/etc/mme/mme.conf \n", para5GData["S1_MMEIP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s|172.16.5.220/24|%s|g\" /usr/local/etc/mme/mme.conf \n", para5GData["S11_MMEIP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s|172.16.5.220|%s|g\" /usr/local/etc/mme/mme.conf \n", para5GData["MME_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s|172.16.5.221/24|%s|g\" /usr/local/etc/mme/mme.conf \n", para5GData["S10_MMEIP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mnc001.mcc001/mnc%s.mcc%s/g\" /usr/local/etc/mme/mme.conf \n", para5GData["MNC_DOMAIN"], para5GData["MCC"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/\"00101\"/\"%s%s\"/g\" /usr/local/etc/mme/mme.conf \n", para5GData["MCC"], para5GData["MNC"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i 's/MCC=\"001\"/MCC=\"%s\"/g' /usr/local/etc/mme/mme.conf \n", para5GData["MCC"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i 's/MCC = \"001\"/MCC = \"%s\"/g' /usr/local/etc/mme/mme.conf \n", para5GData["MCC"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i 's/MNC=\"01\";/MNC=\"%s\";/g' /usr/local/etc/mme/mme.conf \n", para5GData["MNC"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i 's/MNC = \"01\";/MNC = \"%s\";/g' /usr/local/etc/mme/mme.conf \n", para5GData["MNC"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i 's/TAC = \"1\";/TAC = \"%s\";/g' /usr/local/etc/mme/mme.conf \n", para5GData["TAC"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i 's/TAC = 1;/TAC = %s;/g' /usr/local/etc/mme/mme.conf \n", para5GData["TAC"])) + // SMF开启 + cmdStrArr = append(cmdStrArr, "sudo sed -i \"/^ *gxcfg:/,/^ *[^ ]/{s/enable: false/enable: true/;b};\" /usr/local/etc/smf/smf_conf.yaml \n") + cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s mme' /etc/hosts || echo '%s mme' | sudo tee -a /etc/hosts \n", para5GData["MME_IP"], para5GData["MME_IP"])) + } + // N3IWF配置修改 + if neTypeLower == "n3iwf" { + cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/n3iwf/default/n3iwfcfg.yaml /usr/local/etc/n3iwf/n3iwfcfg.yaml \n") + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/MCC: 001/MCC: %s/g\" /usr/local/etc/n3iwf/n3iwfcfg.yaml \n", para5GData["MCC"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/MNC: 01/MNC: %s/g\" /usr/local/etc/n3iwf/n3iwfcfg.yaml \n", para5GData["MNC"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.12.161/%s/g\" /usr/local/etc/n3iwf/n3iwfcfg.yaml \n", para5GData["N3IWF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.12.160/%s/g\" /usr/local/etc/n3iwf/n3iwfcfg.yaml \n", para5GData["N3IWF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.8.27/%s/g\" /usr/local/etc/n3iwf/n3iwfcfg.yaml \n", para5GData["UDM_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.1.239/%s/g\" /usr/local/etc/n3iwf/n3iwfcfg.yaml \n", para5GData["SMF_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.8.22/%s/g\" /usr/local/etc/n3iwf/n3iwfcfg.yaml \n", para5GData["N2_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.1.161/%s/g\" /usr/local/etc/n3iwf/n3iwfcfg.yaml \n", para5GData["N3_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.1.160/%s/g\" /usr/local/etc/n3iwf/n3iwfcfg.yaml \n", para5GData["N6_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s n3iwf' /etc/hosts || echo '%s n3iwf' | sudo tee -a /etc/hosts \n", para5GData["N3IWF_IP"], para5GData["N3IWF_IP"])) + } + // SMSC配置修改 + if neTypeLower == "smsc" { + cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/smsc/default/mcms_conf.txt /usr/local/etc/smsc/mcms_conf.txt \n") + cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/smsc/default/sccp.conf /usr/local/etc/smsc/sccp.conf \n") + cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/smsc/default/smsc_config.yaml /usr/local/etc/smsc/smsc_config.yaml \n") + cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/smsc/default/wxc2_sys.conf /usr/local/etc/smsc/wxc2_sys.conf \n") + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.8.240/%s/g\" /usr/local/etc/smsc/mcms_conf.txt \n", para5GData["SMSC_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.8.240/%s/g\" /usr/local/etc/smsc/sccp.conf \n", para5GData["SMSC_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.1.123/%s/g\" /usr/local/etc/smsc/smsc_config.yaml \n", para5GData["SMSC_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.8.240/%s/g\" /usr/local/etc/smsc/wxc2_sys.conf \n", para5GData["SMSC_IP"])) + cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s smsc' /etc/hosts || echo '%s smsc' | sudo tee -a /etc/hosts \n", para5GData["SMSC_IP"], para5GData["SMSC_IP"])) + } + + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo service %s restart \n", neTypeLower)) + // 30s后停止服务 + // cmdStrArr = append(cmdStrArr, fmt.Sprintf("nohup sh -c \"sleep 30s && sudo service %s stop\" > /dev/null 2>&1 & \n", neTypeLower)) + } else { + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo service %s stop \n", neTypeLower)) + cmdStrArr = append(cmdStrArr, pkgCmdStr+" \n") + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo service %s restart \n", neTypeLower)) + } + } + + // 安装操作有redis安装时需要重启 + if action == "install" && (neTypeLower == "ims" || neTypeLower == "udm") { + // adb + if strings.Contains(pkgCmdStr, "adb") { + para5GData := NewNeInfo.Para5GData + cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/adb/default/adb.conf /usr/local/etc/adb/adb.conf \n") + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/bind 127.0.0.1/bind 127.0.0.1 %s/g\" /usr/local/etc/adb/adb.conf \n", para5GData["DB_IP"])) + cmdStrArr = append(cmdStrArr, "sudo service adb restart \n") + } + // kvdb + if strings.Contains(pkgCmdStr, "kvdb") { + para5GData := NewNeInfo.Para5GData + cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/kvdb/default/kvdb.conf /usr/local/etc/kvdb/kvdb.conf \n") + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/bind 127.0.0.1/bind 127.0.0.1 %s/g\" /usr/local/etc/kvdb/kvdb.conf \n", para5GData["DB_IP"])) + cmdStrArr = append(cmdStrArr, "sudo service kvdb restart \n") + } + } + + // 删除软件包 + cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo rm %s \n", strings.Join(neFilePaths, " "))) + // 结束 + cmdStrArr = append(cmdStrArr, fmt.Sprintf("echo '%s' \n", okFlagStr)) + + return okFlagStr, cmdStrArr, nil +} + +// operateRun 操作版本-执行阶段 +func (r *NeVersion) operateRun(sshClient *ssh.ConnSSH, preinput map[string]string, cmdStrArr []string, neType string, okFlagStr string) (string, error) { + // ssh连接会话 + clientSession, err := sshClient.NewClientSession(127, 42) + if err != nil { + return "", fmt.Errorf("neinfo ssh client session new err") + } + defer clientSession.Close() + + firstRead := true // 首次命令进行记录日志信息 + commandLineText := "" // 日志信息 + done := make(chan bool) // 完成信号 + // 超时退出 120s + timeoutTicker := time.NewTicker(120 * time.Second) + defer timeoutTicker.Stop() + // 实时读取SSH消息直接输出 + msTicker := time.NewTicker(100 * time.Millisecond) + defer msTicker.Stop() + go func() { + for { + select { + case <-timeoutTicker.C: + done <- true + return + case <-msTicker.C: + outputByte := clientSession.Read() + if len(outputByte) > 0 { + outputStr := string(outputByte) + // 非首次进行记录命令 + if !firstRead { + commandLineText += outputStr + } + + // IMS预输入 + if neType == "IMS" { + // IMS包 P/I/S-CSCF Config 配置覆盖 + if strings.Contains(outputStr, "(P/I/S-CSCF Config)? ") { + if pisCSCF, ok := preinput["pisCSCF"]; ok && pisCSCF != "" { + clientSession.Write(fmt.Sprintf("%s \n", pisCSCF)) + } else { + clientSession.Write("y \n") + } + continue + } + // MF包 etc下目录覆盖 + if strings.Contains(outputStr, "/usr/local/etc/mf directory? (Yes/No, default: No)") { + if pisCSCF, ok := preinput["updateMFetc"]; ok && pisCSCF != "" { + clientSession.Write(fmt.Sprintf("%s \n", pisCSCF)) + } else { + clientSession.Write("No \n") + } + continue + } + // MF包 share下目录覆盖 + if strings.Contains(outputStr, "/usr/local/share/mf directory? (Yes/No, default: No)") { + if pisCSCF, ok := preinput["updateMFshare"]; ok && pisCSCF != "" { + clientSession.Write(fmt.Sprintf("%s \n", pisCSCF)) + } else { + clientSession.Write("No \n") + } + continue + } + } + + // 命令终止符后继续执行命令 + suffix := strings.HasSuffix(outputStr, "~]# ") || strings.HasSuffix(outputStr, "~$ ") + if len(cmdStrArr) > 0 && suffix { + if firstRead { + firstRead = false + } + shiftElement := cmdStrArr[0] // 获取第一个元素 + cmdStrArr = cmdStrArr[1:] // 将第一个元素从切片中移除 + clientSession.Write(shiftElement) + continue + } + // 最后输出的退出标记 + if strings.LastIndex(outputStr, okFlagStr) > 5 { + done <- true + break + } + } + } + } + }() + // 等待写入协程完成 + <-done + + return commandLineText, nil +} + +// operateDome 操作版本-完成阶段 +func (r *NeVersion) operateDome(action string, neVersion model.NeVersion) error { + if action == "install" { + // 网元信息 + neInfo := NewNeInfo.SelectNeInfoByNeTypeAndNeID(neVersion.NeType, neVersion.NeId) + if neInfo.NeId != neVersion.NeId { + return fmt.Errorf("error found neinfo") + } + + // ========= 网元OAM配置文件 start ========== + if err := NewNeInfo.NeConfOAMWirteSync(neInfo, nil, true); err != nil { + return fmt.Errorf("error wirte OAM file info") + } + // ========= 网元OAM配置文件 end =========== + + // SMSC配置修改IMS和UDM 配置 + if neInfo.NeType == "SMSC" { + para5GData := NewNeInfo.Para5GData + mnc_mcc := fmt.Sprintf("mnc%s.mcc%s", para5GData["MNC_DOMAIN"], para5GData["MCC"]) + smscHost := fmt.Sprintf("%s smsc.ims.%s.3gppnetwork.org", para5GData["SMSC_IP"], mnc_mcc) + smscHostCMD := fmt.Sprintf("grep -qxF '%s' /etc/hosts || echo '%s' | sudo tee -a /etc/hosts \n", smscHost, smscHost) + smscIPCMD := fmt.Sprintf("grep -qxF '%s smsc' /etc/hosts || echo '%s smsc' | sudo tee -a /etc/hosts \n", para5GData["SMSC_IP"], para5GData["SMSC_IP"]) + smsHost := fmt.Sprintf("sudo sed -i '/^%s smsc.*smsc$/c\\' /etc/hosts", para5GData["SIP_IP"]) + + // IMS 配置 + imsNEs := NewNeInfo.SelectList(model.NeInfo{NeType: "IMS"}, false, false) + for _, v := range imsNEs { + NewNeInfo.NeRunSSHCmd(v.NeType, v.NeId, smscIPCMD) + NewNeInfo.NeRunSSHCmd(v.NeType, v.NeId, smscHostCMD) + NewNeInfo.NeRunSSHCmd(v.NeType, v.NeId, smsHost) + NewNeInfo.NeRunSSHCmd(v.NeType, v.NeId, "sudo sed -i '/^#!define WITH_SMS/ s/^/#/' /usr/local/etc/ims/vars.cfg") + NewNeInfo.NeRunSSHCmd(v.NeType, v.NeId, "ims-stop || true && ims-start") + } + // UDM 配置 + smscASName := fmt.Sprintf("sudo sed -i \"/- name: 'sms_as'/{n;s|serverName: .*|serverName: 'sip:%s:5060'|}\" /usr/local/etc/udm/as.yaml", para5GData["SMSC_IP"]) + smscASAddress := fmt.Sprintf("sudo sed -i \"/- name: 'sms_as'/{n;n;n;s|diameterAddress: .*|diameterAddress: 'smsc.ims.%s.3gppnetwork.org'|}\" /usr/local/etc/udm/as.yaml", mnc_mcc) + udmNEs := NewNeInfo.SelectList(model.NeInfo{NeType: "UDM"}, false, false) + for _, v := range udmNEs { + NewNeInfo.NeRunSSHCmd(v.NeType, v.NeId, smscIPCMD) + NewNeInfo.NeRunSSHCmd(v.NeType, v.NeId, smscHostCMD) + NewNeInfo.NeRunSSHCmd(v.NeType, v.NeId, smscASName) + NewNeInfo.NeRunSSHCmd(v.NeType, v.NeId, smscASAddress) + NewNeInfo.NeRunSSHCmd(v.NeType, v.NeId, "sudo service udm restart") + } + } + } + + // 更新Version + verInfo := r.SelectByNeTypeAndNeID(neVersion.NeType, neVersion.NeId) + if verInfo.NeId == neVersion.NeId { + curName := verInfo.Name + curVersion := verInfo.Version + curPath := verInfo.Path + if action == "install" { + verInfo.Name = neVersion.NewName + verInfo.Version = neVersion.NewVersion + verInfo.Path = neVersion.NewPath + verInfo.PreName = "-" + verInfo.PreVersion = "-" + verInfo.PrePath = "-" + verInfo.NewName = "-" + verInfo.NewVersion = "-" + verInfo.NewPath = "-" + } + if action == "upgrade" { + verInfo.Name = neVersion.NewName + verInfo.Version = neVersion.NewVersion + verInfo.Path = neVersion.NewPath + verInfo.PreName = curName + verInfo.PreVersion = curVersion + verInfo.PrePath = curPath + verInfo.NewName = "-" + verInfo.NewVersion = "-" + verInfo.NewPath = "-" + } + if action == "rollback" { + verInfo.Name = neVersion.PreName + verInfo.Version = neVersion.PreVersion + verInfo.Path = neVersion.PrePath + verInfo.PreName = curName + verInfo.PreVersion = curVersion + verInfo.PrePath = curPath + } + + verInfo.Status = "1" + r.Update(verInfo) + } + return nil } diff --git a/src/modules/network_element/service/ne_version.impl.go b/src/modules/network_element/service/ne_version.impl.go deleted file mode 100644 index 489fb2b1..00000000 --- a/src/modules/network_element/service/ne_version.impl.go +++ /dev/null @@ -1,642 +0,0 @@ -package service - -import ( - "fmt" - "os" - "path/filepath" - "strings" - "time" - - "be.ems/src/framework/utils/file" - "be.ems/src/framework/utils/ssh" - "be.ems/src/modules/network_element/model" - "be.ems/src/modules/network_element/repository" -) - -// 实例化服务层 NeVersionImpl 结构体 -var NewNeVersionImpl = &NeVersionImpl{ - neVersionRepository: repository.NewNeVersionImpl, -} - -// NeVersionImpl 网元版本信息 服务层处理 -type NeVersionImpl struct { - // 网元版本信息表 - neVersionRepository repository.INeVersion -} - -// SelectNeHostPage 分页查询列表数据 -func (r *NeVersionImpl) SelectPage(query map[string]any) map[string]any { - return r.neVersionRepository.SelectPage(query) -} - -// SelectConfigList 查询列表 -func (r *NeVersionImpl) SelectList(neVersion model.NeVersion) []model.NeVersion { - return r.neVersionRepository.SelectList(neVersion) -} - -// SelectByIds 通过ID查询 -func (r *NeVersionImpl) SelectById(id string) model.NeVersion { - if id == "" { - return model.NeVersion{} - } - neVersions := r.neVersionRepository.SelectByIds([]string{id}) - if len(neVersions) > 0 { - return neVersions[0] - } - return model.NeVersion{} -} - -// Insert 新增信息 -func (r *NeVersionImpl) Insert(neVersion model.NeVersion) string { - return r.neVersionRepository.Insert(neVersion) -} - -// Update 修改信息 -func (r *NeVersionImpl) Update(neVersion model.NeVersion) int64 { - return r.neVersionRepository.Update(neVersion) -} - -// DeleteByIds 批量删除信息 -func (r *NeVersionImpl) DeleteByIds(ids []string) (int64, error) { - // 检查是否存在 - rowIds := r.neVersionRepository.SelectByIds(ids) - if len(rowIds) <= 0 { - return 0, fmt.Errorf("neVersion.noData") - } - - if len(rowIds) == len(ids) { - rows := r.neVersionRepository.DeleteByIds(ids) - return rows, nil - } - // 删除信息失败! - return 0, fmt.Errorf("delete fail") -} - -// SelectByNeTypeAndNeID 通过网元类型和网元ID查询 -func (r *NeVersionImpl) SelectByNeTypeAndNeID(neType, neId string) model.NeVersion { - neVersions := r.neVersionRepository.SelectList(model.NeVersion{ - NeType: neType, - NeId: neId, - }) - if len(neVersions) > 0 { - return neVersions[0] - } - return model.NeVersion{} -} - -// Operate 操作版本上传到网元主机执行命令 -// -// action 安装行为:install upgrade rollback -func (r *NeVersionImpl) Operate(action string, neVersion model.NeVersion, preinput map[string]string) (string, error) { - // 网元主机的SSH客户端 - sshClient, err := NewNeInfoImpl.NeRunSSHClient(neVersion.NeType, neVersion.NeId) - if err != nil { - return "", err - } - defer sshClient.Close() - - // ========= 文件传输阶段 ========= - softwarePath := neVersion.Path - if action == "install" || action == "upgrade" { - softwarePath = neVersion.NewPath - } - if action == "rollback" { - softwarePath = neVersion.PrePath - } - neFilePaths, err := r.operateFile(sshClient, softwarePath) - if err != nil { - return "", err - } - - // ========= 安装时设置 ========= - if action == "install" { - // 网元公共配置文件 - para5GMap, err := NewNeInfoImpl.NeConfPara5GRead() - if para5GMap == nil || err != nil { - return "", fmt.Errorf("error read para5G file info") - } - if err := NewNeInfoImpl.NeConfPara5GWirte(para5GMap, []string{fmt.Sprintf("%s@%s", neVersion.NeType, neVersion.NeId)}); err != nil { - return "", fmt.Errorf("error wirte para5G file info") - } - } - - // ========= 命令生成阶段 ========= - okFlagStr, cmdStrArr, err := r.operateCommand(action, neVersion.NeType, neFilePaths) - if err != nil { - return "", err - } - - // ========= 执行阶段 ========= - commandLine, err := r.operateRun(sshClient, preinput, cmdStrArr, neVersion.NeType, okFlagStr) - if err != nil { - return "", err - } - - // ========= 完成阶段 ========= - if strings.LastIndex(commandLine, okFlagStr) > 5 { - if err := r.operateDome(action, neVersion); err != nil { - return "", err - } - } - return commandLine, nil -} - -// operateDome 操作版本-文件传输阶段 -func (r *NeVersionImpl) operateFile(sshClient *ssh.ConnSSH, softwarePath string) ([]string, error) { - // 网元主机的SSH客户端进行文件传输 - sftpClient, err := sshClient.NewClientSFTP() - if err != nil { - return nil, err - } - defer sftpClient.Close() - - nePath := "/tmp" - copyFileToNeMap := map[string]string{} - - // 统一处理多个文件和单个文件的情况 - var softwarePaths []string - if strings.Contains(softwarePath, ",") { - softwarePaths = strings.Split(softwarePath, ",") - } else { - softwarePaths = []string{softwarePath} - } - - for _, path := range softwarePaths { - // 检查文件是否存在 - localFilePath := file.ParseUploadFilePath(path) - if _, err := os.Stat(localFilePath); err != nil { - return nil, fmt.Errorf("file read failure") - } - - fileName := filepath.Base(path) - neFilePath := fmt.Sprintf("%s/%s", nePath, fileName) - copyFileToNeMap[localFilePath] = neFilePath - } - - // 上传软件包到 /tmp - neFilePaths := []string{} - for k, v := range copyFileToNeMap { - if err = sftpClient.CopyFileLocalToRemote(k, v); err != nil { - return nil, fmt.Errorf("error uploading package") - } - neFilePaths = append(neFilePaths, v) - } - return neFilePaths, nil -} - -// operateDome 操作版本-命令生成阶段 -func (r *NeVersionImpl) operateCommand(action, neType string, neFilePaths []string) (string, []string, error) { - neTypeLower := strings.ToLower(neType) - // 命令终止结束标记 - okFlagStr := fmt.Sprintf("%s version %s successful!", neTypeLower, action) - // 安装软件包 - pkgCmdStr := fmt.Sprintf("sudo dpkg -i %s", strings.Join(neFilePaths, " ")) - fileExt := filepath.Ext(strings.ToLower(neFilePaths[0])) - if strings.HasSuffix(fileExt, "rpm") { - pkgCmdStr = fmt.Sprintf("sudo rpm -Uvh %s", strings.Join(neFilePaths, " ")) - } - - // 组合命令输入 - cmdStrArr := []string{} - if neType == "OMC" { - omcStrArr := []string{} - omcStrArr = append(omcStrArr, pkgCmdStr) - if action == "install" { - omcStrArr = append(omcStrArr, "/usr/local/omc/bin/setomc.sh -m install") // 初始化数据库 - } else { - omcStrArr = append(omcStrArr, "/usr/local/omc/bin/setomc.sh -m upgrade") // 升级数据库 - } - omcStrArr = append(omcStrArr, "sudo systemctl restart restagent") // 重启服务 - omcStrArr = append(omcStrArr, fmt.Sprintf("sudo rm %s", strings.Join(neFilePaths, " "))) // 删除软件包 - - // 2s后安装 - cmdStrArr = append(cmdStrArr, fmt.Sprintf("nohup sh -c \"sleep 2s && %s\" > /tmp/omc_%s.out 2>&1 & \n", strings.Join(omcStrArr, " && "), action)) - // 结束 - cmdStrArr = append(cmdStrArr, fmt.Sprintf("echo '%s' \n", okFlagStr)) - return okFlagStr, cmdStrArr, nil - } else if neType == "IMS" { - if action == "install" { - para5GData := NewNeInfoImpl.Para5GData - cmdStrArr = append(cmdStrArr, pkgCmdStr+" \n") - - // 公网 PLMN地址 - cmdStrArr = append(cmdStrArr, fmt.Sprintf("/usr/local/etc/ims/default/tools/modipplmn.sh %s %s %s \n", para5GData["SIP_IP"], para5GData["MCC"], para5GData["MNC"])) - // 内网 服务地址 - cmdStrArr = append(cmdStrArr, fmt.Sprintf("/usr/local/etc/ims/default/tools/modintraip.sh %s \n", para5GData["IMS_IP"])) - // IWF连接PCF服务 - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.160/%s/g\" /usr/local/etc/iwf/iwf_conf.yaml \n", para5GData["PCF_IP"])) - // 设置 HOST - cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s ims' /etc/hosts || echo '%s ims' | sudo tee -a /etc/hosts \n", para5GData["IMS_IP"], para5GData["IMS_IP"])) - mnc_mcc := fmt.Sprintf("mnc%s.mcc%s", para5GData["MNC_DOMAIN"], para5GData["MCC"]) - hssHost := fmt.Sprintf("%s hss.ims.%s.3gppnetwork.org hss", para5GData["UDM_IP"], mnc_mcc) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s' /etc/hosts || echo '%s' | sudo tee -a /etc/hosts \n", hssHost, hssHost)) - pcrfHost := fmt.Sprintf("%s pcrf.epc.%s.3gppnetwork.org pcrf", para5GData["IMS_IP"], mnc_mcc) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s' /etc/hosts || echo '%s' | sudo tee -a /etc/hosts \n", pcrfHost, pcrfHost)) - imsOrgHost := fmt.Sprintf("ims.%s.3gppnetwork.org", mnc_mcc) - imsHost := fmt.Sprintf("%s %s ims", para5GData["SIP_IP"], imsOrgHost) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s' /etc/hosts || echo '%s' | sudo tee -a /etc/hosts \n", imsHost, imsHost)) - pcscfHost := fmt.Sprintf("%s pcscf.%s pcscf", para5GData["SIP_IP"], imsOrgHost) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s' /etc/hosts || echo '%s' | sudo tee -a /etc/hosts \n", pcscfHost, pcscfHost)) - icscfHost := fmt.Sprintf("%s icscf.%s icscf", para5GData["SIP_IP"], imsOrgHost) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s' /etc/hosts || echo '%s' | sudo tee -a /etc/hosts \n", icscfHost, icscfHost)) - scscfHost := fmt.Sprintf("%s scscf.%s scscf", para5GData["SIP_IP"], imsOrgHost) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s' /etc/hosts || echo '%s' | sudo tee -a /etc/hosts \n", scscfHost, scscfHost)) - mmtelHost := fmt.Sprintf("%s mmtel.%s mmtel", para5GData["SIP_IP"], imsOrgHost) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s' /etc/hosts || echo '%s' | sudo tee -a /etc/hosts \n", mmtelHost, mmtelHost)) - mrfcHost := fmt.Sprintf("%s mrfc.%s mrfc", para5GData["SIP_IP"], imsOrgHost) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s' /etc/hosts || echo '%s' | sudo tee -a /etc/hosts \n", mrfcHost, mrfcHost)) - smsHost := fmt.Sprintf("%s smsc.%s smsc", para5GData["SIP_IP"], imsOrgHost) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s' /etc/hosts || echo '%s' | sudo tee -a /etc/hosts \n", smsHost, smsHost)) - - cmdStrArr = append(cmdStrArr, "ims-stop || true && ims-start \n") - // 30s后停止服务 - // cmdStrArr = append(cmdStrArr, "nohup sh -c \"sleep 30s && sudo ims-stop\" > /dev/null 2>&1 & \n") - } else { - cmdStrArr = append(cmdStrArr, "ims-stop \n") - cmdStrArr = append(cmdStrArr, pkgCmdStr+" \n") - cmdStrArr = append(cmdStrArr, "ims-start \n") - } - } else { - if action == "install" { - para5GData := NewNeInfoImpl.Para5GData - cmdStrArr = append(cmdStrArr, pkgCmdStr+" \n") - - // AMF配置修改 - if neTypeLower == "amf" { - cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/amf/default/amfcfg.yaml /usr/local/etc/amf/amfcfg.yaml \n") - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.120/%s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["AMF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.8.120/%s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["N2_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mcc: 001/mcc: %s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["MCC"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mnc: 01/mnc: %s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["MNC"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/sst: 1/sst: %s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["SST"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/sd: 000001/sd: %s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["SD"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/- 4388/- %s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["TAC"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.130/%s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["AUSF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.140/%s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["UDM_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.150/%s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["SMF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.160/%s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["PCF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.180/%s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["NRF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.200/%s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["LMF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.210/%s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["NEF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/- internet/- %s/g\" /usr/local/etc/amf/amfcfg.yaml \n", para5GData["DNN_DATA"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s n2' /etc/hosts || echo '%s n2' | sudo tee -a /etc/hosts \n", para5GData["N2_IP"], para5GData["N2_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s amf' /etc/hosts || echo '%s amf' | sudo tee -a /etc/hosts \n", para5GData["AMF_IP"], para5GData["AMF_IP"])) - } - // AUSF配置修改 - if neTypeLower == "ausf" { - cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/ausf/default/ausfcfg.yaml /usr/local/etc/ausf/ausfcfg.yaml \n") - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.130/%s/g\" /usr/local/etc/ausf/ausfcfg.yaml \n", para5GData["AUSF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.140/%s/g\" /usr/local/etc/ausf/ausfcfg.yaml \n", para5GData["UDM_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.180/%s/g\" /usr/local/etc/ausf/ausfcfg.yaml \n", para5GData["NRF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mcc: 001/mcc: %s/g\" /usr/local/etc/ausf/ausfcfg.yaml \n", para5GData["MCC"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mnc: 01/mnc: %s/g\" /usr/local/etc/ausf/ausfcfg.yaml \n", para5GData["MNC"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s ausf' /etc/hosts || echo '%s ausf' | sudo tee -a /etc/hosts \n", para5GData["AUSF_IP"], para5GData["AUSF_IP"])) - } - // UDM配置修改 - if neTypeLower == "udm" { - cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/udm/default/udmcfg.yaml /usr/local/etc/udm/udmcfg.yaml \n") - cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/udm/default/nssai.yaml /usr/local/etc/udm/nssai.yaml \n") - cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/udm/default/snssai.yaml /usr/local/etc/udm/snssai.yaml \n") - cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/udm/default/as.yaml /usr/local/etc/udm/as.yaml \n") - cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/udm/default/dnn.yaml /usr/local/etc/udm/dnn.yaml \n") - cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/udm/default/scscfSet.yaml /usr/local/etc/udm/scscfSet.yaml \n") - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.180/%s/g\" /usr/local/etc/udm/udmcfg.yaml \n", para5GData["NRF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.140/%s/g\" /usr/local/etc/udm/udmcfg.yaml \n", para5GData["UDM_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.130/%s/g\" /usr/local/etc/udm/udmcfg.yaml \n", para5GData["AUSF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.120/%s/g\" /usr/local/etc/udm/udmcfg.yaml \n", para5GData["AMF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mcc: 001/mcc: %s/g\" /usr/local/etc/udm/udmcfg.yaml \n", para5GData["MCC"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mnc: 01/mnc: %s/g\" /usr/local/etc/udm/udmcfg.yaml \n", para5GData["MNC"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mnc001.mcc001/mnc%s.mcc%s/g\" /usr/local/etc/udm/udmcfg.yaml \n", para5GData["MNC_DOMAIN"], para5GData["MCC"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mnc001.mcc001/mnc%s.mcc%s/g\" /usr/local/etc/udm/as.yaml \n", para5GData["MNC_DOMAIN"], para5GData["MCC"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mnc001.mcc001/mnc%s.mcc%s/g\" /usr/local/etc/udm/scscfSet.yaml \n", para5GData["MNC_DOMAIN"], para5GData["MCC"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/sst: 1/sst: %s/g\" /usr/local/etc/udm/nssai.yaml \n", para5GData["SST"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/sst: 1/sst: %s/g\" /usr/local/etc/udm/snssai.yaml \n", para5GData["SST"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/sd: 000001/sd: %s/g\" /usr/local/etc/udm/nssai.yaml \n", para5GData["SD"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/sd: 000001/sd: %s/g\" /usr/local/etc/udm/snssai.yaml \n", para5GData["SD"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/dnn: internet/dnn: %s/g\" /usr/local/etc/udm/snssai.yaml \n", para5GData["DNN_DATA"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/dnn: ims/dnn: %s/g\" /usr/local/etc/udm/snssai.yaml \n", para5GData["DNN_IMS"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/serviceSelection: 'internet'/serviceSelection: '%s'/g\" /usr/local/etc/udm/epsApn.yaml \n", para5GData["DNN_DATA"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/serviceSelection: 'ims'/serviceSelection: '%s'/g\" /usr/local/etc/udm/epsApn.yaml \n", para5GData["DNN_IMS"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/dnn: internet/dnn: %s/g\" /usr/local/etc/udm/dnn.yaml \n", para5GData["DNN_DATA"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/dnn: ims/dnn: %s/g\" /usr/local/etc/udm/dnn.yaml \n", para5GData["DNN_IMS"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.8.110/%s/g\" /usr/local/etc/udm/as.yaml \n", para5GData["SIP_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s udm' /etc/hosts || echo '%s udm' | sudo tee -a /etc/hosts \n", para5GData["UDM_IP"], para5GData["UDM_IP"])) - } - // SMF配置修改 - if neTypeLower == "smf" { - cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/smf/default/smf_conf.yaml /usr/local/etc/smf/smf_conf.yaml \n") - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.8.110/%s/g\" /usr/local/etc/smf/smf_conf.yaml \n", para5GData["SIP_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.120/%s/g\" /usr/local/etc/smf/smf_conf.yaml \n", para5GData["AMF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.140/%s/g\" /usr/local/etc/smf/smf_conf.yaml \n", para5GData["UDM_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.150/%s/g\" /usr/local/etc/smf/smf_conf.yaml \n", para5GData["SMF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.160/%s/g\" /usr/local/etc/smf/smf_conf.yaml \n", para5GData["PCF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.180/%s/g\" /usr/local/etc/smf/smf_conf.yaml \n", para5GData["NRF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.190/%s/g\" /usr/local/etc/smf/smf_conf.yaml \n", para5GData["UPF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s|10.2.1.0/24|%s|g\" /usr/local/etc/smf/smf_conf.yaml \n", para5GData["UE_POOL"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/internet/%s/g\" /usr/local/etc/smf/smf_conf.yaml \n", para5GData["DNN_DATA"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s smf' /etc/hosts || echo '%s smf' | sudo tee -a /etc/hosts \n", para5GData["SMF_IP"], para5GData["SMF_IP"])) - } - // PCF配置修改 - if neTypeLower == "pcf" { - cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/pcf/default/pcfcfg.yaml /usr/local/etc/pcf/pcfcfg.yaml \n") - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.120/%s/g\" /usr/local/etc/pcf/pcfcfg.yaml \n", para5GData["AMF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.140/%s/g\" /usr/local/etc/pcf/pcfcfg.yaml \n", para5GData["UDM_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.160/%s/g\" /usr/local/etc/pcf/pcfcfg.yaml \n", para5GData["PCF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.180/%s/g\" /usr/local/etc/pcf/pcfcfg.yaml \n", para5GData["NRF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.210/%s/g\" /usr/local/etc/pcf/pcfcfg.yaml \n", para5GData["NEF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mcc: 001/mcc: %s/g\" /usr/local/etc/pcf/pcfcfg.yaml \n", para5GData["MCC"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mnc: 01/mnc: %s/g\" /usr/local/etc/pcf/pcfcfg.yaml \n", para5GData["MNC"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mnc001.mcc001/mnc%s.mcc%s/g\" /usr/local/etc/pcf/pcfcfg.yaml \n", para5GData["MNC_DOMAIN"], para5GData["MCC"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s pcf' /etc/hosts || echo '%s pcf' | sudo tee -a /etc/hosts \n", para5GData["PCF_IP"], para5GData["PCF_IP"])) - } - - // NSSF配置修改 - if neTypeLower == "nssf" { - cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/nssf/default/nssfcfg.yaml /usr/local/etc/nssf/nssfcfg.yaml \n") - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.170/%s/g\" /usr/local/etc/nssf/nssfcfg.yaml \n", para5GData["NSSF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.180/%s/g\" /usr/local/etc/nssf/nssfcfg.yaml \n", para5GData["NRF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mcc: 001/mcc: %s/g\" /usr/local/etc/nssf/nssfcfg.yaml \n", para5GData["MCC"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mnc: 01/mnc: %s/g\" /usr/local/etc/nssf/nssfcfg.yaml \n", para5GData["MNC"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s nssf' /etc/hosts || echo '%s nssf' | sudo tee -a /etc/hosts \n", para5GData["NSSF_IP"], para5GData["NSSF_IP"])) - } - // NRF配置修改 - if neTypeLower == "nrf" { - cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/nrf/default/nrfcfg.yaml /usr/local/etc/nrf/nrfcfg.yaml \n") - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.180/%s/g\" /usr/local/etc/nrf/nrfcfg.yaml \n", para5GData["NRF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mcc: 001/mcc: %s/g\" /usr/local/etc/nrf/nrfcfg.yaml \n", para5GData["MCC"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mnc: 01/mnc: %s/g\" /usr/local/etc/nrf/nrfcfg.yaml \n", para5GData["MNC"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s nrf' /etc/hosts || echo '%s nrf' | sudo tee -a /etc/hosts \n", para5GData["NRF_IP"], para5GData["NRF_IP"])) - } - - // UPF配置修改 - if neTypeLower == "upf" { - cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/upf/default/upfcfg.yaml /usr/local/etc/upf/upfcfg.yaml \n") - cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/upf/default/upfForwarder_1.yaml /usr/local/etc/upf/upfForwarder_1.yaml \n") - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.190/%s/g\" /usr/local/etc/upf/upfcfg.yaml \n", para5GData["UPF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/localhost/%s/g\" /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["UPF_IP"])) - // UE - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i '/- interfaceType: \"N6\"/,/ueIpv4: 10.2.1.0/s/ueIpv4: 10.2.1.0/ueIpv4: %s/' /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["UE_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i '/- interfaceType: \"N6\"/,/ueIpv4Mask: 255.255.255.0/s/ueIpv4Mask: 255.255.255.0/ueIpv4Mask: %s/' /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["UE_MASK"])) - // N3 - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.8.190/%s/g\" /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["N3_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i '/- interfaceType: \"N3\"/,/ipv4Mask: 255.255.240.0/s/ipv4Mask: 255.255.240.0/ipv4Mask: %s/' /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["N3_MASK"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i '/- interfaceType: \"N3\"/,/gatewayIpv4: 192.168.1.254/s/gatewayIpv4: 192.168.1.254/gatewayIpv4: %s/' /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["N3_GW"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i '/- interfaceType: \"N3\"/,/interfacePCI: \"0000:00:00.0\"/s/interfacePCI: \"0000:00:00.0\"/interfacePCI: \"%s\"/' /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["N3_PCI"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i '/- interfaceType: \"N3\"/,/macAddr: \"00:00:00:00:00:00\"/s/macAddr: \"00:00:00:00:00:00\"/macAddr: \"%s\"/' /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["N3_MAC"])) - // 标准版 N6 - if para5GData["UPF_TYPE"] == "StandardUPF" { - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.8.191/%s/g\" /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["N6_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i '/- interfaceType: \"N6\"/,/ipv4Mask: 255.255.240.0/s/ipv4Mask: 255.255.240.0/ipv4Mask: %s/' /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["N6_MASK"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i '/- interfaceType: \"N6\"/,/gatewayIpv4: 192.168.1.254/s/gatewayIpv4: 192.168.1.254/gatewayIpv4: %s/' /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["N6_GW"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i '/- interfaceType: \"N6\"/,/interfacePCI: \"0000:00:00.0\"/s/interfacePCI: \"0000:00:00.0\"/interfacePCI: \"%s\"/' /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["N6_PCI"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i '/- interfaceType: \"N6\"/,/macAddr: \"00:00:00:00:00:00\"/s/macAddr: \"00:00:00:00:00:00\"/macAddr: \"%s\"/' /usr/local/etc/upf/upfForwarder_1.yaml \n", para5GData["N6_MAC"])) - // 路由 - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo ip route add '%s/%s' via '%s' \n", para5GData["UE_IP"], para5GData["UE_CIDR"], para5GData["N6_IP"])) - } - // 轻量版 - if para5GData["UPF_TYPE"] == "LightUPF" { - cmdStrArr = append(cmdStrArr, "sudo sed -i \"s/192.168.8.191/0.0.0.0/g\" /usr/local/etc/upf/upfForwarder_1.yaml \n") - cmdStrArr = append(cmdStrArr, "sudo sed -i \"s/type: upfd/type: tun/g\" /usr/local/etc/upf/upfForwarder_1.yaml \n") - cmdStrArr = append(cmdStrArr, "sudo sed -i 's/driverType: vmxnet3/driverType: \"\"/g' /usr/local/etc/upf/upfForwarder_1.yaml \n") - } - cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s upf' /etc/hosts || echo '%s upf' | sudo tee -a /etc/hosts \n", para5GData["UPF_IP"], para5GData["UPF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s upfn3' /etc/hosts || echo '%s upfn3' | sudo tee -a /etc/hosts \n", para5GData["N3_IP"], para5GData["N3_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s upfn6' /etc/hosts || echo '%s upfn6' | sudo tee -a /etc/hosts \n", para5GData["N6_IP"], para5GData["N6_IP"])) - } - - // LMF配置修改 - 已不再维护,导致激活License失败 - // NEF配置修改 - SNMP无需License - if neTypeLower == "nef" { - cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/nef/default/nef_conf.yaml /usr/local/etc/nef/nef_conf.yaml \n") - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.110/%s/g\" /usr/local/etc/nef/nef_conf.yaml \n", para5GData["IMS_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.120/%s/g\" /usr/local/etc/nef/nef_conf.yaml \n", para5GData["AMF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.130/%s/g\" /usr/local/etc/nef/nef_conf.yaml \n", para5GData["AUSF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.140/%s/g\" /usr/local/etc/nef/nef_conf.yaml \n", para5GData["UDM_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.150/%s/g\" /usr/local/etc/nef/nef_conf.yaml \n", para5GData["SMF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.160/%s/g\" /usr/local/etc/nef/nef_conf.yaml \n", para5GData["PCF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.170/%s/g\" /usr/local/etc/nef/nef_conf.yaml \n", para5GData["NSSF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.180/%s/g\" /usr/local/etc/nef/nef_conf.yaml \n", para5GData["NRF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.190/%s/g\" /usr/local/etc/nef/nef_conf.yaml \n", para5GData["UPF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.210/%s/g\" /usr/local/etc/nef/nef_conf.yaml \n", para5GData["NEF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s nef' /etc/hosts || echo '%s nef' | sudo tee -a /etc/hosts \n", para5GData["NEF_IP"], para5GData["NEF_IP"])) - } - - // MME配置修改 - 4G - if neTypeLower == "mme" { - cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/mme/default/mme.conf /usr/local/etc/mme/mme.conf \n") - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.120/%s/g\" /usr/local/etc/mme/mme.conf \n", para5GData["AMF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/172.16.5.150/%s/g\" /usr/local/etc/mme/mme.conf \n", para5GData["SMF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s|192.168.8.220/20|%s|g\" /usr/local/etc/mme/mme.conf \n", para5GData["S1_MMEIP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s|172.16.5.220/24|%s|g\" /usr/local/etc/mme/mme.conf \n", para5GData["S11_MMEIP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s|172.16.5.220|%s|g\" /usr/local/etc/mme/mme.conf \n", para5GData["MME_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s|172.16.5.221/24|%s|g\" /usr/local/etc/mme/mme.conf \n", para5GData["S10_MMEIP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/mnc001.mcc001/mnc%s.mcc%s/g\" /usr/local/etc/mme/mme.conf \n", para5GData["MNC_DOMAIN"], para5GData["MCC"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/\"00101\"/\"%s%s\"/g\" /usr/local/etc/mme/mme.conf \n", para5GData["MCC"], para5GData["MNC"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i 's/MCC=\"001\"/MCC=\"%s\"/g' /usr/local/etc/mme/mme.conf \n", para5GData["MCC"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i 's/MCC = \"001\"/MCC = \"%s\"/g' /usr/local/etc/mme/mme.conf \n", para5GData["MCC"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i 's/MNC=\"01\";/MNC=\"%s\";/g' /usr/local/etc/mme/mme.conf \n", para5GData["MNC"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i 's/MNC = \"01\";/MNC = \"%s\";/g' /usr/local/etc/mme/mme.conf \n", para5GData["MNC"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i 's/TAC = \"1\";/TAC = \"%s\";/g' /usr/local/etc/mme/mme.conf \n", para5GData["TAC"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i 's/TAC = 1;/TAC = %s;/g' /usr/local/etc/mme/mme.conf \n", para5GData["TAC"])) - // SMF开启 - cmdStrArr = append(cmdStrArr, "sudo sed -i \"/^ *gxcfg:/,/^ *[^ ]/{s/enable: false/enable: true/;b};\" /usr/local/etc/smf/smf_conf.yaml \n") - cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s mme' /etc/hosts || echo '%s mme' | sudo tee -a /etc/hosts \n", para5GData["MME_IP"], para5GData["MME_IP"])) - } - // N3IWF配置修改 - if neTypeLower == "n3iwf" { - cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/n3iwf/default/n3iwfcfg.yaml /usr/local/etc/n3iwf/n3iwfcfg.yaml \n") - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/MCC: 001/MCC: %s/g\" /usr/local/etc/n3iwf/n3iwfcfg.yaml \n", para5GData["MCC"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/MNC: 01/MNC: %s/g\" /usr/local/etc/n3iwf/n3iwfcfg.yaml \n", para5GData["MNC"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.12.161/%s/g\" /usr/local/etc/n3iwf/n3iwfcfg.yaml \n", para5GData["N3IWF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.12.160/%s/g\" /usr/local/etc/n3iwf/n3iwfcfg.yaml \n", para5GData["N3IWF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.8.27/%s/g\" /usr/local/etc/n3iwf/n3iwfcfg.yaml \n", para5GData["UDM_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.1.239/%s/g\" /usr/local/etc/n3iwf/n3iwfcfg.yaml \n", para5GData["SMF_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.8.22/%s/g\" /usr/local/etc/n3iwf/n3iwfcfg.yaml \n", para5GData["N2_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.1.161/%s/g\" /usr/local/etc/n3iwf/n3iwfcfg.yaml \n", para5GData["N3_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/192.168.1.160/%s/g\" /usr/local/etc/n3iwf/n3iwfcfg.yaml \n", para5GData["N6_IP"])) - cmdStrArr = append(cmdStrArr, fmt.Sprintf("grep -qxF '%s n3iwf' /etc/hosts || echo '%s n3iwf' | sudo tee -a /etc/hosts \n", para5GData["N3IWF_IP"], para5GData["N3IWF_IP"])) - } - - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo service %s restart \n", neTypeLower)) - // 30s后停止服务 - // cmdStrArr = append(cmdStrArr, fmt.Sprintf("nohup sh -c \"sleep 30s && sudo service %s stop\" > /dev/null 2>&1 & \n", neTypeLower)) - } else { - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo service %s stop \n", neTypeLower)) - cmdStrArr = append(cmdStrArr, pkgCmdStr+" \n") - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo service %s restart \n", neTypeLower)) - } - } - - // 安装操作有redis安装时需要重启 - if action == "install" && (neTypeLower == "ims" || neTypeLower == "udm") { - // adb - if strings.Contains(pkgCmdStr, "adb") { - para5GData := NewNeInfoImpl.Para5GData - cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/adb/default/adb.conf /usr/local/etc/adb/adb.conf \n") - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/bind 127.0.0.1/bind %s/g\" /usr/local/etc/adb/adb.conf \n", para5GData["DB_IP"])) - cmdStrArr = append(cmdStrArr, "sudo service adb restart \n") - } - // kvdb - if strings.Contains(pkgCmdStr, "kvdb") { - para5GData := NewNeInfoImpl.Para5GData - cmdStrArr = append(cmdStrArr, "sudo cp /usr/local/etc/kvdb/default/kvdb.conf /usr/local/etc/kvdb/kvdb.conf \n") - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo sed -i \"s/bind 127.0.0.1/bind %s/g\" /usr/local/etc/kvdb/kvdb.conf \n", para5GData["DB_IP"])) - cmdStrArr = append(cmdStrArr, "sudo service kvdb restart \n") - } - } - - // 删除软件包 - cmdStrArr = append(cmdStrArr, fmt.Sprintf("sudo rm %s \n", strings.Join(neFilePaths, " "))) - // 结束 - cmdStrArr = append(cmdStrArr, fmt.Sprintf("echo '%s' \n", okFlagStr)) - - return okFlagStr, cmdStrArr, nil -} - -// operateDome 操作版本-执行阶段 -func (r *NeVersionImpl) operateRun(sshClient *ssh.ConnSSH, preinput map[string]string, cmdStrArr []string, neType string, okFlagStr string) (string, error) { - // ssh连接会话 - clientSession, err := sshClient.NewClientSession(127, 42) - if err != nil { - return "", fmt.Errorf("neinfo ssh client session new err") - } - defer clientSession.Close() - - firstRead := true // 首次命令进行记录日志信息 - commandLineText := "" // 日志信息 - done := make(chan bool) // 完成信号 - // 超时退出 120s - timeoutTicker := time.NewTicker(120 * time.Second) - defer timeoutTicker.Stop() - // 实时读取SSH消息直接输出 - msTicker := time.NewTicker(100 * time.Millisecond) - defer msTicker.Stop() - go func() { - for { - select { - case <-timeoutTicker.C: - done <- true - return - case <-msTicker.C: - outputByte := clientSession.Read() - if len(outputByte) > 0 { - outputStr := string(outputByte) - // 非首次进行记录命令 - if !firstRead { - commandLineText += outputStr - } - - // IMS预输入 - if neType == "IMS" { - // IMS包 P/I/S-CSCF Config 配置覆盖 - if strings.Contains(outputStr, "(P/I/S-CSCF Config)? ") { - if pisCSCF, ok := preinput["pisCSCF"]; ok && pisCSCF != "" { - clientSession.Write(fmt.Sprintf("%s \n", pisCSCF)) - } else { - clientSession.Write("y \n") - } - continue - } - // MF包 etc下目录覆盖 - if strings.Contains(outputStr, "/usr/local/etc/mf directory? (Yes/No, default: No)") { - if pisCSCF, ok := preinput["updateMFetc"]; ok && pisCSCF != "" { - clientSession.Write(fmt.Sprintf("%s \n", pisCSCF)) - } else { - clientSession.Write("No \n") - } - continue - } - // MF包 share下目录覆盖 - if strings.Contains(outputStr, "/usr/local/share/mf directory? (Yes/No, default: No)") { - if pisCSCF, ok := preinput["updateMFshare"]; ok && pisCSCF != "" { - clientSession.Write(fmt.Sprintf("%s \n", pisCSCF)) - } else { - clientSession.Write("No \n") - } - continue - } - } - - // 命令终止符后继续执行命令 - suffix := strings.HasSuffix(outputStr, "~]# ") || strings.HasSuffix(outputStr, "~$ ") - if len(cmdStrArr) > 0 && suffix { - if firstRead { - firstRead = false - } - shiftElement := cmdStrArr[0] // 获取第一个元素 - cmdStrArr = cmdStrArr[1:] // 将第一个元素从切片中移除 - clientSession.Write(shiftElement) - continue - } - // 最后输出的退出标记 - if strings.LastIndex(outputStr, okFlagStr) > 5 { - done <- true - break - } - } - } - } - }() - // 等待写入协程完成 - <-done - - return commandLineText, nil -} - -// operateDome 操作版本-完成阶段 -func (r *NeVersionImpl) operateDome(action string, neVersion model.NeVersion) error { - if action == "install" { - // 网元信息 - neInfo := NewNeInfoImpl.SelectNeInfoByNeTypeAndNeID(neVersion.NeType, neVersion.NeId) - if neInfo.NeId != neVersion.NeId { - return fmt.Errorf("error found neinfo") - } - // ========= 网元OAM配置文件 start ========== - if err := NewNeInfoImpl.NeConfOAMSync(neInfo, nil, true); err != nil { - return fmt.Errorf("error wirte OAM file info") - } - // ========= 网元OAM配置文件 end =========== - } - - // 更新Version - verInfo := r.SelectByNeTypeAndNeID(neVersion.NeType, neVersion.NeId) - if verInfo.NeId == neVersion.NeId { - curName := verInfo.Name - curVersion := verInfo.Version - curPath := verInfo.Path - if action == "install" { - verInfo.Name = neVersion.NewName - verInfo.Version = neVersion.NewVersion - verInfo.Path = neVersion.NewPath - verInfo.PreName = "-" - verInfo.PreVersion = "-" - verInfo.PrePath = "-" - verInfo.NewName = "-" - verInfo.NewVersion = "-" - verInfo.NewPath = "-" - } - if action == "upgrade" { - verInfo.Name = neVersion.NewName - verInfo.Version = neVersion.NewVersion - verInfo.Path = neVersion.NewPath - verInfo.PreName = curName - verInfo.PreVersion = curVersion - verInfo.PrePath = curPath - verInfo.NewName = "-" - verInfo.NewVersion = "-" - verInfo.NewPath = "-" - } - if action == "rollback" { - verInfo.Name = neVersion.PreName - verInfo.Version = neVersion.PreVersion - verInfo.Path = neVersion.PrePath - verInfo.PreName = curName - verInfo.PreVersion = curVersion - verInfo.PrePath = curPath - } - - verInfo.Status = "1" - NewNeVersionImpl.Update(verInfo) - } - return nil -} diff --git a/src/modules/system/controller/sys_config.go b/src/modules/system/controller/sys_config.go index e4a02837..5809dca6 100644 --- a/src/modules/system/controller/sys_config.go +++ b/src/modules/system/controller/sys_config.go @@ -214,7 +214,7 @@ func (s *SysConfigController) ConfigKey(c *gin.Context) { } key := s.sysConfigService.SelectConfigValueByKey(configKey) if key != "" { - c.JSON(200, result.OkData(key)) + c.JSON(200, result.OkData(i18n.TKey(language, key))) return } c.JSON(200, result.Err(nil)) diff --git a/src/modules/system/controller/sys_dict_data.go b/src/modules/system/controller/sys_dict_data.go index 8e3d56bc..c3eec5f3 100644 --- a/src/modules/system/controller/sys_dict_data.go +++ b/src/modules/system/controller/sys_dict_data.go @@ -20,18 +20,16 @@ import ( // 实例化控制层 SysDictDataController 结构体 var NewSysDictData = &SysDictDataController{ - sysDictDataService: service.NewSysDictDataImpl, - sysDictTypeService: service.NewSysDictTypeImpl, + sysDictDataService: service.NewSysDictData, + sysDictTypeService: service.NewSysDictType, } // 字典类型对应的字典数据信息 // // PATH /system/dict/data type SysDictDataController struct { - // 字典数据服务 - sysDictDataService service.ISysDictData - // 字典类型服务 - sysDictTypeService service.ISysDictType + sysDictDataService *service.SysDictData // 字典数据服务 + sysDictTypeService *service.SysDictType // 字典类型服务 } // 字典数据列表 diff --git a/src/modules/system/controller/sys_dict_type.go b/src/modules/system/controller/sys_dict_type.go index 1af741e5..939cd3e3 100644 --- a/src/modules/system/controller/sys_dict_type.go +++ b/src/modules/system/controller/sys_dict_type.go @@ -21,15 +21,14 @@ import ( // 实例化控制层 SysDictTypeController 结构体 var NewSysDictType = &SysDictTypeController{ - sysDictTypeService: service.NewSysDictTypeImpl, + sysDictTypeService: service.NewSysDictType, } // 字典类型信息 // // PATH /system/dict/type type SysDictTypeController struct { - // 字典类型服务 - sysDictTypeService service.ISysDictType + sysDictTypeService *service.SysDictType // 字典类型服务 } // 字典类型列表 diff --git a/src/modules/system/controller/sys_log_login.go b/src/modules/system/controller/sys_log_login.go index 59e9a73d..0444d327 100644 --- a/src/modules/system/controller/sys_log_login.go +++ b/src/modules/system/controller/sys_log_login.go @@ -22,7 +22,7 @@ import ( // 实例化控制层 SysLogLoginController 结构体 var NewSysLogLogin = &SysLogLoginController{ sysLogLoginService: service.NewSysLogLoginImpl, - accountService: commonService.NewAccountImpl, + accountService: commonService.NewAccount, } // 系统登录日志信息 @@ -31,8 +31,7 @@ var NewSysLogLogin = &SysLogLoginController{ type SysLogLoginController struct { // 系统登录日志服务 sysLogLoginService service.ISysLogLogin - // 账号身份操作服务 - accountService commonService.IAccount + accountService *commonService.Account // 账号身份操作服务 } // 系统登录日志列表 diff --git a/src/modules/system/controller/sys_profile.go b/src/modules/system/controller/sys_profile.go index 55095bcf..34626a2c 100644 --- a/src/modules/system/controller/sys_profile.go +++ b/src/modules/system/controller/sys_profile.go @@ -157,6 +157,7 @@ func (s *SysProfileController) UpdateProfile(c *gin.Context) { PhoneNumber: body.PhoneNumber, Email: body.Email, Sex: body.Sex, + Remark: loginUser.User.Remark, } rows := s.sysUserService.UpdateUser(sysUser) if rows > 0 { @@ -229,9 +230,13 @@ func (s *SysProfileController) UpdatePwd(c *gin.Context) { // 修改新密码 sysUser := model.SysUser{ - UserID: userId, - UpdateBy: userName, - Password: body.NewPassword, + UserID: userId, + UpdateBy: userName, + Password: body.NewPassword, + Sex: user.Sex, + PhoneNumber: user.PhoneNumber, + Email: user.Email, + Remark: user.Remark, } rows := s.sysUserService.UpdateUser(sysUser) if rows > 0 { @@ -268,9 +273,13 @@ func (s *SysProfileController) Avatar(c *gin.Context) { // 更新头像地址 sysUser := model.SysUser{ - UserID: loginUser.UserID, - UpdateBy: loginUser.User.UserName, - Avatar: filePath, + UserID: loginUser.UserID, + UpdateBy: loginUser.User.UserName, + Avatar: filePath, + Sex: loginUser.User.Sex, + PhoneNumber: loginUser.User.PhoneNumber, + Email: loginUser.User.Email, + Remark: loginUser.User.Remark, } rows := s.sysUserService.UpdateUser(sysUser) if rows > 0 { diff --git a/src/modules/system/controller/sys_role.go b/src/modules/system/controller/sys_role.go index 2300679c..c291c3fe 100644 --- a/src/modules/system/controller/sys_role.go +++ b/src/modules/system/controller/sys_role.go @@ -24,7 +24,7 @@ import ( var NewSysRole = &SysRoleController{ sysRoleService: service.NewSysRoleImpl, sysUserService: service.NewSysUserImpl, - sysDictDataService: service.NewSysDictDataImpl, + sysDictDataService: service.NewSysDictData, } // 角色信息 @@ -34,9 +34,8 @@ type SysRoleController struct { // 角色服务 sysRoleService service.ISysRole // 用户服务 - sysUserService service.ISysUser - // 字典数据服务 - sysDictDataService service.ISysDictData + sysUserService service.ISysUser + sysDictDataService *service.SysDictData // 字典数据服务 } // 角色列表 diff --git a/src/modules/system/controller/sys_user.go b/src/modules/system/controller/sys_user.go index 546ac8ff..34f6834a 100644 --- a/src/modules/system/controller/sys_user.go +++ b/src/modules/system/controller/sys_user.go @@ -28,7 +28,7 @@ var NewSysUser = &SysUserController{ sysUserService: service.NewSysUserImpl, sysRoleService: service.NewSysRoleImpl, sysPostService: service.NewSysPostImpl, - sysDictDataService: service.NewSysDictDataImpl, + sysDictDataService: service.NewSysDictData, sysConfigService: service.NewSysConfigImpl, } @@ -41,9 +41,8 @@ type SysUserController struct { // 角色服务 sysRoleService service.ISysRole // 岗位服务 - sysPostService service.ISysPost - // 字典数据服务 - sysDictDataService service.ISysDictData + sysPostService service.ISysPost + sysDictDataService *service.SysDictData // 字典数据服务 // 参数配置服务 sysConfigService service.ISysConfig } @@ -412,9 +411,13 @@ func (s *SysUserController) ResetPwd(c *gin.Context) { userName := ctx.LoginUserToUserName(c) info := model.SysUser{ - UserID: body.UserID, - Password: body.Password, - UpdateBy: userName, + UserID: body.UserID, + Password: body.Password, + UpdateBy: userName, + Sex: user.Sex, + PhoneNumber: user.PhoneNumber, + Email: user.Email, + Remark: user.Remark, } rows := s.sysUserService.UpdateUser(info) if rows > 0 { @@ -455,9 +458,13 @@ func (s *SysUserController) Status(c *gin.Context) { userName := ctx.LoginUserToUserName(c) info := model.SysUser{ - UserID: body.UserID, - Status: body.Status, - UpdateBy: userName, + UserID: body.UserID, + Status: body.Status, + UpdateBy: userName, + Sex: user.Sex, + PhoneNumber: user.PhoneNumber, + Email: user.Email, + Remark: user.Remark, } rows := s.sysUserService.UpdateUser(info) if rows > 0 { @@ -571,23 +578,11 @@ func (s *SysUserController) Export(c *gin.Context) { // // GET /importTemplate func (s *SysUserController) Template(c *gin.Context) { + fileName := fmt.Sprintf("user_import_template_%d.xlsx", time.Now().UnixMilli()) + // 多语言处理 language := ctx.AcceptLanguage(c) - // 登录用户 - loginUser, err := ctx.LoginUser(c) - if err != nil { - c.JSON(200, result.ErrMsg(i18n.TKey(language, err.Error()))) - return - } - // 根据角色指定导入模板 - fileKey := "user" - roles := loginUser.User.Roles - if len(roles) == 1 && roles[0].RoleKey == "teacher" { - fileKey = "student" - } - - fileName := fmt.Sprintf("%s_import_template_%d.xlsx", fileKey, time.Now().UnixMilli()) - asserPath := fmt.Sprintf("assets/template/excel/%s_import_template_%s.xlsx", fileKey, language) + asserPath := fmt.Sprintf("assets/template/excel/user_import_template_%s.xlsx", language) // 从 embed.FS 中读取默认配置文件内容 assetsDir := config.GetAssetsDirFS() @@ -611,12 +606,6 @@ func (s *SysUserController) Template(c *gin.Context) { // POST /importData func (s *SysUserController) ImportData(c *gin.Context) { language := ctx.AcceptLanguage(c) - // 登录用户 - loginUser, err := ctx.LoginUser(c) - if err != nil { - c.JSON(200, result.ErrMsg(i18n.TKey(language, err.Error()))) - return - } // 允许进行更新 updateSupport := c.PostForm("updateSupport") // 上传的文件 @@ -640,12 +629,6 @@ func (s *SysUserController) ImportData(c *gin.Context) { return } - // 根据角色指定导入模板 - roleKey := "" - roles := loginUser.User.Roles - if len(roles) == 1 && roles[0].RoleKey == "teacher" { - roleKey = "student" - } // 获取操作人名称 operName := ctx.LoginUserToUserName(c) isUpdateSupport := parse.Boolean(updateSupport) @@ -683,49 +666,77 @@ func (s *SysUserController) ImportData(c *gin.Context) { sysUserSex := "0" for _, v := range dictSysUserSex { label := i18n.TKey(language, v.DictLabel) - if row["D"] == label { + if row["F"] == label { sysUserSex = v.DictValue break } } // 用户状态 sysUserStatus := common.STATUS_NO - if row["E"] == "正常" || row["G"] == "Normal" { + if row["G"] == "正常" || row["G"] == "Normal" { sysUserStatus = common.STATUS_YES } - - sysUserRole := "" // 用户角色 - sysUserPost := "" // 用户岗位 - sysUserDept := "101" // 用户部门 101未指定 - if roleKey == "student" { - sysUserRole = "4" - sysUserPost = "3" - sysUserDept = loginUser.DeptID - } - if v, ok := row["F"]; ok && v != "" { - if v == "学生" || v == "Student" { - sysUserRole = "4" - sysUserPost = "3" - } else if v == "教师" || v == "Teacher" { - sysUserRole = "3" - sysUserPost = "2" + // 用户角色 拿编号 + sysUserRole := "" + if v, ok := row["H"]; ok && v != "" { + sysUserRole = strings.SplitN(v, "-", 2)[0] + if sysUserRole == "1" { + sysUserRole = "" } } - if v, ok := row["G"]; ok && v != "" && v != "100" { - sysUserDept = v - } // 构建用户实体信息 newSysUser := model.SysUser{ - UserType: "sys", - Password: initPassword, - DeptID: sysUserDept, - UserName: row["B"], - NickName: row["C"], - Status: sysUserStatus, - Sex: sysUserSex, - RoleIDs: []string{sysUserRole}, - PostIDs: []string{sysUserPost}, + UserType: "sys", + Password: initPassword, + DeptID: row["I"], + UserName: row["B"], + NickName: row["C"], + PhoneNumber: row["E"], + Email: row["D"], + Status: sysUserStatus, + Sex: sysUserSex, + RoleIDs: []string{sysUserRole}, + } + + // 检查手机号码格式并判断是否唯一 + if newSysUser.PhoneNumber != "" { + if regular.ValidMobile(newSysUser.PhoneNumber) { + uniquePhone := s.sysUserService.CheckUniquePhone(newSysUser.PhoneNumber, "") + if !uniquePhone { + // 用户编号:%s 手机号码 %s 已存在 + msg := i18n.TTemplate(language, "user.import.phoneExist", map[string]any{"id": row["A"], "phone": newSysUser.PhoneNumber}) + failureNum++ + failureMsgArr = append(failureMsgArr, msg) + continue + } + } else { + // 用户编号:%s 手机号码 %s 格式错误 + msg := i18n.TTemplate(language, "user.import.phoneFormat", map[string]any{"id": row["A"], "phone": newSysUser.PhoneNumber}) + failureNum++ + failureMsgArr = append(failureMsgArr, msg) + continue + } + } + + // 检查邮箱格式并判断是否唯一 + if newSysUser.Email != "" { + if regular.ValidEmail(newSysUser.Email) { + uniqueEmail := s.sysUserService.CheckUniqueEmail(newSysUser.Email, "") + if !uniqueEmail { + // 用户编号:%s 用户邮箱 %s 已存在 + msg := i18n.TTemplate(language, "user.import.emailExist", map[string]any{"id": row["A"], "email": newSysUser.Email}) + failureNum++ + failureMsgArr = append(failureMsgArr, msg) + continue + } + } else { + // 用户编号:%s 用户邮箱 %s 格式错误 + msg := i18n.TTemplate(language, "user.import.emailFormat", map[string]any{"id": row["A"], "email": newSysUser.Email}) + failureNum++ + failureMsgArr = append(failureMsgArr, msg) + continue + } } // 验证是否存在这个用户 diff --git a/src/modules/system/repository/sys_dict_data.impl.go b/src/modules/system/repository/sys_dict_data.impl.go index 3782a14c..5231a2a6 100644 --- a/src/modules/system/repository/sys_dict_data.impl.go +++ b/src/modules/system/repository/sys_dict_data.impl.go @@ -13,7 +13,7 @@ import ( ) // 实例化数据层 SysDictDataImpl 结构体 -var NewSysDictDataImpl = &SysDictDataImpl{ +var NewSysDictData = &SysDictDataImpl{ selectSql: `select dict_code, dict_sort, dict_label, dict_value, dict_type, tag_class, tag_type, status, create_by, create_time, remark from sys_dict_data`, diff --git a/src/modules/system/repository/sys_dict_type.impl.go b/src/modules/system/repository/sys_dict_type.impl.go index 1da01121..db321360 100644 --- a/src/modules/system/repository/sys_dict_type.impl.go +++ b/src/modules/system/repository/sys_dict_type.impl.go @@ -13,7 +13,7 @@ import ( ) // 实例化数据层 SysDictTypeImpl 结构体 -var NewSysDictTypeImpl = &SysDictTypeImpl{ +var NewSysDictType = &SysDictTypeImpl{ selectSql: `select dict_id, dict_name, dict_type, status, create_by, create_time, remark from sys_dict_type`, diff --git a/src/modules/system/repository/sys_user.impl.go b/src/modules/system/repository/sys_user.impl.go index 0d880ce2..b2403043 100644 --- a/src/modules/system/repository/sys_user.impl.go +++ b/src/modules/system/repository/sys_user.impl.go @@ -138,7 +138,7 @@ func (r *SysUserImpl) SelectUserPage(query map[string]any, dataScopeSQL string) params = append(params, v) } if v, ok := query["userName"]; ok && v != "" { - conditions = append(conditions, "u.user_name like concat(concat('%', ?), '%')") + conditions = append(conditions, "u.user_name like concat(?, '%')") params = append(params, v) } if v, ok := query["status"]; ok && v != "" { @@ -513,7 +513,9 @@ func (r *SysUserImpl) UpdateUser(sysUser model.SysUser) int64 { func (r *SysUserImpl) DeleteUserByIds(userIds []string) int64 { placeholder := repo.KeyPlaceholderByQuery(len(userIds)) username := "CASE WHEN user_name = '' THEN user_name WHEN LENGTH(user_name) >= 36 THEN CONCAT('del_', SUBSTRING(user_name, 5, 36)) ELSE CONCAT('del_', user_name) END" - sql := fmt.Sprintf("update sys_user set del_flag = '1', user_name = %s where user_id in (%s)", username, placeholder) + email := "CASE WHEN email = '' THEN email WHEN LENGTH(email) >= 64 THEN CONCAT('del_', SUBSTRING(email, 5, 64)) ELSE CONCAT('del_', email) END" + phonenumber := "CASE WHEN phonenumber = '' THEN phonenumber WHEN LENGTH(phonenumber) >= 16 THEN CONCAT('del_', SUBSTRING(phonenumber, 5, 16)) ELSE CONCAT('del_', phonenumber) END" + sql := fmt.Sprintf("update sys_user set del_flag = '1', user_name = %s, email = %s, phonenumber = %s where user_id in (%s)", username, email, phonenumber, placeholder) parameters := repo.ConvertIdsSlice(userIds) results, err := datasource.ExecDB("", sql, parameters) if err != nil { diff --git a/src/modules/system/service/sys_dict_data.go b/src/modules/system/service/sys_dict_data.go index 716eb2ac..d5581078 100644 --- a/src/modules/system/service/sys_dict_data.go +++ b/src/modules/system/service/sys_dict_data.go @@ -1,33 +1,114 @@ package service -import "be.ems/src/modules/system/model" +import ( + "fmt" -// ISysDictData 字典类型数据 服务层接口 -type ISysDictData interface { - // SelectDictDataPage 根据条件分页查询字典数据 - SelectDictDataPage(query map[string]any) map[string]any + "be.ems/src/modules/system/model" + "be.ems/src/modules/system/repository" +) - // SelectDictDataList 根据条件查询字典数据 - SelectDictDataList(sysDictData model.SysDictData) []model.SysDictData - - // SelectDictDataByCode 根据字典数据编码查询信息 - SelectDictDataByCode(dictCode string) model.SysDictData - - // SelectDictDataByType 根据字典类型查询信息 - SelectDictDataByType(dictType string) []model.SysDictData - - // CheckUniqueDictLabel 校验字典标签是否唯一 - CheckUniqueDictLabel(dictType, dictLabel, dictCode string) bool - - // CheckUniqueDictValue 校验字典键值是否唯一 - CheckUniqueDictValue(dictType, dictValue, dictCode string) bool - - // DeleteDictDataByCodes 批量删除字典数据信息 - DeleteDictDataByCodes(dictCodes []string) (int64, error) - - // InsertDictData 新增字典数据信息 - InsertDictData(sysDictData model.SysDictData) string - - // UpdateDictData 修改字典数据信息 - UpdateDictData(sysDictData model.SysDictData) int64 +// 实例化服务层 SysDictData 结构体 +var NewSysDictData = &SysDictData{ + sysDictDataRepository: repository.NewSysDictData, + sysDictTypeService: NewSysDictType, +} + +// SysDictData 字典类型数据 服务层处理 +type SysDictData struct { + sysDictDataRepository repository.ISysDictData // 字典数据服务 + sysDictTypeService *SysDictType // 字典类型服务 +} + +// SelectDictDataPage 根据条件分页查询字典数据 +func (r *SysDictData) SelectDictDataPage(query map[string]any) map[string]any { + return r.sysDictDataRepository.SelectDictDataPage(query) +} + +// SelectDictDataList 根据条件查询字典数据 +func (r *SysDictData) SelectDictDataList(sysDictData model.SysDictData) []model.SysDictData { + return r.sysDictDataRepository.SelectDictDataList(sysDictData) +} + +// SelectDictDataByCode 根据字典数据编码查询信息 +func (r *SysDictData) SelectDictDataByCode(dictCode string) model.SysDictData { + if dictCode == "" { + return model.SysDictData{} + } + dictCodes := r.sysDictDataRepository.SelectDictDataByCodes([]string{dictCode}) + if len(dictCodes) > 0 { + return dictCodes[0] + } + return model.SysDictData{} +} + +// SelectDictDataByType 根据字典类型查询信息 +func (r *SysDictData) SelectDictDataByType(dictType string) []model.SysDictData { + return r.sysDictTypeService.DictDataCache(dictType) +} + +// CheckUniqueDictLabel 校验字典标签是否唯一 +func (r *SysDictData) CheckUniqueDictLabel(dictType, dictLabel, dictCode string) bool { + uniqueId := r.sysDictDataRepository.CheckUniqueDictData(model.SysDictData{ + DictType: dictType, + DictLabel: dictLabel, + }) + if uniqueId == dictCode { + return true + } + return uniqueId == "" +} + +// CheckUniqueDictValue 校验字典键值是否唯一 +func (r *SysDictData) CheckUniqueDictValue(dictType, dictValue, dictCode string) bool { + uniqueId := r.sysDictDataRepository.CheckUniqueDictData(model.SysDictData{ + DictType: dictType, + DictValue: dictValue, + }) + if uniqueId == dictCode { + return true + } + return uniqueId == "" +} + +// DeleteDictDataByCodes 批量删除字典数据信息 +func (r *SysDictData) DeleteDictDataByCodes(dictCodes []string) (int64, error) { + // 检查是否存在 + dictDatas := r.sysDictDataRepository.SelectDictDataByCodes(dictCodes) + if len(dictDatas) <= 0 { + // 没有可访问字典编码数据! + return 0, fmt.Errorf("there is no accessible dictionary-encoded data") + } + if len(dictDatas) == len(dictCodes) { + for _, v := range dictDatas { + // 刷新缓存 + r.sysDictTypeService.ClearDictCache(v.DictType) + r.sysDictTypeService.LoadingDictCache(v.DictType) + } + rows := r.sysDictDataRepository.DeleteDictDataByCodes(dictCodes) + return rows, nil + } + // 删除字典数据信息失败! + return 0, fmt.Errorf("failed to delete dictionary data information") +} + +// InsertDictData 新增字典数据信息 +func (r *SysDictData) InsertDictData(sysDictData model.SysDictData) string { + insertId := r.sysDictDataRepository.InsertDictData(sysDictData) + if insertId != "" { + // 刷新缓存 + r.sysDictTypeService.ClearDictCache(sysDictData.DictType) + r.sysDictTypeService.LoadingDictCache(sysDictData.DictType) + } + return insertId +} + +// UpdateDictData 修改字典数据信息 +func (r *SysDictData) UpdateDictData(sysDictData model.SysDictData) int64 { + rows := r.sysDictDataRepository.UpdateDictData(sysDictData) + if rows > 0 { + // 刷新缓存 + r.sysDictTypeService.ClearDictCache(sysDictData.DictType) + r.sysDictTypeService.LoadingDictCache(sysDictData.DictType) + } + return rows } diff --git a/src/modules/system/service/sys_dict_data.impl.go b/src/modules/system/service/sys_dict_data.impl.go deleted file mode 100644 index 13dcf89a..00000000 --- a/src/modules/system/service/sys_dict_data.impl.go +++ /dev/null @@ -1,116 +0,0 @@ -package service - -import ( - "fmt" - - "be.ems/src/modules/system/model" - "be.ems/src/modules/system/repository" -) - -// 实例化服务层 SysDictDataImpl 结构体 -var NewSysDictDataImpl = &SysDictDataImpl{ - sysDictDataRepository: repository.NewSysDictDataImpl, - sysDictTypeService: NewSysDictTypeImpl, -} - -// SysDictDataImpl 字典类型数据 服务层处理 -type SysDictDataImpl struct { - // 字典数据服务 - sysDictDataRepository repository.ISysDictData - // 字典类型服务 - sysDictTypeService ISysDictType -} - -// SelectDictDataPage 根据条件分页查询字典数据 -func (r *SysDictDataImpl) SelectDictDataPage(query map[string]any) map[string]any { - return r.sysDictDataRepository.SelectDictDataPage(query) -} - -// SelectDictDataList 根据条件查询字典数据 -func (r *SysDictDataImpl) SelectDictDataList(sysDictData model.SysDictData) []model.SysDictData { - return r.sysDictDataRepository.SelectDictDataList(sysDictData) -} - -// SelectDictDataByCode 根据字典数据编码查询信息 -func (r *SysDictDataImpl) SelectDictDataByCode(dictCode string) model.SysDictData { - if dictCode == "" { - return model.SysDictData{} - } - dictCodes := r.sysDictDataRepository.SelectDictDataByCodes([]string{dictCode}) - if len(dictCodes) > 0 { - return dictCodes[0] - } - return model.SysDictData{} -} - -// SelectDictDataByType 根据字典类型查询信息 -func (r *SysDictDataImpl) SelectDictDataByType(dictType string) []model.SysDictData { - return r.sysDictTypeService.DictDataCache(dictType) -} - -// CheckUniqueDictLabel 校验字典标签是否唯一 -func (r *SysDictDataImpl) CheckUniqueDictLabel(dictType, dictLabel, dictCode string) bool { - uniqueId := r.sysDictDataRepository.CheckUniqueDictData(model.SysDictData{ - DictType: dictType, - DictLabel: dictLabel, - }) - if uniqueId == dictCode { - return true - } - return uniqueId == "" -} - -// CheckUniqueDictValue 校验字典键值是否唯一 -func (r *SysDictDataImpl) CheckUniqueDictValue(dictType, dictValue, dictCode string) bool { - uniqueId := r.sysDictDataRepository.CheckUniqueDictData(model.SysDictData{ - DictType: dictType, - DictValue: dictValue, - }) - if uniqueId == dictCode { - return true - } - return uniqueId == "" -} - -// DeleteDictDataByCodes 批量删除字典数据信息 -func (r *SysDictDataImpl) DeleteDictDataByCodes(dictCodes []string) (int64, error) { - // 检查是否存在 - dictDatas := r.sysDictDataRepository.SelectDictDataByCodes(dictCodes) - if len(dictDatas) <= 0 { - // 没有可访问字典编码数据! - return 0, fmt.Errorf("there is no accessible dictionary-encoded data") - } - if len(dictDatas) == len(dictCodes) { - for _, v := range dictDatas { - // 刷新缓存 - r.sysDictTypeService.ClearDictCache(v.DictType) - r.sysDictTypeService.LoadingDictCache(v.DictType) - } - rows := r.sysDictDataRepository.DeleteDictDataByCodes(dictCodes) - return rows, nil - } - // 删除字典数据信息失败! - return 0, fmt.Errorf("failed to delete dictionary data information") -} - -// InsertDictData 新增字典数据信息 -func (r *SysDictDataImpl) InsertDictData(sysDictData model.SysDictData) string { - insertId := r.sysDictDataRepository.InsertDictData(sysDictData) - if insertId != "" { - // 刷新缓存 - r.sysDictTypeService.ClearDictCache(sysDictData.DictType) - r.sysDictTypeService.LoadingDictCache(sysDictData.DictType) - } - return insertId -} - -// UpdateDictData 修改字典数据信息 -func (r *SysDictDataImpl) UpdateDictData(sysDictData model.SysDictData) int64 { - rows := r.sysDictDataRepository.UpdateDictData(sysDictData) - if rows > 0 { - // 刷新缓存 - r.sysDictTypeService.ClearDictCache(sysDictData.DictType) - r.sysDictTypeService.LoadingDictCache(sysDictData.DictType) - } - return rows -} diff --git a/src/modules/system/service/sys_dict_type.go b/src/modules/system/service/sys_dict_type.go index 5171d22b..4008b327 100644 --- a/src/modules/system/service/sys_dict_type.go +++ b/src/modules/system/service/sys_dict_type.go @@ -1,45 +1,212 @@ package service -import "be.ems/src/modules/system/model" +import ( + "encoding/json" + "fmt" -// ISysDictType 字典类型 服务层接口 -type ISysDictType interface { - // SelectDictTypePage 根据条件分页查询字典类型 - SelectDictTypePage(query map[string]any) map[string]any + "be.ems/src/framework/constants/cachekey" + "be.ems/src/framework/constants/common" + "be.ems/src/framework/redis" + "be.ems/src/modules/system/model" + "be.ems/src/modules/system/repository" +) - // SelectDictTypeList 根据条件查询字典类型 - SelectDictTypeList(sysDictType model.SysDictType) []model.SysDictType - - // SelectDictTypeByID 根据字典类型ID查询信息 - SelectDictTypeByID(dictID string) model.SysDictType - - // SelectDictTypeByType 根据字典类型查询信息 - SelectDictTypeByType(dictType string) model.SysDictType - - // CheckUniqueDictName 校验字典名称是否唯一 - CheckUniqueDictName(dictName, dictID string) bool - - // CheckUniqueDictType 校验字典类型是否唯一 - CheckUniqueDictType(dictType, dictID string) bool - - // InsertDictType 新增字典类型信息 - InsertDictType(sysDictType model.SysDictType) string - - // UpdateDictType 修改字典类型信息 - UpdateDictType(sysDictType model.SysDictType) int64 - - // DeleteDictTypeByIDs 批量删除字典类型信息 - DeleteDictTypeByIDs(dictIDs []string) (int64, error) - - // ResetDictCache 重置字典缓存数据 - ResetDictCache() - - // 加载字典缓存数据 - LoadingDictCache(dictType string) - - // 清空字典缓存数据 - ClearDictCache(dictType string) bool - - // DictDataCache 获取字典数据缓存数据 - DictDataCache(dictType string) []model.SysDictData +// 实例化服务层 SysDictType 结构体 +var NewSysDictType = &SysDictType{ + sysDictTypeRepository: repository.NewSysDictType, + sysDictDataRepository: repository.NewSysDictData, +} + +// SysDictType 字典类型 服务层处理 +type SysDictType struct { + // 字典类型服务 + sysDictTypeRepository repository.ISysDictType + // 字典数据服务 + sysDictDataRepository repository.ISysDictData +} + +// SelectDictTypePage 根据条件分页查询字典类型 +func (r *SysDictType) SelectDictTypePage(query map[string]any) map[string]any { + return r.sysDictTypeRepository.SelectDictTypePage(query) +} + +// SelectDictTypeList 根据条件查询字典类型 +func (r *SysDictType) SelectDictTypeList(sysDictType model.SysDictType) []model.SysDictType { + return r.sysDictTypeRepository.SelectDictTypeList(sysDictType) +} + +// SelectDictTypeByID 根据字典类型ID查询信息 +func (r *SysDictType) SelectDictTypeByID(dictID string) model.SysDictType { + if dictID == "" { + return model.SysDictType{} + } + dictTypes := r.sysDictTypeRepository.SelectDictTypeByIDs([]string{dictID}) + if len(dictTypes) > 0 { + return dictTypes[0] + } + return model.SysDictType{} +} + +// SelectDictTypeByType 根据字典类型查询信息 +func (r *SysDictType) SelectDictTypeByType(dictType string) model.SysDictType { + return r.sysDictTypeRepository.SelectDictTypeByType(dictType) +} + +// CheckUniqueDictName 校验字典名称是否唯一 +func (r *SysDictType) CheckUniqueDictName(dictName, dictID string) bool { + uniqueId := r.sysDictTypeRepository.CheckUniqueDictType(model.SysDictType{ + DictName: dictName, + }) + if uniqueId == dictID { + return true + } + return uniqueId == "" +} + +// CheckUniqueDictType 校验字典类型是否唯一 +func (r *SysDictType) CheckUniqueDictType(dictType, dictID string) bool { + uniqueId := r.sysDictTypeRepository.CheckUniqueDictType(model.SysDictType{ + DictType: dictType, + }) + if uniqueId == dictID { + return true + } + return uniqueId == "" +} + +// InsertDictType 新增字典类型信息 +func (r *SysDictType) InsertDictType(sysDictType model.SysDictType) string { + insertId := r.sysDictTypeRepository.InsertDictType(sysDictType) + if insertId != "" { + r.LoadingDictCache(sysDictType.DictType) + } + return insertId +} + +// UpdateDictType 修改字典类型信息 +func (r *SysDictType) UpdateDictType(sysDictType model.SysDictType) int64 { + data := r.sysDictTypeRepository.SelectDictTypeByIDs([]string{sysDictType.DictID}) + if len(data) == 0 { + return 0 + } + // 修改字典类型key时同步更新其字典数据的类型key + oldDictType := data[0].DictType + rows := r.sysDictTypeRepository.UpdateDictType(sysDictType) + if rows > 0 && oldDictType != "" && oldDictType != sysDictType.DictType { + r.sysDictDataRepository.UpdateDictDataType(oldDictType, sysDictType.DictType) + } + // 刷新缓存 + r.ClearDictCache(oldDictType) + r.LoadingDictCache(sysDictType.DictType) + return rows +} + +// DeleteDictTypeByIDs 批量删除字典类型信息 +func (r *SysDictType) DeleteDictTypeByIDs(dictIDs []string) (int64, error) { + // 检查是否存在 + dictTypes := r.sysDictTypeRepository.SelectDictTypeByIDs(dictIDs) + if len(dictTypes) <= 0 { + // 没有可访问字典类型数据! + return 0, fmt.Errorf("there is no accessible dictionary type data") + } + for _, v := range dictTypes { + // 字典类型下级含有数据 + useCount := r.sysDictDataRepository.CountDictDataByType(v.DictType) + if useCount > 0 { + // 【%s】存在字典数据,不能删除 + return 0, fmt.Errorf("[%s] dictionary data exists and cannot be deleted", v.DictName) + } + // 清除缓存 + r.ClearDictCache(v.DictType) + } + if len(dictTypes) == len(dictIDs) { + rows := r.sysDictTypeRepository.DeleteDictTypeByIDs(dictIDs) + return rows, nil + } + // 删除字典数据信息失败! + return 0, fmt.Errorf("failed to delete dictionary data information") +} + +// ResetDictCache 重置字典缓存数据 +func (r *SysDictType) ResetDictCache() { + r.ClearDictCache("*") + r.LoadingDictCache("") +} + +// getCacheKey 组装缓存key +func (r *SysDictType) getDictCache(dictType string) string { + return cachekey.SYS_DICT_KEY + dictType +} + +// LoadingDictCache 加载字典缓存数据 +func (r *SysDictType) LoadingDictCache(dictType string) { + sysDictData := model.SysDictData{ + Status: common.STATUS_YES, + } + + // 指定字典类型 + if dictType != "" { + sysDictData.DictType = dictType + // 删除缓存 + key := r.getDictCache(dictType) + redis.Del("", key) + } + + sysDictDataList := r.sysDictDataRepository.SelectDictDataList(sysDictData) + if len(sysDictDataList) == 0 { + return + } + + // 将字典数据按类型分组 + m := make(map[string][]model.SysDictData, 0) + for _, v := range sysDictDataList { + key := v.DictType + if item, ok := m[key]; ok { + m[key] = append(item, v) + } else { + m[key] = []model.SysDictData{v} + } + } + + // 放入缓存 + for k, v := range m { + key := r.getDictCache(k) + values, _ := json.Marshal(v) + redis.Set("", key, string(values)) + } +} + +// ClearDictCache 清空字典缓存数据 +func (r *SysDictType) ClearDictCache(dictType string) bool { + key := r.getDictCache(dictType) + keys, err := redis.GetKeys("", key) + if err != nil { + return false + } + delOk, _ := redis.DelKeys("", keys) + return delOk +} + +// DictDataCache 获取字典数据缓存数据 +func (r *SysDictType) DictDataCache(dictType string) []model.SysDictData { + data := []model.SysDictData{} + key := r.getDictCache(dictType) + jsonStr, _ := redis.Get("", key) + if len(jsonStr) > 7 { + err := json.Unmarshal([]byte(jsonStr), &data) + if err != nil { + data = []model.SysDictData{} + } + } else { + data = r.sysDictDataRepository.SelectDictDataList(model.SysDictData{ + Status: common.STATUS_YES, + DictType: dictType, + }) + if len(data) > 0 { + redis.Del("", key) + values, _ := json.Marshal(data) + redis.Set("", key, string(values)) + } + } + return data } diff --git a/src/modules/system/service/sys_dict_type.impl.go b/src/modules/system/service/sys_dict_type.impl.go deleted file mode 100644 index 4f8c6d5c..00000000 --- a/src/modules/system/service/sys_dict_type.impl.go +++ /dev/null @@ -1,212 +0,0 @@ -package service - -import ( - "encoding/json" - "fmt" - - "be.ems/src/framework/constants/cachekey" - "be.ems/src/framework/constants/common" - "be.ems/src/framework/redis" - "be.ems/src/modules/system/model" - "be.ems/src/modules/system/repository" -) - -// 实例化服务层 SysDictTypeImpl 结构体 -var NewSysDictTypeImpl = &SysDictTypeImpl{ - sysDictTypeRepository: repository.NewSysDictTypeImpl, - sysDictDataRepository: repository.NewSysDictDataImpl, -} - -// SysDictTypeImpl 字典类型 服务层处理 -type SysDictTypeImpl struct { - // 字典类型服务 - sysDictTypeRepository repository.ISysDictType - // 字典数据服务 - sysDictDataRepository repository.ISysDictData -} - -// SelectDictTypePage 根据条件分页查询字典类型 -func (r *SysDictTypeImpl) SelectDictTypePage(query map[string]any) map[string]any { - return r.sysDictTypeRepository.SelectDictTypePage(query) -} - -// SelectDictTypeList 根据条件查询字典类型 -func (r *SysDictTypeImpl) SelectDictTypeList(sysDictType model.SysDictType) []model.SysDictType { - return r.sysDictTypeRepository.SelectDictTypeList(sysDictType) -} - -// SelectDictTypeByID 根据字典类型ID查询信息 -func (r *SysDictTypeImpl) SelectDictTypeByID(dictID string) model.SysDictType { - if dictID == "" { - return model.SysDictType{} - } - dictTypes := r.sysDictTypeRepository.SelectDictTypeByIDs([]string{dictID}) - if len(dictTypes) > 0 { - return dictTypes[0] - } - return model.SysDictType{} -} - -// SelectDictTypeByType 根据字典类型查询信息 -func (r *SysDictTypeImpl) SelectDictTypeByType(dictType string) model.SysDictType { - return r.sysDictTypeRepository.SelectDictTypeByType(dictType) -} - -// CheckUniqueDictName 校验字典名称是否唯一 -func (r *SysDictTypeImpl) CheckUniqueDictName(dictName, dictID string) bool { - uniqueId := r.sysDictTypeRepository.CheckUniqueDictType(model.SysDictType{ - DictName: dictName, - }) - if uniqueId == dictID { - return true - } - return uniqueId == "" -} - -// CheckUniqueDictType 校验字典类型是否唯一 -func (r *SysDictTypeImpl) CheckUniqueDictType(dictType, dictID string) bool { - uniqueId := r.sysDictTypeRepository.CheckUniqueDictType(model.SysDictType{ - DictType: dictType, - }) - if uniqueId == dictID { - return true - } - return uniqueId == "" -} - -// InsertDictType 新增字典类型信息 -func (r *SysDictTypeImpl) InsertDictType(sysDictType model.SysDictType) string { - insertId := r.sysDictTypeRepository.InsertDictType(sysDictType) - if insertId != "" { - r.LoadingDictCache(sysDictType.DictType) - } - return insertId -} - -// UpdateDictType 修改字典类型信息 -func (r *SysDictTypeImpl) UpdateDictType(sysDictType model.SysDictType) int64 { - data := r.sysDictTypeRepository.SelectDictTypeByIDs([]string{sysDictType.DictID}) - if len(data) == 0 { - return 0 - } - // 修改字典类型key时同步更新其字典数据的类型key - oldDictType := data[0].DictType - rows := r.sysDictTypeRepository.UpdateDictType(sysDictType) - if rows > 0 && oldDictType != "" && oldDictType != sysDictType.DictType { - r.sysDictDataRepository.UpdateDictDataType(oldDictType, sysDictType.DictType) - } - // 刷新缓存 - r.ClearDictCache(oldDictType) - r.LoadingDictCache(sysDictType.DictType) - return rows -} - -// DeleteDictTypeByIDs 批量删除字典类型信息 -func (r *SysDictTypeImpl) DeleteDictTypeByIDs(dictIDs []string) (int64, error) { - // 检查是否存在 - dictTypes := r.sysDictTypeRepository.SelectDictTypeByIDs(dictIDs) - if len(dictTypes) <= 0 { - // 没有可访问字典类型数据! - return 0, fmt.Errorf("there is no accessible dictionary type data") - } - for _, v := range dictTypes { - // 字典类型下级含有数据 - useCount := r.sysDictDataRepository.CountDictDataByType(v.DictType) - if useCount > 0 { - // 【%s】存在字典数据,不能删除 - return 0, fmt.Errorf("[%s] dictionary data exists and cannot be deleted", v.DictName) - } - // 清除缓存 - r.ClearDictCache(v.DictType) - } - if len(dictTypes) == len(dictIDs) { - rows := r.sysDictTypeRepository.DeleteDictTypeByIDs(dictIDs) - return rows, nil - } - // 删除字典数据信息失败! - return 0, fmt.Errorf("failed to delete dictionary data information") -} - -// ResetDictCache 重置字典缓存数据 -func (r *SysDictTypeImpl) ResetDictCache() { - r.ClearDictCache("*") - r.LoadingDictCache("") -} - -// getCacheKey 组装缓存key -func (r *SysDictTypeImpl) getDictCache(dictType string) string { - return cachekey.SYS_DICT_KEY + dictType -} - -// LoadingDictCache 加载字典缓存数据 -func (r *SysDictTypeImpl) LoadingDictCache(dictType string) { - sysDictData := model.SysDictData{ - Status: common.STATUS_YES, - } - - // 指定字典类型 - if dictType != "" { - sysDictData.DictType = dictType - // 删除缓存 - key := r.getDictCache(dictType) - redis.Del("", key) - } - - sysDictDataList := r.sysDictDataRepository.SelectDictDataList(sysDictData) - if len(sysDictDataList) == 0 { - return - } - - // 将字典数据按类型分组 - m := make(map[string][]model.SysDictData, 0) - for _, v := range sysDictDataList { - key := v.DictType - if item, ok := m[key]; ok { - m[key] = append(item, v) - } else { - m[key] = []model.SysDictData{v} - } - } - - // 放入缓存 - for k, v := range m { - key := r.getDictCache(k) - values, _ := json.Marshal(v) - redis.Set("", key, string(values)) - } -} - -// ClearDictCache 清空字典缓存数据 -func (r *SysDictTypeImpl) ClearDictCache(dictType string) bool { - key := r.getDictCache(dictType) - keys, err := redis.GetKeys("", key) - if err != nil { - return false - } - delOk, _ := redis.DelKeys("", keys) - return delOk -} - -// DictDataCache 获取字典数据缓存数据 -func (r *SysDictTypeImpl) DictDataCache(dictType string) []model.SysDictData { - data := []model.SysDictData{} - key := r.getDictCache(dictType) - jsonStr, _ := redis.Get("", key) - if len(jsonStr) > 7 { - err := json.Unmarshal([]byte(jsonStr), &data) - if err != nil { - data = []model.SysDictData{} - } - } else { - data = r.sysDictDataRepository.SelectDictDataList(model.SysDictData{ - Status: common.STATUS_YES, - DictType: dictType, - }) - if len(data) > 0 { - redis.Del("", key) - values, _ := json.Marshal(data) - redis.Set("", key, string(values)) - } - } - return data -} diff --git a/src/modules/system/system.go b/src/modules/system/system.go index 850f72eb..1331f83b 100644 --- a/src/modules/system/system.go +++ b/src/modules/system/system.go @@ -439,5 +439,5 @@ func InitLoad() { // 启动时,刷新缓存-参数配置 service.NewSysConfigImpl.ResetConfigCache() // 启动时,刷新缓存-字典类型数据 - service.NewSysDictTypeImpl.ResetDictCache() + service.NewSysDictType.ResetDictCache() } diff --git a/src/modules/tool/controller/iperf.go b/src/modules/tool/controller/iperf.go new file mode 100644 index 00000000..c6849748 --- /dev/null +++ b/src/modules/tool/controller/iperf.go @@ -0,0 +1,154 @@ +package controller + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "be.ems/src/framework/i18n" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/ctx" + "be.ems/src/framework/vo/result" + neService "be.ems/src/modules/network_element/service" + "be.ems/src/modules/tool/service" + wsService "be.ems/src/modules/ws/service" + + "github.com/gin-gonic/gin" +) + +// 实例化控制层 IPerfController 结构体 +var NewIPerf = &IPerfController{ + iperfService: service.NewIPerf, + wsService: wsService.NewWS, +} + +// iperf 网络性能测试工具 https://iperf.fr/iperf-download.php +// +// PATH /tool/iperf +type IPerfController struct { + iperfService *service.IPerf // IPerf3 网络性能测试工具服务 + wsService *wsService.WS // WebSocket 服务 +} + +// iperf 版本信息 +// +// GET /v +func (s *IPerfController) Version(c *gin.Context) { + language := ctx.AcceptLanguage(c) + var query struct { + NeType string `form:"neType" binding:"required"` // 网元类型 + NeId string `form:"neId" binding:"required"` // 网元ID + Version string `form:"version" binding:"required,oneof=V2 V3"` // 版本 + } + if err := c.ShouldBindQuery(&query); err != nil { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + + output, err := s.iperfService.Version(query.NeType, query.NeId, query.Version) + if err != nil { + c.JSON(200, result.ErrMsg(i18n.TKey(language, err.Error()))) + return + } + data := strings.Split(output, "\n") + c.JSON(200, result.OkData(data)) +} + +// iperf 软件安装 +// +// POST /i +func (s *IPerfController) Install(c *gin.Context) { + language := ctx.AcceptLanguage(c) + var body struct { + NeType string `json:"neType" binding:"required"` // 网元类型 + NeId string `json:"neId" binding:"required"` // 网元ID + Version string `form:"version" binding:"required,oneof=V2 V3"` // 版本 + } + if err := c.ShouldBindBodyWithJSON(&body); err != nil { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + + if err := s.iperfService.Install(body.NeType, body.NeId, body.Version); err != nil { + c.JSON(200, result.ErrMsg(i18n.TKey(language, err.Error()))) + return + } + c.JSON(200, result.Ok(nil)) +} + +// iperf 软件运行 +// +// GET /run +func (s *IPerfController) Run(c *gin.Context) { + language := ctx.AcceptLanguage(c) + var query struct { + NeType string `form:"neType" binding:"required"` // 网元类型 + NeId string `form:"neId" binding:"required"` // 网元标识id + Cols int `form:"cols"` // 终端单行字符数 + Rows int `form:"rows"` // 终端显示行数 + } + if err := c.ShouldBindQuery(&query); err != nil { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + + // 登录用户信息 + loginUser, err := ctx.LoginUser(c) + if err != nil { + c.JSON(401, result.CodeMsg(401, i18n.TKey(language, err.Error()))) + return + } + + // 网元主机的SSH客户端 + sshClient, err := neService.NewNeInfo.NeRunSSHClient(query.NeType, query.NeId) + if err != nil { + c.JSON(200, result.ErrMsg(err.Error())) + return + } + defer sshClient.Close() + // ssh连接会话 + clientSession, err := sshClient.NewClientSession(query.Cols, query.Rows) + if err != nil { + c.JSON(200, result.ErrMsg("neinfo ssh client session new err")) + return + } + defer clientSession.Close() + + // 将 HTTP 连接升级为 WebSocket 连接 + wsConn := s.wsService.UpgraderWs(c.Writer, c.Request) + if wsConn == nil { + return + } + defer wsConn.Close() + + wsClient := s.wsService.ClientCreate(loginUser.UserID, nil, wsConn, clientSession) + go s.wsService.ClientWriteListen(wsClient) + go s.wsService.ClientReadListen(wsClient, s.iperfService.Run) + + // 等待1秒,排空首次消息 + time.Sleep(1 * time.Second) + _ = clientSession.Read() + + // 实时读取Run消息直接输出 + msTicker := time.NewTicker(100 * time.Millisecond) + defer msTicker.Stop() + for { + select { + case ms := <-msTicker.C: + outputByte := clientSession.Read() + if len(outputByte) > 0 { + outputStr := string(outputByte) + msgByte, _ := json.Marshal(result.Ok(map[string]any{ + "requestId": fmt.Sprintf("iperf3_%d", ms.UnixMilli()), + "data": outputStr, + })) + wsClient.MsgChan <- msgByte + } + case <-wsClient.StopChan: // 等待停止信号 + s.wsService.ClientClose(wsClient.ID) + logger.Infof("ws Stop Client UID %s", wsClient.BindUid) + return + } + } +} diff --git a/src/modules/tool/controller/ping.go b/src/modules/tool/controller/ping.go new file mode 100644 index 00000000..aa70e036 --- /dev/null +++ b/src/modules/tool/controller/ping.go @@ -0,0 +1,180 @@ +package controller + +import ( + "encoding/json" + "fmt" + "time" + + "be.ems/src/framework/i18n" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/ctx" + "be.ems/src/framework/vo/result" + neService "be.ems/src/modules/network_element/service" + "be.ems/src/modules/tool/model" + "be.ems/src/modules/tool/service" + wsService "be.ems/src/modules/ws/service" + + "github.com/gin-gonic/gin" +) + +// 实例化控制层 PingController 结构体 +var NewPing = &PingController{ + pingService: service.NewPing, + wsService: wsService.NewWS, +} + +// ping ICMP网络探测工具 https://github.com/prometheus-community/pro-bing +// +// PATH /tool/ping +type PingController struct { + pingService *service.Ping // ping ICMP网络探测工具 + wsService *wsService.WS // WebSocket 服务 +} + +// ping 基本信息运行 +// +// POST / +func (s *PingController) Statistics(c *gin.Context) { + language := ctx.AcceptLanguage(c) + var body model.Ping + if err := c.ShouldBindBodyWithJSON(&body); err != nil { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + + info, err := s.pingService.Statistics(body) + if err != nil { + c.JSON(200, result.ErrMsg(i18n.TKey(language, err.Error()))) + return + } + c.JSON(200, result.OkData(info)) +} + +// ping 传统UNIX运行 +// +// GET / +func (s *PingController) StatisticsOn(c *gin.Context) { + language := ctx.AcceptLanguage(c) + // 登录用户信息 + loginUser, err := ctx.LoginUser(c) + if err != nil { + c.JSON(401, result.CodeMsg(401, i18n.TKey(language, err.Error()))) + return + } + + // 将 HTTP 连接升级为 WebSocket 连接 + wsConn := s.wsService.UpgraderWs(c.Writer, c.Request) + if wsConn == nil { + return + } + defer wsConn.Close() + + wsClient := s.wsService.ClientCreate(loginUser.UserID, nil, wsConn, nil) + go s.wsService.ClientWriteListen(wsClient) + go s.wsService.ClientReadListen(wsClient, s.pingService.StatisticsOn) + + // 等待停止信号 + for value := range wsClient.StopChan { + s.wsService.ClientClose(wsClient.ID) + logger.Infof("ws Stop Client UID %s %s", wsClient.BindUid, value) + return + } +} + +// ping 网元端版本信息 +// +// GET /v +func (s *PingController) Version(c *gin.Context) { + language := ctx.AcceptLanguage(c) + var query struct { + NeType string `form:"neType" binding:"required"` // 网元类型 + NeID string `form:"neId" binding:"required"` // 网元ID + } + if err := c.ShouldBindQuery(&query); err != nil { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + + output, err := s.pingService.Version(query.NeType, query.NeID) + if err != nil { + c.JSON(200, result.ErrMsg(i18n.TKey(language, err.Error()))) + return + } + c.JSON(200, result.OkData(output)) +} + +// ping 网元端UNIX运行 +// +// GET /run +func (s *PingController) Run(c *gin.Context) { + language := ctx.AcceptLanguage(c) + var query struct { + NeType string `form:"neType" binding:"required"` // 网元类型 + NeId string `form:"neId" binding:"required"` // 网元标识id + Cols int `form:"cols"` // 终端单行字符数 + Rows int `form:"rows"` // 终端显示行数 + } + if err := c.ShouldBindQuery(&query); err != nil { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + + // 登录用户信息 + loginUser, err := ctx.LoginUser(c) + if err != nil { + c.JSON(401, result.CodeMsg(401, i18n.TKey(language, err.Error()))) + return + } + + // 网元主机的SSH客户端 + sshClient, err := neService.NewNeInfo.NeRunSSHClient(query.NeType, query.NeId) + if err != nil { + c.JSON(200, result.ErrMsg(err.Error())) + return + } + defer sshClient.Close() + // ssh连接会话 + clientSession, err := sshClient.NewClientSession(query.Cols, query.Rows) + if err != nil { + c.JSON(200, result.ErrMsg("neinfo ssh client session new err")) + return + } + defer clientSession.Close() + + // 将 HTTP 连接升级为 WebSocket 连接 + wsConn := s.wsService.UpgraderWs(c.Writer, c.Request) + if wsConn == nil { + return + } + defer wsConn.Close() + + wsClient := s.wsService.ClientCreate(loginUser.UserID, nil, wsConn, clientSession) + go s.wsService.ClientWriteListen(wsClient) + go s.wsService.ClientReadListen(wsClient, s.pingService.Run) + + // 等待1秒,排空首次消息 + time.Sleep(1 * time.Second) + _ = clientSession.Read() + + // 实时读取Run消息直接输出 + msTicker := time.NewTicker(100 * time.Millisecond) + defer msTicker.Stop() + for { + select { + case ms := <-msTicker.C: + outputByte := clientSession.Read() + if len(outputByte) > 0 { + outputStr := string(outputByte) + msgByte, _ := json.Marshal(result.Ok(map[string]any{ + "requestId": fmt.Sprintf("ping_%d", ms.UnixMilli()), + "data": outputStr, + })) + wsClient.MsgChan <- msgByte + } + case <-wsClient.StopChan: // 等待停止信号 + s.wsService.ClientClose(wsClient.ID) + logger.Infof("ws Stop Client UID %s", wsClient.BindUid) + return + } + } +} diff --git a/src/modules/tool/model/ping.go b/src/modules/tool/model/ping.go new file mode 100644 index 00000000..9a00b1ff --- /dev/null +++ b/src/modules/tool/model/ping.go @@ -0,0 +1,62 @@ +package model + +import ( + "runtime" + "time" + + probing "github.com/prometheus-community/pro-bing" +) + +// Ping 探针发包参数 +type Ping struct { + DesAddr string `json:"desAddr" binding:"required"` // 目的 IP 地址(字符串类型,必填) + SrcAddr string `json:"srcAddr"` // 源 IP 地址(字符串类型,可选) + Interval int `json:"interval"` // 发包间隔(整数类型,可选,单位:秒,取值范围:1-60,默认值:1) + TTL int `json:"ttl"` // TTL(整数类型,可选,取值范围:1-255,默认值:255) + Count int `json:"count"` // 发包数(整数类型,可选,取值范围:1-65535,默认值:5) + Size int `json:"size"` // 报文大小(整数类型,可选,取值范围:36-8192,默认值:36) + Timeout int `json:"timeout"` // 报文超时时间(整数类型,可选,单位:秒,取值范围:1-60,默认值:2) +} + +// setDefaultValue 设置默认值 +func (p *Ping) setDefaultValue() { + if p.Interval < 1 || p.Interval > 10 { + p.Interval = 1 + } + if p.TTL < 1 || p.TTL > 255 { + p.TTL = 255 + } + if p.Count < 1 || p.Count > 65535 { + p.Count = 5 + } + if p.Size < 36 || p.Size > 8192 { + p.Size = 36 + } + if p.Timeout < 1 || p.Timeout > 60 { + p.Timeout = 2 + } +} + +// NewPinger ping对象 +func (p *Ping) NewPinger() (*probing.Pinger, error) { + p.setDefaultValue() + + pinger, err := probing.NewPinger(p.DesAddr) + if err != nil { + return nil, err + } + if p.SrcAddr != "" { + pinger.Source = p.SrcAddr + } + pinger.Interval = time.Duration(p.Interval) * time.Second + pinger.TTL = p.TTL + pinger.Count = p.Count + pinger.Size = p.Size + pinger.Timeout = time.Duration(p.Timeout) * time.Second + + // 设置特权模式(需要管理员权限) + if runtime.GOOS == "windows" { + pinger.SetPrivileged(true) + } + return pinger, nil +} diff --git a/src/modules/tool/service/iperf.go b/src/modules/tool/service/iperf.go new file mode 100644 index 00000000..bdb3b7ea --- /dev/null +++ b/src/modules/tool/service/iperf.go @@ -0,0 +1,289 @@ +package service + +import ( + "encoding/json" + "fmt" + "io" + "strings" + "time" + + "be.ems/src/framework/config" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/ssh" + "be.ems/src/framework/vo/result" + neService "be.ems/src/modules/network_element/service" + wsModel "be.ems/src/modules/ws/model" +) + +// 实例化服务层 IPerf 结构体 +var NewIPerf = &IPerf{} + +// IPerf 网络性能测试工具 服务层处理 +type IPerf struct{} + +// Version 查询版本信息 +func (s *IPerf) Version(meType, neId, version string) (string, error) { + if version != "V2" && version != "V3" { + return "", fmt.Errorf("iperf version is required V2 or V3") + } + cmd := "iperf3 --version" + if version == "V2" { + cmd = "iperf -v" + } + + // 网元主机的SSH客户端 + sshClient, err := neService.NewNeInfo.NeRunSSHClient(meType, neId) + if err != nil { + return "", err + } + defer sshClient.Close() + + // 检查是否安装iperf + output, err := sshClient.RunCMD(cmd) + if err != nil { + if version == "V2" && strings.HasSuffix(err.Error(), "status 1") { // V2 版本 + return strings.TrimSpace(output), nil + } + return "", fmt.Errorf("iperf %s not installed", version) + } + return strings.TrimSpace(output), err +} + +// Install 安装iperf3 +func (s *IPerf) Install(meType, neId, version string) error { + if version != "V2" && version != "V3" { + return fmt.Errorf("iperf version is required V2 or V3") + } + + // 网元主机的SSH客户端 + sshClient, err := neService.NewNeInfo.NeRunSSHClient(meType, neId) + if err != nil { + return err + } + defer sshClient.Close() + // 网元主机的SSH客户端进行文件传输 + sftpClient, err := sshClient.NewClientSFTP() + if err != nil { + return err + } + defer sftpClient.Close() + + nePath := "/tmp" + depPkg := "sudo dpkg -i" + depDir := "assets/dependency/iperf3/deb" + + // 检查平台类型 + if _, err := sshClient.RunCMD("sudo dpkg --version"); err == nil { + depPkg = "sudo dpkg -i" + depDir = "assets/dependency/iperf3/deb" + // sudo apt remove iperf3 libiperf0 libsctp1 libsctp-dev lksctp-tools + } else if _, err := sshClient.RunCMD("sudo yum --version"); err == nil { + depPkg = "sudo rpm -Uvh --nosignature --reinstall --force" + depDir = "assets/dependency/iperf3/rpm" + // yum remove iperf3 iperf3-help.noarch + } else { + return fmt.Errorf("iperf %s not supported install", version) + } + + // V2版本和V3版本的安装包路径不同 + if version == "V2" { + depDir = strings.Replace(depDir, "iperf3", "iperf", 1) + } + + // 从 embed.FS 中读取默认配置文件内容 + assetsDir := config.GetAssetsDirFS() + fsDirEntrys, err := assetsDir.ReadDir(depDir) + if err != nil { + return err + } + neFilePaths := []string{} + for _, d := range fsDirEntrys { + // 打开本地文件 + localFile, err := assetsDir.Open(fmt.Sprintf("%s/%s", depDir, d.Name())) + if err != nil { + return fmt.Errorf("iperf %s file local error", version) + } + defer localFile.Close() + // 创建远程文件 + remotePath := fmt.Sprintf("%s/%s", nePath, d.Name()) + remoteFile, err := sftpClient.Client.Create(remotePath) + if err != nil { + return fmt.Errorf("iperf %s file remote error", version) + } + defer remoteFile.Close() + // 使用 io.Copy 将嵌入的文件内容复制到目标文件 + if _, err := io.Copy(remoteFile, localFile); err != nil { + return fmt.Errorf("iperf %s file copy error", version) + } + neFilePaths = append(neFilePaths, remotePath) + } + + // 删除软件包 + defer func() { + pkgRemove := fmt.Sprintf("sudo rm %s", strings.Join(neFilePaths, " ")) + sshClient.RunCMD(pkgRemove) + }() + + // 安装软件包 + pkgInstall := fmt.Sprintf("%s %s", depPkg, strings.Join(neFilePaths, " ")) + if _, err := sshClient.RunCMD(pkgInstall); err != nil { + return fmt.Errorf("iperf %s install error", version) + } + return err +} + +// Run 接收IPerf3终端交互业务处理 +func (s *IPerf) Run(client *wsModel.WSClient, reqMsg wsModel.WSRequest) { + // 必传requestId确认消息 + if reqMsg.RequestID == "" { + msg := "message requestId is required" + logger.Infof("ws IPerf Run UID %s err: %s", client.BindUid, msg) + msgByte, _ := json.Marshal(result.ErrMsg(msg)) + client.MsgChan <- msgByte + return + } + + var resByte []byte + var err error + + switch reqMsg.Type { + case "close": + // 主动关闭 + resultByte, _ := json.Marshal(result.OkMsg("user initiated closure")) + client.MsgChan <- resultByte + // 等待1s后关闭连接 + time.Sleep(1 * time.Second) + client.StopChan <- struct{}{} + return + case "iperf": + // SSH会话消息接收写入会话 + var command string + command, err = s.parseOptions(reqMsg.Data) + if command != "" && err == nil { + sshClientSession := client.ChildConn.(*ssh.SSHClientSession) + _, err = sshClientSession.Write(command) + } + case "ctrl-c": + // 模拟按下 Ctrl+C + sshClientSession := client.ChildConn.(*ssh.SSHClientSession) + _, err = sshClientSession.Write("\u0003\n") + case "resize": + // 会话窗口重置 + msgByte, _ := json.Marshal(reqMsg.Data) + var data struct { + Cols int `json:"cols"` + Rows int `json:"rows"` + } + err = json.Unmarshal(msgByte, &data) + if err == nil { + sshClientSession := client.ChildConn.(*ssh.SSHClientSession) + err = sshClientSession.Session.WindowChange(data.Rows, data.Cols) + } + default: + err = fmt.Errorf("message type %s not supported", reqMsg.Type) + } + + if err != nil { + logger.Warnf("ws IPerf Run UID %s err: %s", client.BindUid, err.Error()) + msgByte, _ := json.Marshal(result.ErrMsg(err.Error())) + client.MsgChan <- msgByte + if err == io.EOF { + // 等待1s后关闭连接 + time.Sleep(1 * time.Second) + client.StopChan <- struct{}{} + } + return + } + if len(resByte) > 0 { + client.MsgChan <- resByte + } +} + +// parseOptions 解析拼装iperf3命令 iperf [-s|-c host] [options] +func (s *IPerf) parseOptions(reqData any) (string, error) { + msgByte, _ := json.Marshal(reqData) + var data struct { + Command string `json:"command"` // 命令字符串 + Version string `json:"version"` // 服务版本,默认V3 + Mode string `json:"mode"` // 服务端或客户端,默认客户端client + Host string `json:"host"` // 客户端连接到的服务端IP地址 + // Server or Client + Port int `json:"port"` // 服务端口 + Interval int `json:"interval"` // 每次报告之间的时间间隔,单位为秒 + // Server + OneOff bool `json:"oneOff"` // 只进行一次连接 + // Client + UDP bool `json:"udp"` // use UDP rather than TCP + Time int `json:"time"` // 以秒为单位的传输时间(默认为 10 秒) + Reverse bool `json:"reverse"` // 以反向模式运行(服务器发送,客户端接收) + Window string `json:"window"` // 设置窗口大小/套接字缓冲区大小 + Parallel int `json:"parallel"` // 运行的并行客户端流数量 + Bitrate int `json:"bitrate"` // 以比特/秒为单位(0 表示无限制) + } + if err := json.Unmarshal(msgByte, &data); err != nil { + logger.Warnf("ws processor parseClient err: %s", err.Error()) + return "", fmt.Errorf("query data structure error") + } + if data.Version != "V3" && data.Version != "V2" { + return "", fmt.Errorf("query data version support V3 or V2") + } + + command := []string{"iperf3"} + if data.Version == "V2" { + command = []string{"iperf"} + } + // 命令字符串高优先级 + if data.Command != "" { + command = append(command, data.Command) + command = append(command, "\n") + return strings.Join(command, " "), nil + } + + if data.Mode != "client" && data.Mode != "server" { + return "", fmt.Errorf("query data mode support client or server") + } + if data.Mode == "client" && data.Host == "" { + return "", fmt.Errorf("query data client host empty") + } + + if data.Mode == "client" { + command = append(command, "-c") + command = append(command, data.Host) + // Client + if data.UDP { + command = append(command, "-u") + } + if data.Time > 0 { + command = append(command, fmt.Sprintf("-t %d", data.Time)) + } + if data.Bitrate > 0 { + command = append(command, fmt.Sprintf("-b %d", data.Bitrate)) + } + if data.Parallel > 0 { + command = append(command, fmt.Sprintf("-P %d", data.Parallel)) + } + if data.Reverse { + command = append(command, "-R") + } + if data.Window != "" { + command = append(command, fmt.Sprintf("-w %s", data.Window)) + } + } + if data.Mode == "server" { + command = append(command, "-s") + // Server + if data.OneOff { + command = append(command, "-1") + } + } + + // Server or Client + if data.Port > 0 { + command = append(command, fmt.Sprintf("-p %d", data.Port)) + } + if data.Interval > 0 { + command = append(command, fmt.Sprintf("-i %d", data.Interval)) + } + command = append(command, "\n") + return strings.Join(command, " "), nil +} diff --git a/src/modules/tool/service/ping.go b/src/modules/tool/service/ping.go new file mode 100644 index 00000000..79f51a2a --- /dev/null +++ b/src/modules/tool/service/ping.go @@ -0,0 +1,261 @@ +package service + +import ( + "encoding/json" + "fmt" + "io" + "strings" + "time" + + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/ssh" + "be.ems/src/framework/vo/result" + neService "be.ems/src/modules/network_element/service" + "be.ems/src/modules/tool/model" + wsModel "be.ems/src/modules/ws/model" + probing "github.com/prometheus-community/pro-bing" +) + +// 实例化服务层 Ping 结构体 +var NewPing = &Ping{} + +// Ping 网络性能测试工具 服务层处理 +type Ping struct{} + +// Statistics ping基本信息 +func (s *Ping) Statistics(ping model.Ping) (map[string]any, error) { + pinger, err := ping.NewPinger() + if err != nil { + return nil, err + } + if err = pinger.Run(); err != nil { + return nil, err + } + defer pinger.Stop() + stats := pinger.Statistics() + return map[string]any{ + "minTime": stats.MinRtt.Microseconds(), // 最小时延(整数类型,可选,单位:微秒) + "maxTime": stats.MaxRtt.Microseconds(), // 最大时延(整数类型,可选,单位:微秒) + "avgTime": stats.AvgRtt.Microseconds(), // 平均时延(整数类型,可选,单位:微秒) + "lossRate": int64(stats.PacketLoss), // 丢包率(整数类型,可选,单位:%) + "jitter": stats.StdDevRtt.Microseconds(), // 时延抖动(整数类型,可选,单位:微秒) + }, nil +} + +// StatisticsOn ping模拟传统UNIX +func (s *Ping) StatisticsOn(client *wsModel.WSClient, reqMsg wsModel.WSRequest) { + // 必传requestId确认消息 + if reqMsg.RequestID == "" { + msg := "message requestId is required" + logger.Infof("ws Commont UID %s err: %s", client.BindUid, msg) + msgByte, _ := json.Marshal(result.ErrMsg(msg)) + client.MsgChan <- msgByte + return + } + + var resByte []byte + var err error + + switch reqMsg.Type { + case "close": + // 主动关闭 + resultByte, _ := json.Marshal(result.OkMsg("user initiated closure")) + client.MsgChan <- resultByte + // 等待1s后关闭连接 + time.Sleep(1 * time.Second) + client.StopChan <- struct{}{} + return + case "ping": + msgByte, _ := json.Marshal(reqMsg.Data) + var ping model.Ping + if errj := json.Unmarshal(msgByte, &ping); errj != nil { + err = fmt.Errorf("query data structure error") + } + var pinger *probing.Pinger + pinger, errp := ping.NewPinger() + if errp != nil { + logger.Warnf("ws pinger new err: %s", errp.Error()) + err = fmt.Errorf("pinger error") + } + defer pinger.Stop() + + // 接收的数据包 + pinger.OnRecv = func(pkt *probing.Packet) { + resultByte, _ := json.Marshal(result.Ok(map[string]any{ + "requestId": reqMsg.RequestID, + "data": fmt.Sprintf("%d bytes from %s: icmp_seq=%d time=%v\\r\\n", pkt.Nbytes, pkt.IPAddr, pkt.Seq, pkt.Rtt), + })) + client.MsgChan <- resultByte + } + // 已接收过的数据包 + pinger.OnDuplicateRecv = func(pkt *probing.Packet) { + resultByte, _ := json.Marshal(result.Ok(map[string]any{ + "requestId": reqMsg.RequestID, + "data": fmt.Sprintf("%d bytes from %s: icmp_seq=%d time=%v ttl=%v (DUP!)\\r\\n", pkt.Nbytes, pkt.IPAddr, pkt.Seq, pkt.Rtt, pkt.TTL), + })) + client.MsgChan <- resultByte + } + // 接收结束 + pinger.OnFinish = func(stats *probing.Statistics) { + end1 := fmt.Sprintf("\\r\\n--- %s ping statistics ---\\r\\n", stats.Addr) + end2 := fmt.Sprintf("%d packets transmitted, %d packets received, %v%% packet loss\\r\\n", stats.PacketsSent, stats.PacketsRecv, stats.PacketLoss) + end3 := fmt.Sprintf("round-trip min/avg/max/stddev = %v/%v/%v/%v\\r\\n", stats.MinRtt, stats.AvgRtt, stats.MaxRtt, stats.StdDevRtt) + resultByte, _ := json.Marshal(result.Ok(map[string]any{ + "requestId": reqMsg.RequestID, + "data": fmt.Sprintf("%s%s%s", end1, end2, end3), + })) + client.MsgChan <- resultByte + } + resultByte, _ := json.Marshal(result.Ok(map[string]any{ + "requestId": reqMsg.RequestID, + "data": fmt.Sprintf("PING %s (%s) %d bytes of data.\\r\\n", pinger.Addr(), pinger.IPAddr(), pinger.Size), + })) + client.MsgChan <- resultByte + if errp := pinger.Run(); errp != nil { + logger.Warnf("ws pinger run err: %s", errp.Error()) + err = fmt.Errorf("pinger error") + } + default: + err = fmt.Errorf("message type %s not supported", reqMsg.Type) + } + + if err != nil { + logger.Warnf("ws ping run UID %s err: %s", client.BindUid, err.Error()) + msgByte, _ := json.Marshal(result.ErrMsg(err.Error())) + client.MsgChan <- msgByte + if err == io.EOF { + // 等待1s后关闭连接 + time.Sleep(1 * time.Second) + client.StopChan <- struct{}{} + } + return + } + if len(resByte) > 0 { + client.MsgChan <- resByte + } +} + +// Version 查询版本信息 +func (s *Ping) Version(meType, neId string) (string, error) { + // 检查是否安装ping + output, err := neService.NewNeInfo.NeRunSSHCmd(meType, neId, "ping -V") + if err != nil { + return "", fmt.Errorf("ping not installed") + } + return strings.TrimSpace(output), err +} + +// Run 接收ping终端交互业务处理 +func (s *Ping) Run(client *wsModel.WSClient, reqMsg wsModel.WSRequest) { + // 必传requestId确认消息 + if reqMsg.RequestID == "" { + msg := "message requestId is required" + logger.Infof("ws ping run UID %s err: %s", client.BindUid, msg) + msgByte, _ := json.Marshal(result.ErrMsg(msg)) + client.MsgChan <- msgByte + return + } + + var resByte []byte + var err error + + switch reqMsg.Type { + case "close": + // 主动关闭 + resultByte, _ := json.Marshal(result.OkMsg("user initiated closure")) + client.MsgChan <- resultByte + // 等待1s后关闭连接 + time.Sleep(1 * time.Second) + client.StopChan <- struct{}{} + return + case "ping": + // SSH会话消息接收写入会话 + var command string + command, err = s.parseOptions(reqMsg.Data) + if command != "" && err == nil { + sshClientSession := client.ChildConn.(*ssh.SSHClientSession) + _, err = sshClientSession.Write(command) + } + case "ctrl-c": + // 模拟按下 Ctrl+C + sshClientSession := client.ChildConn.(*ssh.SSHClientSession) + _, err = sshClientSession.Write("\u0003\n") + case "resize": + // 会话窗口重置 + msgByte, _ := json.Marshal(reqMsg.Data) + var data struct { + Cols int `json:"cols"` + Rows int `json:"rows"` + } + err = json.Unmarshal(msgByte, &data) + if err == nil { + sshClientSession := client.ChildConn.(*ssh.SSHClientSession) + err = sshClientSession.Session.WindowChange(data.Rows, data.Cols) + } + default: + err = fmt.Errorf("message type %s not supported", reqMsg.Type) + } + + if err != nil { + logger.Warnf("ws ping run UID %s err: %s", client.BindUid, err.Error()) + msgByte, _ := json.Marshal(result.ErrMsg(err.Error())) + client.MsgChan <- msgByte + if err == io.EOF { + // 等待1s后关闭连接 + time.Sleep(1 * time.Second) + client.StopChan <- struct{}{} + } + return + } + if len(resByte) > 0 { + client.MsgChan <- resByte + } +} + +// parseOptions 解析拼装ping命令 ping [options] +func (s *Ping) parseOptions(reqData any) (string, error) { + msgByte, _ := json.Marshal(reqData) + var data struct { + Command string `json:"command"` // 命令字符串 + DesAddr string `json:"desAddr"` // dns name or ip address + // Options + Interval int `json:"interval"` // seconds between sending each packet + TTL int `json:"ttl"` // define time to live + Cunt int `json:"count"` // 次回复后停止 + Size int `json:"size"` // 使用 作为要发送的数据字节数 + Timeout int `json:"timeout"` // time to wait for response + } + if err := json.Unmarshal(msgByte, &data); err != nil { + logger.Warnf("ws processor parseClient err: %s", err.Error()) + return "", fmt.Errorf("query data structure error") + } + + command := []string{"ping"} + // 命令字符串高优先级 + if data.Command != "" { + command = append(command, data.Command) + command = append(command, "\n") + return strings.Join(command, " "), nil + } + + // Options + if data.Interval > 0 { + command = append(command, fmt.Sprintf("-i %d", data.Interval)) + } + if data.TTL > 0 { + command = append(command, fmt.Sprintf("-t %d", data.TTL)) + } + if data.Cunt > 0 { + command = append(command, fmt.Sprintf("-c %d", data.Cunt)) + } + if data.Size > 0 { + command = append(command, fmt.Sprintf("-s %d", data.Size)) + } + if data.Timeout > 0 { + command = append(command, fmt.Sprintf("-w %d", data.Timeout)) + } + + command = append(command, data.DesAddr) + command = append(command, "\n") + return strings.Join(command, " "), nil +} diff --git a/src/modules/tool/tool.go b/src/modules/tool/tool.go new file mode 100644 index 00000000..70385683 --- /dev/null +++ b/src/modules/tool/tool.go @@ -0,0 +1,57 @@ +package tool + +import ( + "be.ems/src/framework/logger" + "be.ems/src/framework/middleware" + "be.ems/src/framework/middleware/collectlogs" + "be.ems/src/modules/tool/controller" + + "github.com/gin-gonic/gin" +) + +// 模块路由注册 +func Setup(router *gin.Engine) { + logger.Infof("开始加载 ====> tool 模块路由") + + // iperf 网络性能测试工具 + iperfGroup := router.Group("/tool/iperf") + { + iperfGroup.GET("/v", + middleware.PreAuthorize(nil), + controller.NewIPerf.Version, + ) + iperfGroup.POST("/i", + middleware.PreAuthorize(nil), + collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.iperf", collectlogs.BUSINESS_TYPE_OTHER)), + controller.NewIPerf.Install, + ) + iperfGroup.GET("/run", + middleware.PreAuthorize(nil), + collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.iperf", collectlogs.BUSINESS_TYPE_OTHER)), + controller.NewIPerf.Run, + ) + } + + // ping ICMP网络探测工具 + pingGroup := router.Group("/tool/ping") + { + pingGroup.POST("", + middleware.PreAuthorize(nil), + controller.NewPing.Statistics, + ) + pingGroup.GET("", + middleware.PreAuthorize(nil), + collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.ping", collectlogs.BUSINESS_TYPE_OTHER)), + controller.NewPing.StatisticsOn, + ) + pingGroup.GET("/v", + middleware.PreAuthorize(nil), + controller.NewPing.Version, + ) + pingGroup.GET("/run", + middleware.PreAuthorize(nil), + collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.ping", collectlogs.BUSINESS_TYPE_OTHER)), + controller.NewPing.Run, + ) + } +} diff --git a/src/modules/trace/controller/packet.go b/src/modules/trace/controller/packet.go new file mode 100644 index 00000000..ac54aa2b --- /dev/null +++ b/src/modules/trace/controller/packet.go @@ -0,0 +1,121 @@ +package controller + +import ( + "be.ems/src/framework/i18n" + "be.ems/src/framework/utils/ctx" + "be.ems/src/framework/vo/result" + traceService "be.ems/src/modules/trace/service" + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" +) + +// 实例化控制层 PacketController 结构体 +var NewPacket = &PacketController{ + packetService: traceService.NewPacket, +} + +// 信令跟踪 +// +// PATH /trace/packet +type PacketController struct { + packetService *traceService.Packet // 信令跟踪服务 +} + +// 信令跟踪网卡设备列表 +// +// GET /devices +func (s *PacketController) Devices(c *gin.Context) { + data := s.packetService.NetworkDevices() + c.JSON(200, result.OkData(data)) +} + +// 信令跟踪开始 +// +// POST /start +func (s *PacketController) Start(c *gin.Context) { + language := ctx.AcceptLanguage(c) + var body struct { + TaskNo string `json:"taskNo" binding:"required"` // 任务编号 + Device string `json:"device" binding:"required"` // 网卡设备 + Filter string `json:"filter" ` // 过滤表达式(port 33030 or 33040) + OutputPCAP bool `json:"outputPCAP" ` // 输出PCAP文件 (默认false) + } + if err := c.ShouldBindBodyWith(&body, binding.JSON); err != nil { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + + msg, err := s.packetService.LiveStart(body.TaskNo, body.Device, body.Filter, body.OutputPCAP) + if err != nil { + c.JSON(200, result.ErrMsg(i18n.TKey(language, err.Error()))) + return + } + c.JSON(200, result.OkData(msg)) +} + +// 信令跟踪结束 +// +// POST /stop +func (s *PacketController) Stop(c *gin.Context) { + language := ctx.AcceptLanguage(c) + var body struct { + TaskNo string `json:"taskNo" binding:"required"` // 任务编号 + } + if err := c.ShouldBindBodyWith(&body, binding.JSON); err != nil { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + + if err := s.packetService.LiveStop(body.TaskNo); err != nil { + c.JSON(200, result.ErrMsg(i18n.TKey(language, err.Error()))) + return + } + c.JSON(200, result.Ok(nil)) +} + +// 信令跟踪过滤 +// +// PUT /filter +func (s *PacketController) Filter(c *gin.Context) { + language := ctx.AcceptLanguage(c) + var body struct { + TaskNo string `json:"taskNo" binding:"required"` // 任务编号 + Expr string `json:"expr" ` // 过滤表达式(port 33030 or 33040) + } + if err := c.ShouldBindBodyWith(&body, binding.JSON); err != nil { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + + if err := s.packetService.LiveFilter(body.TaskNo, body.Expr); err != nil { + c.JSON(200, result.ErrMsg(i18n.TKey(language, err.Error()))) + return + } + c.JSON(200, result.Ok(nil)) +} + +// 信令跟踪续期保活 +// +// PUT /keep-alive +func (s *PacketController) KeepAlive(c *gin.Context) { + language := ctx.AcceptLanguage(c) + var body struct { + TaskNo string `json:"taskNo" binding:"required"` // 任务编号 + Duration int `json:"duration" ` // 服务失效的时间,默认设置为120秒 + } + if err := c.ShouldBindBodyWith(&body, binding.JSON); err != nil { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + + // 默认设置为120秒 + if body.Duration <= 1 { + body.Duration = 120 + } + + if err := s.packetService.LiveTimeout(body.TaskNo, body.Duration); err != nil { + c.JSON(200, result.ErrMsg(i18n.TKey(language, err.Error()))) + return + } + c.JSON(200, result.Ok(nil)) +} diff --git a/src/modules/trace/controller/tcpdump.go b/src/modules/trace/controller/tcpdump.go index c932dd01..0133f067 100644 --- a/src/modules/trace/controller/tcpdump.go +++ b/src/modules/trace/controller/tcpdump.go @@ -4,33 +4,35 @@ import ( "be.ems/src/framework/i18n" "be.ems/src/framework/utils/ctx" "be.ems/src/framework/vo/result" + neService "be.ems/src/modules/network_element/service" traceService "be.ems/src/modules/trace/service" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" ) -// 实例化控制层 TcpdumpController 结构体 -var NewTcpdump = &TcpdumpController{ - TcpdumpService: traceService.NewTcpdumpImpl, +// 实例化控制层 TCPdumpController 结构体 +var NewTCPdump = &TCPdumpController{ + tcpdumpService: traceService.NewTCPdump, + neInfoService: neService.NewNeInfo, } -// 信令抓包请求 +// 信令抓包 // // PATH /tcpdump -type TcpdumpController struct { - // 信令抓包服务 - TcpdumpService traceService.ITcpdump +type TCPdumpController struct { + tcpdumpService *traceService.TCPdump // 信令抓包服务 + neInfoService *neService.NeInfo // 网元信息服务 } // 网元抓包PACP 开始 // // POST /start -func (s *TcpdumpController) DumpStart(c *gin.Context) { +func (s *TCPdumpController) DumpStart(c *gin.Context) { language := ctx.AcceptLanguage(c) var body struct { NeType string `json:"neType" binding:"required"` // 网元类型 NeId string `json:"neId" binding:"required"` // 网元ID - Cmd string `json:"cmd" binding:"required"` // 命令 "-n -s 0 -v -w" + Cmd string `json:"cmd" binding:"required"` // 命令 "-n -s 0 -v" } err := c.ShouldBindBodyWith(&body, binding.JSON) if err != nil { @@ -38,32 +40,23 @@ func (s *TcpdumpController) DumpStart(c *gin.Context) { return } - fileName, err := s.TcpdumpService.DumpStart(body.NeType, body.NeId, body.Cmd) + taskCode, err := s.tcpdumpService.DumpStart(body.NeType, body.NeId, body.Cmd) if err != nil { - msg := err.Error() - if msg == "noData" { - // 找不到 %s %s 对应网元信息 - msg = i18n.TTemplate(language, "trace.tcpdump.noData", map[string]any{"type": body.NeType, "id": body.NeId}) - } - c.JSON(200, result.ErrMsg(msg)) + c.JSON(200, result.ErrMsg(i18n.TKey(language, err.Error()))) return } - c.JSON(200, result.OkData(map[string]any{ - "msg": "tcpdump started", - "out": fileName, - "log": "", - })) + c.JSON(200, result.OkData(taskCode)) } // 网元抓包PACP 结束 // // POST /stop -func (s *TcpdumpController) DumpStop(c *gin.Context) { +func (s *TCPdumpController) DumpStop(c *gin.Context) { language := ctx.AcceptLanguage(c) var body struct { - NeType string `json:"neType" binding:"required"` // 网元类型 - NeId string `json:"neId" binding:"required"` // 网元ID - FileName string `json:"fileName"` // 文件名 查看日志信息 + NeType string `json:"neType" binding:"required"` // 网元类型 + NeId string `json:"neId" binding:"required"` // 网元ID + TaskCode string `json:"taskCode" binding:"required"` // 任务码,停止任务并查看日志信息 } err := c.ShouldBindBodyWith(&body, binding.JSON) if err != nil { @@ -71,29 +64,18 @@ func (s *TcpdumpController) DumpStop(c *gin.Context) { return } - logMsg, err := s.TcpdumpService.DumpStop(body.NeType, body.NeId, body.FileName) + logFiles, err := s.tcpdumpService.DumpStop(body.NeType, body.NeId, body.TaskCode) if err != nil { - msg := err.Error() - if msg == "noData" { - // 找不到 %s %s 对应网元信息 - msg := i18n.TTemplate(language, "trace.tcpdump.noData", map[string]any{"type": body.NeType, "id": body.NeId}) - c.JSON(200, result.ErrMsg(msg)) - return - } - c.JSON(200, result.ErrMsg(msg)) + c.JSON(200, result.ErrMsg(i18n.TKey(language, err.Error()))) return } - c.JSON(200, result.OkData(map[string]any{ - "msg": "tcpdump stopped", - "out": body.FileName, - "log": logMsg, - })) + c.JSON(200, result.OkData(logFiles)) } // UPF标准版内部抓包 // -// POST /traceUPF -func (s *TcpdumpController) TraceUPF(c *gin.Context) { +// POST /upf +func (s *TCPdumpController) UPFTrace(c *gin.Context) { language := ctx.AcceptLanguage(c) var body struct { NeType string `json:"neType" binding:"required"` // 网元类型 @@ -106,19 +88,10 @@ func (s *TcpdumpController) TraceUPF(c *gin.Context) { return } - fileName, logMsg, err := s.TcpdumpService.DumpUPF(body.NeType, body.NeId, body.Cmd) + msg, err := s.tcpdumpService.UPFTrace(body.NeType, body.NeId, body.Cmd) if err != nil { - msg := err.Error() - if msg == "noData" { - // 找不到 %s %s 对应网元信息 - msg = i18n.TTemplate(language, "trace.tcpdump.noData", map[string]any{"type": body.NeType, "id": body.NeId}) - } - c.JSON(200, result.ErrMsg(msg)) + c.JSON(200, result.ErrMsg(i18n.TKey(language, err.Error()))) return } - c.JSON(200, result.OkData(map[string]any{ - "msg": "trace UPF dump pacp", - "out": fileName, - "log": logMsg, - })) + c.JSON(200, result.OkData(msg)) } diff --git a/src/modules/trace/controller/trace_data.go b/src/modules/trace/controller/trace_data.go new file mode 100644 index 00000000..2d8222b9 --- /dev/null +++ b/src/modules/trace/controller/trace_data.go @@ -0,0 +1,62 @@ +package controller + +import ( + "strings" + + "be.ems/src/framework/i18n" + "be.ems/src/framework/utils/ctx" + "be.ems/src/framework/utils/parse" + "be.ems/src/framework/vo/result" + traceService "be.ems/src/modules/trace/service" + "github.com/gin-gonic/gin" +) + +// 实例化控制层 TraceDataController 结构体 +var NewTraceData = &TraceDataController{ + traceDataService: traceService.NewTraceData, +} + +// 跟踪任务数据 +// +// PATH /data +type TraceDataController struct { + // 跟踪_数据信息服务 + traceDataService *traceService.TraceData +} + +// 跟踪任务数据列表 +// +// GET /list +func (s *TraceDataController) List(c *gin.Context) { + query := ctx.QueryMap(c) + + // 查询数据 + data := s.traceDataService.SelectPage(query) + c.JSON(200, result.Ok(data)) +} + +// 跟踪任务数据删除 +// +// DELETE /:ids +func (s *TraceDataController) Remove(c *gin.Context) { + language := ctx.AcceptLanguage(c) + rowIds := c.Param("ids") + if rowIds == "" { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + // 处理字符转id数组后去重 + ids := strings.Split(rowIds, ",") + uniqueIDs := parse.RemoveDuplicates(ids) + if len(uniqueIDs) <= 0 { + c.JSON(200, result.Err(nil)) + return + } + rows, err := s.traceDataService.DeleteByIds(uniqueIDs) + if err != nil { + c.JSON(200, result.ErrMsg(i18n.TKey(language, err.Error()))) + return + } + msg := i18n.TTemplate(language, "app.common.deleteSuccess", map[string]any{"num": rows}) + c.JSON(200, result.OkMsg(msg)) +} diff --git a/src/modules/trace/controller/trace_task.go b/src/modules/trace/controller/trace_task.go new file mode 100644 index 00000000..fc912eab --- /dev/null +++ b/src/modules/trace/controller/trace_task.go @@ -0,0 +1,155 @@ +package controller + +import ( + "fmt" + "path/filepath" + "runtime" + "strings" + + "be.ems/src/framework/i18n" + "be.ems/src/framework/utils/ctx" + "be.ems/src/framework/utils/parse" + "be.ems/src/framework/vo/result" + "be.ems/src/modules/trace/model" + traceService "be.ems/src/modules/trace/service" + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" +) + +// 实例化控制层 TraceTaskController 结构体 +var NewTraceTask = &TraceTaskController{ + traceTaskService: traceService.NewTraceTask, +} + +// 跟踪任务 +// +// PATH /task +type TraceTaskController struct { + // 跟踪_任务信息服务 + traceTaskService *traceService.TraceTask +} + +// 跟踪任务列表 +// +// GET /list +func (s *TraceTaskController) List(c *gin.Context) { + query := ctx.QueryMap(c) + + // 查询数据 + data := s.traceTaskService.SelectPage(query) + c.JSON(200, result.Ok(data)) +} + +// 跟踪任务信息 +// +// GET /:id +func (s *TraceTaskController) Info(c *gin.Context) { + language := ctx.AcceptLanguage(c) + id := c.Param("id") + if id == "" { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + + data := s.traceTaskService.SelectById(id) + if data.ID == id { + c.JSON(200, result.OkData(data)) + return + } + c.JSON(200, result.Err(nil)) +} + +// 跟踪任务新增 +// +// POST / +func (s *TraceTaskController) Add(c *gin.Context) { + language := ctx.AcceptLanguage(c) + var body model.TraceTask + err := c.ShouldBindBodyWith(&body, binding.JSON) + if err != nil || body.ID != "" { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + + body.CreateBy = ctx.LoginUserToUserName(c) + if err = s.traceTaskService.Insert(body); err != nil { + c.JSON(200, result.ErrMsg(i18n.TKey(language, err.Error()))) + return + } + c.JSON(200, result.Ok(nil)) +} + +// 跟踪任务修改 +// +// PUT / +func (s *TraceTaskController) Edit(c *gin.Context) { + language := ctx.AcceptLanguage(c) + var body model.TraceTask + err := c.ShouldBindBodyWith(&body, binding.JSON) + if err != nil || body.ID == "" { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + + // 检查是否存在 + taskInfo := s.traceTaskService.SelectById(body.ID) + if taskInfo.ID != body.ID { + // 没有可访问任务信息数据! + c.JSON(200, result.ErrMsg(i18n.TKey(language, "task.noData"))) + return + } + + body.UpdateBy = ctx.LoginUserToUserName(c) + if err = s.traceTaskService.Update(body); err != nil { + c.JSON(200, result.ErrMsg(i18n.TKey(language, err.Error()))) + return + } + c.JSON(200, result.Ok(nil)) +} + +// 跟踪任务删除 +// +// DELETE /:ids +func (s *TraceTaskController) Remove(c *gin.Context) { + language := ctx.AcceptLanguage(c) + rowIds := c.Param("ids") + if rowIds == "" { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + // 处理字符转id数组后去重 + ids := strings.Split(rowIds, ",") + uniqueIDs := parse.RemoveDuplicates(ids) + if len(uniqueIDs) <= 0 { + c.JSON(200, result.Err(nil)) + return + } + rows, err := s.traceTaskService.DeleteByIds(uniqueIDs) + if err != nil { + c.JSON(200, result.ErrMsg(i18n.TKey(language, err.Error()))) + return + } + msg := i18n.TTemplate(language, "app.common.deleteSuccess", map[string]any{"num": rows}) + c.JSON(200, result.OkMsg(msg)) +} + +// 跟踪任务文件 +// +// GET /filePull +func (s *TraceTaskController) FilePull(c *gin.Context) { + language := ctx.AcceptLanguage(c) + var querys struct { + TraceId string `form:"traceId" binding:"required"` + } + if err := c.ShouldBindQuery(&querys); err != nil { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + + fileName := fmt.Sprintf("task_%s.pcap", querys.TraceId) + localFilePath := filepath.Join("/tmp/omc/trace", fileName) + if runtime.GOOS == "windows" { + localFilePath = fmt.Sprintf("C:%s", localFilePath) + } + c.FileAttachment(localFilePath, fileName) +} diff --git a/src/modules/trace/controller/trace_task_hlr.go b/src/modules/trace/controller/trace_task_hlr.go new file mode 100644 index 00000000..07acc5ba --- /dev/null +++ b/src/modules/trace/controller/trace_task_hlr.go @@ -0,0 +1,240 @@ +package controller + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + + "be.ems/src/framework/i18n" + "be.ems/src/framework/utils/ctx" + "be.ems/src/framework/utils/generate" + "be.ems/src/framework/utils/parse" + "be.ems/src/framework/vo/result" + neService "be.ems/src/modules/network_element/service" + "be.ems/src/modules/trace/model" + traceService "be.ems/src/modules/trace/service" + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" +) + +// 实例化控制层 TraceTaskHlrController 结构体 +var NewTraceTaskHlr = &TraceTaskHlrController{ + neInfoService: neService.NewNeInfo, + traceTaskHlrService: traceService.NewTraceTaskHlr, +} + +// 跟踪任务网元HLR +// +// PATH /task/hlr +type TraceTaskHlrController struct { + neInfoService *neService.NeInfo // 网元信息服务 + traceTaskHlrService *traceService.TraceTaskHlr // 跟踪_任务给HRL网元信息服务 +} + +// 跟踪任务列表 +// +// GET /list +func (s *TraceTaskHlrController) List(c *gin.Context) { + language := ctx.AcceptLanguage(c) + var querys model.TraceTaskHlrQuery + if err := c.ShouldBindQuery(&querys); err != nil { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + + // 查询数据 + data := s.traceTaskHlrService.SelectPage(querys) + c.JSON(200, result.Ok(data)) +} + +// 跟踪任务删除 +// +// DELETE /:ids +func (s *TraceTaskHlrController) Remove(c *gin.Context) { + language := ctx.AcceptLanguage(c) + rowIds := c.Param("ids") + if rowIds == "" { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + // 处理字符转id数组后去重 + ids := strings.Split(rowIds, ",") + uniqueIDs := parse.RemoveDuplicates(ids) + if len(uniqueIDs) <= 0 { + c.JSON(200, result.Err(nil)) + return + } + rows, err := s.traceTaskHlrService.DeleteByIds(uniqueIDs) + if err != nil { + c.JSON(200, result.ErrMsg(i18n.TKey(language, err.Error()))) + return + } + msg := i18n.TTemplate(language, "app.common.deleteSuccess", map[string]any{"num": rows}) + c.JSON(200, result.OkMsg(msg)) +} + +// 跟踪任务创建 +// +// POST /start +func (s *TraceTaskHlrController) Start(c *gin.Context) { + language := ctx.AcceptLanguage(c) + var body struct { + IMSI string `json:"imsi"` // IMSI + MSISDN string `json:"msisdn"` // MSISDN + StartTime int64 `json:"startTime"` // 开始时间 + EndTime int64 `json:"endTime"` // 结束时间 + Remark string `json:"remark"` // 备注说明 + } + if err := c.ShouldBindBodyWith(&body, binding.JSON); err != nil { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + + if body.IMSI == "" && body.MSISDN == "" { + c.JSON(400, result.CodeMsg(400, "imsi amd msisdn is empty")) + return + } + + task := model.TraceTaskHlr{ + IMSI: body.IMSI, + MSISDN: body.MSISDN, + StartTime: body.StartTime, + EndTime: body.EndTime, + Remark: body.Remark, + CreateBy: ctx.LoginUserToUserName(c), + } + id, err := s.traceTaskHlrService.Start(task) + if err != nil { + c.JSON(200, result.ErrMsg(err.Error())) + return + } + c.JSON(200, result.OkData(id)) +} + +// 跟踪任务停止 +// +// POST /stop +func (s *TraceTaskHlrController) Stop(c *gin.Context) { + language := ctx.AcceptLanguage(c) + var body struct { + ID string `json:"id" binding:"required"` // 任务ID + } + if err := c.ShouldBindBodyWith(&body, binding.JSON); err != nil { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + + // 处理字符转id数组后去重 + ids := strings.Split(body.ID, ",") + uniqueIDs := parse.RemoveDuplicates(ids) + if len(uniqueIDs) <= 0 { + c.JSON(200, result.Err(nil)) + return + } + + errArr := []map[string]any{} + for _, id := range uniqueIDs { + task := s.traceTaskHlrService.SelectById(id) + if task.ID != id || task.ID == "" { + errArr = append(errArr, map[string]any{"id": id, "err": "task not found"}) + continue + } + + task.UpdateBy = ctx.LoginUserToUserName(c) + err := s.traceTaskHlrService.Stop(task) + if err != nil { + errArr = append(errArr, map[string]any{"id": id, "err": err.Error()}) + continue + } + } + c.JSON(200, result.OkData(errArr)) +} + +// 跟踪任务文件 +// +// POST /file +func (s *TraceTaskHlrController) File(c *gin.Context) { + language := ctx.AcceptLanguage(c) + var body struct { + ID string `json:"id" binding:"required"` // 任务ID + Dir string `json:"dir" binding:"required"` // 网元文件目录 + } + if err := c.ShouldBindBodyWith(&body, binding.JSON); err != nil { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + + task := s.traceTaskHlrService.SelectById(body.ID) + if task.ID != body.ID || task.ID == "" { + c.JSON(200, result.CodeMsg(400, "task not found")) + return + } + + list, err := s.traceTaskHlrService.File(task.TraceId, body.Dir) + if err != nil { + c.JSON(200, result.ErrMsg(err.Error())) + return + } + c.JSON(200, result.OkData(list)) +} + +// 跟踪任务文件从网元到本地 +// +// GET /filePull +func (s *TraceTaskHlrController) FilePull(c *gin.Context) { + language := ctx.AcceptLanguage(c) + var querys struct { + NeType string `form:"neType" binding:"required"` + NeID string `form:"neId" binding:"required"` + Path string `form:"path" binding:"required"` + FileName string `form:"fileName" binding:"required"` + DelTemp bool `form:"delTemp"` // 删除本地临时文件 + } + if err := c.ShouldBindQuery(&querys); err != nil { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + + // 查询网元获取IP + neInfo := s.neInfoService.SelectNeInfoByNeTypeAndNeID(querys.NeType, querys.NeID) + if neInfo.NeId != querys.NeID || neInfo.IP == "" { + c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) + return + } + + // 网元主机的SSH客户端 + sshClient, err := s.neInfoService.NeRunSSHClient(neInfo.NeType, neInfo.NeId) + if err != nil { + c.JSON(200, result.ErrMsg(err.Error())) + return + } + defer sshClient.Close() + // 网元主机的SSH客户端进行文件传输 + sftpClient, err := sshClient.NewClientSFTP() + if err != nil { + c.JSON(200, result.ErrMsg(err.Error())) + return + } + defer sftpClient.Close() + + nePath := filepath.ToSlash(filepath.Join(querys.Path, querys.FileName)) + fileName := generate.Code(6) + "_" + querys.FileName + localFilePath := filepath.Join("/tmp/omc/pull", fileName) + if runtime.GOOS == "windows" { + localFilePath = fmt.Sprintf("C:%s", localFilePath) + } + // 复制到本地 + if err = sftpClient.CopyFileRemoteToLocal(nePath, localFilePath); err != nil { + c.JSON(200, result.ErrMsg(err.Error())) + return + } + + defer func() { + if querys.DelTemp { + _ = os.Remove(localFilePath) + } + }() + c.FileAttachment(localFilePath, fileName) +} diff --git a/src/modules/trace/model/trace_data.go b/src/modules/trace/model/trace_data.go new file mode 100644 index 00000000..368d16e7 --- /dev/null +++ b/src/modules/trace/model/trace_data.go @@ -0,0 +1,23 @@ +package model + +// TraceData 跟踪_数据 trace_data +type TraceData struct { + ID string `json:"id" gorm:"column:id;primaryKey;autoIncrement"` + TaskId int64 `json:"taskId" gorm:"task_id"` // 任务ID + IMSI string `json:"imsi" gorm:"imsi"` + MSISDN string `json:"msisdn" gorm:"msisdn"` // 可能存在 + SrcAddr string `json:"srcAddr" gorm:"src_addr"` // 源地址带端口 + DstAddr string `json:"dstAddr" gorm:"dst_addr"` // 目标地址带端口 + IfType int64 `json:"ifType" gorm:"if_type"` // 接口类型,未分类 + MsgType int64 `json:"msgType" gorm:"msg_type"` + MsgDirect int64 `json:"msgDirect" gorm:"msg_direct"` + Length int64 `json:"length" gorm:"length"` // 去除头后的原始数据byte长度 + Timestamp int64 `json:"timestamp" gorm:"timestamp"` // 毫秒 + RawMsg string `json:"rawMsg" gorm:"raw_msg"` // 去除头后的原始数据byteBase64 + DecMsg string `json:"decMsg" gorm:"dec_msg"` // TCP内容消息 +} + +// TableName 表名称 +func (*TraceData) TableName() string { + return "trace_data" +} diff --git a/src/modules/trace/model/trace_task.go b/src/modules/trace/model/trace_task.go new file mode 100644 index 00000000..752d4f70 --- /dev/null +++ b/src/modules/trace/model/trace_task.go @@ -0,0 +1,31 @@ +package model + +// TraceTask 跟踪_任务 +type TraceTask struct { + ID string `json:"id" gorm:"column:id;primaryKey;autoIncrement"` // 跟踪任务ID + TraceId string `json:"traceId" gorm:"trace_id"` // 任务编号 + TraceType string `json:"traceType" gorm:"trace_type"` // 1-Interface,2-Device,3-User + StartTime int64 `json:"startTime" gorm:"start_time"` // 开始时间 毫秒 + EndTime int64 `json:"endTime" gorm:"end_time"` // 结束时间 毫秒 + Interfaces string `json:"interfaces" gorm:"interfaces"` // 接口跟踪必须 例如 N8,N10 + IMSI string `json:"imsi" gorm:"imsi"` // 用户跟踪必须 + MSISDN string `json:"msisdn" gorm:"msisdn"` // 用户跟踪可选 + UeIp string `json:"ueIp" gorm:"ue_ip"` // 设备跟踪必须 IP + SrcIp string `json:"srcIp" gorm:"src_ip"` // 源地址IP + DstIp string `json:"dstIp" gorm:"dst_ip"` // 目标地址IP + SignalPort int64 `json:"signalPort" gorm:"signal_port"` // 地址IP端口 + CreateBy string `json:"createBy" gorm:"create_by"` // 创建者 + CreateTime int64 `json:"createTime" gorm:"create_time"` // 创建时间 + UpdateBy string `json:"updateBy" gorm:"update_by"` // 更新者 + UpdateTime int64 `json:"updateTime" gorm:"update_time"` // 更新时间 + Remark string `json:"remark" gorm:"remark"` // 备注 + NeType string `json:"neType" gorm:"ne_type"` // 网元类型 + NeId string `json:"neId" gorm:"ne_id"` // 网元ID + NotifyUrl string `json:"notifyUrl" gorm:"notify_url"` // 信息数据通知回调地址UDP 例如udp:192.168.5.58:29500 + FetchMsg string `json:"fetchMsg" gorm:"fetch_msg"` // 任务下发请求响应消息 +} + +// TableName 表名称 +func (*TraceTask) TableName() string { + return "trace_task" +} diff --git a/src/modules/trace/model/trace_task_hlr.go b/src/modules/trace/model/trace_task_hlr.go new file mode 100644 index 00000000..2d306c4d --- /dev/null +++ b/src/modules/trace/model/trace_task_hlr.go @@ -0,0 +1,35 @@ +package model + +// TraceTaskHlr 跟踪_任务给HRL网元 trace_task_hlr +type TraceTaskHlr struct { + ID string `json:"id" gorm:"column:id;primaryKey;autoIncrement"` + TraceId string `json:"traceId" gorm:"trace_id"` // 任务编号 + IMSI string `json:"imsi" gorm:"imsi"` // IMSI + MSISDN string `json:"msisdn" gorm:"msisdn"` // MSISDN + StartTime int64 `json:"startTime" gorm:"start_time"` // 开始时间 + EndTime int64 `json:"endTime" gorm:"end_time"` // 结束时间 + Status string `json:"status" gorm:"status"` // 任务状态(0停止 1进行) + Msg string `json:"msg" gorm:"msg"` // 任务信息 + Remark string `json:"remark" gorm:"remark"` // 备注说明 + CreateBy string `json:"createBy" gorm:"create_by"` // 创建者 + CreateTime int64 `json:"createTime" gorm:"create_time"` // 创建时间 + UpdateBy string `json:"updateBy" gorm:"update_by"` // 更新者 + UpdateTime int64 `json:"updateTime" gorm:"update_time"` // 更新时间 +} + +// TableName 表名称 +func (*TraceTaskHlr) TableName() string { + return "trace_task_hlr" +} + +// TraceTaskHlrQuery 查询参数结构体 +type TraceTaskHlrQuery struct { + IMSI string `json:"imsi" form:"imsi"` // imsi + MSISDN string `json:"msisdn" form:"msisdn"` // msisdn + StartTime string `json:"startTime" form:"startTime"` + EndTime string `json:"endTime" form:"endTime"` + SortField string `json:"sortField" form:"sortField" binding:"omitempty,oneof=imsi msisdn"` // 排序字段,填写结果字段 + SortOrder string `json:"sortOrder" form:"sortOrder" binding:"omitempty,oneof=asc desc"` // 排序升降序,asc desc + PageNum int64 `json:"pageNum" form:"pageNum" binding:"required"` + PageSize int64 `json:"pageSize" form:"pageSize" binding:"required"` +} diff --git a/src/modules/trace/packet_task/packet.go b/src/modules/trace/packet_task/packet.go new file mode 100644 index 00000000..5df723b3 --- /dev/null +++ b/src/modules/trace/packet_task/packet.go @@ -0,0 +1,245 @@ +package packet_task + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + "sync" + "time" + + "be.ems/src/framework/logger" + wsService "be.ems/src/modules/ws/service" + "github.com/gopacket/gopacket" + "github.com/gopacket/gopacket/layers" + "github.com/gopacket/gopacket/pcap" + "github.com/gopacket/gopacket/pcapgo" +) + +// 捕获任务 +var taskMap sync.Map + +// task 任务信息 +type task struct { + TaskNo string // 任务编号 + Handle *pcap.Handle // 捕获句柄 + File *os.File // 捕获信息输出文件句柄 + Writer *pcapgo.Writer // 捕获信息输出句柄 + Filter string // 过滤表达式 + Ticker *time.Ticker // 任务失效定时器 +} + +// NetworkDevices 获取网卡设备信息 +func NetworkDevices() ([]pcap.Interface, error) { + devices, err := pcap.FindAllDevs() + if err != nil { + logger.Errorf("interfaces find all devices err: %s", err.Error()) + return nil, err + } + return devices, nil +} + +// verifyDevice 检查网卡设备是否存在 +func verifyDevice(str string) (string, bool) { + devices, err := pcap.FindAllDevs() + if err != nil { + logger.Errorf("interfaces find all devices err: %s", err.Error()) + return "", false + } + for _, device := range devices { + if len(device.Addresses) == 0 { + continue + } + if device.Name == str { + return device.Name, true + } + for _, address := range device.Addresses { + if address.IP.String() == str { + return device.Name, true + } + } + } + return "", false +} + +// outputPCAP 输出 pcap 文件 +// 新文件时需要 snaplen 最大长度 linktype 链路类型 +func outputPCAP(snaplen uint32, linktype layers.LinkType, outputFile string) (*os.File, *pcapgo.Writer, error) { + var err error + var f *os.File + if err := os.MkdirAll(filepath.Dir(outputFile), 0775); err != nil { + return nil, nil, err + } + + // 检查文件是否存在 + if _, err = os.Stat(outputFile); os.IsNotExist(err) { + f, err = os.Create(outputFile) + if err != nil { + return nil, nil, err + } + w := pcapgo.NewWriter(f) + w.WriteFileHeader(snaplen, linktype) // new file, must do this. + return f, w, nil + } + + f, err = os.OpenFile(outputFile, os.O_APPEND, 0700) + if err != nil { + return nil, nil, err + } + w := pcapgo.NewWriter(f) + return f, w, nil +} + +// capturePacketSource 捕获数据 +func capturePacketSource(taskInfo *task) { + // capture packets + packetSource := gopacket.NewPacketSource(taskInfo.Handle, taskInfo.Handle.LinkType()) + packetSource.Lazy = false + packetSource.NoCopy = true + packetSource.DecodeStreamsAsDatagrams = true + + // 协程停止后关闭句柄并移除任务信息 + defer func() { + taskInfo.Ticker.Stop() + taskInfo.Handle.Close() + if taskInfo.File != nil { + taskInfo.File.Close() + } + taskMap.Delete(taskInfo.TaskNo) + }() + + frameNumber := 0 // 帧编号 + frameTime := 0.000000 // 时间 + var startTimestamp time.Time // 开始时间 + + for { + select { + case <-taskInfo.Ticker.C: + return + case packet := <-packetSource.Packets(): + if packet == nil { + continue + } + // if packet.Metadata().Timestamp.Before(time.Now()) { + // continue + // } + if taskInfo.Writer != nil { + taskInfo.Writer.WritePacket(packet.Metadata().CaptureInfo, packet.Data()) + } + fmt.Println("---------- packet.Layers() ", len(packet.Layers())) + frameNumber++ // 帧编号 + currentTimestamp := packet.Metadata().Timestamp + if !startTimestamp.IsZero() { + // 计算时间差转换为秒 + frameTime = currentTimestamp.Sub(startTimestamp).Seconds() + } else { + startTimestamp = currentTimestamp + } + + // 数据 + frameMeta := parsePacketFrame(frameNumber, frameTime, packet) + + // 推送到ws订阅组 + wsService.NewWSSend.ByGroupID(fmt.Sprintf("%s%s", wsService.GROUP_TRACE_PACKET, taskInfo.TaskNo), frameMeta) + } + } +} + +// LiveStart 开始捕获数据 +func LiveStart(taskNo, deviceName, filterBPF string, outputFile bool) (string, error) { + if _, ok := taskMap.Load(taskNo); ok { + return "", fmt.Errorf("task no. %s already exist", taskNo) + } + + // Verify the specified network interface exists + device, deviceOk := verifyDevice(deviceName) + if !deviceOk { + return "", fmt.Errorf("network device not exist: %s", deviceName) + } + + snapshotLength := 262144 + + // open device + handle, err := pcap.OpenLive(device, int32(snapshotLength), true, pcap.BlockForever) + if err != nil { + logger.Errorf("open live err: %s", err.Error()) + if strings.Contains(err.Error(), "operation not permitted") { + return "", fmt.Errorf("you don't have permission to capture on that/these device(s)") + } + return "", err + } + + // write a new file + var w *pcapgo.Writer + var f *os.File + if outputFile { + // 网管本地路径 + localFilePath := fmt.Sprintf("/tmp/omc/packet/%s.pcap", taskNo) + if runtime.GOOS == "windows" { + localFilePath = fmt.Sprintf("C:%s", localFilePath) + } + f, w, err = outputPCAP(uint32(snapshotLength), handle.LinkType(), localFilePath) + if err != nil { + return "", err + } + } + + // set filter + if filterBPF != "" { + if err = handle.SetBPFFilter(filterBPF); err != nil { + logger.Errorf("packet BPF Filter %s => %s", filterBPF, err.Error()) + filterBPF = "" + } + } + + // save tasks + taskInfo := &task{ + TaskNo: taskNo, + Handle: handle, + File: f, + Writer: w, + Filter: filterBPF, + Ticker: time.NewTicker(time.Second * 120), + } + + go capturePacketSource(taskInfo) + taskMap.Store(taskNo, taskInfo) + return fmt.Sprintf("task no. %s initiated", taskNo), nil +} + +// LiveFilter 捕获过滤 +func LiveFilter(taskNo, expr string) error { + info, ok := taskMap.Load(taskNo) + if !ok { + return fmt.Errorf("task no. %s not exist", taskNo) + } + task := info.(*task) + task.Filter = expr + err := task.Handle.SetBPFFilter(expr) + if err != nil { + logger.Errorf("packet BPF Filter %s => %s", expr, err.Error()) + return fmt.Errorf("can't parse filter expression") + } + return nil +} + +// LiveTimeout 更新捕获失效时间 +func LiveTimeout(taskNo string, seconds int) error { + info, ok := taskMap.Load(taskNo) + if !ok { + return fmt.Errorf("task no. %s not exist", taskNo) + } + info.(*task).Ticker.Reset(time.Duration(seconds) * time.Second) + return nil +} + +// LiveStop 停止捕获数据 +func LiveStop(taskNo string) error { + info, ok := taskMap.Load(taskNo) + if !ok { + return fmt.Errorf("task no. %s not exist", taskNo) + } + info.(*task).Ticker.Reset(time.Millisecond) + return nil +} diff --git a/src/modules/trace/packet_task/packet_frame.go b/src/modules/trace/packet_task/packet_frame.go new file mode 100644 index 00000000..d060149d --- /dev/null +++ b/src/modules/trace/packet_task/packet_frame.go @@ -0,0 +1,843 @@ +package packet_task + +import ( + "encoding/base64" + "fmt" + "strings" + + "be.ems/src/framework/logger" + "github.com/gopacket/gopacket" + "github.com/gopacket/gopacket/layers" +) + +// FrameMeta 数据帧元信息 +type FrameMeta struct { + Number int `json:"number"` + Comments bool `json:"comments"` + Ignored bool `json:"ignored"` + Marked bool `json:"marked"` + Bg int `json:"bg"` // 背景色 数值转字符串16进制 15007687->e4ffc7 + Fg int `json:"fg"` // 前景色 文字 + Columns [7]string `json:"columns"` // 长度对应字段 ['No.', 'Time', 'Source', 'Destination', 'Protocol', 'Length', 'Info'] + Frame Frame `json:"frame"` +} + +// Frame 数据帧信息 +type Frame struct { + Number int `json:"number"` + Comments []string `json:"comments"` + DataSource []map[string]string `json:"data_sources"` + Tree []ProtoTree `json:"tree"` + Follow [][]string `json:"follow"` +} + +// ProtoTree 数据帧协议树 +type ProtoTree struct { + Label string `json:"label"` // 显示的文本 + Filter string `json:"filter"` // 过滤条件 + Severity string `json:"severity"` + Type string `json:"type"` + URL string `json:"url"` + Fnum int `json:"fnum"` + Start int `json:"start"` // 开始位置 + Length int `json:"length"` // 长度 + DataSourceIdx int `json:"data_source_idx"` + Tree []ProtoTree `json:"tree"` // 子节点 +} + +// parsePacketFrame 解析数据包帧信息 +// frameNumber 帧编号 i++ +// frameTime 时间秒 0.000000 +func parsePacketFrame(frameNumber int, frameTime float64, packet gopacket.Packet) FrameMeta { + frameSrcHost := "" // 源主机IP + frameDstHost := "" // 目的主机IP + frameProtocol := "" // 协议 + frameLength := fmt.Sprintf("%d", packet.Metadata().Length) // 长度 + frameInfo := "" // 信息 + fg, bg := colorRuleFB(packet) // 背景色 数值转字符串16进制 15007687->e4ffc7 + + frame := Frame{ + Number: frameNumber, + Comments: []string{}, + DataSource: []map[string]string{ + { + "name": fmt.Sprintf("Frame (%d bytes)", packet.Metadata().Length), + "data": base64.StdEncoding.EncodeToString(packet.Data()), + }, + }, + Tree: []ProtoTree{}, // 各层的数据 + Follow: [][]string{}, // {"TCP", "tcp.stream eq 0"} + } + + // 连接层 + // fmt.Println(packet.LinkLayer()) + if linkLayer := packet.LinkLayer(); linkLayer != nil { + linkTree := linkLayerTree(linkLayer) + frame.Tree = append(frame.Tree, linkTree) + } + + // 网络层 + // fmt.Println(packet.NetworkLayer()) + if networkLayer := packet.NetworkLayer(); networkLayer != nil { + networkTree := networkLayerTree(networkLayer) + frame.Tree = append(frame.Tree, networkTree) + + src, dst := networkLayer.NetworkFlow().Endpoints() + frameSrcHost = src.String() + frameDstHost = dst.String() + if frameDstHost == "ff:ff:ff:ff" { + frameDstHost = "Broadcast" + } + } + + // 传输层 + // fmt.Println(packet.TransportLayer()) + if transportLayer := packet.TransportLayer(); transportLayer != nil { + info, transportTree := transportLayerTree(transportLayer) + frame.Tree = append(frame.Tree, transportTree) + + frameProtocol = transportLayer.LayerType().String() + frameInfo += info + frame.Follow = append(frame.Follow, []string{ + frameProtocol, + fmt.Sprintf("%s.stream eq 0", strings.ToLower(frameProtocol)), + }) + } + + // 应用层 + // fmt.Println(packet.ApplicationLayer()) + if applicationLayer := packet.ApplicationLayer(); applicationLayer != nil { + applicationTree := applicationLayerTree(applicationLayer) + frame.Tree = append(frame.Tree, applicationTree) + } + + return FrameMeta{ + Number: frameNumber, + Comments: false, + Ignored: false, + Marked: false, + Bg: fg, + Fg: bg, + Columns: [7]string{ + fmt.Sprintf("%d", frameNumber), + fmt.Sprintf("%.6f", frameTime), // 格式化为 0.000000 + frameSrcHost, + frameDstHost, + frameProtocol, + frameLength, + frameInfo, + }, + Frame: frame, + } +} + +// linkLayerTree 连接层 +func linkLayerTree(linkLayer gopacket.LinkLayer) ProtoTree { + var protoTree ProtoTree + switch layer := linkLayer.(type) { + case *layers.Ethernet: // 最常见的链路层协议,用于局域网(LAN)中。 + srcMAC := layer.SrcMAC + dstMAC := layer.DstMAC + ethernetLayerLen := len(layer.Contents) + protoTree = ProtoTree{ + Label: fmt.Sprintf("%s II, Src: %s, Dst: %s", layer.LayerType(), srcMAC, dstMAC), + Filter: "eth", + Start: 0, + Length: ethernetLayerLen, + DataSourceIdx: 0, + Tree: []ProtoTree{ + { + Label: fmt.Sprintf("Destination: %s", dstMAC.String()), + Filter: fmt.Sprintf("eth.dst == %s", dstMAC.String()), + Start: 0, + Length: ethernetLayerLen, + DataSourceIdx: 0, + Tree: []ProtoTree{ + { + Label: fmt.Sprintf("Address: %s", dstMAC.String()), + Filter: fmt.Sprintf("eth.addr == %s", dstMAC.String()), + Start: 0, + Length: 6, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 926233912, + URL: "", + }, + { + Label: ".... ..0. .... .... .... .... = LG bit: Globally unique address (factory default)", + Filter: "eth.dst.lg == 0", + Start: 0, + Length: 3, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 926233912, + URL: "", + }, + { + Label: ".... ...0 .... .... .... .... = IG bit: Individual address (unicast)", + Filter: "eth.dst.ig == 0", + Start: 0, + Length: 3, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 926233912, + URL: "", + }, + }, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + { + Label: fmt.Sprintf("Source: %s", srcMAC.String()), + Filter: fmt.Sprintf("eth.src == %s", srcMAC.String()), + Start: ethernetLayerLen, + Length: ethernetLayerLen, + DataSourceIdx: 0, + Tree: []ProtoTree{ + { + Label: fmt.Sprintf("Address: %s", srcMAC.String()), + Filter: fmt.Sprintf("eth.addr == %s", dstMAC.String()), + Start: ethernetLayerLen, + Length: ethernetLayerLen, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 926233912, + URL: "", + }, + { + Label: ".... ..0. .... .... .... .... = LG bit: Globally unique address (factory default)", + Filter: "eth.src.lg == 0", + Start: len(srcMAC), + Length: len(srcMAC) / 2, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 926233912, + URL: "", + }, + { + Label: ".... ...0 .... .... .... .... = IG bit: Individual address (unicast)", + Filter: "eth.src.ig == 0", + Start: len(srcMAC), + Length: len(srcMAC) / 2, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 926233912, + URL: "", + }, + }, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + { + Label: "Type: IPv4 (0x0800)", + Filter: "eth.type == 0x0800", + Start: len(dstMAC) + len(srcMAC), + Length: len(layer.LayerContents()) - (len(dstMAC) + len(srcMAC)), + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + }, + Severity: "", + Type: "proto", + Fnum: 1052, + URL: "", + } + case *layers.PPP: // 点对点协议,通常用于拨号连接。 + protoTree = ProtoTree{ + Label: fmt.Sprintf("%s ", layer.LayerType()), + Filter: "ppp", + Start: 0, + Length: len(layer.LayerContents()), + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "proto", + Fnum: 1052, + URL: "", + } + } + return protoTree +} + +// networkLayerTree 网络层 +func networkLayerTree(networkLayer gopacket.NetworkLayer) ProtoTree { + var protoTree ProtoTree + switch layer := networkLayer.(type) { + case *layers.IPv4: // 第四版因特网协议,广泛使用。 + // 偏移量取连接层的长度Length + linkLayerLen := 14 + networkLayerLen := len(layer.Contents) + + version := layer.Version + length := layer.Length + srcIP := layer.SrcIP + dstIP := layer.DstIP + ihl := layer.IHL + headerLength := ihl * 4 // 提取头部长度 + tos := layer.TOS + dscp, ecn := networkDSCPAndECN(tos) + identification := layer.Id + flags := layer.Flags // 提取标志位 + // 生成标志描述 + flagsDesc := networkFlagsDesc(flags) + rb, rbDesc := networkFlagsEvil(flags) + df, dfDesc := networkFlagsDF(flags) + mf, mfDesc := networkFlagsMF(flags) + fragOffset := layer.FragOffset + fragOffsetDesc := networkOffset(fragOffset) + ttl := layer.TTL + proto := layer.Protocol + checksum := layer.Checksum + + protoTree = ProtoTree{ + Label: fmt.Sprintf("Internet Protocol Version %d, Src: %s, Dst: %s", version, srcIP, dstIP), + Filter: "ip", + Start: linkLayerLen, + Length: networkLayerLen, + DataSourceIdx: 0, + Tree: []ProtoTree{ + { + Label: fmt.Sprintf("%04b .... = Version: %d", version, version), + Filter: fmt.Sprintf("ip.version == %d", version), + Start: linkLayerLen, + Length: 1, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + { + Label: fmt.Sprintf(".... 0101 = Header Length: %d bytes (%d)", headerLength, ihl), + Filter: fmt.Sprintf("ip.hdr_len == %d", headerLength), + Start: linkLayerLen, + Length: 1, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + { + Label: fmt.Sprintf("Differentiated Services Field: 0x%02x (DSCP: %s, ECN: %s)", tos, dscp, ecn), + Filter: fmt.Sprintf("ip.dsfield == 0x%02x", tos), + Start: linkLayerLen + 1, + Length: 1, + DataSourceIdx: 0, + Tree: []ProtoTree{ + { + Label: fmt.Sprintf("0000 00.. = Differentiated Services Codepoint: %s (%d)", dscp, tos), + Filter: fmt.Sprintf("ip.dsfield.dscp == %d", tos>>2), + Start: linkLayerLen + 1, + Length: 1, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 926233912, + URL: "", + }, + { + Label: fmt.Sprintf(".... ..00 = Explicit Congestion Notification: %s Capable Transport (%d)", ecn, tos), + Filter: fmt.Sprintf("ip.dsfield.ecn == %d", tos&0x03), + Start: linkLayerLen + 1, + Length: 1, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 926233912, + URL: "", + }, + }, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + { + Label: fmt.Sprintf("Total Length: %d", length), + Filter: fmt.Sprintf("ip.len == %d", length), + Start: linkLayerLen + 2, + Length: 2, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + { + Label: fmt.Sprintf("Identification: 0x%X (%d)", identification, identification), + Filter: fmt.Sprintf("ip.id == 0x%X", identification), + Start: linkLayerLen + 4, + Length: 2, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + { + Label: fmt.Sprintf("%03b. .... = Flags: %s", flags, flagsDesc), + Filter: fmt.Sprintf("ip.flags == 0x%X", flags), + Start: linkLayerLen + 6, + Length: 1, + DataSourceIdx: 0, + Tree: []ProtoTree{ + { + Label: fmt.Sprintf("0... .... = Reserved bit: %s", rbDesc), + Filter: fmt.Sprintf("ip.flags.rb == %d", rb), + Start: linkLayerLen + 6, + Length: 1, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 926233912, + URL: "", + }, + { + Label: fmt.Sprintf(".1.. .... = Don't fragment: %s", dfDesc), + Filter: fmt.Sprintf("ip.flags.df == %d", df), + Start: linkLayerLen + 6, + Length: 1, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 926233912, + URL: "", + }, + { + Label: fmt.Sprintf("..0. .... = More fragments: %s", mfDesc), + Filter: fmt.Sprintf("ip.flags.mf == %d", mf), + Start: linkLayerLen + 6, + Length: 1, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 926233912, + URL: "", + }, + }, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + { + Label: fmt.Sprintf("%s = Fragment Offset: %d", fragOffsetDesc, fragOffset), + Filter: fmt.Sprintf("ip.frag_offset == %d", fragOffset), + Start: linkLayerLen + 6, + Length: 2, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + { + Label: fmt.Sprintf("Time to Live: %d", ttl), + Filter: fmt.Sprintf("ip.ttl == %d", ttl), + Start: linkLayerLen + 8, + Length: 1, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + { + Label: fmt.Sprintf("Protocol: TCP (%d)", proto), + Filter: fmt.Sprintf("ip.proto == %d", proto), + Start: linkLayerLen + 9, + Length: 1, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + { + Label: fmt.Sprintf("Header Checksum: 0x%x [validation disabled]", checksum), + Filter: fmt.Sprintf("ip.checksum == 0x%x", checksum), + Start: linkLayerLen + 10, + Length: 2, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + { + Label: "Header checksum status: Unverified", + Filter: "ip.checksum.status == \"Unverified\"", + Start: 0, + Length: 0, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + { + Label: fmt.Sprintf("Source Address: %s", srcIP), + Filter: fmt.Sprintf("ip.src == %s", srcIP), + Start: linkLayerLen + 12, + Length: 4, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + { + Label: fmt.Sprintf("Destination Address: %s", dstIP), + Filter: fmt.Sprintf("ip.dst == %s", dstIP), + Start: linkLayerLen + 16, + Length: 4, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + }, + Severity: "", + Type: "proto", + Fnum: 1052, + URL: "", + } + + logger.Infof("-> (tos 0x%x, ttl %d, id %d, offset %d, flags [%s], proto %s (%d), length %d)\n", tos, ttl, identification, fragOffset, flags, proto, proto, len(layer.Contents)+len(layer.Payload)) + case *layers.IPv6: // 第六版因特网协议,逐渐取代 IPv4。 + logger.Infof("-> (flowlabel 0x%x, hlim %d, next-header %s (%d), payload length: %d)\n", layer.FlowLabel, layer.HopLimit, layer.NextHeader, layer.NextHeader, len(layer.Payload)) + } + return protoTree +} + +// transportLayerTree 传输层 +func transportLayerTree(transportLayer gopacket.TransportLayer) (string, ProtoTree) { + var info string + var tree ProtoTree + switch layer := transportLayer.(type) { + case *layers.TCP: // 传输控制协议,提供可靠的数据传输。 + // 偏移量取连接层加网络层的长度Length + linkLayerAndNetworkLayerLen := 14 + 20 + transportLayerLen := len(layer.Contents) + payloadrLen := len(layer.Payload) + seq := layer.Seq + ack := layer.Ack + srcPort := layer.SrcPort + dstPort := layer.DstPort + dataOffset := layer.DataOffset + hdrLen := dataOffset * 4 + flags, flagsDesc := transportFlagsDesc(layer) + flagsACK, flagsACKDesc := transportFlagsStatus(layer.ACK) + flagsPSH, flagsPSHDesc := transportFlagsStatus(layer.PSH) + window := layer.Window + checksum := layer.Checksum + urgent := layer.Urgent + optionsLen, optionsDesc := transportOptions(layer.Options) + payloadStr := bytesToHexString(layer.Payload) + + tree = ProtoTree{ + Label: fmt.Sprintf("Transmission Control Protocol, Src Port: %s, Dst Port: %s, Seq: %d, Ack: %d, Len: %d", srcPort, dstPort, seq, ack, payloadrLen), + Filter: "tcp", + Start: linkLayerAndNetworkLayerLen, + Length: transportLayerLen, + DataSourceIdx: 0, + Tree: []ProtoTree{ + { + Label: fmt.Sprintf("Source Port: %s", srcPort), + Filter: fmt.Sprintf("tcp.srcport == %d", srcPort), + Start: linkLayerAndNetworkLayerLen, + Length: 2, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + { + Label: fmt.Sprintf("Destination Port: %s", dstPort), + Filter: fmt.Sprintf("tcp.dstport == %d", dstPort), + Start: linkLayerAndNetworkLayerLen + 2, + Length: 2, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + { + Label: fmt.Sprintf("TCP Segment Len: %d", payloadrLen), + Filter: fmt.Sprintf("tcp.len == %d", payloadrLen), + Start: linkLayerAndNetworkLayerLen + 12, + Length: 1, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + { + Label: fmt.Sprintf("Sequence Number: %d (relative sequence number)", seq), + Filter: fmt.Sprintf("tcp.seq == %d", seq), + Start: linkLayerAndNetworkLayerLen + 4, + Length: 4, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + { + Label: fmt.Sprintf("Acknowledgment Number: %d (relative ack number)", ack), + Filter: fmt.Sprintf("tcp.ack == %d", ack), + Start: linkLayerAndNetworkLayerLen + 8, + Length: 4, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + { + Label: fmt.Sprintf("%04b .... = Header Length: %d bytes (%d)", dataOffset, hdrLen, dataOffset), + Filter: fmt.Sprintf("tcp.hdr_len == %d", hdrLen), + Start: linkLayerAndNetworkLayerLen + 12, + Length: 1, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + { + Label: fmt.Sprintf("Flags: 0x%03X (%s)", flags, flagsDesc), + Filter: fmt.Sprintf("ip.frag_offset == 0x%03X", flags), + Start: linkLayerAndNetworkLayerLen + 12, + Length: 2, + DataSourceIdx: 0, + Tree: []ProtoTree{ + { + Label: fmt.Sprintf(".... ...%d .... = Acknowledgment: %s", flagsACK, flagsACKDesc), + Filter: fmt.Sprintf("tcp.flags.ack == %d", flagsACK), + Start: linkLayerAndNetworkLayerLen + 13, + Length: 1, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 926233912, + URL: "", + }, + { + Label: fmt.Sprintf(".... .... %d... = Push: %s", flagsPSH, flagsPSHDesc), + Filter: fmt.Sprintf("tcp.flags.push == %d", flagsPSH), + Start: linkLayerAndNetworkLayerLen + 13, + Length: 1, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 926233912, + URL: "", + }, + }, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + { + Label: fmt.Sprintf("Window: %d", window), + Filter: fmt.Sprintf("tcp.window_size_value == %d", window), + Start: linkLayerAndNetworkLayerLen + 14, + Length: 2, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + { + Label: fmt.Sprintf("Calculated window size: %d", window), + Filter: fmt.Sprintf("tcp.window_size == %d", window), + Start: linkLayerAndNetworkLayerLen + 14, + Length: 2, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + { + Label: fmt.Sprintf("Checksum: 0x%04x [unverified]", checksum), + Filter: fmt.Sprintf("tcp.checksum == 0x%04x", checksum), + Start: linkLayerAndNetworkLayerLen + 16, + Length: 2, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + { + Label: "Checksum Status: Unverified", + Filter: "tcp.checksum.status == \"Unverified\"", + Start: 0, + Length: 0, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + { + Label: fmt.Sprintf("Urgent Pointer: %d", urgent), + Filter: fmt.Sprintf("tcp.urgent_pointer == %d", urgent), + Start: linkLayerAndNetworkLayerLen + 18, + Length: 2, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + { + Label: fmt.Sprintf("Options: (%d bytes), %s", optionsLen, optionsDesc), + Filter: fmt.Sprintf("tcp.options == %d", optionsLen), + Start: linkLayerAndNetworkLayerLen + 20, + Length: int(optionsLen), + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + { + Label: fmt.Sprintf("TCP payload (%d bytes)", payloadrLen), + Filter: fmt.Sprintf("tcp.payload == %s", payloadStr), + Start: linkLayerAndNetworkLayerLen + 32, + Length: payloadrLen, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "", + Fnum: 0, + URL: "", + }, + }, + Severity: "", + Type: "proto", + Fnum: 1052, + URL: "", + } + + info = fmt.Sprintf("%v -> %v [%s], Seq=%d Ack=%d Win=%d Len=1%d ", srcPort, dstPort, flagsDesc, seq, ack, window, payloadrLen) + logger.Infof("-> TCP, %s", info) + case *layers.UDP: // 用户数据报协议,提供无连接的快速数据传输。 + logger.Infof("-> UDP, length %d", len(layer.Payload)) + case *layers.UDPLite: + logger.Infof("-> UDPLite, length %d", len(layer.Payload)) + case *layers.SCTP: // 流控制传输协议,支持多流和多宿主机。 + logger.Infof("-> SCTP, length %d", len(layer.Payload)) + } + return info, tree +} + +// applicationLayerTree 应用层 +func applicationLayerTree(applicationLayer gopacket.ApplicationLayer) ProtoTree { + var protoTree ProtoTree + switch layer := applicationLayer.(type) { + case *layers.DNS: + logger.Infof("-> DNS, %d", layer.ID) + case *layers.SIP: + logger.Infof("-> SIP, %s", layer.RequestURI) + default: + logger.Infof("-> %s, length %d", layer.LayerType(), layer.Payload()) + if applicationHTTP(layer.LayerContents()) { + logger.Infof("-> HTTP, %s", layer.LayerContents()) + // 偏移量取连接层加网络层加协议层的长度Length + linkLayerAndNetworkLayerAndTransportLayerLen := 14 + 20 + 32 + length := len(layer.LayerContents()) + + protoTree = ProtoTree{ + Label: "Hypertext Transfer Protocol", + Filter: "http", + Start: linkLayerAndNetworkLayerAndTransportLayerLen, + Length: length, + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "Chat", + Fnum: 1052, + URL: "", + } + + result := applicationHTTPProcess(string(layer.LayerContents())) + for _, v := range result { + protoTree.Tree = append(protoTree.Tree, ProtoTree{ + Label: v["label"].(string), + Filter: fmt.Sprintf("http.%s == %s", v["key"].(string), v["value"].(string)), + Start: linkLayerAndNetworkLayerAndTransportLayerLen + v["length"].(int), + Length: v["length"].(int), + DataSourceIdx: 0, + Tree: []ProtoTree{}, + Severity: "", + Type: "Chat", + Fnum: 1052, + URL: "", + }) + } + + } + } + return protoTree +} diff --git a/src/modules/trace/packet_task/packet_frame_util.go b/src/modules/trace/packet_task/packet_frame_util.go new file mode 100644 index 00000000..77d86a9f --- /dev/null +++ b/src/modules/trace/packet_task/packet_frame_util.go @@ -0,0 +1,346 @@ +package packet_task + +import ( + "encoding/binary" + "fmt" + "net" + "strings" + + "github.com/gopacket/gopacket" + "github.com/gopacket/gopacket/layers" +) + +// networkDSCPAndECN 提取 TOS 字段并获取 DSCP 和 ECN +func networkDSCPAndECN(tos uint8) (string, string) { + // 提取 DSCP 和 ECN + dscp := tos >> 2 // 高 6 位 + ecn := tos & 0x03 // 低 2 位 + + // 定义 DSCP 映射 + dscpMapping := map[uint8]string{ + 0: "Default CS0", // Default Forwarding (DF) + 8: "CS1", // Class Selector 1 + 16: "CS2", // Class Selector 2 + 24: "CS3", // Class Selector 3 + 32: "CS4", // Class Selector 4 + 40: "CS5", // Class Selector 5 + 48: "CS6", // Class Selector 6 + 56: "CS7", // Class Selector 7 + } + + // 定义 ECN 映射 + ecnMapping := map[uint8]string{ + 0: "Not-ECT", // Not ECN-Capable Transport + 1: "ECT(1)", // ECN-Capable Transport + 2: "ECT(0)", // ECN-Capable Transport + 3: "CE", // Congestion Experienced + } + + // 返回可读的 DSCP 和 ECN 字符串 + return dscpMapping[dscp], ecnMapping[ecn] +} + +// networkFlagsDesc 生成标志描述 +func networkFlagsDesc(flags layers.IPv4Flag) string { + f := fmt.Sprintf("Flags: 0x%X", flags) + if flags&layers.IPv4DontFragment != 0 { + f += ", Don't fragment" + } + if flags&layers.IPv4MoreFragments != 0 { + f += ", More fragments" + } + return f +} + +// networkFlagsEvil 生成标志描述 Evil +func networkFlagsEvil(flags layers.IPv4Flag) (int, string) { + if flags&layers.IPv4EvilBit != 0 { + return 1, "Set" + } + return 0, "Not set" +} + +// networkFlagsDF 生成标志描述 DF +func networkFlagsDF(flags layers.IPv4Flag) (int, string) { + if flags&layers.IPv4DontFragment != 0 { + return 1, " Set" + } + return 0, "Not set" +} + +// networkFlagsMF 生成标志描述 MF +func networkFlagsMF(flags layers.IPv4Flag) (int, string) { + if flags&layers.IPv4MoreFragments != 0 { + return 1, " Set" + } + return 0, "Not set" +} + +// networkOffset 二进制Fragment Offset表示 ...0 0000 0000 0000 +func networkOffset(offset uint16) string { + return fmt.Sprintf("...0 %04b %04b %04b %04b", + (offset>>12)&0xF, // 高四位 + (offset>>8)&0xF, // 次四位 + (offset>>4)&0xF, // 再次四位 + offset&0xF, // 低四位 + ) +} + +// transportFlagsDesc 生成标志描述 +func transportFlagsDesc(layer *layers.TCP) (byte, string) { + var flags byte + var flagsDesc []string + if layer.FIN { + flags |= 1 << 0 // 0b00000001 + flagsDesc = append(flagsDesc, "FIN") + } + if layer.SYN { + flags |= 1 << 1 // 0b00000010 + flagsDesc = append(flagsDesc, "SYN") + } + if layer.RST { + flags |= 1 << 2 // 0b00000100 + flagsDesc = append(flagsDesc, "RST") + } + if layer.PSH { + flags |= 1 << 3 // 0b00001000 + flagsDesc = append(flagsDesc, "PSH") + } + if layer.ACK { + flags |= 1 << 4 // 0b00010000 + flagsDesc = append(flagsDesc, "ACK") + } + if layer.URG { + flags |= 1 << 5 // 0b00100000 + flagsDesc = append(flagsDesc, "URG") + } + if layer.ECE { + flags |= 1 << 6 // 0b01000000 + flagsDesc = append(flagsDesc, "ECE") + } + if layer.CWR { + flags |= 1 << 7 // 0b10000000 + flagsDesc = append(flagsDesc, "CWR") + } + if layer.NS { + flagsDesc = append(flagsDesc, "NS") + } + + return flags, strings.Join(flagsDesc, ", ") +} + +// transportFlagsStatus 生成标志描述状态 +func transportFlagsStatus(flag bool) (int, string) { + if flag { + return 1, " Set" + } + return 0, "Not set" +} + +// bytesToHexString 转换为十六进制字符串格式 +func bytesToHexString(data []byte) string { + var sb strings.Builder + for i, b := range data { + if i > 0 { + sb.WriteString(":") + } + sb.WriteString(fmt.Sprintf("%02x", b)) + } + return sb.String() +} + +// transportOptions 生成头部选项描述 +func transportOptions(options []layers.TCPOption) (uint8, string) { + var opts []string + var optLen uint8 + for _, opt := range options { + if opt.OptionType == layers.TCPOptionKindMSS && len(opt.OptionData) == 2 { + optLen += opt.OptionLength + opts = append(opts, fmt.Sprintf("%s val %v", + opt.OptionType.String(), + binary.BigEndian.Uint16(opt.OptionData), + )) + } else if opt.OptionType == layers.TCPOptionKindTimestamps && len(opt.OptionData) == 8 { + optLen += opt.OptionLength + opts = append(opts, fmt.Sprintf("%s val %v echo %v", + opt.OptionType.String(), + binary.BigEndian.Uint32(opt.OptionData[:4]), + binary.BigEndian.Uint32(opt.OptionData[4:8]), + )) + } else { + optLen += opt.OptionLength + opts = append(opts, opt.OptionType.String()) + } + } + return optLen, strings.Join(opts, ", ") +} + +// applicationHTTP 辨别 HTTP 数据 +func applicationHTTP(data []byte) bool { + if len(data) == 0 { + return false + } + prefixStr := string(data) + return strings.HasPrefix(prefixStr, "GET ") || strings.HasPrefix(prefixStr, "POST ") || + strings.HasPrefix(prefixStr, "PUT ") || strings.HasPrefix(prefixStr, "DELETE ") || + strings.HasPrefix(prefixStr, "HEAD ") || strings.HasPrefix(prefixStr, "OPTIONS ") || + strings.HasPrefix(prefixStr, "HTTP/") +} + +// applicationHTTP 处理 HTTP 请求 +func applicationHTTPProcess(data string) map[string]map[string]any { + p := make(map[string]map[string]any, 0) + // 按行分割 + lines := strings.Split(data, "\r\n") + for i, line := range lines { + if i == 0 { + label := line + "\r\n" + p[label] = map[string]any{ + "label": label, + "length": len([]byte(label)), + "key": "", + "value": "", + } + continue + } + + // 空行表示头部结束,Body开始 + if line == "" { + break + } + + label := line + "\r\n" + p[label] = map[string]any{ + "label": label, + "length": len([]byte(label)), + "key": "", + "value": "", + } + + // 分割键值对 + parts := strings.SplitN(line, ":", 2) + if len(parts) == 2 { + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + p[label]["key"] = key + p[label]["value"] = value + } + } + return p +} + +// colorRuleFB 着色规则-F前景,B背景 +// +// This file was created by Wireshark. Edit with care. +func colorRuleFB(packet gopacket.Packet) (int, int) { + // Ethernet + if ethernetLayer := packet.Layer(layers.LayerTypeEthernet); ethernetLayer != nil { + eth := ethernetLayer.(*layers.Ethernet) + ethData := eth.Contents + // Broadcast 检查第一个字节的最低位 + // #babdb6, #ffffff + if len(ethData) > 0 && (ethData[0]&1) == 1 { + return 12238262, 16777215 + } + // Routing CDP (Cisco Discovery Protocol) 检查前三个字节 + // #12272e, #fff3d6 + if ethernetLayer.LayerPayload()[0] == 0x01 && ethernetLayer.LayerPayload()[1] == 0x00 && ethernetLayer.LayerPayload()[2] == 0x0c { + return 1189678, 16774102 + } + // Routing CARP (Common Address Redundancy Protocol) uses a specific Ethernet type (0x0800) + // #12272e, #fff3d6 + if ethernetLayer.LayerType() == 0x0800 { + return 1189678, 16774102 + } + } + // ARP + if arpLayer := packet.Layer(layers.LayerTypeARP); arpLayer != nil { + // #12272e, #faf0d7 + return 1189678, 16445655 + } + // ICMP + if icmpLayer := packet.Layer(layers.LayerTypeICMPv4); icmpLayer != nil { + // #12272e, #fce0ff + return 1189678, 16572671 + } + if icmpLayer := packet.Layer(layers.LayerTypeICMPv6); icmpLayer != nil { + // #12272e, #fce0ff + return 1189678, 16572671 + } + // SCTP + if sctpLayer := packet.Layer(layers.LayerTypeSCTP); sctpLayer != nil { + sctp := sctpLayer.(*layers.SCTP) + // SCTP ABORT + // #fffc9c, #a40000 + if sctp.Checksum == 6 { + return 16776348, 10747904 + } + } + // TCP + if tcpLayer := packet.Layer(layers.LayerTypeTCP); tcpLayer != nil { + tcp := tcpLayer.(*layers.TCP) + // TCP SYN/FIN + // #12272e, #a0a0a0 + if tcp.SYN && tcp.FIN { + return 1189678, 10526880 + } + // TCP RST + // #fffc9c, #a40000 + if tcp.RST { + return 16776348, 10747904 + } + // HTTP + // #12272e, #e4ffc7 + if tcp.SrcPort == 80 || tcp.DstPort == 80 || tcp.SrcPort == 443 || tcp.DstPort == 443 { + return 1189678, 15007687 + } + // 检查 SMB - 通常基于 TCP 445 或 139 + // #12272e, #feffd0 + if tcp.SrcPort == 445 || tcp.DstPort == 445 || tcp.SrcPort == 139 || tcp.DstPort == 139 { + return 1189678, 16711632 + } + // Routing BGP usually runs on TCP port 179 + // #12272e, #fff3d6 + if tcp.DstPort == 179 || tcp.SrcPort == 179 { + return 1189678, 16774102 + } + } + // UDP + if udpLayer := packet.Layer(layers.LayerTypeUDP); udpLayer != nil { + udp := udpLayer.(*layers.UDP) + // 检查 SMB NetBIOS 名称服务 (NBNS) - 端口 53 + // 检查 SMB NetBIOS 数据报服务 (NBDS) - 端口 138 + if udp.SrcPort == 53 || udp.DstPort == 53 || udp.SrcPort == 138 || udp.DstPort == 138 { + return 1189678, 16711632 + } + } + // IPv4 + if ipv4Layer := packet.Layer(layers.LayerTypeIPv4); ipv4Layer != nil { + ipv4 := ipv4Layer.(*layers.IPv4) + // TCP(6) + // #12272e, #e7e6ff + if ipv4.Protocol == layers.IPProtocolTCP { + return 1189678, 15197951 + } + // UDP(17) + // #12272e, #daeeff + if ipv4.Protocol == layers.IPProtocolUDP || ipv4.Protocol == layers.IPProtocolUDPLite { + return 1189678, 14348031 + } + // Routing EIGRP(0x2f) OSPF(89) + // #12272e, #fff3d6 + if ipv4.Protocol == 0x2f || ipv4.Protocol == layers.IPProtocolOSPF { + return 1189678, 16774102 + } + // Routing + // GVRP (GARP VLAN Registration Protocol) + // GVRP typically utilizes the same multicast address as GARP + // HSRP (Hot Standby Router Protocol) uses multicast IP 224.0.0.2 + // VRRP (Virtual Router Redundancy Protocol) uses multicast IP 224.0.0.18 + // #12272e, #fff3d6 + if ipv4.DstIP.Equal(net.IPv4(224, 0, 0, 2)) || ipv4.DstIP.Equal(net.IPv4(224, 0, 0, 100)) { + return 1189678, 16774102 + } + } + return 16222087, 1189678 // 默认颜色值 #f78787, #12272e +} diff --git a/src/modules/trace/repository/trace_data.go b/src/modules/trace/repository/trace_data.go new file mode 100644 index 00000000..c1e791b4 --- /dev/null +++ b/src/modules/trace/repository/trace_data.go @@ -0,0 +1,246 @@ +package repository + +import ( + "strings" + + "be.ems/src/framework/datasource" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/parse" + "be.ems/src/framework/utils/repo" + "be.ems/src/modules/trace/model" +) + +// 实例化数据层 TraceData 结构体 +var NewTraceData = &TraceData{ + selectSql: `select id, task_id, imsi, msisdn, src_addr, dst_addr, if_type, msg_type, msg_direct, length, timestamp, raw_msg, dec_msg from trace_data`, + + resultMap: map[string]string{ + "id": "ID", + "task_id": "TaskId", + "imsi": "IMSI", + "msisdn": "MSISDN", + "src_addr": "SrcAddr", + "dst_addr": "DstAddr", + "if_type": "IfType", + "msg_type": "MsgType", + "msg_direct": "MsgDirect", + "length": "Length", + "timestamp": "Timestamp", + "raw_msg": "RawMsg", + "dec_msg": "DecMsg", + }, +} + +// CDREventIMSImpl 跟踪_任务给HRL网元 数据层处理 +type TraceData struct { + // 查询视图对象SQL + selectSql string + // 结果字段与实体映射 + resultMap map[string]string +} + +// convertResultRows 将结果记录转实体结果组 +func (r *TraceData) convertResultRows(rows []map[string]any) []model.TraceData { + arr := make([]model.TraceData, 0) + for _, row := range rows { + item := model.TraceData{} + for key, value := range row { + if keyMapper, ok := r.resultMap[key]; ok { + repo.SetFieldValue(&item, keyMapper, value) + } + } + arr = append(arr, item) + } + return arr +} + +// SelectPage 根据条件分页查询 +func (r *TraceData) SelectPage(query map[string]any) map[string]any { + // 查询条件拼接 + var conditions []string + var params []any + if v, ok := query["imsi"]; ok && v != "" { + conditions = append(conditions, "imsi like concat(?, '%')") + params = append(params, v) + } + if v, ok := query["msisdn"]; ok && v != "" { + conditions = append(conditions, "msisdn like concat(?, '%')") + params = append(params, v) + } + if v, ok := query["startTime"]; ok && v != "" { + conditions = append(conditions, "timestamp >= ?") + params = append(params, v) + } + if v, ok := query["endTime"]; ok && v != "" { + conditions = append(conditions, "timestamp <= ?") + params = append(params, v) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + result := map[string]any{ + "total": 0, + "rows": []model.TraceData{}, + } + + // 查询数量 长度为0直接返回 + totalSql := "select count(1) as 'total' from trace_data" + totalRows, err := datasource.RawDB("", totalSql+whereSql, params) + if err != nil { + logger.Errorf("total err => %v", err) + return result + } + total := parse.Number(totalRows[0]["total"]) + if total == 0 { + return result + } else { + result["total"] = total + } + + // 分页 + pageNum, pageSize := repo.PageNumSize(query["pageNum"], query["pageSize"]) + pageSql := " limit ?,? " + params = append(params, pageNum*pageSize) + params = append(params, pageSize) + + // 查询数据 + querySql := r.selectSql + whereSql + pageSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + } + + // 转换实体 + result["rows"] = r.convertResultRows(results) + return result +} + +// SelectList 根据实体查询 +func (r *TraceData) SelectList(data model.TraceData) []model.TraceData { + // 查询条件拼接 + var conditions []string + var params []any + if data.IMSI != "" { + conditions = append(conditions, "imsi = ?") + params = append(params, data.IMSI) + } + if data.MSISDN != "" { + conditions = append(conditions, "msisdn = ?") + params = append(params, data.MSISDN) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询数据 + querySql := r.selectSql + whereSql + " order by id desc " + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + } + + // 转换实体 + return r.convertResultRows(results) +} + +// SelectByIds 通过ID查询 +func (r *TraceData) SelectByIds(ids []string) []model.TraceData { + placeholder := repo.KeyPlaceholderByQuery(len(ids)) + querySql := r.selectSql + " where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(ids) + results, err := datasource.RawDB("", querySql, parameters) + if err != nil { + logger.Errorf("query err => %v", err) + return []model.TraceData{} + } + // 转换实体 + return r.convertResultRows(results) +} + +// Insert 新增信息 +func (r *TraceData) Insert(data model.TraceData) string { + // 参数拼接 + params := make(map[string]any) + if data.TaskId > 0 { + params["task_id"] = data.TaskId + } + if data.IMSI != "" { + params["imsi"] = data.IMSI + } + if data.MSISDN != "" { + params["msisdn"] = data.MSISDN + } + if data.SrcAddr != "" { + params["src_addr"] = data.SrcAddr + } + if data.DstAddr != "" { + params["dst_addr"] = data.DstAddr + } + if data.IfType > -1 { + params["if_type"] = data.IfType + } + if data.MsgType > -1 { + params["msg_type"] = data.MsgType + } + if data.MsgDirect > -1 { + params["msg_direct"] = data.MsgDirect + } + if data.Length > 0 { + params["length"] = data.Length + } + if data.Timestamp > 0 { + params["timestamp"] = data.Timestamp + } + if data.RawMsg != "" { + params["raw_msg"] = data.RawMsg + } + if data.DecMsg != "" { + params["dec_msg"] = data.DecMsg + } + + // 构建执行语句 + keys, placeholder, values := repo.KeyPlaceholderValueByInsert(params) + sql := "insert into trace_data (" + strings.Join(keys, ",") + ")values(" + placeholder + ")" + + db := datasource.DefaultDB() + // 开启事务 + tx := db.Begin() + // 执行插入 + err := tx.Exec(sql, values...).Error + if err != nil { + logger.Errorf("insert row : %v", err.Error()) + tx.Rollback() + return "" + } + // 获取生成的自增 ID + var insertedID string + err = tx.Raw("select last_insert_id()").Row().Scan(&insertedID) + if err != nil { + logger.Errorf("insert last id : %v", err.Error()) + tx.Rollback() + return "" + } + // 提交事务 + tx.Commit() + return insertedID +} + +// DeleteByIds 批量删除信息 +func (r *TraceData) DeleteByIds(ids []string) int64 { + placeholder := repo.KeyPlaceholderByQuery(len(ids)) + sql := "delete from trace_data where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(ids) + results, err := datasource.ExecDB("", sql, parameters) + if err != nil { + logger.Errorf("delete err => %v", err) + return 0 + } + return results +} diff --git a/src/modules/trace/repository/trace_task.go b/src/modules/trace/repository/trace_task.go new file mode 100644 index 00000000..39f3aa30 --- /dev/null +++ b/src/modules/trace/repository/trace_task.go @@ -0,0 +1,358 @@ +package repository + +import ( + "fmt" + "strings" + "time" + + "be.ems/src/framework/datasource" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/parse" + "be.ems/src/framework/utils/repo" + "be.ems/src/modules/trace/model" +) + +// 实例化数据层 TraceTask 结构体 +var NewTraceTask = &TraceTask{ + selectSql: `select id, trace_id, trace_type, start_time, end_time, + interfaces, imsi, msisdn, + ue_ip, src_ip, dst_ip, signal_port, + create_by, create_time, update_by, update_time, remark, + ne_type, ne_id, notify_url, fetch_msg + from trace_task`, + + resultMap: map[string]string{ + "id": "ID", + "trace_id": "TraceId", + "trace_type": "TraceType", + "start_time": "StartTime", + "end_time": "EndTime", + "interfaces": "Interfaces", + "imsi": "IMSI", + "msisdn": "MSISDN", + "ue_ip": "UeIp", + "src_ip": "SrcIp", + "dst_ip": "DstIp", + "signal_port": "SignalPort", + "create_by": "CreateBy", + "create_time": "CreateTime", + "update_by": "UpdateBy", + "update_time": "UpdateTime", + "remark": "Remark", + "ne_type": "NeType", + "ne_id": "NeId", + "notify_url": "NotifyUrl", + "fetch_msg": "FetchMsg", + }, +} + +// TraceTask 跟踪_任务 数据层处理 +type TraceTask struct { + // 查询视图对象SQL + selectSql string + // 结果字段与实体映射 + resultMap map[string]string +} + +// convertResultRows 将结果记录转实体结果组 +func (r *TraceTask) convertResultRows(rows []map[string]any) []model.TraceTask { + arr := make([]model.TraceTask, 0) + for _, row := range rows { + item := model.TraceTask{} + for key, value := range row { + if keyMapper, ok := r.resultMap[key]; ok { + repo.SetFieldValue(&item, keyMapper, value) + } + } + arr = append(arr, item) + } + return arr +} + +// SelectPage 根据条件分页查询 +func (r *TraceTask) SelectPage(query map[string]any) map[string]any { + // 查询条件拼接 + var conditions []string + var params []any + if v, ok := query["neType"]; ok && v != "" { + conditions = append(conditions, "ne_type = ?") + params = append(params, v) + } + if v, ok := query["imsi"]; ok && v != "" { + conditions = append(conditions, "imsi like concat(?, '%')") + params = append(params, v) + } + if v, ok := query["msisdn"]; ok && v != "" { + conditions = append(conditions, "msisdn like concat(?, '%')") + params = append(params, v) + } + if v, ok := query["startTime"]; ok && v != "" { + conditions = append(conditions, "start_time >= ?") + params = append(params, v) + } + if v, ok := query["endTime"]; ok && v != "" { + conditions = append(conditions, "end_time <= ?") + params = append(params, v) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + result := map[string]any{ + "total": 0, + "rows": []model.TraceTask{}, + } + + // 查询数量 长度为0直接返回 + totalSql := "select count(1) as 'total' from trace_task" + totalRows, err := datasource.RawDB("", totalSql+whereSql, params) + if err != nil { + logger.Errorf("total err => %v", err) + return result + } + total := parse.Number(totalRows[0]["total"]) + if total == 0 { + return result + } else { + result["total"] = total + } + + // 分页 + pageNum, pageSize := repo.PageNumSize(query["pageNum"], query["pageSize"]) + pageSql := " limit ?,? " + params = append(params, pageNum*pageSize) + params = append(params, pageSize) + + // 排序 + orderSql := "" + if v, ok := query["sortField"]; ok && v != "" { + sortSql := v.(string) + if v, ok := query["sortOrder"]; ok && v != "" { + if v.(string) == "desc" { + sortSql += " desc " + } else { + sortSql += " asc " + } + } + orderSql = fmt.Sprintf(" order by %s ", sortSql) + } + + // 查询数据 + querySql := r.selectSql + whereSql + orderSql + pageSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + } + + // 转换实体 + result["rows"] = r.convertResultRows(results) + return result +} + +// SelectList 根据实体查询 +func (r *TraceTask) SelectList(task model.TraceTask) []model.TraceTask { + // 查询条件拼接 + var conditions []string + var params []any + if task.IMSI != "" { + conditions = append(conditions, "imsi = ?") + params = append(params, task.IMSI) + } + if task.SrcIp != "" { + conditions = append(conditions, "src_ip = ?") + params = append(params, task.SrcIp) + } + if task.DstIp != "" { + conditions = append(conditions, "dst_ip = ?") + params = append(params, task.DstIp) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询数据 + querySql := r.selectSql + whereSql + " order by id desc " + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + } + + // 转换实体 + return r.convertResultRows(results) +} + +// SelectByIds 通过ID查询 +func (r *TraceTask) SelectByIds(ids []string) []model.TraceTask { + placeholder := repo.KeyPlaceholderByQuery(len(ids)) + querySql := r.selectSql + " where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(ids) + results, err := datasource.RawDB("", querySql, parameters) + if err != nil { + logger.Errorf("query err => %v", err) + return []model.TraceTask{} + } + // 转换实体 + return r.convertResultRows(results) +} + +// Insert 新增信息 +func (r *TraceTask) Insert(task model.TraceTask) string { + // 参数拼接 + params := make(map[string]any) + if task.TraceId != "" { + params["trace_id"] = task.TraceId + } + if task.TraceType != "" { + params["trace_type"] = task.TraceType + } + if task.StartTime > 0 { + params["start_time"] = task.StartTime + } + if task.EndTime > 0 { + params["end_time"] = task.EndTime + } + if task.Interfaces != "" { + params["interfaces"] = task.Interfaces + } + if task.IMSI != "" { + params["imsi"] = task.IMSI + } + if task.MSISDN != "" { + params["msisdn"] = task.MSISDN + } + if task.UeIp != "" { + params["ue_ip"] = task.UeIp + } + if task.SrcIp != "" { + params["src_ip"] = task.SrcIp + } + if task.DstIp != "" { + params["dst_ip"] = task.DstIp + } + if task.SignalPort != 0 { + params["signal_port"] = task.SignalPort + } + if task.NeType != "" { + params["ne_type"] = task.NeType + } + if task.NeId != "" { + params["ne_id"] = task.NeId + } + if task.NotifyUrl != "" { + params["notify_url"] = task.NotifyUrl + } + if task.FetchMsg != "" { + params["fetch_msg"] = task.FetchMsg + } + if task.Remark != "" { + params["remark"] = task.Remark + } + if task.CreateBy != "" { + params["create_by"] = task.CreateBy + params["create_time"] = time.Now().UnixMilli() + } + + // 构建执行语句 + keys, placeholder, values := repo.KeyPlaceholderValueByInsert(params) + sql := "insert into trace_task (" + strings.Join(keys, ",") + ")values(" + placeholder + ")" + + db := datasource.DefaultDB() + // 开启事务 + tx := db.Begin() + // 执行插入 + err := tx.Exec(sql, values...).Error + if err != nil { + logger.Errorf("insert row : %v", err.Error()) + tx.Rollback() + return "" + } + // 获取生成的自增 ID + var insertedID string + err = tx.Raw("select last_insert_id()").Row().Scan(&insertedID) + if err != nil { + logger.Errorf("insert last id : %v", err.Error()) + tx.Rollback() + return "" + } + // 提交事务 + tx.Commit() + return insertedID +} + +// Update 修改信息 +func (r *TraceTask) Update(task model.TraceTask) int64 { + // 参数拼接 + params := make(map[string]any) + params["trace_id"] = task.TraceId + params["trace_type"] = task.TraceType + params["ne_type"] = task.NeType + params["ne_id"] = task.NeId + params["notify_url"] = task.NotifyUrl + + params["start_time"] = task.StartTime + params["end_time"] = task.EndTime + params["fetch_msg"] = task.FetchMsg + params["remark"] = task.Remark + + params["interfaces"] = task.Interfaces + + params["imsi"] = task.IMSI + params["msisdn"] = task.MSISDN + + params["ue_ip"] = task.UeIp + params["src_ip"] = task.SrcIp + params["dst_ip"] = task.DstIp + params["signal_port"] = task.SignalPort + + if task.UpdateBy != "" { + params["update_by"] = task.UpdateBy + params["update_time"] = time.Now().UnixMilli() + } + + // 构建执行语句 + keys, values := repo.KeyValueByUpdate(params) + sql := "update trace_task set " + strings.Join(keys, ",") + " where id = ?" + + // 执行更新 + values = append(values, task.ID) + rows, err := datasource.ExecDB("", sql, values) + if err != nil { + logger.Errorf("update row : %v", err.Error()) + return 0 + } + return rows +} + +// DeleteByIds 批量删除信息 +func (r *TraceTask) DeleteByIds(ids []string) int64 { + placeholder := repo.KeyPlaceholderByQuery(len(ids)) + sql := "delete from trace_task where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(ids) + results, err := datasource.ExecDB("", sql, parameters) + if err != nil { + logger.Errorf("delete err => %v", err) + return 0 + } + return results +} + +// LastID 最后一条ID +func (r *TraceTask) LastID() int64 { + // 查询数据 + querySql := "SELECT id as 'str' FROM trace_task ORDER BY id DESC LIMIT 1" + results, err := datasource.RawDB("", querySql, nil) + if err != nil { + logger.Errorf("query err %v", err) + return 0 + } + if len(results) > 0 { + return parse.Number(results[0]["str"]) + } + return 0 +} diff --git a/src/modules/trace/repository/trace_task_hlr.go b/src/modules/trace/repository/trace_task_hlr.go new file mode 100644 index 00000000..15b6cbf5 --- /dev/null +++ b/src/modules/trace/repository/trace_task_hlr.go @@ -0,0 +1,316 @@ +package repository + +import ( + "fmt" + "strings" + "time" + + "be.ems/src/framework/datasource" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/parse" + "be.ems/src/framework/utils/repo" + "be.ems/src/modules/trace/model" +) + +// 实例化数据层 TraceTaskHlr 结构体 +var NewTraceTaskHlr = &TraceTaskHlr{ + selectSql: `select id, trace_id, imsi, msisdn, start_time, end_time, status, msg, remark, create_by, create_time, update_by, update_time from trace_task_hlr`, + + resultMap: map[string]string{ + "id": "ID", + "trace_id": "TraceId", + "imsi": "IMSI", + "msisdn": "MSISDN", + "start_time": "StartTime", + "end_time": "EndTime", + "status": "Status", + "msg": "Msg", + "remark": "Remark", + "create_by": "CreateBy", + "create_time": "CreateTime", + "update_by": "UpdateBy", + "update_time": "UpdateTime", + }, +} + +// TraceTaskHlr 跟踪_任务给HRL网元 数据层处理 +type TraceTaskHlr struct { + // 查询视图对象SQL + selectSql string + // 结果字段与实体映射 + resultMap map[string]string +} + +// convertResultRows 将结果记录转实体结果组 +func (r *TraceTaskHlr) convertResultRows(rows []map[string]any) []model.TraceTaskHlr { + arr := make([]model.TraceTaskHlr, 0) + for _, row := range rows { + item := model.TraceTaskHlr{} + for key, value := range row { + if keyMapper, ok := r.resultMap[key]; ok { + repo.SetFieldValue(&item, keyMapper, value) + } + } + arr = append(arr, item) + } + return arr +} + +// SelectPage 根据条件分页查询 +func (r *TraceTaskHlr) SelectPage(querys model.TraceTaskHlrQuery) map[string]any { + // 查询条件拼接 + var conditions []string + var params []any + if querys.IMSI != "" { + conditions = append(conditions, "imsi like concat(?, '%')") + params = append(params, querys.IMSI) + } + if querys.MSISDN != "" { + conditions = append(conditions, "msisdn like concat(?, '%')") + params = append(params, querys.MSISDN) + } + if querys.StartTime != "" && len(querys.StartTime) == 13 { + conditions = append(conditions, "start_time >= ?") + params = append(params, querys.StartTime) + } + if querys.EndTime != "" && len(querys.EndTime) == 13 { + conditions = append(conditions, "end_time <= ?") + params = append(params, querys.EndTime) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + result := map[string]any{ + "total": 0, + "rows": []model.TraceTaskHlr{}, + } + + // 查询数量 长度为0直接返回 + totalSql := "select count(1) as 'total' from trace_task_hlr" + totalRows, err := datasource.RawDB("", totalSql+whereSql, params) + if err != nil { + logger.Errorf("total err => %v", err) + return result + } + total := parse.Number(totalRows[0]["total"]) + if total == 0 { + return result + } else { + result["total"] = total + } + + // 分页 + pageNum, pageSize := repo.PageNumSize(querys.PageNum, querys.PageSize) + pageSql := " limit ?,? " + params = append(params, pageNum*pageSize) + params = append(params, pageSize) + + // 排序 + orderSql := "" + if querys.SortField != "" { + sortSql := querys.SortField + if querys.SortOrder != "" { + if querys.SortOrder == "desc" { + sortSql += " desc " + } else { + sortSql += " asc " + } + } + orderSql = fmt.Sprintf(" order by id desc, %s ", sortSql) + } + + // 查询数据 + querySql := r.selectSql + whereSql + orderSql + pageSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + } + + // 转换实体 + result["rows"] = r.convertResultRows(results) + return result +} + +// SelectList 根据实体查询 +func (r *TraceTaskHlr) SelectList(task model.TraceTaskHlr) []model.TraceTaskHlr { + // 查询条件拼接 + var conditions []string + var params []any + if task.IMSI != "" { + conditions = append(conditions, "imsi = ?") + params = append(params, task.IMSI) + } + if task.MSISDN != "" { + conditions = append(conditions, "msisdn = ?") + params = append(params, task.MSISDN) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询数据 + querySql := r.selectSql + whereSql + " order by id desc " + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + } + + // 转换实体 + return r.convertResultRows(results) +} + +// SelectByIds 通过ID查询 +func (r *TraceTaskHlr) SelectByIds(ids []string) []model.TraceTaskHlr { + placeholder := repo.KeyPlaceholderByQuery(len(ids)) + querySql := r.selectSql + " where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(ids) + results, err := datasource.RawDB("", querySql, parameters) + if err != nil { + logger.Errorf("query err => %v", err) + return []model.TraceTaskHlr{} + } + // 转换实体 + return r.convertResultRows(results) +} + +// Insert 新增信息 +func (r *TraceTaskHlr) Insert(task model.TraceTaskHlr) string { + // 参数拼接 + params := make(map[string]any) + if task.TraceId != "" { + params["trace_id"] = task.TraceId + } + if task.IMSI != "" { + params["imsi"] = task.IMSI + } + if task.MSISDN != "" { + params["msisdn"] = task.MSISDN + } + if task.StartTime != 0 { + params["start_time"] = task.StartTime + } + if task.EndTime != 0 { + params["end_time"] = task.EndTime + } + if task.Status != "" { + params["status"] = task.Status + } + if task.Msg != "" { + params["msg"] = task.Msg + } + if task.Remark != "" { + params["remark"] = task.Remark + } + if task.CreateBy != "" { + params["create_by"] = task.CreateBy + params["create_time"] = time.Now().UnixMilli() + } + + // 构建执行语句 + keys, placeholder, values := repo.KeyPlaceholderValueByInsert(params) + sql := "insert into trace_task_hlr (" + strings.Join(keys, ",") + ")values(" + placeholder + ")" + + db := datasource.DefaultDB() + // 开启事务 + tx := db.Begin() + // 执行插入 + err := tx.Exec(sql, values...).Error + if err != nil { + logger.Errorf("insert row : %v", err.Error()) + tx.Rollback() + return "" + } + // 获取生成的自增 ID + var insertedID string + err = tx.Raw("select last_insert_id()").Row().Scan(&insertedID) + if err != nil { + logger.Errorf("insert last id : %v", err.Error()) + tx.Rollback() + return "" + } + // 提交事务 + tx.Commit() + return insertedID +} + +// Update 修改信息 +func (r *TraceTaskHlr) Update(task model.TraceTaskHlr) int64 { + // 参数拼接 + params := make(map[string]any) + if task.TraceId != "" { + params["trace_id"] = task.TraceId + } + if task.IMSI != "" { + params["imsi"] = task.IMSI + } + if task.MSISDN != "" { + params["msisdn"] = task.MSISDN + } + if task.StartTime != 0 { + params["start_time"] = task.StartTime + } + if task.EndTime != 0 { + params["end_time"] = task.EndTime + } + if task.Status != "" { + params["status"] = task.Status + } + if task.Msg != "" { + params["msg"] = task.Msg + } + if task.Remark != "" { + params["remark"] = task.Remark + } + if task.UpdateBy != "" { + params["update_by"] = task.UpdateBy + params["update_time"] = time.Now().UnixMilli() + } + + // 构建执行语句 + keys, values := repo.KeyValueByUpdate(params) + sql := "update trace_task_hlr set " + strings.Join(keys, ",") + " where id = ?" + + // 执行更新 + values = append(values, task.ID) + rows, err := datasource.ExecDB("", sql, values) + if err != nil { + logger.Errorf("update row : %v", err.Error()) + return 0 + } + return rows +} + +// DeleteByIds 批量删除信息 +func (r *TraceTaskHlr) DeleteByIds(ids []string) int64 { + placeholder := repo.KeyPlaceholderByQuery(len(ids)) + sql := "delete from trace_task_hlr where id in (" + placeholder + ")" + parameters := repo.ConvertIdsSlice(ids) + results, err := datasource.ExecDB("", sql, parameters) + if err != nil { + logger.Errorf("delete err => %v", err) + return 0 + } + return results +} + +// LastID 最后一条ID +func (r *TraceTaskHlr) LastID() int64 { + // 查询数据 + querySql := "SELECT id as 'str' FROM trace_task_hlr ORDER BY id DESC LIMIT 1" + results, err := datasource.RawDB("", querySql, nil) + if err != nil { + logger.Errorf("query err %v", err) + return 0 + } + if len(results) > 0 { + return parse.Number(results[0]["str"]) + } + return 0 +} diff --git a/src/modules/trace/service/packet.go b/src/modules/trace/service/packet.go new file mode 100644 index 00000000..64cc6430 --- /dev/null +++ b/src/modules/trace/service/packet.go @@ -0,0 +1,67 @@ +package service + +import ( + "be.ems/src/framework/vo" + packetTask "be.ems/src/modules/trace/packet_task" +) + +// 实例化服务层 Packet 结构体 +var NewPacket = &Packet{} + +// 信令跟踪 服务层处理 +type Packet struct{} + +// NetworkDevices 获取网卡设备信息 +func (s *Packet) NetworkDevices() []vo.TreeSelect { + arr := make([]vo.TreeSelect, 0) + devices, err := packetTask.NetworkDevices() + if err != nil { + return arr + } + + for _, device := range devices { + if len(device.Addresses) == 0 { + continue + } + + lable := device.Description + if lable == "" { + lable = device.Name + } + item := vo.TreeSelect{ + ID: device.Name, + Label: lable, + Children: []vo.TreeSelect{}, + } + + for _, address := range device.Addresses { + if address.IP != nil { + ip := address.IP.String() + item.Children = append(item.Children, vo.TreeSelect{ID: ip, Label: ip}) + } + } + arr = append(arr, item) + } + + return arr +} + +// LiveStart 开始捕获数据 +func (s *Packet) LiveStart(taskNo, deviceName, filterBPF string, outputPCAP bool) (string, error) { + return packetTask.LiveStart(taskNo, deviceName, filterBPF, outputPCAP) +} + +// LiveFilter 捕获过滤 +func (s *Packet) LiveFilter(taskNo, expr string) error { + return packetTask.LiveFilter(taskNo, expr) +} + +// LiveTimeout 更新捕获失效时间 +func (s *Packet) LiveTimeout(taskNo string, seconds int) error { + return packetTask.LiveTimeout(taskNo, seconds) +} + +// LiveStop 停止捕获数据 +func (s *Packet) LiveStop(taskNo string) error { + return packetTask.LiveStop(taskNo) +} diff --git a/src/modules/trace/service/tcpdump.go b/src/modules/trace/service/tcpdump.go index cc746064..b94a027d 100644 --- a/src/modules/trace/service/tcpdump.go +++ b/src/modules/trace/service/tcpdump.go @@ -1,13 +1,284 @@ package service -// 信令抓包 服务层接口 -type ITcpdump interface { - // DumpStart 触发tcpdump开始抓包 filePcapName, err - DumpStart(neType, neId, cmdStr string) (string, error) +import ( + "fmt" + "path/filepath" + "regexp" + "strings" + "sync" + "time" - // DumpStop 停止已存在抓包句柄 - DumpStop(neType, neId, fileName string) (string, error) + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/ssh" + neService "be.ems/src/modules/network_element/service" +) - // DumpUPF UPF标准版抓包 - DumpUPF(neType, neId, cmdStr string) (string, string, error) +// 实例化服务层 TCPdump 结构体 +var NewTCPdump = &TCPdump{ + neInfoService: neService.NewNeInfo, +} + +// 信令抓包 服务层处理 +type TCPdump struct { + neInfoService *neService.NeInfo // 网元信息服务 +} + +// 抓包进程PID +var dumpPIDMap sync.Map + +// DumpStart 触发tcpdump开始抓包 +func (s *TCPdump) DumpStart(neType, neId, cmdStr string) (string, error) { + // 命令检查 + if strings.Contains(cmdStr, "-w") { + return "", fmt.Errorf("command cannot contain -w") + } + + // 查询网元获取IP + neInfo := s.neInfoService.SelectNeInfoByNeTypeAndNeID(neType, neId) + if neInfo.NeId != neId || neInfo.IP == "" { + return "", fmt.Errorf("app.common.noNEInfo") + } + // 网元主机的SSH客户端 + sshClient, err := s.neInfoService.NeRunSSHClient(neInfo.NeType, neInfo.NeId) + if err != nil { + return "", err + } + defer sshClient.Close() + + // 检查是否安装tcpdump + if msg, err := sshClient.RunCMD("sudo tcpdump --version"); err != nil { + // bash: tcpdump: command not found + msg = strings.TrimSpace(msg) + logger.Errorf("DumpStart err: %s => %s", msg, err.Error()) + return "", fmt.Errorf("%s", msg) + } + + taskCode := time.Now().Format("20060102150405") + // 存放文件目录 /tmp/omc/tcpdump/udm/001/20240817104241 + neDirTemp := fmt.Sprintf("/tmp/omc/tcpdump/%s/%s/%s", strings.ToLower(neInfo.NeType), neInfo.NeId, taskCode) + sshClient.RunCMD(fmt.Sprintf("mkdir -p %s && sudo chmod 777 -R /tmp/omc", neDirTemp)) + + // 命令拼装 + logPath := fmt.Sprintf("%s/tcpdump.log", neDirTemp) + filePath := fmt.Sprintf("%s/part_%s.pcap ", neDirTemp, taskCode) + if strings.Contains(cmdStr, "-G") { + filePath = fmt.Sprintf("%s/part_%%Y%%m%%d%%H%%M%%S.pcap ", neDirTemp) + } + sendCmd := fmt.Sprintf("sudo timeout 60m sudo tcpdump -i any %s -w %s > %s 2>&1 & echo $!", cmdStr, filePath, logPath) + // sudo timeout 60m sudo tcpdump -i any -n -s 0 -v -G 60 -W 6 -w /tmp/omc/tcpdump/udm/001/20240817104241/part_%Y-%m-%d_%H:%M:%S.pcap > /tmp/omc/tcpdump/udm/001/20240817104241/tcpdump.log 2>&1 & echo $! + // sudo timeout 60m sudo tcpdump -i any -n -s 0 -v -w /tmp/omc/tcpdump/udm/001/20240817105440/part_2024-08-17_10:54:40.pcap > /tmp/omc/tcpdump/udm/001/20240817105440/tcpdump.log 2>&1 & echo $! + // + // timeout 超时60分钟后发送kill命令,1分钟后强制终止命令。tcpdump -G 文件轮转间隔时间(秒) -W 文件轮转保留最近数量 + // sudo timeout --kill-after=1m 60m sudo tcpdump -i any -n -s 0 -v -G 10 -W 7 -w /tmp/part_%Y%m%d%H%M%S.pcap > /tmp/part.log 2>&1 & echo $! + // sudo kill $(pgrep -P 722729) + outputPID, err := sshClient.RunCMD(sendCmd) + outputPID = strings.TrimSpace(outputPID) + if err != nil || strings.HasPrefix(outputPID, "stderr:") { + logger.Errorf("DumpStart err: %s => %s", outputPID, err.Error()) + return "", err + } + + // 日志文件行号 + PIDMap := s.logFileLastLine(neType, sshClient) + PIDMap["neType"] = neInfo.NeType + PIDMap["neId"] = neInfo.NeId + PIDMap["taskCode"] = taskCode + PIDMap["pid"] = outputPID + PIDMap["cmd"] = sendCmd + + // 检查进程 ps aux | grep tcpdump + // 强杀 sudo pkill tcpdump + pidKey := fmt.Sprintf("%s_%s_%s", strings.ToLower(neInfo.NeType), neInfo.NeId, taskCode) + dumpPIDMap.Store(pidKey, PIDMap) + return taskCode, err +} + +// DumpStop 停止已存在抓包句柄 +func (s *TCPdump) DumpStop(neType, neId, taskCode string) ([]string, error) { + // 查询网元获取IP + neInfo := s.neInfoService.SelectNeInfoByNeTypeAndNeID(neType, neId) + if neInfo.NeId != neId || neInfo.IP == "" { + return []string{}, fmt.Errorf("app.common.noNEInfo") + } + // 网元主机的SSH客户端 + sshClient, err := s.neInfoService.NeRunSSHClient(neInfo.NeType, neInfo.NeId) + if err != nil { + return []string{}, err + } + defer sshClient.Close() + + // 是否存在执行过的进程 + pidKey := fmt.Sprintf("%s_%s_%s", strings.ToLower(neInfo.NeType), neInfo.NeId, taskCode) + PIDMap, ok := dumpPIDMap.Load(pidKey) + if !ok || PIDMap == nil { + return []string{}, fmt.Errorf("tcpdump is not running") + } + pid, ok := PIDMap.(map[string]string)["pid"] + if !ok || pid == "" { + return []string{}, fmt.Errorf("tcpdump is not running") + } + s.logFileLastLineToFile(PIDMap.(map[string]string), sshClient) + + // 存放文件目录 /tmp/omc/tcpdump/udm/001/20240817104241 + neDirTemp := fmt.Sprintf("/tmp/omc/tcpdump/%s/%s/%s", strings.ToLower(neInfo.NeType), neInfo.NeId, taskCode) + // 命令拼装 + sendCmd := fmt.Sprintf("pids=$(pgrep -P %s) && [ -n \"$pids\" ] && sudo kill $pids;sudo timeout 2s ls %s", pid, neDirTemp) + // pids=$(pgrep -P 1914341) && [ -n "$pids" ] && sudo kill $pids;sudo timeout 2s ls /tmp/omc/tcpdump/udm/001/20240817104241 + output, err := sshClient.RunCMD(sendCmd) + output = strings.TrimSpace(output) + if err != nil || strings.HasPrefix(output, "ls: ") { + logger.Errorf("DumpStop err: %s => %s", output, err.Error()) + return []string{}, err + } + files := strings.Split(output, "\n") + dumpPIDMap.Delete(pidKey) + return files, nil +} + +// logFileLastLine 日志文件最后行号 +func (s *TCPdump) logFileLastLine(neType string, sshClient *ssh.ConnSSH) map[string]string { + logFileArr := make([]string, 0) + mapFile := make(map[string]string, 0) + + // 存放文件目录 /var/log/xxx.log + if neType == "IMS" { + logFileArr = append(logFileArr, + "/var/log/ims/pcscf/pcscf.log", + "/var/log/ims/bgcf/bgcf.log", + "/var/log/ims/bsf/bsf.log", + "/var/log/ims/icscf/icscf.log", + "/var/log/ims/ismc/ismc.log", + "/var/log/ims/mmtel/mmtel.log", + "/var/log/ims/scscf/scscf.log", + "/var/log/ims/iwf/iwf.log", + ) + } else { + neLogFile := fmt.Sprintf("/var/log/%s.log", strings.ToLower(neType)) + logFileArr = append(logFileArr, neLogFile) + } + + for _, v := range logFileArr { + lastLine, err := sshClient.RunCMD(fmt.Sprintf("sed -n '$=' %s", v)) + lastLine = strings.TrimSpace(lastLine) + if err != nil || strings.HasPrefix(lastLine, "sed: can't") { + logger.Errorf("logFileLastLine err: %s => %s", lastLine, err.Error()) + continue + } + mapFile[v] = lastLine + } + return mapFile +} + +// logFileLastLine 日志文件最后行号 +func (s *TCPdump) logFileLastLineToFile(PIDMap map[string]string, sshClient *ssh.ConnSSH) error { + // 网元主机的SSH客户端进行文件传输 + sftpClient, err := sshClient.NewClientSFTP() + if err != nil { + return fmt.Errorf("ne info sftp client err") + } + defer sftpClient.Close() + + neType := PIDMap["neType"] + neId := PIDMap["neId"] + taskCode := PIDMap["taskCode"] + // 存放文件目录 /tmp/omc/tcpdump/udm/001/20240817104241 + sshClient.RunCMD("mkdir -p /tmp/omc && sudo chmod 777 -R /tmp/omc") + neDirTemp := fmt.Sprintf("/tmp/omc/tcpdump/%s/%s/%s", strings.ToLower(neType), neId, taskCode) + + lastLineMap := s.logFileLastLine(neType, sshClient) + for lastLogFile, lastFileLine := range lastLineMap { + for startLogFile, startFileLine := range PIDMap { + if lastLogFile == startLogFile && lastFileLine != "" { + if startFileLine == "" { + startFileLine = "1" // 起始行号从第一行开始 + } + outputFile := fmt.Sprintf("%s/%s", neDirTemp, filepath.Base(lastLogFile)) + sendCmd := fmt.Sprintf("sed -n \"%s,%sp\" \"%s\" > \"%s\"", startFileLine, lastFileLine, lastLogFile, outputFile) + // sed -n "1,5p" "/var/log/amf.log" > "/tmp/omc/tcpdump/amf/001/20241008141336/amf.log" + output, err := sshClient.RunCMD(sendCmd) + if err != nil || strings.HasPrefix(output, "stderr:") { + logger.Errorf("logFileLastLineToFile err: %s => %s", strings.TrimSpace(output), err.Error()) + continue + } + } + } + } + return nil +} + +// UPFTrace UPF标准版内部抓包 +func (s *TCPdump) UPFTrace(neType, neId, cmdStr string) (string, error) { + // 命令检查 + if strings.Contains(cmdStr, "file") { + return "", fmt.Errorf("command cannot contain file") + } + + // 查询网元获取IP + neInfo := s.neInfoService.SelectNeInfoByNeTypeAndNeID(neType, neId) + if neInfo.NeId != neId || neInfo.IP == "" { + return "", fmt.Errorf("app.common.noNEInfo") + } + // 网元主机的SSH客户端 + sshClient, err := s.neInfoService.NeRunSSHClient(neInfo.NeType, neInfo.NeId) + if err != nil { + return "", err + } + defer sshClient.Close() + // 网元主机的Telnet客户端 + telnetClient, err := s.neInfoService.NeRunTelnetClient("UPF", neInfo.NeId, 2) + if err != nil { + return "", err + } + defer telnetClient.Close() + + // 命令拼装 + fileName := fmt.Sprintf("%s_%s_part_%s.pcap ", neInfo.NeType, neInfo.NeId, time.Now().Format("20060102150405")) + pcapCmd := fmt.Sprintf("%s\r\n", cmdStr) + // 以off结尾是停止抓包,不需要写文件 + if !strings.Contains(cmdStr, "off") { + // pcap trace rx tx max 100000 intfc any file UPF_001_part_20240817164516.pcap + pcapCmd = fmt.Sprintf("%s file %s\r\n", cmdStr, fileName) + } + // 发送命令 UPF内部默认输出路径/tmp只能写文件名 + // pcap trace rx tx max 100000 intfc any file upf_test.pcap + // pcap trace rx tx off + output, err := telnetClient.RunCMD(pcapCmd) + if err != nil { + logger.Warnf("DumpUPF err: %s => %s", output, err.Error()) + return "", err + } + + // 结果截取 + arr := strings.Split(output, "\r\n") + if len(arr) == 2 { + return "", fmt.Errorf("trace pacp run failed") + } + if len(arr) > 3 { + resMsg := arr[2] + // pcap trace: unknown input `f file UPF_001_part_2024-08-19...' + // pcap trace: dispatch trace already enabled... + // pcap trace: dispatch trace already disabled... + // pcap trace: No packets captured... + // Write 100000 packets to /tmp/UPF_001_part_20240817164516.pcap, and stop capture... + if strings.Contains(resMsg, "unknown input") { + return "", fmt.Errorf("trace pacp unknown input") + } + if strings.Contains(resMsg, "already enabled") { + return "", fmt.Errorf("trace pacp already running") + } + if strings.Contains(resMsg, "already disabled") { + return "", fmt.Errorf("trace pacp not running") + } + if strings.Contains(resMsg, "No packets") { + return "", fmt.Errorf("trace pacp not packets") + } + if strings.Contains(resMsg, "packets to") { + matches := regexp.MustCompile(`(/tmp/[^,\s]+)`).FindStringSubmatch(resMsg) + if len(matches) == 0 { + return "", fmt.Errorf("file path not found") + } + return matches[0], nil + } + } + return "trace pacp running", nil } diff --git a/src/modules/trace/service/tcpdump.impl.go b/src/modules/trace/service/tcpdump.impl.go deleted file mode 100644 index 005ee2b8..00000000 --- a/src/modules/trace/service/tcpdump.impl.go +++ /dev/null @@ -1,193 +0,0 @@ -package service - -import ( - "fmt" - "strings" - "time" - - "be.ems/src/framework/logger" - "be.ems/src/framework/utils/date" - neService "be.ems/src/modules/network_element/service" -) - -// 实例化服务层 TcpdumpImpl 结构体 -var NewTcpdumpImpl = &TcpdumpImpl{ - neInfoService: neService.NewNeInfoImpl, - tcpdumpPIDMap: map[string]string{}, -} - -// 信令抓包 服务层处理 -type TcpdumpImpl struct { - // 网元信息服务 - neInfoService neService.INeInfo - // 抓包进程PID - tcpdumpPIDMap map[string]string -} - -// DumpStart 触发tcpdump开始抓包 filePcapName, err -func (s *TcpdumpImpl) DumpStart(neType, neId, cmdStr string) (string, error) { - // 查询网元获取IP - neInfo := s.neInfoService.SelectNeInfoByNeTypeAndNeID(neType, neId) - if neInfo.NeId != neId || neInfo.IP == "" { - return "", fmt.Errorf("noData") - } - - // 网元主机的SSH客户端 - sshClient, err := s.neInfoService.NeRunSSHClient(neInfo.NeType, neInfo.NeId) - if err != nil { - return "", err - } - defer sshClient.Close() - - // 是否拥有sudo权限并拼接 - withSudo := "" - if _, err := sshClient.RunCMD("sudo -n uname"); err == nil { - withSudo = "sudo " - } - - if msg, err := sshClient.RunCMD(fmt.Sprintf("%s tcpdump --version", withSudo)); err != nil { - // stderr: bash: tcpdump:未找到命令 => exit status 127 - msg = strings.TrimSpace(msg) - logger.Warnf("DumpStart err: %s => %s", msg, err.Error()) - return "", fmt.Errorf(msg) - } - - // 拼装命令 - neTypeID := fmt.Sprintf("%s_%s", neInfo.NeType, neInfo.NeId) - timeStr := date.ParseDateToStr(time.Now(), date.YYYYMMDDHHMMSS) - fileName := fmt.Sprintf("%s_%s", timeStr, neTypeID) - sendCmd := fmt.Sprintf("cd /tmp \n %s nohup timeout 30m tcpdump -i any %s -s0 -w %s.pcap > %s.log 2>&1 & \necho $!", withSudo, cmdStr, fileName, fileName) - // cd /tmp - // sudo nohup timeout 60m tcpdump -i any -n -s 0 -v -w -s0 -w 20240115140822_UDM_001.pcap > 20240115140822_UDM_001.log 2>&1 & echo $! - msg, err := sshClient.RunCMD(sendCmd) - msg = strings.TrimSpace(msg) - if err != nil || strings.HasPrefix(msg, "stderr:") { - logger.Warnf("DumpStart err: %s => %s", msg, err.Error()) - return "", err - } - - // 检查进程 ps aux | grep tcpdump - // 强杀 sudo pkill tcpdump - s.tcpdumpPIDMap[neTypeID] = msg - return fileName, err -} - -// DumpStop 停止已存在抓包句柄 -func (s *TcpdumpImpl) DumpStop(neType, neId, fileName string) (string, error) { - // 查询网元获取IP - neInfo := s.neInfoService.SelectNeInfoByNeTypeAndNeID(neType, neId) - if neInfo.NeId != neId || neInfo.IP == "" { - return "", fmt.Errorf("noData") - } - - // 网元主机的SSH客户端 - sshClient, err := s.neInfoService.NeRunSSHClient(neInfo.NeType, neInfo.NeId) - if err != nil { - return "", err - } - defer sshClient.Close() - - // 是否拥有sudo权限并拼接 - withSudo := "" - if _, err := sshClient.RunCMD("sudo -n uname"); err == nil { - withSudo = "sudo " - } - - // 是否存在进程 - neTypeID := fmt.Sprintf("%s_%s", neInfo.NeType, neInfo.NeId) - pid, ok := s.tcpdumpPIDMap[neTypeID] - if !ok || pid == "" { - return "", fmt.Errorf("tcpdump is not running") - } - - // 查看日志 - viewLogFile := "" - if fileName != "" && strings.Contains(fileName, neTypeID) { - viewLogFile = fmt.Sprintf("\n cat %s.log", fileName) - } - - // 拼装命令 - sendCmd := fmt.Sprintf("cd /tmp \n %s kill %s %s", withSudo, pid, viewLogFile) - msg, err := sshClient.RunCMD(sendCmd) - delete(s.tcpdumpPIDMap, neTypeID) - if err != nil || strings.HasPrefix(msg, "stderr:") { - logger.Warnf("DumpStop err: %s => %s", strings.TrimSpace(msg), err.Error()) - return "", err - } - return msg, nil -} - -// DumpUPF UPF标准版抓包 -func (s *TcpdumpImpl) DumpUPF(neType, neId, cmdStr string) (string, string, error) { - // 查询网元获取IP - neInfo := s.neInfoService.SelectNeInfoByNeTypeAndNeID(neType, neId) - if neInfo.NeId != neId || neInfo.IP == "" { - return "", "", fmt.Errorf("noData") - } - - // 网元主机的SSH客户端 - sshClient, err := s.neInfoService.NeRunSSHClient(neInfo.NeType, neInfo.NeId) - if err != nil { - return "", "", err - } - defer sshClient.Close() - - // 是否拥有sudo权限并拼接 - withSudo := "" - if _, err := sshClient.RunCMD("sudo -n uname"); err == nil { - withSudo = "sudo " - } - - if msg, err := sshClient.RunCMD(fmt.Sprintf("%s expect -version", withSudo)); err != nil { - // stderr: bash: expect:未找到命令 => exit status 127 - msg = strings.TrimSpace(msg) - logger.Warnf("DumpUPF err: %s => %s", msg, err.Error()) - return "", "", fmt.Errorf(msg) - } - - // 拼装命令 - neTypeID := fmt.Sprintf("%s_%s", neInfo.NeType, neInfo.NeId) - timeStr := date.ParseDateToStr(time.Now(), date.YYYYMMDDHHMMSS) - fileName := fmt.Sprintf("%s_%s", timeStr, neTypeID) - // UPF标准版本telnet脚本 - scriptStr := "set pcapCmd [lindex $argv 0]\nspawn telnet " + neInfo.IP + " 5002\nexpect \"upfd1# \"\nsend \"$pcapCmd\\n\"\nexpect \"upfd1# \"\nsend \"quit\\n\"\nexpect \"eof\"" - // scriptStr := "set pcapCmd [lindex $argv 0]\nspawn telnet localhost 5002\nexpect \"upfd1# \"\nsend \"$pcapCmd\\n\"\nexpect \"upfd1# \"\nsend \"quit\\n\"\nexpect \"eof\"" - writePcapFile := fmt.Sprintf("echo '%s' > pcapUPF.sh\n %s chmod +x pcapUPF.sh", scriptStr, withSudo) - writeLogFile := fmt.Sprintf("> %s.log 2>&1 \ncat %s.log", fileName, fileName) - - // 以off结尾是停止抓包,不需要写文件 - pcapCmd := cmdStr - if !strings.HasSuffix(pcapCmd, "off") { - pcapCmd = fmt.Sprintf("%s file %s.pcap", cmdStr, fileName) - } - sendCmd := fmt.Sprintf("cd /tmp \n%s\n expect ./pcapUPF.sh '%s' %s", writePcapFile, pcapCmd, writeLogFile) - // cd /tmp - // echo '' > - // expect ./cap.sh > pcapUPF.sh - // sudo chmod +x pcapUPF.sh - // expect ./cap.sh 'pcap dispatch trace off' > 20240115165701_UDM_001.log 2>&1 - // cat 20240115165701_UDM_001.log - msg, err := sshClient.RunCMD(sendCmd) - msg = strings.TrimSpace(msg) - if err != nil || strings.HasPrefix(msg, "stderr:") { - logger.Warnf("DumpUPF err: %s => %s", msg, err.Error()) - return "", "", err - } - if strings.Contains(msg, "Unable to connect to remote host") { - return "", "", fmt.Errorf("connection refused") - } - // 以off结尾是停止抓包,不需要写文件 - if strings.HasSuffix(pcapCmd, "off") { - if strings.Contains(msg, "Write ") { - lastTmpIndex := strings.LastIndex(msg, "/tmp/") - text := msg[lastTmpIndex+5:] - extensionIndex := strings.LastIndex(text, ".pcap") - if extensionIndex != -1 { - fileName = text[:extensionIndex] - } - } else { - fileName = "" - } - } - return fileName, msg, err -} diff --git a/src/modules/trace/service/trace_data.go b/src/modules/trace/service/trace_data.go new file mode 100644 index 00000000..4a9ee7c6 --- /dev/null +++ b/src/modules/trace/service/trace_data.go @@ -0,0 +1,54 @@ +package service + +import ( + "fmt" + + "be.ems/src/modules/trace/model" + "be.ems/src/modules/trace/repository" +) + +// 实例化数据层 TraceData 结构体 +var NewTraceData = &TraceData{ + traceDataRepository: repository.NewTraceData, +} + +// TraceData 跟踪_数据 服务层处理 +type TraceData struct { + // 跟踪_数据信息 + traceDataRepository *repository.TraceData +} + +// SelectPage 根据条件分页查询 +func (r *TraceData) SelectPage(query map[string]any) map[string]any { + return r.traceDataRepository.SelectPage(query) +} + +// SelectById 通过ID查询 +func (r *TraceData) SelectById(id string) model.TraceData { + tasks := r.traceDataRepository.SelectByIds([]string{id}) + if len(tasks) > 0 { + return tasks[0] + } + return model.TraceData{} +} + +// Insert 新增信息 +func (r *TraceData) Insert(task model.TraceData) string { + return r.traceDataRepository.Insert(task) +} + +// DeleteByIds 批量删除信息 +func (r *TraceData) DeleteByIds(ids []string) (int64, error) { + // 检查是否存在 + rows := r.traceDataRepository.SelectByIds(ids) + if len(rows) <= 0 { + return 0, fmt.Errorf("not data") + } + + if len(rows) == len(ids) { + rows := r.traceDataRepository.DeleteByIds(ids) + return rows, nil + } + // 删除信息失败! + return 0, fmt.Errorf("delete fail") +} diff --git a/src/modules/trace/service/trace_task.go b/src/modules/trace/service/trace_task.go new file mode 100644 index 00000000..15f09976 --- /dev/null +++ b/src/modules/trace/service/trace_task.go @@ -0,0 +1,343 @@ +package service + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "net" + "strings" + + "be.ems/src/framework/config" + "be.ems/src/framework/logger" + "be.ems/src/framework/socket" + "be.ems/src/framework/utils/date" + "be.ems/src/framework/utils/parse" + neFetchlink "be.ems/src/modules/network_element/fetch_link" + neService "be.ems/src/modules/network_element/service" + "be.ems/src/modules/trace/model" + "be.ems/src/modules/trace/repository" + wsService "be.ems/src/modules/ws/service" +) + +// 实例化数据层 TraceTask 结构体 +var NewTraceTask = &TraceTask{ + udpService: socket.SocketUDP{}, + traceTaskRepository: repository.NewTraceTask, + traceDataRepository: repository.NewTraceData, +} + +// TraceTask 跟踪任务 服务层处理 +type TraceTask struct { + // UDP服务对象 + udpService socket.SocketUDP + // 跟踪_任务数据信息 + traceTaskRepository *repository.TraceTask + // 跟踪_数据信息 + traceDataRepository *repository.TraceData +} + +// CreateUDP 创建UDP数据通道 +func (r *TraceTask) CreateUDP() error { + // 跟踪配置是否开启 + if v := config.Get("trace.enabled"); v != nil { + if !v.(bool) { + return nil + } + } + host := "127.0.0.1" + if v := config.Get("trace.host"); v != nil { + host = v.(string) + } + var port int64 = 33033 + if v := config.Get("trace.port"); v != nil { + port = parse.Number(v) + } + + // 初始化UDP服务 + r.udpService = socket.SocketUDP{Addr: host, Port: port} + if _, err := r.udpService.New(); err != nil { + return err + } + + // 接收处理UDP数据 + go r.udpService.Resolve(func(conn *net.UDPConn, err error) { + if err != nil { + logger.Errorf("UDP Resolve %s", err.Error()) + return + } + + // 读取数据 + buf := make([]byte, 2048) + n, addr, err := conn.ReadFromUDPAddrPort(buf) + if err != nil { + logger.Errorf("UDP Resolve ReadFromUDPAddrPort Error: %s", err.Error()) + return + } + + logger.Infof("socket UDP: %s", string(buf[:n])) + // logger.Infof("socket UDP Base64: %s", base64.StdEncoding.EncodeToString(buf[:n])) + mData, err := UDPDataHandler(buf, n) + if err != nil { + logger.Errorf("UDP Resolve UDPDataHandler Error: %s", err.Error()) + return + } + taskId := parse.Number(mData["taskId"]) + + // 插入数据库做记录 + r.traceDataRepository.Insert(model.TraceData{ + TaskId: taskId, + IMSI: mData["imsi"].(string), + SrcAddr: mData["srcAddr"].(string), + DstAddr: mData["dstAddr"].(string), + IfType: parse.Number(mData["ifType"]), + MsgType: parse.Number(mData["msgType"]), + MsgDirect: parse.Number(mData["msgDirect"]), + Length: parse.Number(mData["dataLen"]), + RawMsg: mData["dataInfo"].(string), + Timestamp: parse.Number(mData["timestamp"]), + DecMsg: mData["decMsg"].(string), + }) + + // 推送文件 + if v, ok := mData["pcapFile"]; ok && v != "" { + logger.Infof("pcapFile: %s", v) + wsService.NewWSSend.ByGroupID(fmt.Sprintf("%s%d", wsService.GROUP_TRACE_NE, taskId), taskId) + } + + // 发送响应 + if _, err := conn.WriteToUDPAddrPort([]byte("udp>"), addr); err != nil { + logger.Errorf("UDP Resolve WriteToUDPAddrPort Error: %s", err.Error()) + } + }) + + // ============ 测试接收网元UDP发过来的数据 + // 初始化TCP服务 后续调整TODO + tcpService := socket.SocketTCP{Addr: host, Port: port + 1} + if _, err := tcpService.New(); err != nil { + return err + } + // 接收处理TCP数据 + go tcpService.Resolve(func(conn *net.Conn, err error) { + if err != nil { + logger.Errorf("TCP Resolve %s", err.Error()) + return + } + + c := (*conn) + // 读取数据 + buf := make([]byte, 2048) + n, err := c.Read(buf) + if err != nil { + logger.Errorf("TCP Resolve Read Error: %s", err.Error()) + return + } + + logger.Infof("socket TCP: %s", string(buf[:n])) + deData, _ := base64.StdEncoding.DecodeString(string(buf[:n])) + logger.Infof("socket TCP Base64: %s", deData) + mData, err := UDPDataHandler(deData, len(deData)) + if err != nil { + logger.Errorf("TCP Resolve UDPDataHandler Error: %s", err.Error()) + return + } + taskId := parse.Number(mData["taskId"]) + + // 插入数据库做记录 + r.traceDataRepository.Insert(model.TraceData{ + TaskId: taskId, + IMSI: mData["imsi"].(string), + SrcAddr: mData["srcAddr"].(string), + DstAddr: mData["dstAddr"].(string), + IfType: parse.Number(mData["ifType"]), + MsgType: parse.Number(mData["msgType"]), + MsgDirect: parse.Number(mData["msgDirect"]), + Length: parse.Number(mData["dataLen"]), + RawMsg: mData["dataInfo"].(string), + Timestamp: parse.Number(mData["timestamp"]), + DecMsg: mData["decMsg"].(string), + }) + + // 推送文件 + if v, ok := mData["pcapFile"]; ok && v != "" { + logger.Infof("pcapFile: %s", v) + wsService.NewWSSend.ByGroupID(fmt.Sprintf("%s%d", wsService.GROUP_TRACE_NE, taskId), taskId) + } + + // 发送响应 + if _, err = c.Write([]byte("tcp>")); err != nil { + logger.Errorf("TCP Resolve Write Error: %s", err.Error()) + } + }) + return nil +} + +// CloseUDP 关闭UDP数据通道 +func (r *TraceTask) CloseUDP() { + r.udpService.Close() +} + +// SelectPage 根据条件分页查询 +func (r *TraceTask) SelectPage(query map[string]any) map[string]any { + return r.traceTaskRepository.SelectPage(query) +} + +// SelectById 通过ID查询 +func (r *TraceTask) SelectById(id string) model.TraceTask { + tasks := r.traceTaskRepository.SelectByIds([]string{id}) + if len(tasks) > 0 { + return tasks[0] + } + return model.TraceTask{} +} + +// Insert 新增信息 +func (r *TraceTask) Insert(task model.TraceTask) error { + // 跟踪配置是否开启 + if v := config.Get("trace.enabled"); v != nil { + if !v.(bool) { + return fmt.Errorf("tracking is not enabled") + } + } + host := "127.0.0.1" + if v := config.Get("trace.host"); v != nil { + host = v.(string) + } + var port int64 = 33033 + if v := config.Get("trace.port"); v != nil { + port = parse.Number(v) + } + task.NotifyUrl = fmt.Sprintf("udp:%s:%d", host, port) + + // 查询网元获取IP + neInfo := neService.NewNeInfo.SelectNeInfoByNeTypeAndNeID(task.NeType, task.NeId) + if neInfo.NeId != task.NeId || neInfo.IP == "" { + return fmt.Errorf("app.common.noNEInfo") + } + traceId := r.traceTaskRepository.LastID() + 1 // 生成任务ID < 65535 + task.TraceId = fmt.Sprint(traceId) + + // 发送任务给网元 + data := map[string]any{ + "neType": neInfo.NeType, + "neId": neInfo.NeId, + "notifyUrl": task.NotifyUrl, + "id": traceId, + "startTime": date.ParseDateToStr(task.StartTime, date.YYYY_MM_DD_HH_MM_SS), + "endTime": date.ParseDateToStr(task.EndTime, date.YYYY_MM_DD_HH_MM_SS), + } + switch task.TraceType { + case "1": // Interface + data["traceType"] = "Interface" + data["interfaces"] = strings.Split(task.Interfaces, ",") + case "2": // Device + data["traceType"] = "Device" + data["ueIp"] = task.UeIp + data["srcIp"] = task.SrcIp + data["dstIp"] = task.DstIp + data["signalPort"] = task.SignalPort + task.UeIp = neInfo.IP + case "3": // UE + data["traceType"] = "UE" + data["imsi"] = task.IMSI + data["msisdn"] = task.MSISDN + default: + return fmt.Errorf("trace type is not disabled") + } + msg, err := neFetchlink.NeTraceAdd(neInfo, data) + if err != nil { + return err + } + s, _ := json.Marshal(msg) + task.FetchMsg = string(s) + + // 插入数据库 + r.traceTaskRepository.Insert(task) + return nil +} + +// Update 修改信息 +func (r *TraceTask) Update(task model.TraceTask) error { + // 跟踪配置是否开启 + if v := config.Get("trace.enabled"); v != nil { + if !v.(bool) { + return fmt.Errorf("tracking is not enabled") + } + } + host := "127.0.0.1" + if v := config.Get("trace.host"); v != nil { + host = v.(string) + } + var port int64 = 33033 + if v := config.Get("trace.port"); v != nil { + port = parse.Number(v) + } + task.NotifyUrl = fmt.Sprintf("udp:%s:%d", host, port) + + // 查询网元获取IP + neInfo := neService.NewNeInfo.SelectNeInfoByNeTypeAndNeID(task.NeType, task.NeId) + if neInfo.NeId != task.NeId || neInfo.IP == "" { + return fmt.Errorf("app.common.noNEInfo") + } + + // 查询网元任务信息 + if msg, err := neFetchlink.NeTraceInfo(neInfo, task.TraceId); err == nil { + s, _ := json.Marshal(msg) + task.FetchMsg = string(s) + // 修改任务信息 + data := map[string]any{ + "neType": neInfo.NeType, + "neId": neInfo.NeId, + "notifyUrl": task.NotifyUrl, + "id": parse.Number(task.TraceId), + "startTime": date.ParseDateToStr(task.StartTime, date.YYYY_MM_DD_HH_MM_SS), + "endTime": date.ParseDateToStr(task.EndTime, date.YYYY_MM_DD_HH_MM_SS), + } + switch task.TraceType { + case "1": // Interface + data["traceType"] = "Interface" + data["interfaces"] = strings.Split(task.Interfaces, ",") + case "2": // Device + task.UeIp = neInfo.IP + data["traceType"] = "Device" + data["ueIp"] = task.UeIp + data["srcIp"] = task.SrcIp + data["dstIp"] = task.DstIp + data["signalPort"] = task.SignalPort + case "3": // UE + data["traceType"] = "UE" + data["imsi"] = task.IMSI + data["msisdn"] = task.MSISDN + default: + return fmt.Errorf("trace type is not disabled") + } + neFetchlink.NeTraceEdit(neInfo, data) + } + + // 更新数据库 + r.traceTaskRepository.Update(task) + return nil +} + +// DeleteByIds 批量删除信息 +func (r *TraceTask) DeleteByIds(ids []string) (int64, error) { + // 检查是否存在 + rows := r.traceTaskRepository.SelectByIds(ids) + if len(rows) <= 0 { + return 0, fmt.Errorf("not data") + } + + if len(rows) == len(ids) { + // 停止任务 + for _, v := range rows { + neInfo := neService.NewNeInfo.SelectNeInfoByNeTypeAndNeID(v.NeType, v.NeId) + if neInfo.NeId != v.NeId || neInfo.IP == "" { + continue + } + neFetchlink.NeTraceDelete(neInfo, v.TraceId) + } + num := r.traceTaskRepository.DeleteByIds(ids) + return num, nil + } + // 删除信息失败! + return 0, fmt.Errorf("delete fail") +} diff --git a/src/modules/trace/service/trace_task_hlr.go b/src/modules/trace/service/trace_task_hlr.go new file mode 100644 index 00000000..9eee3843 --- /dev/null +++ b/src/modules/trace/service/trace_task_hlr.go @@ -0,0 +1,206 @@ +package service + +import ( + "encoding/json" + "fmt" + "path/filepath" + + "be.ems/src/framework/utils/date" + "be.ems/src/framework/utils/ssh" + neFetchlink "be.ems/src/modules/network_element/fetch_link" + neModel "be.ems/src/modules/network_element/model" + neService "be.ems/src/modules/network_element/service" + "be.ems/src/modules/trace/model" + "be.ems/src/modules/trace/repository" +) + +// 实例化数据层 TraceTaskHlr 结构体 +var NewTraceTaskHlr = &TraceTaskHlr{ + traceTaskHlrRepository: repository.NewTraceTaskHlr, + neInfoService: neService.NewNeInfo, +} + +// TraceTaskHlr 跟踪_任务给HRL网元 服务层处理 +type TraceTaskHlr struct { + traceTaskHlrRepository *repository.TraceTaskHlr // 跟踪_任务给HRL网元数据信息 + neInfoService *neService.NeInfo // 网元信息服务 +} + +// SelectPage 根据条件分页查询 +func (r *TraceTaskHlr) SelectPage(querys model.TraceTaskHlrQuery) map[string]any { + return r.traceTaskHlrRepository.SelectPage(querys) +} + +// SelectById 通过ID查询 +func (r *TraceTaskHlr) SelectById(id string) model.TraceTaskHlr { + tasks := r.traceTaskHlrRepository.SelectByIds([]string{id}) + if len(tasks) > 0 { + return tasks[0] + } + return model.TraceTaskHlr{} +} + +// Insert 新增信息 +func (r *TraceTaskHlr) Insert(task model.TraceTaskHlr) string { + return r.traceTaskHlrRepository.Insert(task) +} + +// Update 修改信息 +func (r *TraceTaskHlr) Update(task model.TraceTaskHlr) int64 { + return r.traceTaskHlrRepository.Update(task) +} + +// DeleteByIds 批量删除信息 +func (r *TraceTaskHlr) DeleteByIds(ids []string) (int64, error) { + // 检查是否存在 + rows := r.traceTaskHlrRepository.SelectByIds(ids) + if len(rows) <= 0 { + return 0, fmt.Errorf("not data") + } + + if len(rows) == len(ids) { + // 停止任务 + neInfos := r.neInfoService.SelectList(neModel.NeInfo{NeType: "UDM"}, false, false) + for _, r := range rows { + if r.Status == "0" { + continue + } + for _, v := range neInfos { + neFetchlink.HLRTraceStop(v, map[string]any{ + "traceIDArray": []string{r.TraceId}, + }) + } + } + num := r.traceTaskHlrRepository.DeleteByIds(ids) + return num, nil + } + // 删除信息失败! + return 0, fmt.Errorf("delete fail") +} + +// Start 创建任务 +func (r *TraceTaskHlr) Start(task model.TraceTaskHlr) (string, error) { + hlrList := []map[string]any{} + traceId := r.traceTaskHlrRepository.LastID() + 1 // 生成任务ID < 65535 + data := map[string]any{ + "traceID": traceId, + "imsi": task.IMSI, + "msisdn": task.MSISDN, + } + if task.StartTime > task.EndTime { + return "", fmt.Errorf("startTime must less than endTime") + } + if task.StartTime > 0 { + data["startTime"] = date.ParseDateToStr(task.StartTime, date.YYYY_MM_DDTHH_MM_SSZ) + } + if task.StartTime > 0 { + data["endTime"] = date.ParseDateToStr(task.EndTime, date.YYYY_MM_DDTHH_MM_SSZ) + } + + // 发送创建任务 + neInfos := r.neInfoService.SelectList(neModel.NeInfo{NeType: "UDM"}, false, false) + for _, neInfo := range neInfos { + hlrItem := map[string]any{ + "neType": neInfo.NeType, + "neId": neInfo.NeId, + "msg": "", + } + msg, err := neFetchlink.HLRTraceStart(neInfo, data) + if err != nil { + hlrItem["err"] = err.Error() + } else { + hlrItem["err"] = msg + } + hlrList = append(hlrList, hlrItem) + } + + msg, _ := json.Marshal(hlrList) + task.Msg = string(msg) + task.Status = "1" + task.TraceId = fmt.Sprint(traceId) + id := r.traceTaskHlrRepository.Insert(task) + if id == "" { + return "", fmt.Errorf("start task fail") + } + return id, nil +} + +// Stop 停止任务 +func (r *TraceTaskHlr) Stop(task model.TraceTaskHlr) error { + hlrList := []map[string]any{} + // 发送停止任务 + neInfos := r.neInfoService.SelectList(neModel.NeInfo{NeType: "UDM"}, false, false) + for _, neInfo := range neInfos { + hlrItem := map[string]any{ + "neType": neInfo.NeType, + "neId": neInfo.NeId, + "msg": "", + } + msg, err := neFetchlink.HLRTraceStop(neInfo, map[string]any{ + "traceIDArray": []string{task.TraceId}, + }) + if err != nil { + hlrItem["err"] = err.Error() + } else { + hlrItem["err"] = msg + } + hlrList = append(hlrList, hlrItem) + } + + msg, _ := json.Marshal(hlrList) + task.Msg = string(msg) + task.Status = "0" + rows := r.traceTaskHlrRepository.Update(task) + if rows <= 0 { + return fmt.Errorf("stop task fail") + } + return nil +} + +// File 任务文件 +func (r *TraceTaskHlr) File(traceId, dirPath string) ([]map[string]any, error) { + hlrList := []map[string]any{} + // 查询所有匹配的网元类型 + neInfos := r.neInfoService.SelectList(neModel.NeInfo{NeType: "UDM"}, false, false) + if len(neInfos) == 0 { + return nil, fmt.Errorf("not found network element") + } + + // 遍历多个网元主机获取文件 + for _, neInfo := range neInfos { + hlrItem := map[string]any{ + "neType": neInfo.NeType, + "neId": neInfo.NeId, + "neName": neInfo.NeName, + "err": "", + } + + // 网元主机的SSH客户端 + sshClient, err := r.neInfoService.NeRunSSHClient(neInfo.NeType, neInfo.NeId) + if err != nil { + hlrItem["err"] = "ssh link fail" + hlrList = append(hlrList, hlrItem) + continue + } + defer sshClient.Close() + + // 获取文件列表 + fileName := fmt.Sprintf("%s_%s", neInfo.NeName, traceId) + rows, err := ssh.FileList(sshClient, filepath.ToSlash(dirPath), fileName) + if err != nil { + hlrItem["err"] = "file not found" + hlrList = append(hlrList, hlrItem) + continue + } + + // 遍历组装 + for _, v := range rows { + if v.FileType == "file" { + hlrItem["fileName"] = v.FileName + hlrItem["filePath"] = filepath.ToSlash(filepath.Join(dirPath, v.FileName)) + hlrList = append(hlrList, hlrItem) + } + } + } + return hlrList, nil +} diff --git a/src/modules/trace/service/trace_task_udp_data.go b/src/modules/trace/service/trace_task_udp_data.go new file mode 100644 index 00000000..2f007f38 --- /dev/null +++ b/src/modules/trace/service/trace_task_udp_data.go @@ -0,0 +1,334 @@ +package service + +import ( + "encoding/base64" + "encoding/binary" + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + "time" + + "golang.org/x/net/http/httpguts" + "golang.org/x/net/http2/hpack" +) + +const ( + GTPU_V1_VERSION = 1 << 5 + GTPU_VER_MASK = 7 << 5 + GTPU_PT_GTP = 1 << 4 + GTPU_HEADER_LEN = 12 + GTPU_E_S_PB_BIT = 7 + GTPU_E_BI = 1 << 2 +) + +const ( + GTPU_HEADER_VERSION_INDEX = 0 + GTPU_HEADER_MSG_TYPE_INDEX = 1 + GTPU_HEADER_LENGTH_INDEX = 2 + GTPU_HEADER_TEID_INDEX = 4 +) + +type ExtHeader struct { + TaskId uint32 + IMSI string + IfType int + MsgType int + MsgDirect int // 0-recv,1-send + TimeStamp int64 + SrcIP string + DstIP string + SrcPort uint16 + DstPort uint16 + Proto int // Protocol + PPI int // only for SCTP + DataLen uint16 + DataInfo []byte +} + +// parseUDPData 解析UDP数据 +func parseUDPData(rvMsg []byte, rvLen int) (ExtHeader, error) { + var extHdr ExtHeader + // var tr dborm.TraceData + var off int + msg := rvMsg + + verFlags := msg[GTPU_HEADER_VERSION_INDEX] + + gtpuHdrLen := GTPU_HEADER_LEN + + localTeid := binary.BigEndian.Uint32(msg[GTPU_HEADER_TEID_INDEX:]) + + extHdr.TaskId = localTeid + + if (verFlags & GTPU_E_S_PB_BIT) != 0 { + if (verFlags & GTPU_E_BI) != 0 { + extTypeIndex := GTPU_HEADER_LEN - 1 + + extType := msg[extTypeIndex] + + if extType == 0xFE { + extHdr.IMSI = string(msg[extTypeIndex+2 : extTypeIndex+17]) + extHdr.IfType = int(msg[extTypeIndex+17]) + extHdr.MsgType = int(msg[extTypeIndex+18]) + extHdr.MsgDirect = int(msg[extTypeIndex+19]) + + extHdr.TimeStamp = time.Now().UTC().UnixMilli() + // extHdr.TimeStamp = int64(binary.BigEndian.Uint64(msg[extTypeIndex+19:])) + // fmt.Printf("ext info %v %s %d %d %d \n", msg[(extTypeIndex+2):(extTypeIndex+20)], extHdr.IMSI, extHdr.IfType, extHdr.MsgType, extHdr.MsgDirect) + // set offset of IP Packet + off = 40 + 4 + //src ip: msg+40+12 + extHdr.SrcIP = fmt.Sprintf("%d.%d.%d.%d", msg[off+12], msg[off+13], msg[off+14], msg[off+15]) + //dst ip: msg+40+12+4 + extHdr.DstIP = fmt.Sprintf("%d.%d.%d.%d", msg[off+16], msg[off+17], msg[off+18], msg[off+19]) + extHdr.SrcPort = uint16(binary.BigEndian.Uint16(msg[off+20:])) + extHdr.DstPort = uint16(binary.BigEndian.Uint16(msg[off+22:])) + // fmt.Printf("info %s:%d %s:%d \n", extHdr.SrcIP, extHdr.SrcPort, extHdr.DstIP, extHdr.DstPort) + // ip header start msg+40 + extHdr.DataLen = uint16(rvLen - off) + extHdr.DataInfo = make([]byte, int(rvLen-off)) + copy(extHdr.DataInfo, []byte(msg[off:])) + + // 132 SCTP + // 6 TCP + // 17 UDP + extHdr.Proto = int(msg[off+9]) + if extHdr.Proto == 132 { + extHdr.PPI = int(msg[off+47]) + extHdr.DataLen = uint16(binary.BigEndian.Uint16(msg[(off+34):]) - 16) + // fmt.Printf("dat len %d %d \n", extHdr.DataLen, extHdr.PPI) + } + } + + for extType != 0 && extTypeIndex < rvLen { + extLen := msg[extTypeIndex+1] << 2 + if extLen == 0 { + return extHdr, fmt.Errorf("error, extLen is zero") + } + + gtpuHdrLen += int(extLen) + extTypeIndex += int(extLen) + extType = msg[extTypeIndex] + } + } + } else { + gtpuHdrLen -= 4 + } + return extHdr, nil +} + +// UDPDataHandler UDP数据处理 +func UDPDataHandler(data []byte, n int) (map[string]any, error) { + extHdr, err := parseUDPData(data, n) + if err != nil { + return nil, err + } + if extHdr.TaskId == 0 || extHdr.DataLen < 1 { + return nil, fmt.Errorf("data error") + } + + m := map[string]any{ + "taskId": extHdr.TaskId, + "imsi": extHdr.IMSI, + "ifType": extHdr.IfType, + "srcAddr": fmt.Sprintf("%s:%d", extHdr.SrcIP, extHdr.SrcPort), + "dstAddr": fmt.Sprintf("%s:%d", extHdr.DstIP, extHdr.DstPort), + "msgType": extHdr.MsgType, + "msgDirect": extHdr.MsgDirect, + "timestamp": extHdr.TimeStamp, + "dataLen": extHdr.DataLen, + // "dataInfo": extHdr.DataInfo, + "decMsg": "", + } + // Base64 编码 + m["dataInfo"] = base64.StdEncoding.EncodeToString(extHdr.DataInfo) + + if extHdr.Proto == 6 { // TCP + // 取响应数据 + iplen := uint16(binary.BigEndian.Uint16(extHdr.DataInfo[2:])) + tcplen := uint16(iplen - 32 - 20) + hdrlen := uint16(binary.BigEndian.Uint16(extHdr.DataInfo[20+32+1:])) + offset := uint16(52) + // fmt.Printf("HTTP %d %d %d \n", iplen, tcplen, hdrlen) + if tcplen > (hdrlen + 9) { // has data + doffset := uint16(offset + hdrlen + 9) + datlen := uint16(binary.BigEndian.Uint16(extHdr.DataInfo[doffset+1:])) + // fmt.Printf("HTTP datlen %d \n", datlen) + m["decMsg"], _ = httpDataMsg(extHdr.DataInfo[offset+9:offset+9+hdrlen], extHdr.DataInfo[doffset+9:doffset+datlen+9]) + } else { + m["decMsg"], _ = httpDataMsg(extHdr.DataInfo[offset+9:hdrlen], nil) + } + } + + // pcap文件 + m["pcapFile"] = writePcap(extHdr) + return m, nil +} + +// =========== TCP协议Body =========== + +// httpDataMsg Http数据信息处理 +func httpDataMsg(header []byte, data []byte) (string, error) { + var remainSize = uint32(16 << 20) + var sawRegular bool + var invalid bool // pseudo header field errors + var Fields []hpack.HeaderField + + invalid = false + hdec := hpack.NewDecoder(4096, nil) + hdec.SetEmitEnabled(true) + hdec.SetMaxStringLength(int(16 << 20)) + hdec.SetEmitFunc(func(hf hpack.HeaderField) { + if !httpguts.ValidHeaderFieldValue(hf.Value) { + // Don't include the value in the error, because it may be sensitive. + invalid = true + } + isPseudo := strings.HasPrefix(hf.Name, ":") + if isPseudo { + if sawRegular { + invalid = true + } + } else { + sawRegular = true + if !validWireHeaderFieldName(hf.Name) { + invalid = true + } + } + + if invalid { + hdec.SetEmitEnabled(false) + return + } + + size := hf.Size() + if size > remainSize { + hdec.SetEmitEnabled(false) + //mh.Truncated = true + return + } + remainSize -= size + + Fields = append(Fields, hf) + }) + + // defer hdec.SetEmitFunc(func(hf hpack.HeaderField) {}) + + frag := header + if _, err := hdec.Write(frag); err != nil { + return "", err + } + + if err := hdec.Close(); err != nil { + return "", err + } + + // hdec.SetEmitFunc(func(hf hpack.HeaderField) {}) + + var headers []byte + var line string + for i := range Fields { + line = fmt.Sprintf("\"%s\":\"%s\",", Fields[i].Name, Fields[i].Value) + headers = append(headers, []byte(line)...) + } + + if len(data) > 0 { + return fmt.Sprintf("{ %s \"content\":%s }", string(headers), string(data)), nil + } else { + return fmt.Sprintf("{ %s }", string(headers)), nil + } +} + +// validWireHeaderFieldName 校验报文头字段名称 +func validWireHeaderFieldName(v string) bool { + if len(v) == 0 { + return false + } + for _, r := range v { + if !httpguts.IsTokenRune(r) { + return false + } + if 'A' <= r && r <= 'Z' { + return false + } + } + return true +} + +// =========== writePcap 写Pcap文件 =========== + +const magicMicroseconds = 0xA1B2C3D4 +const versionMajor = 2 +const versionMinor = 4 + +func writeEmptyPcap(filename string, timeStamp int64, length int, data []byte) error { + var err error + var file *os.File + if err := os.MkdirAll(filepath.Dir(filename), 0775); err != nil { + return err + } + if _, err = os.Stat(filename); os.IsNotExist(err) { + file, err = os.Create(filename) + // File Header + var fileHeaderBuf [24]byte + binary.LittleEndian.PutUint32(fileHeaderBuf[0:4], magicMicroseconds) + binary.LittleEndian.PutUint16(fileHeaderBuf[4:6], versionMajor) + binary.LittleEndian.PutUint16(fileHeaderBuf[6:8], versionMinor) + // bytes 8:12 stay 0 (timezone = UTC) + // bytes 12:16 stay 0 (sigfigs is always set to zero, according to + // http://wiki.wireshark.org/Development/LibpcapFileFormat + binary.LittleEndian.PutUint32(fileHeaderBuf[16:20], 0x00040000) + binary.LittleEndian.PutUint32(fileHeaderBuf[20:24], 0x00000071) + if _, err := file.Write(fileHeaderBuf[:]); err != nil { + return err + } + } else { + file, err = os.OpenFile(filename, os.O_WRONLY|os.O_APPEND, 0666) + } + if err != nil { + return err + } + defer file.Close() + + // Packet Header + var packetHeaderBuf [24]byte + t := time.UnixMilli(timeStamp) + if t.IsZero() { + t = time.Now() + } + secs := t.Unix() + usecs := t.Nanosecond() / 1000 + binary.LittleEndian.PutUint32(packetHeaderBuf[0:4], uint32(secs)) + binary.LittleEndian.PutUint32(packetHeaderBuf[4:8], uint32(usecs)) + binary.LittleEndian.PutUint32(packetHeaderBuf[8:12], uint32(length+16)) + binary.LittleEndian.PutUint32(packetHeaderBuf[12:16], uint32(length+16)) + if _, err := file.Write(packetHeaderBuf[:]); err != nil { + return err + } + + // 数据包内容的定义 + cooked := [...]byte{0x00, 0x00, 0x03, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00} + if _, err := file.Write(cooked[:]); err != nil { + return err + } + + // Packet Data + if _, err := file.Write(data); err != nil { + return err + } + return nil +} + +// writePcap 写Pcap文件并返回文件路径 +func writePcap(extHdr ExtHeader) string { + filePath := fmt.Sprintf("/tmp/omc/trace/task_%d.pcap", extHdr.TaskId) + if runtime.GOOS == "windows" { + filePath = fmt.Sprintf("C:%s", filePath) + } + err := writeEmptyPcap(filePath, extHdr.TimeStamp, int(extHdr.DataLen), extHdr.DataInfo) + if err != nil { + return "" + } + return filePath +} diff --git a/src/modules/trace/trace.go b/src/modules/trace/trace.go index dd38c724..0fe3b742 100644 --- a/src/modules/trace/trace.go +++ b/src/modules/trace/trace.go @@ -5,6 +5,7 @@ import ( "be.ems/src/framework/middleware" "be.ems/src/framework/middleware/collectlogs" "be.ems/src/modules/trace/controller" + "be.ems/src/modules/trace/service" "github.com/gin-gonic/gin" ) @@ -13,25 +14,131 @@ import ( func Setup(router *gin.Engine) { logger.Infof("开始加载 ====> trace 模块路由") - traceGroup := router.Group("/trace") + // 启动时需要的初始参数 + InitLoad() // 信令抓包 - tcpdumpGroup := traceGroup.Group("/tcpdump") + tcpdumpGroup := router.Group("/trace/tcpdump") { tcpdumpGroup.POST("/start", middleware.PreAuthorize(nil), collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.tcpdump", collectlogs.BUSINESS_TYPE_OTHER)), - controller.NewTcpdump.DumpStart, + controller.NewTCPdump.DumpStart, ) tcpdumpGroup.POST("/stop", middleware.PreAuthorize(nil), collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.tcpdump", collectlogs.BUSINESS_TYPE_OTHER)), - controller.NewTcpdump.DumpStop, + controller.NewTCPdump.DumpStop, ) - tcpdumpGroup.POST("/traceUPF", + tcpdumpGroup.POST("/upf", middleware.PreAuthorize(nil), collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.tcpdump", collectlogs.BUSINESS_TYPE_OTHER)), - controller.NewTcpdump.TraceUPF, + controller.NewTCPdump.UPFTrace, + ) + } + + // 信令跟踪 + packetGroup := router.Group("/trace/packet") + { + packetGroup.GET("/devices", + middleware.PreAuthorize(nil), + controller.NewPacket.Devices, + ) + packetGroup.POST("/start", + middleware.PreAuthorize(nil), + collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.packet", collectlogs.BUSINESS_TYPE_OTHER)), + controller.NewPacket.Start, + ) + packetGroup.POST("/stop", + middleware.PreAuthorize(nil), + collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.packet", collectlogs.BUSINESS_TYPE_OTHER)), + controller.NewPacket.Stop, + ) + packetGroup.PUT("/filter", + middleware.PreAuthorize(nil), + collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.packet", collectlogs.BUSINESS_TYPE_OTHER)), + controller.NewPacket.Filter, + ) + packetGroup.PUT("/keep-alive", + middleware.PreAuthorize(nil), + controller.NewPacket.KeepAlive, + ) + } + + // 跟踪任务 网元HLR (免登录) + taskHLRGroup := router.Group("/trace/task/hlr") + { + taskHLRGroup.GET("/list", + controller.NewTraceTaskHlr.List, + ) + taskHLRGroup.DELETE("/:ids", + controller.NewTraceTaskHlr.Remove, + ) + taskHLRGroup.POST("/start", + controller.NewTraceTaskHlr.Start, + ) + taskHLRGroup.POST("/stop", + controller.NewTraceTaskHlr.Stop, + ) + taskHLRGroup.POST("/file", + controller.NewTraceTaskHlr.File, + ) + taskHLRGroup.GET("/filePull", + controller.NewTraceTaskHlr.FilePull, + ) + } + + // 跟踪任务 + taskGroup := router.Group("/trace/task") + { + taskGroup.GET("/list", + middleware.PreAuthorize(nil), + controller.NewTraceTask.List, + ) + taskGroup.GET("/:id", + middleware.PreAuthorize(nil), + controller.NewTraceTask.Info, + ) + taskGroup.POST("", + middleware.PreAuthorize(nil), + collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.task", collectlogs.BUSINESS_TYPE_INSERT)), + controller.NewTraceTask.Add, + ) + taskGroup.PUT("", + middleware.PreAuthorize(nil), + collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.task", collectlogs.BUSINESS_TYPE_UPDATE)), + controller.NewTraceTask.Edit, + ) + taskGroup.DELETE("/:ids", + middleware.PreAuthorize(nil), + collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.task", collectlogs.BUSINESS_TYPE_DELETE)), + controller.NewTraceTask.Remove, + ) + taskGroup.GET("/filePull", + middleware.PreAuthorize(nil), + controller.NewTraceTask.FilePull, + ) + } + + // 跟踪数据 + taskDataGroup := router.Group("/trace/data") + { + taskDataGroup.GET("/list", + middleware.PreAuthorize(nil), + controller.NewTraceData.List, + ) + taskDataGroup.DELETE("/:ids", + middleware.PreAuthorize(nil), + collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.taskData", collectlogs.BUSINESS_TYPE_DELETE)), + controller.NewTraceData.Remove, ) } } + +// InitLoad 初始参数 +func InitLoad() { + // 创建跟踪任务信令数据通道UDP + if err := service.NewTraceTask.CreateUDP(); err != nil { + logger.Errorf("socket udp init fail: %s", err.Error()) + } +} diff --git a/src/modules/ws/controller/ws.go b/src/modules/ws/controller/ws.go index 8e077c03..b0aece83 100644 --- a/src/modules/ws/controller/ws.go +++ b/src/modules/ws/controller/ws.go @@ -1,45 +1,35 @@ package controller import ( - "encoding/json" - "fmt" - "strconv" "strings" - "time" - - neService "be.ems/src/modules/network_element/service" "be.ems/src/framework/i18n" "be.ems/src/framework/logger" "be.ems/src/framework/utils/ctx" "be.ems/src/framework/utils/parse" - "be.ems/src/framework/utils/ssh" - "be.ems/src/framework/utils/telnet" "be.ems/src/framework/vo/result" "be.ems/src/modules/ws/service" + "github.com/gin-gonic/gin" ) -// 实例化控制层 WSController 结构体 +// NewWSController 实例化控制层 WSController 结构体 var NewWSController = &WSController{ - wsService: service.NewWSImpl, - wsSendService: service.NewWSSendImpl, - neHostService: neService.NewNeHostImpl, + wsService: service.NewWS, + wsSendService: service.NewWSSend, + wsReceiveService: service.NewWSReceive, } -// WebSocket通信 +// WSController WebSocket通信 // // PATH /ws type WSController struct { - // WebSocket 服务 - wsService service.IWS - // WebSocket消息发送 服务 - wsSendService service.IWSSend - // 网元主机连接服务 - neHostService neService.INeHost + wsService *service.WS // WebSocket 服务 + wsSendService *service.WSSend // WebSocket消息发送 服务 + wsReceiveService *service.WSReceive // WebSocket消息接收 服务 } -// 通用 +// WS 通用 // // GET /?subGroupIDs=0 func (s *WSController) WS(c *gin.Context) { @@ -71,19 +61,21 @@ func (s *WSController) WS(c *gin.Context) { } defer conn.Close() - wsClient := s.wsService.NewClient(loginUser.UserID, subGroupIDs, conn, nil) + wsClient := s.wsService.ClientCreate(loginUser.UserID, subGroupIDs, conn, nil) + go s.wsService.ClientWriteListen(wsClient) + go s.wsService.ClientReadListen(wsClient, s.wsReceiveService.Commont) // 等待停止信号 for value := range wsClient.StopChan { - s.wsService.CloseClient(wsClient.ID) + s.wsService.ClientClose(wsClient.ID) logger.Infof("ws Stop Client UID %s %s", wsClient.BindUid, value) return } } -// 测试 +// Test 测试 // -// GET /test?clientId=&groupID= +// GET /test?clientId=xxx&groupID=xxx func (s *WSController) Test(c *gin.Context) { language := ctx.AcceptLanguage(c) @@ -114,205 +106,3 @@ func (s *WSController) Test(c *gin.Context) { c.JSON(200, result.OkData(errMsgArr)) } - -// SSH终端 -// -// GET /ssh?hostId=1&cols=80&rows=40 -func (s *WSController) SSH(c *gin.Context) { - language := ctx.AcceptLanguage(c) - - // 登录用户信息 - loginUser, err := ctx.LoginUser(c) - if err != nil { - c.JSON(401, result.CodeMsg(401, i18n.TKey(language, err.Error()))) - return - } - - // 连接主机ID - hostId := c.Query("hostId") - if hostId == "" { - c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) - return - } - neHost := s.neHostService.SelectById(hostId) - if neHost.HostID != hostId || neHost.HostType != "ssh" { - // 没有可访问主机信息数据! - c.JSON(200, result.ErrMsg(i18n.TKey(language, "neHost.noData"))) - return - } - - // 创建链接SSH客户端 - var connSSH ssh.ConnSSH - neHost.CopyTo(&connSSH) - var client *ssh.ConnSSH - var clientErr error - if neHost.AuthMode == "2" { - client, clientErr = connSSH.NewClientByLocalPrivate() - } else { - client, clientErr = connSSH.NewClient() - } - if clientErr != nil { - // 连接主机失败,请检查连接参数后重试 - c.JSON(200, result.ErrMsg(i18n.TKey(language, "neHost.errByHostInfo"))) - return - } - defer client.Close() - - // 终端单行字符数 - cols, err := strconv.Atoi(c.Query("cols")) - if err != nil { - cols = 80 - } - // 终端显示行数 - rows, err := strconv.Atoi(c.Query("rows")) - if err != nil { - rows = 40 - } - - // 创建SSH客户端会话 - clientSession, err := client.NewClientSession(cols, rows) - if err != nil { - // 连接主机失败,请检查连接参数后重试 - c.JSON(200, result.ErrMsg(i18n.TKey(language, "neHost.errByHostInfo"))) - return - } - defer clientSession.Close() - - // 将 HTTP 连接升级为 WebSocket 连接 - wsConn := s.wsService.UpgraderWs(c.Writer, c.Request) - if wsConn == nil { - return - } - defer wsConn.Close() - - wsClient := s.wsService.NewClient(loginUser.UserID, nil, wsConn, clientSession) - - // 实时读取SSH消息直接输出 - msTicker := time.NewTicker(100 * time.Millisecond) - defer msTicker.Stop() - go func() { - for ms := range msTicker.C { - outputByte := clientSession.Read() - if len(outputByte) > 0 { - outputStr := string(outputByte) - msgByte, _ := json.Marshal(result.Ok(map[string]any{ - "requestId": fmt.Sprintf("ssh_%s_%d", hostId, ms.UnixMilli()), - "data": outputStr, - })) - wsClient.MsgChan <- msgByte - - // 退出ssh登录 - // if strings.LastIndex(outputStr, "logout\r\n") != -1 { - // time.Sleep(1 * time.Second) - // s.wsService.CloseClient(wsClient.ID) - // return - // } - } - } - }() - - // 等待停止信号 - for value := range wsClient.StopChan { - s.wsService.CloseClient(wsClient.ID) - logger.Infof("ws Stop Client UID %s %s", wsClient.BindUid, value) - return - } -} - -// Telnet终端 -// -// GET /telnet?hostId=1 -func (s *WSController) Telnet(c *gin.Context) { - language := ctx.AcceptLanguage(c) - - // 登录用户信息 - loginUser, err := ctx.LoginUser(c) - if err != nil { - c.JSON(401, result.CodeMsg(401, i18n.TKey(language, err.Error()))) - return - } - - // 连接主机ID - hostId := c.Query("hostId") - if hostId == "" { - c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) - return - } - neHost := s.neHostService.SelectById(hostId) - if neHost.HostID != hostId || neHost.HostType != "telnet" { - // 没有可访问主机信息数据! - c.JSON(200, result.ErrMsg(i18n.TKey(language, "neHost.noData"))) - return - } - - // 创建链接Telnet客户端 - var connTelnet telnet.ConnTelnet - neHost.CopyTo(&connTelnet) - client, err := connTelnet.NewClient() - if err != nil { - // 连接主机失败,请检查连接参数后重试 - c.JSON(200, result.ErrMsg(i18n.TKey(language, "neHost.errByHostInfo"))) - return - } - defer client.Close() - - // 终端单行字符数 - cols, err := strconv.Atoi(c.DefaultQuery("cols", "120")) - if err != nil { - cols = 120 - } - // 终端显示行数 - rows, err := strconv.Atoi(c.DefaultQuery("rows", "128")) - if err != nil { - rows = 128 - } - - // 创建Telnet客户端会话 - clientSession, err := client.NewClientSession(cols, rows) - if err != nil { - // 连接主机失败,请检查连接参数后重试 - c.JSON(200, result.ErrMsg(i18n.TKey(language, "neHost.errByHostInfo"))) - return - } - defer clientSession.Close() - - // 将 HTTP 连接升级为 WebSocket 连接 - wsConn := s.wsService.UpgraderWs(c.Writer, c.Request) - if wsConn == nil { - return - } - defer wsConn.Close() - - wsClient := s.wsService.NewClient(loginUser.UserID, nil, wsConn, clientSession) - - // 实时读取Telnet消息直接输出 - msTicker := time.NewTicker(100 * time.Millisecond) - defer msTicker.Stop() - go func() { - for ms := range msTicker.C { - outputByte := clientSession.Read() - if len(outputByte) > 0 { - outputStr := strings.TrimRight(string(outputByte), "\x00") - msgByte, _ := json.Marshal(result.Ok(map[string]any{ - "requestId": fmt.Sprintf("telnet_%s_%d", hostId, ms.UnixMilli()), - "data": outputStr, - })) - wsClient.MsgChan <- msgByte - - // 退出telnet登录 - // if strings.LastIndex(outputStr, "logout\r\n") != -1 { - // time.Sleep(1 * time.Second) - // s.wsService.CloseClient(wsClient.ID) - // return - // } - } - } - }() - - // 等待停止信号 - for value := range wsClient.StopChan { - s.wsService.CloseClient(wsClient.ID) - logger.Infof("ws Stop Client UID %s %s", wsClient.BindUid, value) - return - } -} diff --git a/src/modules/ws/controller/ws_redis.go b/src/modules/ws/controller/ws_redis.go new file mode 100644 index 00000000..febcd3c7 --- /dev/null +++ b/src/modules/ws/controller/ws_redis.go @@ -0,0 +1,69 @@ +package controller + +import ( + "be.ems/src/framework/i18n" + "be.ems/src/framework/logger" + "be.ems/src/framework/redis" + "be.ems/src/framework/utils/ctx" + "be.ems/src/framework/vo/result" + neService "be.ems/src/modules/network_element/service" + + "github.com/gin-gonic/gin" +) + +// Redis 终端 +// +// GET /redis?hostId=1 +func (s *WSController) Redis(c *gin.Context) { + language := ctx.AcceptLanguage(c) + var query struct { + HostId string `form:"hostId" binding:"required"` // 连接主机ID + } + if err := c.ShouldBindQuery(&query); err != nil { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + + // 登录用户信息 + loginUser, err := ctx.LoginUser(c) + if err != nil { + c.JSON(401, result.CodeMsg(401, i18n.TKey(language, err.Error()))) + return + } + + neHost := neService.NewNeHost.SelectById(query.HostId) + if neHost.HostID != query.HostId || neHost.HostType != "redis" { + // 没有可访问主机信息数据! + c.JSON(200, result.ErrMsg(i18n.TKey(language, "neHost.noData"))) + return + } + + // 创建链接Redis客户端 + var connRedis redis.ConnRedis + neHost.CopyTo(&connRedis) + client, err := connRedis.NewClient() + if err != nil { + // 连接主机失败,请检查连接参数后重试 + c.JSON(200, result.ErrMsg(i18n.TKey(language, "neHost.errByHostInfo"))) + return + } + defer client.Close() + + // 将 HTTP 连接升级为 WebSocket 连接 + wsConn := s.wsService.UpgraderWs(c.Writer, c.Request) + if wsConn == nil { + return + } + defer wsConn.Close() + + wsClient := s.wsService.ClientCreate(loginUser.UserID, nil, wsConn, client) + go s.wsService.ClientWriteListen(wsClient) + go s.wsService.ClientReadListen(wsClient, s.wsReceiveService.Redis) + + // 等待停止信号 + for value := range wsClient.StopChan { + s.wsService.ClientClose(wsClient.ID) + logger.Infof("ws Stop Client UID %s %s", wsClient.BindUid, value) + return + } +} diff --git a/src/modules/ws/controller/ws_ssh.go b/src/modules/ws/controller/ws_ssh.go new file mode 100644 index 00000000..db33b6bd --- /dev/null +++ b/src/modules/ws/controller/ws_ssh.go @@ -0,0 +1,118 @@ +package controller + +import ( + "encoding/json" + "fmt" + "time" + + "be.ems/src/framework/i18n" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/ctx" + "be.ems/src/framework/utils/ssh" + "be.ems/src/framework/vo/result" + neService "be.ems/src/modules/network_element/service" + + "github.com/gin-gonic/gin" +) + +// SSH 终端 +// +// GET /ssh?hostId=1&cols=80&rows=40 +func (s *WSController) SSH(c *gin.Context) { + language := ctx.AcceptLanguage(c) + var query struct { + HostId string `form:"hostId" binding:"required"` // 连接主机ID + Cols int `form:"cols"` // 终端单行字符数 + Rows int `form:"rows"` // 终端显示行数 + } + if err := c.ShouldBindQuery(&query); err != nil { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + if query.Cols < 80 || query.Cols > 400 { + query.Cols = 80 + } + if query.Rows < 40 || query.Rows > 1200 { + query.Rows = 40 + } + + // 登录用户信息 + loginUser, err := ctx.LoginUser(c) + if err != nil { + c.JSON(401, result.CodeMsg(401, i18n.TKey(language, err.Error()))) + return + } + + neHost := neService.NewNeHost.SelectById(query.HostId) + if neHost.HostID != query.HostId || neHost.HostType != "ssh" { + // 没有可访问主机信息数据! + c.JSON(200, result.ErrMsg(i18n.TKey(language, "neHost.noData"))) + return + } + + // 创建链接SSH客户端 + var connSSH ssh.ConnSSH + neHost.CopyTo(&connSSH) + var client *ssh.ConnSSH + var clientErr error + if neHost.AuthMode == "2" { + client, clientErr = connSSH.NewClientByLocalPrivate() + } else { + client, clientErr = connSSH.NewClient() + } + if clientErr != nil { + // 连接主机失败,请检查连接参数后重试 + c.JSON(200, result.ErrMsg(i18n.TKey(language, "neHost.errByHostInfo"))) + return + } + defer client.Close() + + // 创建SSH客户端会话 + clientSession, err := client.NewClientSession(query.Cols, query.Rows) + if err != nil { + // 连接主机失败,请检查连接参数后重试 + c.JSON(200, result.ErrMsg(i18n.TKey(language, "neHost.errByHostInfo"))) + return + } + defer clientSession.Close() + + // 将 HTTP 连接升级为 WebSocket 连接 + wsConn := s.wsService.UpgraderWs(c.Writer, c.Request) + if wsConn == nil { + return + } + defer wsConn.Close() + + wsClient := s.wsService.ClientCreate(loginUser.UserID, nil, wsConn, clientSession) + go s.wsService.ClientWriteListen(wsClient) + go s.wsService.ClientReadListen(wsClient, s.wsReceiveService.Shell) + + // 实时读取SSH消息直接输出 + msTicker := time.NewTicker(100 * time.Millisecond) + defer msTicker.Stop() + for { + select { + case ms := <-msTicker.C: + outputByte := clientSession.Read() + if len(outputByte) > 0 { + outputStr := string(outputByte) + msgByte, _ := json.Marshal(result.Ok(map[string]any{ + "requestId": fmt.Sprintf("ssh_%s_%d", neHost.HostID, ms.UnixMilli()), + "data": outputStr, + })) + wsClient.MsgChan <- msgByte + + // 退出ssh登录 + // if strings.LastIndex(outputStr, "logout\r\n") != -1 { + // time.Sleep(1 * time.Second) + // s.wsService.CloseClient(wsClient.ID) + // return + // } + } + case <-wsClient.StopChan: // 等待停止信号 + s.wsService.ClientClose(wsClient.ID) + logger.Infof("ws Stop Client UID %s", wsClient.BindUid) + return + } + } +} diff --git a/src/modules/ws/controller/ws_telnet.go b/src/modules/ws/controller/ws_telnet.go new file mode 100644 index 00000000..34f52240 --- /dev/null +++ b/src/modules/ws/controller/ws_telnet.go @@ -0,0 +1,111 @@ +package controller + +import ( + "encoding/json" + "fmt" + "time" + + "be.ems/src/framework/i18n" + "be.ems/src/framework/logger" + "be.ems/src/framework/telnet" + "be.ems/src/framework/utils/ctx" + "be.ems/src/framework/vo/result" + neService "be.ems/src/modules/network_element/service" + + "github.com/gin-gonic/gin" +) + +// Telnet 终端 +// +// GET /telnet?hostId=1 +func (s *WSController) Telnet(c *gin.Context) { + language := ctx.AcceptLanguage(c) + var query struct { + HostId string `form:"hostId" binding:"required"` // 连接主机ID + Cols int `form:"cols"` // 终端单行字符数 + Rows int `form:"rows"` // 终端显示行数 + } + if err := c.ShouldBindQuery(&query); err != nil { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + if query.Cols < 120 || query.Cols > 400 { + query.Cols = 120 + } + if query.Rows < 128 || query.Rows > 1200 { + query.Rows = 128 + } + + // 登录用户信息 + loginUser, err := ctx.LoginUser(c) + if err != nil { + c.JSON(401, result.CodeMsg(401, i18n.TKey(language, err.Error()))) + return + } + + neHost := neService.NewNeHost.SelectById(query.HostId) + if neHost.HostID != query.HostId || neHost.HostType != "telnet" { + // 没有可访问主机信息数据! + c.JSON(200, result.ErrMsg(i18n.TKey(language, "neHost.noData"))) + return + } + + // 创建链接Telnet客户端 + var connTelnet telnet.ConnTelnet + neHost.CopyTo(&connTelnet) + client, err := connTelnet.NewClient() + if err != nil { + // 连接主机失败,请检查连接参数后重试 + c.JSON(200, result.ErrMsg(i18n.TKey(language, "neHost.errByHostInfo"))) + return + } + defer client.Close() + // 创建Telnet客户端会话 + clientSession, err := client.NewClientSession(query.Cols, query.Rows) + if err != nil { + // 连接主机失败,请检查连接参数后重试 + c.JSON(200, result.ErrMsg(i18n.TKey(language, "neHost.errByHostInfo"))) + return + } + defer clientSession.Close() + + // 将 HTTP 连接升级为 WebSocket 连接 + wsConn := s.wsService.UpgraderWs(c.Writer, c.Request) + if wsConn == nil { + return + } + defer wsConn.Close() + + wsClient := s.wsService.ClientCreate(loginUser.UserID, nil, wsConn, clientSession) + go s.wsService.ClientWriteListen(wsClient) + go s.wsService.ClientReadListen(wsClient, s.wsReceiveService.Telnet) + + // 实时读取Telnet消息直接输出 + msTicker := time.NewTicker(100 * time.Millisecond) + defer msTicker.Stop() + for { + select { + case ms := <-msTicker.C: + outputByte := clientSession.Read() + if len(outputByte) > 0 { + outputStr := string(outputByte) + msgByte, _ := json.Marshal(result.Ok(map[string]any{ + "requestId": fmt.Sprintf("telnet_%s_%d", neHost.HostID, ms.UnixMilli()), + "data": outputStr, + })) + wsClient.MsgChan <- msgByte + + // 退出telnet登录 + // if strings.LastIndex(outputStr, "logout\r\n") != -1 { + // time.Sleep(1 * time.Second) + // s.wsService.CloseClient(wsClient.ID) + // return + // } + } + case <-wsClient.StopChan: // 等待停止信号 + s.wsService.ClientClose(wsClient.ID) + logger.Infof("ws Stop Client UID %s", wsClient.BindUid) + return + } + } +} diff --git a/src/modules/ws/controller/ws_view.go b/src/modules/ws/controller/ws_view.go new file mode 100644 index 00000000..2b2594d2 --- /dev/null +++ b/src/modules/ws/controller/ws_view.go @@ -0,0 +1,97 @@ +package controller + +import ( + "encoding/json" + "fmt" + "time" + + "be.ems/src/framework/i18n" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/ctx" + "be.ems/src/framework/vo/result" + neService "be.ems/src/modules/network_element/service" + + "github.com/gin-gonic/gin" +) + +// ShellView 终端交互式文件内容查看 +// +// GET /view +func (s *WSController) ShellView(c *gin.Context) { + language := ctx.AcceptLanguage(c) + var query struct { + NeType string `form:"neType" binding:"required"` // 网元类型 + NeId string `form:"neId" binding:"required"` // 网元标识id + Cols int `form:"cols"` // 终端单行字符数 + Rows int `form:"rows"` // 终端显示行数 + } + if err := c.ShouldBindQuery(&query); err != nil { + c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400"))) + return + } + if query.Cols < 120 || query.Cols > 400 { + query.Cols = 120 + } + if query.Rows < 40 || query.Rows > 1200 { + query.Rows = 40 + } + + // 登录用户信息 + loginUser, err := ctx.LoginUser(c) + if err != nil { + c.JSON(401, result.CodeMsg(401, i18n.TKey(language, err.Error()))) + return + } + + // 网元主机的SSH客户端 + sshClient, err := neService.NewNeInfo.NeRunSSHClient(query.NeType, query.NeId) + if err != nil { + c.JSON(200, result.ErrMsg(err.Error())) + return + } + defer sshClient.Close() + // ssh连接会话 + clientSession, err := sshClient.NewClientSession(query.Cols, query.Rows) + if err != nil { + c.JSON(200, result.ErrMsg("neinfo ssh client session new err")) + return + } + defer clientSession.Close() + + // 将 HTTP 连接升级为 WebSocket 连接 + wsConn := s.wsService.UpgraderWs(c.Writer, c.Request) + if wsConn == nil { + return + } + defer wsConn.Close() + + wsClient := s.wsService.ClientCreate(loginUser.UserID, nil, wsConn, clientSession) + go s.wsService.ClientWriteListen(wsClient) + go s.wsService.ClientReadListen(wsClient, s.wsReceiveService.ShellView) + + // 等待1秒,排空首次消息 + time.Sleep(1 * time.Second) + _ = clientSession.Read() + + // 实时读取SSH消息直接输出 + msTicker := time.NewTicker(100 * time.Millisecond) + defer msTicker.Stop() + for { + select { + case ms := <-msTicker.C: + outputByte := clientSession.Read() + if len(outputByte) > 0 { + outputStr := string(outputByte) + msgByte, _ := json.Marshal(result.Ok(map[string]any{ + "requestId": fmt.Sprintf("view_%d", ms.UnixMilli()), + "data": outputStr, + })) + wsClient.MsgChan <- msgByte + } + case <-wsClient.StopChan: // 等待停止信号 + s.wsService.ClientClose(wsClient.ID) + logger.Infof("ws Stop Client UID %s", wsClient.BindUid) + return + } + } +} diff --git a/src/modules/ws/model/net_connect.go b/src/modules/ws/model/net_connect.go index 8116c285..0c246897 100644 --- a/src/modules/ws/model/net_connect.go +++ b/src/modules/ws/model/net_connect.go @@ -1,20 +1,20 @@ package model -import "github.com/shirou/gopsutil/v3/net" +import "github.com/shirou/gopsutil/v4/net" // NetConnectData 网络连接进程数据 type NetConnectData struct { Type string `json:"type"` Status string `json:"status"` - Laddr net.Addr `json:"localaddr"` - Raddr net.Addr `json:"remoteaddr"` - PID int32 `json:"PID"` + Laddr net.Addr `json:"localAddr"` + Raddr net.Addr `json:"remoteAddr"` + PID int32 `json:"pid"` Name string `json:"name"` } // NetConnectQuery 网络连接进程查询 type NetConnectQuery struct { - Port int32 `json:"port"` - ProcessName string `json:"processName"` - ProcessID int32 `json:"processID"` + Port int32 `json:"port"` + Name string `json:"name"` + PID int32 `json:"pid"` } diff --git a/src/modules/ws/model/ps_process.go b/src/modules/ws/model/ps_process.go index e93247a8..d52e833b 100644 --- a/src/modules/ws/model/ps_process.go +++ b/src/modules/ws/model/ps_process.go @@ -2,37 +2,33 @@ package model // PsProcessData 进程数据 type PsProcessData struct { - PID int32 `json:"PID"` + PID int32 `json:"pid"` Name string `json:"name"` - PPID int32 `json:"PPID"` + PPID int32 `json:"ppid"` Username string `json:"username"` Status string `json:"status"` - StartTime string `json:"startTime"` + StartTime int64 `json:"startTime"` NumThreads int32 `json:"numThreads"` NumConnections int `json:"numConnections"` CpuPercent string `json:"cpuPercent"` - DiskRead string `json:"diskRead"` - DiskWrite string `json:"diskWrite"` - CmdLine string `json:"cmdLine"` + DiskRead uint64 `json:"diskRead"` + DiskWrite uint64 `json:"diskWrite"` - Rss string `json:"rss"` - VMS string `json:"vms"` - HWM string `json:"hwm"` - Data string `json:"data"` - Stack string `json:"stack"` - Locked string `json:"locked"` - Swap string `json:"swap"` + Rss uint64 `json:"rss"` + VMS uint64 `json:"vms"` + HWM uint64 `json:"hwm"` + Data uint64 `json:"data"` + Stack uint64 `json:"stack"` + Locked uint64 `json:"locked"` + Swap uint64 `json:"swap"` - CpuValue float64 `json:"cpuValue"` - RssValue uint64 `json:"rssValue"` - - Envs []string `json:"envs"` + CmdLine string `json:"cmdLine"` } // PsProcessQuery 进程查询 type PsProcessQuery struct { - Pid int32 `json:"pid"` + PID int32 `json:"pid"` Name string `json:"name"` Username string `json:"username"` } diff --git a/src/modules/ws/processor/cdr_connect.go b/src/modules/ws/processor/cdr_connect.go index fc8dc1e8..5cfc7776 100644 --- a/src/modules/ws/processor/cdr_connect.go +++ b/src/modules/ws/processor/cdr_connect.go @@ -8,6 +8,7 @@ import ( "be.ems/src/framework/vo/result" neDataModel "be.ems/src/modules/network_data/model" neDataService "be.ems/src/modules/network_data/service" + neInfoService "be.ems/src/modules/network_element/service" ) // GetCDRConnectByIMS 获取CDR会话事件-IMS @@ -20,7 +21,14 @@ func GetCDRConnectByIMS(requestID string, data any) ([]byte, error) { return nil, fmt.Errorf("query data structure error") } - dataMap := neDataService.NewCDREventIMSImpl.SelectPage(query) + // 查询网元信息 rmUID + neInfo := neInfoService.NewNeInfo.SelectNeInfoByNeTypeAndNeID(query.NeType, query.NeID) + if neInfo.NeId != query.NeID || neInfo.IP == "" { + return nil, fmt.Errorf("query neinfo not found") + } + query.RmUID = neInfo.RmUID + + dataMap := neDataService.NewCDREventIMS.SelectPage(query) resultByte, err := json.Marshal(result.Ok(map[string]any{ "requestId": requestID, "data": dataMap, @@ -38,7 +46,39 @@ func GetCDRConnectBySMF(requestID string, data any) ([]byte, error) { return nil, fmt.Errorf("query data structure error") } - dataMap := neDataService.NewCDREventSMFImpl.SelectPage(query) + // 查询网元信息 rmUID + neInfo := neInfoService.NewNeInfo.SelectNeInfoByNeTypeAndNeID(query.NeType, query.NeID) + if neInfo.NeId != query.NeID || neInfo.IP == "" { + return nil, fmt.Errorf("query neinfo not found") + } + query.RmUID = neInfo.RmUID + + dataMap := neDataService.NewCDREventSMF.SelectPage(query) + resultByte, err := json.Marshal(result.Ok(map[string]any{ + "requestId": requestID, + "data": dataMap, + })) + return resultByte, err +} + +// GetCDRConnectBySMSC 获取CDR会话事件-SMSC +func GetCDRConnectBySMSC(requestID string, data any) ([]byte, error) { + msgByte, _ := json.Marshal(data) + var query neDataModel.CDREventSMSCQuery + err := json.Unmarshal(msgByte, &query) + if err != nil { + logger.Warnf("ws processor GetCDRConnect err: %s", err.Error()) + return nil, fmt.Errorf("query data structure error") + } + + // 查询网元信息 rmUID + neInfo := neInfoService.NewNeInfo.SelectNeInfoByNeTypeAndNeID(query.NeType, query.NeID) + if neInfo.NeId != query.NeID || neInfo.IP == "" { + return nil, fmt.Errorf("query neinfo not found") + } + query.RmUID = neInfo.RmUID + + dataMap := neDataService.NewCDREventSMSC.SelectPage(query) resultByte, err := json.Marshal(result.Ok(map[string]any{ "requestId": requestID, "data": dataMap, diff --git a/src/modules/ws/processor/ne_state.go b/src/modules/ws/processor/ne_state.go index 64e76e72..bd4b1446 100644 --- a/src/modules/ws/processor/ne_state.go +++ b/src/modules/ws/processor/ne_state.go @@ -28,7 +28,7 @@ func GetNeState(requestID string, data any) ([]byte, error) { } // 查询网元获取IP - neInfo := neService.NewNeInfoImpl.SelectNeInfoByNeTypeAndNeID(querys.NeType, querys.NeID) + neInfo := neService.NewNeInfo.SelectNeInfoByNeTypeAndNeID(querys.NeType, querys.NeID) if neInfo.NeId != querys.NeID || neInfo.IP == "" { return nil, fmt.Errorf("no matching network element information found") } diff --git a/src/modules/ws/processor/net_connect.go b/src/modules/ws/processor/net_connect.go index 6530d0c1..5e242435 100644 --- a/src/modules/ws/processor/net_connect.go +++ b/src/modules/ws/processor/net_connect.go @@ -8,8 +8,8 @@ import ( "be.ems/src/framework/logger" "be.ems/src/framework/vo/result" "be.ems/src/modules/ws/model" - "github.com/shirou/gopsutil/v3/net" - "github.com/shirou/gopsutil/v3/process" + "github.com/shirou/gopsutil/v4/net" + "github.com/shirou/gopsutil/v4/process" ) // GetNetConnections 获取网络连接进程 @@ -29,13 +29,16 @@ func GetNetConnections(requestID string, data any) ([]byte, error) { continue } for _, conn := range connections { - if query.ProcessID > 0 && query.ProcessID != conn.Pid { + if query.PID > 0 && query.PID != conn.Pid { continue } proc, err := process.NewProcess(conn.Pid) if err == nil { - name, _ := proc.Name() - if name != "" && query.ProcessName != "" && !strings.Contains(name, query.ProcessName) { + name, err := proc.Name() + if err != nil { + continue + } + if query.Name != "" && !strings.Contains(name, query.Name) { continue } if query.Port > 0 && query.Port != int32(conn.Laddr.Port) && query.Port != int32(conn.Raddr.Port) { diff --git a/src/modules/ws/processor/ps_process.go b/src/modules/ws/processor/ps_process.go index 45fe6ff3..a685eba3 100644 --- a/src/modules/ws/processor/ps_process.go +++ b/src/modules/ws/processor/ps_process.go @@ -8,11 +8,9 @@ import ( "sync" "be.ems/src/framework/logger" - "be.ems/src/framework/utils/date" - "be.ems/src/framework/utils/parse" "be.ems/src/framework/vo/result" "be.ems/src/modules/ws/model" - "github.com/shirou/gopsutil/v3/process" + "github.com/shirou/gopsutil/v4/process" ) // GetProcessData 获取进程数据 @@ -31,86 +29,68 @@ func GetProcessData(requestID string, data any) ([]byte, error) { return nil, err } - var ( - dataArr = []model.PsProcessData{} - resultMutex sync.Mutex - wg sync.WaitGroup - numWorkers = 4 - ) - - handleData := func(proc *process.Process) { + // 解析数据 + handleData := func(proc *process.Process) (model.PsProcessData, bool) { procData := model.PsProcessData{ PID: proc.Pid, } - if query.Pid > 0 && query.Pid != proc.Pid { - return - } - procName, err := proc.Name() - if procName == "" || err != nil { - return - } else { + if procName, err := proc.Name(); err == nil { procData.Name = procName } - if query.Name != "" && !strings.Contains(procData.Name, query.Name) { - return - } if username, err := proc.Username(); err == nil { procData.Username = username } + + // 查询过滤 + if query.PID > 0 && procData.PID != query.PID { + return procData, false + } + if query.Name != "" && !strings.Contains(procData.Name, query.Name) { + return procData, false + } if query.Username != "" && !strings.Contains(procData.Username, query.Username) { - return + return procData, false } procData.PPID, _ = proc.Ppid() - statusArray, _ := proc.Status() - if len(statusArray) > 0 { + if statusArray, err := proc.Status(); err == nil && len(statusArray) > 0 { procData.Status = strings.Join(statusArray, ",") } - createTime, procErr := proc.CreateTime() - if procErr == nil { - procData.StartTime = date.ParseDateToStr(createTime, date.YYYY_MM_DD_HH_MM_SS) + if createTime, err := proc.CreateTime(); err == nil { + procData.StartTime = createTime } procData.NumThreads, _ = proc.NumThreads() - procData.CpuValue, _ = proc.CPUPercent() - procData.CpuPercent = fmt.Sprintf("%.2f", procData.CpuValue) + "%" + if connections, err := proc.Connections(); err == nil { + procData.NumConnections = len(connections) + } + cpuPercent, _ := proc.CPUPercent() + procData.CpuPercent = fmt.Sprintf("%.2f", cpuPercent) menInfo, procErr := proc.MemoryInfo() if procErr == nil { - procData.Rss = parse.Bit(float64(menInfo.RSS)) - procData.Data = parse.Bit(float64(menInfo.Data)) - procData.VMS = parse.Bit(float64(menInfo.VMS)) - procData.HWM = parse.Bit(float64(menInfo.HWM)) - procData.Stack = parse.Bit(float64(menInfo.Stack)) - procData.Locked = parse.Bit(float64(menInfo.Locked)) - procData.Swap = parse.Bit(float64(menInfo.Swap)) - - procData.RssValue = menInfo.RSS - } else { - procData.Rss = "--" - procData.Data = "--" - procData.VMS = "--" - procData.HWM = "--" - procData.Stack = "--" - procData.Locked = "--" - procData.Swap = "--" - - procData.RssValue = 0 + procData.Rss = menInfo.RSS + procData.Data = menInfo.Data + procData.VMS = menInfo.VMS + procData.HWM = menInfo.HWM + procData.Stack = menInfo.Stack + procData.Locked = menInfo.Locked + procData.Swap = menInfo.Swap } - ioStat, procErr := proc.IOCounters() - if procErr == nil { - procData.DiskWrite = parse.Bit(float64(ioStat.WriteBytes)) - procData.DiskRead = parse.Bit(float64(ioStat.ReadBytes)) - } else { - procData.DiskWrite = "--" - procData.DiskRead = "--" + if ioStat, err := proc.IOCounters(); err == nil { + procData.DiskWrite = ioStat.WriteBytes + procData.DiskRead = ioStat.ReadBytes } procData.CmdLine, _ = proc.Cmdline() - procData.Envs, _ = proc.Environ() - resultMutex.Lock() - dataArr = append(dataArr, procData) - resultMutex.Unlock() + return procData, true } + var ( + dataArr = []model.PsProcessData{} + mu sync.Mutex + wg sync.WaitGroup + numWorkers = 4 + ) + chunkSize := (len(processes) + numWorkers - 1) / numWorkers for i := 0; i < numWorkers; i++ { wg.Add(1) @@ -122,9 +102,15 @@ func GetProcessData(requestID string, data any) ([]byte, error) { go func(start, end int) { defer wg.Done() + localDataArr := make([]model.PsProcessData, 0, end-start) // 本地切片避免竞态 for j := start; j < end; j++ { - handleData(processes[j]) + if data, ok := handleData(processes[j]); ok { + localDataArr = append(localDataArr, data) + } } + mu.Lock() + dataArr = append(dataArr, localDataArr...) + mu.Unlock() }(start, end) } diff --git a/src/modules/ws/processor/shell_command.go b/src/modules/ws/processor/shell_command.go new file mode 100644 index 00000000..38f9f583 --- /dev/null +++ b/src/modules/ws/processor/shell_command.go @@ -0,0 +1,71 @@ +package processor + +import ( + "encoding/json" + "fmt" + "strings" + + "be.ems/src/framework/logger" +) + +// ParseCat 解析拼装cat命令 +func ParseCat(reqData any) (string, error) { + msgByte, _ := json.Marshal(reqData) + var data struct { + FilePath string `json:"filePath"` // 文件地址 + ShowNumber bool `json:"showNumber"` // 显示文件的行号,从 1 开始 + ShowAll bool `json:"showAll"` // 结合 -vET 参数,显示所有特殊字符,包括行尾符、制表符等 + } + if err := json.Unmarshal(msgByte, &data); err != nil { + logger.Warnf("ws processor ParseCat err: %s", err.Error()) + return "", fmt.Errorf("query data structure error") + } + if data.FilePath == "" { + return "", fmt.Errorf("query data filePath empty") + } + + command := []string{"cat"} + if data.ShowNumber { + command = append(command, "-n") + } + if data.ShowAll { + command = append(command, "-A") + } + + command = append(command, data.FilePath) + command = append(command, "\n") + return strings.Join(command, " "), nil +} + +// ParseTail 解析拼装tail命令 +func ParseTail(reqData any) (string, error) { + msgByte, _ := json.Marshal(reqData) + var data struct { + FilePath string `json:"filePath"` // 文件地址 + Lines int `json:"lines"` // 显示文件末尾的指定行数 + Char int `json:"char"` // 显示文件末尾的指定字数 + Follow bool `json:"follow"` // 输出文件末尾的内容,并继续监视文件的新增内容 + } + if err := json.Unmarshal(msgByte, &data); err != nil { + logger.Warnf("ws processor ParseTail err: %s", err.Error()) + return "", fmt.Errorf("query data structure error") + } + if data.FilePath == "" { + return "", fmt.Errorf("query data filePath empty") + } + + command := []string{"tail"} + if data.Follow { + command = append(command, "-f") + } + if data.Lines > 0 { + command = append(command, fmt.Sprintf("-n %d", data.Lines)) + } + if data.Char > 0 { + command = append(command, fmt.Sprintf("-c %d", data.Char)) + } + + command = append(command, data.FilePath) + command = append(command, "\n") + return strings.Join(command, " "), nil +} diff --git a/src/modules/ws/processor/ue_connect.go b/src/modules/ws/processor/ue_connect.go index bb0f6492..f6903819 100644 --- a/src/modules/ws/processor/ue_connect.go +++ b/src/modules/ws/processor/ue_connect.go @@ -20,7 +20,7 @@ func GetUEConnectByAMF(requestID string, data any) ([]byte, error) { return nil, fmt.Errorf("query data structure error") } - dataMap := neDataService.NewUEEventAMFImpl.SelectPage(query) + dataMap := neDataService.NewUEEventAMF.SelectPage(query) resultByte, err := json.Marshal(result.Ok(map[string]any{ "requestId": requestID, "data": dataMap, @@ -38,7 +38,7 @@ func GetUEConnectByMME(requestID string, data any) ([]byte, error) { return nil, fmt.Errorf("query data structure error") } - dataMap := neDataService.NewUEEventMMEImpl.SelectPage(query) + dataMap := neDataService.NewUEEventMME.SelectPage(query) resultByte, err := json.Marshal(result.Ok(map[string]any{ "requestId": requestID, "data": dataMap, diff --git a/src/modules/ws/processor/upf_total_flow.go b/src/modules/ws/processor/upf_total_flow.go index 9a86786a..0186dcd3 100644 --- a/src/modules/ws/processor/upf_total_flow.go +++ b/src/modules/ws/processor/upf_total_flow.go @@ -29,12 +29,12 @@ func GetUPFTotalFlow(requestID string, data any) ([]byte, error) { } // 查询网元获取IP - neInfo := neService.NewNeInfoImpl.SelectNeInfoByNeTypeAndNeID(querys.NeType, querys.NeID) + neInfo := neService.NewNeInfo.SelectNeInfoByNeTypeAndNeID(querys.NeType, querys.NeID) if neInfo.NeId != querys.NeID || neInfo.IP == "" { return nil, fmt.Errorf("no matching network element information found") } - dataMap := neDataService.NewPerfKPIImpl.SelectUPFTotalFlow(neInfo.NeType, neInfo.RmUID, querys.Day) + dataMap := neDataService.NewPerfKPI.SelectUPFTotalFlow(neInfo.NeType, neInfo.RmUID, querys.Day) resultByte, err := json.Marshal(result.Ok(map[string]any{ "requestId": requestID, diff --git a/src/modules/ws/service/ws.go b/src/modules/ws/service/ws.go index 32c15f33..728dd609 100644 --- a/src/modules/ws/service/ws.go +++ b/src/modules/ws/service/ws.go @@ -1,25 +1,212 @@ package service import ( + "encoding/json" "net/http" + "sync" + "time" + "be.ems/src/framework/logger" + "be.ems/src/framework/utils/generate" + "be.ems/src/framework/vo/result" "be.ems/src/modules/ws/model" "github.com/gorilla/websocket" ) -// IWS WebSocket通信 服务层接口 -type IWS interface { - // UpgraderWs http升级ws请求 - UpgraderWs(w http.ResponseWriter, r *http.Request) *websocket.Conn +var ( + wsClients sync.Map // ws客户端 [clientId: client] + wsUsers sync.Map // ws用户对应的多个客户端id [uid:clientIds] + wsGroup sync.Map // ws组对应的多个客户端id [groupId:clientIds] +) - // NewClient 新建客户端 - // - // uid 登录用户ID - // groupIDs 用户订阅组 - // conn ws连接实例 - // childConn 子连接实例 - NewClient(uid string, groupIDs []string, conn *websocket.Conn, childConn any) *model.WSClient +// NewWS 实例化服务层 WS 结构体 +var NewWS = &WS{} - // CloseClient 关闭客户端 - CloseClient(clientID string) +// WS WebSocket通信 服务层处理 +type WS struct{} + +// UpgraderWs http升级ws请求 +func (s *WS) UpgraderWs(w http.ResponseWriter, r *http.Request) *websocket.Conn { + wsUpgrader := websocket.Upgrader{ + Subprotocols: []string{"omc-ws"}, + // 设置消息发送缓冲区大小(byte),如果这个值设置得太小,可能会导致服务端在发送大型消息时遇到问题 + WriteBufferSize: 1024, + // 消息包启用压缩 + EnableCompression: true, + // ws握手超时时间 + HandshakeTimeout: 5 * time.Second, + // ws握手过程中允许跨域 + CheckOrigin: func(r *http.Request) bool { + return true + }, + } + conn, err := wsUpgrader.Upgrade(w, r, nil) + if err != nil { + logger.Errorf("ws Upgrade err: %s", err.Error()) + } + return conn +} + +// ClientCreate 客户端新建 +// +// uid 登录用户ID +// groupIDs 用户订阅组 +// conn ws连接实例 +// childConn 子连接实例 +func (s *WS) ClientCreate(uid string, groupIDs []string, conn *websocket.Conn, childConn any) *model.WSClient { + // clientID也可以用其他方式生成,只要能保证在所有服务端中都能保证唯一即可 + clientID := generate.Code(16) + + wsClient := &model.WSClient{ + ID: clientID, + Conn: conn, + LastHeartbeat: time.Now().UnixMilli(), + BindUid: uid, + SubGroup: groupIDs, + MsgChan: make(chan []byte, 100), + StopChan: make(chan struct{}, 1), // 卡死循环标记 + ChildConn: childConn, + } + + // 存入客户端 + wsClients.Store(clientID, wsClient) + + // 存入用户持有客户端 + if uid != "" { + if v, ok := wsUsers.Load(uid); ok { + uidClientIds := v.(*[]string) + *uidClientIds = append(*uidClientIds, clientID) + } else { + wsUsers.Store(uid, &[]string{clientID}) + } + } + + // 存入用户订阅组 + if uid != "" && len(groupIDs) > 0 { + for _, groupID := range groupIDs { + if v, ok := wsGroup.Load(groupID); ok { + groupClientIds := v.(*[]string) + *groupClientIds = append(*groupClientIds, clientID) + } else { + wsGroup.Store(groupID, &[]string{clientID}) + } + } + } + + return wsClient +} + +// ClientClose 客户端关闭 +func (s *WS) ClientClose(clientID string) { + v, ok := wsClients.Load(clientID) + if !ok { + return + } + + client := v.(*model.WSClient) + defer func() { + client.MsgChan <- []byte("ws:close") + client.StopChan <- struct{}{} + client.Conn.Close() + wsClients.Delete(clientID) + }() + + // 客户端断线时自动踢出Uid绑定列表 + if client.BindUid != "" { + if v, ok := wsUsers.Load(client.BindUid); ok { + uidClientIds := v.(*[]string) + if len(*uidClientIds) > 0 { + tempClientIds := make([]string, 0, len(*uidClientIds)) + for _, v := range *uidClientIds { + if v != client.ID { + tempClientIds = append(tempClientIds, v) + } + } + *uidClientIds = tempClientIds + } + } + } + + // 客户端断线时自动踢出已加入的组 + if len(client.SubGroup) > 0 { + for _, groupID := range client.SubGroup { + v, ok := wsGroup.Load(groupID) + if !ok { + continue + } + groupClientIds := v.(*[]string) + if len(*groupClientIds) > 0 { + tempClientIds := make([]string, 0, len(*groupClientIds)) + for _, v := range *groupClientIds { + if v != client.ID { + tempClientIds = append(tempClientIds, v) + } + } + *groupClientIds = tempClientIds + } + } + } +} + +// ClientReadListen 客户端读取消息监听 +// receiveFn 接收函数进行消息处理 +func (s *WS) ClientReadListen(wsClient *model.WSClient, receiveFn func(*model.WSClient, model.WSRequest)) { + defer func() { + if err := recover(); err != nil { + logger.Errorf("ws ReadMessage Panic Error: %v", err) + } + }() + for { + // 读取消息 + messageType, msg, err := wsClient.Conn.ReadMessage() + if err != nil { + logger.Warnf("ws ReadMessage UID %s err: %s", wsClient.BindUid, err.Error()) + s.ClientClose(wsClient.ID) + return + } + // fmt.Println(messageType, string(msg)) + + // 文本 只处理文本json + if messageType == websocket.TextMessage { + var reqMsg model.WSRequest + if err := json.Unmarshal(msg, &reqMsg); err != nil { + msgByte, _ := json.Marshal(result.ErrMsg("message format json error")) + wsClient.MsgChan <- msgByte + continue + } + // 接收器处理 + go receiveFn(wsClient, reqMsg) + } + } +} + +// ClientWriteListen 客户端写入消息监听 +// wsClient.MsgChan <- msgByte 写入消息 +func (s *WS) ClientWriteListen(wsClient *model.WSClient) { + defer func() { + if err := recover(); err != nil { + logger.Errorf("ws WriteMessage Panic Error: %v", err) + } + }() + // 发客户端id确认是否连接 + msgByte, _ := json.Marshal(result.OkData(map[string]string{ + "clientId": wsClient.ID, + })) + wsClient.MsgChan <- msgByte + // 消息发送监听 + for msg := range wsClient.MsgChan { + // 关闭句柄 + if string(msg) == "ws:close" { + wsClient.Conn.WriteMessage(websocket.CloseMessage, []byte{}) + return + } + // 发送消息 + err := wsClient.Conn.WriteMessage(websocket.TextMessage, msg) + if err != nil { + logger.Warnf("ws WriteMessage UID %s err: %s", wsClient.BindUid, err.Error()) + s.ClientClose(wsClient.ID) + return + } + wsClient.LastHeartbeat = time.Now().UnixMilli() + } } diff --git a/src/modules/ws/service/ws.impl.go b/src/modules/ws/service/ws.impl.go deleted file mode 100644 index 71c2615d..00000000 --- a/src/modules/ws/service/ws.impl.go +++ /dev/null @@ -1,223 +0,0 @@ -package service - -import ( - "encoding/json" - "net/http" - "sync" - "time" - - "be.ems/src/framework/logger" - "be.ems/src/framework/utils/generate" - "be.ems/src/framework/vo/result" - "be.ems/src/modules/ws/model" - "github.com/gorilla/websocket" -) - -var ( - // ws客户端 [clientId: client] - WsClients sync.Map - // ws用户对应的多个客户端id [uid:clientIds] - WsUsers sync.Map - // ws组对应的多个用户id [groupID:uids] - WsGroup sync.Map -) - -// 实例化服务层 WSImpl 结构体 -var NewWSImpl = &WSImpl{} - -// WSImpl WebSocket通信 服务层处理 -type WSImpl struct{} - -// UpgraderWs http升级ws请求 -func (s *WSImpl) UpgraderWs(w http.ResponseWriter, r *http.Request) *websocket.Conn { - wsUpgrader := websocket.Upgrader{ - Subprotocols: []string{"omc-ws"}, - // 设置消息发送缓冲区大小(byte),如果这个值设置得太小,可能会导致服务端在发送大型消息时遇到问题 - WriteBufferSize: 1024, - // 消息包启用压缩 - EnableCompression: true, - // ws握手超时时间 - HandshakeTimeout: 5 * time.Second, - // ws握手过程中允许跨域 - CheckOrigin: func(r *http.Request) bool { - return true - }, - } - conn, err := wsUpgrader.Upgrade(w, r, nil) - if err != nil { - logger.Errorf("ws Upgrade err: %s", err.Error()) - } - return conn -} - -// NewClient 新建客户端 -// -// uid 登录用户ID -// groupIDs 用户订阅组 -// conn ws连接实例 -// childConn 子连接实例 -func (s *WSImpl) NewClient(uid string, groupIDs []string, conn *websocket.Conn, childConn any) *model.WSClient { - // clientID也可以用其他方式生成,只要能保证在所有服务端中都能保证唯一即可 - clientID := generate.Code(16) - - wsClient := &model.WSClient{ - ID: clientID, - Conn: conn, - LastHeartbeat: time.Now().UnixMilli(), - BindUid: uid, - SubGroup: groupIDs, - MsgChan: make(chan []byte, 100), - StopChan: make(chan struct{}, 1), // 卡死循环标记 - ChildConn: childConn, - } - - // 存入客户端 - WsClients.Store(clientID, wsClient) - - // 存入用户持有客户端 - if uid != "" { - if v, ok := WsUsers.Load(uid); ok { - uidClientIds := v.(*[]string) - *uidClientIds = append(*uidClientIds, clientID) - } else { - WsUsers.Store(uid, &[]string{clientID}) - } - } - - // 存入用户订阅组 - if uid != "" && len(groupIDs) > 0 { - for _, groupID := range groupIDs { - if v, ok := WsGroup.Load(groupID); ok { - groupUIDs := v.(*[]string) - // 避免同组内相同用户 - hasUid := false - for _, uidv := range *groupUIDs { - if uidv == uid { - hasUid = true - break - } - } - if !hasUid { - *groupUIDs = append(*groupUIDs, uid) - } - } else { - WsGroup.Store(groupID, &[]string{uid}) - } - } - } - - go s.clientRead(wsClient) - go s.clientWrite(wsClient) - - // 发客户端id确认是否连接 - msgByte, _ := json.Marshal(result.OkData(map[string]string{ - "clientId": clientID, - })) - wsClient.MsgChan <- msgByte - - return wsClient -} - -// clientRead 客户端读取消息 -func (s *WSImpl) clientRead(wsClient *model.WSClient) { - defer func() { - if err := recover(); err != nil { - logger.Errorf("ws ReadMessage Panic Error: %v", err) - } - }() - for { - // 读取消息 - messageType, msg, err := wsClient.Conn.ReadMessage() - if err != nil { - logger.Warnf("ws ReadMessage UID %s err: %s", wsClient.BindUid, err.Error()) - s.CloseClient(wsClient.ID) - return - } - // fmt.Println(messageType, string(msg)) - - // 文本和二进制类型,只处理文本json - if messageType == websocket.TextMessage { - var reqMsg model.WSRequest - err := json.Unmarshal(msg, &reqMsg) - if err != nil { - msgByte, _ := json.Marshal(result.ErrMsg("message format not supported")) - wsClient.MsgChan <- msgByte - } else { - // 协程异步处理 - go NewWSReceiveImpl.AsyncReceive(wsClient, reqMsg) - } - } - } -} - -// clientWrite 客户端写入消息 -func (s *WSImpl) clientWrite(wsClient *model.WSClient) { - defer func() { - if err := recover(); err != nil { - logger.Errorf("ws WriteMessage Panic Error: %v", err) - } - }() - for msg := range wsClient.MsgChan { - // 发送消息 - err := wsClient.Conn.WriteMessage(websocket.TextMessage, msg) - if err != nil { - logger.Warnf("ws WriteMessage UID %s err: %s", wsClient.BindUid, err.Error()) - s.CloseClient(wsClient.ID) - return - } - wsClient.LastHeartbeat = time.Now().UnixMilli() - } -} - -// CloseClient 客户端关闭 -func (s *WSImpl) CloseClient(clientID string) { - v, ok := WsClients.Load(clientID) - if !ok { - return - } - - client := v.(*model.WSClient) - defer func() { - client.Conn.WriteMessage(websocket.CloseMessage, []byte{}) - client.Conn.Close() - WsClients.Delete(clientID) - client.StopChan <- struct{}{} - }() - - // 客户端断线时自动踢出Uid绑定列表 - if client.BindUid != "" { - if clientIds, ok := WsUsers.Load(client.BindUid); ok { - uidClientIds := clientIds.(*[]string) - if len(*uidClientIds) > 0 { - tempClientIds := make([]string, 0, len(*uidClientIds)) - for _, v := range *uidClientIds { - if v != client.ID { - tempClientIds = append(tempClientIds, v) - } - } - *uidClientIds = tempClientIds - } - } - } - - // 客户端断线时自动踢出已加入的组 - if client.BindUid != "" && len(client.SubGroup) > 0 { - for _, groupID := range client.SubGroup { - uids, ok := WsGroup.Load(groupID) - if !ok { - continue - } - - groupUIDs := uids.(*[]string) - if len(*groupUIDs) > 0 { - tempUIDs := make([]string, 0, len(*groupUIDs)) - for _, v := range *groupUIDs { - if v != client.BindUid { - tempUIDs = append(tempUIDs, v) - } - } - *groupUIDs = tempUIDs - } - } - } -} diff --git a/src/modules/ws/service/ws_receive.go b/src/modules/ws/service/ws_receive.go index 06b2362c..1b3b516d 100644 --- a/src/modules/ws/service/ws_receive.go +++ b/src/modules/ws/service/ws_receive.go @@ -1,9 +1,334 @@ package service -import "be.ems/src/modules/ws/model" +import ( + "encoding/json" + "fmt" + "io" + "reflect" + "strings" + "time" -// IWSReceive WebSocket消息接收处理 服务层接口 -type IWSReceive interface { - // AsyncReceive 接收业务异步处理 - AsyncReceive(client *model.WSClient, reqMsg model.WSRequest) + "be.ems/src/framework/logger" + "be.ems/src/framework/redis" + "be.ems/src/framework/telnet" + "be.ems/src/framework/utils/ssh" + "be.ems/src/framework/vo/result" + "be.ems/src/modules/ws/model" + "be.ems/src/modules/ws/processor" +) + +// 实例化服务层 WSReceive 结构体 +var NewWSReceive = &WSReceive{} + +// WSReceive WebSocket消息接收处理 服务层处理 +type WSReceive struct{} + +// close 关闭服务连接 +func (s *WSReceive) close(client *model.WSClient) { + // 主动关闭 + resultByte, _ := json.Marshal(result.OkMsg("user initiated closure")) + client.MsgChan <- resultByte + // 等待1s后关闭连接 + time.Sleep(1 * time.Second) + NewWS.ClientClose(client.ID) +} + +// Commont 接收通用业务处理 +func (s *WSReceive) Commont(client *model.WSClient, reqMsg model.WSRequest) { + // 必传requestId确认消息 + if reqMsg.RequestID == "" { + msg := "message requestId is required" + logger.Infof("ws Commont UID %s err: %s", client.BindUid, msg) + msgByte, _ := json.Marshal(result.ErrMsg(msg)) + client.MsgChan <- msgByte + return + } + + var resByte []byte + var err error + + switch reqMsg.Type { + case "close": + s.close(client) + return + case "ps": + resByte, err = processor.GetProcessData(reqMsg.RequestID, reqMsg.Data) + case "net": + resByte, err = processor.GetNetConnections(reqMsg.RequestID, reqMsg.Data) + case "ims_cdr": + resByte, err = processor.GetCDRConnectByIMS(reqMsg.RequestID, reqMsg.Data) + case "smf_cdr": + resByte, err = processor.GetCDRConnectBySMF(reqMsg.RequestID, reqMsg.Data) + case "smsc_cdr": + resByte, err = processor.GetCDRConnectBySMSC(reqMsg.RequestID, reqMsg.Data) + case "amf_ue": + resByte, err = processor.GetUEConnectByAMF(reqMsg.RequestID, reqMsg.Data) + case "mme_ue": + resByte, err = processor.GetUEConnectByMME(reqMsg.RequestID, reqMsg.Data) + case "upf_tf": + resByte, err = processor.GetUPFTotalFlow(reqMsg.RequestID, reqMsg.Data) + case "ne_state": + resByte, err = processor.GetNeState(reqMsg.RequestID, reqMsg.Data) + default: + err = fmt.Errorf("message type %s not supported", reqMsg.Type) + } + + if err != nil { + logger.Warnf("ws Commont UID %s err: %s", client.BindUid, err.Error()) + msgByte, _ := json.Marshal(result.ErrMsg(err.Error())) + client.MsgChan <- msgByte + return + } + if len(resByte) > 0 { + client.MsgChan <- resByte + } +} + +// Shell 接收终端交互业务处理 +func (s *WSReceive) Shell(client *model.WSClient, reqMsg model.WSRequest) { + // 必传requestId确认消息 + if reqMsg.RequestID == "" { + msg := "message requestId is required" + logger.Infof("ws Shell UID %s err: %s", client.BindUid, msg) + msgByte, _ := json.Marshal(result.ErrMsg(msg)) + client.MsgChan <- msgByte + return + } + + var resByte []byte + var err error + + switch reqMsg.Type { + case "close": + s.close(client) + return + case "ssh": + // SSH会话消息接收写入会话 + command := reqMsg.Data.(string) + sshClientSession := client.ChildConn.(*ssh.SSHClientSession) + _, err = sshClientSession.Write(command) + case "resize": + // SSH会话窗口重置 + msgByte, _ := json.Marshal(reqMsg.Data) + var data struct { + Cols int `json:"cols"` + Rows int `json:"rows"` + } + err = json.Unmarshal(msgByte, &data) + if err == nil { + sshClientSession := client.ChildConn.(*ssh.SSHClientSession) + err = sshClientSession.Session.WindowChange(data.Rows, data.Cols) + } + default: + err = fmt.Errorf("message type %s not supported", reqMsg.Type) + } + + if err != nil { + logger.Warnf("ws Shell UID %s err: %s", client.BindUid, err.Error()) + msgByte, _ := json.Marshal(result.ErrMsg(err.Error())) + client.MsgChan <- msgByte + if err == io.EOF { + // 等待1s后关闭连接 + time.Sleep(1 * time.Second) + client.StopChan <- struct{}{} + } + return + } + if len(resByte) > 0 { + client.MsgChan <- resByte + } +} + +// ShellView 接收查看文件终端交互业务处理 +func (s *WSReceive) ShellView(client *model.WSClient, reqMsg model.WSRequest) { + // 必传requestId确认消息 + if reqMsg.RequestID == "" { + msg := "message requestId is required" + logger.Infof("ws ShellView UID %s err: %s", client.BindUid, msg) + msgByte, _ := json.Marshal(result.ErrMsg(msg)) + client.MsgChan <- msgByte + return + } + + var resByte []byte + var err error + + switch reqMsg.Type { + case "close": + s.close(client) + return + case "cat", "tail": + var command string + if reqMsg.Type == "cat" { + command, err = processor.ParseCat(reqMsg.Data) + } + if reqMsg.Type == "tail" { + command, err = processor.ParseTail(reqMsg.Data) + } + if command != "" && err == nil { + sshClientSession := client.ChildConn.(*ssh.SSHClientSession) + _, err = sshClientSession.Write(command) + } + case "ctrl-c": + // 模拟按下 Ctrl+C + sshClientSession := client.ChildConn.(*ssh.SSHClientSession) + _, err = sshClientSession.Write("\u0003\n") + case "resize": + // 会话窗口重置 + msgByte, _ := json.Marshal(reqMsg.Data) + var data struct { + Cols int `json:"cols"` + Rows int `json:"rows"` + } + err = json.Unmarshal(msgByte, &data) + if err == nil { + sshClientSession := client.ChildConn.(*ssh.SSHClientSession) + err = sshClientSession.Session.WindowChange(data.Rows, data.Cols) + } + default: + err = fmt.Errorf("message type %s not supported", reqMsg.Type) + } + + if err != nil { + logger.Warnf("ws ShellView UID %s err: %s", client.BindUid, err.Error()) + msgByte, _ := json.Marshal(result.ErrMsg(err.Error())) + client.MsgChan <- msgByte + if err == io.EOF { + // 等待1s后关闭连接 + time.Sleep(1 * time.Second) + client.StopChan <- struct{}{} + } + return + } + if len(resByte) > 0 { + client.MsgChan <- resByte + } +} + +// Telnet 接收终端交互业务处理 +func (s *WSReceive) Telnet(client *model.WSClient, reqMsg model.WSRequest) { + // 必传requestId确认消息 + if reqMsg.RequestID == "" { + msg := "message requestId is required" + logger.Infof("ws Shell UID %s err: %s", client.BindUid, msg) + msgByte, _ := json.Marshal(result.ErrMsg(msg)) + client.MsgChan <- msgByte + return + } + + var resByte []byte + var err error + + switch reqMsg.Type { + case "close": + s.close(client) + return + case "telnet": + // Telnet会话消息接收写入会话 + command := reqMsg.Data.(string) + telnetClientSession := client.ChildConn.(*telnet.TelnetClientSession) + _, err = telnetClientSession.Write(command) + case "resize": + // Telnet会话窗口重置 + msgByte, _ := json.Marshal(reqMsg.Data) + var data struct { + Cols int `json:"cols"` + Rows int `json:"rows"` + } + err = json.Unmarshal(msgByte, &data) + if err == nil { + // telnetClientSession := client.ChildConn.(*telnet.TelnetClientSession) + // _ = telnetClientSession.WindowChange(data.Rows, data.Cols) + } + default: + err = fmt.Errorf("message type %s not supported", reqMsg.Type) + } + + if err != nil { + logger.Warnf("ws Shell UID %s err: %s", client.BindUid, err.Error()) + msgByte, _ := json.Marshal(result.ErrMsg(err.Error())) + client.MsgChan <- msgByte + if err == io.EOF { + // 等待1s后关闭连接 + time.Sleep(1 * time.Second) + client.StopChan <- struct{}{} + } + return + } + if len(resByte) > 0 { + client.MsgChan <- resByte + } +} + +// Redis 接收终端交互业务处理 +func (s *WSReceive) Redis(client *model.WSClient, reqMsg model.WSRequest) { + // 必传requestId确认消息 + if reqMsg.RequestID == "" { + msg := "message requestId is required" + logger.Infof("ws Shell UID %s err: %s", client.BindUid, msg) + msgByte, _ := json.Marshal(result.ErrMsg(msg)) + client.MsgChan <- msgByte + return + } + + var resByte []byte + var err error + + switch reqMsg.Type { + case "close": + s.close(client) + return + case "redis": + // Redis会话消息接收写入会话 + command := fmt.Sprint(reqMsg.Data) + redisClientSession := client.ChildConn.(*redis.ConnRedis) + output, err := redisClientSession.RunCMD(command) + dataStr := "" + if err != nil { + dataStr = fmt.Sprintf("%s \r\n", err.Error()) + } else { + // 获取结果的反射类型 + resultType := reflect.TypeOf(output) + switch resultType.Kind() { + case reflect.Slice: + // 如果是切片类型需要进一步判断是否是 []string 或 []interface{} + if resultType.Elem().Kind() == reflect.String { + dataStr = fmt.Sprintf("%s \r\n", strings.Join(output.([]string), "\r\n")) + } else if resultType.Elem().Kind() == reflect.Interface { + arr := []string{} + for _, v := range output.([]any) { + arr = append(arr, fmt.Sprintf("%s", v)) + } + dataStr = fmt.Sprintf("%s \r\n", strings.Join(arr, "\r\n")) + } + case reflect.Ptr: + dataStr = "\r\n" + case reflect.String, reflect.Int64: + dataStr = fmt.Sprintf("%s \r\n", output) + default: + dataStr = fmt.Sprintf("%s \r\n", output) + } + } + resByte, _ = json.Marshal(result.Ok(map[string]any{ + "requestId": reqMsg.RequestID, + "data": dataStr, + })) + default: + err = fmt.Errorf("message type %s not supported", reqMsg.Type) + } + + if err != nil { + logger.Warnf("ws Shell UID %s err: %s", client.BindUid, err.Error()) + msgByte, _ := json.Marshal(result.ErrMsg(err.Error())) + client.MsgChan <- msgByte + if err == io.EOF { + // 等待1s后关闭连接 + time.Sleep(1 * time.Second) + client.StopChan <- struct{}{} + } + return + } + if len(resByte) > 0 { + client.MsgChan <- resByte + } } diff --git a/src/modules/ws/service/ws_receive.impl.go b/src/modules/ws/service/ws_receive.impl.go deleted file mode 100644 index 553b1c63..00000000 --- a/src/modules/ws/service/ws_receive.impl.go +++ /dev/null @@ -1,101 +0,0 @@ -package service - -import ( - "encoding/json" - "fmt" - "io" - "time" - - "be.ems/src/framework/logger" - "be.ems/src/framework/utils/ssh" - "be.ems/src/framework/utils/telnet" - "be.ems/src/framework/vo/result" - "be.ems/src/modules/ws/model" - "be.ems/src/modules/ws/processor" -) - -// 实例化服务层 WSReceiveImpl 结构体 -var NewWSReceiveImpl = &WSReceiveImpl{} - -// WSReceiveImpl WebSocket消息接收处理 服务层处理 -type WSReceiveImpl struct{} - -// AsyncReceive 接收业务异步处理 -func (s *WSReceiveImpl) AsyncReceive(client *model.WSClient, reqMsg model.WSRequest) { - // 必传requestId确认消息 - if reqMsg.RequestID == "" { - msg := "message requestId is required" - logger.Infof("ws AsyncReceive UID %s err: %s", client.BindUid, msg) - msgByte, _ := json.Marshal(result.ErrMsg(msg)) - client.MsgChan <- msgByte - return - } - - var resByte []byte - var err error - - switch reqMsg.Type { - case "close": - // 主动关闭 - resultByte, _ := json.Marshal(result.OkMsg("user initiated closure")) - client.MsgChan <- resultByte - // 等待1s后关闭连接 - time.Sleep(1 * time.Second) - client.StopChan <- struct{}{} - case "ssh": - // SSH会话消息接收直接写入会话 - command := reqMsg.Data.(string) - sshClientSession := client.ChildConn.(*ssh.SSHClientSession) - _, err = sshClientSession.Write(command) - case "ssh_resize": - // SSH会话窗口重置 - msgByte, _ := json.Marshal(reqMsg.Data) - var data struct { - Cols int `json:"cols"` - Rows int `json:"rows"` - } - err = json.Unmarshal(msgByte, &data) - if err == nil { - sshClientSession := client.ChildConn.(*ssh.SSHClientSession) - err = sshClientSession.Session.WindowChange(data.Rows, data.Cols) - } - case "telnet": - // Telnet会话消息接收直接写入会话 - command := reqMsg.Data.(string) - telnetClientSession := client.ChildConn.(*telnet.TelnetClientSession) - _, err = telnetClientSession.Write(command) - case "ps": - resByte, err = processor.GetProcessData(reqMsg.RequestID, reqMsg.Data) - case "net": - resByte, err = processor.GetNetConnections(reqMsg.RequestID, reqMsg.Data) - case "ims_cdr": - resByte, err = processor.GetCDRConnectByIMS(reqMsg.RequestID, reqMsg.Data) - case "smf_cdr": - resByte, err = processor.GetCDRConnectBySMF(reqMsg.RequestID, reqMsg.Data) - case "amf_ue": - resByte, err = processor.GetUEConnectByAMF(reqMsg.RequestID, reqMsg.Data) - case "mme_ue": - resByte, err = processor.GetUEConnectByMME(reqMsg.RequestID, reqMsg.Data) - case "upf_tf": - resByte, err = processor.GetUPFTotalFlow(reqMsg.RequestID, reqMsg.Data) - case "ne_state": - resByte, err = processor.GetNeState(reqMsg.RequestID, reqMsg.Data) - default: - err = fmt.Errorf("message type not supported") - } - - if err != nil { - logger.Warnf("ws AsyncReceive UID %s err: %s", client.BindUid, err.Error()) - msgByte, _ := json.Marshal(result.ErrMsg(err.Error())) - client.MsgChan <- msgByte - if err == io.EOF { - // 等待1s后关闭连接 - time.Sleep(1 * time.Second) - client.StopChan <- struct{}{} - } - return - } - if len(resByte) > 0 { - client.MsgChan <- resByte - } -} diff --git a/src/modules/ws/service/ws_send.go b/src/modules/ws/service/ws_send.go index 020d022a..4d024143 100644 --- a/src/modules/ws/service/ws_send.go +++ b/src/modules/ws/service/ws_send.go @@ -1,10 +1,89 @@ package service -// IWSSend WebSocket消息发送处理 服务层接口 -type IWSSend interface { - // ByClientID 给已知客户端发消息 - ByClientID(clientID string, data any) error +import ( + "encoding/json" + "fmt" - // ByGroupID 给订阅组的用户发送消息 - ByGroupID(gid string, data any) error + "be.ems/src/framework/vo/result" + "be.ems/src/modules/ws/model" +) + +// 订阅组指定编号为支持服务器向客户端主动推送数据 +const ( + // 组号-其他 + GROUP_OTHER = "0" + // 组号-跟踪任务网元数据变更 2_traceId + GROUP_TRACE_NE = "2_" + // 组号-信令跟踪Packet 4_taskNo + GROUP_TRACE_PACKET = "4_" + // 组号-指标通用 10_neType_neId + GROUP_KPI = "10_" + // 组号-指标UPF 12_neId + GROUP_KPI_UPF = "12_" + // 组号-自定义KPI指标 20_neType_neId + GROUP_KPI_C = "20_" + // 组号-IMS_CDR会话事件 1005_neId + GROUP_IMS_CDR = "1005_" + // 组号-SMF_CDR会话事件 1006_neId + GROUP_SMF_CDR = "1006_" + // 组号-SMSC_CDR会话事件 1007_neId + GROUP_SMSC_CDR = "1007_" + // 组号-AMF_UE会话事件 + GROUP_AMF_UE = "1010" + // 组号-MME_UE会话事件 1011_neId + GROUP_MME_UE = "1011_" +) + +// 实例化服务层 WSSend 结构体 +var NewWSSend = &WSSend{} + +// WSSend WebSocket消息发送处理 服务层处理 +type WSSend struct{} + +// ByClientID 给已知客户端发消息 +func (s *WSSend) ByClientID(clientID string, data any) error { + v, ok := wsClients.Load(clientID) + if !ok { + return fmt.Errorf("no fount client ID: %s", clientID) + } + + dataByte, err := json.Marshal(result.OkData(data)) + if err != nil { + return err + } + + client := v.(*model.WSClient) + if len(client.MsgChan) > 90 { + NewWS.ClientClose(client.ID) + return fmt.Errorf("msg chan over 90 will close client ID: %s", clientID) + } + client.MsgChan <- dataByte + return nil +} + +// ByGroupID 给订阅组的客户端发送消息 +func (s *WSSend) ByGroupID(groupID string, data any) error { + clientIds, ok := wsGroup.Load(groupID) + if !ok { + return fmt.Errorf("no fount Group ID: %s", groupID) + } + + // 检查组内是否有客户端 + ids := clientIds.(*[]string) + if len(*ids) == 0 { + return fmt.Errorf("no members in the group") + } + + // 遍历给客户端发消息 + for _, clientId := range *ids { + err := s.ByClientID(clientId, map[string]any{ + "groupId": groupID, + "data": data, + }) + if err != nil { + continue + } + } + + return nil } diff --git a/src/modules/ws/service/ws_send.impl.go b/src/modules/ws/service/ws_send.impl.go deleted file mode 100644 index 2eea685b..00000000 --- a/src/modules/ws/service/ws_send.impl.go +++ /dev/null @@ -1,89 +0,0 @@ -package service - -import ( - "encoding/json" - "fmt" - - "be.ems/src/framework/vo/result" - "be.ems/src/modules/ws/model" -) - -// 订阅组指定编号为支持服务器向客户端主动推送数据 -const ( - // 组号-其他 - GROUP_OTHER = "0" - // 组号-指标通用 10_neType_neId - GROUP_KPI = "10_" - // 组号-指标UPF 12_neId - GROUP_KPI_UPF = "12_" - // 组号-IMS_CDR会话事件 - GROUP_IMS_CDR = "1005" - // 组号-SMF_CDR会话事件 - GROUP_SMF_CDR = "1006" - // 组号-AMF_UE会话事件 - GROUP_AMF_UE = "1010" - // 组号-MME_UE会话事件 - GROUP_MME_UE = "1011" -) - -// 实例化服务层 WSSendImpl 结构体 -var NewWSSendImpl = &WSSendImpl{} - -// IWSSend WebSocket消息发送处理 服务层处理 -type WSSendImpl struct{} - -// ByClientID 给已知客户端发消息 -func (s *WSSendImpl) ByClientID(clientID string, data any) error { - v, ok := WsClients.Load(clientID) - if !ok { - return fmt.Errorf("no fount client ID: %s", clientID) - } - - dataByte, err := json.Marshal(result.OkData(data)) - if err != nil { - return err - } - - client := v.(*model.WSClient) - if len(client.MsgChan) > 90 { - NewWSImpl.CloseClient(client.ID) - return fmt.Errorf("msg chan over 90 will close client ID: %s", clientID) - } - client.MsgChan <- dataByte - return nil -} - -// ByGroupID 给订阅组的用户发送消息 -func (s *WSSendImpl) ByGroupID(groupID string, data any) error { - uids, ok := WsGroup.Load(groupID) - if !ok { - return fmt.Errorf("no fount Group ID: %s", groupID) - } - - groupUids := uids.(*[]string) - // 群组中没有成员 - if len(*groupUids) == 0 { - return fmt.Errorf("no members in the group") - } - - // 在群组中找到对应的 uid - for _, uid := range *groupUids { - clientIds, ok := WsUsers.Load(uid) - if !ok { - continue - } - // 在用户中找到客户端并发送 - uidClientIds := clientIds.(*[]string) - for _, clientId := range *uidClientIds { - err := s.ByClientID(clientId, map[string]any{ - "groupId": groupID, - "data": data, - }) - if err != nil { - continue - } - } - } - - return nil -} diff --git a/src/modules/ws/ws.go b/src/modules/ws/ws.go index cfca8323..35e63f85 100644 --- a/src/modules/ws/ws.go +++ b/src/modules/ws/ws.go @@ -21,6 +21,10 @@ func Setup(router *gin.Engine) { collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.ws", collectlogs.BUSINESS_TYPE_OTHER)), controller.NewWSController.WS, ) + wsGroup.GET("/test", + middleware.PreAuthorize(nil), + controller.NewWSController.Test, + ) wsGroup.GET("/ssh", middleware.PreAuthorize(nil), collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.ws", collectlogs.BUSINESS_TYPE_OTHER)), @@ -31,9 +35,15 @@ func Setup(router *gin.Engine) { collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.ws", collectlogs.BUSINESS_TYPE_OTHER)), controller.NewWSController.Telnet, ) - wsGroup.GET("/test", + wsGroup.GET("/redis", middleware.PreAuthorize(nil), - controller.NewWSController.Test, + collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.ws", collectlogs.BUSINESS_TYPE_OTHER)), + controller.NewWSController.Redis, + ) + wsGroup.GET("/view", + middleware.PreAuthorize(nil), + collectlogs.OperateLog(collectlogs.OptionNew("log.operate.title.ws", collectlogs.BUSINESS_TYPE_OTHER)), + controller.NewWSController.ShellView, ) } }