Merge remote-tracking branch 'origin/main' into multi-tenant

This commit is contained in:
TsMask
2025-02-11 18:40:36 +08:00
37 changed files with 626 additions and 215 deletions

View File

@@ -22,6 +22,7 @@ var URL_WHITE_LIST = []string{
"/omcNeConfig",
"/cdrEvent",
"/ueEvent",
"/objectType/nbState",
"/upload-ue",
"/oauth/token",
}

View File

@@ -18,7 +18,6 @@ import (
"be.ems/src/framework/utils/crypto"
"be.ems/src/framework/utils/ssh"
systemService "be.ems/src/modules/system/service"
"github.com/jlaffaye/ftp"
)
var NewProcessor = &BarProcessor{
@@ -177,7 +176,7 @@ func (s BarProcessor) putFTP(filePath, fileName string) {
Username string `json:"username" binding:"required"`
ToIp string `json:"toIp" binding:"required"`
ToPort int64 `json:"toPort" binding:"required"`
Protocol string `json:"protocol" binding:"required,oneof=ssh ftp"`
Enable bool `json:"enable"`
Dir string `json:"dir" binding:"required"`
}
cfg := systemService.NewSysConfigImpl.SelectConfigByKey("sys.exportTable")
@@ -195,68 +194,37 @@ func (s BarProcessor) putFTP(filePath, fileName string) {
return
}
}
if !cfgData.Enable {
return
}
localFilePath := filepath.Join(filePath, fileName)
if cfgData.Protocol == "ssh" {
connSSH := ssh.ConnSSH{
User: cfgData.Username,
Password: cfgData.Password,
Addr: cfgData.ToIp,
Port: cfgData.ToPort,
AuthMode: "0",
}
sshClient, err := connSSH.NewClient()
if err != nil {
logger.Errorf("putFTP ssh error: %v", err)
return
}
defer sshClient.Close()
// 网元主机的SSH客户端进行文件传输
sftpClient, err := sshClient.NewClientSFTP()
if err != nil {
logger.Errorf("putFTP sftp error: %v", err)
return
}
defer sftpClient.Close()
// 远程文件
remotePath := filepath.Join(cfgData.Dir, path.Base(filePath), fileName)
// 复制到远程
if err = sftpClient.CopyFileLocalToRemote(localFilePath, remotePath); err != nil {
logger.Errorf("putFTP uploading error: %v", err)
return
}
connSSH := ssh.ConnSSH{
User: cfgData.Username,
Password: cfgData.Password,
Addr: cfgData.ToIp,
Port: cfgData.ToPort,
AuthMode: "0",
}
if cfgData.Protocol == "ftp" {
// 连接到 FTP 服务器
addr := fmt.Sprintf("%s:%d", cfgData.ToIp, cfgData.ToPort)
ftpComm, err := ftp.Dial(addr, ftp.DialWithTimeout(15*time.Second))
if err != nil {
logger.Errorf("putFTP ftp error: %v", err)
return
}
// 登录到 FTP 服务器
err = ftpComm.Login(cfgData.Username, cfgData.Password)
if err != nil {
logger.Errorf("putFTP login error: %v", err)
return
}
defer ftpComm.Quit()
// 打开本地文件
file, err := os.Open(localFilePath)
if err != nil {
logger.Errorf("putFTP open error: %v", err)
return
}
defer file.Close()
// 远程文件
remotePath := filepath.Join(cfgData.Dir, path.Base(filePath), fileName)
// 上传文件到 FTP 服务器
err = ftpComm.Stor(remotePath, file)
if err != nil {
logger.Errorf("putFTP uploading error: %v", err)
return
}
sshClient, err := connSSH.NewClient()
if err != nil {
logger.Errorf("putFTP ssh error: %v", err)
return
}
defer sshClient.Close()
// 网元主机的SSH客户端进行文件传输
sftpClient, err := sshClient.NewClientSFTP()
if err != nil {
logger.Errorf("putFTP sftp error: %v", err)
return
}
defer sftpClient.Close()
// 远程文件
remotePath := filepath.Join(cfgData.Dir, path.Base(filePath), fileName)
// 复制到远程
if err = sftpClient.CopyFileLocalToRemote(localFilePath, remotePath); err != nil {
logger.Errorf("putFTP uploading error: %v", err)
return
}
}

View File

@@ -0,0 +1,121 @@
package controller
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
"be.ems/src/framework/i18n"
"be.ems/src/framework/utils/ctx"
"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"
)
// 实例化控制层 NBStateController 结构体
var NewNBState = &NBStateController{
neInfoService: neService.NewNeInfo,
nbStateService: neDataService.NewNBState,
}
// 基站状态历史记录 AMF/MME
//
// PATH /nb-state
type NBStateController struct {
neInfoService *neService.NeInfo // 网元信息服务
nbStateService *neDataService.NBState // 基站状态服务
}
// 历史记录列表
//
// GET /list
//
// @Tags network_data/amf,network_data/mme
// @Accept json
// @Produce json
// @Param neType query string true "NE Type only AMF/MME" Enums(AMF,MME) default(AMF)
// @Param neId query string true "NE ID" default(001)
// @Param pageNum query number true "pageNum" default(1)
// @Param pageSize query number true "pageSize" default(10)
// @Param startTime query number false "Start time (timestamped milliseconds)" default(1729162507596)
// @Param endTime query number false "End time (timestamped milliseconds)" default(1729164187611)
// @Param sortField query string false "Sort fields, fill in result fields" Enums(id,create_time) default(id)
// @Param sortOrder query string false "Sort by ascending or descending order" Enums(asc,desc) default(asc)
// @Success 200 {object} object "Response Results"
// @Security TokenAuth
// @Summary Base Station Status List
// @Description Base Station Status List
// @Router /nb-state/list [get]
func (s NBStateController) List(c *gin.Context) {
language := ctx.AcceptLanguage(c)
var query model.NBStateQuery
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(query.NeType, query.NeID)
if neInfo.NeId != query.NeID || neInfo.IP == "" {
c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.noNEInfo")))
return
}
query.RmUID = neInfo.RmUID
// 查询数据
rows, total := s.nbStateService.SelectPage(query)
c.JSON(200, result.Ok(map[string]any{"rows": rows, "total": total}))
}
// 历史记录列表导出
//
// POST /export
//
// @Tags network_data/amf,network_data/mme
// @Accept json
// @Produce json
// @Param data body object true "Request Param"
// @Success 200 {object} object "Response Results"
// @Security TokenAuth
// @Summary Base Station Status List Export
// @Description Base Station Status List Export
// @Router /nb-state/export [post]
func (s NBStateController) Export(c *gin.Context) {
language := ctx.AcceptLanguage(c)
// 查询结果,根据查询条件结果,单页最大值限制
var querys model.NBStateQuery
if err := c.ShouldBindBodyWithJSON(&querys); 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
rows, total := s.nbStateService.SelectPage(querys)
if total == 0 {
// 导出数据记录为空
c.JSON(200, result.ErrMsg(i18n.TKey(language, "app.common.exportEmpty")))
return
}
// 导出文件名称
fileName := fmt.Sprintf("nb_state_records_export_%d_%d.xlsx", len(rows), time.Now().UnixMilli())
// 导出数据表格
saveFilePath, err := s.nbStateService.ExportXlsx(rows, fileName, language)
if err != nil {
c.JSON(200, result.ErrMsg(err.Error()))
return
}
c.FileAttachment(saveFilePath, fileName)
}

