package service import ( "errors" "fmt" "io" "mime/multipart" "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/officialaccount" "github.com/silenceper/wechat/v2/officialaccount/material" ) type MpMaterialService struct{} // getOfficialAccount 获取微信公众号实例 func (m *MpMaterialService) getOfficialAccount() (*officialaccount.OfficialAccount, error) { // 使用账号服务获取默认公众号实例 return nil, nil } // GetMaterialList 获取素材列表 func (m *MpMaterialService) GetMaterialList(page, pageSize int, materialType string, permanent *bool) ([]model.MpMaterial, int64, error) { var materials []model.MpMaterial var total int64 db := global.GVA_DB.Model(&model.MpMaterial{}) // 根据素材类型筛选 if materialType != "" { db = db.Where("type = ?", materialType) } // 根据是否永久素材筛选 if permanent != nil { db = db.Where("permanent = ?", *permanent) } // 获取总数 err := db.Count(&total).Error if err != nil { return nil, 0, err } // 分页查询 offset := (page - 1) * pageSize err = db.Offset(offset).Limit(pageSize).Order("created_at desc").Find(&materials).Error if err != nil { return nil, 0, err } return materials, total, nil } // UploadTempMaterial 上传临时素材 func (m *MpMaterialService) UploadTempMaterial(file multipart.File, filename, materialType string) (*model.MpMaterial, error) { if file == nil { return nil, errors.New("文件不能为空") } if filename == "" { return nil, errors.New("文件名不能为空") } if materialType == "" { return nil, errors.New("素材类型不能为空") } // 获取微信公众号实例 oa, err := m.getOfficialAccount() if err != nil { return nil, err } // 转换素材类型 var mediaType material.MediaType switch materialType { case "image": mediaType = material.MediaTypeImage case "voice": mediaType = material.MediaTypeVoice case "video": mediaType = material.MediaTypeVideo case "thumb": mediaType = material.MediaTypeThumb default: return nil, fmt.Errorf("不支持的素材类型: %s", materialType) } // 调用微信API上传临时素材 materialService := oa.GetMaterial() media, err := materialService.MediaUploadFromReader(mediaType, filename, file) if err != nil { global.GVA_LOG.Error("上传临时素材失败: " + err.Error()) return nil, fmt.Errorf("上传临时素材失败: %v", err) } // 保存到数据库 mpMaterial := &model.MpMaterial{ MediaID: media.MediaID, Type: materialType, Name: &filename, Permanent: false, } err = global.GVA_DB.Create(mpMaterial).Error if err != nil { global.GVA_LOG.Error("保存临时素材记录失败: " + err.Error()) return nil, fmt.Errorf("保存素材记录失败: %v", err) } global.GVA_LOG.Info(fmt.Sprintf("成功上传临时素材,MediaID: %s", media.MediaID)) return mpMaterial, nil } // UploadPermanentMaterial 上传永久素材 func (m *MpMaterialService) UploadPermanentMaterial(file multipart.File, filename, materialType string) (*model.MpMaterial, error) { if file == nil { return nil, errors.New("文件不能为空") } if filename == "" { return nil, errors.New("文件名不能为空") } if materialType == "" { return nil, errors.New("素材类型不能为空") } // 获取微信公众号实例 oa, err := m.getOfficialAccount() if err != nil { return nil, err } materialService := oa.GetMaterial() var mediaID, url string // 根据素材类型调用不同的上传方法 switch materialType { case "image": mediaID, url, err = materialService.AddMaterialFromReader(material.MediaTypeImage, filename, file) case "voice": mediaID, url, err = materialService.AddMaterialFromReader(material.MediaTypeVoice, filename, file) case "thumb": mediaID, url, err = materialService.AddMaterialFromReader(material.MediaTypeThumb, filename, file) case "video": // 视频素材需要额外的标题和描述参数,这里使用文件名作为标题 mediaID, url, err = materialService.AddVideoFromReader(filename, filename, "视频素材", file) default: return nil, fmt.Errorf("不支持的永久素材类型: %s", materialType) } if err != nil { global.GVA_LOG.Error("上传永久素材失败: " + err.Error()) return nil, fmt.Errorf("上传永久素材失败: %v", err) } // 保存到数据库 mpMaterial := &model.MpMaterial{ MediaID: mediaID, Type: materialType, Name: &filename, Permanent: true, URL: &url, } err = global.GVA_DB.Create(mpMaterial).Error if err != nil { global.GVA_LOG.Error("保存永久素材记录失败: " + err.Error()) return nil, fmt.Errorf("保存素材记录失败: %v", err) } global.GVA_LOG.Info(fmt.Sprintf("成功上传永久素材,MediaID: %s", mediaID)) return mpMaterial, nil } // DeleteMaterial 删除素材 func (m *MpMaterialService) DeleteMaterial(id uint) error { // 先获取素材信息 var mat model.MpMaterial err := global.GVA_DB.Where("id = ?", id).First(&mat).Error if err != nil { return err } // 如果是永久素材,需要从微信服务器删除 if mat.IsPermanent() { mpUserService := &MpUserService{} oa, err := mpUserService.GetOfficialAccount() if err != nil { return err } err = oa.GetMaterial().DeleteMaterial(mat.MediaID) if err != nil { global.GVA_LOG.Error("删除微信素材失败: " + err.Error()) // 即使微信删除失败,也继续删除本地记录 } } // 删除本地记录 err = global.GVA_DB.Delete(&model.MpMaterial{}, id).Error if err != nil { global.GVA_LOG.Error("删除素材记录失败: " + err.Error()) return err } return nil } // GetMaterialByID 根据ID获取素材 func (m *MpMaterialService) GetMaterialByID(id uint) (*model.MpMaterial, error) { var mat model.MpMaterial err := global.GVA_DB.Where("id = ?", id).First(&mat).Error if err != nil { return nil, err } return &mat, nil } // GetMaterialByMediaID 根据MediaID获取素材 func (m *MpMaterialService) GetMaterialByMediaID(mediaID string) (*model.MpMaterial, error) { var mat model.MpMaterial err := global.GVA_DB.Where("media_id = ?", mediaID).First(&mat).Error if err != nil { return nil, err } return &mat, nil } // DownloadMaterial 下载素材 func (m *MpMaterialService) DownloadMaterial(mediaID string) (io.ReadCloser, string, error) { if mediaID == "" { return nil, "", errors.New("MediaID不能为空") } // 获取微信公众号实例 oa, err := m.getOfficialAccount() if err != nil { return nil, "", err } // 获取素材下载URL materialService := oa.GetMaterial() mediaURL, err := materialService.GetMediaURL(mediaID) if err != nil { global.GVA_LOG.Error("获取素材下载URL失败: " + err.Error()) return nil, "", fmt.Errorf("获取素材下载URL失败: %v", err) } global.GVA_LOG.Info(fmt.Sprintf("成功获取素材下载URL,MediaID: %s", mediaID)) // 注意:这里返回的是下载URL,实际的文件下载需要客户端自行处理 // 由于微信API返回的是URL而不是文件流,这里暂时返回错误提示 return nil, mediaURL, fmt.Errorf("请使用返回的URL进行下载: %s", mediaURL) } // SyncMaterialsFromWechat 从微信服务器同步素材列表 func (m *MpMaterialService) SyncMaterialsFromWechat() error { // 获取微信公众号实例 oa, err := m.getOfficialAccount() if err != nil { return err } materialService := oa.GetMaterial() // 获取素材总数 materialCount, err := materialService.GetMaterialCount() if err != nil { global.GVA_LOG.Error("获取素材总数失败: " + err.Error()) return fmt.Errorf("获取素材总数失败: %v", err) } global.GVA_LOG.Info(fmt.Sprintf("开始同步素材,总数统计 - 图片: %d, 语音: %d, 视频: %d, 图文: %d", materialCount.ImageCount, materialCount.VoiceCount, materialCount.VideoCount, materialCount.NewsCount)) // 同步各类型的永久素材 materialTypes := []material.PermanentMaterialType{ material.PermanentMaterialTypeImage, material.PermanentMaterialTypeVoice, material.PermanentMaterialTypeVideo, material.PermanentMaterialTypeNews, } totalSynced := 0 for _, materialType := range materialTypes { synced, err := m.syncMaterialsByType(materialService, materialType) if err != nil { global.GVA_LOG.Error(fmt.Sprintf("同步%s类型素材失败: %s", string(materialType), err.Error())) continue } totalSynced += synced } global.GVA_LOG.Info(fmt.Sprintf("素材同步完成,共同步 %d 个素材", totalSynced)) return nil } // syncMaterialsByType 按类型同步素材 func (m *MpMaterialService) syncMaterialsByType(materialService *material.Material, materialType material.PermanentMaterialType) (int, error) { const batchSize = 20 // 每次获取20个素材 offset := int64(0) synced := 0 for { // 批量获取素材 articleList, err := materialService.BatchGetMaterial(materialType, offset, batchSize) if err != nil { return synced, err } if len(articleList.Item) == 0 { break } // 处理每个素材 for _, item := range articleList.Item { // 检查是否已存在 var existingMaterial model.MpMaterial err := global.GVA_DB.Where("media_id = ?", item.MediaID).First(&existingMaterial).Error if err == nil { // 已存在,跳过 continue } // 创建新的素材记录 mpMaterial := &model.MpMaterial{ MediaID: item.MediaID, Type: string(materialType), Permanent: true, URL: &item.URL, } if item.Name != "" { mpMaterial.Name = &item.Name } err = global.GVA_DB.Create(mpMaterial).Error if err != nil { global.GVA_LOG.Error(fmt.Sprintf("保存素材记录失败: %s", err.Error())) continue } synced++ } // 如果返回的素材数量少于批次大小,说明已经获取完毕 if int64(len(articleList.Item)) < batchSize { break } offset += batchSize } return synced, nil }