diff --git a/src/modules/network_data/controller/ims.go b/src/modules/network_data/controller/ims.go index 48bddf15..cfa523ed 100644 --- a/src/modules/network_data/controller/ims.go +++ b/src/modules/network_data/controller/ims.go @@ -170,9 +170,11 @@ func (s *IMSController) CDRExport(c *gin.Context) { "G1": "Duration", "H1": "Result Code", "I1": "Result Cause", - "J1": "Call Start Time", - "K1": "Hangup Time", - "L1": "Tenant Name", // for multi-tenancy + "J1": "MOS Average", + "K1": "Call Connection Time", + "L1": "Call Start Time", + "M1": "Hangup Time", + "N1": "Tenant Name", // for multi-tenancy } // 读取字典数据 CDR SIP响应代码类别类型 dictCDRSipCode := sysService.NewSysDictData.SelectDictDataByType("cdr_sip_code") @@ -265,6 +267,16 @@ func (s *IMSController) CDRExport(c *gin.Context) { } // for multi-tenant, get tenant name tenantName := row.TenantName + // 通话质量 + var mosAverage int64 = 0 + if v, ok := cdrJSON["mosAverage"]; ok && v != nil && callType != "sms" { + mosAverage = parse.Number(v) + } + // 通话连接时间 + callConnectionTime := "-" + if v, ok := cdrJSON["callConnectionTime"]; ok && v != nil && callType != "sms" { + callConnectionTime = fmt.Sprintf("%ds", parse.Number(v)) + } dataCells = append(dataCells, map[string]any{ "A" + idx: row.ID, @@ -275,10 +287,12 @@ func (s *IMSController) CDRExport(c *gin.Context) { "F" + idx: called, "G" + idx: duration, "H" + idx: callResult, - "I" + idx: callCause, - "J" + idx: seizureTimeStr, - "K" + idx: releaseTimeStr, - "L" + idx: tenantName, + "I" + idx: mosAverage, + "J" + idx: callConnectionTime, + "K" + idx: callCause, + "L" + idx: seizureTimeStr, + "M" + idx: releaseTimeStr, + "N" + idx: tenantName, }) } @@ -476,3 +490,85 @@ func (s IMSController) KPIBusyWeek(c *gin.Context) { data := s.kpiReportService.IMSBusyWeek(neInfo.RmUID, query.WeekStart, query.WeekEnd) c.JSON(200, resp.OkData(data)) } + +// CDR MOS +// +// GET /cdr/mos-hour +// +// @Tags network_data/ims +// @Accept json +// @Produce json +// @Param neId query string true "NE ID" default(001) +// @Param timestamp query int64 false "timestamp" +// @Success 200 {object} object "Response Results" +// @Security TokenAuth +// @Summary MOS hour statistics +// @Description MOS hour statistics +// @Router /neData/ims/cdr/mos-hour [get] +func (s IMSController) CDRMOSHour(c *gin.Context) { + language := reqctx.AcceptLanguage(c) + var query struct { + NeID string `form:"neId" binding:"required"` + Timestamp int64 `form:"timestamp" binding:"required"` // 时间戳毫秒 年月日返回每小时的总和 年月日时返回该小时的总和 + } + if err := c.ShouldBindQuery(&query); err != nil { + errMsgs := fmt.Sprintf("bind err: %s", resp.FormatBindError(err)) + c.JSON(422, resp.CodeMsg(resp.CODE_PARAM_PARSER, errMsgs)) + return + } + if query.Timestamp < 1e12 || query.Timestamp > 1e13 { + c.JSON(422, resp.CodeMsg(resp.CODE_PARAM_CHEACK, "timestamp format is ms")) + return + } + + // 查询网元获取IP + neInfo := s.neInfoService.FindByNeTypeAndNeID("IMS", query.NeID) + if neInfo.NeId != query.NeID || neInfo.IP == "" { + c.JSON(200, resp.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) + return + } + + data := s.cdrEventService.CDRMOSHour(neInfo.RmUID, query.Timestamp) + c.JSON(200, resp.OkData(data)) +} + +// CDR Call Connection Time +// +// GET /cdr/cct-hour +// +// @Tags network_data/ims +// @Accept json +// @Produce json +// @Param neId query string true "NE ID" default(001) +// @Param timestamp query int64 false "timestamp" +// @Success 200 {object} object "Response Results" +// @Security TokenAuth +// @Summary MOS hour statistics +// @Description MOS hour statistics +// @Router /neData/ims/cdr/cct-hour [get] +func (s IMSController) CDRCCTHour(c *gin.Context) { + language := reqctx.AcceptLanguage(c) + var query struct { + NeID string `form:"neId" binding:"required"` + Timestamp int64 `form:"timestamp" binding:"required"` // 时间戳毫秒 年月日返回每小时的总和 年月日时返回该小时的总和 + } + if err := c.ShouldBindQuery(&query); err != nil { + errMsgs := fmt.Sprintf("bind err: %s", resp.FormatBindError(err)) + c.JSON(422, resp.CodeMsg(resp.CODE_PARAM_PARSER, errMsgs)) + return + } + if query.Timestamp < 1e12 || query.Timestamp > 1e13 { + c.JSON(422, resp.CodeMsg(resp.CODE_PARAM_CHEACK, "timestamp format is ms")) + return + } + + // 查询网元获取IP + neInfo := s.neInfoService.FindByNeTypeAndNeID("IMS", query.NeID) + if neInfo.NeId != query.NeID || neInfo.IP == "" { + c.JSON(200, resp.ErrMsg(i18n.TKey(language, "app.common.noNEInfo"))) + return + } + + data := s.cdrEventService.CDRCCTHour(neInfo.RmUID, query.Timestamp) + c.JSON(200, resp.OkData(data)) +} diff --git a/src/modules/network_data/network_data.go b/src/modules/network_data/network_data.go index d38329aa..9bbe784e 100644 --- a/src/modules/network_data/network_data.go +++ b/src/modules/network_data/network_data.go @@ -113,6 +113,14 @@ func Setup(router *gin.Engine) { middleware.PreAuthorize(nil), controller.NewIMS.KPIBusyWeek, ) + imsGroup.GET("/cdr/mos-hour", + // middleware.PreAuthorize(nil), + controller.NewIMS.CDRMOSHour, + ) + imsGroup.GET("/cdr/cct-hour", + // middleware.PreAuthorize(nil), + controller.NewIMS.CDRCCTHour, + ) } // 网元SMSC diff --git a/src/modules/network_data/repository/cdr_event_ims.go b/src/modules/network_data/repository/cdr_event_ims.go index a5d74fd0..2762d1b6 100644 --- a/src/modules/network_data/repository/cdr_event_ims.go +++ b/src/modules/network_data/repository/cdr_event_ims.go @@ -354,3 +354,28 @@ func (r *CDREventIMS) DeleteByIds(ids []string) int64 { } return tx.RowsAffected } + +// SelectF 查询 +func (r *CDREventIMS) Select(querys model.CDREventIMSQuery) []model.CDREventIMS { + tx := datasource.DB("").Model(&model.CDREventIMS{}) + // 查询条件拼接 + if querys.RmUID != "" { + tx = tx.Where("rm_uid = ?", querys.RmUID) + } + if querys.StartTime > 0 { + tx = tx.Where("timestamp >= ?", querys.StartTime) + } + if querys.EndTime > 0 { + tx = tx.Where("timestamp <= ?", querys.EndTime) + } + if querys.RecordType != "" { + tx = tx.Where("JSON_EXTRACT(cdr_json, '$.recordType') = ?", querys.RecordType) + } + rows := []model.CDREventIMS{} + err := tx.Find(&rows).Error + if err != nil { + logger.Errorf("query find err => %v", err.Error()) + return rows + } + return rows +} diff --git a/src/modules/network_data/service/cdr_event_ims.go b/src/modules/network_data/service/cdr_event_ims.go index 242aa9fb..aa3e924b 100644 --- a/src/modules/network_data/service/cdr_event_ims.go +++ b/src/modules/network_data/service/cdr_event_ims.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "strconv" + "time" "be.ems/src/framework/i18n" "be.ems/src/framework/logger" @@ -63,8 +64,10 @@ func (r CDREventIMS) ExportXlsx(rows []model.CDREventIMS, fileName, language str "F1": "Duration", "G1": "Result Code", "H1": "Result Cause", - "I1": "Call Start Time", - "J1": "Hangup Time", + "I1": "MOS Average", + "J1": "Call Connection Time", + "K1": "Call Start Time", + "L1": "Hangup Time", } // 读取字典数据 CDR SIP响应代码类别类型 dictCDRSipCode := sysService.NewSysDictData.SelectDictDataByType("cdr_sip_code") @@ -156,6 +159,16 @@ func (r CDREventIMS) ExportXlsx(rows []model.CDREventIMS, fileName, language str releaseTimeStr = v.(string) } } + // 通话质量 + var mosAverage int64 = 0 + if v, ok := cdrJSON["mosAverage"]; ok && v != nil && callType != "sms" { + mosAverage = parse.Number(v) + } + // 通话连接时间 + callConnectionTime := "-" + if v, ok := cdrJSON["callConnectionTime"]; ok && v != nil && callType != "sms" { + callConnectionTime = fmt.Sprintf("%ds", parse.Number(v)) + } dataCells = append(dataCells, map[string]any{ "A" + idx: row.ID, @@ -166,11 +179,157 @@ func (r CDREventIMS) ExportXlsx(rows []model.CDREventIMS, fileName, language str "F" + idx: duration, "G" + idx: callResult, "H" + idx: callCause, - "I" + idx: seizureTimeStr, - "J" + idx: releaseTimeStr, + "I" + idx: mosAverage, + "J" + idx: callConnectionTime, + "K" + idx: seizureTimeStr, + "L" + idx: releaseTimeStr, }) } // 导出数据表格 return file.WriteSheet(headerCells, dataCells, fileName, "") } + +// CDRMOSHour CDR MOS 统计 +func (r CDREventIMS) CDRMOSHour(rmUID string, timestamp int64) []map[string]any { + t := time.UnixMilli(timestamp) + beginTime := t + endTime := t + // 检查时分秒是否都为零 + if t.Hour() == 0 && t.Minute() == 0 && t.Second() == 0 { + // 获取当天起始时间(00:00:00) + beginTime = t.Truncate(time.Hour) + // 计算当天结束时间(23:59:59) + endTime = beginTime.Add(23*time.Hour + 59*time.Minute + 59*time.Second) + } else { + // 起始时间:当前小时的 00 分 00 秒 + beginTime = t.Truncate(time.Hour) + // 结束时间:当前小时的 59 分 59 秒 999 毫秒 + endTime = beginTime.Add(time.Hour - time.Millisecond) + } + + querys := model.CDREventIMSQuery{ + RmUID: rmUID, + RecordType: "MOC", + StartTime: beginTime.Unix(), + EndTime: endTime.Unix(), + } + rows := r.cdrEventIMSRepository.Select(querys) + // 创建一个map来存储按时间段合并后的数据 + timeGroup := make(map[int64]map[string]float64) + // 遍历每个数据项 + for _, row := range rows { + // 将毫秒时间戳转换为小时级时间戳(保留到小时的起始毫秒) + timeHour := row.Timestamp / 3600 * 3600 // 1小时 = 3600000毫秒 + + // 解析 JSON 字符串为 map + var cdrJSON map[string]interface{} + err := json.Unmarshal([]byte(row.CDRJSONStr), &cdrJSON) + if err != nil { + logger.Warnf("Unmarshal JSON: %s", err.Error()) + continue + } + + // 记录类型 + var mosAverage float64 = 0 + if v, ok := cdrJSON["mosAverage"]; ok && v != nil { + mosAverage = v.(float64) + } else { + continue + } + + // 合并到对应的小时段 + if _, exists := timeGroup[timeHour]; !exists { + timeGroup[timeHour] = map[string]float64{ + "total": 0, + "mosSum": 0, + } + } + timeGroup[timeHour]["total"] += 1 + timeGroup[timeHour]["mosSum"] += mosAverage + } + + // 时间组合输出 + data := make([]map[string]any, 0, len(timeGroup)) + for hour, sums := range timeGroup { + data = append(data, map[string]any{ + "timeGroup": fmt.Sprintf("%d", hour), + "total": sums["total"], + "mosSum": sums["mosSum"], + "mosAvg": sums["mosAvg"] / sums["total"], + }) + } + return data +} + +// CDRCCTHour CDR CCT 统计 +func (r CDREventIMS) CDRCCTHour(rmUID string, timestamp int64) []map[string]any { + t := time.UnixMilli(timestamp) + beginTime := t + endTime := t + // 检查时分秒是否都为零 + if t.Hour() == 0 && t.Minute() == 0 && t.Second() == 0 { + // 获取当天起始时间(00:00:00) + beginTime = t.Truncate(time.Hour) + // 计算当天结束时间(23:59:59) + endTime = beginTime.Add(23*time.Hour + 59*time.Minute + 59*time.Second) + } else { + // 起始时间:当前小时的 00 分 00 秒 + beginTime = t.Truncate(time.Hour) + // 结束时间:当前小时的 59 分 59 秒 999 毫秒 + endTime = beginTime.Add(time.Hour - time.Millisecond) + } + + querys := model.CDREventIMSQuery{ + RmUID: rmUID, + RecordType: "MOC", + StartTime: beginTime.Unix(), + EndTime: endTime.Unix(), + } + rows := r.cdrEventIMSRepository.Select(querys) + // 创建一个map来存储按时间段合并后的数据 + timeGroup := make(map[int64]map[string]float64) + // 遍历每个数据项 + for _, row := range rows { + // 将毫秒时间戳转换为小时级时间戳(保留到小时的起始毫秒) + timeHour := row.Timestamp / 3600 * 3600 // 1小时 = 3600000毫秒 + + // 解析 JSON 字符串为 map + var cdrJSON map[string]interface{} + err := json.Unmarshal([]byte(row.CDRJSONStr), &cdrJSON) + if err != nil { + logger.Warnf("Unmarshal JSON: %s", err.Error()) + continue + } + + // 记录类型 + var callConnectionTime float64 = 0 + if v, ok := cdrJSON["callConnectionTime"]; ok && v != nil { + callConnectionTime = v.(float64) + } else { + continue + } + + // 合并到对应的小时段 + if _, exists := timeGroup[timeHour]; !exists { + timeGroup[timeHour] = map[string]float64{ + "total": 0, + "cctSum": 0, + } + } + timeGroup[timeHour]["total"] += 1 + timeGroup[timeHour]["cctSum"] += callConnectionTime + } + + // 时间组合输出 + data := make([]map[string]any, 0, len(timeGroup)) + for hour, sums := range timeGroup { + data = append(data, map[string]any{ + "timeGroup": fmt.Sprintf("%d", hour), + "total": sums["total"], + "cctSum": sums["cctSum"], + "cctAvg": sums["cctSum"] / sums["total"], + }) + } + return data +}