View File

@@ -0,0 +1,35 @@
package model
// NBState 基站状态记录表 nb_state
type NBState struct {
ID int64 `json:"id" gorm:"column:id;primaryKey;autoIncrement"`
NeType string `json:"neType" gorm:"column:ne_type"` // 网元类型 AMF MME
NeId string `json:"neId" gorm:"column:ne_id"` // 网元ID
RmUid string `json:"rmUid" gorm:"column:rm_uid"` // 资源唯一标识
CreateTime int64 `json:"createTime" gorm:"column:create_time"` // 创建时间
Address string `json:"address" gorm:"column:address"` // 基站IP地址
NbName string `json:"nbName" gorm:"column:nb_name"` // 基站设备名称
Name string `json:"name" gorm:"column:name"` // 基站名称
Position string `json:"position" gorm:"column:position"` // 基站位置
State string `json:"state" gorm:"column:state"` // 基站状态 OFF ON
Time string `json:"time" gorm:"column:time"` // 状态时间
}
// TableName 表名称
func (*NBState) TableName() string {
return "nb_state"
}
// NBStateQuery 查询参数结构体
type NBStateQuery struct {
NeType string `json:"neType" form:"neType" binding:"required"`
NeID string `json:"neId" form:"neId" binding:"required"`
PageNum int64 `json:"pageNum" form:"pageNum" binding:"required"`
PageSize int64 `json:"pageSize" form:"pageSize" binding:"required"`
Status string `json:"status" form:"status"`
StartTime string `json:"startTime" form:"startTime"`
EndTime string `json:"endTime" form:"endTime"`
SortField string `json:"sortField" form:"sortField" binding:"omitempty,oneof=id create_time"` // 排序字段,填写结果字段
SortOrder string `json:"sortOrder" form:"sortOrder" binding:"omitempty,oneof=asc desc"` // 排序升降序asc desc
RmUID string `json:"rmUID" form:"rmUID"`
}

