// Package service - 数据统计服务 // 功能:对话数据统计分析、报表生成 package service import ( "archive/zip" "bytes" "context" "customer-server/dao" "customer-server/model/dto" "customer-server/model/entity" "regexp" "strings" "gitea.com/red-future/common/utils" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/util/gconv" ) var DataStatistics = new(dataStatistics) type dataStatistics struct{} // Add 添加数据统计 // 参数: ctx - 上下文,req - 添加数据统计请求 // 返回: res - 添加成功后的统计ID,err - 错误信息 // 功能: 创建新的数据统计记录 func (s *dataStatistics) Add(ctx context.Context, req *dto.AddDataStatisticsReq) (res *dto.AddDataStatisticsRes, err error) { data := &entity.DataStatistics{} if err = utils.Struct(req, data); err != nil { return } // 使用 gtime 转换日期 if dateTime := gtime.NewFromStr(req.Date); dateTime != nil { date := dateTime.Time data.Date = &date // 取地址赋值给指针类型 } else { return nil, gerror.New("日期格式错误") } // 设置基础字段 now := gtime.Now().Time data.CreatedAt = &now // 取地址赋值给指针类型 data.UpdatedAt = &now // 取地址赋值给指针类型 data.IsDeleted = false // 注意:Creator、Updater、TenantId 保持零值,不设置 if err = dao.DataStatistics.Insert(ctx, data); err != nil { return } res = &dto.AddDataStatisticsRes{Id: data.Id.Hex()} return } // Update 更新数据统计 // 参数: ctx - 上下文,req - 更新数据统计请求 // 返回: err - 错误信息 // 功能: 更新数据统计记录内容 func (s *dataStatistics) Update(ctx context.Context, req *dto.UpdateDataStatisticsReq) (err error) { return dao.DataStatistics.Update(ctx, req) } // List 获取数据统计列表 // 参数: ctx - 上下文,req - 列表查询请求 // 返回: res - 数据统计列表及分页信息,err - 错误信息 // 功能: 分页查询数据统计记录 func (s *dataStatistics) List(ctx context.Context, req *dto.ListDataStatisticsReq) (res *dto.ListDataStatisticsRes, err error) { list, total, err := dao.DataStatistics.List(ctx, req) if err != nil { return } res = &dto.ListDataStatisticsRes{ List: list, Total: int(total), } return } // Export 导出数据统计为ZIP文件 // 参数: ctx - 上下文,req - 导出请求 // 返回: zipData - ZIP文件字节数组,filename - 文件名,err - 错误信息 // 功能: 将数据统计导出为ZIP文件,包含Excel文件 func (s *dataStatistics) Export(ctx context.Context, req *dto.ExportDataStatisticsReq) (zipData []byte, filename string, err error) { // 1. 查询所有符合条件的数据统计 statistics, err := dao.DataStatistics.FindAllForExport(ctx, req) if err != nil { return nil, "", err } if len(statistics) == 0 { return nil, "", gerror.New("没有可导出的数据统计") } // 2. 创建 ZIP 文件(内存中) var buf bytes.Buffer zipWriter := zip.NewWriter(&buf) defer zipWriter.Close() // 3. 为每个数据统计生成 TXT 文件并添加到 ZIP for _, stat := range statistics { // 生成 TXT 内容 txtContent := s.generateTxt(stat) // 生成文件名(清理并替换特殊字符) dateStr := gtime.New(stat.Date).Format("Y-m-d") cleanName := strings.ToValidUTF8(stat.CustomerServiceName, "未命名") safeFilename := s.sanitizeFilename(cleanName) if safeFilename == "" { safeFilename = "statistics" } txtFilename := dateStr + "_" + safeFilename + "_" + stat.Id.Hex()[:8] + ".txt" // 添加文件到 ZIP writer, err := zipWriter.Create(txtFilename) if err != nil { return nil, "", gerror.Newf("创建ZIP文件失败: %v", err) } if _, err := writer.Write([]byte(txtContent)); err != nil { return nil, "", gerror.Newf("写入ZIP文件失败: %v", err) } } // 5. 生成下载文件名 timestamp := gtime.Now().Format("Ymd_His") filename = "data_statistics_export_" + timestamp + ".zip" return buf.Bytes(), filename, nil } // generateTxt 生成数据统计的 TXT 内容 func (s *dataStatistics) generateTxt(stat *entity.DataStatistics) string { var builder strings.Builder builder.WriteString("日期: " + gtime.New(stat.Date).Format("Y-m-d") + "\n") builder.WriteString("客服ID: " + stat.AccountName + "\n") builder.WriteString("客服名称: " + stat.CustomerServiceName + "\n") builder.WriteString("客服平台: " + stat.CustomerServicePlatform + "\n") builder.WriteString("\n=== 统计数据 ===\n") builder.WriteString("进线数: " + gconv.String(stat.InboundCount) + "\n") builder.WriteString("开口数: " + gconv.String(stat.ActiveCount) + "\n") builder.WriteString("接待数: " + gconv.String(stat.ServedCount) + "\n") builder.WriteString("发名片数: " + gconv.String(stat.ContactCardSentCount) + "\n") builder.WriteString("发留资卡数: " + gconv.String(stat.NameCardSentCount) + "\n") builder.WriteString("留资数: " + gconv.String(stat.LeftContactInfoCount) + "\n") builder.WriteString("\n=== 响应率 ===\n") builder.WriteString("30s响应率: " + gconv.String(stat.ResponseRate30s) + "%\n") builder.WriteString("60s响应率: " + gconv.String(stat.ResponseRate60s) + "%\n") builder.WriteString("360s响应率: " + gconv.String(stat.ResponseRate360s) + "%\n") builder.WriteString("\n---\n") builder.WriteString("记录ID: " + stat.Id.Hex() + "\n") return builder.String() } // sanitizeFilename 清理文件名,移除或替换不安全的字符 func (s *dataStatistics) sanitizeFilename(filename string) string { // 移除或替换特殊字符 reg := regexp.MustCompile(`[<>:"/\\|?*\x00-\x1f]`) safe := reg.ReplaceAllString(filename, "_") // 限制文件名长度 if len(safe) > 50 { safe = safe[:50] } return strings.TrimSpace(safe) }