@@ -0,0 +1,34 @@ | |||
package model | |||
import ( | |||
"time" | |||
) | |||
type SysModule struct { | |||
ModId int `json:"mod_id" xorm:"not null pk autoincr INT(10)"` | |||
ModPid int `json:"mod_pid" xorm:"not null default 0 comment('父级模块ID') INT(10)"` | |||
TemplateId int `json:"template_id" xorm:"not null default 0 comment('模板ID') INT(11)"` | |||
ModName string `json:"mod_name" xorm:"not null default '' comment('模块名称') VARCHAR(250)"` | |||
Position string `json:"position" xorm:"not null default '' comment('位置') VARCHAR(250)"` | |||
SkipIdentifier string `json:"skip_identifier" xorm:"not null default '' comment('跳转标识') VARCHAR(250)"` | |||
Title string `json:"title" xorm:"not null default '' comment('标题') VARCHAR(128)"` | |||
Subtitle string `json:"subtitle" xorm:"not null default '' comment('副标题') VARCHAR(255)"` | |||
Url string `json:"url" xorm:"not null default '' comment('跳转链接') VARCHAR(512)"` | |||
Margin string `json:"margin" xorm:"not null default '0,0,0,0' comment('边距,上右下左') VARCHAR(64)"` | |||
AspectRatio string `json:"aspect_ratio" xorm:"not null default 0.00 comment('宽高比,宽/高保留两位小数') DECIMAL(4,2)"` | |||
Icon string `json:"icon" xorm:"not null default '' comment('图标') VARCHAR(512)"` | |||
Img string `json:"img" xorm:"not null default '' comment('图片') VARCHAR(512)"` | |||
FontColor string `json:"font_color" xorm:"not null default '' comment('文字颜色') VARCHAR(128)"` | |||
BgImg string `json:"bg_img" xorm:"not null default '' comment('背景图片') VARCHAR(512)"` | |||
BgColor string `json:"bg_color" xorm:"not null default '' comment('背景颜色') VARCHAR(512)"` | |||
BgColorT string `json:"bg_color_t" xorm:"not null default '' comment('背景颜色过度') VARCHAR(255)"` | |||
Badge string `json:"badge" xorm:"not null default '' comment('badge图片') VARCHAR(512)"` | |||
Path string `json:"path" xorm:"not null default '' comment('跳转路径') VARCHAR(255)"` | |||
Data string `json:"data" xorm:"comment('内容') TEXT"` | |||
Sort int `json:"sort" xorm:"not null default 0 comment('排序') INT(11)"` | |||
State int `json:"state" xorm:"not null default 1 comment('0不显示,1显示') TINYINT(1)"` | |||
IsGlobal int `json:"is_global" xorm:"not null default 0 comment('是否全局显示') TINYINT(1)"` | |||
Platform int `json:"platform" xorm:"not null default 1 comment('平台;1:全平台;2:App应用(ios和android);3:H5(wap);4:微信小程序;5:抖音小程序;6:百度小程序') TINYINT(1)"` | |||
CreateAt time.Time `json:"create_at" xorm:"not null default CURRENT_TIMESTAMP comment('创建时间') TIMESTAMP"` | |||
UpdateAt time.Time `json:"update_at" xorm:"not null default CURRENT_TIMESTAMP comment('更新时间') TIMESTAMP"` | |||
} |
@@ -0,0 +1,36 @@ | |||
package hdl | |||
import ( | |||
"applet/app/e" | |||
"applet/app/svc" | |||
"applet/app/utils" | |||
"fmt" | |||
"github.com/gin-gonic/gin" | |||
"github.com/tidwall/gjson" | |||
) | |||
func ImgUpload(c *gin.Context) { | |||
file, err := c.FormFile("file") | |||
if err != nil { | |||
e.OutErr(c, 400, e.NewErr(400, "上传图片失败")) | |||
return | |||
} | |||
fileStr := "./public/img/" + file.Filename | |||
c.SaveUploadedFile(file, fileStr) | |||
res := map[string]string{ | |||
"fileName": "http://ywym.jiaxiandingding.top/public/img/" + file.Filename, | |||
"saveName": "public/img/" + file.Filename, | |||
} | |||
token, err := svc.GetWechatToken() | |||
if err != nil { | |||
e.OutErr(c, 400, err.Error()) | |||
return | |||
} | |||
uri := "https://api.weixin.qq.com/cgi-bin/material/add_material?access_token" + token + "&type=image" | |||
postFile, err := utils.PostFile("media", res["fileName"], uri) | |||
fmt.Println(postFile) | |||
fmt.Println(err) | |||
res["media_id"] = gjson.Get(string(postFile), "media_id").String() | |||
e.OutSuc(c, res, nil) | |||
return | |||
} |
@@ -5,13 +5,16 @@ import ( | |||
"applet/app/db/model" | |||
"applet/app/enum" | |||
"applet/app/md" | |||
"applet/app/svc" | |||
"applet/app/utils" | |||
"encoding/json" | |||
"encoding/xml" | |||
"errors" | |||
"fmt" | |||
"github.com/tidwall/gjson" | |||
"log" | |||
"net/http" | |||
"strings" | |||
"time" | |||
"github.com/gin-gonic/gin" | |||
@@ -110,6 +113,22 @@ func WXMsgReceive(c *gin.Context) { | |||
WXMsgReply(c, eventMsg.ToUserName, eventMsg.FromUserName, err.Error()) | |||
} | |||
} | |||
if eventMsg.Event == "click" { //公众号点击 | |||
if strings.Contains(strings.ToLower(eventMsg.EventKey), "official_account_custom_reply") { | |||
id := strings.ReplaceAll(strings.ToLower(eventMsg.EventKey), "official_account_custom_reply_", "") | |||
var mod model.SysModule | |||
db.Db.Where("mod_id=?", id).Get(&mod) | |||
if mod.Data == "" { | |||
return | |||
} | |||
if gjson.Get(mod.Data, "msgType").String() == "text" { | |||
svc.WXMsgTextReply(c, msg.ToUserName, msg.FromUserName, gjson.Get(mod.Data, "text.content").String()) | |||
} | |||
if gjson.Get(mod.Data, "msgType").String() == "image" { | |||
svc.WXMsgImageReply(c, msg.ToUserName, msg.FromUserName, gjson.Get(mod.Data, "image.mediaId").String()) | |||
} | |||
} | |||
} | |||
} | |||
if msg.MsgType == "text" { | |||
//文本类型消息 | |||
@@ -1,21 +1,37 @@ | |||
package md | |||
type WechatButton struct { | |||
Name string `json:"name"` | |||
Type string `json:"type"` | |||
Url string `json:"url"` | |||
Appid string `json:"appid,omitempty"` | |||
Pagepath string `json:"pagepath"` | |||
Key string `json:"key,omitempty"` | |||
SubButton []WechatSubButton `json:"sub_button,omitempty"` | |||
Name string `json:"name"` | |||
Type string `json:"type"` | |||
Url string `json:"url"` | |||
Appid string `json:"appid,omitempty"` | |||
Pagepath string `json:"pagepath"` | |||
Key string `json:"key,omitempty"` | |||
SubButton []WechatSubButton `json:"sub_button,omitempty"` | |||
ReplyContent []ReplyContent `json:"replyContent"` | |||
} | |||
type WechatSubButton struct { | |||
Type string `json:"type"` | |||
Name string `json:"name"` | |||
Url string `json:"url"` | |||
Appid string `json:"appid,omitempty"` | |||
Pagepath string `json:"pagepath"` | |||
Key string `json:"key,omitempty"` | |||
Type string `json:"type"` | |||
Name string `json:"name"` | |||
Url string `json:"url"` | |||
Appid string `json:"appid,omitempty"` | |||
Pagepath string `json:"pagepath"` | |||
Key string `json:"key"` | |||
ReplyContent []ReplyContent `json:"replyContent"` | |||
Value string `json:"value"` | |||
} | |||
type ReplyContent struct { | |||
Text ReplyContentText `json:"text"` | |||
MsgType string `json:"msgType"` | |||
Image ReplyContentImg `json:"image"` | |||
} | |||
type ReplyContentText struct { | |||
Content string `json:"content"` | |||
} | |||
type ReplyContentImg struct { | |||
MediaId string `json:"mediaId"` | |||
MediaUrl string `json:"mediaUrl"` | |||
Media string `json:"media"` | |||
} | |||
type OffcialWechatButton struct { | |||
Type string `json:"type,omitempty"` | |||
@@ -33,3 +49,23 @@ type SubButtonMap struct { | |||
type WechatReq struct { | |||
Button []WechatButton `json:"button"` | |||
} | |||
type WechatParam struct { | |||
Button []WechatButtonParam `json:"button"` | |||
} | |||
type WechatButtonParam struct { | |||
Name string `json:"name"` | |||
Type string `json:"type"` | |||
Url string `json:"url"` | |||
Appid string `json:"appid,omitempty"` | |||
Pagepath string `json:"pagepath"` | |||
Key string `json:"key,omitempty"` | |||
SubButton []WechatSubButtonParam `json:"sub_button,omitempty"` | |||
} | |||
type WechatSubButtonParam struct { | |||
Type string `json:"type"` | |||
Name string `json:"name"` | |||
Url string `json:"url"` | |||
Appid string `json:"appid,omitempty"` | |||
Pagepath string `json:"pagepath"` | |||
Key string `json:"key"` | |||
} |
@@ -44,7 +44,7 @@ func Init() *gin.Engine { | |||
func route(r *gin.RouterGroup) { | |||
r.Any("/demo", hdl.Demo) | |||
r.POST("/login", hdl.Login) | |||
r.POST("/img_upload", hdl.ImgUpload) | |||
r.Group("/wx") | |||
{ | |||
r.Use(mw.DB) | |||
@@ -61,12 +61,12 @@ func route(r *gin.RouterGroup) { | |||
r.Use(mw.Checker) // 以下接口需要检查Header: platform | |||
{ | |||
} | |||
r.GET("/wechat_menu/get", hdl.GetMenu) | |||
r.POST("/wechat_menu/set", hdl.SetMenu) | |||
r.GET("/qrcodeBatchDownload", hdl.QrcodeBatchDownload) //二维码批次-下载 | |||
r.Use(mw.Auth) // 以下接口需要JWT验证 | |||
{ | |||
r.GET("/wechat_menu/get", hdl.GetMenu) | |||
r.POST("/wechat_menu/set", hdl.SetMenu) | |||
r.GET("/userInfo", hdl.UserInfo) //用户信息 | |||
r.GET("/sysCfg", hdl.GetSysCfg) //基础配置-获取 | |||
r.POST("/sysCfg", hdl.SetSysCfg) //基础配置-设置 | |||
@@ -2,6 +2,7 @@ package svc | |||
import ( | |||
"applet/app/db" | |||
"applet/app/db/model" | |||
"applet/app/e" | |||
"applet/app/enum" | |||
"applet/app/md" | |||
@@ -11,7 +12,9 @@ import ( | |||
"errors" | |||
"fmt" | |||
"github.com/gin-gonic/gin" | |||
"github.com/jinzhu/copier" | |||
"github.com/tidwall/gjson" | |||
"time" | |||
) | |||
func GetMenu(c *gin.Context) { | |||
@@ -63,12 +66,10 @@ func GetMenu(c *gin.Context) { | |||
} | |||
sysCfgDb.SysCfgUpdate("wechat_menu", utils.SerializeStr(menuList)) | |||
} | |||
replyContentSet := []map[string]string{ | |||
{"msgType": "text", "name": "文本"}, | |||
{"msgType": "image", "name": "图片"}, | |||
{"msgType": "video", "name": "视频"}, | |||
{"msgType": "voice", "name": "语音"}, | |||
{"msgType": "article", "name": "文章"}, | |||
} | |||
res := map[string]interface{}{ | |||
"button": menuList, | |||
@@ -76,6 +77,7 @@ func GetMenu(c *gin.Context) { | |||
} | |||
e.OutSuc(c, res, nil) | |||
} | |||
func SetMenu(c *gin.Context) { | |||
var args md.WechatReq | |||
if err := c.ShouldBindJSON(&args); err != nil { | |||
@@ -90,7 +92,19 @@ func SetMenu(c *gin.Context) { | |||
e.OutErr(c, 400, err.Error()) | |||
return | |||
} | |||
menu, err := utils.SetWechatSelfMenu(token, args) | |||
var param md.WechatParam | |||
copier.Copy(¶m, &args) | |||
for k, v := range param.Button { | |||
if v.Type == "click" { | |||
param.Button[k].Key = commSetModule(utils.SerializeStr(args.Button[k].ReplyContent)) | |||
} | |||
for k1, v1 := range v.SubButton { | |||
if v1.Type == "click" { | |||
param.Button[k].SubButton[k1].Key = commSetModule(utils.SerializeStr(args.Button[k].SubButton[k1].ReplyContent)) | |||
} | |||
} | |||
} | |||
menu, err := utils.SetWechatSelfMenu(token, param) | |||
if err != nil { | |||
e.OutErr(c, 400, err.Error()) | |||
return | |||
@@ -102,6 +116,17 @@ func SetMenu(c *gin.Context) { | |||
e.OutSuc(c, "success", nil) | |||
return | |||
} | |||
func commSetModule(modData string) string { | |||
mod := &model.SysModule{ | |||
ModName: "official_account_custom_reply", | |||
SkipIdentifier: "pub.flutter.official_account_custom_reply", | |||
Title: "公众号菜单点击事件回复消息", | |||
Data: modData, | |||
CreateAt: time.Now(), | |||
} | |||
db.Db.Insert(mod) | |||
return "official_account_custom_reply" + "_" + utils.IntToStr(mod.ModId) | |||
} | |||
func GetWechatToken() (string, error) { | |||
sysCfgDb := db.SysCfgDb{} | |||
@@ -0,0 +1,66 @@ | |||
package svc | |||
import ( | |||
"encoding/xml" | |||
"github.com/gin-gonic/gin" | |||
"log" | |||
"time" | |||
) | |||
// WXRepTextMsg 微信回复文本消息结构体 | |||
type WXRepTextMsg struct { | |||
ToUserName string | |||
FromUserName string | |||
CreateTime int64 | |||
MsgType string | |||
Content string | |||
// 若不标记XMLName, 则解析后的xml名为该结构体的名称 | |||
XMLName xml.Name `xml:"xml"` | |||
} | |||
// WXRepImageMsg 微信回复图片消息结构体 | |||
type WXRepImageMsg struct { | |||
ToUserName string | |||
FromUserName string | |||
CreateTime int64 | |||
MsgType string | |||
Image struct { | |||
MediaId string | |||
} | |||
// 若不标记XMLName, 则解析后的xml名为该结构体的名称 | |||
XMLName xml.Name `xml:"xml"` | |||
} | |||
// WXMsgTextReply 微信消息回复 | |||
func WXMsgTextReply(c *gin.Context, fromUser, toUser, content string) { | |||
repTextMsg := WXRepTextMsg{ | |||
ToUserName: toUser, | |||
FromUserName: fromUser, | |||
CreateTime: time.Now().Unix(), | |||
MsgType: "text", | |||
Content: content, | |||
} | |||
msg, err := xml.Marshal(&repTextMsg) | |||
if err != nil { | |||
log.Printf("[消息回复] - 将对象进行XML编码出错: %v\n", err) | |||
return | |||
} | |||
_, _ = c.Writer.Write(msg) | |||
} | |||
func WXMsgImageReply(c *gin.Context, fromUser, toUser, content string) { | |||
repTextMsg := WXRepImageMsg{ | |||
ToUserName: toUser, | |||
FromUserName: fromUser, | |||
CreateTime: time.Now().Unix(), | |||
MsgType: "image", | |||
Image: struct{ MediaId string }{MediaId: content}, | |||
} | |||
msg, err := xml.Marshal(&repTextMsg) | |||
if err != nil { | |||
log.Printf("[消息回复] - 将对象进行XML编码出错: %v\n", err) | |||
return | |||
} | |||
_, _ = c.Writer.Write(msg) | |||
} |
@@ -48,7 +48,6 @@ func NewRedis(addr string) { | |||
Wait: true, | |||
Dial: func() (redigo.Conn, error) { | |||
c, err := redigo.Dial("tcp", addr, | |||
redigo.DialPassword(redisPassword), | |||
redigo.DialConnectTimeout(redisDialTTL), | |||
redigo.DialReadTimeout(redisReadTTL), | |||
redigo.DialWriteTimeout(redisWriteTTL), | |||
@@ -2,9 +2,15 @@ package utils | |||
import ( | |||
"applet/app/md" | |||
"bytes" | |||
"crypto/sha1" | |||
"encoding/hex" | |||
"fmt" | |||
"io" | |||
"io/ioutil" | |||
"mime/multipart" | |||
"net/http" | |||
"os" | |||
"sort" | |||
"strings" | |||
) | |||
@@ -42,10 +48,87 @@ func GetWechatSelfMenu(token string) (string, error) { | |||
return string(get), err | |||
} | |||
func SetWechatSelfMenu(token string, args md.WechatReq) (string, error) { | |||
func SetWechatSelfMenu(token string, args md.WechatParam) (string, error) { | |||
str := SerializeStr(args) | |||
str = strings.ReplaceAll(str, "\\u0026", "&") | |||
fmt.Println(str) | |||
get, err := CurlPost("https://api.weixin.qq.com/cgi-bin/menu/create?access_token="+token, str, nil) | |||
return string(get), err | |||
} | |||
func UploadWxImg(token string, args md.WechatParam) (string, error) { | |||
str := SerializeStr(args) | |||
str = strings.ReplaceAll(str, "\\u0026", "&") | |||
fmt.Println(str) | |||
get, err := CurlPost("https://api.weixin.qq.com/cgi-bin/material/add_material?access_token"+token+"&type=image", str, nil) | |||
return string(get), err | |||
} | |||
func PostFile(fieldname, filename, uri string) ([]byte, error) { | |||
fields := []MultipartFormField{ | |||
{ | |||
IsFile: true, | |||
Fieldname: fieldname, | |||
Filename: filename, | |||
}, | |||
} | |||
return PostMultipartForm(fields, uri) | |||
} | |||
//MultipartFormField 保存文件或其他字段信息 | |||
type MultipartFormField struct { | |||
IsFile bool | |||
Fieldname string | |||
Value []byte | |||
Filename string | |||
} | |||
//PostMultipartForm 上传文件或其他多个字段 | |||
func PostMultipartForm(fields []MultipartFormField, uri string) (respBody []byte, err error) { | |||
bodyBuf := &bytes.Buffer{} | |||
bodyWriter := multipart.NewWriter(bodyBuf) | |||
for _, field := range fields { | |||
if field.IsFile { | |||
fileWriter, e := bodyWriter.CreateFormFile(field.Fieldname, field.Filename) | |||
if e != nil { | |||
err = fmt.Errorf("error writing to buffer , err=%v", e) | |||
return | |||
} | |||
fh, e := os.Open(field.Filename) | |||
if e != nil { | |||
err = fmt.Errorf("error opening file , err=%v", e) | |||
return | |||
} | |||
defer fh.Close() | |||
if _, err = io.Copy(fileWriter, fh); err != nil { | |||
return | |||
} | |||
} else { | |||
partWriter, e := bodyWriter.CreateFormField(field.Fieldname) | |||
if e != nil { | |||
err = e | |||
return | |||
} | |||
valueReader := bytes.NewReader(field.Value) | |||
if _, err = io.Copy(partWriter, valueReader); err != nil { | |||
return | |||
} | |||
} | |||
} | |||
contentType := bodyWriter.FormDataContentType() | |||
bodyWriter.Close() | |||
resp, e := http.Post(uri, contentType, bodyBuf) | |||
if e != nil { | |||
err = e | |||
return | |||
} | |||
defer resp.Body.Close() | |||
if resp.StatusCode != http.StatusOK { | |||
return nil, err | |||
} | |||
respBody, err = ioutil.ReadAll(resp.Body) | |||
return | |||
} |
@@ -20,6 +20,7 @@ require ( | |||
github.com/golang/snappy v0.0.3 // indirect | |||
github.com/gomodule/redigo v2.0.0+incompatible | |||
github.com/gorilla/sessions v1.2.1 // indirect | |||
github.com/jinzhu/copier v0.3.5 | |||
github.com/json-iterator/go v1.1.10 // indirect | |||
github.com/kr/text v0.2.0 // indirect | |||
github.com/leodido/go-urn v1.2.1 // indirect | |||