From c07d43c779dbec45d0f36a9d331b5b1d0102ee0e Mon Sep 17 00:00:00 2001 From: yvan <8574526@qq.com> Date: Tue, 5 Aug 2025 20:07:37 +0800 Subject: [PATCH] wechat --- .../service/mp_statistics_service.go | 479 ++++++++++++++++++ 1 file changed, 479 insertions(+) create mode 100644 server/plugin/wechat-integration/service/mp_statistics_service.go diff --git a/server/plugin/wechat-integration/service/mp_statistics_service.go b/server/plugin/wechat-integration/service/mp_statistics_service.go new file mode 100644 index 00000000..ca8dfe9d --- /dev/null +++ b/server/plugin/wechat-integration/service/mp_statistics_service.go @@ -0,0 +1,479 @@ +package service + +import ( + "errors" + "fmt" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model" + "github.com/silenceper/wechat/v2" + "github.com/silenceper/wechat/v2/cache" + "github.com/silenceper/wechat/v2/officialaccount" + "github.com/silenceper/wechat/v2/officialaccount/config" + "go.uber.org/zap" +) + +type MpStatisticsService struct{} + +// GetOfficialAccount 获取微信公众号实例(从数据库获取配置) +func (m *MpStatisticsService) GetOfficialAccount() (*officialaccount.OfficialAccount, error) { + // 从数据库获取微信公众号配置 + var mpConfig model.MpConfig + err := global.GVA_DB.Where("config_type = ?", model.ConfigTypeMP).First(&mpConfig).Error + if err != nil { + return nil, fmt.Errorf("获取微信公众号配置失败: %v", err) + } + + if mpConfig.AppID == "" || mpConfig.AppSecret == "" { + return nil, errors.New("微信公众号配置不完整") + } + + wc := wechat.NewWechat() + memory := cache.NewMemory() + + cfg := &config.Config{ + AppID: mpConfig.AppID, + AppSecret: mpConfig.AppSecret, + Cache: memory, + } + + // 如果有Token,设置服务器配置 + if mpConfig.Token != nil && *mpConfig.Token != "" { + cfg.Token = *mpConfig.Token + } + + return wc.GetOfficialAccount(cfg), nil +} + +// formatDate 格式化日期为微信API要求的格式 (YYYY-MM-DD) +func (m *MpStatisticsService) formatDate(date string) (string, error) { + // 尝试解析不同格式的日期 + layouts := []string{ + "2006-01-02", + "2006/01/02", + "20060102", + } + + var t time.Time + var err error + + for _, layout := range layouts { + t, err = time.Parse(layout, date) + if err == nil { + break + } + } + + if err != nil { + return "", fmt.Errorf("无效的日期格式: %s", date) + } + + return t.Format("2006-01-02"), nil +} + +// validateDateRange 验证日期范围 +func (m *MpStatisticsService) validateDateRange(beginDate, endDate string, maxDays int) error { + // 格式化日期 + formattedBeginDate, err := m.formatDate(beginDate) + if err != nil { + return fmt.Errorf("开始日期格式错误: %v", err) + } + + formattedEndDate, err := m.formatDate(endDate) + if err != nil { + return fmt.Errorf("结束日期格式错误: %v", err) + } + + // 解析日期 + beginTime, err := time.Parse("2006-01-02", formattedBeginDate) + if err != nil { + return fmt.Errorf("解析开始日期失败: %v", err) + } + + endTime, err := time.Parse("2006-01-02", formattedEndDate) + if err != nil { + return fmt.Errorf("解析结束日期失败: %v", err) + } + + // 检查日期顺序 + if endTime.Before(beginTime) { + return errors.New("结束日期不能早于开始日期") + } + + // 检查日期范围 + daysDiff := int(endTime.Sub(beginTime).Hours()/24) + 1 + if daysDiff > maxDays { + return fmt.Errorf("日期范围不能超过%d天,当前范围:%d天", maxDays, daysDiff) + } + + return nil +} + +// validateUserStatisticsDateRange 验证用户统计数据的日期范围(特殊限制:不能包括今天) +func (m *MpStatisticsService) validateUserStatisticsDateRange(beginDate, endDate string, maxDays int) error { + // 先进行基础日期范围验证 + if err := m.validateDateRange(beginDate, endDate, maxDays); err != nil { + return err + } + + // 格式化日期 + formattedEndDate, err := m.formatDate(endDate) + if err != nil { + return fmt.Errorf("结束日期格式错误: %v", err) + } + + // 解析结束日期 + endTime, err := time.Parse("2006-01-02", formattedEndDate) + if err != nil { + return fmt.Errorf("解析结束日期失败: %v", err) + } + + // 检查是否包含今天 + today := time.Now().Format("2006-01-02") + todayTime, _ := time.Parse("2006-01-02", today) + + if !endTime.Before(todayTime) { + return errors.New("微信统计API限制:查询日期不能包括今天,最晚只能查询到昨天") + } + + return nil +} + +// GetUserSummary 获取用户增减数据 +func (m *MpStatisticsService) GetUserSummary(beginDate, endDate string) ([]map[string]interface{}, error) { + // 验证日期范围(最长7天,且不能包括今天) + if err := m.validateUserStatisticsDateRange(beginDate, endDate, 7); err != nil { + return nil, err + } + + // 格式化日期 + formattedBeginDate, err := m.formatDate(beginDate) + if err != nil { + return nil, err + } + + formattedEndDate, err := m.formatDate(endDate) + if err != nil { + return nil, err + } + + // 获取微信公众号实例 + oa, err := m.GetOfficialAccount() + if err != nil { + return nil, err + } + + // 使用 silenceper/wechat 库的 DataCube 方法 + dataCube := oa.GetDataCube() + response, err := dataCube.GetUserSummary(formattedBeginDate, formattedEndDate) + if err != nil { + global.GVA_LOG.Error("调用微信用户增减数据API失败", zap.Error(err)) + return nil, fmt.Errorf("获取用户增减数据失败: %v", err) + } + + // 转换为前端需要的格式 + result := make([]map[string]interface{}, 0, len(response.List)) + for _, item := range response.List { + result = append(result, map[string]interface{}{ + "refDate": item.RefDate, + "userSource": item.UserSource, + "newUser": item.NewUser, + "cancelUser": item.CancelUser, + // 注意:用户增减数据中没有累计用户数字段,这个字段在用户累计数据中 + }) + } + + global.GVA_LOG.Info("获取用户增减数据成功", + zap.String("beginDate", formattedBeginDate), + zap.String("endDate", formattedEndDate), + zap.Int("count", len(result))) + + return result, nil +} + +// GetUserCumulate 获取用户累计数据 +func (m *MpStatisticsService) GetUserCumulate(beginDate, endDate string) ([]map[string]interface{}, error) { + // 验证日期范围(最长7天) + if err := m.validateDateRange(beginDate, endDate, 7); err != nil { + return nil, err + } + + // 格式化日期 + formattedBeginDate, err := m.formatDate(beginDate) + if err != nil { + return nil, err + } + + formattedEndDate, err := m.formatDate(endDate) + if err != nil { + return nil, err + } + + // 获取微信公众号实例 + oa, err := m.GetOfficialAccount() + if err != nil { + return nil, err + } + + // 使用 silenceper/wechat 库的 DataCube 方法 + dataCube := oa.GetDataCube() + response, err := dataCube.GetUserAccumulate(formattedBeginDate, formattedEndDate) + if err != nil { + global.GVA_LOG.Error("调用微信用户累计数据API失败", zap.Error(err)) + return nil, fmt.Errorf("获取用户累计数据失败: %v", err) + } + + // 转换为前端需要的格式 + result := make([]map[string]interface{}, 0, len(response.List)) + for _, item := range response.List { + result = append(result, map[string]interface{}{ + "refDate": item.RefDate, + "cumulateUser": item.CumulateUser, + }) + } + + global.GVA_LOG.Info("获取用户累计数据成功", + zap.String("beginDate", formattedBeginDate), + zap.String("endDate", formattedEndDate), + zap.Int("count", len(result))) + + return result, nil +} + +// GetUpstreamMessage 获取消息发送概况数据 +func (m *MpStatisticsService) GetUpstreamMessage(beginDate, endDate string) ([]map[string]interface{}, error) { + // 验证日期范围(最长7天,且不能包括今天) + if err := m.validateUserStatisticsDateRange(beginDate, endDate, 7); err != nil { + return nil, err + } + + // 格式化日期 + formattedBeginDate, err := m.formatDate(beginDate) + if err != nil { + return nil, err + } + + formattedEndDate, err := m.formatDate(endDate) + if err != nil { + return nil, err + } + + // 获取微信公众号实例 + oa, err := m.GetOfficialAccount() + if err != nil { + return nil, err + } + + // 使用 silenceper/wechat 库的 DataCube 方法 + dataCube := oa.GetDataCube() + response, err := dataCube.GetUpstreamMsg(formattedBeginDate, formattedEndDate) + if err != nil { + global.GVA_LOG.Error("调用微信消息发送概况API失败", zap.Error(err)) + return nil, fmt.Errorf("获取消息发送概况数据失败: %v", err) + } + + // 转换为前端需要的格式 + result := make([]map[string]interface{}, 0, len(response.List)) + for _, item := range response.List { + result = append(result, map[string]interface{}{ + "refDate": item.RefDate, + "msgType": item.MsgType, + "msgUser": item.MsgUser, + "msgCount": item.MsgCount, + }) + } + + global.GVA_LOG.Info("获取消息发送概况数据成功", + zap.String("beginDate", formattedBeginDate), + zap.String("endDate", formattedEndDate), + zap.Int("count", len(result))) + + return result, nil +} + +// GetInterfaceSummary 获取接口调用概况数据 +func (m *MpStatisticsService) GetInterfaceSummary(beginDate, endDate string) ([]map[string]interface{}, error) { + // 验证日期范围(最长30天) + if err := m.validateDateRange(beginDate, endDate, 30); err != nil { + return nil, err + } + + // 格式化日期 + formattedBeginDate, err := m.formatDate(beginDate) + if err != nil { + return nil, err + } + + formattedEndDate, err := m.formatDate(endDate) + if err != nil { + return nil, err + } + + // 获取微信公众号实例 + oa, err := m.GetOfficialAccount() + if err != nil { + return nil, err + } + + // 使用 silenceper/wechat 库的 DataCube 方法 + dataCube := oa.GetDataCube() + response, err := dataCube.GetInterfaceSummary(formattedBeginDate, formattedEndDate) + if err != nil { + global.GVA_LOG.Error("调用微信接口调用概况API失败", zap.Error(err)) + return nil, fmt.Errorf("获取接口调用概况数据失败: %v", err) + } + + // 转换为前端需要的格式 + result := make([]map[string]interface{}, 0, len(response.List)) + for _, item := range response.List { + result = append(result, map[string]interface{}{ + "refDate": item.RefDate, + "callbackCount": item.CallbackCount, + "failCount": item.FailCount, + "totalTimeCost": item.TotalTimeCost, + "maxTimeCost": item.MaxTimeCost, + }) + } + + global.GVA_LOG.Info("获取接口调用概况数据成功", + zap.String("beginDate", formattedBeginDate), + zap.String("endDate", formattedEndDate), + zap.Int("count", len(result))) + + return result, nil +} + +// GetMessageTypeDistribution 获取消息类型分布数据 +func (m *MpStatisticsService) GetMessageTypeDistribution(beginDate, endDate string) ([]map[string]interface{}, error) { + // 验证日期范围(最长7天,且不能包括今天) + if err := m.validateUserStatisticsDateRange(beginDate, endDate, 7); err != nil { + return nil, err + } + + // 格式化日期 + formattedBeginDate, err := m.formatDate(beginDate) + if err != nil { + return nil, err + } + + formattedEndDate, err := m.formatDate(endDate) + if err != nil { + return nil, err + } + + // 获取微信公众号实例 + oa, err := m.GetOfficialAccount() + if err != nil { + return nil, err + } + + // 使用 silenceper/wechat 库的 GetUpstreamMsg 方法获取消息发送概况数据 + dataCube := oa.GetDataCube() + response, err := dataCube.GetUpstreamMsg(formattedBeginDate, formattedEndDate) + if err != nil { + global.GVA_LOG.Error("调用微信消息发送概况API失败", zap.Error(err)) + return nil, fmt.Errorf("获取消息类型分布数据失败: %v", err) + } + + // 消息类型映射 + msgTypeMap := map[int]string{ + 1: "文本消息", + 2: "图片消息", + 3: "语音消息", + 4: "视频消息", + 6: "链接消息", + } + + // 统计各消息类型的总数 + msgTypeStats := make(map[int]int) + totalCount := 0 + + for _, item := range response.List { + msgTypeStats[item.MsgType] += item.MsgCount + totalCount += item.MsgCount + } + + // 转换为前端需要的格式 + result := make([]map[string]interface{}, 0) + + for msgType, count := range msgTypeStats { + msgTypeName := msgTypeMap[msgType] + if msgTypeName == "" { + msgTypeName = fmt.Sprintf("其他消息(%d)", msgType) + } + + // 计算百分比 + percentage := float64(count) / float64(totalCount) * 100 + + result = append(result, map[string]interface{}{ + "name": msgTypeName, + "value": count, + "percentage": fmt.Sprintf("%.1f%%", percentage), + "type": msgType, + }) + } + + global.GVA_LOG.Info("获取消息类型分布数据成功", + zap.String("beginDate", formattedBeginDate), + zap.String("endDate", formattedEndDate), + zap.Int("totalCount", totalCount), + zap.Int("typeCount", len(result))) + + return result, nil +} + +// GetUserRegionData 获取用户地区分布数据 +// 注意:微信官方API暂不提供地区分布接口,这里使用模拟数据 +// 实际项目中可以通过用户标签、自定义字段或第三方数据分析服务获取 +func (m *MpStatisticsService) GetUserRegionData() ([]map[string]interface{}, error) { + // 模拟地区分布数据 + // 实际项目中可以: + // 1. 通过用户标签系统获取地区信息 + // 2. 通过用户自定义字段存储地区 + // 3. 通过第三方数据分析服务 + // 4. 通过IP地址解析地区(需要额外的IP库) + + regionData := []map[string]interface{}{ + { + "region": "广东省", + "count": 320, + "percentage": 23.7, + }, + { + "region": "北京市", + "count": 280, + "percentage": 20.7, + }, + { + "region": "上海市", + "count": 245, + "percentage": 18.1, + }, + { + "region": "江苏省", + "count": 180, + "percentage": 13.3, + }, + { + "region": "浙江省", + "count": 150, + "percentage": 11.1, + }, + { + "region": "山东省", + "count": 95, + "percentage": 7.0, + }, + { + "region": "其他地区", + "count": 80, + "percentage": 5.9, + }, + } + + global.GVA_LOG.Info("获取用户地区分布数据成功(模拟数据)", + zap.Int("count", len(regionData))) + + return regionData, nil +}