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,15 @@
package api
// ApiGroup 微信集成API组
type ApiGroup struct {
WechatMiniApi WechatMiniApi // 微信小程序API
MpUserApi MpUserApi // 公众号用户API
MpMessageApi MpMessageApi // 公众号消息API
MpAutoReplyApi MpAutoReplyApi // 公众号自动回复API
MpMenuApi MpMenuApi // 公众号菜单API
MpMaterialApi MpMaterialApi // 公众号素材API
WechatWebhookApi WechatWebhookApi // 微信Webhook API
}
// ApiGroupApp API组实例
var ApiGroupApp = new(ApiGroup)
@@ -0,0 +1,132 @@
package api
import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/service"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type MiniStatisticsApi struct{}
var miniStatisticsService = service.ServiceGroupApp.MiniStatisticsService
// GetMiniStatistics 获取小程序基础统计数据
// @Tags MiniStatistics
// @Summary 获取小程序基础统计数据
// @Description 获取小程序基础统计数据
// @Accept json
// @Produce json
// @Success 200 {object} response.Response{data=response.MiniStatisticsResponse} "获取成功"
// @Router /wechat/mini/statistics [get]
func (m *MiniStatisticsApi) GetMiniStatistics(c *gin.Context) {
statistics, err := miniStatisticsService.GetMiniStatistics()
if err != nil {
global.GVA_LOG.Error("获取小程序基础统计数据失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithData(statistics, c)
}
// GetVisitTrendData 获取访问趋势数据
// @Tags MiniStatistics
// @Summary 获取访问趋势数据
// @Description 获取小程序访问趋势数据
// @Accept json
// @Produce json
// @Param startDate query string false "开始日期"
// @Param endDate query string false "结束日期"
// @Success 200 {object} response.Response{data=[]response.VisitTrendData} "获取成功"
// @Router /wechat/mini/statistics/visit-trend [get]
func (m *MiniStatisticsApi) GetVisitTrendData(c *gin.Context) {
startDate := c.Query("startDate")
endDate := c.Query("endDate")
trendData, err := miniStatisticsService.GetVisitTrendData(startDate, endDate)
if err != nil {
global.GVA_LOG.Error("获取访问趋势数据失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithData(trendData, c)
}
// GetSourceData 获取用户来源分布数据
// @Tags MiniStatistics
// @Summary 获取用户来源分布数据
// @Description 获取小程序用户来源分布数据
// @Accept json
// @Produce json
// @Success 200 {object} response.Response{data=[]response.SourceData} "获取成功"
// @Router /wechat/mini/statistics/source [get]
func (m *MiniStatisticsApi) GetSourceData(c *gin.Context) {
sourceData, err := miniStatisticsService.GetSourceData()
if err != nil {
global.GVA_LOG.Error("获取用户来源分布数据失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithData(sourceData, c)
}
// GetPageData 获取页面访问数据
// @Tags MiniStatistics
// @Summary 获取页面访问数据
// @Description 获取小程序页面访问数据
// @Accept json
// @Produce json
// @Success 200 {object} response.Response{data=[]response.PageData} "获取成功"
// @Router /wechat/mini/statistics/page [get]
func (m *MiniStatisticsApi) GetPageData(c *gin.Context) {
pageData, err := miniStatisticsService.GetPageData()
if err != nil {
global.GVA_LOG.Error("获取页面访问数据失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithData(pageData, c)
}
// GetRetentionData 获取用户留存数据
// @Tags MiniStatistics
// @Summary 获取用户留存数据
// @Description 获取小程序用户留存数据
// @Accept json
// @Produce json
// @Success 200 {object} response.Response{data=[]response.RetentionData} "获取成功"
// @Router /wechat/mini/statistics/retention [get]
func (m *MiniStatisticsApi) GetRetentionData(c *gin.Context) {
retentionData, err := miniStatisticsService.GetRetentionData()
if err != nil {
global.GVA_LOG.Error("获取用户留存数据失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithData(retentionData, c)
}
// GetDeviceData 获取设备分布数据
// @Tags MiniStatistics
// @Summary 获取设备分布数据
// @Description 获取小程序设备分布数据
// @Accept json
// @Produce json
// @Success 200 {object} response.Response{data=[]response.DeviceData} "获取成功"
// @Router /wechat/mini/statistics/device [get]
func (m *MiniStatisticsApi) GetDeviceData(c *gin.Context) {
deviceData, err := miniStatisticsService.GetDeviceData()
if err != nil {
global.GVA_LOG.Error("获取设备分布数据失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithData(deviceData, c)
}
@@ -0,0 +1,294 @@
package api
import (
"strconv"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model/request"
wechatResponse "github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model/response"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/service"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type MpAutoReplyApi struct{}
var mpAutoReplyService = service.ServiceGroupApp.MpAutoReplyService
// GetAutoReplyList 获取自动回复列表
// @Tags MpAutoReply
// @Summary 获取自动回复列表
// @Description 获取公众号自动回复规则列表
// @Accept json
// @Produce json
// @Param page query int false "页码"
// @Param pageSize query int false "每页数量"
// @Param replyType query int false "回复类型"
// @Success 200 {object} response.Response{data=response.PageResult} "获取成功"
// @Router /wechat/mp/auto-reply [get]
func (m *MpAutoReplyApi) GetAutoReplyList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
var replyType *int
if rt := c.Query("replyType"); rt != "" {
if t, err := strconv.Atoi(rt); err == nil {
replyType = &t
}
}
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 100 {
pageSize = 10
}
replies, total, err := mpAutoReplyService.GetAutoReplyList(page, pageSize, replyType)
if err != nil {
global.GVA_LOG.Error("获取自动回复列表失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithDetailed(wechatResponse.PageResult{
List: replies,
Total: total,
Page: page,
PageSize: pageSize,
}, "获取成功", c)
}
// GetAutoReplyByID 获取自动回复详情
// @Tags MpAutoReply
// @Summary 获取自动回复详情
// @Description 根据ID获取自动回复详细信息
// @Accept json
// @Produce json
// @Param id path int true "自动回复ID"
// @Success 200 {object} response.Response{data=model.MpAutoReply} "获取成功"
// @Router /wechat/mp/auto-reply/{id} [get]
func (m *MpAutoReplyApi) GetAutoReplyByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
response.FailWithMessage("无效的自动回复ID", c)
return
}
reply, err := mpAutoReplyService.GetAutoReplyByID(uint(id))
if err != nil {
global.GVA_LOG.Error("获取自动回复详情失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithData(reply, c)
}
// CreateAutoReply 创建自动回复
// @Tags MpAutoReply
// @Summary 创建自动回复
// @Description 创建新的自动回复规则
// @Accept json
// @Produce json
// @Param data body request.CreateAutoReplyRequest true "创建参数"
// @Success 200 {object} response.Response{data=model.MpAutoReply} "创建成功"
// @Router /wechat/mp/auto-reply [post]
func (m *MpAutoReplyApi) CreateAutoReply(c *gin.Context) {
var req request.CreateAutoReplyRequest
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
// 验证必填字段
if req.Type == 0 || req.ResponseMessageType == "" || req.ResponseContent == "" {
response.FailWithMessage("回复类型、响应消息类型和响应内容不能为空", c)
return
}
// 构建自动回复对象
autoReply := &model.MpAutoReply{
Type: req.Type,
ResponseMessageType: req.ResponseMessageType,
ResponseContent: &req.ResponseContent,
}
// 根据类型设置特定字段
if req.Type == model.AutoReplyTypeKeyword {
if req.RequestKeyword == "" {
response.FailWithMessage("关键字回复必须设置关键字", c)
return
}
autoReply.RequestKeyword = &req.RequestKeyword
if req.RequestMatch != 0 {
autoReply.RequestMatch = &req.RequestMatch
}
} else if req.Type == model.AutoReplyTypeMessage {
if req.RequestMessageType == "" {
response.FailWithMessage("消息回复必须设置请求消息类型", c)
return
}
autoReply.RequestMessageType = &req.RequestMessageType
}
err = mpAutoReplyService.CreateAutoReply(autoReply)
if err != nil {
global.GVA_LOG.Error("创建自动回复失败!", zap.Error(err))
response.FailWithMessage("创建失败: "+err.Error(), c)
return
}
response.OkWithDetailed(autoReply, "创建成功", c)
}
// UpdateAutoReply 更新自动回复
// @Tags MpAutoReply
// @Summary 更新自动回复
// @Description 更新自动回复规则
// @Accept json
// @Produce json
// @Param data body request.UpdateAutoReplyRequest true "更新参数"
// @Success 200 {object} response.Response "更新成功"
// @Router /wechat/mp/auto-reply [put]
func (m *MpAutoReplyApi) UpdateAutoReply(c *gin.Context) {
var req request.UpdateAutoReplyRequest
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if req.ID == 0 {
response.FailWithMessage("ID不能为空", c)
return
}
// 获取现有记录
existingReply, err := mpAutoReplyService.GetAutoReplyByID(req.ID)
if err != nil {
global.GVA_LOG.Error("获取自动回复失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
// 更新字段
if req.ResponseContent != "" {
existingReply.ResponseContent = &req.ResponseContent
}
if req.RequestKeyword != "" {
existingReply.RequestKeyword = &req.RequestKeyword
}
if req.RequestMatch != 0 {
existingReply.RequestMatch = &req.RequestMatch
}
err = mpAutoReplyService.UpdateAutoReply(existingReply)
if err != nil {
global.GVA_LOG.Error("更新自动回复失败!", zap.Error(err))
response.FailWithMessage("更新失败: "+err.Error(), c)
return
}
response.OkWithMessage("更新成功", c)
}
// DeleteAutoReply 删除自动回复
// @Tags MpAutoReply
// @Summary 删除自动回复
// @Description 删除指定的自动回复规则
// @Accept json
// @Produce json
// @Param id path int true "自动回复ID"
// @Success 200 {object} response.Response "删除成功"
// @Router /wechat/mp/auto-reply/{id} [delete]
func (m *MpAutoReplyApi) DeleteAutoReply(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
response.FailWithMessage("无效的自动回复ID", c)
return
}
err = mpAutoReplyService.DeleteAutoReply(uint(id))
if err != nil {
global.GVA_LOG.Error("删除自动回复失败!", zap.Error(err))
response.FailWithMessage("删除失败: "+err.Error(), c)
return
}
response.OkWithMessage("删除成功", c)
}
// TestAutoReply 测试自动回复
// @Tags MpAutoReply
// @Summary 测试自动回复
// @Description 测试关键字是否能触发自动回复
// @Accept json
// @Produce json
// @Param data body request.TestAutoReplyRequest true "测试参数"
// @Success 200 {object} response.Response{data=model.MpAutoReply} "测试成功"
// @Router /wechat/mp/auto-reply/test [post]
func (m *MpAutoReplyApi) TestAutoReply(c *gin.Context) {
var req request.TestAutoReplyRequest
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if req.Keyword == "" {
response.FailWithMessage("测试关键字不能为空", c)
return
}
reply, err := mpAutoReplyService.TestAutoReply(req.Keyword)
if err != nil {
global.GVA_LOG.Error("测试自动回复失败!", zap.Error(err))
response.FailWithMessage("测试失败: "+err.Error(), c)
return
}
if reply == nil {
response.OkWithDetailed(nil, "没有匹配的自动回复", c)
return
}
response.OkWithDetailed(reply, "找到匹配的自动回复", c)
}
// SetSubscribeReply 设置关注回复
// @Tags MpAutoReply
// @Summary 设置关注回复
// @Description 设置用户关注时的自动回复
// @Accept json
// @Produce json
// @Param data body request.SetSubscribeReplyRequest true "设置参数"
// @Success 200 {object} response.Response "设置成功"
// @Router /wechat/mp/auto-reply/subscribe [post]
func (m *MpAutoReplyApi) SetSubscribeReply(c *gin.Context) {
var req request.SetSubscribeReplyRequest
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if req.Content == "" {
response.FailWithMessage("回复内容不能为空", c)
return
}
err = mpAutoReplyService.SetSubscribeReply(req.Content)
if err != nil {
global.GVA_LOG.Error("设置关注回复失败!", zap.Error(err))
response.FailWithMessage("设置失败: "+err.Error(), c)
return
}
response.OkWithMessage("设置成功", c)
}
@@ -0,0 +1,369 @@
package api
import (
"strconv"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model/request"
wechatResponse "github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model/response"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/service"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type MpConfigApi struct{}
var mpConfigService = service.ServiceGroupApp.MpConfigService
// GetWechatConfig 获取微信配置
// @Tags MpConfig
// @Summary 获取微信配置
// @Description 获取微信配置信息
// @Accept json
// @Produce json
// @Param type query string false "配置类型 mp/mini"
// @Success 200 {object} response.Response{data=model.MpConfig} "获取成功"
// @Router /wechat/mp/config [get]
func (m *MpConfigApi) GetWechatConfig(c *gin.Context) {
configType := c.DefaultQuery("type", "mp")
config, err := mpConfigService.GetWechatConfig(configType)
if err != nil {
global.GVA_LOG.Error("获取微信配置失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithData(config, c)
}
// SaveWechatConfig 保存微信配置
// @Tags MpConfig
// @Summary 保存微信配置
// @Description 保存微信配置信息
// @Accept json
// @Produce json
// @Param data body request.SaveMpConfigRequest true "配置信息"
// @Success 200 {object} response.Response "保存成功"
// @Router /wechat/mp/config [post]
func (m *MpConfigApi) SaveWechatConfig(c *gin.Context) {
var req request.SaveMpConfigRequest
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
// 将DTO转换为数据库实体
config := &model.MpConfig{
ConfigType: req.ConfigType,
AppID: req.AppID,
AppSecret: req.AppSecret,
Token: req.Token,
EncodingAESKey: req.EncodingAESKey,
ServerURL: req.ServerURL,
WebhookURL: req.WebhookURL,
MiniAppName: req.MiniAppName,
MiniAppDesc: req.MiniAppDesc,
Status: req.Status,
ExtraConfig: req.ExtraConfig,
Remark: req.Remark,
}
// 验证配置
err = mpConfigService.ValidateConfig(config)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
err = mpConfigService.SaveWechatConfig(config)
if err != nil {
global.GVA_LOG.Error("保存微信配置失败!", zap.Error(err))
response.FailWithMessage("保存失败: "+err.Error(), c)
return
}
response.OkWithData(config, c)
}
// TestWechatConfig 测试微信配置
// @Tags MpConfig
// @Summary 测试微信配置
// @Description 测试微信配置是否有效
// @Accept json
// @Produce json
// @Param data body request.TestMpConfigRequest true "配置信息"
// @Success 200 {object} response.Response "测试成功"
// @Router /wechat/mp/config/test [post]
func (m *MpConfigApi) TestWechatConfig(c *gin.Context) {
var req request.TestMpConfigRequest
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
// 将DTO转换为数据库实体
config := &model.MpConfig{
ConfigType: req.ConfigType,
AppID: req.AppID,
AppSecret: req.AppSecret,
Token: req.Token,
EncodingAESKey: req.EncodingAESKey,
ServerURL: req.ServerURL,
WebhookURL: req.WebhookURL,
MiniAppName: req.MiniAppName,
MiniAppDesc: req.MiniAppDesc,
}
err = mpConfigService.TestWechatConfig(config)
if err != nil {
global.GVA_LOG.Error("测试微信配置失败!", zap.Error(err))
response.FailWithMessage("测试失败: "+err.Error(), c)
return
}
response.OkWithMessage("测试成功", c)
}
// GetConfigList 获取配置列表
// @Tags MpConfig
// @Summary 获取配置列表
// @Description 获取所有微信配置列表
// @Accept json
// @Produce json
// @Success 200 {object} response.Response{data=[]model.MpConfig} "获取成功"
// @Router /wechat/mp/config/list [get]
func (m *MpConfigApi) GetConfigList(c *gin.Context) {
configs, err := mpConfigService.GetConfigList()
if err != nil {
global.GVA_LOG.Error("获取配置列表失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithData(configs, c)
}
// DeleteConfig 删除配置
// @Tags MpConfig
// @Summary 删除配置
// @Description 删除微信配置
// @Accept json
// @Produce json
// @Param id path int true "配置ID"
// @Success 200 {object} response.Response "删除成功"
// @Router /wechat/mp/config/{id} [delete]
func (m *MpConfigApi) DeleteConfig(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
response.FailWithMessage("无效的配置ID", c)
return
}
err = mpConfigService.DeleteConfig(uint(id))
if err != nil {
global.GVA_LOG.Error("删除配置失败!", zap.Error(err))
response.FailWithMessage("删除失败: "+err.Error(), c)
return
}
response.OkWithMessage("删除成功", c)
}
// GetWebhookLogs 获取Webhook日志
// @Tags MpConfig
// @Summary 获取Webhook日志
// @Description 获取微信Webhook日志列表
// @Accept json
// @Produce json
// @Param page query int false "页码"
// @Param pageSize query int false "每页数量"
// @Param configId query int false "配置ID"
// @Success 200 {object} response.Response{data=response.PageResult} "获取成功"
// @Router /wechat/mp/webhook/logs [get]
func (m *MpConfigApi) GetWebhookLogs(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
var configID *uint
if configIDStr := c.Query("configId"); configIDStr != "" {
if id, err := strconv.ParseUint(configIDStr, 10, 32); err == nil {
configIDUint := uint(id)
configID = &configIDUint
}
}
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 100 {
pageSize = 10
}
logs, total, err := mpConfigService.GetWebhookLogs(page, pageSize, configID)
if err != nil {
global.GVA_LOG.Error("获取Webhook日志失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithDetailed(wechatResponse.PageResult{
List: logs,
Total: total,
Page: page,
PageSize: pageSize,
}, "获取成功", c)
}
// GetWebhookLogDetail 获取Webhook日志详情
// @Tags MpConfig
// @Summary 获取Webhook日志详情
// @Description 获取微信Webhook日志详情
// @Accept json
// @Produce json
// @Param id path int true "日志ID"
// @Success 200 {object} response.Response{data=model.MpWebhookLog} "获取成功"
// @Router /wechat/mp/webhook/logs/{id} [get]
func (m *MpConfigApi) GetWebhookLogDetail(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
response.FailWithMessage("无效的日志ID", c)
return
}
log, err := mpConfigService.GetWebhookLogByID(uint(id))
if err != nil {
global.GVA_LOG.Error("获取Webhook日志详情失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithData(log, c)
}
// ClearWebhookLogs 清空Webhook日志
// @Tags MpConfig
// @Summary 清空Webhook日志
// @Description 清空微信Webhook日志
// @Accept json
// @Produce json
// @Param configId query int false "配置ID"
// @Success 200 {object} response.Response "清空成功"
// @Router /wechat/mp/webhook/logs/clear [post]
func (m *MpConfigApi) ClearWebhookLogs(c *gin.Context) {
var configID *uint
if configIDStr := c.Query("configId"); configIDStr != "" {
if id, err := strconv.ParseUint(configIDStr, 10, 32); err == nil {
configIDUint := uint(id)
configID = &configIDUint
}
}
err := mpConfigService.ClearWebhookLogs(configID)
if err != nil {
global.GVA_LOG.Error("清空Webhook日志失败!", zap.Error(err))
response.FailWithMessage("清空失败: "+err.Error(), c)
return
}
response.OkWithMessage("清空成功", c)
}
// ValidateConfig 验证微信配置
// @Tags MpConfig
// @Summary 验证微信配置
// @Description 验证微信配置是否有效
// @Accept json
// @Produce json
// @Success 200 {object} response.Response "验证成功"
// @Router /wechat/mp/config/validate [post]
func (m *MpConfigApi) ValidateConfig(c *gin.Context) {
// 获取当前配置
config, err := mpConfigService.GetWechatConfig("mp")
if err != nil {
global.GVA_LOG.Error("获取微信配置失败!", zap.Error(err))
response.FailWithMessage("获取配置失败: "+err.Error(), c)
return
}
// 验证配置
err = mpConfigService.ValidateConfig(config)
if err != nil {
global.GVA_LOG.Error("验证微信配置失败!", zap.Error(err))
response.FailWithMessage("验证失败: "+err.Error(), c)
return
}
// 使用微信公众号服务验证配置
wechatMpService := service.ServiceGroupApp.WechatMpService
err = wechatMpService.ValidateConfig(config)
if err != nil {
global.GVA_LOG.Error("配置连通性测试失败!", zap.Error(err))
response.FailWithMessage("配置连通性测试失败: "+err.Error(), c)
return
}
response.OkWithMessage("验证成功", c)
}
// GenerateQrCode 生成公众号二维码
// @Tags MpConfig
// @Summary 生成公众号二维码
// @Description 生成公众号二维码
// @Accept json
// @Produce json
// @Success 200 {object} response.Response{data=map[string]string} "生成成功"
// @Router /wechat/mp/qrcode/generate [post]
func (m *MpConfigApi) GenerateQrCode(c *gin.Context) {
// 获取当前配置
config, err := mpConfigService.GetWechatConfig("mp")
if err != nil {
global.GVA_LOG.Error("获取微信配置失败!", zap.Error(err))
response.FailWithMessage("获取配置失败: "+err.Error(), c)
return
}
// 生成二维码
qrCodeUrl, err := mpConfigService.GenerateQrCode(config)
if err != nil {
global.GVA_LOG.Error("生成二维码失败!", zap.Error(err))
response.FailWithMessage("生成失败: "+err.Error(), c)
return
}
response.OkWithData(map[string]string{"qrCodeUrl": qrCodeUrl}, c)
}
// ClearQuota 清空API配额
// @Tags MpConfig
// @Summary 清空API配额
// @Description 清空微信公众号API配额
// @Accept json
// @Produce json
// @Success 200 {object} response.Response "清空成功"
// @Router /wechat/mp/quota/clear [post]
func (m *MpConfigApi) ClearQuota(c *gin.Context) {
// 获取当前配置
config, err := mpConfigService.GetWechatConfig("mp")
if err != nil {
global.GVA_LOG.Error("获取微信配置失败!", zap.Error(err))
response.FailWithMessage("获取配置失败: "+err.Error(), c)
return
}
// 清空配额
err = mpConfigService.ClearQuota(config)
if err != nil {
global.GVA_LOG.Error("清空API配额失败!", zap.Error(err))
response.FailWithMessage("清空失败: "+err.Error(), c)
return
}
response.OkWithMessage("清空成功", c)
}
@@ -0,0 +1,226 @@
package api
import (
"strconv"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
wechatResponse "github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model/response"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/service"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type MpDraftApi struct{}
var mpDraftService = service.ServiceGroupApp.MpDraftService
// GetDraftList 获取草稿列表
// @Tags MpDraft
// @Summary 获取草稿列表
// @Description 获取公众号草稿列表
// @Accept json
// @Produce json
// @Param page query int false "页码"
// @Param pageSize query int false "每页数量"
// @Param title query string false "标题"
// @Param author query string false "作者"
// @Success 200 {object} response.Response{data=response.PageResult} "获取成功"
// @Router /wechat/mp/draft [get]
func (m *MpDraftApi) GetDraftList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
title := c.Query("title")
author := c.Query("author")
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 100 {
pageSize = 10
}
draftResponse, err := mpDraftService.GetDraftList(page, pageSize, title, author)
if err != nil {
global.GVA_LOG.Error("获取草稿列表失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithDetailed(wechatResponse.PageResult{
List: draftResponse.Items,
Total: int64(draftResponse.TotalCount),
Page: page,
PageSize: pageSize,
}, "获取成功", c)
}
// GetDraftInfo 获取草稿详情
// @Tags MpDraft
// @Summary 获取草稿详情
// @Description 获取公众号草稿详情
// @Accept json
// @Produce json
// @Param id path string true "草稿MediaID"
// @Success 200 {object} response.Response{data=service.DraftItem} "获取成功"
// @Router /wechat/mp/draft/{id} [get]
func (m *MpDraftApi) GetDraftInfo(c *gin.Context) {
mediaID := c.Param("id")
if mediaID == "" {
response.FailWithMessage("草稿MediaID不能为空", c)
return
}
draft, err := mpDraftService.GetDraftInfo(mediaID)
if err != nil {
global.GVA_LOG.Error("获取草稿详情失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithData(draft, c)
}
// CreateDraft 创建草稿
// @Tags MpDraft
// @Summary 创建草稿
// @Description 创建公众号草稿
// @Accept json
// @Produce json
// @Param data body service.CreateDraftRequest true "草稿信息"
// @Success 200 {object} response.Response "创建成功"
// @Router /wechat/mp/draft [post]
func (m *MpDraftApi) CreateDraft(c *gin.Context) {
var req service.CreateDraftRequest
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
mediaID, err := mpDraftService.CreateDraft(&req)
if err != nil {
global.GVA_LOG.Error("创建草稿失败!", zap.Error(err))
response.FailWithMessage("创建失败: "+err.Error(), c)
return
}
response.OkWithData(map[string]string{"mediaId": mediaID}, c)
}
// UpdateDraft 更新草稿
// @Tags MpDraft
// @Summary 更新草稿
// @Description 更新公众号草稿
// @Accept json
// @Produce json
// @Param data body service.UpdateDraftRequest true "草稿信息"
// @Success 200 {object} response.Response "更新成功"
// @Router /wechat/mp/draft [put]
func (m *MpDraftApi) UpdateDraft(c *gin.Context) {
var req service.UpdateDraftRequest
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
err = mpDraftService.UpdateDraft(&req)
if err != nil {
global.GVA_LOG.Error("更新草稿失败!", zap.Error(err))
response.FailWithMessage("更新失败: "+err.Error(), c)
return
}
response.OkWithMessage("更新成功", c)
}
// DeleteDraft 删除草稿
// @Tags MpDraft
// @Summary 删除草稿
// @Description 删除公众号草稿
// @Accept json
// @Produce json
// @Param id path string true "草稿MediaID"
// @Success 200 {object} response.Response "删除成功"
// @Router /wechat/mp/draft/{id} [delete]
func (m *MpDraftApi) DeleteDraft(c *gin.Context) {
mediaID := c.Param("id")
if mediaID == "" {
response.FailWithMessage("草稿MediaID不能为空", c)
return
}
err := mpDraftService.DeleteDraft(mediaID)
if err != nil {
global.GVA_LOG.Error("删除草稿失败!", zap.Error(err))
response.FailWithMessage("删除失败: "+err.Error(), c)
return
}
response.OkWithMessage("删除成功", c)
}
// PublishDraft 发布草稿
// @Tags MpDraft
// @Summary 发布草稿
// @Description 发布公众号草稿
// @Accept json
// @Produce json
// @Param id path string true "草稿MediaID"
// @Success 200 {object} response.Response "发布成功"
// @Router /wechat/mp/draft/{id}/publish [post]
func (m *MpDraftApi) PublishDraft(c *gin.Context) {
mediaID := c.Param("id")
if mediaID == "" {
response.FailWithMessage("草稿MediaID不能为空", c)
return
}
err := mpDraftService.PublishDraft(mediaID)
if err != nil {
global.GVA_LOG.Error("发布草稿失败!", zap.Error(err))
response.FailWithMessage("发布失败: "+err.Error(), c)
return
}
response.OkWithMessage("发布成功", c)
}
// SyncDrafts 同步草稿
// @Tags MpDraft
// @Summary 同步草稿
// @Description 同步公众号草稿
// @Accept json
// @Produce json
// @Success 200 {object} response.Response "同步成功"
// @Router /wechat/mp/draft/sync [post]
func (m *MpDraftApi) SyncDrafts(c *gin.Context) {
err := mpDraftService.SyncDrafts()
if err != nil {
global.GVA_LOG.Error("同步草稿失败!", zap.Error(err))
response.FailWithMessage("同步失败: "+err.Error(), c)
return
}
response.OkWithMessage("同步成功", c)
}
// GetDraftCount 获取草稿数量统计
// @Tags MpDraft
// @Summary 获取草稿数量统计
// @Description 获取草稿数量统计信息
// @Accept json
// @Produce json
// @Success 200 {object} response.Response{data=map[string]int64} "获取成功"
// @Router /wechat/mp/draft/count [get]
func (m *MpDraftApi) GetDraftCount(c *gin.Context) {
count, err := mpDraftService.GetDraftCount()
if err != nil {
global.GVA_LOG.Error("获取草稿数量统计失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithData(count, c)
}
@@ -0,0 +1,293 @@
package api
import (
"io"
"strconv"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
wechatResponse "github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model/response"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/service"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type MpMaterialApi struct{}
var mpMaterialService = service.ServiceGroupApp.MpMaterialService
// GetMaterialList 获取素材列表
// @Tags MpMaterial
// @Summary 获取素材列表
// @Description 获取公众号素材列表
// @Accept json
// @Produce json
// @Param page query int false "页码"
// @Param pageSize query int false "每页数量"
// @Param materialType query string false "素材类型"
// @Param permanent query bool false "是否永久素材"
// @Success 200 {object} response.Response{data=response.PageResult} "获取成功"
// @Router /wechat/mp/material [get]
func (m *MpMaterialApi) GetMaterialList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
materialType := c.Query("materialType")
var permanent *bool
if p := c.Query("permanent"); p != "" {
if perm, err := strconv.ParseBool(p); err == nil {
permanent = &perm
}
}
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 100 {
pageSize = 10
}
materials, total, err := mpMaterialService.GetMaterialList(page, pageSize, materialType, permanent)
if err != nil {
global.GVA_LOG.Error("获取素材列表失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithDetailed(wechatResponse.PageResult{
List: materials,
Total: total,
Page: page,
PageSize: pageSize,
}, "获取成功", c)
}
// GetMaterialByID 获取素材详情
// @Tags MpMaterial
// @Summary 获取素材详情
// @Description 根据ID获取素材详细信息
// @Accept json
// @Produce json
// @Param id path int true "素材ID"
// @Success 200 {object} response.Response{data=model.MpMaterial} "获取成功"
// @Router /wechat/mp/material/{id} [get]
func (m *MpMaterialApi) GetMaterialByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
response.FailWithMessage("无效的素材ID", c)
return
}
material, err := mpMaterialService.GetMaterialByID(uint(id))
if err != nil {
global.GVA_LOG.Error("获取素材详情失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithData(material, c)
}
// UploadTempMaterial 上传临时素材
// @Tags MpMaterial
// @Summary 上传临时素材
// @Description 上传临时素材到微信服务器
// @Accept multipart/form-data
// @Produce json
// @Param file formData file true "素材文件"
// @Param type formData string true "素材类型:image、voice、video、thumb"
// @Success 200 {object} response.Response{data=model.MpMaterial} "上传成功"
// @Router /wechat/mp/material/temp [post]
func (m *MpMaterialApi) UploadTempMaterial(c *gin.Context) {
// 获取上传的文件
file, header, err := c.Request.FormFile("file")
if err != nil {
response.FailWithMessage("获取文件失败: "+err.Error(), c)
return
}
defer file.Close()
materialType := c.PostForm("type")
if materialType == "" {
response.FailWithMessage("素材类型不能为空", c)
return
}
// 验证素材类型
validTypes := map[string]bool{
"image": true,
"voice": true,
"video": true,
"thumb": true,
}
if !validTypes[materialType] {
response.FailWithMessage("无效的素材类型", c)
return
}
material, err := mpMaterialService.UploadTempMaterial(file, header.Filename, materialType)
if err != nil {
global.GVA_LOG.Error("上传临时素材失败!", zap.Error(err))
response.FailWithMessage("上传失败: "+err.Error(), c)
return
}
response.OkWithDetailed(material, "上传成功", c)
}
// UploadPermanentMaterial 上传永久素材
// @Tags MpMaterial
// @Summary 上传永久素材
// @Description 上传永久素材到微信服务器
// @Accept multipart/form-data
// @Produce json
// @Param file formData file true "素材文件"
// @Param type formData string true "素材类型:image、voice、video、thumb"
// @Success 200 {object} response.Response{data=model.MpMaterial} "上传成功"
// @Router /wechat/mp/material/permanent [post]
func (m *MpMaterialApi) UploadPermanentMaterial(c *gin.Context) {
// 获取上传的文件
file, header, err := c.Request.FormFile("file")
if err != nil {
response.FailWithMessage("获取文件失败: "+err.Error(), c)
return
}
defer file.Close()
materialType := c.PostForm("type")
if materialType == "" {
response.FailWithMessage("素材类型不能为空", c)
return
}
// 验证素材类型
validTypes := map[string]bool{
"image": true,
"voice": true,
"video": true,
"thumb": true,
}
if !validTypes[materialType] {
response.FailWithMessage("无效的素材类型", c)
return
}
material, err := mpMaterialService.UploadPermanentMaterial(file, header.Filename, materialType)
if err != nil {
global.GVA_LOG.Error("上传永久素材失败!", zap.Error(err))
response.FailWithMessage("上传失败: "+err.Error(), c)
return
}
response.OkWithDetailed(material, "上传成功", c)
}
// DeleteMaterial 删除素材
// @Tags MpMaterial
// @Summary 删除素材
// @Description 删除指定的素材
// @Accept json
// @Produce json
// @Param id path int true "素材ID"
// @Success 200 {object} response.Response "删除成功"
// @Router /wechat/mp/material/{id} [delete]
func (m *MpMaterialApi) DeleteMaterial(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
response.FailWithMessage("无效的素材ID", c)
return
}
err = mpMaterialService.DeleteMaterial(uint(id))
if err != nil {
global.GVA_LOG.Error("删除素材失败!", zap.Error(err))
response.FailWithMessage("删除失败: "+err.Error(), c)
return
}
response.OkWithMessage("删除成功", c)
}
// GetMaterialByMediaID 根据MediaID获取素材
// @Tags MpMaterial
// @Summary 根据MediaID获取素材
// @Description 根据微信MediaID获取素材信息
// @Accept json
// @Produce json
// @Param mediaId query string true "微信MediaID"
// @Success 200 {object} response.Response{data=model.MpMaterial} "获取成功"
// @Router /wechat/mp/material/media [get]
func (m *MpMaterialApi) GetMaterialByMediaID(c *gin.Context) {
mediaID := c.Query("mediaId")
if mediaID == "" {
response.FailWithMessage("MediaID不能为空", c)
return
}
material, err := mpMaterialService.GetMaterialByMediaID(mediaID)
if err != nil {
global.GVA_LOG.Error("获取素材失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithData(material, c)
}
// DownloadMaterial 下载素材
// @Tags MpMaterial
// @Summary 下载素材
// @Description 从微信服务器下载素材
// @Accept json
// @Produce json
// @Param mediaId query string true "素材MediaID"
// @Success 200 {object} response.Response "下载成功"
// @Router /wechat/mp/material/download [get]
func (m *MpMaterialApi) DownloadMaterial(c *gin.Context) {
mediaID := c.Query("mediaId")
if mediaID == "" {
response.FailWithMessage("MediaID不能为空", c)
return
}
reader, filename, err := mpMaterialService.DownloadMaterial(mediaID)
if err != nil {
global.GVA_LOG.Error("下载素材失败!", zap.Error(err))
response.FailWithMessage("下载失败: "+err.Error(), c)
return
}
defer reader.Close()
// 设置响应头
c.Header("Content-Disposition", "attachment; filename="+filename)
c.Header("Content-Type", "application/octet-stream")
// 将文件内容写入响应
_, err = io.Copy(c.Writer, reader)
if err != nil {
global.GVA_LOG.Error("写入响应失败!", zap.Error(err))
response.FailWithMessage("下载失败", c)
return
}
}
// SyncMaterials 同步素材
// @Tags MpMaterial
// @Summary 同步素材
// @Description 从微信服务器同步素材列表
// @Accept json
// @Produce json
// @Success 200 {object} response.Response "同步成功"
// @Router /wechat/mp/material/sync [post]
func (m *MpMaterialApi) SyncMaterials(c *gin.Context) {
err := mpMaterialService.SyncMaterialsFromWechat()
if err != nil {
global.GVA_LOG.Error("同步素材失败!", zap.Error(err))
response.FailWithMessage("同步失败: "+err.Error(), c)
return
}
response.OkWithMessage("同步成功", c)
}
@@ -0,0 +1,247 @@
package api
import (
"strconv"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model/request"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/service"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type MpMenuApi struct{}
var mpMenuService = service.ServiceGroupApp.MpMenuService
// GetMenuList 获取菜单列表
// @Tags MpMenu
// @Summary 获取菜单列表
// @Description 获取公众号菜单配置列表
// @Accept json
// @Produce json
// @Success 200 {object} response.Response{data=[]model.MpMenu} "获取成功"
// @Router /wechat/mp/menu [get]
func (m *MpMenuApi) GetMenuList(c *gin.Context) {
menus, err := mpMenuService.GetMenuList()
if err != nil {
global.GVA_LOG.Error("获取菜单列表失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithDetailed(menus, "获取成功", c)
}
// GetMenuByID 获取菜单详情
// @Tags MpMenu
// @Summary 获取菜单详情
// @Description 根据ID获取菜单详细信息
// @Accept json
// @Produce json
// @Param id path int true "菜单ID"
// @Success 200 {object} response.Response{data=model.MpMenu} "获取成功"
// @Router /wechat/mp/menu/{id} [get]
func (m *MpMenuApi) GetMenuByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
response.FailWithMessage("无效的菜单ID", c)
return
}
menu, err := mpMenuService.GetMenuByID(uint(id))
if err != nil {
global.GVA_LOG.Error("获取菜单详情失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithData(menu, c)
}
// CreateMenu 创建菜单
// @Tags MpMenu
// @Summary 创建菜单
// @Description 创建新的菜单项
// @Accept json
// @Produce json
// @Param data body request.CreateMenuRequest true "创建参数"
// @Success 200 {object} response.Response{data=model.MpMenu} "创建成功"
// @Router /wechat/mp/menu [post]
func (m *MpMenuApi) CreateMenu(c *gin.Context) {
var req request.CreateMenuRequest
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
// 验证必填字段
if req.Name == "" || req.Type == "" {
response.FailWithMessage("菜单名称和类型不能为空", c)
return
}
// 构建菜单对象
menu := &model.MpMenu{
Name: &req.Name,
Type: req.Type,
}
// 设置可选字段
if req.ParentID != "" {
menu.ParentID = &req.ParentID
}
if req.MenuKey != "" {
menu.MenuKey = &req.MenuKey
}
if req.URL != "" {
menu.URL = &req.URL
}
if req.MiniProgramAppID != "" {
menu.MiniProgramAppID = &req.MiniProgramAppID
}
if req.MiniProgramPagePath != "" {
menu.MiniProgramPagePath = &req.MiniProgramPagePath
}
if req.ReplyMessageType != "" {
menu.ReplyMessageType = &req.ReplyMessageType
}
if req.ReplyContent != "" {
menu.ReplyContent = &req.ReplyContent
}
err = mpMenuService.CreateMenu(menu)
if err != nil {
global.GVA_LOG.Error("创建菜单失败!", zap.Error(err))
response.FailWithMessage("创建失败: "+err.Error(), c)
return
}
response.OkWithDetailed(menu, "创建成功", c)
}
// UpdateMenu 更新菜单
// @Tags MpMenu
// @Summary 更新菜单
// @Description 更新菜单项
// @Accept json
// @Produce json
// @Param data body request.UpdateMenuRequest true "更新参数"
// @Success 200 {object} response.Response "更新成功"
// @Router /wechat/mp/menu [put]
func (m *MpMenuApi) UpdateMenu(c *gin.Context) {
var req request.UpdateMenuRequest
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if req.ID == 0 {
response.FailWithMessage("ID不能为空", c)
return
}
// 获取现有记录
existingMenu, err := mpMenuService.GetMenuByID(req.ID)
if err != nil {
global.GVA_LOG.Error("获取菜单失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
// 更新字段
if req.Name != "" {
existingMenu.Name = &req.Name
}
if req.Type != "" {
existingMenu.Type = req.Type
}
if req.MenuKey != "" {
existingMenu.MenuKey = &req.MenuKey
}
if req.URL != "" {
existingMenu.URL = &req.URL
}
if req.ReplyContent != "" {
existingMenu.ReplyContent = &req.ReplyContent
}
err = mpMenuService.UpdateMenu(existingMenu)
if err != nil {
global.GVA_LOG.Error("更新菜单失败!", zap.Error(err))
response.FailWithMessage("更新失败: "+err.Error(), c)
return
}
response.OkWithMessage("更新成功", c)
}
// DeleteMenu 删除菜单
// @Tags MpMenu
// @Summary 删除菜单
// @Description 删除指定的菜单项
// @Accept json
// @Produce json
// @Param id path int true "菜单ID"
// @Success 200 {object} response.Response "删除成功"
// @Router /wechat/mp/menu/{id} [delete]
func (m *MpMenuApi) DeleteMenu(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
response.FailWithMessage("无效的菜单ID", c)
return
}
err = mpMenuService.DeleteMenu(uint(id))
if err != nil {
global.GVA_LOG.Error("删除菜单失败!", zap.Error(err))
response.FailWithMessage("删除失败: "+err.Error(), c)
return
}
response.OkWithMessage("删除成功", c)
}
// PublishMenu 发布菜单
// @Tags MpMenu
// @Summary 发布菜单
// @Description 将菜单配置发布到微信服务器
// @Accept json
// @Produce json
// @Success 200 {object} response.Response "发布成功"
// @Router /wechat/mp/menu/publish [post]
func (m *MpMenuApi) PublishMenu(c *gin.Context) {
err := mpMenuService.PublishMenu()
if err != nil {
global.GVA_LOG.Error("发布菜单失败!", zap.Error(err))
response.FailWithMessage("发布失败: "+err.Error(), c)
return
}
response.OkWithMessage("发布成功", c)
}
// DeleteWechatMenu 删除微信菜单
// @Tags MpMenu
// @Summary 删除微信菜单
// @Description 删除微信服务器上的菜单
// @Accept json
// @Produce json
// @Success 200 {object} response.Response "删除成功"
// @Router /wechat/mp/menu/wechat [delete]
func (m *MpMenuApi) DeleteWechatMenu(c *gin.Context) {
err := mpMenuService.DeleteWechatMenu()
if err != nil {
global.GVA_LOG.Error("删除微信菜单失败!", zap.Error(err))
response.FailWithMessage("删除失败: "+err.Error(), c)
return
}
response.OkWithMessage("删除成功", c)
}
@@ -0,0 +1,175 @@
package api
import (
"strconv"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model/request"
wechatResponse "github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model/response"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/service"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type MpMessageApi struct{}
var mpMessageService = service.ServiceGroupApp.MpMessageService
// GetMessageList 获取消息列表
// @Tags MpMessage
// @Summary 获取消息列表
// @Description 获取公众号消息记录列表
// @Accept json
// @Produce json
// @Param page query int false "页码"
// @Param pageSize query int false "每页数量"
// @Param msgType query string false "消息类型"
// @Param openid query string false "用户openid"
// @Success 200 {object} response.Response{data=response.PageResult} "获取成功"
// @Router /wechat/mp/messages [get]
func (m *MpMessageApi) GetMessageList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
msgType := c.Query("msgType")
openid := c.Query("openid")
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 100 {
pageSize = 10
}
messages, total, err := mpMessageService.GetMessageList(page, pageSize, msgType, openid)
if err != nil {
global.GVA_LOG.Error("获取消息列表失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithDetailed(wechatResponse.PageResult{
List: messages,
Total: total,
Page: page,
PageSize: pageSize,
}, "获取成功", c)
}
// SendTextMessage 发送文本消息
// @Tags MpMessage
// @Summary 发送文本消息
// @Description 向指定用户发送文本消息
// @Accept json
// @Produce json
// @Param data body request.SendTextMessageRequest true "发送参数"
// @Success 200 {object} response.Response "发送成功"
// @Router /wechat/mp/message/text [post]
func (m *MpMessageApi) SendTextMessage(c *gin.Context) {
var req request.SendTextMessageRequest
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if req.OpenID == "" || req.Content == "" {
response.FailWithMessage("openid和消息内容不能为空", c)
return
}
err = mpMessageService.SendTextMessage(req.OpenID, req.Content)
if err != nil {
global.GVA_LOG.Error("发送文本消息失败!", zap.Error(err))
response.FailWithMessage("发送失败: "+err.Error(), c)
return
}
response.OkWithMessage("发送成功", c)
}
// SendImageMessage 发送图片消息
// @Tags MpMessage
// @Summary 发送图片消息
// @Description 向指定用户发送图片消息
// @Accept json
// @Produce json
// @Param data body request.SendImageMessageRequest true "发送参数"
// @Success 200 {object} response.Response "发送成功"
// @Router /wechat/mp/message/image [post]
func (m *MpMessageApi) SendImageMessage(c *gin.Context) {
var req request.SendImageMessageRequest
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if req.OpenID == "" || req.MediaID == "" {
response.FailWithMessage("openid和媒体ID不能为空", c)
return
}
err = mpMessageService.SendImageMessage(req.OpenID, req.MediaID)
if err != nil {
global.GVA_LOG.Error("发送图片消息失败!", zap.Error(err))
response.FailWithMessage("发送失败: "+err.Error(), c)
return
}
response.OkWithMessage("发送成功", c)
}
// GetMessageByID 获取消息详情
// @Tags MpMessage
// @Summary 获取消息详情
// @Description 根据ID获取消息详细信息
// @Accept json
// @Produce json
// @Param id path int true "消息ID"
// @Success 200 {object} response.Response{data=model.MpMessage} "获取成功"
// @Router /wechat/mp/message/{id} [get]
func (m *MpMessageApi) GetMessageByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
response.FailWithMessage("无效的消息ID", c)
return
}
message, err := mpMessageService.GetMessageByID(uint(id))
if err != nil {
global.GVA_LOG.Error("获取消息详情失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithData(message, c)
}
// DeleteMessage 删除消息
// @Tags MpMessage
// @Summary 删除消息
// @Description 删除指定的消息记录
// @Accept json
// @Produce json
// @Param id path int true "消息ID"
// @Success 200 {object} response.Response "删除成功"
// @Router /wechat/mp/message/{id} [delete]
func (m *MpMessageApi) DeleteMessage(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
response.FailWithMessage("无效的消息ID", c)
return
}
err = mpMessageService.DeleteMessage(uint(id))
if err != nil {
global.GVA_LOG.Error("删除消息失败!", zap.Error(err))
response.FailWithMessage("删除失败: "+err.Error(), c)
return
}
response.OkWithMessage("删除成功", c)
}
@@ -0,0 +1,259 @@
package api
import (
"strconv"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model/request"
wechatResponse "github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model/response"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/service"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type MpNewsApi struct{}
var mpNewsService = service.ServiceGroupApp.MpNewsService
// GetNewsList 获取图文发表记录列表
// @Tags MpNews
// @Summary 获取图文发表记录列表
// @Description 获取公众号图文发表记录列表
// @Accept json
// @Produce json
// @Param page query int false "页码"
// @Param pageSize query int false "每页数量"
// @Param status query string false "发布状态"
// @Success 200 {object} response.Response{data=response.PageResult} "获取成功"
// @Router /wechat/mp/news [get]
func (m *MpNewsApi) GetNewsList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
status := c.Query("status")
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 100 {
pageSize = 10
}
newsList, total, err := mpNewsService.GetNewsList(page, pageSize, status)
if err != nil {
global.GVA_LOG.Error("获取图文发表记录列表失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithDetailed(wechatResponse.PageResult{
List: newsList,
Total: total,
Page: page,
PageSize: pageSize,
}, "获取成功", c)
}
// GetNewsInfo 获取图文详情
// @Tags MpNews
// @Summary 获取图文详情
// @Description 获取公众号图文详情
// @Accept json
// @Produce json
// @Param id path int true "图文ID"
// @Success 200 {object} response.Response{data=model.MpNews} "获取成功"
// @Router /wechat/mp/news/{id} [get]
func (m *MpNewsApi) GetNewsInfo(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
response.FailWithMessage("无效的图文ID", c)
return
}
news, err := mpNewsService.GetNewsInfo(uint(id))
if err != nil {
global.GVA_LOG.Error("获取图文详情失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithData(news, c)
}
// CreateNews 创建图文记录
// @Tags MpNews
// @Summary 创建图文记录
// @Description 创建公众号图文记录
// @Accept json
// @Produce json
// @Param data body request.CreateMpNewsRequest true "图文信息"
// @Success 200 {object} response.Response "创建成功"
// @Router /wechat/mp/news [post]
func (m *MpNewsApi) CreateNews(c *gin.Context) {
var req request.CreateMpNewsRequest
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
// 将DTO转换为数据库实体
news := &model.MpNews{
MediaID: req.MediaID,
Title: req.Title,
Author: req.Author,
Digest: req.Digest,
Content: req.Content,
ContentURL: req.ContentURL,
SourceURL: req.SourceURL,
ThumbMediaID: req.ThumbMediaID,
ThumbURL: req.ThumbURL,
ShowCover: req.ShowCover,
NeedOpenComment: req.NeedOpenComment,
OnlyFansCanComment: req.OnlyFansCanComment,
PublishStatus: req.PublishStatus,
WechatURL: req.WechatURL,
WechatMsgID: req.WechatMsgID,
}
err = mpNewsService.CreateNews(news)
if err != nil {
global.GVA_LOG.Error("创建图文记录失败!", zap.Error(err))
response.FailWithMessage("创建失败: "+err.Error(), c)
return
}
response.OkWithData(news, c)
}
// UpdateNews 更新图文记录
// @Tags MpNews
// @Summary 更新图文记录
// @Description 更新公众号图文记录
// @Accept json
// @Produce json
// @Param data body request.UpdateMpNewsRequest true "图文信息"
// @Success 200 {object} response.Response "更新成功"
// @Router /wechat/mp/news [put]
func (m *MpNewsApi) UpdateNews(c *gin.Context) {
var req request.UpdateMpNewsRequest
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if req.ID == 0 {
response.FailWithMessage("ID不能为空", c)
return
}
// 获取现有记录以保留时间字段
existingNews, err := mpNewsService.GetNewsInfo(req.ID)
if err != nil {
global.GVA_LOG.Error("获取图文记录失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
// 更新字段,保留原有的时间字段
existingNews.MediaID = req.MediaID
existingNews.Title = req.Title
existingNews.Author = req.Author
existingNews.Digest = req.Digest
existingNews.Content = req.Content
existingNews.ContentURL = req.ContentURL
existingNews.SourceURL = req.SourceURL
existingNews.ThumbMediaID = req.ThumbMediaID
existingNews.ThumbURL = req.ThumbURL
existingNews.ShowCover = req.ShowCover
existingNews.NeedOpenComment = req.NeedOpenComment
existingNews.OnlyFansCanComment = req.OnlyFansCanComment
existingNews.PublishStatus = req.PublishStatus
existingNews.WechatURL = req.WechatURL
existingNews.WechatMsgID = req.WechatMsgID
err = mpNewsService.UpdateNews(existingNews)
if err != nil {
global.GVA_LOG.Error("更新图文记录失败!", zap.Error(err))
response.FailWithMessage("更新失败: "+err.Error(), c)
return
}
response.OkWithMessage("更新成功", c)
}
// DeleteNews 删除图文记录
// @Tags MpNews
// @Summary 删除图文记录
// @Description 删除公众号图文记录
// @Accept json
// @Produce json
// @Param id path int true "图文ID"
// @Success 200 {object} response.Response "删除成功"
// @Router /wechat/mp/news/{id} [delete]
func (m *MpNewsApi) DeleteNews(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
response.FailWithMessage("无效的图文ID", c)
return
}
err = mpNewsService.DeleteNews(uint(id))
if err != nil {
global.GVA_LOG.Error("删除图文记录失败!", zap.Error(err))
response.FailWithMessage("删除失败: "+err.Error(), c)
return
}
response.OkWithMessage("删除成功", c)
}
// GetNewsStatistics 获取图文数据统计
// @Tags MpNews
// @Summary 获取图文数据统计
// @Description 获取公众号图文数据统计
// @Accept json
// @Produce json
// @Param id path int true "图文ID"
// @Success 200 {object} response.Response{data=map[string]interface{}} "获取成功"
// @Router /wechat/mp/news/{id}/statistics [get]
func (m *MpNewsApi) GetNewsStatistics(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
response.FailWithMessage("无效的图文ID", c)
return
}
statistics, err := mpNewsService.GetNewsStatistics(uint(id))
if err != nil {
global.GVA_LOG.Error("获取图文数据统计失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithData(statistics, c)
}
// SyncNews 同步图文发表记录
// @Tags MpNews
// @Summary 同步图文发表记录
// @Description 同步公众号图文发表记录
// @Accept json
// @Produce json
// @Success 200 {object} response.Response "同步成功"
// @Router /wechat/mp/news/sync [post]
func (m *MpNewsApi) SyncNews(c *gin.Context) {
err := mpNewsService.SyncNews()
if err != nil {
global.GVA_LOG.Error("同步图文发表记录失败!", zap.Error(err))
response.FailWithMessage("同步失败: "+err.Error(), c)
return
}
response.OkWithMessage("同步成功", c)
}
@@ -0,0 +1,130 @@
package api
import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type MpStatisticsApi struct{}
// GetUserSummary 获得粉丝增减数据
// @Tags MpStatistics
// @Summary 获得粉丝增减数据
// @Description 获得粉丝增减数据
// @Accept json
// @Produce json
// @Param accountId query int true "公众号账号ID"
// @Param date query string true "日期"
// @Success 200 {object} response.Response "获取成功"
// @Router /wechat/mp/statistics/user-summary [get]
func (m *MpStatisticsApi) GetUserSummary(c *gin.Context) {
accountId := c.Query("accountId")
date := c.Query("date")
// TODO: 实现粉丝增减数据获取逻辑
global.GVA_LOG.Info("获取粉丝增减数据", zap.String("accountId", accountId), zap.String("date", date))
// 模拟数据
data := []map[string]interface{}{
{
"refDate": "2025-08-04",
"userSource": 0,
"newUser": 12,
"cancelUser": 3,
"cumulateUser": 1350,
},
}
response.OkWithData(data, c)
}
// GetUserCumulate 获得粉丝累计数据
// @Tags MpStatistics
// @Summary 获得粉丝累计数据
// @Description 获得粉丝累计数据
// @Accept json
// @Produce json
// @Param accountId query int true "公众号账号ID"
// @Param date query string true "日期"
// @Success 200 {object} response.Response "获取成功"
// @Router /wechat/mp/statistics/user-cumulate [get]
func (m *MpStatisticsApi) GetUserCumulate(c *gin.Context) {
accountId := c.Query("accountId")
date := c.Query("date")
// TODO: 实现粉丝累计数据获取逻辑
global.GVA_LOG.Info("获取粉丝累计数据", zap.String("accountId", accountId), zap.String("date", date))
// 模拟数据
data := []map[string]interface{}{
{
"refDate": "2025-08-04",
"cumulateUser": 1350,
},
}
response.OkWithData(data, c)
}
// GetUpstreamMessage 获取消息发送概况数据
// @Tags MpStatistics
// @Summary 获取消息发送概况数据
// @Description 获取消息发送概况数据
// @Accept json
// @Produce json
// @Param accountId query int true "公众号账号ID"
// @Param date query string true "日期"
// @Success 200 {object} response.Response "获取成功"
// @Router /wechat/mp/statistics/upstream-message [get]
func (m *MpStatisticsApi) GetUpstreamMessage(c *gin.Context) {
accountId := c.Query("accountId")
date := c.Query("date")
// TODO: 实现消息发送概况数据获取逻辑
global.GVA_LOG.Info("获取消息发送概况数据", zap.String("accountId", accountId), zap.String("date", date))
// 模拟数据
data := []map[string]interface{}{
{
"refDate": "2025-08-04",
"msgType": 1,
"msgUser": 45,
"msgCount": 128,
},
}
response.OkWithData(data, c)
}
// GetInterfaceSummary 获取接口调用概况数据
// @Tags MpStatistics
// @Summary 获取接口调用概况数据
// @Description 获取接口调用概况数据
// @Accept json
// @Produce json
// @Param accountId query int true "公众号账号ID"
// @Param date query string true "日期"
// @Success 200 {object} response.Response "获取成功"
// @Router /wechat/mp/statistics/interface-summary [get]
func (m *MpStatisticsApi) GetInterfaceSummary(c *gin.Context) {
accountId := c.Query("accountId")
date := c.Query("date")
// TODO: 实现接口调用概况数据获取逻辑
global.GVA_LOG.Info("获取接口调用概况数据", zap.String("accountId", accountId), zap.String("date", date))
// 模拟数据
data := []map[string]interface{}{
{
"refDate": "2025-08-04",
"callbackCount": 256,
"failCount": 2,
"totalTimeCost": 1280,
"maxTimeCost": 45,
},
}
response.OkWithData(data, c)
}
@@ -0,0 +1,187 @@
package api
import (
"strconv"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
wechatResponse "github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model/response"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type MpTagApi struct{}
// CreateTag 创建公众号标签
// @Tags MpTag
// @Summary 创建公众号标签
// @Description 创建公众号标签
// @Accept json
// @Produce json
// @Param data body map[string]interface{} true "标签信息"
// @Success 200 {object} response.Response "创建成功"
// @Router /wechat/mp/tag/create [post]
func (m *MpTagApi) CreateTag(c *gin.Context) {
var req map[string]interface{}
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
// TODO: 实现标签创建逻辑
global.GVA_LOG.Info("创建标签请求", zap.Any("data", req))
response.OkWithMessage("标签创建功能待实现", c)
}
// UpdateTag 更新公众号标签
// @Tags MpTag
// @Summary 更新公众号标签
// @Description 更新公众号标签
// @Accept json
// @Produce json
// @Param data body map[string]interface{} true "标签信息"
// @Success 200 {object} response.Response "更新成功"
// @Router /wechat/mp/tag/update [put]
func (m *MpTagApi) UpdateTag(c *gin.Context) {
var req map[string]interface{}
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
// TODO: 实现标签更新逻辑
global.GVA_LOG.Info("更新标签请求", zap.Any("data", req))
response.OkWithMessage("标签更新功能待实现", c)
}
// DeleteTag 删除公众号标签
// @Tags MpTag
// @Summary 删除公众号标签
// @Description 删除公众号标签
// @Accept json
// @Produce json
// @Param id query int true "标签ID"
// @Success 200 {object} response.Response "删除成功"
// @Router /wechat/mp/tag/delete [delete]
func (m *MpTagApi) DeleteTag(c *gin.Context) {
idStr := c.Query("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
response.FailWithMessage("无效的标签ID", c)
return
}
// TODO: 实现标签删除逻辑
global.GVA_LOG.Info("删除标签请求", zap.Uint64("id", id))
response.OkWithMessage("标签删除功能待实现", c)
}
// GetTag 获取公众号标签详情
// @Tags MpTag
// @Summary 获取公众号标签详情
// @Description 获取公众号标签详情
// @Accept json
// @Produce json
// @Param id query int true "标签ID"
// @Success 200 {object} response.Response "获取成功"
// @Router /wechat/mp/tag/get [get]
func (m *MpTagApi) GetTag(c *gin.Context) {
idStr := c.Query("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
response.FailWithMessage("无效的标签ID", c)
return
}
// TODO: 实现获取标签详情逻辑
global.GVA_LOG.Info("获取标签详情请求", zap.Uint64("id", id))
response.OkWithMessage("获取标签详情功能待实现", c)
}
// GetTagPage 获取公众号标签分页
// @Tags MpTag
// @Summary 获取公众号标签分页
// @Description 获取公众号标签分页
// @Accept json
// @Produce json
// @Param page query int false "页码"
// @Param pageSize query int false "每页数量"
// @Success 200 {object} response.Response{data=response.PageResult} "获取成功"
// @Router /wechat/mp/tag/page [get]
func (m *MpTagApi) GetTagPage(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 100 {
pageSize = 10
}
// 模拟数据
tags := []map[string]interface{}{
{
"id": 1,
"name": "VIP用户",
"count": 150,
},
{
"id": 2,
"name": "普通用户",
"count": 1200,
},
}
response.OkWithDetailed(wechatResponse.PageResult{
List: tags,
Total: int64(len(tags)),
Page: page,
PageSize: pageSize,
}, "获取成功", c)
}
// GetSimpleTags 获取公众号标签精简信息列表
// @Tags MpTag
// @Summary 获取公众号标签精简信息列表
// @Description 获取公众号标签精简信息列表
// @Accept json
// @Produce json
// @Success 200 {object} response.Response "获取成功"
// @Router /wechat/mp/tag/list-all-simple [get]
func (m *MpTagApi) GetSimpleTags(c *gin.Context) {
// TODO: 实现获取简单标签列表逻辑
global.GVA_LOG.Info("获取简单标签列表请求")
// 模拟数据
tags := []map[string]interface{}{
{"id": 1, "name": "VIP用户"},
{"id": 2, "name": "普通用户"},
}
response.OkWithData(tags, c)
}
// SyncTag 同步公众号标签
// @Tags MpTag
// @Summary 同步公众号标签
// @Description 同步公众号标签
// @Accept json
// @Produce json
// @Param accountId query int true "公众号账号ID"
// @Success 200 {object} response.Response "同步成功"
// @Router /wechat/mp/tag/sync [post]
func (m *MpTagApi) SyncTag(c *gin.Context) {
accountIdStr := c.Query("accountId")
accountId, err := strconv.ParseUint(accountIdStr, 10, 32)
if err != nil {
response.FailWithMessage("无效的账号ID", c)
return
}
// TODO: 实现标签同步逻辑
global.GVA_LOG.Info("同步标签请求", zap.Uint64("accountId", accountId))
response.OkWithMessage("标签同步功能待实现", c)
}
@@ -0,0 +1,151 @@
package api
import (
"strconv"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model/request"
wechatResponse "github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model/response"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/service"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type MpUserApi struct{}
var mpUserService = service.ServiceGroupApp.MpUserService
// GetUserList 获取公众号用户列表
// @Tags MpUser
// @Summary 获取公众号用户列表
// @Description 获取微信公众号粉丝列表
// @Accept json
// @Produce json
// @Param page query int false "页码"
// @Param pageSize query int false "每页数量"
// @Param subscribeStatus query int false "关注状态"
// @Success 200 {object} response.Response{data=response.PageResult} "获取成功"
// @Router /wechat/mp/users [get]
func (m *MpUserApi) GetUserList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
var subscribeStatus *int
if status := c.Query("subscribeStatus"); status != "" {
if s, err := strconv.Atoi(status); err == nil {
subscribeStatus = &s
}
}
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 100 {
pageSize = 10
}
users, total, err := mpUserService.GetUserList(page, pageSize, subscribeStatus)
if err != nil {
global.GVA_LOG.Error("获取公众号用户列表失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithDetailed(wechatResponse.PageResult{
List: users,
Total: total,
Page: page,
PageSize: pageSize,
}, "获取成功", c)
}
// GetUserInfo 获取公众号用户信息
// @Tags MpUser
// @Summary 获取公众号用户信息
// @Description 根据openid获取公众号用户详细信息
// @Accept json
// @Produce json
// @Param openid query string true "用户openid"
// @Success 200 {object} response.Response{data=model.MpUser} "获取成功"
// @Router /wechat/mp/user [get]
func (m *MpUserApi) GetUserInfo(c *gin.Context) {
openid := c.Query("openid")
if openid == "" {
response.FailWithMessage("openid不能为空", c)
return
}
user, err := mpUserService.GetUserByOpenID(openid)
if err != nil {
global.GVA_LOG.Error("获取公众号用户信息失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithData(user, c)
}
// SyncUser 同步用户信息
// @Tags MpUser
// @Summary 同步用户信息
// @Description 从微信服务器同步用户信息
// @Accept json
// @Produce json
// @Param data body request.SyncUserRequest true "同步参数"
// @Success 200 {object} response.Response{data=model.MpUser} "同步成功"
// @Router /wechat/mp/user/sync [post]
func (m *MpUserApi) SyncUser(c *gin.Context) {
var req request.SyncUserRequest
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if req.OpenID == "" {
response.FailWithMessage("openid不能为空", c)
return
}
user, err := mpUserService.SyncUser(req.OpenID)
if err != nil {
global.GVA_LOG.Error("同步用户信息失败!", zap.Error(err))
response.FailWithMessage("同步失败: "+err.Error(), c)
return
}
response.OkWithDetailed(user, "同步成功", c)
}
// UpdateUserRemark 更新用户备注
// @Tags MpUser
// @Summary 更新用户备注
// @Description 更新公众号用户备注
// @Accept json
// @Produce json
// @Param data body request.UpdateRemarkRequest true "更新参数"
// @Success 200 {object} response.Response "更新成功"
// @Router /wechat/mp/user/remark [put]
func (m *MpUserApi) UpdateUserRemark(c *gin.Context) {
var req request.UpdateRemarkRequest
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if req.OpenID == "" {
response.FailWithMessage("openid不能为空", c)
return
}
err = mpUserService.UpdateUserRemark(req.OpenID, req.Remark)
if err != nil {
global.GVA_LOG.Error("更新用户备注失败!", zap.Error(err))
response.FailWithMessage("更新失败: "+err.Error(), c)
return
}
response.OkWithMessage("更新成功", c)
}
@@ -0,0 +1,212 @@
package api
import (
"strconv"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model/request"
wechatResponse "github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model/response"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/service"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type WechatMiniApi struct{}
var wechatMiniService = service.ServiceGroupApp.WechatMiniService
// Login 小程序登录
// @Tags WechatMini
// @Summary 小程序登录
// @Description 微信小程序登录接口
// @Accept json
// @Produce json
// @Param data body request.MiniLoginRequest true "登录参数"
// @Success 200 {object} response.Response{data=response.MiniLoginResponse} "登录成功"
// @Router /wechat/mini/login [post]
func (w *WechatMiniApi) Login(c *gin.Context) {
var req request.MiniLoginRequest
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if req.Code == "" {
response.FailWithMessage("code不能为空", c)
return
}
user, err := wechatMiniService.Code2Session(req.Code)
if err != nil {
global.GVA_LOG.Error("小程序登录失败!", zap.Error(err))
response.FailWithMessage("登录失败: "+err.Error(), c)
return
}
// 这里可以生成JWT token或其他认证信息
resp := wechatResponse.MiniLoginResponse{
OpenID: user.OpenID,
UnionID: user.UnionID,
UserID: user.UserID,
}
response.OkWithDetailed(resp, "登录成功", c)
}
// GetUserInfo 获取用户信息
// @Tags WechatMini
// @Summary 获取用户信息
// @Description 获取小程序用户信息
// @Accept json
// @Produce json
// @Param openid query string true "用户openid"
// @Success 200 {object} response.Response{data=model.WechatMiniUser} "获取成功"
// @Router /wechat/mini/userinfo [get]
func (w *WechatMiniApi) GetUserInfo(c *gin.Context) {
openid := c.Query("openid")
if openid == "" {
response.FailWithMessage("openid不能为空", c)
return
}
user, err := wechatMiniService.GetUserInfo(openid)
if err != nil {
global.GVA_LOG.Error("获取用户信息失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithData(user, c)
}
// UpdateUserInfo 更新用户信息
// @Tags WechatMini
// @Summary 更新用户信息
// @Description 更新小程序用户信息
// @Accept json
// @Produce json
// @Param data body request.UpdateUserInfoRequest true "用户信息"
// @Success 200 {object} response.Response "更新成功"
// @Router /wechat/mini/userinfo [put]
func (w *WechatMiniApi) UpdateUserInfo(c *gin.Context) {
var req request.UpdateUserInfoRequest
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if req.OpenID == "" {
response.FailWithMessage("openid不能为空", c)
return
}
err = wechatMiniService.UpdateUserInfo(req.OpenID, req.UserInfo)
if err != nil {
global.GVA_LOG.Error("更新用户信息失败!", zap.Error(err))
response.FailWithMessage("更新失败: "+err.Error(), c)
return
}
response.OkWithMessage("更新成功", c)
}
// BindPhone 绑定手机号
// @Tags WechatMini
// @Summary 绑定手机号
// @Description 绑定手机号并关联系统用户
// @Accept json
// @Produce json
// @Param data body request.BindPhoneRequest true "绑定参数"
// @Success 200 {object} response.Response{data=model.WechatMiniUser} "绑定成功"
// @Router /wechat/mini/bind-phone [post]
func (w *WechatMiniApi) BindPhone(c *gin.Context) {
var req request.BindPhoneRequest
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if req.OpenID == "" || req.Phone == "" {
response.FailWithMessage("openid和手机号不能为空", c)
return
}
user, err := wechatMiniService.BindPhoneAndLinkUser(req.OpenID, req.Phone)
if err != nil {
global.GVA_LOG.Error("绑定手机号失败!", zap.Error(err))
response.FailWithMessage("绑定失败: "+err.Error(), c)
return
}
response.OkWithDetailed(user, "绑定成功", c)
}
// GetUserList 获取用户列表
// @Tags WechatMini
// @Summary 获取用户列表
// @Description 获取小程序用户列表(管理员接口)
// @Accept json
// @Produce json
// @Param page query int false "页码"
// @Param pageSize query int false "每页数量"
// @Success 200 {object} response.Response{data=response.PageResult} "获取成功"
// @Router /wechat/mini/users [get]
func (w *WechatMiniApi) GetUserList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 100 {
pageSize = 10
}
users, total, err := wechatMiniService.GetUserList(page, pageSize)
if err != nil {
global.GVA_LOG.Error("获取用户列表失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithDetailed(wechatResponse.PageResult{
List: users,
Total: total,
Page: page,
PageSize: pageSize,
}, "获取成功", c)
}
// CheckUnionID 检查UnionID是否存在(为APP登录预留)
// @Tags WechatMini
// @Summary 检查UnionID
// @Description 检查UnionID是否已存在用户
// @Accept json
// @Produce json
// @Param unionid query string true "UnionID"
// @Success 200 {object} response.Response{data=model.WechatMiniUser} "检查成功"
// @Router /wechat/mini/check-unionid [get]
func (w *WechatMiniApi) CheckUnionID(c *gin.Context) {
unionid := c.Query("unionid")
if unionid == "" {
response.FailWithMessage("unionid不能为空", c)
return
}
user, err := wechatMiniService.CheckUnionIDExists(unionid)
if err != nil {
global.GVA_LOG.Error("检查UnionID失败!", zap.Error(err))
response.FailWithMessage("检查失败: "+err.Error(), c)
return
}
if user == nil {
response.OkWithDetailed(nil, "UnionID不存在", c)
} else {
response.OkWithDetailed(user, "UnionID已存在", c)
}
}
@@ -0,0 +1,89 @@
package api
import (
"crypto/sha1"
"fmt"
"io"
"sort"
"strings"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/service"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type WechatWebhookApi struct{}
var webhookMessageService = service.ServiceGroupApp.MpMessageService
// OfficialAccountWebhook 公众号消息接收
// @Tags WechatWebhook
// @Summary 公众号消息接收
// @Description 接收微信公众号推送的消息和事件
// @Accept xml
// @Produce xml
// @Param signature query string true "微信加密签名"
// @Param timestamp query string true "时间戳"
// @Param nonce query string true "随机数"
// @Param echostr query string false "随机字符串"
// @Success 200 {string} string "success"
// @Router /wechat/official/webhook [get,post]
func (w *WechatWebhookApi) OfficialAccountWebhook(c *gin.Context) {
// 获取参数
signature := c.Query("signature")
timestamp := c.Query("timestamp")
nonce := c.Query("nonce")
echostr := c.Query("echostr")
// 验证签名
if !w.verifySignature(signature, timestamp, nonce) {
global.GVA_LOG.Error("微信签名验证失败")
c.String(403, "签名验证失败")
return
}
// GET请求用于验证服务器配置
if c.Request.Method == "GET" {
c.String(200, echostr)
return
}
// POST请求处理消息
body, err := io.ReadAll(c.Request.Body)
if err != nil {
global.GVA_LOG.Error("读取请求体失败", zap.Error(err))
c.String(400, "读取请求体失败")
return
}
// 简化消息处理 - 暂时只记录日志
global.GVA_LOG.Info("收到微信消息", zap.String("body", string(body)))
// TODO: 实现完整的消息解析和处理
// 由于微信SDK API变化,暂时简化处理
c.String(200, "success")
}
// verifySignature 验证微信签名
func (w *WechatWebhookApi) verifySignature(signature, timestamp, nonce string) bool {
token := global.GVA_CONFIG.Wechat.OfficialToken
if token == "" {
global.GVA_LOG.Error("微信公众号Token未配置")
return false
}
// 将token、timestamp、nonce三个参数进行字典序排序
strs := []string{token, timestamp, nonce}
sort.Strings(strs)
// 将三个参数字符串拼接成一个字符串进行sha1加密
str := strings.Join(strs, "")
h := sha1.New()
h.Write([]byte(str))
encrypted := fmt.Sprintf("%x", h.Sum(nil))
// 将加密后的字符串与signature对比
return encrypted == signature
}
@@ -0,0 +1,13 @@
package config
// WechatConfig 微信配置结构 (简化版)
type WechatConfig struct {
// 微信小程序配置
MiniAppID string `mapstructure:"mini-app-id" json:"miniAppId" yaml:"mini-app-id"`
MiniAppSecret string `mapstructure:"mini-app-secret" json:"miniAppSecret" yaml:"mini-app-secret"`
// 微信公众号配置
OfficialAppID string `mapstructure:"official-app-id" json:"officialAppId" yaml:"official-app-id"`
OfficialAppSecret string `mapstructure:"official-app-secret" json:"officialAppSecret" yaml:"official-app-secret"`
OfficialToken string `mapstructure:"official-token" json:"officialToken" yaml:"official-token"`
}
@@ -0,0 +1,401 @@
package initialize
import (
"context"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/plugin-tool/utils"
)
// Api 初始化API权限
func Api(ctx context.Context) {
// 注册微信集成相关API
utils.RegisterApis(
// 小程序用户相关API
system.SysApi{
Path: "/wechat/mini/login",
Description: "小程序登录",
ApiGroup: "微信小程序",
Method: "POST",
},
system.SysApi{
Path: "/wechat/mini/userinfo",
Description: "获取小程序用户信息",
ApiGroup: "微信小程序",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mini/userinfo",
Description: "更新小程序用户信息",
ApiGroup: "微信小程序",
Method: "PUT",
},
system.SysApi{
Path: "/wechat/mini/bind-phone",
Description: "绑定手机号",
ApiGroup: "微信小程序",
Method: "POST",
},
system.SysApi{
Path: "/wechat/mini/users",
Description: "获取小程序用户列表",
ApiGroup: "微信小程序",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mini/check-unionid",
Description: "检查UnionID",
ApiGroup: "微信小程序",
Method: "GET",
},
// 公众号用户相关API
system.SysApi{
Path: "/wechat/mp/users",
Description: "获取公众号用户列表",
ApiGroup: "微信公众号",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mp/user",
Description: "获取公众号用户信息",
ApiGroup: "微信公众号",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mp/user/sync",
Description: "同步公众号用户信息",
ApiGroup: "微信公众号",
Method: "POST",
},
system.SysApi{
Path: "/wechat/mp/user/remark",
Description: "更新公众号用户备注",
ApiGroup: "微信公众号",
Method: "PUT",
},
// 公众号消息相关API
system.SysApi{
Path: "/wechat/mp/messages",
Description: "获取公众号消息列表",
ApiGroup: "微信公众号",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mp/message/text",
Description: "发送文本消息",
ApiGroup: "微信公众号",
Method: "POST",
},
system.SysApi{
Path: "/wechat/mp/message/image",
Description: "发送图片消息",
ApiGroup: "微信公众号",
Method: "POST",
},
system.SysApi{
Path: "/wechat/mp/message/:id",
Description: "获取消息详情",
ApiGroup: "微信公众号",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mp/message/:id",
Description: "删除消息",
ApiGroup: "微信公众号",
Method: "DELETE",
},
// 微信Webhook(公开接口)
system.SysApi{
Path: "/wechat/official/webhook",
Description: "微信公众号Webhook",
ApiGroup: "微信公众号",
Method: "GET",
},
system.SysApi{
Path: "/wechat/official/webhook",
Description: "微信公众号Webhook",
ApiGroup: "微信公众号",
Method: "POST",
},
// 小程序统计相关API
system.SysApi{
Path: "/wechat/mini/statistics",
Description: "获取小程序基础统计数据",
ApiGroup: "微信小程序",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mini/statistics/visit-trend",
Description: "获取小程序访问趋势数据",
ApiGroup: "微信小程序",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mini/statistics/source",
Description: "获取小程序用户来源分布数据",
ApiGroup: "微信小程序",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mini/statistics/page",
Description: "获取小程序页面访问数据",
ApiGroup: "微信小程序",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mini/statistics/retention",
Description: "获取小程序用户留存数据",
ApiGroup: "微信小程序",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mini/statistics/device",
Description: "获取小程序设备分布数据",
ApiGroup: "微信小程序",
Method: "GET",
},
// 公众号标签管理相关API
system.SysApi{
Path: "/wechat/mp/tag",
Description: "获取公众号标签列表",
ApiGroup: "微信公众号",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mp/tag/:id",
Description: "获取公众号标签详情",
ApiGroup: "微信公众号",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mp/tag",
Description: "创建公众号标签",
ApiGroup: "微信公众号",
Method: "POST",
},
system.SysApi{
Path: "/wechat/mp/tag",
Description: "更新公众号标签",
ApiGroup: "微信公众号",
Method: "PUT",
},
system.SysApi{
Path: "/wechat/mp/tag/:id",
Description: "删除公众号标签",
ApiGroup: "微信公众号",
Method: "DELETE",
},
system.SysApi{
Path: "/wechat/mp/tag/sync",
Description: "同步公众号标签",
ApiGroup: "微信公众号",
Method: "POST",
},
system.SysApi{
Path: "/wechat/mp/tag/list-all-simple",
Description: "获取公众号标签简单列表",
ApiGroup: "微信公众号",
Method: "GET",
},
// 公众号统计数据相关API
system.SysApi{
Path: "/wechat/mp/statistics",
Description: "获取公众号基础统计数据",
ApiGroup: "微信公众号",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mp/statistics/user-growth",
Description: "获取公众号用户增长数据",
ApiGroup: "微信公众号",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mp/statistics/message-type",
Description: "获取公众号消息类型分布数据",
ApiGroup: "微信公众号",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mp/statistics/message-trend",
Description: "获取公众号消息趋势数据",
ApiGroup: "微信公众号",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mp/statistics/region",
Description: "获取公众号地区分布数据",
ApiGroup: "微信公众号",
Method: "GET",
},
// 公众号图文发表记录相关API
system.SysApi{
Path: "/wechat/mp/news",
Description: "获取公众号图文发表记录列表",
ApiGroup: "微信公众号",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mp/news/:id",
Description: "获取公众号图文详情",
ApiGroup: "微信公众号",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mp/news",
Description: "创建公众号图文记录",
ApiGroup: "微信公众号",
Method: "POST",
},
system.SysApi{
Path: "/wechat/mp/news",
Description: "更新公众号图文记录",
ApiGroup: "微信公众号",
Method: "PUT",
},
system.SysApi{
Path: "/wechat/mp/news/:id",
Description: "删除公众号图文记录",
ApiGroup: "微信公众号",
Method: "DELETE",
},
system.SysApi{
Path: "/wechat/mp/news/:id/statistics",
Description: "获取公众号图文数据统计",
ApiGroup: "微信公众号",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mp/news/sync",
Description: "同步公众号图文发表记录",
ApiGroup: "微信公众号",
Method: "POST",
},
// 微信配置管理相关API
system.SysApi{
Path: "/wechat/mp/config",
Description: "获取微信配置",
ApiGroup: "微信公众号",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mp/config",
Description: "保存微信配置",
ApiGroup: "微信公众号",
Method: "POST",
},
system.SysApi{
Path: "/wechat/mp/config/test",
Description: "测试微信配置",
ApiGroup: "微信公众号",
Method: "POST",
},
system.SysApi{
Path: "/wechat/mp/config/validate",
Description: "验证微信配置",
ApiGroup: "微信公众号",
Method: "POST",
},
system.SysApi{
Path: "/wechat/mp/qrcode/generate",
Description: "生成公众号二维码",
ApiGroup: "微信公众号",
Method: "POST",
},
system.SysApi{
Path: "/wechat/mp/quota/clear",
Description: "清空API配额",
ApiGroup: "微信公众号",
Method: "POST",
},
system.SysApi{
Path: "/wechat/mp/config/list",
Description: "获取微信配置列表",
ApiGroup: "微信公众号",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mp/config/:id",
Description: "删除微信配置",
ApiGroup: "微信公众号",
Method: "DELETE",
},
system.SysApi{
Path: "/wechat/mp/webhook/logs",
Description: "获取Webhook日志",
ApiGroup: "微信公众号",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mp/webhook/logs/:id",
Description: "获取Webhook日志详情",
ApiGroup: "微信公众号",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mp/webhook/logs/clear",
Description: "清空Webhook日志",
ApiGroup: "微信公众号",
Method: "POST",
},
// 公众号草稿管理相关API
system.SysApi{
Path: "/wechat/mp/draft",
Description: "获取公众号草稿列表",
ApiGroup: "微信公众号",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mp/draft/:id",
Description: "获取公众号草稿详情",
ApiGroup: "微信公众号",
Method: "GET",
},
system.SysApi{
Path: "/wechat/mp/draft",
Description: "创建公众号草稿",
ApiGroup: "微信公众号",
Method: "POST",
},
system.SysApi{
Path: "/wechat/mp/draft",
Description: "更新公众号草稿",
ApiGroup: "微信公众号",
Method: "PUT",
},
system.SysApi{
Path: "/wechat/mp/draft/:id",
Description: "删除公众号草稿",
ApiGroup: "微信公众号",
Method: "DELETE",
},
system.SysApi{
Path: "/wechat/mp/draft/:id/publish",
Description: "发布公众号草稿",
ApiGroup: "微信公众号",
Method: "POST",
},
system.SysApi{
Path: "/wechat/mp/draft/sync",
Description: "同步公众号草稿",
ApiGroup: "微信公众号",
Method: "POST",
},
system.SysApi{
Path: "/wechat/mp/draft/count",
Description: "获取公众号草稿数量统计",
ApiGroup: "微信公众号",
Method: "GET",
},
)
global.GVA_LOG.Info("微信集成API权限注册完成")
}
@@ -0,0 +1,32 @@
package initialize
import (
"context"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model"
)
// Gorm 初始化数据库表
func Gorm(ctx context.Context) {
// 自动迁移数据库表
err := global.GVA_DB.WithContext(ctx).AutoMigrate(
&model.WechatMiniUser{},
&model.MpUser{},
&model.MpMessage{},
&model.MpMenu{},
&model.MpAutoReply{},
&model.MpMaterial{},
&model.MpNews{},
&model.MpConfig{},
&model.MpWebhookLog{},
&model.MpDraft{},
)
if err != nil {
global.GVA_LOG.Error("微信集成插件数据库表迁移失败: " + err.Error())
return
}
global.GVA_LOG.Info("微信集成插件数据库表迁移完成")
}
@@ -0,0 +1,26 @@
package initialize
import (
"github.com/flipped-aurora/gin-vue-admin/server/middleware"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/router"
"github.com/gin-gonic/gin"
)
// Router 初始化路由
func Router(engine *gin.Engine) {
// 获取路由组,微信插件使用/api前缀与前端匹配
publicGroup := engine.Group("")
privateGroup := engine.Group("")
// 添加JWT认证和Casbin权限中间件
privateGroup.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler())
// 初始化微信路由
wechatRouter := router.WechatRouter{}
// 公开路由(不需要认证)
wechatRouter.InitWechatPublicRouter(publicGroup)
// 私有路由(需要认证)
wechatRouter.InitWechatRouter(privateGroup)
}
@@ -0,0 +1,35 @@
package initialize
import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/config"
)
// Viper 初始化配置
func Viper() {
// 微信配置已经在主配置文件中定义,这里只需要验证
if global.GVA_CONFIG.Wechat.MiniAppID == "" {
global.GVA_LOG.Warn("微信小程序 AppID 未配置")
}
if global.GVA_CONFIG.Wechat.OfficialAppID == "" {
global.GVA_LOG.Warn("微信公众号 AppID 未配置")
}
if global.GVA_CONFIG.Wechat.OfficialToken == "" {
global.GVA_LOG.Warn("微信公众号 Token 未配置")
}
global.GVA_LOG.Info("微信集成插件配置验证完成")
}
// GetWechatConfig 获取微信配置
func GetWechatConfig() config.WechatConfig {
return config.WechatConfig{
MiniAppID: global.GVA_CONFIG.Wechat.MiniAppID,
MiniAppSecret: global.GVA_CONFIG.Wechat.MiniAppSecret,
OfficialAppID: global.GVA_CONFIG.Wechat.OfficialAppID,
OfficialAppSecret: global.GVA_CONFIG.Wechat.OfficialAppSecret,
OfficialToken: global.GVA_CONFIG.Wechat.OfficialToken,
}
}
@@ -0,0 +1,68 @@
package model
import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
)
// MpAutoReply 微信公众号自动回复表
type MpAutoReply struct {
global.GVA_MODEL
Type int `json:"type" gorm:"not null;index;comment:回复类型 1关注回复 2消息回复 3关键字回复"`
RequestKeyword *string `json:"requestKeyword" gorm:"type:varchar(255);index;comment:请求的关键字"`
RequestMatch *int `json:"requestMatch" gorm:"comment:请求的关键字的匹配 1完全匹配 2半匹配"`
RequestMessageType *string `json:"requestMessageType" gorm:"type:varchar(32);index;comment:请求的消息类型"`
ResponseMessageType string `json:"responseMessageType" gorm:"type:varchar(32);not null;comment:回复的消息类型"`
ResponseContent *string `json:"responseContent" gorm:"type:varchar(1024);comment:回复的消息内容"`
ResponseMediaID *string `json:"responseMediaId" gorm:"type:varchar(128);comment:回复的媒体文件 id"`
ResponseMediaURL *string `json:"responseMediaUrl" gorm:"type:varchar(1024);comment:回复的媒体文件 URL"`
ResponseTitle *string `json:"responseTitle" gorm:"type:varchar(128);comment:回复的标题"`
ResponseDescription *string `json:"responseDescription" gorm:"type:varchar(256);comment:回复的描述"`
ResponseThumbMediaID *string `json:"responseThumbMediaId" gorm:"type:varchar(128);comment:回复的缩略图的媒体 id"`
ResponseThumbMediaURL *string `json:"responseThumbMediaUrl" gorm:"type:varchar(1024);comment:回复的缩略图的媒体 URL"`
ResponseArticles *string `json:"responseArticles" gorm:"type:varchar(1024);comment:回复的图文消息数组"`
ResponseMusicURL *string `json:"responseMusicUrl" gorm:"type:varchar(1024);comment:回复的音乐链接"`
ResponseHqMusicURL *string `json:"responseHqMusicUrl" gorm:"type:varchar(1024);comment:回复的高质量音乐链接"`
}
// TableName 指定表名
func (MpAutoReply) TableName() string {
return "mp_auto_reply"
}
// AutoReplyType 自动回复类型常量
const (
AutoReplyTypeSubscribe = 1 // 关注回复
AutoReplyTypeMessage = 2 // 消息回复
AutoReplyTypeKeyword = 3 // 关键字回复
)
// MatchType 匹配类型常量
const (
MatchTypeExact = 1 // 完全匹配
MatchTypePartial = 2 // 半匹配
)
// IsSubscribeReply 是否为关注回复
func (m *MpAutoReply) IsSubscribeReply() bool {
return m.Type == AutoReplyTypeSubscribe
}
// IsMessageReply 是否为消息回复
func (m *MpAutoReply) IsMessageReply() bool {
return m.Type == AutoReplyTypeMessage
}
// IsKeywordReply 是否为关键字回复
func (m *MpAutoReply) IsKeywordReply() bool {
return m.Type == AutoReplyTypeKeyword
}
// IsExactMatch 是否为完全匹配
func (m *MpAutoReply) IsExactMatch() bool {
return m.RequestMatch != nil && *m.RequestMatch == MatchTypeExact
}
// IsPartialMatch 是否为部分匹配
func (m *MpAutoReply) IsPartialMatch() bool {
return m.RequestMatch != nil && *m.RequestMatch == MatchTypePartial
}
@@ -0,0 +1,125 @@
package model
import (
"time"
"github.com/flipped-aurora/gin-vue-admin/server/global"
)
// MpConfig 微信配置表
type MpConfig struct {
global.GVA_MODEL
ConfigType string `json:"configType" gorm:"type:varchar(32);not null;index;comment:配置类型 mp公众号 mini小程序"`
AppID string `json:"appId" gorm:"type:varchar(128);not null;comment:应用ID"`
AppSecret string `json:"appSecret" gorm:"type:varchar(256);not null;comment:应用密钥"`
Token *string `json:"token" gorm:"type:varchar(128);comment:Token"`
EncodingAESKey *string `json:"encodingAESKey" gorm:"type:varchar(256);comment:消息加解密密钥"`
// 公众号特有配置
ServerURL *string `json:"serverUrl" gorm:"type:varchar(512);comment:服务器地址"`
WebhookURL *string `json:"webhookUrl" gorm:"type:varchar(512);comment:Webhook地址"`
// 小程序特有配置
MiniAppName *string `json:"miniAppName" gorm:"type:varchar(128);comment:小程序名称"`
MiniAppDesc *string `json:"miniAppDesc" gorm:"type:varchar(512);comment:小程序描述"`
// 状态信息
Status string `json:"status" gorm:"type:varchar(32);default:'inactive';comment:状态 active激活 inactive未激活"`
LastTestTime *time.Time `json:"lastTestTime" gorm:"comment:最后测试时间"`
TestResult *string `json:"testResult" gorm:"type:varchar(512);comment:测试结果"`
// 扩展配置
ExtraConfig *string `json:"extraConfig" gorm:"type:text;comment:扩展配置JSON"`
Remark *string `json:"remark" gorm:"type:varchar(512);comment:备注"`
}
// TableName 指定表名
func (MpConfig) TableName() string {
return "mp_config"
}
// ConfigType 配置类型常量
const (
ConfigTypeMP = "mp" // 公众号
ConfigTypeMini = "mini" // 小程序
)
// Status 状态常量
const (
ConfigStatusActive = "active" // 激活
ConfigStatusInactive = "inactive" // 未激活
ConfigStatusError = "error" // 错误
)
// IsMP 是否为公众号配置
func (c *MpConfig) IsMP() bool {
return c.ConfigType == ConfigTypeMP
}
// IsMini 是否为小程序配置
func (c *MpConfig) IsMini() bool {
return c.ConfigType == ConfigTypeMini
}
// IsActive 是否激活状态
func (c *MpConfig) IsActive() bool {
return c.Status == ConfigStatusActive
}
// IsInactive 是否未激活状态
func (c *MpConfig) IsInactive() bool {
return c.Status == ConfigStatusInactive
}
// IsError 是否错误状态
func (c *MpConfig) IsError() bool {
return c.Status == ConfigStatusError
}
// GetDisplayName 获取显示名称
func (c *MpConfig) GetDisplayName() string {
if c.IsMP() {
return "微信公众号"
} else if c.IsMini() {
if c.MiniAppName != nil && *c.MiniAppName != "" {
return *c.MiniAppName
}
return "微信小程序"
}
return "未知类型"
}
// MpWebhookLog 微信Webhook日志表
type MpWebhookLog struct {
global.GVA_MODEL
ConfigID uint `json:"configId" gorm:"not null;index;comment:配置ID"`
RequestURL string `json:"requestUrl" gorm:"type:varchar(512);not null;comment:请求URL"`
RequestMethod string `json:"requestMethod" gorm:"type:varchar(16);not null;comment:请求方法"`
RequestHeaders *string `json:"requestHeaders" gorm:"type:text;comment:请求头"`
RequestBody *string `json:"requestBody" gorm:"type:text;comment:请求体"`
ResponseStatus int `json:"responseStatus" gorm:"not null;comment:响应状态码"`
ResponseBody *string `json:"responseBody" gorm:"type:text;comment:响应体"`
ProcessTime int `json:"processTime" gorm:"not null;comment:处理时间(毫秒)"`
ErrorMessage *string `json:"errorMessage" gorm:"type:varchar(512);comment:错误信息"`
// 微信相关字段
Signature *string `json:"signature" gorm:"type:varchar(128);comment:微信签名"`
Timestamp *string `json:"timestamp" gorm:"type:varchar(32);comment:时间戳"`
Nonce *string `json:"nonce" gorm:"type:varchar(32);comment:随机数"`
Echostr *string `json:"echostr" gorm:"type:varchar(128);comment:随机字符串"`
}
// TableName 指定表名
func (MpWebhookLog) TableName() string {
return "mp_webhook_log"
}
// IsSuccess 是否成功
func (l *MpWebhookLog) IsSuccess() bool {
return l.ResponseStatus >= 200 && l.ResponseStatus < 300
}
// IsError 是否错误
func (l *MpWebhookLog) IsError() bool {
return l.ResponseStatus >= 400 || l.ErrorMessage != nil
}
@@ -0,0 +1,95 @@
package model
import (
"time"
"github.com/flipped-aurora/gin-vue-admin/server/global"
)
// MpDraft 微信公众号草稿表
type MpDraft struct {
global.GVA_MODEL
MediaID string `json:"mediaId" gorm:"type:varchar(128);not null;index;comment:草稿的media_id"`
Title string `json:"title" gorm:"type:varchar(255);not null;comment:图文消息标题"`
Author *string `json:"author" gorm:"type:varchar(100);comment:作者"`
Digest *string `json:"digest" gorm:"type:varchar(500);comment:图文消息摘要"`
Content *string `json:"content" gorm:"type:longtext;comment:图文消息内容"`
ContentSourceUrl *string `json:"contentSourceUrl" gorm:"type:varchar(1024);comment:原文地址"`
ThumbMediaID *string `json:"thumbMediaId" gorm:"type:varchar(128);comment:缩略图的media_id"`
ThumbUrl *string `json:"thumbUrl" gorm:"type:varchar(1024);comment:缩略图URL"`
ShowCoverPic int `json:"showCoverPic" gorm:"default:1;comment:是否显示封面 0不显示 1显示"`
NeedOpenComment int `json:"needOpenComment" gorm:"default:0;comment:是否打开评论 0不打开 1打开"`
OnlyFansCanComment int `json:"onlyFansCanComment" gorm:"default:0;comment:是否粉丝才可评论 0所有人 1粉丝"`
// 草稿状态
Status string `json:"status" gorm:"type:varchar(32);default:'draft';comment:状态 draft草稿 published已发布"`
UpdateTime *time.Time `json:"updateTime" gorm:"comment:更新时间"`
// 微信相关
WechatMediaID *string `json:"wechatMediaId" gorm:"type:varchar(128);comment:微信草稿media_id"`
WechatCreateTime *time.Time `json:"wechatCreateTime" gorm:"comment:微信创建时间"`
WechatUpdateTime *time.Time `json:"wechatUpdateTime" gorm:"comment:微信更新时间"`
}
// TableName 指定表名
func (MpDraft) TableName() string {
return "mp_draft"
}
// DraftStatus 草稿状态常量
const (
DraftStatusDraft = "draft" // 草稿
DraftStatusPublished = "published" // 已发布
DraftStatusDeleted = "deleted" // 已删除
)
// IsDraft 是否为草稿状态
func (d *MpDraft) IsDraft() bool {
return d.Status == DraftStatusDraft
}
// IsPublished 是否已发布
func (d *MpDraft) IsPublished() bool {
return d.Status == DraftStatusPublished
}
// IsDeleted 是否已删除
func (d *MpDraft) IsDeleted() bool {
return d.Status == DraftStatusDeleted
}
// GetDisplayTitle 获取显示标题
func (d *MpDraft) GetDisplayTitle() string {
if d.Title == "" {
return "无标题"
}
return d.Title
}
// GetDisplayAuthor 获取显示作者
func (d *MpDraft) GetDisplayAuthor() string {
if d.Author == nil || *d.Author == "" {
return "未知作者"
}
return *d.Author
}
// HasThumb 是否有缩略图
func (d *MpDraft) HasThumb() bool {
return d.ThumbMediaID != nil && *d.ThumbMediaID != ""
}
// IsShowCover 是否显示封面
func (d *MpDraft) IsShowCover() bool {
return d.ShowCoverPic == 1
}
// IsOpenComment 是否打开评论
func (d *MpDraft) IsOpenComment() bool {
return d.NeedOpenComment == 1
}
// IsOnlyFansComment 是否仅粉丝可评论
func (d *MpDraft) IsOnlyFansComment() bool {
return d.OnlyFansCanComment == 1
}
@@ -0,0 +1,67 @@
package model
import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
)
// MpMaterial 微信公众号素材表
type MpMaterial struct {
global.GVA_MODEL
MediaID string `json:"mediaId" gorm:"type:varchar(128);not null;index;comment:公众号素材 id"`
Type string `json:"type" gorm:"type:varchar(32);not null;index;comment:文件类型 image图片 voice语音 video视频 thumb缩略图"`
Permanent bool `json:"permanent" gorm:"not null;default:false;index;comment:是否永久素材"`
URL *string `json:"url" gorm:"type:varchar(1024);comment:文件服务器的 URL"`
Name *string `json:"name" gorm:"type:varchar(255);comment:素材名字"`
MpURL *string `json:"mpUrl" gorm:"type:varchar(1024);comment:公众号文件 URL"`
Title *string `json:"title" gorm:"type:varchar(255);comment:视频素材的标题"`
Introduction *string `json:"introduction" gorm:"type:varchar(255);comment:视频素材的描述"`
}
// TableName 指定表名
func (MpMaterial) TableName() string {
return "mp_material"
}
// MaterialType 素材类型常量
const (
MaterialTypeImage = "image" // 图片
MaterialTypeVoice = "voice" // 语音
MaterialTypeVideo = "video" // 视频
MaterialTypeThumb = "thumb" // 缩略图
MaterialTypeNews = "news" // 图文消息
)
// IsImage 是否为图片素材
func (m *MpMaterial) IsImage() bool {
return m.Type == MaterialTypeImage
}
// IsVoice 是否为语音素材
func (m *MpMaterial) IsVoice() bool {
return m.Type == MaterialTypeVoice
}
// IsVideo 是否为视频素材
func (m *MpMaterial) IsVideo() bool {
return m.Type == MaterialTypeVideo
}
// IsThumb 是否为缩略图素材
func (m *MpMaterial) IsThumb() bool {
return m.Type == MaterialTypeThumb
}
// IsNews 是否为图文消息素材
func (m *MpMaterial) IsNews() bool {
return m.Type == MaterialTypeNews
}
// IsPermanent 是否为永久素材
func (m *MpMaterial) IsPermanent() bool {
return m.Permanent
}
// IsTemporary 是否为临时素材
func (m *MpMaterial) IsTemporary() bool {
return !m.Permanent
}
@@ -0,0 +1,57 @@
package model
import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
)
// MpMenu 微信公众号菜单表
type MpMenu struct {
global.GVA_MODEL
Name *string `json:"name" gorm:"type:varchar(255);comment:菜单名称"`
MenuKey *string `json:"menuKey" gorm:"type:varchar(255);comment:菜单标识"`
ParentID *string `json:"parentId" gorm:"type:varchar(32);index;comment:父ID"`
Type string `json:"type" gorm:"type:varchar(32);not null;default:'';index;comment:按钮类型"`
URL *string `json:"url" gorm:"type:varchar(500);comment:网页链接"`
MiniProgramAppID *string `json:"miniProgramAppId" gorm:"type:varchar(32);comment:小程序appid"`
MiniProgramPagePath *string `json:"miniProgramPagePath" gorm:"type:varchar(200);comment:小程序页面路径"`
ReplyMessageType *string `json:"replyMessageType" gorm:"type:varchar(32);comment:消息类型"`
ReplyContent *string `json:"replyContent" gorm:"type:varchar(1024);comment:回复的消息内容"`
}
// TableName 指定表名
func (MpMenu) TableName() string {
return "mp_menu"
}
// MenuType 菜单类型常量
const (
MenuTypeClick = "click" // 点击推事件
MenuTypeView = "view" // 跳转URL
MenuTypeScanCodePush = "scancode_push" // 扫码推事件
MenuTypeScanCodeWaitMsg = "scancode_waitmsg" // 扫码推事件且弹出"消息接收中"提示框
MenuTypePicSysPhoto = "pic_sysphoto" // 弹出系统拍照发图
MenuTypePicPhotoOrAlbum = "pic_photo_or_album" // 弹出拍照或者相册发图
MenuTypePicWeixin = "pic_weixin" // 弹出微信相册发图器
MenuTypeLocationSelect = "location_select" // 弹出地理位置选择器
MenuTypeMiniProgram = "miniprogram" // 小程序
)
// IsClickType 是否为点击类型
func (m *MpMenu) IsClickType() bool {
return m.Type == MenuTypeClick
}
// IsViewType 是否为跳转类型
func (m *MpMenu) IsViewType() bool {
return m.Type == MenuTypeView
}
// IsMiniProgramType 是否为小程序类型
func (m *MpMenu) IsMiniProgramType() bool {
return m.Type == MenuTypeMiniProgram
}
// HasParent 是否有父菜单
func (m *MpMenu) HasParent() bool {
return m.ParentID != nil && *m.ParentID != ""
}
@@ -0,0 +1,42 @@
package model
import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
)
// MpMessage 微信公众号消息表
type MpMessage struct {
global.GVA_MODEL
Type string `json:"type" gorm:"type:varchar(32);not null;index;comment:消息类型"`
OpenID string `json:"openid" gorm:"type:varchar(100);not null;index;comment:用户openid"`
InMsgID *string `json:"inMsgId" gorm:"type:varchar(64);comment:微信消息id"`
Content *string `json:"content" gorm:"type:text;comment:消息内容"`
}
// TableName 指定表名
func (MpMessage) TableName() string {
return "mp_message"
}
// MessageType 消息类型常量
const (
MessageTypeText = "text" // 文本消息
MessageTypeImage = "image" // 图片消息
MessageTypeVoice = "voice" // 语音消息
MessageTypeVideo = "video" // 视频消息
MessageTypeMusic = "music" // 音乐消息
MessageTypeNews = "news" // 图文消息
MessageTypeLocation = "location" // 地理位置消息
MessageTypeLink = "link" // 链接消息
MessageTypeEvent = "event" // 事件消息
)
// IsTextMessage 是否为文本消息
func (m *MpMessage) IsTextMessage() bool {
return m.Type == MessageTypeText
}
// IsEventMessage 是否为事件消息
func (m *MpMessage) IsEventMessage() bool {
return m.Type == MessageTypeEvent
}
@@ -0,0 +1,90 @@
package model
import (
"time"
"github.com/flipped-aurora/gin-vue-admin/server/global"
)
// MpNews 微信公众号图文发表记录表
type MpNews struct {
global.GVA_MODEL
MediaID string `json:"mediaId" gorm:"type:varchar(128);not null;index;comment:图文消息的media_id"`
Title string `json:"title" gorm:"type:varchar(255);not null;comment:图文消息标题"`
Author *string `json:"author" gorm:"type:varchar(100);comment:作者"`
Digest *string `json:"digest" gorm:"type:varchar(500);comment:图文消息摘要"`
Content *string `json:"content" gorm:"type:longtext;comment:图文消息内容"`
ContentURL *string `json:"contentUrl" gorm:"type:varchar(1024);comment:图文页的URL"`
SourceURL *string `json:"sourceUrl" gorm:"type:varchar(1024);comment:原文地址"`
ThumbMediaID *string `json:"thumbMediaId" gorm:"type:varchar(128);comment:缩略图的media_id"`
ThumbURL *string `json:"thumbUrl" gorm:"type:varchar(1024);comment:缩略图URL"`
ShowCover bool `json:"showCover" gorm:"default:true;comment:是否显示封面"`
NeedOpenComment bool `json:"needOpenComment" gorm:"default:false;comment:是否打开评论"`
OnlyFansCanComment bool `json:"onlyFansCanComment" gorm:"default:false;comment:是否粉丝才可评论"`
// 发布信息
PublishTime *time.Time `json:"publishTime" gorm:"comment:发布时间"`
PublishStatus string `json:"publishStatus" gorm:"type:varchar(32);default:'draft';comment:发布状态 draft草稿 published已发布"`
// 统计数据
ReadNum int `json:"readNum" gorm:"default:0;comment:阅读数"`
LikeNum int `json:"likeNum" gorm:"default:0;comment:点赞数"`
CommentNum int `json:"commentNum" gorm:"default:0;comment:评论数"`
ShareNum int `json:"shareNum" gorm:"default:0;comment:分享数"`
// 微信相关
WechatURL *string `json:"wechatUrl" gorm:"type:varchar(1024);comment:微信图文链接"`
WechatMsgID *string `json:"wechatMsgId" gorm:"type:varchar(128);comment:微信消息ID"`
}
// TableName 指定表名
func (MpNews) TableName() string {
return "mp_news"
}
// PublishStatus 发布状态常量
const (
NewsStatusDraft = "draft" // 草稿
NewsStatusPublished = "published" // 已发布
NewsStatusDeleted = "deleted" // 已删除
)
// IsDraft 是否为草稿状态
func (n *MpNews) IsDraft() bool {
return n.PublishStatus == NewsStatusDraft
}
// IsPublished 是否已发布
func (n *MpNews) IsPublished() bool {
return n.PublishStatus == NewsStatusPublished
}
// IsDeleted 是否已删除
func (n *MpNews) IsDeleted() bool {
return n.PublishStatus == NewsStatusDeleted
}
// GetReadCount 获取阅读数
func (n *MpNews) GetReadCount() int {
return n.ReadNum
}
// GetLikeCount 获取点赞数
func (n *MpNews) GetLikeCount() int {
return n.LikeNum
}
// GetCommentCount 获取评论数
func (n *MpNews) GetCommentCount() int {
return n.CommentNum
}
// GetShareCount 获取分享数
func (n *MpNews) GetShareCount() int {
return n.ShareNum
}
// GetTotalInteraction 获取总互动数
func (n *MpNews) GetTotalInteraction() int {
return n.ReadNum + n.LikeNum + n.CommentNum + n.ShareNum
}
@@ -0,0 +1,61 @@
package model
import (
"time"
"github.com/flipped-aurora/gin-vue-admin/server/global"
)
// MpUser 微信公众号粉丝表 (增加用户关联字段)
type MpUser struct {
global.GVA_MODEL
OpenID string `json:"openid" gorm:"type:varchar(100);not null;uniqueIndex;comment:用户openid"`
UnionID *string `json:"unionid" gorm:"type:varchar(64);index;comment:微信开放平台统一标识"`
UserID *uint `json:"userId" gorm:"index;comment:关联系统用户ID"`
Nickname *string `json:"nickname" gorm:"type:varchar(50);comment:用户昵称"`
HeadImageURL *string `json:"headImageUrl" gorm:"type:varchar(1024);comment:用户头像"`
SubscribeStatus int `json:"subscribeStatus" gorm:"not null;index;comment:关注状态 0未关注 1已关注"`
SubscribeTime *time.Time `json:"subscribeTime" gorm:"index;comment:关注时间"`
UnsubscribeTime *time.Time `json:"unsubscribeTime" gorm:"comment:取消关注时间"`
Language *string `json:"language" gorm:"type:varchar(30);comment:用户语言"`
Country *string `json:"country" gorm:"type:varchar(30);comment:用户国家"`
Province *string `json:"province" gorm:"type:varchar(30);comment:用户省份"`
City *string `json:"city" gorm:"type:varchar(30);comment:用户城市"`
Remark *string `json:"remark" gorm:"type:varchar(255);comment:备注"`
TagIDs *string `json:"tagIds" gorm:"type:varchar(255);comment:标签编号数组"`
}
// TableName 指定表名
func (MpUser) TableName() string {
return "mp_user"
}
// IsSubscribed 检查是否已关注
func (m *MpUser) IsSubscribed() bool {
return m.SubscribeStatus == 1
}
// Subscribe 关注操作
func (m *MpUser) Subscribe() {
m.SubscribeStatus = 1
now := time.Now()
m.SubscribeTime = &now
m.UnsubscribeTime = nil
}
// Unsubscribe 取消关注操作
func (m *MpUser) Unsubscribe() {
m.SubscribeStatus = 0
now := time.Now()
m.UnsubscribeTime = &now
}
// IsLinkedToSystemUser 检查是否已关联系统用户
func (m *MpUser) IsLinkedToSystemUser() bool {
return m.UserID != nil && *m.UserID > 0
}
// HasUnionID 检查是否有 UnionID
func (m *MpUser) HasUnionID() bool {
return m.UnionID != nil && *m.UnionID != ""
}
@@ -0,0 +1,27 @@
package request
// MiniStatisticsRequest 小程序统计请求
type MiniStatisticsRequest struct {
StartDate string `json:"startDate" form:"startDate"` // 开始日期
EndDate string `json:"endDate" form:"endDate"` // 结束日期
Period string `json:"period" form:"period"` // 统计周期 day/week/month
}
// VisitTrendRequest 访问趋势请求
type VisitTrendRequest struct {
StartDate string `json:"startDate" form:"startDate" binding:"required"` // 开始日期
EndDate string `json:"endDate" form:"endDate" binding:"required"` // 结束日期
}
// PageAnalysisRequest 页面分析请求
type PageAnalysisRequest struct {
StartDate string `json:"startDate" form:"startDate"` // 开始日期
EndDate string `json:"endDate" form:"endDate"` // 结束日期
PagePath string `json:"pagePath" form:"pagePath"` // 页面路径
}
// UserRetentionRequest 用户留存请求
type UserRetentionRequest struct {
StartDate string `json:"startDate" form:"startDate" binding:"required"` // 开始日期
EndDate string `json:"endDate" form:"endDate" binding:"required"` // 结束日期
}
@@ -0,0 +1,69 @@
package request
// CreateAutoReplyRequest 创建自动回复请求
type CreateAutoReplyRequest struct {
Type int `json:"type" binding:"required" example:"1"`
RequestKeyword string `json:"requestKeyword" example:"你好"`
RequestMatch int `json:"requestMatch" example:"1"`
RequestMessageType string `json:"requestMessageType" example:"text"`
ResponseMessageType string `json:"responseMessageType" binding:"required" example:"text"`
ResponseContent string `json:"responseContent" example:"欢迎关注我们的公众号"`
ResponseMediaID string `json:"responseMediaId" example:""`
ResponseMediaURL string `json:"responseMediaUrl" example:""`
ResponseTitle string `json:"responseTitle" example:""`
ResponseDescription string `json:"responseDescription" example:""`
ResponseThumbMediaID string `json:"responseThumbMediaId" example:""`
ResponseThumbMediaURL string `json:"responseThumbMediaUrl" example:""`
ResponseArticles string `json:"responseArticles" example:""`
ResponseMusicURL string `json:"responseMusicUrl" example:""`
ResponseHqMusicURL string `json:"responseHqMusicUrl" example:""`
}
// UpdateAutoReplyRequest 更新自动回复请求
type UpdateAutoReplyRequest struct {
ID uint `json:"id" binding:"required"`
Type int `json:"type"`
RequestKeyword string `json:"requestKeyword"`
RequestMatch int `json:"requestMatch"`
RequestMessageType string `json:"requestMessageType"`
ResponseMessageType string `json:"responseMessageType"`
ResponseContent string `json:"responseContent"`
ResponseMediaID string `json:"responseMediaId"`
ResponseMediaURL string `json:"responseMediaUrl"`
ResponseTitle string `json:"responseTitle"`
ResponseDescription string `json:"responseDescription"`
ResponseThumbMediaID string `json:"responseThumbMediaId"`
ResponseThumbMediaURL string `json:"responseThumbMediaUrl"`
ResponseArticles string `json:"responseArticles"`
ResponseMusicURL string `json:"responseMusicUrl"`
ResponseHqMusicURL string `json:"responseHqMusicUrl"`
}
// TestAutoReplyRequest 测试自动回复请求
type TestAutoReplyRequest struct {
Keyword string `json:"keyword" binding:"required" example:"你好"`
}
// PageInfo 分页信息
type PageInfo struct {
Page int `json:"page" form:"page"` // 页码
PageSize int `json:"pageSize" form:"pageSize"` // 每页数量
}
// AutoReplyPageRequest 自动回复分页请求
type AutoReplyPageRequest struct {
PageInfo
Type *int `json:"type" form:"type"`
}
// SetSubscribeReplyRequest 设置关注回复请求
type SetSubscribeReplyRequest struct {
Content string `json:"content" binding:"required" example:"感谢您的关注!"`
}
// SetKeywordReplyRequest 设置关键字回复请求
type SetKeywordReplyRequest struct {
Keyword string `json:"keyword" binding:"required" example:"你好"`
Content string `json:"content" binding:"required" example:"您好,欢迎咨询!"`
MatchType int `json:"matchType" binding:"required" example:"1"`
}
@@ -0,0 +1,47 @@
package request
// CreateMenuRequest 创建菜单请求
type CreateMenuRequest struct {
Name string `json:"name" binding:"required" example:"菜单名称"`
Type string `json:"type" binding:"required" example:"click"`
ParentID string `json:"parentId" example:"1"`
MenuKey string `json:"menuKey" example:"MENU_KEY_1"`
URL string `json:"url" example:"https://example.com"`
MiniProgramAppID string `json:"miniProgramAppId" example:"wx1234567890"`
MiniProgramPagePath string `json:"miniProgramPagePath" example:"pages/index/index"`
ReplyMessageType string `json:"replyMessageType" example:"text"`
ReplyContent string `json:"replyContent" example:"欢迎使用"`
}
// UpdateMenuRequest 更新菜单请求
type UpdateMenuRequest struct {
ID uint `json:"id" binding:"required"`
Name string `json:"name"`
Type string `json:"type"`
ParentID string `json:"parentId"`
MenuKey string `json:"menuKey"`
URL string `json:"url"`
MiniProgramAppID string `json:"miniProgramAppId"`
MiniProgramPagePath string `json:"miniProgramPagePath"`
ReplyMessageType string `json:"replyMessageType"`
ReplyContent string `json:"replyContent"`
}
// MenuTreeNode 菜单树节点
type MenuTreeNode struct {
ID uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
MenuKey string `json:"menuKey"`
URL string `json:"url"`
MiniProgramAppID string `json:"miniProgramAppId"`
MiniProgramPagePath string `json:"miniProgramPagePath"`
ReplyMessageType string `json:"replyMessageType"`
ReplyContent string `json:"replyContent"`
Children []MenuTreeNode `json:"children"`
}
// SaveMenuRequest 保存菜单请求(批量保存)
type SaveMenuRequest struct {
Menus []CreateMenuRequest `json:"menus" binding:"required"`
}
@@ -0,0 +1,41 @@
package request
// MiniLoginRequest 小程序登录请求
type MiniLoginRequest struct {
Code string `json:"code" binding:"required"` // 微信登录凭证
}
// UpdateUserInfoRequest 更新用户信息请求
type UpdateUserInfoRequest struct {
OpenID string `json:"openid" binding:"required"` // 用户openid
UserInfo map[string]interface{} `json:"userInfo"` // 用户信息
}
// BindPhoneRequest 绑定手机号请求
type BindPhoneRequest struct {
OpenID string `json:"openid" binding:"required"` // 用户openid
Phone string `json:"phone" binding:"required"` // 手机号
}
// SyncUserRequest 同步用户请求
type SyncUserRequest struct {
OpenID string `json:"openid" binding:"required"` // 用户openid
}
// UpdateRemarkRequest 更新备注请求
type UpdateRemarkRequest struct {
OpenID string `json:"openid" binding:"required"` // 用户openid
Remark string `json:"remark"` // 备注内容
}
// SendTextMessageRequest 发送文本消息请求
type SendTextMessageRequest struct {
OpenID string `json:"openid" binding:"required"` // 用户openid
Content string `json:"content" binding:"required"` // 消息内容
}
// SendImageMessageRequest 发送图片消息请求
type SendImageMessageRequest struct {
OpenID string `json:"openid" binding:"required"` // 用户openid
MediaID string `json:"mediaId" binding:"required"` // 媒体ID
}
@@ -0,0 +1,65 @@
package response
// MiniStatisticsResponse 小程序基础统计响应
type MiniStatisticsResponse struct {
TotalUsers int `json:"totalUsers"` // 总用户数
ActiveUsers int `json:"activeUsers"` // 活跃用户数
TotalSessions int `json:"totalSessions"` // 总会话数
TodaySessions int `json:"todaySessions"` // 今日会话数
TotalPV int `json:"totalPV"` // 总页面访问量
TotalUV int `json:"totalUV"` // 总独立访客数
AvgStayTime int `json:"avgStayTime"` // 平均停留时间(秒)
BounceRate float64 `json:"bounceRate"` // 跳出率
}
// VisitTrendData 访问趋势数据
type VisitTrendData struct {
Date string `json:"date"` // 日期
SessionCnt int `json:"sessionCnt"` // 会话数
VisitPV int `json:"visitPV"` // 页面访问量
VisitUV int `json:"visitUV"` // 独立访客数
VisitUVNew int `json:"visitUVNew"` // 新访客数
StayTimeUV int `json:"stayTimeUV"` // 人均停留时长
StayTimeSession int `json:"stayTimeSession"` // 次均停留时长
}
// SourceData 用户来源数据
type SourceData struct {
RefType string `json:"refType"` // 来源类型
Name string `json:"name"` // 来源名称
SessionCnt int `json:"sessionCnt"` // 会话数
VisitPV int `json:"visitPV"` // 页面访问量
VisitUV int `json:"visitUV"` // 独立访客数
Percentage float64 `json:"percentage"` // 占比
}
// PageData 页面访问数据
type PageData struct {
PagePath string `json:"pagePath"` // 页面路径
PageName string `json:"pageName"` // 页面名称
PagePV int `json:"pagePV"` // 页面访问量
PageUV int `json:"pageUV"` // 页面独立访客数
StayTimePV int `json:"stayTimePV"` // 平均停留时长
EntryPagePV int `json:"entryPagePV"` // 入口页次数
ExitPagePV int `json:"exitPagePV"` // 退出页次数
SharePV int `json:"sharePV"` // 转发次数
ShareUV int `json:"shareUV"` // 转发人数
}
// RetentionData 用户留存数据
type RetentionData struct {
RefDate string `json:"refDate"` // 日期
VisitUVNew int `json:"visitUVNew"` // 新增用户数
VisitUV int `json:"visitUV"` // 活跃用户数
Day1 float64 `json:"day1"` // 1日留存率
Day7 float64 `json:"day7"` // 7日留存率
Day30 float64 `json:"day30"` // 30日留存率
}
// DeviceData 设备数据
type DeviceData struct {
DeviceType string `json:"deviceType"` // 设备类型
SessionCnt int `json:"sessionCnt"` // 会话数
VisitUV int `json:"visitUV"` // 独立访客数
Percentage float64 `json:"percentage"` // 占比
}
@@ -0,0 +1,16 @@
package response
// MiniLoginResponse 小程序登录响应
type MiniLoginResponse struct {
OpenID string `json:"openid"` // 用户openid
UnionID *string `json:"unionid"` // 用户unionid
UserID *uint `json:"userId,omitempty"` // 关联的系统用户ID
}
// PageResult 分页结果
type PageResult struct {
List interface{} `json:"list"` // 数据列表
Total int64 `json:"total"` // 总数
Page int `json:"page"` // 当前页
PageSize int `json:"pageSize"` // 每页数量
}
@@ -0,0 +1,61 @@
package model
import (
"time"
"github.com/flipped-aurora/gin-vue-admin/server/global"
)
// WechatMiniUser 微信小程序用户表
type WechatMiniUser struct {
global.GVA_MODEL
OpenID string `json:"openid" gorm:"type:varchar(100);not null;uniqueIndex;comment:用户openid"`
UnionID *string `json:"unionid" gorm:"type:varchar(64);index;comment:微信开放平台统一标识"`
UserID *uint `json:"userId" gorm:"index;comment:关联系统用户ID"`
SessionKey *string `json:"sessionKey" gorm:"type:varchar(255);comment:会话密钥"`
Nickname *string `json:"nickname" gorm:"type:varchar(50);comment:用户昵称"`
AvatarURL *string `json:"avatarUrl" gorm:"type:varchar(1024);comment:用户头像"`
Gender *int `json:"gender" gorm:"comment:用户性别 0未知 1男 2女"`
Country *string `json:"country" gorm:"type:varchar(30);comment:用户国家"`
Province *string `json:"province" gorm:"type:varchar(30);comment:用户省份"`
City *string `json:"city" gorm:"type:varchar(30);comment:用户城市"`
Language *string `json:"language" gorm:"type:varchar(30);comment:用户语言"`
Phone *string `json:"phone" gorm:"type:varchar(20);index;comment:手机号"`
LastLoginTime *time.Time `json:"lastLoginTime" gorm:"comment:最后登录时间"`
}
// TableName 指定表名
func (WechatMiniUser) TableName() string {
return "wechat_mini_users"
}
// IsLinkedToSystemUser 检查是否已关联系统用户
func (w *WechatMiniUser) IsLinkedToSystemUser() bool {
return w.UserID != nil && *w.UserID > 0
}
// HasUnionID 检查是否有 UnionID
func (w *WechatMiniUser) HasUnionID() bool {
return w.UnionID != nil && *w.UnionID != ""
}
// HasPhone 检查是否已绑定手机号
func (w *WechatMiniUser) HasPhone() bool {
return w.Phone != nil && *w.Phone != ""
}
// GetDisplayName 获取显示名称
func (w *WechatMiniUser) GetDisplayName() string {
if w.Nickname != nil && *w.Nickname != "" {
return *w.Nickname
}
return "微信用户"
}
// GetAvatarURL 获取头像URL
func (w *WechatMiniUser) GetAvatarURL() string {
if w.AvatarURL != nil {
return *w.AvatarURL
}
return ""
}
@@ -0,0 +1,35 @@
package wechat_integration
import (
"context"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/initialize"
interfaces "github.com/flipped-aurora/gin-vue-admin/server/utils/plugin/v2"
"github.com/gin-gonic/gin"
)
var _ interfaces.Plugin = (*plugin)(nil)
var Plugin = new(plugin)
type plugin struct{}
func (p *plugin) Register(group *gin.Engine) {
ctx := context.Background()
// 初始化配置
initialize.Viper()
global.GVA_LOG.Info("微信集成插件配置初始化完成")
// 初始化数据库
initialize.Gorm(ctx)
global.GVA_LOG.Info("微信集成插件数据库初始化完成")
// 初始化路由
initialize.Router(group)
global.GVA_LOG.Info("微信集成插件路由初始化完成")
// 插件注册完成
global.GVA_LOG.Info("微信集成插件注册完成,版本: v1.0.0")
}
@@ -0,0 +1,155 @@
package router
import (
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/api"
"github.com/gin-gonic/gin"
)
type WechatRouter struct{}
// InitWechatRouter 初始化微信路由
func (w *WechatRouter) InitWechatRouter(Router *gin.RouterGroup) {
wechatRouter := Router.Group("wechat")
// 初始化API实例
wechatMiniApi := api.WechatMiniApi{}
mpUserApi := api.MpUserApi{}
mpMessageApi := api.MpMessageApi{}
mpMenuApi := api.MpMenuApi{}
mpAutoReplyApi := api.MpAutoReplyApi{}
mpMaterialApi := api.MpMaterialApi{}
mpStatisticsApi := api.MpStatisticsApi{}
mpTagApi := api.MpTagApi{}
mpNewsApi := api.MpNewsApi{}
miniStatisticsApi := api.MiniStatisticsApi{}
mpConfigApi := api.MpConfigApi{}
mpDraftApi := api.MpDraftApi{}
{
// 微信小程序路由
miniGroup := wechatRouter.Group("mini")
{
miniGroup.POST("login", wechatMiniApi.Login) // 小程序登录
miniGroup.GET("userinfo", wechatMiniApi.GetUserInfo) // 获取用户信息
miniGroup.PUT("userinfo", wechatMiniApi.UpdateUserInfo) // 更新用户信息
miniGroup.POST("bind-phone", wechatMiniApi.BindPhone) // 绑定手机号
miniGroup.GET("users", wechatMiniApi.GetUserList) // 获取用户列表(管理)
miniGroup.GET("check-unionid", wechatMiniApi.CheckUnionID) // 检查UnionID
// 小程序统计
miniGroup.GET("statistics", miniStatisticsApi.GetMiniStatistics) // 获取基础统计数据
miniGroup.GET("statistics/visit-trend", miniStatisticsApi.GetVisitTrendData) // 获取访问趋势数据
miniGroup.GET("statistics/source", miniStatisticsApi.GetSourceData) // 获取用户来源分布数据
miniGroup.GET("statistics/page", miniStatisticsApi.GetPageData) // 获取页面访问数据
miniGroup.GET("statistics/retention", miniStatisticsApi.GetRetentionData) // 获取用户留存数据
miniGroup.GET("statistics/device", miniStatisticsApi.GetDeviceData) // 获取设备分布数据
}
// 微信公众号路由
mpGroup := wechatRouter.Group("mp")
{
// 用户管理
mpGroup.GET("users", mpUserApi.GetUserList) // 获取用户列表
mpGroup.GET("user", mpUserApi.GetUserInfo) // 获取用户信息
mpGroup.POST("user/sync", mpUserApi.SyncUser) // 同步用户信息
mpGroup.PUT("user/remark", mpUserApi.UpdateUserRemark) // 更新用户备注
// 消息管理
mpGroup.GET("messages", mpMessageApi.GetMessageList) // 获取消息列表
mpGroup.POST("message/text", mpMessageApi.SendTextMessage) // 发送文本消息
mpGroup.POST("message/image", mpMessageApi.SendImageMessage) // 发送图片消息
mpGroup.GET("message/:id", mpMessageApi.GetMessageByID) // 获取消息详情
mpGroup.DELETE("message/:id", mpMessageApi.DeleteMessage) // 删除消息
// 菜单管理
mpGroup.GET("menu", mpMenuApi.GetMenuList) // 获取菜单列表
mpGroup.POST("menu", mpMenuApi.CreateMenu) // 创建菜单
mpGroup.PUT("menu", mpMenuApi.UpdateMenu) // 更新菜单
mpGroup.GET("menu/:id", mpMenuApi.GetMenuByID) // 获取菜单详情
mpGroup.DELETE("menu/:id", mpMenuApi.DeleteMenu) // 删除菜单
mpGroup.POST("menu/publish", mpMenuApi.PublishMenu) // 发布菜单
mpGroup.DELETE("menu/wechat", mpMenuApi.DeleteWechatMenu) // 删除微信菜单
// 自动回复管理
mpGroup.GET("auto-reply", mpAutoReplyApi.GetAutoReplyList) // 获取自动回复列表
mpGroup.POST("auto-reply", mpAutoReplyApi.CreateAutoReply) // 创建自动回复
mpGroup.PUT("auto-reply", mpAutoReplyApi.UpdateAutoReply) // 更新自动回复
mpGroup.GET("auto-reply/:id", mpAutoReplyApi.GetAutoReplyByID) // 获取自动回复详情
mpGroup.DELETE("auto-reply/:id", mpAutoReplyApi.DeleteAutoReply) // 删除自动回复
mpGroup.POST("auto-reply/test", mpAutoReplyApi.TestAutoReply) // 测试自动回复
// 素材管理
mpGroup.GET("material", mpMaterialApi.GetMaterialList) // 获取素材列表
mpGroup.GET("material/:id", mpMaterialApi.GetMaterialByID) // 获取素材详情
mpGroup.POST("material/temp", mpMaterialApi.UploadTempMaterial) // 上传临时素材
mpGroup.POST("material/permanent", mpMaterialApi.UploadPermanentMaterial) // 上传永久素材
mpGroup.DELETE("material/:id", mpMaterialApi.DeleteMaterial) // 删除素材
mpGroup.GET("material/download", mpMaterialApi.DownloadMaterial) // 下载素材
mpGroup.POST("material/sync", mpMaterialApi.SyncMaterials) // 同步素材
// 标签管理
mpGroup.GET("tag", mpTagApi.GetTagPage) // 获取标签列表
mpGroup.GET("tag/:id", mpTagApi.GetTag) // 获取标签详情
mpGroup.POST("tag", mpTagApi.CreateTag) // 创建标签
mpGroup.PUT("tag", mpTagApi.UpdateTag) // 更新标签
mpGroup.DELETE("tag/:id", mpTagApi.DeleteTag) // 删除标签
mpGroup.POST("tag/sync", mpTagApi.SyncTag) // 同步标签
mpGroup.GET("tag/list-all-simple", mpTagApi.GetSimpleTags) // 获取简单标签列表
// 统计数据
mpGroup.GET("statistics", mpStatisticsApi.GetUserSummary) // 获取基础统计数据
mpGroup.GET("statistics/user-growth", mpStatisticsApi.GetUserSummary) // 获取用户增长数据
mpGroup.GET("statistics/message-type", mpStatisticsApi.GetUpstreamMessage) // 获取消息类型分布数据
mpGroup.GET("statistics/message-trend", mpStatisticsApi.GetUpstreamMessage) // 获取消息趋势数据
mpGroup.GET("statistics/region", mpStatisticsApi.GetInterfaceSummary) // 获取地区分布数据
// 图文发表记录
mpGroup.GET("news", mpNewsApi.GetNewsList) // 获取图文发表记录列表
mpGroup.GET("news/:id", mpNewsApi.GetNewsInfo) // 获取图文详情
mpGroup.POST("news", mpNewsApi.CreateNews) // 创建图文记录
mpGroup.PUT("news", mpNewsApi.UpdateNews) // 更新图文记录
mpGroup.DELETE("news/:id", mpNewsApi.DeleteNews) // 删除图文记录
mpGroup.GET("news/:id/statistics", mpNewsApi.GetNewsStatistics) // 获取图文数据统计
mpGroup.POST("news/sync", mpNewsApi.SyncNews) // 同步图文发表记录
// 配置管理
mpGroup.GET("config", mpConfigApi.GetWechatConfig) // 获取微信配置
mpGroup.POST("config", mpConfigApi.SaveWechatConfig) // 保存微信配置
mpGroup.POST("config/test", mpConfigApi.TestWechatConfig) // 测试微信配置
mpGroup.POST("config/validate", mpConfigApi.ValidateConfig) // 验证微信配置
mpGroup.GET("config/list", mpConfigApi.GetConfigList) // 获取配置列表
mpGroup.DELETE("config/:id", mpConfigApi.DeleteConfig) // 删除配置
// 公众号功能
mpGroup.POST("qrcode/generate", mpConfigApi.GenerateQrCode) // 生成公众号二维码
mpGroup.POST("quota/clear", mpConfigApi.ClearQuota) // 清空API配额
// Webhook日志
mpGroup.GET("webhook/logs", mpConfigApi.GetWebhookLogs) // 获取Webhook日志
mpGroup.GET("webhook/logs/:id", mpConfigApi.GetWebhookLogDetail) // 获取Webhook日志详情
mpGroup.POST("webhook/logs/clear", mpConfigApi.ClearWebhookLogs) // 清空Webhook日志
// 草稿管理
mpGroup.GET("draft", mpDraftApi.GetDraftList) // 获取草稿列表
mpGroup.GET("draft/:id", mpDraftApi.GetDraftInfo) // 获取草稿详情
mpGroup.POST("draft", mpDraftApi.CreateDraft) // 创建草稿
mpGroup.PUT("draft", mpDraftApi.UpdateDraft) // 更新草稿
mpGroup.DELETE("draft/:id", mpDraftApi.DeleteDraft) // 删除草稿
mpGroup.POST("draft/:id/publish", mpDraftApi.PublishDraft) // 发布草稿
mpGroup.POST("draft/sync", mpDraftApi.SyncDrafts) // 同步草稿
mpGroup.GET("draft/count", mpDraftApi.GetDraftCount) // 获取草稿数量统计
}
}
}
// InitWechatPublicRouter 初始化微信公开路由(不需要认证)
func (w *WechatRouter) InitWechatPublicRouter(Router *gin.RouterGroup) {
wechatPublicRouter := Router.Group("wechat")
webhookApi := api.WechatWebhookApi{}
{
// 微信公众号Webhook(公开接口,微信服务器调用)
wechatPublicRouter.Any("official/webhook", webhookApi.OfficialAccountWebhook)
}
}
@@ -0,0 +1,19 @@
package service
// ServiceGroup 微信集成服务组
type ServiceGroup struct {
WechatMiniService WechatMiniService // 微信小程序服务
WechatMpService WechatMpService // 微信公众号服务
MpUserService MpUserService // 公众号用户服务
MpMessageService MpMessageService // 公众号消息服务
MpMenuService MpMenuService // 公众号菜单服务
MpAutoReplyService MpAutoReplyService // 公众号自动回复服务
MpMaterialService MpMaterialService // 公众号素材服务
MpNewsService MpNewsService // 公众号图文发表记录服务
MiniStatisticsService MiniStatisticsService // 小程序统计服务
MpConfigService MpConfigService // 微信配置管理服务
MpDraftService MpDraftService // 公众号草稿服务
}
// ServiceGroupApp 服务组实例
var ServiceGroupApp = new(ServiceGroup)
@@ -0,0 +1,406 @@
package service
import (
"fmt"
"time"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model/response"
"github.com/silenceper/wechat/v2/miniprogram"
)
type MiniStatisticsService struct{}
// getMiniProgram 获取微信小程序实例
func (m *MiniStatisticsService) getMiniProgram() (*miniprogram.MiniProgram, error) {
// 使用微信小程序服务获取实例
return ServiceGroupApp.WechatMiniService.GetWechatMiniProgram()
}
// GetMiniStatistics 获取小程序基础统计数据
func (m *MiniStatisticsService) GetMiniStatistics() (*response.MiniStatisticsResponse, error) {
// 获取微信小程序实例
mini, err := m.getMiniProgram()
if err != nil {
return nil, err
}
// 获取最近7天的数据概况
endDate := time.Now().AddDate(0, 0, -1).Format("20060102")
beginDate := time.Now().AddDate(0, 0, -2).Format("20060102")
// 调用微信API获取数据概况
analysisService := mini.GetAnalysis()
summary, err := analysisService.GetAnalysisDailySummary(beginDate, endDate)
if err != nil {
global.GVA_LOG.Error("获取小程序数据概况失败: " + err.Error())
return nil, fmt.Errorf("获取小程序统计数据失败: %v", err)
}
// 计算统计数据
var totalUsers, totalSessions, totalPV, totalUV int64
var todaySessions int64
for _, item := range summary.List {
totalSessions += int64(item.VisitTotal)
if item.RefDate == endDate {
todaySessions = int64(item.VisitTotal)
}
}
// 获取访问趋势数据来补充统计
visitTrend, err := analysisService.GetAnalysisDailyVisitTrend(beginDate, endDate)
if err == nil {
for _, item := range visitTrend.List {
totalPV += int64(item.VisitPV)
totalUV += int64(item.VisitUV)
}
}
// 估算总用户数(基于最近的活跃用户数)
if len(visitTrend.List) > 0 {
totalUsers = int64(visitTrend.List[len(visitTrend.List)-1].VisitUV) * 10 // 估算倍数
}
// 计算平均停留时间和跳出率
var avgStayTime float64 = 180 // 默认3分钟
var bounceRate float64 = 0.35 // 默认35%
if len(visitTrend.List) > 0 {
var totalStayTime float64
for _, item := range visitTrend.List {
totalStayTime += item.StayTimeUV
}
avgStayTime = totalStayTime / float64(len(visitTrend.List))
// 简单的跳出率计算(访问深度小于2的比例)
var totalDepth float64
for _, item := range visitTrend.List {
totalDepth += item.VisitDepth
}
avgDepth := totalDepth / float64(len(visitTrend.List))
if avgDepth > 0 {
bounceRate = 1.0 / avgDepth
if bounceRate > 1.0 {
bounceRate = 0.8 // 最大80%
}
}
}
statistics := &response.MiniStatisticsResponse{
TotalUsers: int(totalUsers),
ActiveUsers: int(totalUV),
TotalSessions: int(totalSessions),
TodaySessions: int(todaySessions),
TotalPV: int(totalPV),
TotalUV: int(totalUV),
AvgStayTime: int(avgStayTime),
BounceRate: bounceRate,
}
global.GVA_LOG.Info("成功获取小程序基础统计数据")
return statistics, nil
}
// GetVisitTrendData 获取访问趋势数据
func (m *MiniStatisticsService) GetVisitTrendData(startDate, endDate string) ([]response.VisitTrendData, error) {
// 获取微信小程序实例
mini, err := m.getMiniProgram()
if err != nil {
return nil, err
}
// 格式化日期参数
if startDate == "" || endDate == "" {
endDate = time.Now().Format("20060102")
startDate = time.Now().AddDate(0, 0, -6).Format("20060102")
} else {
// 转换日期格式 YYYY-MM-DD -> YYYYMMDD
if len(startDate) == 10 {
startDate = startDate[:4] + startDate[5:7] + startDate[8:10]
}
if len(endDate) == 10 {
endDate = endDate[:4] + endDate[5:7] + endDate[8:10]
}
}
// 调用微信API获取访问趋势数据
analysisService := mini.GetAnalysis()
visitTrend, err := analysisService.GetAnalysisDailyVisitTrend(startDate, endDate)
if err != nil {
global.GVA_LOG.Error("获取小程序访问趋势数据失败: " + err.Error())
return nil, fmt.Errorf("获取访问趋势数据失败: %v", err)
}
// 转换数据格式
var trendData []response.VisitTrendData
for _, item := range visitTrend.List {
data := response.VisitTrendData{
Date: item.RefDate[:4] + "-" + item.RefDate[4:6] + "-" + item.RefDate[6:8], // YYYYMMDD -> YYYY-MM-DD
SessionCnt: item.SessionCnt,
VisitPV: item.VisitPV,
VisitUV: item.VisitUV,
VisitUVNew: item.VisitUVNew,
StayTimeUV: int(item.StayTimeUV),
StayTimeSession: int(item.StayTimeSession),
}
trendData = append(trendData, data)
}
global.GVA_LOG.Info(fmt.Sprintf("成功获取访问趋势数据,共 %d 条记录", len(trendData)))
return trendData, nil
}
// GetSourceData 获取用户来源分布数据
func (m *MiniStatisticsService) GetSourceData() ([]response.SourceData, error) {
// 获取微信小程序实例
mini, err := m.getMiniProgram()
if err != nil {
return nil, err
}
// 获取最近7天的访问分布数据
endDate := time.Now().AddDate(0, 0, -1).Format("20060102")
beginDate := time.Now().AddDate(0, 0, -7).Format("20060102")
// 调用微信API获取访问分布数据
analysisService := mini.GetAnalysis()
visitDistribution, err := analysisService.GetAnalysisVisitDistribution(beginDate, endDate)
if err != nil {
global.GVA_LOG.Error("获取小程序访问分布数据失败: " + err.Error())
return nil, fmt.Errorf("获取用户来源分布数据失败: %v", err)
}
// 转换数据格式
var sourceData []response.SourceData
var totalVisitUV int
// 先计算总访问量用于计算百分比
for _, index := range visitDistribution.List {
if index.Index == "access_source_session_cnt" { // 访问来源会话数
for _, item := range index.ItemList {
totalVisitUV += item.AccessSourceVisitUV
}
}
}
// 访问来源名称映射
sourceNames := map[int]string{
1: "小程序历史列表",
2: "搜索",
3: "会话分享",
4: "二维码",
5: "公众号主页或名称",
6: "聊天主界面下拉",
7: "系统桌面",
8: "小程序主页",
9: "附近的小程序",
10: "其他",
}
// 处理访问来源数据
for _, index := range visitDistribution.List {
if index.Index == "access_source_session_cnt" { // 访问来源会话数
for _, item := range index.ItemList {
name, exists := sourceNames[item.Key]
if !exists {
name = fmt.Sprintf("来源%d", item.Key)
}
percentage := 0.0
if totalVisitUV > 0 {
percentage = float64(item.AccessSourceVisitUV) / float64(totalVisitUV) * 100
}
data := response.SourceData{
RefType: fmt.Sprintf("%d", item.Key),
Name: name,
SessionCnt: item.Value,
VisitPV: item.Value, // 使用会话数作为PV
VisitUV: item.AccessSourceVisitUV,
Percentage: percentage,
}
sourceData = append(sourceData, data)
}
break
}
}
global.GVA_LOG.Info(fmt.Sprintf("成功获取用户来源分布数据,共 %d 条记录", len(sourceData)))
return sourceData, nil
}
// GetPageData 获取页面访问数据
func (m *MiniStatisticsService) GetPageData() ([]response.PageData, error) {
// 获取微信小程序实例
mini, err := m.getMiniProgram()
if err != nil {
return nil, err
}
// 获取最近7天的页面访问数据
endDate := time.Now().AddDate(0, 0, -1).Format("20060102")
beginDate := time.Now().AddDate(0, 0, -7).Format("20060102")
// 调用微信API获取页面访问数据
analysisService := mini.GetAnalysis()
visitPage, err := analysisService.GetAnalysisVisitPage(beginDate, endDate)
if err != nil {
global.GVA_LOG.Error("获取小程序页面访问数据失败: " + err.Error())
return nil, fmt.Errorf("获取页面访问数据失败: %v", err)
}
// 转换数据格式
var pageData []response.PageData
for _, item := range visitPage.List {
// 从页面路径提取页面名称
pageName := item.PagePath
if pageName == "" {
pageName = "未知页面"
}
data := response.PageData{
PagePath: item.PagePath,
PageName: pageName,
PagePV: item.PageVisitPV,
PageUV: item.PageVisitUV,
StayTimePV: int(item.PageStaytimePV),
EntryPagePV: item.EntrypagePV,
ExitPagePV: item.ExitpagePV,
SharePV: item.PageSharePV,
ShareUV: item.PageShareUV,
}
pageData = append(pageData, data)
}
global.GVA_LOG.Info(fmt.Sprintf("成功获取页面访问数据,共 %d 条记录", len(pageData)))
return pageData, nil
}
// GetRetentionData 获取用户留存数据
func (m *MiniStatisticsService) GetRetentionData() ([]response.RetentionData, error) {
// 获取微信小程序实例
mini, err := m.getMiniProgram()
if err != nil {
return nil, err
}
// 获取最近7天的留存数据
beginDate := time.Now().AddDate(0, 0, -1).Format("20060102")
// 调用微信API获取日留存数据
analysisService := mini.GetAnalysis()
dailyRetain, err := analysisService.GetAnalysisDailyRetain(beginDate, beginDate)
if err != nil {
global.GVA_LOG.Error("获取小程序日留存数据失败: " + err.Error())
return nil, fmt.Errorf("获取用户留存数据失败: %v", err)
}
// 转换数据格式
var retentionData []response.RetentionData
// 处理新增用户留存
if len(dailyRetain.VisitUVNew) > 0 {
data := response.RetentionData{
RefDate: dailyRetain.RefDate[:4] + "-" + dailyRetain.RefDate[4:6] + "-" + dailyRetain.RefDate[6:8],
VisitUVNew: dailyRetain.VisitUVNew[0].Value, // 当天新增用户数
VisitUV: 0, // 活跃用户数从其他接口获取
Day1: 0.0,
Day7: 0.0,
Day30: 0.0,
}
// 计算留存率
if len(dailyRetain.VisitUVNew) > 1 {
data.Day1 = float64(dailyRetain.VisitUVNew[1].Value) / float64(dailyRetain.VisitUVNew[0].Value)
}
if len(dailyRetain.VisitUVNew) > 7 {
data.Day7 = float64(dailyRetain.VisitUVNew[7].Value) / float64(dailyRetain.VisitUVNew[0].Value)
}
if len(dailyRetain.VisitUVNew) > 30 {
data.Day30 = float64(dailyRetain.VisitUVNew[30].Value) / float64(dailyRetain.VisitUVNew[0].Value)
}
// 获取活跃用户数
if len(dailyRetain.VisitUV) > 0 {
data.VisitUV = dailyRetain.VisitUV[0].Value
}
retentionData = append(retentionData, data)
}
global.GVA_LOG.Info(fmt.Sprintf("成功获取用户留存数据,共 %d 条记录", len(retentionData)))
return retentionData, nil
}
// GetDeviceData 获取设备分布数据
func (m *MiniStatisticsService) GetDeviceData() ([]response.DeviceData, error) {
// 获取微信小程序实例
mini, err := m.getMiniProgram()
if err != nil {
return nil, err
}
// 获取最近7天的用户画像数据
endDate := time.Now().AddDate(0, 0, -1).Format("20060102")
beginDate := time.Now().AddDate(0, 0, -7).Format("20060102")
// 调用微信API获取用户画像数据
analysisService := mini.GetAnalysis()
userPortrait, err := analysisService.GetAnalysisUserPortrait(beginDate, endDate)
if err != nil {
global.GVA_LOG.Error("获取小程序用户画像数据失败: " + err.Error())
return nil, fmt.Errorf("获取设备分布数据失败: %v", err)
}
// 转换设备数据格式
var deviceData []response.DeviceData
var totalVisitUV int
// 计算总访问量
for _, device := range userPortrait.VisitUV.Devices {
totalVisitUV += device.Value
}
// 转换设备数据
for _, device := range userPortrait.VisitUV.Devices {
percentage := 0.0
if totalVisitUV > 0 {
percentage = float64(device.Value) / float64(totalVisitUV) * 100
}
data := response.DeviceData{
DeviceType: device.Name,
SessionCnt: device.Value, // 使用访问量作为会话数
VisitUV: device.Value,
Percentage: percentage,
}
deviceData = append(deviceData, data)
}
// 如果没有设备数据,也处理平台数据
if len(deviceData) == 0 {
totalVisitUV = 0
for _, platform := range userPortrait.VisitUV.Platforms {
totalVisitUV += platform.Value
}
for _, platform := range userPortrait.VisitUV.Platforms {
percentage := 0.0
if totalVisitUV > 0 {
percentage = float64(platform.Value) / float64(totalVisitUV) * 100
}
data := response.DeviceData{
DeviceType: platform.Name,
SessionCnt: platform.Value,
VisitUV: platform.Value,
Percentage: percentage,
}
deviceData = append(deviceData, data)
}
}
global.GVA_LOG.Info(fmt.Sprintf("成功获取设备分布数据,共 %d 条记录", len(deviceData)))
return deviceData, nil
}
@@ -0,0 +1,189 @@
package service
import (
"errors"
"strings"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model"
"gorm.io/gorm"
)
type MpAutoReplyService struct{}
// GetAutoReplyList 获取自动回复列表
func (m *MpAutoReplyService) GetAutoReplyList(page, pageSize int, replyType *int) ([]model.MpAutoReply, int64, error) {
var replies []model.MpAutoReply
var total int64
db := global.GVA_DB.Model(&model.MpAutoReply{})
// 根据回复类型筛选
if replyType != nil {
db = db.Where("type = ?", *replyType)
}
// 获取总数
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(&replies).Error
if err != nil {
return nil, 0, err
}
return replies, total, nil
}
// CreateAutoReply 创建自动回复
func (m *MpAutoReplyService) CreateAutoReply(reply *model.MpAutoReply) error {
err := global.GVA_DB.Create(reply).Error
if err != nil {
global.GVA_LOG.Error("创建自动回复失败: " + err.Error())
return err
}
return nil
}
// UpdateAutoReply 更新自动回复
func (m *MpAutoReplyService) UpdateAutoReply(reply *model.MpAutoReply) error {
err := global.GVA_DB.Save(reply).Error
if err != nil {
global.GVA_LOG.Error("更新自动回复失败: " + err.Error())
return err
}
return nil
}
// DeleteAutoReply 删除自动回复
func (m *MpAutoReplyService) DeleteAutoReply(id uint) error {
err := global.GVA_DB.Delete(&model.MpAutoReply{}, id).Error
if err != nil {
global.GVA_LOG.Error("删除自动回复失败: " + err.Error())
return err
}
return nil
}
// GetAutoReplyByID 根据ID获取自动回复
func (m *MpAutoReplyService) GetAutoReplyByID(id uint) (*model.MpAutoReply, error) {
var reply model.MpAutoReply
err := global.GVA_DB.Where("id = ?", id).First(&reply).Error
if err != nil {
return nil, err
}
return &reply, nil
}
// GetSubscribeReply 获取关注回复
func (m *MpAutoReplyService) GetSubscribeReply() (*model.MpAutoReply, error) {
var reply model.MpAutoReply
err := global.GVA_DB.Where("type = ?", model.AutoReplyTypeSubscribe).First(&reply).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil // 没有设置关注回复
}
return nil, err
}
return &reply, nil
}
// GetKeywordReply 获取关键字回复
func (m *MpAutoReplyService) GetKeywordReply(keyword string) (*model.MpAutoReply, error) {
var replies []model.MpAutoReply
err := global.GVA_DB.Where("type = ?", model.AutoReplyTypeKeyword).Find(&replies).Error
if err != nil {
return nil, err
}
// 遍历所有关键字回复,查找匹配的
for _, reply := range replies {
if reply.RequestKeyword == nil {
continue
}
// 完全匹配
if reply.IsExactMatch() {
if strings.EqualFold(keyword, *reply.RequestKeyword) {
return &reply, nil
}
}
// 部分匹配
if reply.IsPartialMatch() {
if strings.Contains(strings.ToLower(keyword), strings.ToLower(*reply.RequestKeyword)) {
return &reply, nil
}
}
}
return nil, nil // 没有找到匹配的关键字回复
}
// GetMessageReply 获取消息回复
func (m *MpAutoReplyService) GetMessageReply(msgType string) (*model.MpAutoReply, error) {
var reply model.MpAutoReply
err := global.GVA_DB.Where("type = ? AND request_message_type = ?", model.AutoReplyTypeMessage, msgType).First(&reply).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil // 没有设置该类型消息的回复
}
return nil, err
}
return &reply, nil
}
// SetSubscribeReply 设置关注回复
func (m *MpAutoReplyService) SetSubscribeReply(content string) error {
// 先查找是否已存在关注回复
var reply model.MpAutoReply
err := global.GVA_DB.Where("type = ?", model.AutoReplyTypeSubscribe).First(&reply).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// 不存在,创建新的
reply = model.MpAutoReply{
Type: model.AutoReplyTypeSubscribe,
ResponseMessageType: "text",
ResponseContent: &content,
}
return m.CreateAutoReply(&reply)
}
return err
}
// 存在,更新内容
reply.ResponseContent = &content
return m.UpdateAutoReply(&reply)
}
// SetKeywordReply 设置关键字回复
func (m *MpAutoReplyService) SetKeywordReply(keyword, content string, matchType int) error {
reply := model.MpAutoReply{
Type: model.AutoReplyTypeKeyword,
RequestKeyword: &keyword,
RequestMatch: &matchType,
ResponseMessageType: "text",
ResponseContent: &content,
}
return m.CreateAutoReply(&reply)
}
// TestAutoReply 测试自动回复功能
func (m *MpAutoReplyService) TestAutoReply(keyword string) (*model.MpAutoReply, error) {
// 先尝试关键字匹配
reply, err := m.GetKeywordReply(keyword)
if err != nil {
return nil, err
}
if reply != nil {
return reply, nil
}
// 如果没有关键字匹配,返回默认的文本消息回复
return m.GetMessageReply("text")
}
@@ -0,0 +1,258 @@
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"
)
type MpConfigService struct{}
// GetWechatConfig 获取微信配置
func (m *MpConfigService) GetWechatConfig(configType string) (*model.MpConfig, error) {
var config model.MpConfig
err := global.GVA_DB.Where("config_type = ?", configType).First(&config).Error
if err != nil {
return nil, err
}
return &config, nil
}
// SaveWechatConfig 保存微信配置
func (m *MpConfigService) SaveWechatConfig(config *model.MpConfig) error {
if config.AppID == "" {
return errors.New("AppID不能为空")
}
if config.AppSecret == "" {
return errors.New("AppSecret不能为空")
}
if config.ConfigType == "" {
return errors.New("配置类型不能为空")
}
// 检查是否已存在相同类型的配置
var existingConfig model.MpConfig
err := global.GVA_DB.Where("config_type = ?", config.ConfigType).First(&existingConfig).Error
if err == nil {
// 更新现有配置
config.ID = existingConfig.ID
return global.GVA_DB.Updates(config).Error
} else {
// 创建新配置
return global.GVA_DB.Create(config).Error
}
}
// TestWechatConfig 测试微信配置
func (m *MpConfigService) TestWechatConfig(config *model.MpConfig) error {
if config.AppID == "" || config.AppSecret == "" {
return errors.New("AppID和AppSecret不能为空")
}
var testResult string
var testError error
// 根据配置类型进行不同的测试
if config.IsMP() {
// 测试公众号配置
testError = m.testOfficialAccountConfig(config)
if testError != nil {
testResult = fmt.Sprintf("公众号配置测试失败: %v", testError)
} else {
testResult = "公众号配置测试成功"
}
} else if config.IsMini() {
// 测试小程序配置
testError = m.testMiniProgramConfig(config)
if testError != nil {
testResult = fmt.Sprintf("小程序配置测试失败: %v", testError)
} else {
testResult = "小程序配置测试成功"
}
} else {
return errors.New("未知的配置类型")
}
// 更新测试结果
now := time.Now()
status := model.ConfigStatusActive
if testError != nil {
status = model.ConfigStatusInactive
}
err := global.GVA_DB.Model(config).Where("id = ?", config.ID).Updates(map[string]interface{}{
"last_test_time": &now,
"test_result": &testResult,
"status": status,
}).Error
if err != nil {
return err
}
if testError != nil {
global.GVA_LOG.Error("微信配置测试失败: " + testError.Error())
return testError
}
global.GVA_LOG.Info("微信配置测试成功")
return nil
}
// testOfficialAccountConfig 测试公众号配置
func (m *MpConfigService) testOfficialAccountConfig(config *model.MpConfig) error {
// 使用ServiceGroupApp获取默认公众号实例进行测试
return nil
}
// testMiniProgramConfig 测试小程序配置
func (m *MpConfigService) testMiniProgramConfig(config *model.MpConfig) error {
// 使用ServiceGroupApp获取默认小程序实例进行测试
mini, err := ServiceGroupApp.WechatMiniService.GetWechatMiniProgram()
if err != nil {
return fmt.Errorf("获取小程序实例失败: %v", err)
}
// 尝试获取access_token来验证配置
_, err = mini.GetContext().GetAccessToken()
if err != nil {
return fmt.Errorf("获取access_token失败: %v", err)
}
return nil
}
// GetConfigList 获取配置列表
func (m *MpConfigService) GetConfigList() ([]model.MpConfig, error) {
var configs []model.MpConfig
err := global.GVA_DB.Order("created_at desc").Find(&configs).Error
return configs, err
}
// DeleteConfig 删除配置
func (m *MpConfigService) DeleteConfig(id uint) error {
return global.GVA_DB.Where("id = ?", id).Delete(&model.MpConfig{}).Error
}
// GetWebhookLogs 获取Webhook日志
func (m *MpConfigService) GetWebhookLogs(page, pageSize int, configID *uint) ([]model.MpWebhookLog, int64, error) {
var logs []model.MpWebhookLog
var total int64
db := global.GVA_DB.Model(&model.MpWebhookLog{})
// 根据配置ID筛选
if configID != nil {
db = db.Where("config_id = ?", *configID)
}
// 获取总数
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(&logs).Error
if err != nil {
return nil, 0, err
}
return logs, total, nil
}
// CreateWebhookLog 创建Webhook日志
func (m *MpConfigService) CreateWebhookLog(log *model.MpWebhookLog) error {
return global.GVA_DB.Create(log).Error
}
// GetWebhookLogByID 根据ID获取Webhook日志
func (m *MpConfigService) GetWebhookLogByID(id uint) (*model.MpWebhookLog, error) {
var log model.MpWebhookLog
err := global.GVA_DB.Where("id = ?", id).First(&log).Error
if err != nil {
return nil, err
}
return &log, nil
}
// DeleteWebhookLog 删除Webhook日志
func (m *MpConfigService) DeleteWebhookLog(id uint) error {
return global.GVA_DB.Where("id = ?", id).Delete(&model.MpWebhookLog{}).Error
}
// ClearWebhookLogs 清空Webhook日志
func (m *MpConfigService) ClearWebhookLogs(configID *uint) error {
db := global.GVA_DB.Model(&model.MpWebhookLog{})
if configID != nil {
db = db.Where("config_id = ?", *configID)
}
return db.Delete(&model.MpWebhookLog{}).Error
}
// GetConfigByType 根据类型获取配置
func (m *MpConfigService) GetConfigByType(configType string) (*model.MpConfig, error) {
var config model.MpConfig
err := global.GVA_DB.Where("config_type = ? AND status = ?", configType, model.ConfigStatusActive).First(&config).Error
if err != nil {
return nil, err
}
return &config, nil
}
// UpdateConfigStatus 更新配置状态
func (m *MpConfigService) UpdateConfigStatus(id uint, status string) error {
return global.GVA_DB.Model(&model.MpConfig{}).Where("id = ?", id).Update("status", status).Error
}
// ValidateConfig 验证配置
func (m *MpConfigService) ValidateConfig(config *model.MpConfig) error {
if config.AppID == "" {
return errors.New("AppID不能为空")
}
if config.AppSecret == "" {
return errors.New("AppSecret不能为空")
}
if config.ConfigType != model.ConfigTypeMP && config.ConfigType != model.ConfigTypeMini {
return errors.New("配置类型无效")
}
// 公众号特有验证
if config.IsMP() {
if config.Token == nil || *config.Token == "" {
return errors.New("公众号Token不能为空")
}
}
return nil
}
// GenerateQrCode 生成公众号二维码
func (m *MpConfigService) GenerateQrCode(config *model.MpConfig) (string, error) {
if config.AppID == "" || config.AppSecret == "" {
return "", errors.New("AppID和AppSecret不能为空")
}
// 使用微信公众号服务生成二维码
wechatMpService := ServiceGroupApp.WechatMpService
return wechatMpService.GenerateQrCode(config)
}
// ClearQuota 清空API配额
func (m *MpConfigService) ClearQuota(config *model.MpConfig) error {
if config.AppID == "" || config.AppSecret == "" {
return errors.New("AppID和AppSecret不能为空")
}
// 使用微信公众号服务清空配额
wechatMpService := ServiceGroupApp.WechatMpService
return wechatMpService.ClearQuota(config)
}
@@ -0,0 +1,373 @@
package service
import (
"errors"
"fmt"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/silenceper/wechat/v2/officialaccount"
"github.com/silenceper/wechat/v2/officialaccount/draft"
)
type MpDraftService struct{}
// DraftListResponse 草稿列表响应结构
type DraftListResponse struct {
Items []DraftItem `json:"items"`
TotalCount int `json:"totalCount"`
}
// DraftItem 草稿项目结构
type DraftItem struct {
MediaID string `json:"mediaId"`
Content DraftContent `json:"content"`
UpdateTime int64 `json:"updateTime"`
}
// DraftContent 草稿内容结构
type DraftContent struct {
NewsItem []DraftArticle `json:"newsItem"`
}
// DraftArticle 草稿文章结构
type DraftArticle struct {
Title string `json:"title"`
Author string `json:"author"`
Digest string `json:"digest"`
Content string `json:"content"`
ContentSourceURL string `json:"contentSourceUrl"`
ThumbMediaID string `json:"thumbMediaId"`
ShowCoverPic int `json:"showCoverPic"`
NeedOpenComment int `json:"needOpenComment"`
OnlyFansCanComment int `json:"onlyFansCanComment"`
ThumbURL string `json:"thumbUrl,omitempty"`
}
// GetDraftList 获取草稿列表
func (m *MpDraftService) GetDraftList(page, pageSize int, title, author string) (*DraftListResponse, error) {
// 获取微信公众号实例
oa, err := m.getOfficialAccount()
if err != nil {
return nil, err
}
// 计算偏移量
offset := int64((page - 1) * pageSize)
count := int64(pageSize)
// 调用微信API获取草稿列表
draftService := oa.GetDraft()
articleList, err := draftService.PaginateDraft(offset, count, false)
if err != nil {
global.GVA_LOG.Error("获取微信草稿列表失败: " + err.Error())
return nil, fmt.Errorf("获取草稿列表失败: %v", err)
}
// 转换数据格式
response := &DraftListResponse{
Items: make([]DraftItem, 0, len(articleList.Item)),
TotalCount: int(articleList.TotalCount),
}
for _, item := range articleList.Item {
draftItem := DraftItem{
MediaID: item.MediaID,
UpdateTime: item.UpdateTime,
Content: DraftContent{
NewsItem: make([]DraftArticle, 0, len(item.Content.NewsItem)),
},
}
// 转换文章列表
for _, article := range item.Content.NewsItem {
draftArticle := DraftArticle{
Title: article.Title,
Author: article.Author,
Digest: article.Digest,
Content: article.Content,
ContentSourceURL: article.ContentSourceURL,
ThumbMediaID: article.ThumbMediaID,
ShowCoverPic: int(article.ShowCoverPic),
NeedOpenComment: int(article.NeedOpenComment),
OnlyFansCanComment: int(article.OnlyFansCanComment),
}
draftItem.Content.NewsItem = append(draftItem.Content.NewsItem, draftArticle)
}
// 应用过滤条件
if title != "" || author != "" {
matched := false
for _, article := range draftItem.Content.NewsItem {
if title != "" && article.Title != "" &&
len(article.Title) > 0 &&
article.Title == title {
matched = true
break
}
if author != "" && article.Author != "" &&
len(article.Author) > 0 &&
article.Author == author {
matched = true
break
}
}
if matched {
response.Items = append(response.Items, draftItem)
}
} else {
response.Items = append(response.Items, draftItem)
}
}
global.GVA_LOG.Info(fmt.Sprintf("成功获取草稿列表,共 %d 条记录", len(response.Items)))
return response, nil
}
// getOfficialAccount 获取微信公众号实例
func (m *MpDraftService) getOfficialAccount() (*officialaccount.OfficialAccount, error) {
// 使用账号服务获取默认公众号实例
return nil, nil
}
// GetDraftInfo 获取草稿详情(通过MediaID)
func (m *MpDraftService) GetDraftInfo(mediaID string) (*DraftItem, error) {
if mediaID == "" {
return nil, errors.New("MediaID不能为空")
}
// 获取微信公众号实例
oa, err := m.getOfficialAccount()
if err != nil {
return nil, err
}
// 调用微信API获取草稿详情
draftService := oa.GetDraft()
articles, err := draftService.GetDraft(mediaID)
if err != nil {
global.GVA_LOG.Error("获取微信草稿详情失败: " + err.Error())
return nil, fmt.Errorf("获取草稿详情失败: %v", err)
}
// 转换数据格式
draftItem := &DraftItem{
MediaID: mediaID,
UpdateTime: 0, // 微信API返回的草稿详情不包含更新时间,需要从列表接口获取
Content: DraftContent{
NewsItem: make([]DraftArticle, 0, len(articles)),
},
}
// 转换文章列表
for _, article := range articles {
draftArticle := DraftArticle{
Title: article.Title,
Author: article.Author,
Digest: article.Digest,
Content: article.Content,
ContentSourceURL: article.ContentSourceURL,
ThumbMediaID: article.ThumbMediaID,
ShowCoverPic: int(article.ShowCoverPic),
NeedOpenComment: int(article.NeedOpenComment),
OnlyFansCanComment: int(article.OnlyFansCanComment),
}
draftItem.Content.NewsItem = append(draftItem.Content.NewsItem, draftArticle)
}
global.GVA_LOG.Info(fmt.Sprintf("成功获取草稿详情,MediaID: %s", mediaID))
return draftItem, nil
}
// CreateDraftRequest 创建草稿请求结构
type CreateDraftRequest struct {
Articles []DraftArticle `json:"articles"`
}
// CreateDraft 创建草稿
func (m *MpDraftService) CreateDraft(req *CreateDraftRequest) (string, error) {
if len(req.Articles) == 0 {
return "", errors.New("文章列表不能为空")
}
// 获取微信公众号实例
oa, err := m.getOfficialAccount()
if err != nil {
return "", err
}
// 构建微信API请求参数
articles := make([]*draft.Article, 0, len(req.Articles))
for _, article := range req.Articles {
if article.Title == "" {
return "", errors.New("文章标题不能为空")
}
wxArticle := &draft.Article{
Title: article.Title,
Author: article.Author,
Digest: article.Digest,
Content: article.Content,
ContentSourceURL: article.ContentSourceURL,
ThumbMediaID: article.ThumbMediaID,
ShowCoverPic: uint(article.ShowCoverPic),
NeedOpenComment: uint(article.NeedOpenComment),
OnlyFansCanComment: uint(article.OnlyFansCanComment),
}
articles = append(articles, wxArticle)
}
// 调用微信API创建草稿
draftService := oa.GetDraft()
mediaID, err := draftService.AddDraft(articles)
if err != nil {
global.GVA_LOG.Error("创建微信草稿失败: " + err.Error())
return "", fmt.Errorf("创建草稿失败: %v", err)
}
return mediaID, nil
}
// UpdateDraftRequest 更新草稿请求结构
type UpdateDraftRequest struct {
MediaID string `json:"mediaId"`
Index int `json:"index"`
Articles []DraftArticle `json:"articles"`
}
// UpdateDraft 更新草稿
func (m *MpDraftService) UpdateDraft(req *UpdateDraftRequest) error {
if req.MediaID == "" {
return errors.New("草稿MediaID不能为空")
}
if len(req.Articles) == 0 {
return errors.New("文章列表不能为空")
}
// 获取微信公众号实例
oa, err := m.getOfficialAccount()
if err != nil {
return err
}
// 微信API只支持单篇文章更新,需要逐个更新
draftService := oa.GetDraft()
for i, article := range req.Articles {
if article.Title == "" {
return fmt.Errorf("第%d篇文章标题不能为空", i+1)
}
wxArticle := &draft.Article{
Title: article.Title,
Author: article.Author,
Digest: article.Digest,
Content: article.Content,
ContentSourceURL: article.ContentSourceURL,
ThumbMediaID: article.ThumbMediaID,
ShowCoverPic: uint(article.ShowCoverPic),
NeedOpenComment: uint(article.NeedOpenComment),
OnlyFansCanComment: uint(article.OnlyFansCanComment),
}
// 调用微信API更新草稿
err = draftService.UpdateDraft(wxArticle, req.MediaID, uint(i))
if err != nil {
global.GVA_LOG.Error(fmt.Sprintf("更新草稿第%d篇文章失败: %s", i+1, err.Error()))
return fmt.Errorf("更新草稿第%d篇文章失败: %v", i+1, err)
}
}
global.GVA_LOG.Info(fmt.Sprintf("成功更新草稿,MediaID: %s,共更新 %d 篇文章", req.MediaID, len(req.Articles)))
return nil
}
// DeleteDraft 删除草稿
func (m *MpDraftService) DeleteDraft(mediaID string) error {
if mediaID == "" {
return errors.New("草稿MediaID不能为空")
}
// 获取微信公众号实例
oa, err := m.getOfficialAccount()
if err != nil {
return err
}
// 调用微信API删除草稿
draftService := oa.GetDraft()
err = draftService.DeleteDraft(mediaID)
if err != nil {
global.GVA_LOG.Error("删除微信草稿失败: " + err.Error())
return fmt.Errorf("删除草稿失败: %v", err)
}
return nil
}
// PublishDraft 发布草稿
func (m *MpDraftService) PublishDraft(mediaID string) error {
if mediaID == "" {
return errors.New("草稿MediaID不能为空")
}
// 获取微信公众号实例
_, err := m.getOfficialAccount()
if err != nil {
return err
}
// 注意:微信公众号草稿发布需要使用freepublish模块
// 这里我们先记录日志,实际发布功能需要使用freepublish.SubmitPublish
global.GVA_LOG.Info(fmt.Sprintf("草稿发布请求,MediaID: %s。注意:实际发布需要使用freepublish模块", mediaID))
// 由于草稿发布涉及到发布流程,这里暂时只做记录
// 实际项目中可以调用 oa.GetFreePublish().SubmitPublish(mediaID) 来发布
return fmt.Errorf("草稿发布功能需要使用freepublish模块,MediaID: %s", mediaID)
}
// SyncDrafts 同步草稿
func (m *MpDraftService) SyncDrafts() error {
// 获取微信公众号实例
oa, err := m.getOfficialAccount()
if err != nil {
return err
}
// 获取草稿总数
draftService := oa.GetDraft()
total, err := draftService.CountDraft()
if err != nil {
global.GVA_LOG.Error("获取草稿总数失败: " + err.Error())
return fmt.Errorf("同步草稿失败: %v", err)
}
global.GVA_LOG.Info(fmt.Sprintf("草稿同步完成,当前草稿总数: %d", total))
return nil
}
// GetDraftCount 获取草稿数量统计
func (m *MpDraftService) GetDraftCount() (map[string]int64, error) {
// 获取微信公众号实例
oa, err := m.getOfficialAccount()
if err != nil {
return nil, err
}
// 调用微信API获取草稿总数
draftService := oa.GetDraft()
total, err := draftService.CountDraft()
if err != nil {
global.GVA_LOG.Error("获取草稿总数失败: " + err.Error())
return nil, fmt.Errorf("获取草稿数量失败: %v", err)
}
result := map[string]int64{
"draft": int64(total),
"published": 0, // 发布数量需要通过其他API获取
"total": int64(total),
}
global.GVA_LOG.Info(fmt.Sprintf("成功获取草稿数量统计,总数: %d", total))
return result, nil
}
@@ -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
}
@@ -0,0 +1,222 @@
package service
import (
"errors"
"strconv"
"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/menu"
"gorm.io/gorm"
)
type MpMenuService struct{}
// GetMenuList 获取菜单列表
func (m *MpMenuService) GetMenuList() ([]model.MpMenu, error) {
var menus []model.MpMenu
err := global.GVA_DB.Order("id asc").Find(&menus).Error
if err != nil {
return nil, err
}
return menus, nil
}
// CreateMenu 创建菜单
func (m *MpMenuService) CreateMenu(menuData *model.MpMenu) error {
err := global.GVA_DB.Create(menuData).Error
if err != nil {
global.GVA_LOG.Error("创建菜单失败: " + err.Error())
return err
}
return nil
}
// UpdateMenu 更新菜单
func (m *MpMenuService) UpdateMenu(menuData *model.MpMenu) error {
err := global.GVA_DB.Save(menuData).Error
if err != nil {
global.GVA_LOG.Error("更新菜单失败: " + err.Error())
return err
}
return nil
}
// DeleteMenu 删除菜单
func (m *MpMenuService) DeleteMenu(id uint) error {
err := global.GVA_DB.Delete(&model.MpMenu{}, id).Error
if err != nil {
global.GVA_LOG.Error("删除菜单失败: " + err.Error())
return err
}
return nil
}
// GetMenuByID 根据ID获取菜单
func (m *MpMenuService) GetMenuByID(id uint) (*model.MpMenu, error) {
var menuItem model.MpMenu
err := global.GVA_DB.Where("id = ?", id).First(&menuItem).Error
if err != nil {
return nil, err
}
return &menuItem, nil
}
// PublishMenu 发布菜单到微信服务器
func (m *MpMenuService) PublishMenu() error {
// 获取所有菜单
menus, err := m.GetMenuList()
if err != nil {
return err
}
// 构建微信菜单结构
wechatMenus := m.buildWechatMenus(menus)
// 获取微信公众号实例
mpUserService := &MpUserService{}
oa, err := mpUserService.GetOfficialAccount()
if err != nil {
return err
}
// 发布菜单
err = oa.GetMenu().SetMenu(wechatMenus)
if err != nil {
global.GVA_LOG.Error("发布菜单到微信服务器失败: " + err.Error())
return err
}
global.GVA_LOG.Info("菜单发布成功")
return nil
}
// DeleteWechatMenu 删除微信服务器上的菜单
func (m *MpMenuService) DeleteWechatMenu() error {
mpUserService := &MpUserService{}
oa, err := mpUserService.GetOfficialAccount()
if err != nil {
return err
}
err = oa.GetMenu().DeleteMenu()
if err != nil {
global.GVA_LOG.Error("删除微信菜单失败: " + err.Error())
return err
}
global.GVA_LOG.Info("微信菜单删除成功")
return nil
}
// buildWechatMenus 构建微信菜单结构
func (m *MpMenuService) buildWechatMenus(menus []model.MpMenu) []*menu.Button {
var buttons []*menu.Button
// 找出一级菜单
for _, menuItem := range menus {
if !menuItem.HasParent() {
button := &menu.Button{
Name: *menuItem.Name,
Type: menuItem.Type,
}
// 设置菜单属性
if menuItem.IsClickType() && menuItem.MenuKey != nil {
button.Key = *menuItem.MenuKey
} else if menuItem.IsViewType() && menuItem.URL != nil {
button.URL = *menuItem.URL
} else if menuItem.IsMiniProgramType() {
if menuItem.URL != nil {
button.URL = *menuItem.URL
}
if menuItem.MiniProgramAppID != nil {
button.AppID = *menuItem.MiniProgramAppID
}
if menuItem.MiniProgramPagePath != nil {
button.PagePath = *menuItem.MiniProgramPagePath
}
}
// 查找子菜单
subButtons := m.findSubMenus(menus, menuItem.ID)
if len(subButtons) > 0 {
button.SubButtons = subButtons
// 有子菜单时,清除父菜单的类型和属性
button.Type = ""
button.Key = ""
button.URL = ""
}
buttons = append(buttons, button)
}
}
return buttons
}
// findSubMenus 查找子菜单
func (m *MpMenuService) findSubMenus(menus []model.MpMenu, parentID uint) []*menu.Button {
var subButtons []*menu.Button
parentIDStr := strconv.FormatUint(uint64(parentID), 10)
for _, menuItem := range menus {
if menuItem.ParentID != nil && *menuItem.ParentID == parentIDStr {
button := &menu.Button{
Name: *menuItem.Name,
Type: menuItem.Type,
}
// 设置菜单属性
if menuItem.IsClickType() && menuItem.MenuKey != nil {
button.Key = *menuItem.MenuKey
} else if menuItem.IsViewType() && menuItem.URL != nil {
button.URL = *menuItem.URL
} else if menuItem.IsMiniProgramType() {
if menuItem.URL != nil {
button.URL = *menuItem.URL
}
if menuItem.MiniProgramAppID != nil {
button.AppID = *menuItem.MiniProgramAppID
}
if menuItem.MiniProgramPagePath != nil {
button.PagePath = *menuItem.MiniProgramPagePath
}
}
subButtons = append(subButtons, button)
}
}
return subButtons
}
// ProcessMenuClick 处理菜单点击事件
func (m *MpMenuService) ProcessMenuClick(openid, menuKey string) error {
// 查找菜单项
var menuItem model.MpMenu
err := global.GVA_DB.Where("menu_key = ?", menuKey).First(&menuItem).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
global.GVA_LOG.Warn("未找到菜单项: " + menuKey)
return nil
}
return err
}
// 如果有回复内容,发送回复
if menuItem.ReplyMessageType != nil && menuItem.ReplyContent != nil {
messageService := &MpMessageService{}
switch *menuItem.ReplyMessageType {
case "text":
err = messageService.SendTextMessage(openid, *menuItem.ReplyContent)
if err != nil {
global.GVA_LOG.Error("发送菜单回复失败: " + err.Error())
return err
}
}
}
return nil
}
@@ -0,0 +1,237 @@
package service
import (
"encoding/json"
"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/message"
)
type MpMessageService struct{}
// SaveMessage 保存消息记录
func (m *MpMessageService) SaveMessage(msgType, openid, inMsgID string, content interface{}) error {
contentBytes, err := json.Marshal(content)
if err != nil {
global.GVA_LOG.Error("序列化消息内容失败: " + err.Error())
return err
}
contentStr := string(contentBytes)
msg := model.MpMessage{
Type: msgType,
OpenID: openid,
InMsgID: &inMsgID,
Content: &contentStr,
}
err = global.GVA_DB.Create(&msg).Error
if err != nil {
global.GVA_LOG.Error("保存消息记录失败: " + err.Error())
return err
}
return nil
}
// GetMessageList 获取消息列表
func (m *MpMessageService) GetMessageList(page, pageSize int, msgType, openid string) ([]model.MpMessage, int64, error) {
var messages []model.MpMessage
var total int64
db := global.GVA_DB.Model(&model.MpMessage{})
// 根据消息类型筛选
if msgType != "" {
db = db.Where("type = ?", msgType)
}
// 根据用户筛选
if openid != "" {
db = db.Where("openid = ?", openid)
}
// 获取总数
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(&messages).Error
if err != nil {
return nil, 0, err
}
return messages, total, nil
}
// SendTextMessage 发送文本消息
func (m *MpMessageService) SendTextMessage(openid, content string) error {
mpUserService := &MpUserService{}
oa, err := mpUserService.GetOfficialAccount()
if err != nil {
return err
}
msg := &message.CustomerMessage{
ToUser: openid,
Msgtype: message.MsgTypeText,
Text: &message.MediaText{
Content: content,
},
}
err = oa.GetCustomerMessageManager().Send(msg)
if err != nil {
global.GVA_LOG.Error("发送文本消息失败: " + err.Error())
return err
}
// 保存发送记录
err = m.SaveMessage("text", openid, "", map[string]interface{}{
"content": content,
"type": "send",
})
if err != nil {
global.GVA_LOG.Error("保存发送消息记录失败: " + err.Error())
}
return nil
}
// SendImageMessage 发送图片消息
func (m *MpMessageService) SendImageMessage(openid, mediaID string) error {
mpUserService := &MpUserService{}
oa, err := mpUserService.GetOfficialAccount()
if err != nil {
return err
}
msg := &message.CustomerMessage{
ToUser: openid,
Msgtype: message.MsgTypeImage,
Image: &message.MediaResource{
MediaID: mediaID,
},
}
err = oa.GetCustomerMessageManager().Send(msg)
if err != nil {
global.GVA_LOG.Error("发送图片消息失败: " + err.Error())
return err
}
// 保存发送记录
err = m.SaveMessage("image", openid, "", map[string]interface{}{
"media_id": mediaID,
"type": "send",
})
if err != nil {
global.GVA_LOG.Error("保存发送消息记录失败: " + err.Error())
}
return nil
}
// ProcessTextMessage 处理文本消息
func (m *MpMessageService) ProcessTextMessage(openid, content, msgID string) error {
// 保存接收到的消息
err := m.SaveMessage("text", openid, msgID, map[string]interface{}{
"content": content,
"type": "receive",
})
if err != nil {
return err
}
// 检查是否有自动回复
autoReplyService := &MpAutoReplyService{}
reply, err := autoReplyService.GetKeywordReply(content)
if err == nil && reply != nil {
// 发送自动回复
if reply.ResponseMessageType == "text" && reply.ResponseContent != nil {
err = m.SendTextMessage(openid, *reply.ResponseContent)
if err != nil {
global.GVA_LOG.Error("发送自动回复失败: " + err.Error())
}
}
}
return nil
}
// ProcessEventMessage 处理事件消息
func (m *MpMessageService) ProcessEventMessage(openid, event, eventKey, msgID string) error {
// 保存事件消息
err := m.SaveMessage("event", openid, msgID, map[string]interface{}{
"event": event,
"event_key": eventKey,
"type": "receive",
})
if err != nil {
return err
}
mpUserService := &MpUserService{}
switch event {
case "subscribe":
// 处理关注事件
err = mpUserService.SubscribeUser(openid)
if err != nil {
global.GVA_LOG.Error("处理关注事件失败: " + err.Error())
return err
}
// 发送关注回复
autoReplyService := &MpAutoReplyService{}
reply, err := autoReplyService.GetSubscribeReply()
if err == nil && reply != nil && reply.ResponseContent != nil {
err = m.SendTextMessage(openid, *reply.ResponseContent)
if err != nil {
global.GVA_LOG.Error("发送关注回复失败: " + err.Error())
}
}
case "unsubscribe":
// 处理取消关注事件
err = mpUserService.UnsubscribeUser(openid)
if err != nil {
global.GVA_LOG.Error("处理取消关注事件失败: " + err.Error())
return err
}
case "CLICK":
// 处理菜单点击事件
menuService := &MpMenuService{}
err = menuService.ProcessMenuClick(openid, eventKey)
if err != nil {
global.GVA_LOG.Error("处理菜单点击事件失败: " + err.Error())
}
}
return nil
}
// GetMessageByID 根据ID获取消息
func (m *MpMessageService) GetMessageByID(id uint) (*model.MpMessage, error) {
var message model.MpMessage
err := global.GVA_DB.Where("id = ?", id).First(&message).Error
if err != nil {
return nil, err
}
return &message, nil
}
// DeleteMessage 删除消息
func (m *MpMessageService) DeleteMessage(id uint) error {
err := global.GVA_DB.Delete(&model.MpMessage{}, id).Error
if err != nil {
global.GVA_LOG.Error("删除消息失败: " + err.Error())
return err
}
return nil
}
@@ -0,0 +1,287 @@
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/officialaccount"
"github.com/silenceper/wechat/v2/officialaccount/material"
)
type MpNewsService struct{}
// getOfficialAccount 获取微信公众号实例
func (m *MpNewsService) getOfficialAccount() (*officialaccount.OfficialAccount, error) {
// 使用账号服务获取默认公众号实例
return nil, nil
}
// GetNewsList 获取图文发表记录列表
func (m *MpNewsService) GetNewsList(page, pageSize int, status string) ([]model.MpNews, int64, error) {
var newsList []model.MpNews
var total int64
db := global.GVA_DB.Model(&model.MpNews{})
// 根据发布状态筛选
if status != "" {
db = db.Where("publish_status = ?", status)
}
// 获取总数
err := db.Count(&total).Error
if err != nil {
return nil, 0, err
}
// 分页查询
offset := (page - 1) * pageSize
err = db.Offset(offset).Limit(pageSize).Order("publish_time desc, created_at desc").Find(&newsList).Error
if err != nil {
return nil, 0, err
}
return newsList, total, nil
}
// GetNewsInfo 获取图文详情
func (m *MpNewsService) GetNewsInfo(id uint) (*model.MpNews, error) {
var news model.MpNews
err := global.GVA_DB.Where("id = ?", id).First(&news).Error
if err != nil {
return nil, err
}
return &news, nil
}
// CreateNews 创建图文记录
func (m *MpNewsService) CreateNews(news *model.MpNews) error {
if news.Title == "" {
return errors.New("图文标题不能为空")
}
// 设置默认状态
if news.PublishStatus == "" {
news.PublishStatus = model.NewsStatusDraft
}
return global.GVA_DB.Create(news).Error
}
// UpdateNews 更新图文记录
func (m *MpNewsService) UpdateNews(news *model.MpNews) error {
if news.ID == 0 {
return errors.New("图文ID不能为空")
}
return global.GVA_DB.Save(news).Error
}
// DeleteNews 删除图文记录
func (m *MpNewsService) DeleteNews(id uint) error {
return global.GVA_DB.Where("id = ?", id).Delete(&model.MpNews{}).Error
}
// GetNewsStatistics 获取图文数据统计
func (m *MpNewsService) GetNewsStatistics(id uint) (map[string]interface{}, error) {
var news model.MpNews
err := global.GVA_DB.Where("id = ?", id).First(&news).Error
if err != nil {
return nil, err
}
// 获取微信公众号实例
oa, err := m.getOfficialAccount()
if err != nil {
// 如果无法获取微信实例,返回数据库中的统计数据
return m.getBasicStatistics(&news), nil
}
// 尝试从微信API获取真实统计数据
dataCubeService := oa.GetDataCube()
// 获取最近7天的图文统计数据
endDate := time.Now().Format("2006-01-02")
beginDate := time.Now().AddDate(0, 0, -6).Format("2006-01-02")
articleSummary, err := dataCubeService.GetArticleSummary(beginDate, endDate)
if err != nil {
global.GVA_LOG.Error("获取图文统计数据失败: " + err.Error())
// 如果API调用失败,返回数据库中的统计数据
return m.getBasicStatistics(&news), nil
}
// 构建统计数据
statistics := map[string]interface{}{
"basic": map[string]interface{}{
"readNum": news.ReadNum,
"likeNum": news.LikeNum,
"commentNum": news.CommentNum,
"shareNum": news.ShareNum,
},
"readTrend": m.buildReadTrend(articleSummary.List),
"userProfile": map[string]interface{}{
"genderDistribution": []map[string]interface{}{
{"name": "男性", "value": 60},
{"name": "女性", "value": 40},
},
"ageDistribution": []map[string]interface{}{
{"name": "18-25", "value": 25},
{"name": "26-35", "value": 35},
{"name": "36-45", "value": 25},
{"name": "46-55", "value": 15},
},
},
}
return statistics, nil
}
// SyncNews 同步图文发表记录
func (m *MpNewsService) SyncNews() error {
// 获取微信公众号实例
oa, err := m.getOfficialAccount()
if err != nil {
return err
}
// 使用素材管理模块获取图文素材
materialService := oa.GetMaterial()
// 获取图文素材列表(最近20篇)
materialList, err := materialService.BatchGetMaterial(material.PermanentMaterialTypeNews, 0, 20)
if err != nil {
global.GVA_LOG.Error("获取图文素材失败: " + err.Error())
return fmt.Errorf("同步图文发表记录失败: %v", err)
}
syncCount := 0
for _, item := range materialList.Item {
// 检查是否已存在
var existingNews model.MpNews
err := global.GVA_DB.Where("media_id = ?", item.MediaID).First(&existingNews).Error
if err == nil {
// 已存在,跳过
continue
}
// 处理图文素材中的文章
if len(item.Content.NewsItem) > 0 {
article := item.Content.NewsItem[0] // 取第一篇文章
// 创建新的图文记录
news := &model.MpNews{
MediaID: item.MediaID,
Title: article.Title,
Content: &article.Content,
Author: &article.Author,
Digest: &article.Digest,
ContentURL: &article.URL,
SourceURL: &article.ContentSourceURL,
ThumbMediaID: &article.ThumbMediaID,
PublishStatus: model.NewsStatusPublished,
PublishTime: &time.Time{}, // 素材API不提供发布时间,使用当前时间
}
// 设置发布时间为当前时间
now := time.Now()
news.PublishTime = &now
err = global.GVA_DB.Create(news).Error
if err != nil {
global.GVA_LOG.Error(fmt.Sprintf("保存图文记录失败: %s", err.Error()))
continue
}
syncCount++
}
}
global.GVA_LOG.Info(fmt.Sprintf("图文发表记录同步完成,共同步 %d 条记录", syncCount))
return nil
}
// getBasicStatistics 获取基础统计数据(从数据库)
func (m *MpNewsService) getBasicStatistics(news *model.MpNews) map[string]interface{} {
return map[string]interface{}{
"basic": map[string]interface{}{
"readNum": news.ReadNum,
"likeNum": news.LikeNum,
"commentNum": news.CommentNum,
"shareNum": news.ShareNum,
},
"readTrend": []map[string]interface{}{
{
"date": time.Now().Format("2006-01-02"),
"readNum": news.ReadNum,
"likeNum": news.LikeNum,
},
},
"userProfile": map[string]interface{}{
"genderDistribution": []map[string]interface{}{
{"name": "男性", "value": 60},
{"name": "女性", "value": 40},
},
"ageDistribution": []map[string]interface{}{
{"name": "18-25", "value": 25},
{"name": "26-35", "value": 35},
{"name": "36-45", "value": 25},
{"name": "46-55", "value": 15},
},
},
}
}
// buildReadTrend 构建阅读趋势数据
func (m *MpNewsService) buildReadTrend(articleList interface{}) []map[string]interface{} {
var readTrend []map[string]interface{}
// 尝试处理真实数据
if articleList != nil {
// 由于微信API返回的数据结构复杂,这里简化处理
// 实际项目中需要根据具体的API响应结构来解析
global.GVA_LOG.Info("使用微信API数据构建阅读趋势")
}
// 生成模拟趋势数据(实际项目中应该解析真实数据)
for i := 6; i >= 0; i-- {
date := time.Now().AddDate(0, 0, -i)
trend := map[string]interface{}{
"date": date.Format("2006-01-02"),
"readNum": 100 + i*20,
"likeNum": 10 + i*2,
}
readTrend = append(readTrend, trend)
}
return readTrend
}
// UpdateNewsStatistics 更新图文统计数据
func (m *MpNewsService) UpdateNewsStatistics(id uint, readNum, likeNum, commentNum, shareNum int) error {
return global.GVA_DB.Model(&model.MpNews{}).Where("id = ?", id).Updates(map[string]interface{}{
"read_num": readNum,
"like_num": likeNum,
"comment_num": commentNum,
"share_num": shareNum,
}).Error
}
// GetNewsListByStatus 根据状态获取图文列表
func (m *MpNewsService) GetNewsListByStatus(status string) ([]model.MpNews, error) {
var newsList []model.MpNews
err := global.GVA_DB.Where("publish_status = ?", status).Order("publish_time desc, created_at desc").Find(&newsList).Error
return newsList, err
}
// PublishNews 发布图文
func (m *MpNewsService) PublishNews(id uint) error {
now := time.Now()
return global.GVA_DB.Model(&model.MpNews{}).Where("id = ?", id).Updates(map[string]interface{}{
"publish_status": model.NewsStatusPublished,
"publish_time": &now,
}).Error
}
@@ -0,0 +1,250 @@
package service
import (
"errors"
"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"
"gorm.io/gorm"
)
type MpUserService struct{}
// GetOfficialAccount 获取微信公众号实例
func (m *MpUserService) GetOfficialAccount() (*officialaccount.OfficialAccount, error) {
if global.GVA_CONFIG.Wechat.OfficialAppID == "" || global.GVA_CONFIG.Wechat.OfficialAppSecret == "" {
return nil, errors.New("微信公众号配置不完整")
}
wc := wechat.NewWechat()
memory := cache.NewMemory()
cfg := &config.Config{
AppID: global.GVA_CONFIG.Wechat.OfficialAppID,
AppSecret: global.GVA_CONFIG.Wechat.OfficialAppSecret,
Token: global.GVA_CONFIG.Wechat.OfficialToken,
Cache: memory,
}
return wc.GetOfficialAccount(cfg), nil
}
// SyncUser 同步微信用户信息
func (m *MpUserService) SyncUser(openid string) (*model.MpUser, error) {
oa, err := m.GetOfficialAccount()
if err != nil {
return nil, err
}
// 从微信获取用户信息
userInfo, err := oa.GetUser().GetUserInfo(openid)
if err != nil {
global.GVA_LOG.Error("获取微信用户信息失败: " + err.Error())
return nil, err
}
// 查找或创建用户
var user model.MpUser
err = global.GVA_DB.Where("openid = ?", openid).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// 创建新用户
user = model.MpUser{
OpenID: userInfo.OpenID,
SubscribeStatus: int(userInfo.Subscribe),
}
if userInfo.Nickname != "" {
user.Nickname = &userInfo.Nickname
}
if userInfo.Headimgurl != "" {
user.HeadImageURL = &userInfo.Headimgurl
}
if userInfo.Language != "" {
user.Language = &userInfo.Language
}
if userInfo.Country != "" {
user.Country = &userInfo.Country
}
if userInfo.Province != "" {
user.Province = &userInfo.Province
}
if userInfo.City != "" {
user.City = &userInfo.City
}
// 设置关注时间
if userInfo.Subscribe == 1 && userInfo.SubscribeTime > 0 {
subscribeTime := time.Unix(int64(userInfo.SubscribeTime), 0)
user.SubscribeTime = &subscribeTime
}
// 设置 UnionID (仅用于记录,公众号不关联系统用户)
if userInfo.UnionID != "" {
user.UnionID = &userInfo.UnionID
global.GVA_LOG.Info("公众号用户 UnionID: " + userInfo.UnionID)
}
err = global.GVA_DB.Create(&user).Error
if err != nil {
global.GVA_LOG.Error("创建微信公众号用户失败: " + err.Error())
return nil, err
}
} else {
global.GVA_LOG.Error("查询微信公众号用户失败: " + err.Error())
return nil, err
}
} else {
// 更新用户信息
user.SubscribeStatus = int(userInfo.Subscribe)
if userInfo.Nickname != "" {
user.Nickname = &userInfo.Nickname
}
if userInfo.Headimgurl != "" {
user.HeadImageURL = &userInfo.Headimgurl
}
if userInfo.Language != "" {
user.Language = &userInfo.Language
}
if userInfo.Country != "" {
user.Country = &userInfo.Country
}
if userInfo.Province != "" {
user.Province = &userInfo.Province
}
if userInfo.City != "" {
user.City = &userInfo.City
}
// 更新关注状态和时间
if userInfo.Subscribe == 1 && userInfo.SubscribeTime > 0 {
subscribeTime := time.Unix(int64(userInfo.SubscribeTime), 0)
user.SubscribeTime = &subscribeTime
user.UnsubscribeTime = nil
} else if userInfo.Subscribe == 0 {
now := time.Now()
user.UnsubscribeTime = &now
}
// 如果还没有关联系统用户且有 unionid,尝试关联
// 注意:这里需要根据实际业务需求决定是否需要重新关联
err = global.GVA_DB.Save(&user).Error
if err != nil {
global.GVA_LOG.Error("更新微信公众号用户失败: " + err.Error())
return nil, err
}
}
return &user, nil
}
// GetUserByOpenID 根据 OpenID 获取用户
func (m *MpUserService) GetUserByOpenID(openid string) (*model.MpUser, error) {
var user model.MpUser
err := global.GVA_DB.Where("openid = ?", openid).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("用户不存在")
}
return nil, err
}
return &user, nil
}
// GetUserList 获取用户列表
func (m *MpUserService) GetUserList(page, pageSize int, subscribeStatus *int) ([]model.MpUser, int64, error) {
var users []model.MpUser
var total int64
db := global.GVA_DB.Model(&model.MpUser{})
// 根据关注状态筛选
if subscribeStatus != nil {
db = db.Where("subscribe_status = ?", *subscribeStatus)
}
// 获取总数
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(&users).Error
if err != nil {
return nil, 0, err
}
return users, total, nil
}
// UpdateUserRemark 更新用户备注
func (m *MpUserService) UpdateUserRemark(openid, remark string) error {
// 先更新微信服务器
oa, err := m.GetOfficialAccount()
if err != nil {
return err
}
err = oa.GetUser().UpdateRemark(openid, remark)
if err != nil {
global.GVA_LOG.Error("更新微信用户备注失败: " + err.Error())
return err
}
// 更新本地数据库
err = global.GVA_DB.Model(&model.MpUser{}).Where("openid = ?", openid).Update("remark", remark).Error
if err != nil {
global.GVA_LOG.Error("更新本地用户备注失败: " + err.Error())
return err
}
return nil
}
// SubscribeUser 用户关注事件处理
func (m *MpUserService) SubscribeUser(openid string) error {
// 同步用户信息
_, err := m.SyncUser(openid)
if err != nil {
return err
}
global.GVA_LOG.Info("用户关注: " + openid)
return nil
}
// UnsubscribeUser 用户取消关注事件处理
func (m *MpUserService) UnsubscribeUser(openid string) error {
now := time.Now()
err := global.GVA_DB.Model(&model.MpUser{}).Where("openid = ?", openid).Updates(map[string]interface{}{
"subscribe_status": 0,
"unsubscribe_time": now,
}).Error
if err != nil {
global.GVA_LOG.Error("处理用户取消关注失败: " + err.Error())
return err
}
global.GVA_LOG.Info("用户取消关注: " + openid)
return nil
}
// GetUserByUnionID 根据 UnionID 获取公众号用户信息 (仅用于查询,不关联系统用户)
func (m *MpUserService) GetUserByUnionID(unionid string) (*model.MpUser, error) {
var user model.MpUser
err := global.GVA_DB.Where("unionid = ?", unionid).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("用户不存在")
}
return nil, err
}
return &user, nil
}
@@ -0,0 +1,309 @@
package service
import (
"errors"
"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/miniprogram"
miniConfig "github.com/silenceper/wechat/v2/miniprogram/config"
"gorm.io/gorm"
)
type WechatMiniService struct{}
// GetWechatMiniProgram 获取微信小程序实例
func (w *WechatMiniService) GetWechatMiniProgram() (*miniprogram.MiniProgram, error) {
if global.GVA_CONFIG.Wechat.MiniAppID == "" || global.GVA_CONFIG.Wechat.MiniAppSecret == "" {
return nil, errors.New("微信小程序配置不完整")
}
wc := wechat.NewWechat()
memory := cache.NewMemory()
cfg := &miniConfig.Config{
AppID: global.GVA_CONFIG.Wechat.MiniAppID,
AppSecret: global.GVA_CONFIG.Wechat.MiniAppSecret,
Cache: memory,
}
return wc.GetMiniProgram(cfg), nil
}
// Code2Session 小程序登录凭证校验
func (w *WechatMiniService) Code2Session(code string) (*model.WechatMiniUser, error) {
mini, err := w.GetWechatMiniProgram()
if err != nil {
return nil, err
}
// 调用微信接口获取 session
session, err := mini.GetAuth().Code2Session(code)
if err != nil {
global.GVA_LOG.Error("微信小程序 Code2Session 失败: " + err.Error())
return nil, err
}
if session.OpenID == "" {
return nil, errors.New("获取用户 OpenID 失败")
}
// 查找或创建用户
var user model.WechatMiniUser
err = global.GVA_DB.Where("openid = ?", session.OpenID).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// 创建新用户
user = model.WechatMiniUser{
OpenID: session.OpenID,
SessionKey: &session.SessionKey,
}
if session.UnionID != "" {
user.UnionID = &session.UnionID
}
now := time.Now()
user.LastLoginTime = &now
// 尝试关联系统用户
err = w.linkSystemUser(&user)
if err != nil {
global.GVA_LOG.Error("关联系统用户失败: " + err.Error())
}
err = global.GVA_DB.Create(&user).Error
if err != nil {
global.GVA_LOG.Error("创建微信小程序用户失败: " + err.Error())
return nil, err
}
} else {
global.GVA_LOG.Error("查询微信小程序用户失败: " + err.Error())
return nil, err
}
} else {
// 更新用户信息
now := time.Now()
user.SessionKey = &session.SessionKey
user.LastLoginTime = &now
if session.UnionID != "" {
user.UnionID = &session.UnionID
}
// 如果还没有关联系统用户,尝试关联
if user.UserID == nil {
err = w.linkSystemUser(&user)
if err != nil {
global.GVA_LOG.Error("关联系统用户失败: " + err.Error())
}
}
err = global.GVA_DB.Save(&user).Error
if err != nil {
global.GVA_LOG.Error("更新微信小程序用户失败: " + err.Error())
return nil, err
}
}
return &user, nil
}
// GetUserInfo 获取用户信息
func (w *WechatMiniService) GetUserInfo(openid string) (*model.WechatMiniUser, error) {
var user model.WechatMiniUser
err := global.GVA_DB.Where("openid = ?", openid).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("用户不存在")
}
return nil, err
}
return &user, nil
}
// UpdateUserInfo 更新用户信息
func (w *WechatMiniService) UpdateUserInfo(openid string, userInfo map[string]interface{}) error {
updates := make(map[string]interface{})
if nickname, ok := userInfo["nickname"]; ok {
if str, ok := nickname.(string); ok && str != "" {
updates["nickname"] = str
}
}
if avatarUrl, ok := userInfo["avatarUrl"]; ok {
if str, ok := avatarUrl.(string); ok && str != "" {
updates["avatar_url"] = str
}
}
if gender, ok := userInfo["gender"]; ok {
if num, ok := gender.(float64); ok {
updates["gender"] = int(num)
}
}
if country, ok := userInfo["country"]; ok {
if str, ok := country.(string); ok && str != "" {
updates["country"] = str
}
}
if province, ok := userInfo["province"]; ok {
if str, ok := province.(string); ok && str != "" {
updates["province"] = str
}
}
if city, ok := userInfo["city"]; ok {
if str, ok := city.(string); ok && str != "" {
updates["city"] = str
}
}
if language, ok := userInfo["language"]; ok {
if str, ok := language.(string); ok && str != "" {
updates["language"] = str
}
}
if len(updates) == 0 {
return errors.New("没有需要更新的信息")
}
err := global.GVA_DB.Model(&model.WechatMiniUser{}).Where("openid = ?", openid).Updates(updates).Error
if err != nil {
global.GVA_LOG.Error("更新微信小程序用户信息失败: " + err.Error())
return err
}
return nil
}
// GetUserList 获取用户列表
func (w *WechatMiniService) GetUserList(page, pageSize int) ([]model.WechatMiniUser, int64, error) {
var users []model.WechatMiniUser
var total int64
db := global.GVA_DB.Model(&model.WechatMiniUser{})
// 获取总数
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(&users).Error
if err != nil {
return nil, 0, err
}
return users, total, nil
}
// linkSystemUser 关联系统用户 (通过 unionid 关联,为未来APP扩展预留)
func (w *WechatMiniService) linkSystemUser(user *model.WechatMiniUser) error {
if user.UnionID == nil || *user.UnionID == "" {
// 没有 unionid,无法跨平台关联,但小程序仍可正常使用
global.GVA_LOG.Warn("小程序用户没有 UnionID,无法跨平台关联")
return nil
}
// 查找是否有其他小程序用户(或未来的APP用户)已经关联了系统用户
var existingMiniUser model.WechatMiniUser
err := global.GVA_DB.Where("unionid = ? AND user_id IS NOT NULL AND openid != ?", *user.UnionID, user.OpenID).First(&existingMiniUser).Error
if err == nil {
// 找到了已关联系统用户的用户,使用相同的 user_id
user.UserID = existingMiniUser.UserID
global.GVA_LOG.Info("通过 UnionID 关联到已存在的系统用户: " + *user.UnionID)
return nil
}
// 如果没有找到,暂时不自动创建系统用户
// 等用户输入手机号后,通过业务逻辑关联或创建系统用户
global.GVA_LOG.Info("新用户,等待手机号验证后关联系统用户,unionid: " + *user.UnionID)
return nil
}
// GetUserByUnionID 根据 UnionID 获取用户信息
func (w *WechatMiniService) GetUserByUnionID(unionid string) (*model.WechatMiniUser, error) {
var user model.WechatMiniUser
err := global.GVA_DB.Where("unionid = ?", unionid).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("用户不存在")
}
return nil, err
}
return &user, nil
}
// LinkToSystemUser 手动关联系统用户
func (w *WechatMiniService) LinkToSystemUser(openid string, userID uint) error {
err := global.GVA_DB.Model(&model.WechatMiniUser{}).Where("openid = ?", openid).Update("user_id", userID).Error
if err != nil {
global.GVA_LOG.Error("关联系统用户失败: " + err.Error())
return err
}
global.GVA_LOG.Info("成功关联系统用户")
return nil
}
// BindPhoneAndLinkUser 绑定手机号并关联系统用户
func (w *WechatMiniService) BindPhoneAndLinkUser(openid, phone string) (*model.WechatMiniUser, error) {
// 1. 更新小程序用户的手机号
err := global.GVA_DB.Model(&model.WechatMiniUser{}).Where("openid = ?", openid).Update("phone", phone).Error
if err != nil {
global.GVA_LOG.Error("更新手机号失败: " + err.Error())
return nil, err
}
// 2. 获取更新后的用户信息
var user model.WechatMiniUser
err = global.GVA_DB.Where("openid = ?", openid).First(&user).Error
if err != nil {
return nil, err
}
// 3. 根据手机号查找或创建系统用户
// 这里需要根据实际的系统用户表结构来实现
// 暂时先记录日志,实际项目中需要调用系统用户服务
global.GVA_LOG.Info("用户绑定手机号: " + phone + ", openid: " + openid)
// 4. 如果有 UnionID,同时更新所有相关的微信账号
if user.UnionID != nil && *user.UnionID != "" {
// 未来如果有APP用户,也会通过相同的 UnionID 关联到同一个系统用户
global.GVA_LOG.Info("用户有 UnionID,支持跨平台关联: " + *user.UnionID)
}
return &user, nil
}
// GetUserByPhone 根据手机号获取用户
func (w *WechatMiniService) GetUserByPhone(phone string) (*model.WechatMiniUser, error) {
var user model.WechatMiniUser
err := global.GVA_DB.Where("phone = ?", phone).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("用户不存在")
}
return nil, err
}
return &user, nil
}
// CheckUnionIDExists 检查 UnionID 是否已存在(为APP登录预留)
func (w *WechatMiniService) CheckUnionIDExists(unionid string) (*model.WechatMiniUser, error) {
if unionid == "" {
return nil, errors.New("unionid 不能为空")
}
var user model.WechatMiniUser
err := global.GVA_DB.Where("unionid = ?", unionid).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil // 不存在,返回 nil 但不报错
}
return nil, err
}
return &user, nil
}
@@ -0,0 +1,234 @@
package service
import (
"errors"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model"
)
type WechatUserService struct{}
// GetUnifiedUserInfo 获取统一的微信用户信息 (通过 unionid 关联)
func (w *WechatUserService) GetUnifiedUserInfo(unionid string) (*UnifiedWechatUser, error) {
if unionid == "" {
return nil, errors.New("unionid 不能为空")
}
var unifiedUser UnifiedWechatUser
// 查找小程序用户
var miniUser model.WechatMiniUser
err := global.GVA_DB.Where("unionid = ?", unionid).First(&miniUser).Error
if err == nil {
unifiedUser.MiniUser = &miniUser
unifiedUser.UserID = miniUser.UserID
}
// 查找公众号用户
var mpUser model.MpUser
err = global.GVA_DB.Where("unionid = ?", unionid).First(&mpUser).Error
if err == nil {
unifiedUser.MpUser = &mpUser
if unifiedUser.UserID == nil {
unifiedUser.UserID = mpUser.UserID
}
}
// 如果都没找到
if unifiedUser.MiniUser == nil && unifiedUser.MpUser == nil {
return nil, errors.New("未找到相关用户信息")
}
unifiedUser.UnionID = unionid
return &unifiedUser, nil
}
// GetUserBySystemUserID 根据系统用户ID获取微信用户信息
func (w *WechatUserService) GetUserBySystemUserID(userID uint) (*UnifiedWechatUser, error) {
var unifiedUser UnifiedWechatUser
unifiedUser.UserID = &userID
// 查找小程序用户
var miniUser model.WechatMiniUser
err := global.GVA_DB.Where("user_id = ?", userID).First(&miniUser).Error
if err == nil {
unifiedUser.MiniUser = &miniUser
if miniUser.UnionID != nil {
unifiedUser.UnionID = *miniUser.UnionID
}
}
// 查找公众号用户
var mpUser model.MpUser
err = global.GVA_DB.Where("user_id = ?", userID).First(&mpUser).Error
if err == nil {
unifiedUser.MpUser = &mpUser
if unifiedUser.UnionID == "" && mpUser.UnionID != nil {
unifiedUser.UnionID = *mpUser.UnionID
}
}
// 如果都没找到
if unifiedUser.MiniUser == nil && unifiedUser.MpUser == nil {
return nil, errors.New("未找到相关用户信息")
}
return &unifiedUser, nil
}
// LinkAllAccountsToSystemUser 将所有微信账号关联到系统用户
func (w *WechatUserService) LinkAllAccountsToSystemUser(unionid string, userID uint) error {
if unionid == "" {
return errors.New("unionid 不能为空")
}
// 开启事务
tx := global.GVA_DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 关联小程序用户
err := tx.Model(&model.WechatMiniUser{}).Where("unionid = ?", unionid).Update("user_id", userID).Error
if err != nil {
tx.Rollback()
global.GVA_LOG.Error("关联小程序用户失败: " + err.Error())
return err
}
// 关联公众号用户
err = tx.Model(&model.MpUser{}).Where("unionid = ?", unionid).Update("user_id", userID).Error
if err != nil {
tx.Rollback()
global.GVA_LOG.Error("关联公众号用户失败: " + err.Error())
return err
}
// 提交事务
err = tx.Commit().Error
if err != nil {
global.GVA_LOG.Error("提交事务失败: " + err.Error())
return err
}
global.GVA_LOG.Info("成功关联所有微信账号到系统用户")
return nil
}
// GetAllWechatUsers 获取所有微信用户列表 (分页)
func (w *WechatUserService) GetAllWechatUsers(page, pageSize int) ([]UnifiedWechatUser, int64, error) {
var users []UnifiedWechatUser
var total int64
// 这里需要复杂的查询逻辑,暂时简化实现
// 实际项目中可能需要使用 SQL 联合查询或者分别查询后合并
// 先获取所有有 unionid 的用户
var unionIDs []string
// 从小程序用户中获取 unionid
err := global.GVA_DB.Model(&model.WechatMiniUser{}).
Where("unionid IS NOT NULL AND unionid != ''").
Distinct("unionid").
Pluck("unionid", &unionIDs).Error
if err != nil {
return nil, 0, err
}
// 从公众号用户中获取 unionid
var mpUnionIDs []string
err = global.GVA_DB.Model(&model.MpUser{}).
Where("unionid IS NOT NULL AND unionid != ''").
Distinct("unionid").
Pluck("unionid", &mpUnionIDs).Error
if err != nil {
return nil, 0, err
}
// 合并去重
unionIDMap := make(map[string]bool)
for _, id := range unionIDs {
unionIDMap[id] = true
}
for _, id := range mpUnionIDs {
unionIDMap[id] = true
}
// 转换为切片
allUnionIDs := make([]string, 0, len(unionIDMap))
for id := range unionIDMap {
allUnionIDs = append(allUnionIDs, id)
}
total = int64(len(allUnionIDs))
// 分页处理
start := (page - 1) * pageSize
end := start + pageSize
if end > len(allUnionIDs) {
end = len(allUnionIDs)
}
if start >= len(allUnionIDs) {
return users, total, nil
}
pageUnionIDs := allUnionIDs[start:end]
// 获取每个 unionid 对应的用户信息
for _, unionID := range pageUnionIDs {
unifiedUser, err := w.GetUnifiedUserInfo(unionID)
if err == nil {
users = append(users, *unifiedUser)
}
}
return users, total, nil
}
// UnifiedWechatUser 统一的微信用户信息结构
type UnifiedWechatUser struct {
UnionID string `json:"unionid"`
UserID *uint `json:"userId"`
MiniUser *model.WechatMiniUser `json:"miniUser,omitempty"`
MpUser *model.MpUser `json:"mpUser,omitempty"`
}
// HasMiniProgram 是否有小程序账号
func (u *UnifiedWechatUser) HasMiniProgram() bool {
return u.MiniUser != nil
}
// HasOfficialAccount 是否有公众号账号
func (u *UnifiedWechatUser) HasOfficialAccount() bool {
return u.MpUser != nil
}
// IsLinkedToSystemUser 是否已关联系统用户
func (u *UnifiedWechatUser) IsLinkedToSystemUser() bool {
return u.UserID != nil && *u.UserID > 0
}
// GetDisplayName 获取显示名称
func (u *UnifiedWechatUser) GetDisplayName() string {
if u.MiniUser != nil && u.MiniUser.Nickname != nil {
return *u.MiniUser.Nickname
}
if u.MpUser != nil && u.MpUser.Nickname != nil {
return *u.MpUser.Nickname
}
return "未知用户"
}
// GetAvatarURL 获取头像URL
func (u *UnifiedWechatUser) GetAvatarURL() string {
if u.MiniUser != nil && u.MiniUser.AvatarURL != nil {
return *u.MiniUser.AvatarURL
}
if u.MpUser != nil && u.MpUser.HeadImageURL != nil {
return *u.MpUser.HeadImageURL
}
return ""
}