@@ -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/db/model" | ||||
"applet/app/enum" | "applet/app/enum" | ||||
"applet/app/md" | "applet/app/md" | ||||
"applet/app/svc" | |||||
"applet/app/utils" | "applet/app/utils" | ||||
"encoding/json" | "encoding/json" | ||||
"encoding/xml" | "encoding/xml" | ||||
"errors" | "errors" | ||||
"fmt" | "fmt" | ||||
"github.com/tidwall/gjson" | |||||
"log" | "log" | ||||
"net/http" | "net/http" | ||||
"strings" | |||||
"time" | "time" | ||||
"github.com/gin-gonic/gin" | "github.com/gin-gonic/gin" | ||||
@@ -110,6 +113,22 @@ func WXMsgReceive(c *gin.Context) { | |||||
WXMsgReply(c, eventMsg.ToUserName, eventMsg.FromUserName, err.Error()) | 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" { | if msg.MsgType == "text" { | ||||
//文本类型消息 | //文本类型消息 | ||||
@@ -1,21 +1,37 @@ | |||||
package md | package md | ||||
type WechatButton struct { | 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 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 OffcialWechatButton struct { | ||||
Type string `json:"type,omitempty"` | Type string `json:"type,omitempty"` | ||||
@@ -33,3 +49,23 @@ type SubButtonMap struct { | |||||
type WechatReq struct { | type WechatReq struct { | ||||
Button []WechatButton `json:"button"` | 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) { | func route(r *gin.RouterGroup) { | ||||
r.Any("/demo", hdl.Demo) | r.Any("/demo", hdl.Demo) | ||||
r.POST("/login", hdl.Login) | r.POST("/login", hdl.Login) | ||||
r.POST("/img_upload", hdl.ImgUpload) | |||||
r.Group("/wx") | r.Group("/wx") | ||||
{ | { | ||||
r.Use(mw.DB) | r.Use(mw.DB) | ||||
@@ -61,12 +61,12 @@ func route(r *gin.RouterGroup) { | |||||
r.Use(mw.Checker) // 以下接口需要检查Header: platform | 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.GET("/qrcodeBatchDownload", hdl.QrcodeBatchDownload) //二维码批次-下载 | ||||
r.Use(mw.Auth) // 以下接口需要JWT验证 | 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("/userInfo", hdl.UserInfo) //用户信息 | ||||
r.GET("/sysCfg", hdl.GetSysCfg) //基础配置-获取 | r.GET("/sysCfg", hdl.GetSysCfg) //基础配置-获取 | ||||
r.POST("/sysCfg", hdl.SetSysCfg) //基础配置-设置 | r.POST("/sysCfg", hdl.SetSysCfg) //基础配置-设置 | ||||
@@ -2,6 +2,7 @@ package svc | |||||
import ( | import ( | ||||
"applet/app/db" | "applet/app/db" | ||||
"applet/app/db/model" | |||||
"applet/app/e" | "applet/app/e" | ||||
"applet/app/enum" | "applet/app/enum" | ||||
"applet/app/md" | "applet/app/md" | ||||
@@ -11,7 +12,9 @@ import ( | |||||
"errors" | "errors" | ||||
"fmt" | "fmt" | ||||
"github.com/gin-gonic/gin" | "github.com/gin-gonic/gin" | ||||
"github.com/jinzhu/copier" | |||||
"github.com/tidwall/gjson" | "github.com/tidwall/gjson" | ||||
"time" | |||||
) | ) | ||||
func GetMenu(c *gin.Context) { | func GetMenu(c *gin.Context) { | ||||
@@ -63,12 +66,10 @@ func GetMenu(c *gin.Context) { | |||||
} | } | ||||
sysCfgDb.SysCfgUpdate("wechat_menu", utils.SerializeStr(menuList)) | sysCfgDb.SysCfgUpdate("wechat_menu", utils.SerializeStr(menuList)) | ||||
} | } | ||||
replyContentSet := []map[string]string{ | replyContentSet := []map[string]string{ | ||||
{"msgType": "text", "name": "文本"}, | {"msgType": "text", "name": "文本"}, | ||||
{"msgType": "image", "name": "图片"}, | {"msgType": "image", "name": "图片"}, | ||||
{"msgType": "video", "name": "视频"}, | |||||
{"msgType": "voice", "name": "语音"}, | |||||
{"msgType": "article", "name": "文章"}, | |||||
} | } | ||||
res := map[string]interface{}{ | res := map[string]interface{}{ | ||||
"button": menuList, | "button": menuList, | ||||
@@ -76,6 +77,7 @@ func GetMenu(c *gin.Context) { | |||||
} | } | ||||
e.OutSuc(c, res, nil) | e.OutSuc(c, res, nil) | ||||
} | } | ||||
func SetMenu(c *gin.Context) { | func SetMenu(c *gin.Context) { | ||||
var args md.WechatReq | var args md.WechatReq | ||||
if err := c.ShouldBindJSON(&args); err != nil { | if err := c.ShouldBindJSON(&args); err != nil { | ||||
@@ -90,7 +92,19 @@ func SetMenu(c *gin.Context) { | |||||
e.OutErr(c, 400, err.Error()) | e.OutErr(c, 400, err.Error()) | ||||
return | 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 { | if err != nil { | ||||
e.OutErr(c, 400, err.Error()) | e.OutErr(c, 400, err.Error()) | ||||
return | return | ||||
@@ -102,6 +116,17 @@ func SetMenu(c *gin.Context) { | |||||
e.OutSuc(c, "success", nil) | e.OutSuc(c, "success", nil) | ||||
return | 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) { | func GetWechatToken() (string, error) { | ||||
sysCfgDb := db.SysCfgDb{} | 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, | Wait: true, | ||||
Dial: func() (redigo.Conn, error) { | Dial: func() (redigo.Conn, error) { | ||||
c, err := redigo.Dial("tcp", addr, | c, err := redigo.Dial("tcp", addr, | ||||
redigo.DialPassword(redisPassword), | |||||
redigo.DialConnectTimeout(redisDialTTL), | redigo.DialConnectTimeout(redisDialTTL), | ||||
redigo.DialReadTimeout(redisReadTTL), | redigo.DialReadTimeout(redisReadTTL), | ||||
redigo.DialWriteTimeout(redisWriteTTL), | redigo.DialWriteTimeout(redisWriteTTL), | ||||
@@ -2,9 +2,15 @@ package utils | |||||
import ( | import ( | ||||
"applet/app/md" | "applet/app/md" | ||||
"bytes" | |||||
"crypto/sha1" | "crypto/sha1" | ||||
"encoding/hex" | "encoding/hex" | ||||
"fmt" | "fmt" | ||||
"io" | |||||
"io/ioutil" | |||||
"mime/multipart" | |||||
"net/http" | |||||
"os" | |||||
"sort" | "sort" | ||||
"strings" | "strings" | ||||
) | ) | ||||
@@ -42,10 +48,87 @@ func GetWechatSelfMenu(token string) (string, error) { | |||||
return string(get), err | 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 := SerializeStr(args) | ||||
str = strings.ReplaceAll(str, "\\u0026", "&") | str = strings.ReplaceAll(str, "\\u0026", "&") | ||||
fmt.Println(str) | fmt.Println(str) | ||||
get, err := CurlPost("https://api.weixin.qq.com/cgi-bin/menu/create?access_token="+token, str, nil) | get, err := CurlPost("https://api.weixin.qq.com/cgi-bin/menu/create?access_token="+token, str, nil) | ||||
return string(get), err | 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/golang/snappy v0.0.3 // indirect | ||||
github.com/gomodule/redigo v2.0.0+incompatible | github.com/gomodule/redigo v2.0.0+incompatible | ||||
github.com/gorilla/sessions v1.2.1 // indirect | 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/json-iterator/go v1.1.10 // indirect | ||||
github.com/kr/text v0.2.0 // indirect | github.com/kr/text v0.2.0 // indirect | ||||
github.com/leodido/go-urn v1.2.1 // indirect | github.com/leodido/go-urn v1.2.1 // indirect | ||||