View File

@@ -6,7 +6,6 @@ type UDMAuthUser struct {
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

View File

@@ -42,6 +42,19 @@ func Setup(router *gin.Engine) {
)
}
// 基站状态历史记录信息 含AMF/MME
nbStateGroup := neDataGroup.Group("/nb-state")
{
nbStateGroup.GET("/list",
middleware.PreAuthorize(nil),
controller.NewNBState.List,
)
nbStateGroup.POST("/export",
middleware.PreAuthorize(nil),
controller.NewNBState.Export,
)
}
// 网元IMS
imsGroup := neDataGroup.Group("/ims")
{

View File

@@ -0,0 +1,116 @@
package repository
import (
"time"
"be.ems/src/framework/datasource"
"be.ems/src/framework/logger"
"be.ems/src/modules/network_data/model"
)
// 实例化数据层 NBState 结构体
var NewNBState = &NBState{}
// NBState 基站状态记录表 数据层处理
type NBState struct{}
// SelectByPage 分页查询集合
func (r NBState) SelectByPage(query model.NBStateQuery) ([]model.NBState, int64) {
tx := datasource.DB("").Model(&model.NBState{})
// 查询条件拼接
if query.NeType != "" {
tx = tx.Where("ne_type = ?", query.NeType)
}
if query.NeID != "" {
tx = tx.Where("ne_id = ?", query.NeID)
}
if query.RmUID != "" {
tx = tx.Where("rm_uid = ?", query.RmUID)
}
if query.Status != "" {
tx = tx.Where("state = ?", query.Status)
}
if query.StartTime != "" {
startTime := query.StartTime
if len(startTime) == 10 {
startTime = startTime + "000"
}
tx = tx.Where("create_time >= ?", startTime)
}
if query.EndTime != "" {
endTime := query.EndTime
if len(endTime) == 10 {
endTime = endTime + "999"
}
tx = tx.Where("create_time <= ?", endTime)
}
// 查询结果
var total int64 = 0
rows := []model.NBState{}
// 查询数量为0直接返回
if err := tx.Count(&total).Error; err != nil || total <= 0 {
return rows, total
}
// 排序
if query.SortField != "" {
sortField := query.SortField
if query.SortOrder == "desc" {
sortField = sortField + " desc"
}
tx = tx.Order(sortField)
}
// 查询数据分页
pageNum, pageSize := datasource.PageNumSize(query.PageNum, query.PageSize)
tx = tx.Limit(pageSize).Offset(pageSize * pageNum)
err := tx.Find(&rows).Error
if err != nil {
logger.Errorf("query find err => %v", err.Error())
return rows, total
}
return rows, total
}
// SelectByIds 通过ID查询
func (r NBState) SelectByIds(ids []string) []model.NBState {
rows := []model.NBState{}
if len(ids) <= 0 {
return rows
}
tx := datasource.DB("").Model(&model.NBState{})
// 构建查询条件
tx = tx.Where("id in ?", ids)
// 查询数据
if err := tx.Find(&rows).Error; err != nil {
logger.Errorf("query find err => %v", err.Error())
return rows
}
return rows
}
// DeleteByIds 批量删除信息
func (r NBState) DeleteByIds(ids []string) int64 {
if len(ids) <= 0 {
return 0
}
tx := datasource.DB("").Where("id in ?", ids)
if err := tx.Delete(&model.NBState{}).Error; err != nil {
logger.Errorf("delete err => %v", err.Error())
return 0
}
return tx.RowsAffected
}
// Insert 新增信息
func (r NBState) Insert(param model.NBState) int64 {
param.CreateTime = time.Now().UnixMilli()
// 执行插入
if err := datasource.DB("").Create(&param).Error; err != nil {
logger.Errorf("insert err => %v", err.Error())
return 0
}
return param.ID
}

View File

@@ -0,0 +1,84 @@
package service
import (
"fmt"
"strconv"
"be.ems/src/framework/i18n"
"be.ems/src/framework/utils/file"
"be.ems/src/modules/network_data/model"
"be.ems/src/modules/network_data/repository"
)
// 实例化数据层 NBState 结构体
var NewNBState = &NBState{
nbStateRepository: repository.NewNBState,
}
// NBState 基站状态记录表 服务层处理
type NBState struct {
nbStateRepository *repository.NBState // 基站状态记录信息
}
// SelectPage 根据条件分页查询
func (r NBState) SelectPage(query model.NBStateQuery) ([]model.NBState, int64) {
return r.nbStateRepository.SelectByPage(query)
}
// Insert 插入数据
func (r NBState) Insert(item model.NBState) int64 {
return r.nbStateRepository.Insert(item)
}
// DeleteByIds 批量删除信息
func (r NBState) DeleteByIds(ids []string) (int64, error) {
// 检查是否存在
arr := r.nbStateRepository.SelectByIds(ids)
if len(arr) <= 0 {
return 0, fmt.Errorf("not data")
}
if len(arr) == len(ids) {
rows := r.nbStateRepository.DeleteByIds(ids)
return rows, nil
}
// 删除信息失败!
return 0, fmt.Errorf("delete fail")
}
// ExportXlsx 导出数据到 xlsx 文件
func (r NBState) ExportXlsx(rows []model.NBState, fileName, language string) (string, error) {
// 第一行表头标题
headerCells := map[string]string{
"A1": i18n.TKey(language, "nbState.export.id"),
"B1": i18n.TKey(language, "nbState.export.name"),
"C1": i18n.TKey(language, "nbState.export.position"),
"D1": i18n.TKey(language, "nbState.export.address"),
"E1": i18n.TKey(language, "nbState.export.nbName"),
"F1": i18n.TKey(language, "nbState.export.state"),
"G1": i18n.TKey(language, "nbState.export.time"),
}
// 从第二行开始的数据
dataCells := make([]map[string]any, 0)
for i, row := range rows {
idx := strconv.Itoa(i + 2)
// 角色状态
statusValue := i18n.TKey(language, "dictData.offline")
if row.State == "ON" {
statusValue = i18n.TKey(language, "dictData.online")
}
dataCells = append(dataCells, map[string]any{
"A" + idx: row.ID,
"B" + idx: row.Name,
"C" + idx: row.Position,
"D" + idx: row.Address,
"E" + idx: row.NbName,
"F" + idx: statusValue,
"G" + idx: row.Time,
})
}
// 导出数据表格
return file.WriteSheet(headerCells, dataCells, fileName, "Sheet1")
}

View File

@@ -58,9 +58,9 @@ func (r CDREventIMS) ExportXlsx(rows []model.CDREventIMS, fileName, language str
"A1": "ID",
"B1": "Record Behavior",
"C1": "Type",
"D1": "Called",
"E1": "Result",
"F1": "Caller",
"D1": "Caller",
"E1": "Called",
"F1": "Result",
"G1": "Duration",
"H1": "Call Start Time",
"I1": "Hangup Time",
@@ -110,7 +110,7 @@ func (r CDREventIMS) ExportXlsx(rows []model.CDREventIMS, fileName, language str
// 时长
duration := "-"
if v, ok := cdrJSON["callDuration"]; ok && v != nil && callType != "sms" {
duration = fmt.Sprint(parse.Number(v))
duration = fmt.Sprintf("%ds", parse.Number(v))
}
// 呼叫结果 非短信都有code作为结果 sms短信都ok
callResult := "Success"
@@ -146,9 +146,9 @@ func (r CDREventIMS) ExportXlsx(rows []model.CDREventIMS, fileName, language str
"A" + idx: row.ID,
"B" + idx: recordType,
"C" + idx: callTypeLable,
"D" + idx: called,
"E" + idx: callResult,
"F" + idx: caller,
"D" + idx: caller,
"E" + idx: called,
"F" + idx: callResult,
"G" + idx: duration,
"H" + idx: seizureTimeStr,
"I" + idx: releaseTimeStr,

View File

@@ -62,7 +62,6 @@ func (r *UDMAuthUser) dataByRedis(imsi, neId string) []model.UDMAuthUser {
a := model.UDMAuthUser{
IMSI: imsi,
Amf: amf,
Status: "1", // 默认给1
Ki: m["ki"],
AlgoIndex: m["algo"],
Opc: m["opc"],
@@ -92,7 +91,6 @@ func (r *UDMAuthUser) ParseInfo(imsi, neId string, data map[string]string) model
u.Ki = data["ki"]
u.AlgoIndex = data["algo"]
u.Opc = data["opc"]
u.Status = "1"
return u
}