diff --git a/app/db/model/sys_module.go b/app/db/model/sys_module.go new file mode 100644 index 0000000..82b4e88 --- /dev/null +++ b/app/db/model/sys_module.go @@ -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"` +} diff --git a/app/hdl/hdl_img_upload.go b/app/hdl/hdl_img_upload.go new file mode 100644 index 0000000..896c371 --- /dev/null +++ b/app/hdl/hdl_img_upload.go @@ -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 +} diff --git a/app/hdl/hdl_wx.go b/app/hdl/hdl_wx.go index 7030e6d..baaf749 100644 --- a/app/hdl/hdl_wx.go +++ b/app/hdl/hdl_wx.go @@ -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" { //文本类型消息 diff --git a/app/md/md_wechat_menu.go b/app/md/md_wechat_menu.go index e246b33..e89ccc5 100644 --- a/app/md/md_wechat_menu.go +++ b/app/md/md_wechat_menu.go @@ -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"` +} diff --git a/app/router/router.go b/app/router/router.go index ab11c51..82bc47f 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -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) //基础配置-设置 diff --git a/app/svc/svc_wechat.go b/app/svc/svc_wechat.go index 1ea1a18..261b995 100644 --- a/app/svc/svc_wechat.go +++ b/app/svc/svc_wechat.go @@ -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{} diff --git a/app/svc/svc_wechat_reply.go b/app/svc/svc_wechat_reply.go new file mode 100644 index 0000000..a9c5c02 --- /dev/null +++ b/app/svc/svc_wechat_reply.go @@ -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) +} diff --git a/app/utils/cache/redis.go b/app/utils/cache/redis.go index de3be89..3c4b914 100644 --- a/app/utils/cache/redis.go +++ b/app/utils/cache/redis.go @@ -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), diff --git a/app/utils/wx.go b/app/utils/wx.go index e214614..ec69548 100644 --- a/app/utils/wx.go +++ b/app/utils/wx.go @@ -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 +} diff --git a/go.mod b/go.mod index cd45cac..deea658 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/public/img/0.jfif b/public/img/0.jfif new file mode 100644 index 0000000..5916053 Binary files /dev/null and b/public/img/0.jfif differ