480 lines
13 KiB
Go
480 lines
13 KiB
Go
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
|
||
}
|