This commit is contained in:
2025-08-05 17:46:15 +08:00
parent e43d78ddf0
commit 4409bea9fc
106 changed files with 18564 additions and 7 deletions
@@ -0,0 +1,355 @@
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
}