├── .gitignore ├── README.md ├── cache ├── cache.go ├── redis_cache.go └── redis_cache_test.go ├── corp ├── agent.go ├── api.go ├── card.go ├── common.go ├── context.go ├── department.go ├── kf.go ├── kf_server.go ├── media.go ├── menu.go ├── msg.go ├── oauth2.go ├── recv_msg.go └── server.go ├── dep.sh ├── mch ├── common.go └── pay.go ├── mp ├── api.go ├── card.go ├── common.go ├── context.go ├── mass.go ├── material.go ├── message.go ├── oauth2.go ├── poi.go └── server.go ├── small ├── api.go ├── common.go ├── context.go ├── message.go └── server.go ├── utils ├── api_base.go ├── api_base_xml.go ├── char_data.go ├── context.go ├── crypto.go ├── error.go ├── helper.go ├── http.go ├── message.go ├── nonce.go ├── sign_struct.go ├── sign_struct_value.go ├── signature.go └── time.go └── vendor └── vendor.json /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/*/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-wx-sdk -------------------------------------------------------------------------------- /cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "time" 4 | 5 | //Cache interface 6 | type Cache interface { 7 | Get(key string) interface{} 8 | Set(key string, val interface{}, timeout time.Duration) error 9 | IsExist(key string) bool 10 | Delete(key string) error 11 | } 12 | -------------------------------------------------------------------------------- /cache/redis_cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "gopkg.in/redis.v5" 7 | "time" 8 | ) 9 | 10 | //Memcache struct contains *memcache.Client 11 | type RedisCache struct { 12 | conn *redis.Client 13 | } 14 | 15 | //NewMemcache create new memcache 16 | func NewRedisCache(url string,password string,db int) (Cache, error) { 17 | client := redis.NewClient(&redis.Options{ 18 | Addr: url, 19 | Password: password, // no password set 20 | DB: db, // use default DB 21 | }) 22 | 23 | pong, err := client.Ping().Result() 24 | if err != nil { 25 | fmt.Println(pong, err) 26 | return nil, err 27 | } 28 | return &RedisCache{client}, nil 29 | } 30 | 31 | func NewCache(conn *redis.Client) Cache{ 32 | return &RedisCache{conn} 33 | } 34 | 35 | //Get return cached value 36 | func (redis *RedisCache) Get(key string) interface{} { 37 | if item, err := redis.conn.Get(key).Result(); err == nil { 38 | return string(item) 39 | } 40 | return nil 41 | } 42 | 43 | // IsExist check value exists in memcache. 44 | func (redis *RedisCache) IsExist(key string) bool { 45 | _, err := redis.conn.Get(key).Result() 46 | if err != nil { 47 | return false 48 | } 49 | return true 50 | } 51 | 52 | //Set cached value with key and expire time. 53 | func (redis *RedisCache) Set(key string, val interface{}, timeout time.Duration) error { 54 | v, ok := val.(string) 55 | if !ok { 56 | return errors.New("val must string") 57 | } 58 | 59 | err := redis.conn.Set(key, v, timeout).Err() 60 | if err != nil { 61 | fmt.Println(err) 62 | return err 63 | } 64 | return nil 65 | } 66 | 67 | //Delete delete value in memcache. 68 | func (redis *RedisCache) Delete(key string) error { 69 | return redis.conn.Del(key).Err() 70 | } 71 | -------------------------------------------------------------------------------- /cache/redis_cache_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestMemcache(t *testing.T) { 9 | redis,_ := NewRedisCache("localhost:6379","",0) 10 | var err error 11 | timeoutDuration := 10 * time.Second 12 | if err = redis.Set("username", "silenceper", timeoutDuration); err != nil { 13 | t.Error("set Error", err) 14 | } 15 | 16 | if !redis.IsExist("username") { 17 | t.Error("IsExist Error") 18 | } 19 | 20 | name := redis.Get("username").(string) 21 | if name != "silenceper" { 22 | t.Error("get Error") 23 | } 24 | 25 | if err = redis.Delete("username"); err != nil { 26 | t.Errorf("delete Error , err=%v", err) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /corp/agent.go: -------------------------------------------------------------------------------- 1 | package corp 2 | 3 | import "github.com/qjw/go-wx-sdk/utils" 4 | 5 | const ( 6 | agentGet = "https://qyapi.weixin.qq.com/cgi-bin/agent/get?access_token=%s&agentid=%d" 7 | agentSet = "https://qyapi.weixin.qq.com/cgi-bin/agent/set?access_token=%s" 8 | agentList = "https://qyapi.weixin.qq.com/cgi-bin/agent/list?access_token=%s" 9 | ) 10 | 11 | type AgentLiteObj struct { 12 | AgentID int64 `json:"agentid" doc:"企业应用id"` 13 | Name string `json:"name" doc:"企业应用名称"` 14 | SquareLogoUrl string `json:"square_logo_url" doc:"企业应用方形头像"` 15 | RoundLogoUrl string `json:"round_logo_url" doc:"企业应用圆形头像"` 16 | } 17 | 18 | type AgentListRes struct { 19 | utils.CommonError 20 | AgentList []AgentLiteObj `json:"agentlist"` 21 | } 22 | 23 | func (this CorpApi) GetAgentList() (*AgentListRes, error) { 24 | var res AgentListRes 25 | err := this.DoGet(agentList, &res) 26 | if err == nil { 27 | return &res, nil 28 | } else { 29 | return nil, err 30 | } 31 | } 32 | 33 | type AgentObj struct { 34 | AgentLiteObj 35 | Description string `json:"description" doc:"企业应用详情"` 36 | 37 | AllowUserinfos struct { 38 | Users []struct { 39 | Userid string `json:"userid"` 40 | Status string `json:"status"` 41 | } `json:"user"` 42 | } `json:"allow_userinfos,omitempty" doc:"企业应用可见范围(人员),其中包括userid和关注状态state"` 43 | 44 | AllowPartys struct { 45 | Partyid []int64 `json:"partyid"` 46 | } `json:"allow_partys,omitempty" doc:"企业应用可见范围(部门)"` 47 | AllowTags struct { 48 | Tagid []int64 `json:"tagid"` 49 | } `json:"allow_tags,omitempty" doc:"企业应用可见范围(标签)"` 50 | Close int `json:"close" doc:"企业应用是否被禁用"` 51 | RedirectDomain string `json:"redirect_domain,omitempty" doc:"企业应用可信域名"` 52 | ReportLocationFlag int `json:"report_location_flag" doc:"企业应用是否打开地理位置上报 0:不上报;1:进入会话上报;2:持续上报"` 53 | Isreportuser int `json:"isreportuser" doc:"是否接收用户变更通知。0:不接收;1:接收"` 54 | Isreportenter int `json:"isreportenter" doc:"是否上报用户进入应用事件。0:不接收;1:接收"` 55 | ChatExtensionUrl string `json:"chat_extension_url,omitempty" doc:"关联会话url"` 56 | Type int `json:"type" doc:"应用类型。1:消息型;2:主页型"` 57 | } 58 | 59 | type AgentDetailRes struct { 60 | utils.CommonError 61 | AgentObj 62 | } 63 | 64 | func (this CorpApi) GetAgentDetail(agentid int64) (*AgentDetailRes, error) { 65 | var res AgentDetailRes 66 | err := this.DoGet(agentGet, &res, agentid) 67 | if err == nil { 68 | return &res, nil 69 | } else { 70 | return nil, err 71 | } 72 | } 73 | 74 | type AgentUpdateObj struct { 75 | AgentID int64 `json:"agentid" doc:"企业应用id"` 76 | ReportLocationFlag *int `json:"report_location_flag,omitempty" doc:"企业应用是否打开地理位置上报 0:不上报;1:进入会话上报;2:持续上报"` 77 | RedirectDomain *string `json:"redirect_domain,omitempty" doc:"企业应用可信域名"` 78 | Name *string `json:"name,omitempty" doc:"企业应用名称"` 79 | LogoMediaid *string `json:"logo_mediaid,omitempty" doc:"企业应用头像的mediaid,通过多媒体接口上传图片获得mediaid,上传后会自动裁剪成方形和圆形两个头像"` 80 | Description *string `json:"description,omitempty" doc:"企业应用详情"` 81 | Isreportuser *int `json:"isreportuser,omitempty" doc:"是否接收用户变更通知。0:不接收;1:接收"` 82 | Isreportenter *int `json:"isreportenter,omitempty" doc:"是否上报用户进入应用事件。0:不接收;1:接收"` 83 | ChatExtensionUrl *string `json:"chat_extension_url,omitempty" doc:"关联会话url"` 84 | HomeUrl *string `json:"home_url,omitempty" doc:"主页型应用url。url必须以http或者https开头。消息型应用无需该参数"` 85 | } 86 | 87 | func (this CorpApi) UpdateAgent(agent *AgentUpdateObj) (*utils.CommonError, error) { 88 | var res utils.CommonError 89 | if err := this.DoPostObject(agentSet, agent, &res); err == nil { 90 | return &res, nil 91 | } else { 92 | return nil, err 93 | } 94 | } -------------------------------------------------------------------------------- /corp/api.go: -------------------------------------------------------------------------------- 1 | package corp 2 | 3 | import ( 4 | "github.com/qjw/go-wx-sdk/utils" 5 | "strconv" 6 | ) 7 | 8 | func (this CorpApi) GetAccessToken() (*string, error) { 9 | accessToken, err := this.Context.GetAccessToken() 10 | if err == nil { 11 | return &accessToken, nil 12 | } else { 13 | return nil, err 14 | } 15 | } 16 | 17 | type SignJsRes struct { 18 | AppID string `json:"appid"` 19 | Timestamp string `json:"timestamp"` 20 | NonceStr string `json:"nonceStr"` 21 | Signature string `json:"signature"` 22 | } 23 | 24 | func (this CorpApi) SignJsTicket(nonceStr, timestamp, url string) (*SignJsRes, error) { 25 | jsTicket, err := this.Context.GetJsTicket() 26 | if err != nil { 27 | return nil, err 28 | } 29 | if timestamp == "" { 30 | bb := utils.GetCurrTs() 31 | timestamp = strconv.FormatInt(bb, 10) 32 | } 33 | sign := utils.WXConfigSign(jsTicket, nonceStr, timestamp, url) 34 | return &SignJsRes{ 35 | Signature: sign, 36 | AppID: this.Context.Config.CorpID, 37 | NonceStr: nonceStr, 38 | Timestamp: timestamp, 39 | }, nil 40 | } 41 | 42 | const ( 43 | getcallbackip = "https://qyapi.weixin.qq.com/cgi-bin/getcallbackip?access_token=%s" 44 | ) 45 | 46 | type IpList struct { 47 | IpList []string `json:"ip_list"` 48 | } 49 | 50 | func (this CorpApi) GetIpList() (*IpList, error) { 51 | var res IpList 52 | if err := this.DoGet(getcallbackip, &res); err == nil { 53 | return &res, nil 54 | } else { 55 | return nil, err 56 | } 57 | } 58 | 59 | func (this CorpApi) GetJsTicket() (*string, error) { 60 | jsTicket, err := this.Context.GetJsTicket() 61 | if err == nil { 62 | return &jsTicket, nil 63 | } else { 64 | return nil, err 65 | } 66 | } 67 | 68 | const ( 69 | convert2Openid = "https://qyapi.weixin.qq.com/cgi-bin/user/convert_to_openid?access_token=%s" 70 | Convert2Userid = "https://qyapi.weixin.qq.com/cgi-bin/user/convert_to_userid?access_token=%s" 71 | ) 72 | 73 | type Convert2OpenIDObj struct { 74 | UserID string `json:"userid"` 75 | AgentID int64 `json:"agentid"` 76 | } 77 | 78 | type Convert2OpenIDRes struct { 79 | utils.CommonError 80 | OpenID string `json:"openid"` 81 | AppID string `json:"appid"` 82 | } 83 | 84 | func (this CorpApi) Convert2OpenID(param *Convert2OpenIDObj) (*Convert2OpenIDRes, error) { 85 | var res Convert2OpenIDRes 86 | if err := this.DoPostObject(convert2Openid, param, &res); err == nil { 87 | return &res, nil 88 | } else { 89 | return nil, err 90 | } 91 | } 92 | 93 | 94 | type Convert2UserIDRes struct { 95 | utils.CommonError 96 | UserID string `json:"userid"` 97 | } 98 | 99 | func (this CorpApi) Convert2UserID(openid string) (*Convert2UserIDRes, error) { 100 | var res Convert2UserIDRes 101 | if err := this.DoPostObject(Convert2Userid, &struct{ 102 | OpenID string `json:"openid"` 103 | }{OpenID:openid}, &res); err == nil { 104 | return &res, nil 105 | } else { 106 | return nil, err 107 | } 108 | } -------------------------------------------------------------------------------- /corp/card.go: -------------------------------------------------------------------------------- 1 | package corp 2 | 3 | import ( 4 | "github.com/qjw/go-wx-sdk/utils" 5 | "io" 6 | ) 7 | 8 | const ( 9 | uploadLogo = "https://qyapi.weixin.qq.com/cgi-bin/media/uploadimg?access_token=%s&type=card_logo" 10 | createCard = "https://qyapi.weixin.qq.com/cgi-bin/card/create?access_token=%s" 11 | batchGetCard = "https://qyapi.weixin.qq.com/cgi-bin/card/batchget?access_token=%s" 12 | ) 13 | 14 | type UploadCardLogoRes struct { 15 | utils.CommonError 16 | Url string `json:"url"` 17 | } 18 | 19 | func (this CorpApi) UploadCardLogo(reader io.Reader, filename string) (*UploadCardLogoRes, error) { 20 | var res UploadCardLogoRes 21 | if err := this.DoPostFile(reader, "media", filename, &res, uploadLogo); err == nil { 22 | return &res, nil 23 | } else { 24 | return nil, err 25 | } 26 | } 27 | 28 | type GeneralCouponObj struct { 29 | LogoUrl string `json:"logo_url" doc:"卡券的商户logo"` 30 | BrandName string `json:"brand_name" doc:"商户名字,字数上限为12个汉字"` 31 | CodeType string `json:"code_type"` 32 | Title string `json:"title" doc:"卡券名,字数上限为9个汉字。(建议涵盖卡券属性、服务及金额)。"` 33 | Color string `json:"color" doc:"券颜色。按色彩规范标注填写Color010-Color102"` 34 | Notice string `json:"notice" doc:"卡券使用提醒,字数上限为16个汉字。"` 35 | ServicePhone string `json:"service_phone,omitempty" doc:"客服电话。"` 36 | Description string `json:"description" doc:"卡券使用说明,字数上限为1024个汉字。"` 37 | Sku struct { 38 | Quantity int64 `json:"quantity" doc:"卡券库存的数量,上限为100000000。"` 39 | } `json:"sku"` 40 | GetLimit int64 `json:"get_limit,omitempty" doc:"每人可领券的数量限制,不填写默认为50"` 41 | BindOpenid bool `json:"bind_openid,omitempty" doc:"是否指定用户领取,填写true或false。默认为false。通常指定特殊用户群体投放卡券或防止刷券时选择指定用户领取。"` 42 | CanShare bool `json:"can_share,omitempty" doc:"卡券领取页面是否可分享。"` 43 | CanGiveFriend bool `json:"can_give_friend,omitempty" doc:"卡券是否可转赠。"` 44 | 45 | CustomUrlName string `json:"custom_url_name" doc:"自定义入口名称"` 46 | CustomUrl string `json:"custom_url" doc:"自定义入口URL"` 47 | CustomUrlSubTitle string `json:"custom_url_sub_title" doc:"显示在入口右侧的提示语。"` 48 | PromotionUrlName string `json:"promotion_url_name" doc:"营销场景的自定义入口名称。"` 49 | PromotionUrl string `json:"promotion_url" doc:"入口跳转外链的地址链接。"` 50 | Source string `json:"source" doc:"第三方来源名,例如同程旅游、大众点评。"` 51 | 52 | DateInfo struct { 53 | Type string `json:"type" doc:"使用时间的类型,DATE_TYPE_FIX_TIME_RANGE 表示固定日期区间,DATE_TYPE_FIX_TERM表示固定时长(自领取后按天算。)"` 54 | BeginTimestamp string `json:"begin_timestamp" doc:"type为DATE_TYPE_FIX_TIME_RANGE时专用,表示起用时间。从1970年1月1日00:00:00至起用时间的秒数,最终需转换为字符串形态传入。(东八区时间,单位为秒)"` 55 | EndTimestamp string `json:"end_timestamp" doc:"type为DATE_TYPE_FIX_TIME_RANGE时专用,表示结束时间,建议设置为截止日期的23:59:59过期。(东八区时间,单位为秒)截止日期必须大于当前时间"` 56 | FixedTerm string `json:"fixed_term" doc:"type为DATE_TYPE_FIX_TERM时专用,表示自领取后多少天内有效,不支持填写0。"` 57 | FixedBeginTerm string `json:"end_timestamp" doc:"type为DATE_TYPE_FIX_TERM时专用,表示自领取后多少天开始生效,领取后当天生效填写0。(单位为天)"` 58 | } `json:"date_info" doc:"使用日期,有效期的信息"` 59 | } 60 | 61 | type CouponCardCreateObj struct { 62 | Card struct { 63 | CardType string `json:"card_type"` 64 | GeneralCoupon struct { 65 | BaseInfo *GeneralCouponObj `json:"base_info"` 66 | DefaultDetail string `json:"default_detail" doc:"优惠券专用,填写优惠详情。"` 67 | } `json:"general_coupon"` 68 | } `json:"card"` 69 | } 70 | 71 | type CouponCardCreateParam struct { 72 | BaseInfo GeneralCouponObj `json:"base_info"` 73 | DefaultDetail string `json:"default_detail" doc:"优惠券专用,填写优惠详情。"` 74 | } 75 | 76 | func newCouponObj(param *CouponCardCreateParam) *CouponCardCreateObj { 77 | var res CouponCardCreateObj 78 | res.Card.CardType = "GENERAL_COUPON" 79 | res.Card.GeneralCoupon.BaseInfo = ¶m.BaseInfo 80 | res.Card.GeneralCoupon.DefaultDetail = param.DefaultDetail 81 | return &res 82 | } 83 | 84 | type CouponCreateRes struct { 85 | utils.CommonError 86 | CardID string `json:"card_id"` 87 | } 88 | 89 | func (this CorpApi) CreateCouponCard(param *CouponCardCreateParam) (*CouponCreateRes, error) { 90 | var res CouponCreateRes 91 | if err := this.DoPostObject(createCard, newCouponObj(param), &res); err == nil { 92 | return &res, nil 93 | } else { 94 | return nil, err 95 | } 96 | } 97 | 98 | type CardLiteObj struct { 99 | CardID string `json:"card_id"` 100 | CardType string `json:"card_type"` 101 | Title string `json:"title"` 102 | Status string `json:"status"` 103 | Quantity int `json:"quantity"` 104 | TotalQuantity int `json:"total_quantity"` 105 | Createtime int64 `json:"createtime"` 106 | } 107 | 108 | type CardListRes struct { 109 | utils.CommonError 110 | TotalNum int64 `json:"total_num"` 111 | CardDigestList []CardLiteObj `json:"CardLiteObj"` 112 | } 113 | 114 | type CardListParam struct { 115 | Offset int `json:"offset"` 116 | Count int `json:"count"` 117 | Status string `json:"status,omitempty"` 118 | } 119 | 120 | func (this CorpApi) GetCards(param *CardListParam) (*CouponCreateRes, error) { 121 | var res CouponCreateRes 122 | if err := this.DoPostObject(batchGetCard, param, &res); err == nil { 123 | return &res, nil 124 | } else { 125 | return nil, err 126 | } 127 | } -------------------------------------------------------------------------------- /corp/common.go: -------------------------------------------------------------------------------- 1 | package corp 2 | 3 | import ( 4 | "github.com/qjw/go-wx-sdk/utils" 5 | ) 6 | 7 | 8 | type Config struct { 9 | // 企业号corpid 10 | CorpID string 11 | // 企业号App密钥 12 | CorpSecret string 13 | // 因为同一个企业号会有多个Secret,这里用于区分 14 | Tag string 15 | } 16 | 17 | type AgentConfig struct { 18 | AgentID int64 19 | // 企业号token 20 | Token string 21 | // 企业号消息加密密钥 22 | EncodingAESKey string 23 | } 24 | 25 | type KfConfig struct { 26 | // 企业号token 27 | Token string 28 | // 企业号消息加密密钥 29 | EncodingAESKey string 30 | } 31 | 32 | type CorpApi struct { 33 | utils.ApiTokenBase 34 | Context *Context 35 | } 36 | 37 | func NewCorpApi(context *Context) *CorpApi{ 38 | api := &CorpApi{ 39 | Context:context, 40 | } 41 | api.ContextToken = context 42 | return api 43 | } 44 | -------------------------------------------------------------------------------- /corp/context.go: -------------------------------------------------------------------------------- 1 | package corp 2 | 3 | import ( 4 | "sync" 5 | "fmt" 6 | "encoding/json" 7 | "time" 8 | "github.com/qjw/go-wx-sdk/utils" 9 | "github.com/qjw/go-wx-sdk/cache" 10 | ) 11 | 12 | const ( 13 | accessTokenKey = "corp_access_token_%s_%s" 14 | jsTicketKey = "js_ticket_%s_%s" 15 | accessTokenURL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s" 16 | jsTicketUrl = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=%s" 17 | ) 18 | 19 | // Context struct 20 | type Context struct { 21 | // 配置 22 | Config *Config 23 | 24 | // 缓存处理器 25 | Cache cache.Cache 26 | 27 | //accessTokenLock 读写锁 同一个AppID一个 28 | accessTokenLock *sync.RWMutex 29 | 30 | //jsAPITicket 读写锁 同一个AppID一个 31 | jsAPITicketLock *sync.RWMutex 32 | } 33 | 34 | func (ctx *Context) setJsAPITicketLock(lock *sync.RWMutex) { 35 | ctx.jsAPITicketLock = lock 36 | } 37 | 38 | func NewContext(config *Config,cache cache.Cache) *Context{ 39 | context := &Context{ 40 | Config:config, 41 | Cache:cache, 42 | } 43 | context.setAccessTokenLock(new(sync.RWMutex)) 44 | context.setJsAPITicketLock(new(sync.RWMutex)) 45 | return context 46 | } 47 | 48 | //SetAccessTokenLock 设置读写锁(一个appID一个读写锁) 49 | func (ctx *Context) setAccessTokenLock(l *sync.RWMutex) { 50 | ctx.accessTokenLock = l 51 | } 52 | 53 | //GetAccessToken 获取access_token 54 | func (ctx *Context) GetAccessToken() (accessToken string, err error) { 55 | ctx.accessTokenLock.Lock() 56 | defer ctx.accessTokenLock.Unlock() 57 | 58 | accessTokenCacheKey := fmt.Sprintf(accessTokenKey, ctx.Config.CorpID, ctx.Config.Tag) 59 | val := ctx.Cache.Get(accessTokenCacheKey) 60 | if val != nil { 61 | accessToken = val.(string) 62 | return 63 | } 64 | 65 | //从微信服务器获取 66 | var resAccessToken *utils.ResAccessToken 67 | resAccessToken, err = ctx.GetAccessTokenFromServer() 68 | if err != nil { 69 | return 70 | } 71 | 72 | accessToken = resAccessToken.AccessToken 73 | return 74 | } 75 | 76 | //GetAccessTokenFromServer 强制从微信服务器获取token 77 | func (ctx *Context) GetAccessTokenFromServer() (resAccessToken* utils.ResAccessToken, err error) { 78 | url := fmt.Sprintf(accessTokenURL, 79 | ctx.Config.CorpID, 80 | ctx.Config.CorpSecret) 81 | 82 | body, _, err := utils.HTTPGet(url) 83 | var accessToken utils.ResAccessToken 84 | resAccessToken = &accessToken 85 | err = json.Unmarshal(body, &resAccessToken) 86 | if err != nil { 87 | return 88 | } 89 | 90 | // 企业号=="" ,企业微信=="ok" 91 | if resAccessToken.ErrMsg != "ok" && resAccessToken.ErrMsg != "" { 92 | err = fmt.Errorf("get access_token error : errcode=%v , errormsg=%v", 93 | resAccessToken.ErrCode, resAccessToken.ErrMsg) 94 | return 95 | } 96 | 97 | accessTokenCacheKey := fmt.Sprintf(accessTokenKey, ctx.Config.CorpID, ctx.Config.Tag) 98 | expires := resAccessToken.ExpiresIn - 1500 99 | err = ctx.Cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second) 100 | return 101 | } 102 | 103 | func (ctx *Context) GetJsTicket() (jsTicket string, err error) { 104 | ctx.jsAPITicketLock.Lock() 105 | defer ctx.jsAPITicketLock.Unlock() 106 | 107 | jsTicketCacheKey := fmt.Sprintf(jsTicketKey, ctx.Config.CorpID, ctx.Config.Tag) 108 | val := ctx.Cache.Get(jsTicketCacheKey) 109 | if val != nil { 110 | jsTicket = val.(string) 111 | return 112 | } 113 | 114 | //从微信服务器获取 115 | var resJsTicket *utils.ResJsTicket 116 | resJsTicket, err = ctx.GetJsTicketFromServer() 117 | if err != nil { 118 | return 119 | } 120 | 121 | jsTicket = resJsTicket.Ticket 122 | return 123 | } 124 | 125 | func (ctx *Context) GetJsTicketFromServer() (resJsTicket *utils.ResJsTicket, err error) { 126 | var token string 127 | token,err = ctx.GetAccessToken() 128 | if err != nil{ 129 | return 130 | } 131 | 132 | url := fmt.Sprintf(jsTicketUrl, token) 133 | var jsticket utils.ResJsTicket 134 | resJsTicket = &jsticket 135 | 136 | body, _, err := utils.HTTPGet(url) 137 | err = json.Unmarshal(body, &jsticket) 138 | if err != nil { 139 | return 140 | } 141 | if resJsTicket.ErrCode != 0 || resJsTicket.ErrMsg != "ok"{ 142 | err = fmt.Errorf("get access_token error : errcode=%v , errormsg=%v", 143 | resJsTicket.ErrCode, resJsTicket.ErrMsg) 144 | return 145 | } 146 | 147 | jsTicketCacheKey := fmt.Sprintf(jsTicketKey, ctx.Config.CorpID, ctx.Config.Tag) 148 | expires := resJsTicket.ExpiresIn - 1500 149 | err = ctx.Cache.Set(jsTicketCacheKey, resJsTicket.Ticket, time.Duration(expires)*time.Second) 150 | return 151 | } -------------------------------------------------------------------------------- /corp/department.go: -------------------------------------------------------------------------------- 1 | package corp 2 | 3 | import ( 4 | "github.com/qjw/go-wx-sdk/utils" 5 | "net/url" 6 | "strconv" 7 | ) 8 | 9 | const ( 10 | departmentList = "https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token=%s&id=%d" 11 | departmentList2 = "https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token=%s" 12 | createDepartment = "https://qyapi.weixin.qq.com/cgi-bin/department/create?access_token=%s" 13 | updateDepartment = "https://qyapi.weixin.qq.com/cgi-bin/department/update?access_token=%s" 14 | deleteDepartment = "https://qyapi.weixin.qq.com/cgi-bin/department/delete?access_token=%s&id=%d" 15 | userSimplelist = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist?access_token=%s&%s" 16 | userList = "https://qyapi.weixin.qq.com/cgi-bin/user/list?access_token=%s&%s" 17 | ) 18 | 19 | type DepartmentObj struct { 20 | ID *int64 `json:"id,omitempty"` 21 | Name string `json:"name"` 22 | ParentID int64 `json:"parentid"` 23 | Order *int64 `json:"order,omitempty"` 24 | } 25 | 26 | type DepartListObj struct { 27 | utils.CommonError 28 | Departments []DepartmentObj `json:"department"` 29 | } 30 | 31 | func (this CorpApi) GetDepartments(parent *int64) (*DepartListObj, error) { 32 | var res DepartListObj 33 | var err error 34 | if parent != nil { 35 | err = this.DoGet(departmentList, &res, *parent) 36 | } else { 37 | err = this.DoGet(departmentList2, &res) 38 | } 39 | if err == nil { 40 | return &res, nil 41 | } else { 42 | return nil, err 43 | } 44 | } 45 | 46 | type CreateDepartmentRes struct { 47 | utils.CommonError 48 | id int64 `json:"id"` 49 | } 50 | 51 | func (this CorpApi) CreateDepartment(department *DepartmentObj) (*CreateDepartmentRes, error) { 52 | var res CreateDepartmentRes 53 | if err := this.DoPostObject(createDepartment, department, &res); err == nil { 54 | return &res, nil 55 | } else { 56 | return nil, err 57 | } 58 | } 59 | 60 | type DepartmentUpdateObj struct { 61 | ID int64 `json:"id"` 62 | Name *string `json:"name,omitempty"` 63 | ParentID *int64 `json:"parentid,omitempty"` 64 | Order *int64 `json:"order,omitempty"` 65 | } 66 | 67 | func (this CorpApi) UpdateDepartment(department *DepartmentUpdateObj) (*utils.CommonError, error) { 68 | var res utils.CommonError 69 | if err := this.DoPostObject(updateDepartment, department, &res); err == nil { 70 | return &res, nil 71 | } else { 72 | return nil, err 73 | } 74 | } 75 | 76 | func (this CorpApi) DeleteDepartments(id int64) (*utils.CommonError, error) { 77 | var res utils.CommonError 78 | err := this.DoGet(deleteDepartment, &res, id) 79 | if err == nil { 80 | return &res, nil 81 | } else { 82 | return nil, err 83 | } 84 | } 85 | 86 | type SimpleUserlistObj struct { 87 | ID int64 `json:"department_id"` 88 | FetchChild *int64 `json:"fetch_child,omitempty" doc:"1/0:是否递归获取子部门下面的成员"` 89 | Status *int64 `json:"status,omitempty" doc:"0获取全部成员,1获取已关注成员列表,2获取禁用成员列表,4获取未关注成员列表。status可叠加,未填写则默认为4"` 90 | } 91 | 92 | type SimpleUserListRes struct { 93 | utils.CommonError 94 | UserList []struct { 95 | UserID string `json:"userid"` 96 | Name string `json:"name"` 97 | Department []int64 `json:"department"` 98 | } `json:"userlist"` 99 | } 100 | 101 | func (this CorpApi) DepartmentSimpleUserlist(param *SimpleUserlistObj) (*SimpleUserListRes, error) { 102 | v := url.Values{} 103 | v.Add("department_id", strconv.FormatInt(param.ID, 10)) 104 | if param.FetchChild != nil { 105 | v.Add("fetch_child", strconv.FormatInt(*param.FetchChild, 10)) 106 | } 107 | if param.Status != nil { 108 | v.Add("status", strconv.FormatInt(*param.Status, 10)) 109 | } 110 | 111 | var res SimpleUserListRes 112 | err := this.DoGet(userSimplelist, &res, v.Encode()) 113 | if err == nil { 114 | return &res, nil 115 | } else { 116 | return nil, err 117 | } 118 | } 119 | 120 | type UserCreateObj struct { 121 | UserID string `json:"userid" doc:"成员UserID。对应管理端的帐号,企业内必须唯一。长度为1~64个字节"` 122 | Name string `json:"name" doc:"成员名称。长度为0~64个字节"` 123 | Department []int64 `json:"department" doc:"成员所属部门id列表,不超过20个"` 124 | Position *string `json:"position,omitempty" doc:"职位信息。长度为0~64个字节"` 125 | Mobile *string `json:"mobile,omitempty" doc:"手机号码。企业内必须唯一,mobile/weixinid/email三者不能同时为空"` 126 | Gender *string `json:"gender,omitempty" doc:"性别。1表示男性,2表示女性"` 127 | Email *string `json:"email,omitempty" doc:"邮箱。长度为0~64个字节。企业内必须唯一"` 128 | Weixinid *string `json:"weixinid,omitempty" doc:"微信号。企业内必须唯一。(注意:是微信号,不是微信的名字)"` 129 | Avatar *string `json:"avatar,omitempty" doc:"成员头像的mediaid,通过多媒体接口上传图片获得的mediaid"` 130 | Extattr *struct { 131 | Attrs []struct { 132 | Name string `json:"name"` 133 | Value string `json:"value"` 134 | } `json:"attrs,omitempty"` 135 | } `json:"extattr,omitempty" doc:"扩展属性。扩展属性需要在WEB管理端创建后才生效,否则忽略未知属性的赋值userid"` 136 | } 137 | 138 | type UserUpdateObj struct { 139 | UserID string `json:"userid" doc:"成员UserID。对应管理端的帐号,企业内必须唯一。长度为1~64个字节"` 140 | Name *string `json:"name,omitempty" doc:"成员名称。长度为0~64个字节"` 141 | Department *[]int64 `json:"department,omitempty" doc:"成员所属部门id列表,不超过20个"` 142 | Position *string `json:"position,omitempty" doc:"职位信息。长度为0~64个字节"` 143 | Mobile *string `json:"mobile,omitempty" doc:"手机号码。企业内必须唯一,mobile/weixinid/email三者不能同时为空"` 144 | Gender *string `json:"gender,omitempty" doc:"性别。1表示男性,2表示女性"` 145 | Email *string `json:"email,omitempty" doc:"邮箱。长度为0~64个字节。企业内必须唯一"` 146 | Weixinid *string `json:"weixinid,omitempty" doc:"微信号。企业内必须唯一。(注意:是微信号,不是微信的名字)"` 147 | Avatar *string `json:"avatar,omitempty" doc:"成员头像的mediaid,通过多媒体接口上传图片获得的mediaid"` 148 | Extattr *struct { 149 | Attrs []struct { 150 | Name string `json:"name"` 151 | Value string `json:"value"` 152 | } `json:"attrs,omitempty"` 153 | } `json:"extattr,omitempty" doc:"扩展属性。扩展属性需要在WEB管理端创建后才生效,否则忽略未知属性的赋值userid"` 154 | Enable int `json:"enable" doc:"启用/禁用成员。1表示启用成员,0表示禁用成员"` 155 | } 156 | 157 | type UserObj struct { 158 | UserCreateObj 159 | Status int `json:"status"` 160 | } 161 | 162 | type UserListRes struct { 163 | utils.CommonError 164 | UserList []UserObj `json:"userlist"` 165 | } 166 | 167 | func (this CorpApi) DepartmentUserlist(param *SimpleUserlistObj) (*UserListRes, error) { 168 | v := url.Values{} 169 | v.Add("department_id", strconv.FormatInt(param.ID, 10)) 170 | if param.FetchChild != nil { 171 | v.Add("fetch_child", strconv.FormatInt(*param.FetchChild, 10)) 172 | } 173 | if param.Status != nil { 174 | v.Add("status", strconv.FormatInt(*param.Status, 10)) 175 | } 176 | 177 | var res UserListRes 178 | err := this.DoGet(userList, &res, v.Encode()) 179 | if err == nil { 180 | return &res, nil 181 | } else { 182 | return nil, err 183 | } 184 | } 185 | 186 | const ( 187 | userGet = "https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=%s&userid=%s" 188 | userDelete = "https://qyapi.weixin.qq.com/cgi-bin/user/delete?access_token=%s&userid=%s" 189 | userBatchDelete = "https://qyapi.weixin.qq.com/cgi-bin/user/batchdelete?access_token=%s" 190 | userCreate = "https://qyapi.weixin.qq.com/cgi-bin/user/create?access_token=%s" 191 | userUpdate = "https://qyapi.weixin.qq.com/cgi-bin/user/update?access_token=%s" 192 | ) 193 | 194 | type UserInfoRes struct { 195 | utils.CommonError 196 | UserObj 197 | } 198 | 199 | func (this CorpApi) GetUser(userid string) (*UserInfoRes, error) { 200 | var res UserInfoRes 201 | if err := this.DoGet(userGet, &res, userid); err == nil { 202 | return &res, nil 203 | } else { 204 | return nil, err 205 | } 206 | } 207 | 208 | func (this CorpApi) DeleteUser(userid string) (*utils.CommonError, error) { 209 | var res utils.CommonError 210 | if err := this.DoGet(userDelete, &res, userid); err == nil { 211 | return &res, nil 212 | } else { 213 | return nil, err 214 | } 215 | } 216 | 217 | type BatchDeleteUserObj struct { 218 | UserIDList []string `json:"useridlist"` 219 | } 220 | 221 | func (this CorpApi) BatchDeleteUser(userids []string) (*utils.CommonError, error) { 222 | var res utils.CommonError 223 | if err := this.DoPostObject(userBatchDelete, &BatchDeleteUserObj{ 224 | UserIDList: userids, 225 | }, &res); err == nil { 226 | return &res, nil 227 | } else { 228 | return nil, err 229 | } 230 | } 231 | 232 | func (this CorpApi) CreateUser(param *UserCreateObj) (*utils.CommonError, error) { 233 | var res utils.CommonError 234 | if err := this.DoPostObject(userCreate, param, &res); err == nil { 235 | return &res, nil 236 | } else { 237 | return nil, err 238 | } 239 | } 240 | 241 | func (this CorpApi) UpdateUser(param *UserUpdateObj) (*utils.CommonError, error) { 242 | var res utils.CommonError 243 | if err := this.DoPostObject(userUpdate, param, &res); err == nil { 244 | return &res, nil 245 | } else { 246 | return nil, err 247 | } 248 | } 249 | 250 | const ( 251 | tagCreate = "https://qyapi.weixin.qq.com/cgi-bin/tag/create?access_token=%s" 252 | tagUpdate = "https://qyapi.weixin.qq.com/cgi-bin/tag/update?access_token=%s" 253 | tagDelete = "https://qyapi.weixin.qq.com/cgi-bin/tag/delete?access_token=%s&tagid=%d" 254 | tagGetUsers = "https://qyapi.weixin.qq.com/cgi-bin/tag/get?access_token=%s&tagid=%d" 255 | tagAddUsers = "https://qyapi.weixin.qq.com/cgi-bin/tag/addtagusers?access_token=%s" 256 | tagDelUsers = "https://qyapi.weixin.qq.com/cgi-bin/tag/deltagusers?access_token=%s" 257 | tagList = "https://qyapi.weixin.qq.com/cgi-bin/tag/list?access_token=%s" 258 | ) 259 | 260 | type TagUserListRes struct { 261 | utils.CommonError 262 | Userlist []struct { 263 | Userid string `json:"userid"` 264 | Name string `json:"name"` 265 | } `json:"userlist"` 266 | Partylist []int64 `json:"partylist"` 267 | } 268 | 269 | func (this CorpApi) GetTagUsers(tagid int64) (*TagUserListRes, error) { 270 | var res TagUserListRes 271 | if err := this.DoGet(tagGetUsers, &res, tagid); err == nil { 272 | return &res, nil 273 | } else { 274 | return nil, err 275 | } 276 | } 277 | 278 | type TagUserUpdateObj struct { 279 | TagID int64 `json:"tagid"` 280 | Userlist []string `json:"userlist"` 281 | Partylist []int64 `json:"partylist"` 282 | } 283 | 284 | type TagUpdateRes struct { 285 | utils.CommonError 286 | Invalidlist string `json:"invalidlist"` 287 | Invalidparty []int64 `json:"invalidparty"` 288 | } 289 | 290 | func (this CorpApi) AddTagUsers(tag *TagUserUpdateObj) (*TagUpdateRes, error) { 291 | var res TagUpdateRes 292 | if err := this.DoPostObject(tagAddUsers, tag, &res); err == nil { 293 | return &res, nil 294 | } else { 295 | return nil, err 296 | } 297 | } 298 | 299 | func (this CorpApi) DelTagUsers(tag *TagUserUpdateObj) (*TagUpdateRes, error) { 300 | var res TagUpdateRes 301 | if err := this.DoPostObject(tagDelUsers, tag, &res); err == nil { 302 | return &res, nil 303 | } else { 304 | return nil, err 305 | } 306 | } 307 | 308 | type TagObj struct { 309 | Tagid int64 `json:"tagid"` 310 | Tagname string `json:"tagname"` 311 | } 312 | 313 | type TagCreateObj struct { 314 | Tagid *int64 `json:"tagid,omitempty"` 315 | Tagname string `json:"tagname"` 316 | } 317 | 318 | type TagListRes struct { 319 | utils.CommonError 320 | Taglist []TagObj `json:"taglist"` 321 | } 322 | 323 | func (this CorpApi) UpdateTag(tag *TagObj) (*utils.CommonError, error) { 324 | var res utils.CommonError 325 | if err := this.DoPostObject(tagUpdate, tag, &res); err == nil { 326 | return &res, nil 327 | } else { 328 | return nil, err 329 | } 330 | } 331 | 332 | type TagCreateRes struct { 333 | utils.CommonError 334 | Tagid int64 `json:"tagid"` 335 | } 336 | 337 | func (this CorpApi) CreateTag(tag *TagCreateObj) (*TagCreateRes, error) { 338 | var res TagCreateRes 339 | if err := this.DoPostObject(tagCreate, tag, &res); err == nil { 340 | return &res, nil 341 | } else { 342 | return nil, err 343 | } 344 | } 345 | 346 | func (this CorpApi) DeleteTag(tagid int64) (*utils.CommonError, error) { 347 | var res utils.CommonError 348 | if err := this.DoGet(tagDelete, &res, tagid); err == nil { 349 | return &res, nil 350 | } else { 351 | return nil, err 352 | } 353 | } 354 | 355 | func (this CorpApi) GetTags() (*TagListRes, error) { 356 | var res TagListRes 357 | if err := this.DoGet(tagList, &res); err == nil { 358 | return &res, nil 359 | } else { 360 | return nil, err 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /corp/kf.go: -------------------------------------------------------------------------------- 1 | package corp 2 | 3 | import ( 4 | "github.com/qjw/go-wx-sdk/utils" 5 | ) 6 | 7 | const ( 8 | kfSend = "https://qyapi.weixin.qq.com/cgi-bin/kf/send?access_token=%s" 9 | kfList = "https://qyapi.weixin.qq.com/cgi-bin/kf/list?access_token=%s&type=%s" 10 | ) 11 | 12 | type KfMsgUserObj struct { 13 | Type string `json:"type"` 14 | ID string `json:"id"` 15 | } 16 | 17 | type KfTextMsgObj struct { 18 | Content string `json:"content"` 19 | } 20 | 21 | type KfImageMsgObj struct { 22 | MediaID string `json:"media_id"` 23 | } 24 | 25 | type KfMsgObj struct { 26 | Sender KfMsgUserObj `json:"sender"` 27 | Receiver KfMsgUserObj `json:"receiver"` 28 | Msgtype string `json:"msgtype"` 29 | Text *KfTextMsgObj `json:"text,omitempty"` 30 | Image *KfImageMsgObj `json:"image,omitempty"` 31 | File *KfImageMsgObj `json:"file,omitempty"` 32 | Voice *KfImageMsgObj `json:"voice,omitempty"` 33 | } 34 | 35 | func (this CorpApi) sendKfImp(from, to *KfMsgUserObj, msg *KfMsgObj) (*utils.CommonError, error) { 36 | var res utils.CommonError 37 | if err := this.DoPostObject(kfSend, msg, &res); err == nil { 38 | return &res, nil 39 | } else { 40 | return nil, err 41 | } 42 | } 43 | 44 | func (this CorpApi) SendKfText(from, to *KfMsgUserObj, content string) (*utils.CommonError, error) { 45 | return this.sendKfImp(from, to, &KfMsgObj{ 46 | Sender: *from, 47 | Receiver: *to, 48 | Msgtype: "text", 49 | Text: &KfTextMsgObj{ 50 | Content: content, 51 | }, 52 | }) 53 | } 54 | 55 | func (this CorpApi) SendKfImage(from, to *KfMsgUserObj, mediaid string) (*utils.CommonError, error) { 56 | return this.sendKfImp(from, to, &KfMsgObj{ 57 | Sender: *from, 58 | Receiver: *to, 59 | Msgtype: "image", 60 | Image: &KfImageMsgObj{ 61 | MediaID: mediaid, 62 | }, 63 | }) 64 | } 65 | 66 | func (this CorpApi) SendKfFile(from, to *KfMsgUserObj, mediaid string) (*utils.CommonError, error) { 67 | return this.sendKfImp(from, to, &KfMsgObj{ 68 | Sender: *from, 69 | Receiver: *to, 70 | Msgtype: "file", 71 | File: &KfImageMsgObj{ 72 | MediaID: mediaid, 73 | }, 74 | }) 75 | } 76 | 77 | func (this CorpApi) SendKfVoice(from, to *KfMsgUserObj, mediaid string) (*utils.CommonError, error) { 78 | return this.sendKfImp(from, to, &KfMsgObj{ 79 | Sender: *from, 80 | Receiver: *to, 81 | Msgtype: "voice", 82 | Voice: &KfImageMsgObj{ 83 | MediaID: mediaid, 84 | }, 85 | }) 86 | } 87 | 88 | type KfObj struct { 89 | Users []string `json:"user"` 90 | Parties []int64 `json:"party"` 91 | Tags []string `json:"tag"` 92 | } 93 | 94 | type KfListRes struct { 95 | utils.CommonError 96 | Internal *KfObj `json:"internal,omitempty"` 97 | External *KfObj `json:"external,omitempty"` 98 | } 99 | 100 | func (this CorpApi) GetKfList(tp string) (*KfListRes, error) { 101 | var res KfListRes 102 | err := this.DoGet(kfList, &res, tp) 103 | if err == nil { 104 | return &res, nil 105 | } else { 106 | return nil, err 107 | } 108 | } -------------------------------------------------------------------------------- /corp/kf_server.go: -------------------------------------------------------------------------------- 1 | package corp 2 | 3 | import ( 4 | "log" 5 | "encoding/xml" 6 | "fmt" 7 | "github.com/qjw/go-wx-sdk/utils" 8 | "net/http" 9 | "strconv" 10 | ) 11 | 12 | type KfMessageHandle func(*KfMixMessage) 13 | 14 | type KfServer struct { 15 | Request *http.Request 16 | Responce http.ResponseWriter 17 | CorpContext *Context 18 | KfConfig *KfConfig 19 | 20 | // 收到消息的回调 21 | MessageHandler KfMessageHandle 22 | } 23 | 24 | type KfServerRequest struct { 25 | MixedMsg *KfMixMessage 26 | RequestHttpBody *KfRequestEncryptedXMLMsg 27 | 28 | Random []byte 29 | Nonce string 30 | Timestamp int64 31 | 32 | // 回复的消息 33 | ResponseRawXMLMsg []byte 34 | ResponseMsg utils.Reply 35 | } 36 | 37 | //NewServer init 38 | func NewKfServer(request *http.Request, responce http.ResponseWriter, 39 | handle KfMessageHandle, mpwcontext *Context, kfConfig *KfConfig) *KfServer { 40 | return &KfServer{ 41 | Request: request, 42 | Responce: responce, 43 | CorpContext: mpwcontext, 44 | KfConfig: kfConfig, 45 | MessageHandler: handle, 46 | } 47 | } 48 | 49 | //Serve 处理微信的请求消息 50 | func (srv *KfServer) Ping() { 51 | echostr := srv.Request.URL.Query().Get("echostr") 52 | if echostr == "" { 53 | log.Print("invalid echostr") 54 | http.Error(srv.Responce, "", http.StatusForbidden) 55 | return 56 | } 57 | 58 | if !srv.validate(nil, echostr) { 59 | http.Error(srv.Responce, "", http.StatusForbidden) 60 | return 61 | } 62 | 63 | _, echostrRes, _, err := utils.DecryptMsg(srv.CorpContext.Config.CorpID, echostr, srv.KfConfig.EncodingAESKey) 64 | if err != nil { 65 | log.Print("invalid DecryptMsg") 66 | http.Error(srv.Responce, "", http.StatusForbidden) 67 | } 68 | 69 | http.Error(srv.Responce, string(echostrRes), http.StatusOK) 70 | } 71 | 72 | //Serve 处理微信的请求消息 73 | func (srv *KfServer) Serve() error { 74 | var svrReq KfServerRequest 75 | 76 | // 解析 RequestHttpBody 77 | var requestHttpBody KfRequestEncryptedXMLMsg 78 | if err := xml.NewDecoder(srv.Request.Body).Decode(&requestHttpBody); err != nil { 79 | log.Print(err.Error()) 80 | return err 81 | } 82 | 83 | if !srv.validate(&svrReq, requestHttpBody.EncryptedMsg) { 84 | return fmt.Errorf("请求校验失败") 85 | } 86 | 87 | svrReq.RequestHttpBody = &requestHttpBody 88 | err := srv.handleRequest(&svrReq) 89 | if err != nil { 90 | return err 91 | } 92 | 93 | // 企业在收到数据包时,需回复XML里的PackageId节点值,表示成功接收,否则企业号侧认为回调失败。 94 | http.Error(srv.Responce, strconv.FormatInt(svrReq.MixedMsg.PackageId, 10), http.StatusOK) 95 | return nil 96 | } 97 | 98 | func (srv *KfServer) validate(svrReq *KfServerRequest, content string) bool { 99 | signature := srv.Request.URL.Query().Get("msg_signature") 100 | if signature == "" { 101 | log.Print("invalid msg_signature") 102 | return false 103 | } 104 | timestamp := srv.Request.URL.Query().Get("timestamp") 105 | if timestamp == "" { 106 | log.Print("invalid timestamp") 107 | return false 108 | } 109 | 110 | timestampInt, err := strconv.ParseInt(timestamp, 10, 64) 111 | if err != nil { 112 | log.Print(err.Error()) 113 | return false 114 | } 115 | 116 | nonce := srv.Request.URL.Query().Get("nonce") 117 | if nonce == "" { 118 | log.Print("invalid nonce") 119 | return false 120 | } 121 | 122 | // 验证签名 123 | msgSignature2 := utils.Signature(srv.KfConfig.Token, timestamp, nonce, content) 124 | if signature != msgSignature2 { 125 | log.Print("invalid signature") 126 | return false 127 | } 128 | 129 | if svrReq != nil { 130 | svrReq.Timestamp = timestampInt 131 | svrReq.Nonce = nonce 132 | } 133 | return true 134 | } 135 | 136 | //HandleRequest 处理微信的请求 137 | func (srv *KfServer) handleRequest(svrReq *KfServerRequest) (err error) { 138 | err = srv.getMessage(svrReq) 139 | if err != nil { 140 | return 141 | } 142 | 143 | if srv.MessageHandler != nil { 144 | srv.MessageHandler(svrReq.MixedMsg) 145 | } 146 | return 147 | } 148 | 149 | //getMessage 解析微信返回的消息 150 | func (srv *KfServer) getMessage(svrReq *KfServerRequest) error { 151 | // 解密 152 | random, rawMsgXML, appID, err := utils.DecryptMsg( 153 | srv.CorpContext.Config.CorpID, 154 | svrReq.RequestHttpBody.EncryptedMsg, 155 | srv.KfConfig.EncodingAESKey) 156 | if err != nil { 157 | log.Print("invalid DecryptMsg") 158 | return err 159 | } 160 | svrReq.Random = random 161 | 162 | if svrReq.RequestHttpBody.ToUserName != appID { 163 | err := fmt.Errorf("the RequestHttpBody's ToUserName(==%s) mismatch the appID with aes encrypt(==%s)", 164 | svrReq.RequestHttpBody.ToUserName, appID) 165 | return err 166 | } 167 | 168 | // 解密成功, 解析 MixedMessage 169 | var mixedMsg KfMixMessage 170 | if err = xml.Unmarshal(rawMsgXML, &mixedMsg); err != nil { 171 | log.Print(err.Error()) 172 | return err 173 | } 174 | svrReq.MixedMsg = &mixedMsg 175 | 176 | // 安全考虑再次验证 177 | if svrReq.RequestHttpBody.ToUserName != mixedMsg.ToUserName { 178 | err := fmt.Errorf("the RequestHttpBody's ToUserName(==%s) mismatch the MixedMessage's SuiteId", 179 | svrReq.RequestHttpBody.ToUserName) 180 | return err 181 | } 182 | 183 | return nil 184 | } 185 | 186 | // 微信服务器请求 http body 187 | type KfRequestEncryptedXMLMsg struct { 188 | utils.RequestEncryptedXMLMsg 189 | AgentType string `xml:"AgentType"` 190 | } 191 | 192 | // CommonToken 消息中通用的结构 193 | type KfMixMessage struct { 194 | XMLName xml.Name `xml:"xml" json:"-"` 195 | ToUserName string `xml:"ToUserName" json:"to_username"` 196 | AgentType string `xml:"AgentType" json:"agent_type"` 197 | ItemCount int `xml:"ItemCount" json:"item_count"` 198 | PackageId int64 `xml:"PackageId" json:"package_id"` 199 | Item []KfMixMessageItem `xml:"Item" json:"items"` 200 | } 201 | 202 | type KfMixMessageItemHead struct { 203 | FromUserName string `xml:"FromUserName" json:"from_username"` 204 | CreateTime int64 `xml:"CreateTime" json:"create_time"` 205 | MsgType string `xml:"MsgType" json:"msg_type"` 206 | } 207 | 208 | type KfMixMessageItem struct { 209 | KfMixMessageItemHead 210 | MsgID int64 `xml:"MsgId" json:"msg_id"` 211 | 212 | Content string `xml:"Content" json:"content,omitempty"` 213 | PicURL string `xml:"PicUrl" json:"pic_url,omitempty"` 214 | MediaID string `xml:"MediaId" json:"media_id,omitempty"` 215 | //Format string `xml:"Format" json:"format,omitempty"` 216 | //ThumbMediaID string `xml:"ThumbMediaId" json:"thumb_media_id,omitempty"` 217 | 218 | // link 219 | Title string `xml:"Title" json:"title,omitempty"` 220 | Description string `xml:"Description" json:"description,omitempty"` 221 | URL string `xml:"Url" json:"url,omitempty"` 222 | // location 223 | LocationX float64 `xml:"Location_X" json:"location_x,omitempty"` 224 | LocationY float64 `xml:"Location_Y" json:"location_y,omitempty"` 225 | Scale float64 `xml:"Scale" json:"scale,omitempty"` 226 | Label string `xml:"Label" json:"label,omitempty"` 227 | } 228 | -------------------------------------------------------------------------------- /corp/media.go: -------------------------------------------------------------------------------- 1 | package corp 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "github.com/qjw/go-wx-sdk/utils" 7 | ) 8 | 9 | const ( 10 | mediaUpload = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=%s&type=%s" 11 | mediaGet = "https://qyapi.weixin.qq.com/cgi-bin/media/get?access_token=%s&media_id=%s" 12 | ) 13 | 14 | type MediaUploadRes struct { 15 | utils.CommonError 16 | Type string `json:"type"` 17 | MediaID string `json:"media_id"` 18 | // 企业微信返回字符串,企业号返回数字,坑嗲 19 | // CreatedAt int64 `json:"created_at"` 20 | } 21 | 22 | func (this CorpApi) UploadTmpMedia(reader io.Reader, filename, tp string) (*MediaUploadRes, error) { 23 | var res MediaUploadRes 24 | if err := this.DoPostFile(reader, "media", filename, &res, mediaUpload, tp); err == nil { 25 | return &res, nil 26 | } else { 27 | return nil, err 28 | } 29 | } 30 | 31 | func (this CorpApi) GetTmpMedia(media_id string) (string, error) { 32 | accessToken, err := this.Context.GetAccessToken() 33 | if err != nil { 34 | return "", err 35 | } 36 | return string(fmt.Sprintf(mediaGet, accessToken, media_id)), nil 37 | } 38 | -------------------------------------------------------------------------------- /corp/menu.go: -------------------------------------------------------------------------------- 1 | package corp 2 | 3 | import "github.com/qjw/go-wx-sdk/utils" 4 | 5 | const ( 6 | menuGet = "https://qyapi.weixin.qq.com/cgi-bin/menu/get?access_token=%s&agentid=%d" 7 | menuCreate = "https://qyapi.weixin.qq.com/cgi-bin/menu/create?access_token=%s&agentid=%d" 8 | menuDelete = "https://qyapi.weixin.qq.com/cgi-bin/menu/delete?access_token=%s&agentid=%d" 9 | ) 10 | 11 | type MenuEntryObj struct { 12 | Type string `json:"type"` 13 | Name string `json:"name"` 14 | Key string `json:"key,omitempty"` 15 | Url string `json:"url,omitempty"` 16 | AppID string `json:"appid,omitempty"` 17 | Pagepath string `json:"pagepath,omitempty"` 18 | MediaID string `json:"media_id,omitempty"` 19 | SubButton []*MenuEntryObj `json:"sub_button,omitempty"` 20 | } 21 | 22 | type MenuObj struct { 23 | Menu MenuCreateObj `json:"menu"` 24 | } 25 | 26 | type MenuCreateObj struct { 27 | Buttons []*MenuEntryObj `json:"button,omitempty"` 28 | } 29 | 30 | func (this CorpApi) GetMenu(agentid int64) (*MenuObj, error) { 31 | var res MenuObj 32 | if err := this.DoGet(menuGet, &res, agentid); err == nil { 33 | return &res, nil 34 | } else { 35 | return nil, err 36 | } 37 | } 38 | 39 | 40 | func (this CorpApi) CreateMenu(agentid int64,param *MenuCreateObj) (*utils.CommonError, error) { 41 | var res utils.CommonError 42 | if err := this.DoPostObject(menuCreate, param, &res, agentid); err == nil { 43 | return &res, nil 44 | } else { 45 | return nil, err 46 | } 47 | } 48 | 49 | func (this CorpApi) DeleteMenu(agentid int64) (*utils.CommonError, error) { 50 | var res utils.CommonError 51 | if err := this.DoGet(menuDelete, &res, agentid); err == nil { 52 | return &res, nil 53 | } else { 54 | return nil, err 55 | } 56 | } -------------------------------------------------------------------------------- /corp/msg.go: -------------------------------------------------------------------------------- 1 | package corp 2 | 3 | import ( 4 | "errors" 5 | "github.com/qjw/go-wx-sdk/utils" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | const ( 11 | msssageSend = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=%s" 12 | ) 13 | 14 | type MsgSendRes struct { 15 | utils.CommonError 16 | InvalidUser string `json:"invaliduser"` 17 | InvalidParty string `json:"invalidparty"` 18 | InvalidTag string `json:"invalidtag"` 19 | } 20 | 21 | type textMsgObj struct { 22 | Content string `json:"content"` 23 | } 24 | 25 | type imageMsgObj struct { 26 | MediaID string `json:"media_id"` 27 | } 28 | type voiceMsgObj imageMsgObj 29 | type fileMsgObj imageMsgObj 30 | 31 | type videoMsgObj struct { 32 | MediaID string `json:"media_id"` 33 | Title string `json:"title"` 34 | Description string `json:"description"` 35 | } 36 | 37 | type NewsMsgObj struct { 38 | Articles []struct { 39 | Title string `json:"title"` 40 | Description string `json:"description"` 41 | Url string `json:"url"` 42 | Picurl string `json:"picurl"` 43 | } `json:"articles"` 44 | } 45 | 46 | type TextCardObj struct { 47 | Title string `json:"title"` 48 | Description string `json:"description"` 49 | Url string `json:"url"` 50 | Btntxt string `json:"btntxt"` 51 | } 52 | 53 | type MsgObj struct { 54 | ToUser string `json:"touser,omitempty" doc:"成员ID列表(消息接收者,多个接收者用‘|’分隔,最多支持1000个)。特殊情况:指定为@all,则向关注该企业应用的全部成员发送"` 55 | ToParty string `json:"toparty,omitempty" doc:"部门ID列表,多个接收者用‘|’分隔,最多支持100个。当touser为@all时忽略本参数"` 56 | ToTag string `json:"totag,omitempty" doc:"标签ID列表,多个接收者用‘|’分隔,最多支持100个。当touser为@all时忽略本参数"` 57 | MsgType string `json:"msgtype"` 58 | AgentID int64 `json:"agentid"` 59 | Safe string `json:"safe,omitempty" doc:"表示是否是保密消息,0表示否,1表示是,默认0"` 60 | Text *textMsgObj `json:"text,omitempty"` 61 | Image *imageMsgObj `json:"image,omitempty"` 62 | Voice *voiceMsgObj `json:"voice,omitempty"` 63 | File *fileMsgObj `json:"file,omitempty"` 64 | Video *videoMsgObj `json:"video,omitempty"` 65 | TextCard *TextCardObj `json:"textcard,omitempty"` 66 | News *NewsMsgObj `json:"news,omitempty"` 67 | } 68 | 69 | // 用 '|' 连接 a 的各个元素的十进制字符串 70 | func joinInt64(a []int64, sep string) string { 71 | switch len(a) { 72 | case 0: 73 | return "" 74 | case 1: 75 | return strconv.FormatInt(a[0], 10) 76 | default: 77 | strs := make([]string, len(a)) 78 | for i, n := range a { 79 | strs[i] = strconv.FormatInt(n, 10) 80 | } 81 | return strings.Join(strs, sep) 82 | } 83 | } 84 | 85 | func (this CorpApi) sendMsgImp(touser []string, toparty, totag []int64, msg *MsgObj) (*utils.CommonError, error) { 86 | if len(touser) == 0 && len(toparty) == 0 && len(totag) == 0 { 87 | return nil, errors.New("empty reciver") 88 | } 89 | if len(touser) > 0 { 90 | msg.ToUser = strings.Join(touser, "|") 91 | } 92 | if len(toparty) > 0 { 93 | msg.ToParty = joinInt64(toparty, "|") 94 | } 95 | if len(totag) > 0 { 96 | msg.ToTag = joinInt64(totag, "|") 97 | } 98 | 99 | var res utils.CommonError 100 | if err := this.DoPostObject(msssageSend, msg, &res); err == nil { 101 | return &res, nil 102 | } else { 103 | return nil, err 104 | } 105 | } 106 | 107 | func (this CorpApi) SendTextCardMsg(touser []string, toparty, totag []int64, agentid int64, 108 | card *TextCardObj) (*utils.CommonError, error) { 109 | return this.sendMsgImp(touser, toparty, totag, &MsgObj{ 110 | MsgType: "textcard", 111 | AgentID: agentid, 112 | TextCard: card, 113 | }) 114 | } 115 | 116 | func (this CorpApi) SendTextMsg(touser []string, toparty, totag []int64, agentid int64, 117 | content string) (*utils.CommonError, error) { 118 | return this.sendMsgImp(touser, toparty, totag, &MsgObj{ 119 | MsgType: "text", 120 | AgentID: agentid, 121 | Text: &textMsgObj{ 122 | Content: content, 123 | }, 124 | }) 125 | } 126 | 127 | func (this CorpApi) SendImageMsg(touser []string, toparty, totag []int64, agentid int64, 128 | mediaid string) (*utils.CommonError, error) { 129 | return this.sendMsgImp(touser, toparty, totag, &MsgObj{ 130 | MsgType: "image", 131 | AgentID: agentid, 132 | Image: &imageMsgObj{ 133 | MediaID: mediaid, 134 | }, 135 | }) 136 | } 137 | 138 | func (this CorpApi) SendVoiceMsg(touser []string, toparty, totag []int64, agentid int64, 139 | mediaid string) (*utils.CommonError, error) { 140 | return this.sendMsgImp(touser, toparty, totag, &MsgObj{ 141 | MsgType: "voice", 142 | AgentID: agentid, 143 | Voice: &voiceMsgObj{ 144 | MediaID: mediaid, 145 | }, 146 | }) 147 | } 148 | 149 | func (this CorpApi) SendFileMsg(touser []string, toparty, totag []int64, agentid int64, 150 | mediaid string) (*utils.CommonError, error) { 151 | return this.sendMsgImp(touser, toparty, totag, &MsgObj{ 152 | MsgType: "file", 153 | AgentID: agentid, 154 | File: &fileMsgObj{ 155 | MediaID: mediaid, 156 | }, 157 | }) 158 | } 159 | 160 | func (this CorpApi) SendVideoMsg(touser []string, toparty, totag []int64, agentid int64, 161 | mediaid, title, desc string) (*utils.CommonError, error) { 162 | return this.sendMsgImp(touser, toparty, totag, &MsgObj{ 163 | MsgType: "video", 164 | AgentID: agentid, 165 | Video: &videoMsgObj{ 166 | Title: title, 167 | Description: desc, 168 | MediaID: mediaid, 169 | }, 170 | }) 171 | } 172 | 173 | func (this CorpApi) SendNewsMsg(touser []string, toparty, totag []int64, agentid int64, 174 | news *NewsMsgObj) (*utils.CommonError, error) { 175 | return this.sendMsgImp(touser, toparty, totag, &MsgObj{ 176 | MsgType: "news", 177 | AgentID: agentid, 178 | News: news, 179 | }) 180 | } 181 | -------------------------------------------------------------------------------- /corp/oauth2.go: -------------------------------------------------------------------------------- 1 | package corp 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qjw/go-wx-sdk/utils" 6 | "net/url" 7 | "strconv" 8 | ) 9 | 10 | const ( 11 | oauth2_authorize = "https://open.weixin.qq.com/connect/oauth2/authorize" 12 | oauth2_getuserinfo = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=%s&code=%s" 13 | oauth2_getuserdetail = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserdetail?access_token=%s" 14 | qrConnect = "https://open.work.weixin.qq.com/wwopen/sso/qrConnect" 15 | qrConnecxt_3rd = "https://open.work.weixin.qq.com/wwopen/sso/3rd_qrConnect" 16 | ) 17 | 18 | func (this CorpApi) authorizeUrl(redirectURI, scope, state string, agentID *int64) string { 19 | url := oauth2_authorize + "?appid=" + url.QueryEscape(this.Context.Config.CorpID) + 20 | "&redirect_uri=" + url.QueryEscape(redirectURI) + 21 | "&response_type=code&scope=" + url.QueryEscape(scope) + 22 | "&state=" + url.QueryEscape(state) 23 | if agentID != nil { 24 | url = url + "&agentid=" + strconv.FormatInt(*agentID, 10) 25 | } 26 | return url + "#wechat_redirect" 27 | } 28 | 29 | func (this CorpApi) QrConnectUrl(redirectURI, state, appid, agentID string) string { 30 | return fmt.Sprintf("%s?appid=%s&agentid=%s&redirect_uri=%s&state=%s", 31 | qrConnect, 32 | appid, 33 | agentID, 34 | url.QueryEscape(redirectURI), 35 | state) 36 | } 37 | 38 | func (this CorpApi) AuthorizeUserinfo(redirectURI, state string, agentID int64) string { 39 | return this.authorizeUrl(redirectURI, "snsapi_userinfo", state, &agentID) 40 | } 41 | 42 | func (this CorpApi) AuthorizeBase(redirectURI, state string) string { 43 | return this.authorizeUrl(redirectURI, "snsapi_base", state, nil) 44 | } 45 | 46 | func (this CorpApi) AuthorizePrivateInfo(redirectURI, state string, agentID int64) string { 47 | return this.authorizeUrl(redirectURI, "snsapi_privateinfo", state, &agentID) 48 | } 49 | 50 | type OauthUserDetail struct { 51 | utils.CommonError 52 | UserID string `json:"userid,omitempty"` 53 | Name string `json:"name,omitempty"` 54 | Gender string `json:"gender"` 55 | Department []int64 `json:"department,omitempty"` 56 | Avatar string `json:"avatar,omitempty"` 57 | Email string `json:"email,omitempty"` 58 | Position string `json:"position,omitempty"` 59 | Mobile string `json:"mobile,omitempty"` 60 | } 61 | 62 | func (this CorpApi) Oauth2GetUserDetail(user_ticket string) (*OauthUserDetail, error) { 63 | var res OauthUserDetail 64 | if err := this.DoPostObject(oauth2_getuserdetail, 65 | &struct { 66 | UserTicket string `json:"user_ticket"` 67 | }{UserTicket: user_ticket}, &res); err == nil { 68 | return &res, nil 69 | } else { 70 | return nil, err 71 | } 72 | } 73 | 74 | type AuthorizeAccessToken struct { 75 | utils.CommonError 76 | ExpiresIn int64 `json:"expires_in,omitempty"` 77 | UserTicket string `json:"user_ticket,omitempty"` 78 | DeviceId string `json:"DeviceId,omitempty"` 79 | UserId string `json:"UserId,omitempty"` 80 | } 81 | 82 | func (this CorpApi) Oauth2GetUserInfo(code string) (*AuthorizeAccessToken, error) { 83 | var res AuthorizeAccessToken 84 | err := this.DoGet(oauth2_getuserinfo, &res, code) 85 | if err == nil { 86 | return &res, nil 87 | } else { 88 | return nil, err 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /corp/recv_msg.go: -------------------------------------------------------------------------------- 1 | package corp 2 | 3 | import "encoding/xml" 4 | 5 | 6 | // CommonToken 消息中通用的结构 7 | type MixCommonToken struct { 8 | XMLName xml.Name `xml:"xml" json:"-"` 9 | ToUserName string `xml:"ToUserName" json:"to_username"` 10 | FromUserName string `xml:"FromUserName" json:"from_username"` 11 | CreateTime int64 `xml:"CreateTime" json:"create_time"` 12 | MsgType string `xml:"MsgType" json:"msg_type"` 13 | AgentID int64 `xml:"AgentID" json:"agent_id"` 14 | } 15 | 16 | type MixMessage struct { 17 | MixCommonToken 18 | 19 | MsgID int64 `xml:"MsgId" json:"msg_id"` 20 | 21 | Content string `xml:"Content" json:"content,omitempty"` 22 | PicURL string `xml:"PicUrl" json:"pic_url,omitempty"` 23 | MediaID string `xml:"MediaId" json:"media_id,omitempty"` 24 | Format string `xml:"Format" json:"format,omitempty"` 25 | ThumbMediaID string `xml:"ThumbMediaId" json:"thumb_media_id,omitempty"` 26 | 27 | // location 28 | LocationX float64 `xml:"Location_X" json:"location_x,omitempty"` 29 | LocationY float64 `xml:"Location_Y" json:"location_y,omitempty"` 30 | Scale float64 `xml:"Scale" json:"scale,omitempty"` 31 | Label string `xml:"Label" json:"label,omitempty"` 32 | 33 | // link 34 | Title string `xml:"Title" json:"title,omitempty"` 35 | Description string `xml:"Description" json:"description,omitempty"` 36 | URL string `xml:"Url" json:"url,omitempty"` 37 | } 38 | -------------------------------------------------------------------------------- /corp/server.go: -------------------------------------------------------------------------------- 1 | package corp 2 | 3 | import ( 4 | "github.com/qjw/go-wx-sdk/utils" 5 | "log" 6 | "encoding/xml" 7 | "fmt" 8 | "net/http" 9 | "runtime/debug" 10 | "strconv" 11 | ) 12 | 13 | type MessageHandle func(*MixMessage) utils.Reply 14 | 15 | type Server struct { 16 | Request *http.Request 17 | Responce http.ResponseWriter 18 | 19 | CorpContext *Context 20 | AgentConfig *AgentConfig 21 | 22 | // 收到消息的回调 23 | MessageHandler MessageHandle 24 | } 25 | 26 | type ServerRequest struct { 27 | MixedMsg *MixMessage 28 | RequestHttpBody *utils.RequestEncryptedXMLMsg 29 | 30 | Random []byte 31 | Nonce string 32 | Timestamp int64 33 | 34 | // 回复的消息 35 | ResponseRawXMLMsg []byte 36 | ResponseMsg utils.Reply 37 | } 38 | 39 | //NewServer init 40 | func NewServer(request *http.Request, responce http.ResponseWriter, 41 | handle MessageHandle, mpwcontext *Context, agentConfig *AgentConfig) *Server { 42 | return &Server{ 43 | Request: request, 44 | Responce: responce, 45 | CorpContext: mpwcontext, 46 | AgentConfig: agentConfig, 47 | MessageHandler: handle, 48 | } 49 | } 50 | 51 | //Serve 处理微信的请求消息 52 | func (srv *Server) Ping() { 53 | echostr := srv.Request.URL.Query().Get("echostr") 54 | if echostr == "" { 55 | log.Print("invalid echostr") 56 | http.Error(srv.Responce, "", http.StatusForbidden) 57 | return 58 | } 59 | 60 | if !srv.validate(nil, echostr) { 61 | http.Error(srv.Responce, "", http.StatusForbidden) 62 | return 63 | } 64 | 65 | _, echostrRes, _, err := utils.DecryptMsg(srv.CorpContext.Config.CorpID, echostr, srv.AgentConfig.EncodingAESKey) 66 | if err != nil { 67 | log.Print("invalid DecryptMsg") 68 | http.Error(srv.Responce, "", http.StatusForbidden) 69 | } 70 | 71 | http.Error(srv.Responce, string(echostrRes), http.StatusOK) 72 | } 73 | 74 | //Serve 处理微信的请求消息 75 | func (srv *Server) Serve() error { 76 | var svrReq ServerRequest 77 | 78 | // 解析 RequestHttpBody 79 | var requestHttpBody utils.RequestEncryptedXMLMsg 80 | if err := xml.NewDecoder(srv.Request.Body).Decode(&requestHttpBody); err != nil { 81 | log.Print(err.Error()) 82 | return err 83 | } 84 | 85 | if !srv.validate(&svrReq, requestHttpBody.EncryptedMsg) { 86 | return fmt.Errorf("请求校验失败") 87 | } 88 | 89 | svrReq.RequestHttpBody = &requestHttpBody 90 | err := srv.handleRequest(&svrReq) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | if err = srv.buildResponse(&svrReq);err != nil{ 96 | return err 97 | } 98 | if err = srv.send(&svrReq);err != nil{ 99 | return err 100 | } 101 | return nil 102 | } 103 | 104 | func (srv *Server) validate(svrReq *ServerRequest, content string) bool { 105 | signature := srv.Request.URL.Query().Get("msg_signature") 106 | if signature == "" { 107 | log.Print("invalid msg_signature") 108 | return false 109 | } 110 | timestamp := srv.Request.URL.Query().Get("timestamp") 111 | if timestamp == "" { 112 | log.Print("invalid timestamp") 113 | return false 114 | } 115 | 116 | timestampInt, err := strconv.ParseInt(timestamp, 10, 64) 117 | if err != nil { 118 | log.Print(err.Error()) 119 | return false 120 | } 121 | 122 | nonce := srv.Request.URL.Query().Get("nonce") 123 | if nonce == "" { 124 | log.Print("invalid nonce") 125 | return false 126 | } 127 | 128 | // 验证签名 129 | msgSignature2 := utils.Signature(srv.AgentConfig.Token, timestamp, nonce, content) 130 | if signature != msgSignature2 { 131 | log.Print("invalid signature") 132 | return false 133 | } 134 | 135 | if svrReq != nil { 136 | svrReq.Timestamp = timestampInt 137 | svrReq.Nonce = nonce 138 | } 139 | return true 140 | } 141 | 142 | //HandleRequest 处理微信的请求 143 | func (srv *Server) handleRequest(svrReq *ServerRequest) (err error) { 144 | err = srv.getMessage(svrReq) 145 | if err != nil { 146 | return 147 | } 148 | 149 | if srv.MessageHandler != nil { 150 | svrReq.ResponseMsg = srv.MessageHandler(svrReq.MixedMsg) 151 | } 152 | return 153 | } 154 | 155 | //getMessage 解析微信返回的消息 156 | func (srv *Server) getMessage(svrReq *ServerRequest) error { 157 | // 解密 158 | random, rawMsgXML, appID, err := utils.DecryptMsg( 159 | srv.CorpContext.Config.CorpID, 160 | svrReq.RequestHttpBody.EncryptedMsg, 161 | srv.AgentConfig.EncodingAESKey) 162 | if err != nil { 163 | log.Print("invalid DecryptMsg") 164 | return err 165 | } 166 | svrReq.Random = random 167 | 168 | if svrReq.RequestHttpBody.ToUserName != appID { 169 | err := fmt.Errorf("the RequestHttpBody's ToUserName(==%s) mismatch the appID with aes encrypt(==%s)", 170 | svrReq.RequestHttpBody.ToUserName, appID) 171 | return err 172 | } 173 | 174 | // 解密成功, 解析 MixedMessage 175 | var mixedMsg MixMessage 176 | if err = xml.Unmarshal(rawMsgXML, &mixedMsg); err != nil { 177 | log.Print(err.Error()) 178 | return err 179 | } 180 | svrReq.MixedMsg = &mixedMsg 181 | 182 | // 安全考虑再次验证 183 | if svrReq.RequestHttpBody.ToUserName != mixedMsg.ToUserName { 184 | err := fmt.Errorf("the RequestHttpBody's ToUserName(==%s) mismatch the MixedMessage's SuiteId", 185 | svrReq.RequestHttpBody.ToUserName) 186 | return err 187 | } 188 | 189 | return nil 190 | } 191 | 192 | func (srv *Server) buildResponse(svrReq *ServerRequest) (err error) { 193 | reply := svrReq.ResponseMsg 194 | if reply == nil { 195 | return 196 | } 197 | 198 | defer func() { 199 | if e := recover(); e != nil { 200 | err = fmt.Errorf("panic error: %v\n%s", e, debug.Stack()) 201 | } 202 | }() 203 | msgType := reply.GetMsgType() 204 | switch msgType { 205 | case utils.MsgTypeText: 206 | case utils.MsgTypeImage: 207 | case utils.MsgTypeVoice: 208 | case utils.MsgTypeVideo: 209 | case utils.MsgTypeMusic: 210 | case utils.MsgTypeNews: 211 | case utils.MsgTypeTransfer: 212 | default: 213 | err = utils.ErrUnsupportReply 214 | return 215 | } 216 | 217 | reply.SetToUserName(svrReq.MixedMsg.FromUserName) 218 | reply.SetFromUserName(svrReq.MixedMsg.ToUserName) 219 | reply.SetCreateTime(utils.GetCurrTs()) 220 | svrReq.ResponseRawXMLMsg, err = xml.Marshal(svrReq.ResponseMsg) 221 | return 222 | } 223 | 224 | //Send 将自定义的消息发送 225 | func (srv *Server) send(svrReq *ServerRequest) (err error) { 226 | if svrReq.ResponseMsg == nil || svrReq.ResponseRawXMLMsg == nil { 227 | return 228 | } 229 | //安全模式下对消息进行加密 230 | var encryptedMsg []byte 231 | encryptedMsg, err = utils.EncryptMsg(svrReq.Random, svrReq.ResponseRawXMLMsg, 232 | srv.CorpContext.Config.CorpID, 233 | srv.AgentConfig.EncodingAESKey) 234 | if err != nil { 235 | return 236 | } 237 | // 如果获取不到timestamp nonce 则自己生成 238 | timestamp := svrReq.Timestamp 239 | timestampStr := strconv.FormatInt(timestamp, 10) 240 | msgSignature := utils.Signature(srv.AgentConfig.Token, timestampStr, svrReq.Nonce, string(encryptedMsg)) 241 | replyMsg := utils.ResponseEncryptedXMLMsg{ 242 | EncryptedMsg: string(encryptedMsg), 243 | MsgSignature: msgSignature, 244 | Timestamp: timestamp, 245 | Nonce: svrReq.Nonce, 246 | } 247 | 248 | data, _ := xml.MarshalIndent(replyMsg, "", "\t") 249 | 250 | srv.Responce.Header().Set("Content-Type", "application/xml; charset=utf-8") 251 | srv.Responce.WriteHeader(http.StatusOK) 252 | srv.Responce.Write(data) 253 | return 254 | } 255 | -------------------------------------------------------------------------------- /dep.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | error() 4 | { 5 | echo "$@" 6 | return 1 7 | } 8 | 9 | [ -n "${GOPATH}" -a "${GOPATH}" != "" ] || error "GOPATH not exist" || exit 1 10 | 11 | export PATH=$PATH:$GOPATH/bin 12 | 13 | ensure_bin(){ 14 | which "${1}" 2>/dev/null 15 | if [ $? -ne 0 ];then 16 | echo "install "${1}" tool" 17 | go get -u "${2}" || return 1 18 | else 19 | echo "${1} tool installed yet" 20 | fi 21 | return 0 22 | } 23 | 24 | ensure_bin govendor "github.com/kardianos/govendor" || exit 1 25 | 26 | if ! [ -f vendor/vendor.json ];then 27 | echo "init vendor repertory" 28 | govendor init 29 | else 30 | echo "vendor init yet" 31 | fi 32 | 33 | govendor sync -------------------------------------------------------------------------------- /mch/common.go: -------------------------------------------------------------------------------- 1 | package mch 2 | 3 | import ( 4 | "github.com/qjw/go-wx-sdk/utils" 5 | ) 6 | 7 | type OrderCommonError struct { 8 | ReturnCode string `xml:"return_code,omitempty" json:"return_code,omitempty"` 9 | ReturnMsg string `xml:"return_msg,omitempty" json:"return_msg,omitempty"` 10 | } 11 | 12 | type Config struct { 13 | AppID string `json:"appid" doc:"微信支付分配的公众账号ID(企业号corpid即为此appId)"` 14 | MchID string `json:"mch_id" doc:"微信支付分配的商户号"` 15 | ApiKey string `json:"apiKey" doc:"微信支付key"` 16 | } 17 | 18 | // Context struct 19 | type Context struct { 20 | // 配置 21 | Config *Config 22 | } 23 | 24 | func NewContext(config *Config) *Context { 25 | context := &Context{ 26 | Config: config, 27 | } 28 | return context 29 | } 30 | 31 | type PayApi struct { 32 | utils.ApiBaseXml 33 | Context *Context 34 | } 35 | 36 | func NewPayApi(context *Context) *PayApi { 37 | api := &PayApi{ 38 | Context: context, 39 | } 40 | return api 41 | } -------------------------------------------------------------------------------- /mch/pay.go: -------------------------------------------------------------------------------- 1 | package mch 2 | 3 | import ( 4 | "github.com/qjw/go-wx-sdk/utils" 5 | "fmt" 6 | ) 7 | 8 | const ( 9 | payUnifiedorder = "https://api.mch.weixin.qq.com/pay/unifiedorder" 10 | closeUnifiedorder = "https://api.mch.weixin.qq.com/pay/closeorder" 11 | ) 12 | 13 | type Unifiedorder struct { 14 | XMLName struct{} `xml:"xml" json:"-" sign:"-"` 15 | AppID string `xml:"appid" json:"appid" doc:"微信支付分配的公众账号ID(企业号corpid即为此appId)"` 16 | MchID string `xml:"mch_id" json:"mch_id" doc:"微信支付分配的商户号"` 17 | DeviceInfo utils.CharData `xml:"device_info,omitempty" json:"device_info,omitempty" doc:"自定义参数,可以为终端设备号(门店号或收银设备ID),PC网页或公众号内支付可以传\"WEB\""` 18 | Nonce string `xml:"nonce_str" json:"nonce_str" doc:"随机字符串,长度要求在32位以内"` 19 | Sign string `xml:"sign" json:"sign" doc:"通过签名算法计算得出的签名值"` 20 | SignType string `xml:"sign_type,omitempty" json:"sign_type,omitempty" doc:"签名类型,默认为MD5,支持HMAC-SHA256和MD5。"` 21 | FeeType string `xml:"fee_type,omitempty" json:"fee_type,omitempty" doc:"标价币种,符合ISO 4217标准的三位字母代码,默认人民币:CNY"` 22 | TradeType string `xml:"trade_type" json:"trade_type" doc:"JSAPI 取值如下:JSAPI,NATIVE,APP等,说明详见参数规定"` 23 | 24 | UnifiedordeObj 25 | } 26 | 27 | type UnifiedordeObj struct { 28 | Body utils.CharData `xml:"body" json:"body" doc:"商品简单描述,例如腾讯充值中心-QQ会员充值"` 29 | Detail utils.CharData `xml:"detail,omitempty" json:"detail,omitempty" doc:"商品详情"` 30 | Attach utils.CharData `xml:"attach,omitempty" json:"attach,omitempty" doc:"附加数据,例如深圳分店。附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用。"` 31 | OutTradeNo string `xml:"out_trade_no" json:"out_trade_no" doc:"商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*@ ,且在同一个商户号下唯一。详见商户订单号"` 32 | TotalFee int64 `xml:"total_fee" json:"total_fee" doc:"订单总金额,单位为分"` 33 | SpbillCreateIp string `xml:"spbill_create_ip,omitempty" json:"spbill_create_ip,omitempty" doc:"终端IP,APP和网页支付提交用户端ip,Native支付填调用微信支付API的机器IP。"` 34 | TimeStart string `xml:"time_start,omitempty" json:"time_start,omitempty" doc:"订单生成时间,格式为yyyyMMddHHmmss"` 35 | TimeExpire string `xml:"time_expire,omitempty" json:"time_expire,omitempty" doc:"交易结束时间,格式为yyyyMMddHHmmss"` 36 | GoodsTag string `xml:"goods_tag,omitempty" json:"goods_tag,omitempty" doc:"订单优惠标记,使用代金券或立减优惠功能时需要的参数,说明详见代金券或立减优惠"` 37 | NotifyUrl utils.CharData `xml:"notify_url,omitempty" json:"notify_url,omitempty" doc:"异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数"` 38 | ProductID string `xml:"product_id,omitempty" json:"product_id,omitempty" doc:"trade_type=NATIVE时(即扫码支付),此参数必传。此参数为二维码中包含的商品ID,商户自行定义。"` 39 | LimitPay string `xml:"limit_pay,omitempty" json:"limit_pay,omitempty" doc:"上传此参数no_credit--可限制用户不能使用信用卡支付"` 40 | OpenID string `xml:"openid,omitempty" json:"openid,omitempty" doc:"trade_type=JSAPI时(即公众号支付),此参数必传,此参数为微信用户在商户对应appid下的唯一标识"` 41 | } 42 | 43 | type UnifiedordeRes struct { 44 | OrderCommonError 45 | AppID string `xml:"appid" json:"appid" doc:"调用接口提交的公众账号ID"` 46 | MchID string `xml:"mch_id" json:"mch_id" doc:"调用接口提交的商户号"` 47 | DeviceInfo string `xml:"device_info,omitempty" json:"device_info,omitempty" doc:"自定义参数,可以为请求支付的终端设备号等"` 48 | Nonce string `xml:"nonce_str" json:"nonce_str" doc:"微信返回的随机字符串"` 49 | Sign string `xml:"sign" json:"sign" doc:"微信返回的签名值"` 50 | ResultCode string `xml:"result_code,omitempty" json:"result_code,omitempty" doc:"业务结果"` 51 | ErrCode string `xml:"err_code" json:"err_code"` 52 | ErrCodeDes string `xml:"err_code_des" json:"err_code_des" doc:"错误信息描述"` 53 | TradeType string `xml:"trade_type" json:"trade_type" doc:"交易类型,取值为:JSAPI,NATIVE,APP等"` 54 | PrepayID string `xml:"prepay_id" json:"prepay_id" doc:"微信生成的预支付会话标识,用于后续接口调用中使用,该值有效期为2小时"` 55 | CodeUrl string `xml:"code_url" json:"code_url" doc:"trade_type为NATIVE时有返回,用于生成二维码,展示给用户进行扫码支付"` 56 | } 57 | 58 | func (this PayApi) Sign(variable interface{}) (sign string, err error) { 59 | ss := &utils.SignStruct{ 60 | ToLower: false, 61 | Tag: "xml", 62 | } 63 | sign, err = ss.Sign(variable, nil, this.Context.Config.ApiKey) 64 | return 65 | } 66 | 67 | func (this PayApi) CreateUnifiedOrder(param *UnifiedordeObj) (*UnifiedordeRes, error) { 68 | var realParam Unifiedorder 69 | realParam.AppID = this.Context.Config.AppID 70 | realParam.MchID = this.Context.Config.MchID 71 | 72 | realParam.DeviceInfo = "WEB" 73 | realParam.FeeType = "CNY" 74 | realParam.TradeType = "JSAPI" 75 | realParam.Nonce = utils.RandString(30) 76 | realParam.SignType = "MD5" 77 | 78 | realParam.UnifiedordeObj = *param 79 | 80 | sign, err := this.Sign(&realParam) 81 | if err != nil { 82 | return nil, err 83 | } 84 | realParam.Sign = sign 85 | 86 | var res UnifiedordeRes 87 | if err := this.DoPostObject(payUnifiedorder, &realParam, &res); err == nil { 88 | return &res, nil 89 | } else { 90 | return nil, err 91 | } 92 | } 93 | 94 | type OrderCloseObj struct { 95 | OrderCloseParam 96 | 97 | XMLName struct{} `xml:"xml" json:"-" sign:"-"` 98 | AppID string `xml:"appid" json:"appid" doc:"微信支付分配的公众账号ID(企业号corpid即为此appId)"` 99 | MchID string `xml:"mch_id" json:"mch_id" doc:"微信支付分配的商户号"` 100 | Sign *string `xml:"sign" json:"sign" doc:"通过签名算法计算得出的签名值"` 101 | SignType *string `xml:"sign_type,omitempty" json:"sign_type,omitempty" doc:"签名类型,默认为MD5,支持HMAC-SHA256和MD5。"` 102 | Nonce string `xml:"nonce_str" json:"nonce_str" doc:"随机字符串,长度要求在32位以内"` 103 | } 104 | 105 | type OrderCloseParam struct { 106 | OutTradeNo string `xml:"out_trade_no" json:"out_trade_no" doc:"商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*@ ,且在同一个商户号下唯一。详见商户订单号"` 107 | } 108 | 109 | type UnifiedordeCloseRes struct { 110 | OrderCommonError 111 | AppID string `xml:"appid" json:"appid" doc:"调用接口提交的公众账号ID"` 112 | MchID string `xml:"mch_id" json:"mch_id" doc:"调用接口提交的商户号"` 113 | Nonce string `xml:"nonce_str" json:"nonce_str" doc:"微信返回的随机字符串"` 114 | Sign string `xml:"sign" json:"sign" doc:"微信返回的签名值"` 115 | ResultCode string `xml:"result_code,omitempty" json:"result_code,omitempty" doc:"业务结果"` 116 | ResultMsg string `xml:"result_msg,omitempty" json:"result_msg,omitempty" doc:"业务结果"` 117 | ErrCode string `xml:"err_code" json:"err_code"` 118 | ErrCodeDes string `xml:"err_code_des" json:"err_code_des" doc:"错误信息描述"` 119 | } 120 | 121 | func (this PayApi) CloseUnifiedOrder(param *OrderCloseParam) (*UnifiedordeCloseRes, error) { 122 | var realParam OrderCloseObj 123 | realParam.AppID = this.Context.Config.AppID 124 | realParam.MchID = this.Context.Config.MchID 125 | realParam.Nonce = utils.RandString(30) 126 | realParam.SignType = utils.String("MD5") 127 | realParam.OrderCloseParam = *param 128 | 129 | sign, err := this.Sign(&realParam) 130 | if err != nil { 131 | return nil, err 132 | } 133 | realParam.Sign = &sign 134 | 135 | var res UnifiedordeCloseRes 136 | if err := this.DoPostObject(closeUnifiedorder, &realParam, &res); err == nil { 137 | return &res, nil 138 | } else { 139 | return nil, err 140 | } 141 | } 142 | 143 | // xml的字段名不要修改 144 | type JsUnifiedOrder struct { 145 | AppID string `xml:"appId" json:"appid" doc:"调用接口提交的公众账号ID"` 146 | Nonce string `xml:"nonceStr" json:"nonce_str" doc:"随机字符串,长度要求在32位以内"` 147 | Sign *string `xml:"paySign" json:"sign,omitempty" doc:"通过签名算法计算得出的签名值"` 148 | SignType string `xml:"signType" json:"sign_type" doc:"签名类型,默认为MD5,支持HMAC-SHA256和MD5。"` 149 | Package string `xml:"package" json:"package"` 150 | TimeStamp int64 `xml:"timeStamp" json:"timestamp"` 151 | } 152 | 153 | type JsUnifiedOrderRes struct { 154 | OrderCommonError 155 | JsUnifiedOrder 156 | } 157 | 158 | func (this PayApi) CreateJsUnifiedOrder(param *UnifiedordeRes) (*JsUnifiedOrderRes, error) { 159 | var realRes JsUnifiedOrderRes 160 | realRes.AppID = this.Context.Config.AppID 161 | realRes.Nonce = utils.RandString(30) 162 | realRes.SignType = "MD5" 163 | realRes.TimeStamp = utils.GetCurrTs() 164 | realRes.Package = fmt.Sprintf("prepay_id=%s", param.PrepayID) 165 | 166 | sign, err := this.Sign(&realRes.JsUnifiedOrder) 167 | if err != nil { 168 | return nil, err 169 | } 170 | realRes.Sign = &sign 171 | return &realRes, nil 172 | } 173 | 174 | type UnifiedOrderNotifyObj struct { 175 | OrderCommonError 176 | AppID string `xml:"appid" json:"appid" doc:"微信支付分配的公众账号ID(企业号corpid即为此appId)"` 177 | MchID string `xml:"mch_id" json:"mch_id" doc:"微信支付分配的商户号"` 178 | DeviceInfo string `xml:"device_info,omitempty" json:"device_info,omitempty" doc:"自定义参数,可以为终端设备号(门店号或收银设备ID),PC网页或公众号内支付可以传\"WEB\""` 179 | Nonce string `xml:"nonce_str" json:"nonce_str" doc:"随机字符串,长度要求在32位以内"` 180 | Sign string `xml:"sign" json:"sign" doc:"通过签名算法计算得出的签名值"` 181 | SignType string `xml:"sign_type,omitempty" json:"sign_type,omitempty" doc:"签名类型,默认为MD5,支持HMAC-SHA256和MD5。"` 182 | ResultCode string `xml:"result_code,omitempty" json:"result_code,omitempty" doc:"业务结果"` 183 | ErrCode string `xml:"err_code" json:"err_code"` 184 | ErrCodeDes string `xml:"err_code_des" json:"err_code_des" doc:"错误信息描述"` 185 | OpenID string `xml:"openid" json:"openid" doc:"用户在商户appid下的唯一标识"` 186 | IsSubscribe string `xml:"is_subscribe" json:"is_subscribe" doc:"是否关注公众账号(Y/N)"` 187 | TradeType string `xml:"trade_type" json:"trade_type" doc:"JSAPI 取值如下:JSAPI,NATIVE,APP等,说明详见参数规定"` 188 | BankType string `xml:"bank_type" json:"bank_type" doc:"银行类型,采用字符串类型的银行标识"` 189 | TotalFee int64 `xml:"total_fee" json:"total_fee" doc:"订单总金额,单位为分"` 190 | SettlementTotalFee int64 `xml:"settlement_total_fee" json:"settlement_total_fee" doc:"应结订单金额=订单金额-非充值代金券金额,应结订单金额<=订单金额。"` 191 | CashFee int64 `xml:"cash_fee" json:"cash_fee" doc:"现金支付金额订单现金支付金额"` 192 | CashFeeType string `xml:"cash_fee_type" json:"cash_fee_type" doc:"现金支付货币类型 ,符合ISO4217标准的三位字母代码,默认人民币:CNY"` 193 | CouponFee int64 `xml:"coupon_fee" json:"coupon_fee" doc:"总代金券金额,代金券金额<=订单金额,订单金额-代金券金额=现金支付金额"` 194 | CouponCount int64 `xml:"coupon_count" json:"coupon_count" doc:"代金券使用数量"` 195 | TransactionID string `xml:"transaction_id" json:"transaction_id" doc:"微信支付订单号 "` 196 | TimeEnd string `xml:"time_end,omitempty" json:"time_end,omitempty" doc:"支付完成时间,格式为yyyyMMddHHmmss"` 197 | Attach string `xml:"attach,omitempty" json:"attach,omitempty" doc:"附加数据,例如深圳分店。附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用。"` 198 | OutTradeNo string `xml:"out_trade_no" json:"out_trade_no" doc:"商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*@ ,且在同一个商户号下唯一。详见商户订单号"` 199 | FeeType string `xml:"fee_type,omitempty" json:"fee_type,omitempty" doc:"标价币种,符合ISO 4217标准的三位字母代码,默认人民币:CNY"` 200 | } 201 | -------------------------------------------------------------------------------- /mp/api.go: -------------------------------------------------------------------------------- 1 | package mp 2 | 3 | import ( 4 | "github.com/qjw/go-wx-sdk/utils" 5 | "bytes" 6 | "encoding/json" 7 | "fmt" 8 | "html/template" 9 | "strconv" 10 | ) 11 | 12 | //-----------------------------------客服消息----------------------------------------------------------------------------- 13 | 14 | const ( 15 | sendKfMsg = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=%s" 16 | getkflist = "https://api.weixin.qq.com/cgi-bin/customservice/getkflist?access_token=%s" 17 | addkfaccount = "https://api.weixin.qq.com/customservice/kfaccount/add?access_token=%s" 18 | delkfaccount = "https://api.weixin.qq.com/customservice/kfaccount/del?access_token=%s&kf_account=%s" 19 | updatekfaccount = "https://api.weixin.qq.com/customservice/kfaccount/update?access_token=%s" 20 | ) 21 | const ( 22 | addKfAccountTemp = `{ 23 | "kf_account" : "%s", 24 | "nickname" : "%s", 25 | "password" : "%s", 26 | }` 27 | sendKfTextMsgTemp = `{ 28 | "touser":"%s", 29 | "msgtype":"text", 30 | "text": 31 | { 32 | "content":"%s" 33 | } 34 | }` 35 | sendKfImageMsgTemp = `{ 36 | "touser":"%s", 37 | "msgtype":"image", 38 | "image": 39 | { 40 | "media_id":"%s" 41 | } 42 | }` 43 | sendKfVideoMsgTemp = `{ 44 | "touser":"%s", 45 | "msgtype":"video", 46 | "video": 47 | { 48 | "media_id":"%s", 49 | "thumb_media_id":"%s", 50 | "title":"%s", 51 | "description":"%s" 52 | } 53 | }` 54 | sendKfMpnewsMsgTemp = `{ 55 | "touser":"%s", 56 | "msgtype":"mpnews", 57 | "mpnews": 58 | { 59 | "media_id":"%s" 60 | } 61 | }` 62 | sendKfArticleMsgTemp = `{ 63 | "touser":"{{.ToUser}}", 64 | "msgtype":"news", 65 | "news":{ 66 | "articles": [ 67 | {{range $index, $element := .Articles}} 68 | {{if $index}},{{end}} 69 | { 70 | "title": "{{$element.Title}}", 71 | "description": "{{$element.Description}}", 72 | "url": "{{$element.Url}}", 73 | "picurl": "{{$element.Picurl}}" 74 | } 75 | {{end}} 76 | ] 77 | } 78 | }` 79 | sendKfCardMsgTemp = `{ 80 | "touser":"%s", 81 | "msgtype":"wxcard", 82 | "wxcard": 83 | { 84 | "card_id":"%s" 85 | } 86 | }` 87 | ) 88 | 89 | func (this WechatApi) UpdateKfAccount(kf_account string, nickname string, password string) (*utils.CommonError, error) { 90 | var res utils.CommonError 91 | body := fmt.Sprintf(addKfAccountTemp, kf_account, nickname, password) 92 | if err := this.DoPost(updatekfaccount, body, &res); err == nil { 93 | return &res, nil 94 | } else { 95 | return nil, err 96 | } 97 | } 98 | 99 | func (this WechatApi) DelKfAccount(kf_account string) (*utils.CommonError, error) { 100 | var res utils.CommonError 101 | if err := this.DoGet(delkfaccount, &res, kf_account); err == nil { 102 | return &res, nil 103 | } else { 104 | return nil, err 105 | } 106 | } 107 | 108 | func (this WechatApi) AddKfAccount(kf_account string, nickname string, password string) (*utils.CommonError, error) { 109 | var res utils.CommonError 110 | body := fmt.Sprintf(addKfAccountTemp, kf_account, nickname, password) 111 | if err := this.DoPost(addkfaccount, body, &res); err == nil { 112 | return &res, nil 113 | } else { 114 | return nil, err 115 | } 116 | } 117 | 118 | type KfList struct { 119 | KfList []struct { 120 | KfAccount string `json:"kf_account"` 121 | KfNick string `json:"kf_nick"` 122 | KfID int `json:"kf_id"` 123 | KfHeadimgurl string `json:"kf_headimgurl"` 124 | } `json:"kf_list"` 125 | } 126 | 127 | func (this WechatApi) GetKfList() (*KfList, error) { 128 | var res KfList 129 | if err := this.DoGet(getkflist, &res); err == nil { 130 | return &res, nil 131 | } else { 132 | return nil, err 133 | } 134 | } 135 | 136 | type Article struct { 137 | Title string `json:"title"` 138 | Description string `json:"description"` 139 | Url string `json:"url,omitempty"` 140 | Picurl string `json:"picurl,omitempty"` 141 | } 142 | 143 | func (this WechatApi) SendKfArticleMessage(touser string, articles []Article) (*utils.CommonError, error) { 144 | var res utils.CommonError 145 | ttt := template.New("SendKfArticleMessage") 146 | ttt.Parse(sendKfArticleMsgTemp) 147 | var buf bytes.Buffer 148 | ttt.Execute(&buf, struct { 149 | ToUser string 150 | Articles []Article 151 | }{ 152 | ToUser: touser, 153 | Articles: articles, 154 | }) 155 | if err := this.DoPost(sendKfMsg, buf.String(), &res); err == nil { 156 | return &res, nil 157 | } else { 158 | return nil, err 159 | } 160 | } 161 | 162 | func (this WechatApi) SendKfVideoMessage(touser string, 163 | media_id string, 164 | thumb_media_id string, 165 | title string, 166 | description string) (*utils.CommonError, error) { 167 | var res utils.CommonError 168 | body := fmt.Sprintf(sendKfVideoMsgTemp, touser, media_id, thumb_media_id, title, description) 169 | if err := this.DoPost(sendKfMsg, body, &res); err == nil { 170 | return &res, nil 171 | } else { 172 | return nil, err 173 | } 174 | } 175 | 176 | func (this WechatApi) SendKfMessage(touser string, content string) (*utils.CommonError, error) { 177 | var res utils.CommonError 178 | body := fmt.Sprintf(sendKfTextMsgTemp, touser, content) 179 | if err := this.DoPost(sendKfMsg, body, &res); err == nil { 180 | return &res, nil 181 | } else { 182 | return nil, err 183 | } 184 | } 185 | 186 | func (this WechatApi) SendKfImageMessage(touser string, media_id string) (*utils.CommonError, error) { 187 | var res utils.CommonError 188 | body := fmt.Sprintf(sendKfImageMsgTemp, touser, media_id) 189 | if err := this.DoPost(sendKfMsg, body, &res); err == nil { 190 | return &res, nil 191 | } else { 192 | return nil, err 193 | } 194 | } 195 | 196 | func (this WechatApi) SendKfMpnewsMessage(touser string, media_id string) (*utils.CommonError, error) { 197 | var res utils.CommonError 198 | body := fmt.Sprintf(sendKfMpnewsMsgTemp, touser, media_id) 199 | if err := this.DoPost(sendKfMsg, body, &res); err == nil { 200 | return &res, nil 201 | } else { 202 | return nil, err 203 | } 204 | } 205 | 206 | func (this WechatApi) SendKfCardMessage(touser string, card_id string) (*utils.CommonError, error) { 207 | var res utils.CommonError 208 | body := fmt.Sprintf(sendKfCardMsgTemp, touser, card_id) 209 | if err := this.DoPost(sendKfMsg, body, &res); err == nil { 210 | return &res, nil 211 | } else { 212 | return nil, err 213 | } 214 | } 215 | 216 | //-----------------------------------用户和分组--------------------------------------------------------------------------- 217 | 218 | const ( 219 | userGet = "https://api.weixin.qq.com/cgi-bin/user/get?access_token=%s&next_openid=%s" 220 | userDetailGet = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=%s&openid=%s&lang=zh_CN" 221 | tagListGet = "https://api.weixin.qq.com/cgi-bin/tags/get?access_token=%s" 222 | tagUpdate = "https://api.weixin.qq.com/cgi-bin/tags/update?access_token=%s" 223 | tagCreate = "https://api.weixin.qq.com/cgi-bin/tags/create?access_token=%s" 224 | tagDelete = "https://api.weixin.qq.com/cgi-bin/tags/delete?access_token=%s" 225 | tagUsers = "https://api.weixin.qq.com/cgi-bin/user/tag/get?access_token=%s" 226 | tagUserBatchAdd = "https://api.weixin.qq.com/cgi-bin/tags/members/batchtagging?access_token=%s" 227 | tagUserBatchRm = "https://api.weixin.qq.com/cgi-bin/tags/members/batchuntagging?access_token=%s" 228 | userTags = "https://api.weixin.qq.com/cgi-bin/tags/getidlist?access_token=%s" 229 | userRemark = "https://api.weixin.qq.com/cgi-bin/user/info/updateremark?access_token=%s" 230 | userBatchDetailGet = "https://api.weixin.qq.com/cgi-bin/user/info/batchget?access_token=%s" 231 | ) 232 | 233 | const ( 234 | tagUPdateTemp = `{ 235 | "tag" : { 236 | "id" : %d, 237 | "name" : "%s" 238 | } 239 | }` 240 | tagCreateTemp = `{ 241 | "tag" : { 242 | "name" : "%s" 243 | } 244 | }` 245 | tagDeleteTemp = `{ 246 | "tag":{ 247 | "id" : %d 248 | } 249 | }` 250 | tagUsersTemp = `{ 251 | "tagid" : %d, 252 | "next_openid":"%s" 253 | }` 254 | tagUserBatchAddTemp = `{ 255 | "openid_list": [ 256 | {{range $index, $element := .OpenIDList}} 257 | {{if $index}},{{end}} 258 | "{{$element}}" 259 | {{end}} 260 | ], 261 | "tagid": {{.TagID}} 262 | }` 263 | userTagsTemp = `{ 264 | "openid" : "%s" 265 | }` 266 | userRemarkTemp = `{ 267 | "openid":"%s", 268 | "remark":"%s" 269 | }` 270 | userBatchDetailGetTemp = `{ 271 | "user_list": [ 272 | {{range $index, $element := .OpenIDList}} 273 | {{if $index}},{{end}} 274 | { 275 | "openid": "{{$element}}", 276 | "lang": "zh_CN" 277 | } 278 | {{end}} 279 | ] 280 | }` 281 | ) 282 | 283 | type Users struct { 284 | Total int `json:"total"` 285 | Count int `json:"count"` 286 | Data struct { 287 | OpenID []string `json:"openid"` 288 | } `json:"data"` 289 | NextOpenID string `json:"next_openid"` 290 | } 291 | 292 | func (this WechatApi) GetUser(next_openid string) (*Users, error) { 293 | var res Users 294 | if err := this.DoGet(userGet, &res, next_openid); err == nil { 295 | return &res, nil 296 | } else { 297 | return nil, err 298 | } 299 | } 300 | 301 | type UserInfo struct { 302 | Subscribe int `json:"subscribe"` 303 | Openid string `json:"openid"` 304 | Nickname string `json:"nickname"` 305 | Sex int `json:"sex"` 306 | Language string `json:"language"` 307 | City string `json:"city"` 308 | Province string `json:"province"` 309 | Country string `json:"country"` 310 | Headimgurl string `json:"headimgurl"` 311 | Subscribe_time int64 `json:"subscribe_time"` 312 | Unionid string `json:"unionid"` 313 | Remark string `json:"remark"` 314 | Groupid int `json:"groupid"` 315 | Tagid_list []int `json:"tagid_list"` 316 | } 317 | 318 | type UserInfoList struct { 319 | UserInfoList []*UserInfo `json:"user_info_list"` 320 | } 321 | 322 | func (this WechatApi) GetUserDetail(openid string) (*UserInfo, error) { 323 | var res UserInfo 324 | if err := this.DoGet(userDetailGet, &res, openid); err == nil { 325 | return &res, nil 326 | } else { 327 | return nil, err 328 | } 329 | } 330 | 331 | func (this WechatApi) BatchGetUserDetail(openid_list []string) (*UserInfoList, error) { 332 | var res UserInfoList 333 | 334 | ttt := template.New("BatchGetUserDetail") 335 | ttt.Parse(userBatchDetailGetTemp) 336 | var buf bytes.Buffer 337 | ttt.Execute(&buf, struct { 338 | OpenIDList []string 339 | }{ 340 | OpenIDList: openid_list, 341 | }) 342 | 343 | if err := this.DoPost(userBatchDetailGet, buf.String(), &res); err == nil { 344 | return &res, nil 345 | } else { 346 | return nil, err 347 | } 348 | } 349 | 350 | type UserTag struct { 351 | ID int `json:"id"` 352 | Name string `json:"name"` 353 | Count int `json:"count"` 354 | } 355 | 356 | type TagList struct { 357 | Tags []UserTag `json:"tags"` 358 | } 359 | 360 | func (this WechatApi) GetTagList() (*TagList, error) { 361 | var res TagList 362 | if err := this.DoGet(tagListGet, &res); err == nil { 363 | return &res, nil 364 | } else { 365 | return nil, err 366 | } 367 | } 368 | 369 | func (this WechatApi) UpdateTag(id int, name string) (*utils.CommonError, error) { 370 | var res utils.CommonError 371 | body := fmt.Sprintf(tagUPdateTemp, id, name) 372 | if err := this.DoPost(tagUpdate, body, &res); err == nil { 373 | return &res, nil 374 | } else { 375 | return nil, err 376 | } 377 | } 378 | 379 | type TagCreate struct { 380 | Tag struct { 381 | ID int `json:"id"` 382 | Name string `json:"name"` 383 | } `json:"tag"` 384 | } 385 | 386 | func (this WechatApi) CreateTag(name string) (*TagCreate, error) { 387 | var res TagCreate 388 | body := fmt.Sprintf(tagCreateTemp, name) 389 | if err := this.DoPost(tagCreate, body, &res); err == nil { 390 | return &res, nil 391 | } else { 392 | return nil, err 393 | } 394 | } 395 | 396 | func (this WechatApi) DeleteTag(id int) (*utils.CommonError, error) { 397 | var res utils.CommonError 398 | body := fmt.Sprintf(tagDeleteTemp, id) 399 | if err := this.DoPost(tagDelete, body, &res); err == nil { 400 | return &res, nil 401 | } else { 402 | return nil, err 403 | } 404 | } 405 | 406 | type TagUsers struct { 407 | Count int `json:"count"` 408 | Data struct { 409 | OpenID []string `json:"openid"` 410 | } `json:"data"` 411 | NextOpenID string `json:"next_openid"` 412 | } 413 | 414 | func (this WechatApi) GetTagUsers(tagid int, next_openid string) (*TagUsers, error) { 415 | var res TagUsers 416 | body := fmt.Sprintf(tagUsersTemp, tagid, next_openid) 417 | if err := this.DoPost(tagUsers, body, &res); err == nil { 418 | return &res, nil 419 | } else { 420 | return nil, err 421 | } 422 | } 423 | 424 | func (this WechatApi) AddTagMembers(tagid int, openid_list []string) (*utils.CommonError, error) { 425 | var res utils.CommonError 426 | ttt := template.New("AddTagMembers") 427 | ttt.Parse(tagUserBatchAddTemp) 428 | var buf bytes.Buffer 429 | ttt.Execute(&buf, struct { 430 | TagID int 431 | OpenIDList []string 432 | }{ 433 | TagID: tagid, 434 | OpenIDList: openid_list, 435 | }) 436 | 437 | if err := this.DoPost(tagUserBatchAdd, buf.String(), &res); err == nil { 438 | return &res, nil 439 | } else { 440 | return nil, err 441 | } 442 | } 443 | 444 | func (this WechatApi) RemoveTagMembers(tagid int, openid_list []string) (*utils.CommonError, error) { 445 | var res utils.CommonError 446 | 447 | ttt := template.New("RemoveTagMembers") 448 | ttt.Parse(tagUserBatchAddTemp) 449 | var buf bytes.Buffer 450 | ttt.Execute(&buf, struct { 451 | TagID int 452 | OpenIDList []string 453 | }{ 454 | TagID: tagid, 455 | OpenIDList: openid_list, 456 | }) 457 | if err := this.DoPost(tagUserBatchRm, buf.String(), &res); err == nil { 458 | return &res, nil 459 | } else { 460 | return nil, err 461 | } 462 | } 463 | 464 | type UserTags struct { 465 | TagidList []int `json:"tagid_list"` 466 | } 467 | 468 | func (this WechatApi) GetUserTags(openid string) (*UserTags, error) { 469 | var res UserTags 470 | body := fmt.Sprintf(userTagsTemp, openid) 471 | if err := this.DoPost(userTags, body, &res); err == nil { 472 | return &res, nil 473 | } else { 474 | return nil, err 475 | } 476 | } 477 | 478 | func (this WechatApi) SetUserRemark(openid string, remark string) (*utils.CommonError, error) { 479 | var res utils.CommonError 480 | body := fmt.Sprintf(userRemarkTemp, openid, remark) 481 | if err := this.DoPost(userRemark, body, &res); err == nil { 482 | return &res, nil 483 | } else { 484 | return nil, err 485 | } 486 | } 487 | 488 | //-----------------------------------黑名单------------------------------------------------------------------------------ 489 | 490 | const ( 491 | getblacklist = "https://api.weixin.qq.com/cgi-bin/tags/members/getblacklist?access_token=%s" 492 | batchunblacklist = "https://api.weixin.qq.com/cgi-bin/tags/members/batchunblacklist?access_token=%s" 493 | batchblacklist = "https://api.weixin.qq.com/cgi-bin/tags/members/batchblacklist?access_token=%s" 494 | ) 495 | const ( 496 | getBlacklistTemp = `{ 497 | "begin_openid":"%s" 498 | }` 499 | batchUnblacklistTemp = `{ 500 | "openid_list":[ 501 | {{range $index, $element := .OpenIDList}} 502 | {{if $index}},{{end}} 503 | "{{$element}}" 504 | {{end}} 505 | ] 506 | }` 507 | ) 508 | 509 | type Blacklist struct { 510 | Total int `json:"total"` 511 | Count int `json:"count"` 512 | NextOpenid string `json:"next_openid"` 513 | Data struct { 514 | Openid []string `json:"openid"` 515 | } `json:"data"` 516 | } 517 | 518 | func (this WechatApi) Batchblacklist(openid_list []string) (*utils.CommonError, error) { 519 | var res utils.CommonError 520 | ttt := template.New("Batchblacklist") 521 | ttt.Parse(batchUnblacklistTemp) 522 | var buf bytes.Buffer 523 | ttt.Execute(&buf, struct { 524 | OpenIDList []string 525 | }{ 526 | OpenIDList: openid_list, 527 | }) 528 | if err := this.DoPost(batchblacklist, buf.String(), &res); err == nil { 529 | return &res, nil 530 | } else { 531 | return nil, err 532 | } 533 | } 534 | 535 | func (this WechatApi) BatchUnblacklist(openid_list []string) (*utils.CommonError, error) { 536 | var res utils.CommonError 537 | ttt := template.New("BatchUnblacklist") 538 | ttt.Parse(batchUnblacklistTemp) 539 | var buf bytes.Buffer 540 | ttt.Execute(&buf, struct { 541 | OpenIDList []string 542 | }{ 543 | OpenIDList: openid_list, 544 | }) 545 | if err := this.DoPost(batchunblacklist, buf.String(), &res); err == nil { 546 | return &res, nil 547 | } else { 548 | return nil, err 549 | } 550 | } 551 | 552 | func (this WechatApi) GetBlacklist(next_openid string) (*Blacklist, error) { 553 | var res Blacklist 554 | body := fmt.Sprintf(getBlacklistTemp, next_openid) 555 | if err := this.DoPost(getblacklist, body, &res); err == nil { 556 | return &res, nil 557 | } else { 558 | return nil, err 559 | } 560 | } 561 | 562 | //-----------------------------------模板-------------------------------------------------------------------------------- 563 | const ( 564 | get_all_private_template = "https://api.weixin.qq.com/cgi-bin/template/get_all_private_template?access_token=%s" 565 | get_industry = "https://api.weixin.qq.com/cgi-bin/template/get_industry?access_token=%s" 566 | add_template = "https://api.weixin.qq.com/cgi-bin/template/api_add_template?access_token=%s" 567 | del_template = "https://api.weixin.qq.com/cgi-bin/template/del_private_template?access_token=%s" 568 | send_template = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=%s" 569 | ) 570 | 571 | const ( 572 | addTemplateTemp = `{"template_id_short":"%s"}` 573 | delTemplateTemp = `{"template_id":"%s"}` 574 | ) 575 | 576 | type TemplateSend struct { 577 | Touser string `json:"touser"` 578 | TemplateID string `json:"template_id"` 579 | Url string `json:"url"` 580 | Data map[string]struct { 581 | Value string `json:"value"` 582 | Color string `json:"color,omitempty"` 583 | } `json:"data"` 584 | } 585 | 586 | type TemplateSendRes struct { 587 | utils.CommonError 588 | MsgID string `json:"msgid"` 589 | } 590 | 591 | func (this WechatApi) SendTemplateMsg(msg *TemplateSend) (*TemplateSendRes, error) { 592 | var res TemplateSendRes 593 | body, err := json.Marshal(msg) 594 | if err != nil { 595 | return nil, err 596 | } 597 | if err := this.DoPost(send_template, string(body), &res); err == nil { 598 | return &res, nil 599 | } else { 600 | return nil, err 601 | } 602 | } 603 | 604 | type TemplateRes struct { 605 | utils.CommonError 606 | TemplateId string `json:"template_id"` 607 | } 608 | 609 | func (this WechatApi) AddTemplate(template_id_short string) (*TemplateRes, error) { 610 | var res TemplateRes 611 | body := fmt.Sprintf(addTemplateTemp, template_id_short) 612 | if err := this.DoPost(add_template, body, &res); err == nil { 613 | return &res, nil 614 | } else { 615 | return nil, err 616 | } 617 | } 618 | 619 | func (this WechatApi) DeleteTemplate(template_id string) (*utils.CommonError, error) { 620 | var res utils.CommonError 621 | body := fmt.Sprintf(delTemplateTemp, template_id) 622 | if err := this.DoPost(del_template, body, &res); err == nil { 623 | return &res, nil 624 | } else { 625 | return nil, err 626 | } 627 | } 628 | 629 | type TemplateList struct { 630 | TemplateList []struct { 631 | TemplateID string `json:"template_id"` 632 | Title string `json:"title"` 633 | PrimaryIndustry string `json:"primary_industry"` 634 | DeputyIndustry string `json:"deputy_industry"` 635 | Content string `json:"content"` 636 | Example string `json:"example"` 637 | } `json:"template_list"` 638 | } 639 | 640 | func (this WechatApi) GetAllPrivateTemplate() (*TemplateList, error) { 641 | var res TemplateList 642 | if err := this.DoGet(get_all_private_template, &res); err == nil { 643 | return &res, nil 644 | } else { 645 | return nil, err 646 | } 647 | } 648 | 649 | type Industry struct { 650 | PrimaryIndustry struct { 651 | FirstClass string `json:"first_class"` 652 | SecondClass string `json:"second_class"` 653 | } `json:"primary_industry"` 654 | SecondaryIndustry struct { 655 | FirstClass string `json:"first_class"` 656 | SecondClass string `json:"second_class"` 657 | } `json:"secondary_industry"` 658 | } 659 | 660 | func (this WechatApi) GetIndustry() (*Industry, error) { 661 | var res Industry 662 | if err := this.DoGet(get_industry, &res); err == nil { 663 | return &res, nil 664 | } else { 665 | return nil, err 666 | } 667 | } 668 | 669 | //-----------------------------------二维码------------------------------------------------------------------------------ 670 | 671 | const ( 672 | create_qrcode = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=%s" 673 | show_qrcode = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=%s" 674 | shorturl = "https://api.weixin.qq.com/cgi-bin/shorturl?access_token=%s" 675 | ) 676 | const ( 677 | shortUrlTemp = `{ 678 | "action": "long2short", 679 | "long_url": "%s" 680 | }` 681 | ) 682 | 683 | type ShortUrl struct { 684 | utils.CommonError 685 | ShortUrl string `json:"short_url"` 686 | } 687 | 688 | func (this WechatApi) ShortUrl(long_url string) (*ShortUrl, error) { 689 | var res ShortUrl 690 | body := fmt.Sprintf(shortUrlTemp, long_url) 691 | if err := this.DoPost(shorturl, body, &res); err == nil { 692 | return &res, nil 693 | } else { 694 | return nil, err 695 | } 696 | } 697 | 698 | type qrcodeParam struct { 699 | ActionName string `json:"action_name"` 700 | ExpireSeconds int `json:"expire_seconds,omitempty"` 701 | ActionInfo struct { 702 | Scene struct { 703 | SceneStr string `json:"scene_str,omitempty"` 704 | SceneID int `json:"scene_id,omitempty"` 705 | } `json:"scene"` 706 | } `json:"action_info"` 707 | } 708 | 709 | type QrcodeTicket struct { 710 | Ticket string `json:"ticket"` 711 | Expireseconds int `json:"expire_seconds"` 712 | Url string `json:"url"` 713 | } 714 | 715 | func (this WechatApi) CreateLimitStrQrcode(scene_str string) (*QrcodeTicket, error) { 716 | var res QrcodeTicket 717 | qrcodeParam := &qrcodeParam{ 718 | ActionName: "QR_LIMIT_STR_SCENE", 719 | } 720 | qrcodeParam.ActionInfo.Scene.SceneStr = scene_str 721 | tmp, _ := json.Marshal(qrcodeParam) 722 | body := string(tmp) 723 | if err := this.DoPost(create_qrcode, body, &res); err == nil { 724 | return &res, nil 725 | } else { 726 | return nil, err 727 | } 728 | } 729 | 730 | func (this WechatApi) CreateLimitQrcode(scene_id int) (*QrcodeTicket, error) { 731 | var res QrcodeTicket 732 | qrcodeParam := &qrcodeParam{ 733 | ActionName: "QR_LIMIT_SCENE", 734 | } 735 | qrcodeParam.ActionInfo.Scene.SceneID = scene_id 736 | tmp, _ := json.Marshal(qrcodeParam) 737 | body := string(tmp) 738 | if err := this.DoPost(create_qrcode, body, &res); err == nil { 739 | return &res, nil 740 | } else { 741 | return nil, err 742 | } 743 | } 744 | 745 | func (this WechatApi) CreateQrcode(expire_seconds int, scene_id int) (*QrcodeTicket, error) { 746 | var res QrcodeTicket 747 | qrcodeParam := &qrcodeParam{ 748 | ActionName: "QR_SCENE", 749 | ExpireSeconds: expire_seconds, 750 | } 751 | qrcodeParam.ActionInfo.Scene.SceneID = scene_id 752 | tmp, _ := json.Marshal(qrcodeParam) 753 | body := string(tmp) 754 | if err := this.DoPost(create_qrcode, body, &res); err == nil { 755 | return &res, nil 756 | } else { 757 | return nil, err 758 | } 759 | } 760 | 761 | func (this WechatApi) ShowQrcode(ticket string) string { 762 | return string(fmt.Sprintf(show_qrcode, ticket)) 763 | } 764 | 765 | //-------------------------------------菜单------------------------------------------------------------------------------ 766 | const ( 767 | menu_get = "https://api.weixin.qq.com/cgi-bin/menu/get?access_token=%s" 768 | menu_create = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=%s" 769 | menu_delete = "https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=%s" 770 | ) 771 | 772 | type MenuEntry struct { 773 | Type string `json:"type"` 774 | Name string `json:"name"` 775 | Key string `json:"key,omitempty"` 776 | Url string `json:"url,omitempty"` 777 | AppID string `json:"appid,omitempty"` 778 | Pagepath string `json:"pagepath,omitempty"` 779 | MediaID string `json:"media_id,omitempty"` 780 | SubButton []*MenuEntry `json:"sub_button,omitempty"` 781 | } 782 | 783 | type Menu struct { 784 | Menu struct { 785 | Buttons []*MenuEntry `json:"button"` 786 | } `json:"menu"` 787 | } 788 | 789 | func (this WechatApi) GetMenu() (*Menu, error) { 790 | var res Menu 791 | if err := this.DoGet(menu_get, &res); err == nil { 792 | return &res, nil 793 | } else { 794 | return nil, err 795 | } 796 | } 797 | 798 | func (this WechatApi) CreateMenu(menus []MenuEntry) (*utils.CommonError, error) { 799 | var res utils.CommonError 800 | data := struct { 801 | Button []MenuEntry `json:"button"` 802 | }{ 803 | Button: menus, 804 | } 805 | body, _ := json.Marshal(&data) 806 | 807 | if err := this.DoPostRaw(menu_create, body, &res); err == nil { 808 | return &res, nil 809 | } else { 810 | return nil, err 811 | } 812 | } 813 | 814 | func (this WechatApi) DeleteMenu() (*utils.CommonError, error) { 815 | var res utils.CommonError 816 | if err := this.DoGet(menu_delete, &res); err == nil { 817 | return &res, nil 818 | } else { 819 | return nil, err 820 | } 821 | } 822 | 823 | //-------------------------------------杂项------------------------------------------------------------------------------ 824 | 825 | const ( 826 | getcallbackip = "https://api.weixin.qq.com/cgi-bin/getcallbackip?access_token=%s" 827 | ) 828 | 829 | type IpList struct { 830 | IpList []string `json:"ip_list"` 831 | } 832 | 833 | func (this WechatApi) GetIpList() (*IpList, error) { 834 | var res IpList 835 | if err := this.DoGet(getcallbackip, &res); err == nil { 836 | return &res, nil 837 | } else { 838 | return nil, err 839 | } 840 | } 841 | 842 | func (this WechatApi) GetAccessToken() (*string, error) { 843 | accessToken, err := this.Context.GetAccessToken() 844 | if err == nil { 845 | return &accessToken, nil 846 | } else { 847 | return nil, err 848 | } 849 | } 850 | 851 | func (this WechatApi) GetJsTicket() (*string, error) { 852 | jsTicket, err := this.Context.GetJsTicket() 853 | if err == nil { 854 | return &jsTicket, nil 855 | } else { 856 | return nil, err 857 | } 858 | } 859 | 860 | func (this WechatApi) GetCardTicket() (*string, error) { 861 | jsTicket, err := this.Context.GetCardTicket() 862 | if err == nil { 863 | return &jsTicket, nil 864 | } else { 865 | return nil, err 866 | } 867 | } 868 | 869 | type SignJsRes struct { 870 | AppID string `json:"appid"` 871 | Timestamp string `json:"timestamp"` 872 | NonceStr string `json:"nonceStr"` 873 | Signature string `json:"signature"` 874 | } 875 | 876 | func (this WechatApi) SignJsTicket(nonceStr, timestamp, url string) (*SignJsRes, error) { 877 | jsTicket, err := this.Context.GetJsTicket() 878 | if err != nil { 879 | return nil, err 880 | } 881 | if timestamp == "" { 882 | bb := utils.GetCurrTs() 883 | timestamp = strconv.FormatInt(bb, 10) 884 | } 885 | sign := utils.WXConfigSign(jsTicket, nonceStr, timestamp, url) 886 | return &SignJsRes{ 887 | Signature: sign, 888 | AppID: this.Context.Config.AppID, 889 | NonceStr: nonceStr, 890 | Timestamp: timestamp, 891 | }, nil 892 | } 893 | 894 | type SignChooseCardParam struct { 895 | ApiTicket string `json:"api_ticket"` 896 | ApiID string `json:"app_id"` 897 | ShopID string `json:"location_id"` 898 | TimesTamp int64 `json:"times_tamp"` 899 | NonceStr string `json:"nonce_str"` 900 | CardID string `json:"card_id"` 901 | CardType string `json:"card_type"` 902 | } 903 | 904 | type SignChooseCardResp struct { 905 | ShopID string `json:"shop_id"` 906 | TimesTamp int64 `json:"timetamp"` 907 | NonceStr string `json:"nonce_str"` 908 | CardID string `json:"card_id"` 909 | CardType string `json:"card_type"` 910 | SignType string `json:"sign_type"` 911 | CardSign string `json:"card_sign"` 912 | } 913 | 914 | func (this WechatApi) signCardImp(variable interface{}) (sign string, err error) { 915 | ss := &utils.SignStructValue{} 916 | sign, err = ss.Sign(variable, nil) 917 | return 918 | } 919 | 920 | func (this WechatApi) SignChooseCard(shopID, cardID, cardType string) (*SignChooseCardResp, error) { 921 | jsTicket, err := this.Context.GetCardTicket() 922 | if err != nil { 923 | return nil, err 924 | } 925 | param := &SignChooseCardParam{ 926 | ApiTicket: jsTicket, 927 | ApiID: this.Context.Config.AppID, 928 | TimesTamp: utils.GetCurrTs(), 929 | NonceStr: utils.RandString(30), 930 | CardID: cardID, 931 | CardType: cardType, 932 | ShopID: shopID, 933 | } 934 | 935 | sign, err := this.signCardImp(param) 936 | if err != nil { 937 | return nil, err 938 | } 939 | return &SignChooseCardResp{ 940 | ShopID: param.ShopID, 941 | TimesTamp: param.TimesTamp, 942 | NonceStr: param.NonceStr, 943 | CardID: param.CardID, 944 | CardType: param.CardType, 945 | SignType: "SHA1", 946 | CardSign: sign, 947 | }, nil 948 | } 949 | 950 | type SignAddCardParamObj struct { 951 | CardID string `json:"card_id"` 952 | Code string `json:"code,omitempty" doc:"指定的卡券code码,只能被领一次。自定义code模式的卡券必须填写,非自定义code和预存code模式的卡券不必填写。"` 953 | OpenID string `json:"openid,omitempty" doc:"指定领取者的openid,只有该用户能领取。bind_openid字段为true的卡券必须填写,bind_openid字段为false不必填写。"` 954 | FixedBegintimestamp int64 `json:"fixed_begintimestamp,omitempty" doc:"卡券在第三方系统的实际领取时间,为东八区时间戳(UTC+8,精确到秒)。当卡券的有效期类型为DATE_TYPE_FIX_TERM时专用,标识卡券的实际生效时间,用于解决商户系统内起始时间和领取时间不同步的问题。"` 955 | OuterStr string `json:"outer_str"` 956 | } 957 | 958 | type SignAddCardParam struct { 959 | CardList []SignAddCardParamObj `json:"cardList"` 960 | } 961 | 962 | type SignAddCardRespObj struct { 963 | CardID string `json:"cardId"` 964 | CardExt string `json:"cardExt"` 965 | } 966 | 967 | type SignAddCardResp struct { 968 | CardList []*SignAddCardRespObj `json:"cardList"` 969 | } 970 | 971 | func (this WechatApi) SignAddCard(param *SignAddCardParam) (*SignAddCardResp, error) { 972 | jsTicket, err := this.Context.GetCardTicket() 973 | if err != nil { 974 | return nil, err 975 | } 976 | 977 | resp := &SignAddCardResp{} 978 | resp.CardList = make([]*SignAddCardRespObj, 0, len(param.CardList)) 979 | for _, value := range param.CardList { 980 | tmpVar := &struct { 981 | CardID string `json:"-"` 982 | Code string `json:"code,omitempty"` 983 | OpenID string `json:"openid,omitempty"` 984 | ApiTicket string `json:"-"` 985 | TimesTamp int64 `json:"timetamp"` 986 | NonceStr string `json:"nonce_str"` 987 | Sign string `json:"signature" sign:"-"` 988 | FixedBegintimestamp int64 `json:"fixed_begintimestamp,omitempty" sign:"-"` 989 | OuterStr string `json:"outer_str" sign:"-"` 990 | }{ 991 | CardID: value.CardID, 992 | Code: value.Code, 993 | OpenID: value.OpenID, 994 | FixedBegintimestamp: value.FixedBegintimestamp, 995 | OuterStr: value.OuterStr, 996 | ApiTicket: jsTicket, 997 | TimesTamp: utils.GetCurrTs(), 998 | NonceStr: utils.RandString(30), 999 | } 1000 | 1001 | 1002 | sign, err := this.signCardImp(tmpVar) 1003 | if err != nil { 1004 | return nil, err 1005 | } 1006 | tmpVar.Sign = sign 1007 | 1008 | cardExt,_ := json.Marshal(tmpVar) 1009 | resp.CardList = append(resp.CardList,&SignAddCardRespObj{ 1010 | CardID: value.CardID, 1011 | CardExt: string(cardExt), 1012 | }) 1013 | } 1014 | 1015 | return resp,nil 1016 | } -------------------------------------------------------------------------------- /mp/card.go: -------------------------------------------------------------------------------- 1 | package mp 2 | 3 | import ( 4 | "github.com/qjw/go-wx-sdk/utils" 5 | ) 6 | 7 | const ( 8 | batchGetCard = "https://api.weixin.qq.com/card/batchget?access_token=%s" 9 | getCard = "https://api.weixin.qq.com/card/get?access_token=%s" 10 | createCardQrcode = "https://api.weixin.qq.com/card/qrcode/create?access_token=%s" 11 | createCard = "https://api.weixin.qq.com/card/create?access_token=%s" 12 | setCardWhitelist = "https://api.weixin.qq.com/card/testwhitelist/set?access_token=%s" 13 | ) 14 | 15 | type GetCardListParam struct { 16 | utils.Pagination 17 | StatusList []string `json:"status_list" doc:"CARD_STATUS_NOT_VERIFY,待审核;CARD_STATUS_VERIFY_FAIL,审核失败;CARD_STATUS_VERIFY_OK,通过审核;CARD_STATUS_DELETE,卡券被商户删除;CARD_STATUS_DISPATCH,在公众平台投放过的卡券;"` 18 | } 19 | 20 | type GetCardListResp struct { 21 | utils.CommonError 22 | CardIDList []string `json:"card_id_list"` 23 | } 24 | 25 | func (this WechatApi) GetCardList(param *GetCardListParam) (*GetCardListResp, error) { 26 | var res GetCardListResp 27 | if err := this.DoPostObject(batchGetCard, param, &res); err == nil { 28 | return &res, nil 29 | } else { 30 | return nil, err 31 | } 32 | } 33 | 34 | type CardAdvancedInfoObj struct { 35 | UseCondition *struct { 36 | AcceptCategory string `json:"accept_category,omitempty" doc:"指定可用的商品类目,仅用于代金券类型,填入后将在券面拼写适用于xxx"` 37 | RejectCategory string `json:"reject_category,omitempty" doc:"指定不可用的商品类目,仅用于代金券类型,填入后将在券面拼写不适用于xxxx"` 38 | CanUseWithOtherDiscount bool `json:"can_use_with_other_discount,omitempty" doc:"不可以与其他类型共享门槛,填写false时系统将在使用须知里拼写“不可与其他优惠共享”,填写true时系统将在使用须知里拼写“可与其他优惠共享”,默认为true"` 39 | } `json:"use_condition,omitempty"` 40 | 41 | Abstract *struct { 42 | Abstract string `json:"abstract,omitempty" doc:"封面摘要简介。"` 43 | IconUrlList []string `json:"icon_url_list,omitempty" doc:"封面图片列表,仅支持填入一个封面图片链接,上传图片接口上传获取图片获得链接,填写非CDN链接会报错,并在此填入。建议图片尺寸像素850*350"` 44 | } `json:"abstract,omitempty"` 45 | 46 | TextImageList []struct { 47 | ImageUrl string `json:"image_url,omitempty" doc:"图片链接,必须调用上传图片接口上传图片获得链接,并在此填入,否则报错"` 48 | Text string `json:"text,omitempty" doc:"图文描述"` 49 | } `json:"text_image_list,omitempty" doc:"图文列表,显示在详情内页,优惠券券开发者须至少传入一组图文列表"` 50 | 51 | TimeLimit []struct { 52 | Type string `json:"type,omitempty" doc:"限制类型枚举值:支持填入MONDAY 周一 TUESDAY 周二 WEDNESDAY 周三 THURSDAY 周四 FRIDAY 周五 SATURDAY 周六 SUNDAY 周日 此处只控制显示,不控制实际使用逻辑,不填默认不显示"` 53 | BeginHour int `json:"begin_hour,omitempty" doc:"当前type类型下的起始时间(小时),如当前结构体内填写了MONDAY,此处填写了10,则此处表示周一 10:00可用"` 54 | BeginMinute int `json:"begin_minute,omitempty" doc:"当前type类型下的起始时间(分钟),如当前结构体内填写了MONDAY,begin_hour填写10,此处填写了59,则此处表示周一 10:59可用"` 55 | EndHour int `json:"end_hour,omitempty" doc:"当前type类型下的结束时间(小时),如当前结构体内填写了MONDAY,此处填写了20,则此处表示周一 10:00-20:00可用"` 56 | EndMinute int `json:"end_minute,omitempty" doc:"当前type类型下的结束时间(分钟),如当前结构体内填写了MONDAY,begin_hour填写10,此处填写了59,则此处表示周一 10:59-00:59可用"` 57 | } `json:"time_limit,omitempty" doc:"使用时段限制"` 58 | BusinessService []string `json:"business_service,omitempty" doc:"商家服务类型:BIZ_SERVICE_DELIVER 外卖服务;BIZ_SERVICE_FREE_PARK 停车位;BIZ_SERVICE_WITH_PET 可带宠物;BIZ_SERVICE_FREE_WIFI 免费wifi,可多选"` 59 | } 60 | 61 | type CardBaseInfoCommonObj struct { 62 | LogoUrl string `json:"logo_url" doc:"卡券的商户logo"` 63 | BrandName string `json:"brand_name" doc:"商户名字,字数上限为12个汉字"` 64 | CodeType string `json:"code_type" doc:"CODE_TYPE_TEXT文本;CODE_TYPE_BARCODE一维码 CODE_TYPE_QRCODE二维码CODE_TYPE_ONLY_QRCODE,二维码无code显示;CODE_TYPE_ONLY_BARCODE,一维码无code显示;CODE_TYPE_NONE,不显示code和条形码类型"` 65 | Title string `json:"title" doc:"卡券名,字数上限为9个汉字。(建议涵盖卡券属性、服务及金额)。"` 66 | Color string `json:"color" doc:"券颜色。按色彩规范标注填写Color010-Color102"` 67 | Notice string `json:"notice" doc:"卡券使用提醒,字数上限为16个汉字。"` 68 | Description string `json:"description" doc:"卡券使用说明,字数上限为1024个汉字。"` 69 | 70 | DateInfo struct { 71 | Type string `json:"type" doc:"DATE_TYPE_FIX_TIME_RANGE 表示固定日期区间,DATE_TYPE_FIX_TERM表示固定时长(自领取后按天算),DATE_TYPE_PERMANENT 表示永久有效(会员卡类型专用)"` 72 | BeginTimestamp int64 `json:"begin_timestamp,omitempty" doc:"type为DATE_TYPE_FIX_TIME_RANGE时专用,表示起用时间。从1970年1月1日00:00:00至起用时间的秒数,最终需转换为字符串形态传入。(东八区时间,单位为秒)"` 73 | EndTimestamp int64 `json:"end_timestamp,omitempty" doc:"type为DATE_TYPE_FIX_TIME_RANGE时专用,表示结束时间,建议设置为截止日期的23:59:59过期。(东八区时间,单位为秒)截止日期必须大于当前时间"` 74 | FixedTerm string `json:"fixed_term,omitempty" doc:"type为DATE_TYPE_FIX_TERM时专用,表示自领取后多少天内有效,不支持填写0。"` 75 | FixedBeginTerm string `json:"fixed_begin_term,omitempty" doc:"type为DATE_TYPE_FIX_TERM时专用,表示自领取后多少天开始生效,领取后当天生效填写0。(单位为天)"` 76 | } `json:"date_info" doc:"使用日期,有效期的信息"` 77 | 78 | LocationIDList []int64 `json:"location_id_list,omitempty" doc:"支持更新适用门店列表。"` 79 | UseAllLocations bool `json:"use_all_locations,omitempty" doc:"设置本卡券支持全部门店,与location_id_list互斥"` 80 | 81 | UseCustomCode bool `json:"use_custom_code,omitempty" doc:"是否自定义Code码。填写true或false,默认为false。通常自有优惠码系统的开发者选择自定义Code码,并在卡券投放时带入Code码"` 82 | BindOpenid bool `json:"bind_openid,omitempty" doc:"是否指定用户领取,填写true或false。默认为false。通常指定特殊用户群体投放卡券或防止刷券时选择指定用户领取。"` 83 | ServicePhone string `json:"service_phone,omitempty" doc:"客服电话。"` 84 | CanShare bool `json:"can_share,omitempty" doc:"卡券领取页面是否可分享。"` 85 | Source string `json:"source,omitempty" doc:"第三方来源名,例如同程旅游、大众点评。"` 86 | 87 | CustomUrlName string `json:"custom_url_name,omitempty" doc:"自定义入口名称"` 88 | CustomUrl string `json:"custom_url,omitempty" doc:"自定义入口URL"` 89 | CustomUrlSubTitle string `json:"custom_url_sub_title,omitempty" doc:"显示在入口右侧的提示语。"` 90 | 91 | PromotionUrlName string `json:"promotion_url_name,omitempty" doc:"营销场景的自定义入口名称。"` 92 | PromotionUrl string `json:"promotion_url,omitempty" doc:"入口跳转外链的地址链接。"` 93 | PromotionUrlSubTitle string `json:"promotion_url_sub_title,omitempty" doc:"显示在营销入口右侧的提示语。"` 94 | } 95 | 96 | type CardBaseInfoCreateObj struct { 97 | CardBaseInfoCommonObj 98 | 99 | Sku struct { 100 | Quantity int64 `json:"quantity" doc:"卡券库存的数量,上限为100000000。"` 101 | } `json:"sku"` 102 | 103 | CenterTitle string `json:"center_title,omitempty" doc:"卡券顶部居中的按钮,仅在卡券状态正常(可以核销)时显示"` 104 | CenterSubTitle string `json:"center_sub_title,omitempty" doc:"显示在入口下方的提示语,仅在卡券状态正常(可以核销)时显示。"` 105 | CenterUrl string `json:"center_url,omitempty" doc:"顶部居中的url,仅在卡券状态正常(可以核销)时显示。"` 106 | 107 | GetLimit int64 `json:"get_limit,omitempty" doc:"每人可领券的数量限制,不填写默认为50"` 108 | UseLimit int64 `json:"use_limit,omitempty" doc:"每人可核销的数量限制,不填写默认为50"` 109 | CanGiveFriend bool `json:"can_give_friend,omitempty" doc:"卡券是否可转赠。"` 110 | 111 | GetCustomCodeMode string `json:"get_custom_code_mode,omitempty" doc:"填入GET_CUSTOM_CODE_MODE_DEPOSIT表示该卡券为预存code模式卡券,须导入超过库存数目的自定义code后方可投放,填入该字段后,quantity字段须为0,须导入code后再增加库存"` 112 | } 113 | 114 | type CardBaseInfoObj struct { 115 | CardBaseInfoCommonObj 116 | 117 | Sku struct { 118 | Quantity int64 `json:"quantity" doc:"卡券现有库存的数量"` 119 | TotalQuantity int64 `json:"total_quantity" doc:"卡券全部库存的数量,上限为100000000"` 120 | } `json:"sku"` 121 | 122 | Status string `json:"status" doc:"CARD_STATUS_NOT_VERIFY,待审核;CARD_STATUS_VERIFY_FAIL,审核失败;CARD_STATUS_VERIFY_OK,通过审核;CARD_STATUS_DELETE,卡券被商户删除;CARD_STATUS_DISPATCH,在公众平台投放过的卡券;"` 123 | 124 | ID string `json:"id"` 125 | CreateTime int64 `json:"create_time"` 126 | UpdateTime int64 `json:"update_time"` 127 | } 128 | 129 | type CreateCardParam struct { 130 | Card struct { 131 | CardType string `json:"card_type" doc:"卡券类型。团购券:GROUPON; 折扣券:DISCOUNT; 礼品券:GIFT;代金券:CASH; 通用券:GENERAL_COUPON;会员卡:MEMBER_CARD; 景点门票:SCENIC_TICKET;电影票:MOVIE_TICKET; 飞机票:BOARDING_PASS;会议门票:MEETING_TICKET; 汽车票:BUS_TICKET;"` 132 | Groupon *struct { 133 | BaseInfo CardBaseInfoCreateObj `json:"base_info"` 134 | AdvancedInfo CardAdvancedInfoObj `json:"advanced_info"` 135 | DealDetail string `json:"deal_detail" doc:"团购券专用,团购详情。"` 136 | } `json:"groupon,omitempty" doc:"团购券"` 137 | Cash *struct { 138 | BaseInfo CardBaseInfoCreateObj `json:"base_info"` 139 | AdvancedInfo CardAdvancedInfoObj `json:"advanced_info"` 140 | LeastCost int `json:"least_cost" doc:"代金券专用,表示起用金额(单位为分),如果无起用门槛则填0。"` 141 | ReduceCost int `json:"reduce_cost" doc:"代金券专用,表示减免金额。(单位为分)"` 142 | } `json:"cash,omitempty" doc:"代金券"` 143 | Discount *struct { 144 | BaseInfo CardBaseInfoCreateObj `json:"base_info"` 145 | AdvancedInfo CardAdvancedInfoObj `json:"advanced_info"` 146 | Discount int `json:"discount" doc:"折扣券专用,表示打折额度(百分比)。填30就是七折。"` 147 | } `json:"discount,omitempty" doc:"折扣券"` 148 | Gift *struct { 149 | BaseInfo CardBaseInfoCreateObj `json:"base_info"` 150 | AdvancedInfo CardAdvancedInfoObj `json:"advanced_info"` 151 | Gift string `json:"gift" doc:"兑换券专用,填写兑换内容的名称。"` 152 | } `json:"gift,omitempty" doc:"兑换券"` 153 | GeneralCoupon *struct { 154 | BaseInfo CardBaseInfoCreateObj `json:"base_info"` 155 | AdvancedInfo CardAdvancedInfoObj `json:"advanced_info"` 156 | DefaultDetail string `json:"default_detail" doc:"优惠券专用,填写优惠详情。"` 157 | } `json:"general_coupon,omitempty" doc:"优惠券"` 158 | } `json:"card"` 159 | } 160 | 161 | type GetCardDetailParam struct { 162 | CardID string `json:"card_id"` 163 | } 164 | 165 | type GetCardDetailResp struct { 166 | utils.CommonError 167 | Card struct { 168 | CardType string `json:"card_type" doc:"卡券类型。团购券:GROUPON; 折扣券:DISCOUNT; 礼品券:GIFT;代金券:CASH; 通用券:GENERAL_COUPON;会员卡:MEMBER_CARD; 景点门票:SCENIC_TICKET;电影票:MOVIE_TICKET; 飞机票:BOARDING_PASS;会议门票:MEETING_TICKET; 汽车票:BUS_TICKET;"` 169 | Groupon *struct { 170 | BaseInfo CardBaseInfoObj `json:"base_info"` 171 | AdvancedInfo CardAdvancedInfoObj `json:"advanced_info"` 172 | DealDetail string `json:"deal_detail" doc:"团购券专用,团购详情。"` 173 | } `json:"groupon,omitempty" doc:"团购券"` 174 | Cash *struct { 175 | BaseInfo CardBaseInfoObj `json:"base_info"` 176 | AdvancedInfo CardAdvancedInfoObj `json:"advanced_info"` 177 | LeastCost int `json:"least_cost" doc:"代金券专用,表示起用金额(单位为分),如果无起用门槛则填0。"` 178 | ReduceCost int `json:"reduce_cost" doc:"代金券专用,表示减免金额。(单位为分)"` 179 | } `json:"cash,omitempty" doc:"代金券"` 180 | Discount *struct { 181 | BaseInfo CardBaseInfoObj `json:"base_info"` 182 | AdvancedInfo CardAdvancedInfoObj `json:"advanced_info"` 183 | Discount int `json:"discount" doc:"折扣券专用,表示打折额度(百分比)。填30就是七折。"` 184 | } `json:"discount,omitempty" doc:"折扣券"` 185 | Gift *struct { 186 | BaseInfo CardBaseInfoObj `json:"base_info"` 187 | AdvancedInfo CardAdvancedInfoObj `json:"advanced_info"` 188 | Gift string `json:"gift" doc:"兑换券专用,填写兑换内容的名称。"` 189 | } `json:"gift,omitempty" doc:"兑换券"` 190 | GeneralCoupon *struct { 191 | BaseInfo CardBaseInfoObj `json:"base_info"` 192 | AdvancedInfo CardAdvancedInfoObj `json:"advanced_info"` 193 | DefaultDetail string `json:"default_detail" doc:"优惠券专用,填写优惠详情。"` 194 | } `json:"general_coupon,omitempty" doc:"优惠券"` 195 | } `json:"card"` 196 | } 197 | 198 | func (this WechatApi) GetCardDetail(param *GetCardDetailParam) (*GetCardDetailResp, error) { 199 | var res GetCardDetailResp 200 | if err := this.DoPostObject(getCard, param, &res); err == nil { 201 | return &res, nil 202 | } else { 203 | return nil, err 204 | } 205 | } 206 | 207 | type CreateCardQrcodeParam struct { 208 | Action_name string `json:"action_name" doc:"QR_CARD"` 209 | Expire_seconds int64 `json:"expire_seconds"` 210 | Action_info struct { 211 | Card struct { 212 | CardID string `json:"card_id"` 213 | Code string `json:"code,omitempty"` 214 | Openid string `json:"openid,omitempty"` 215 | IsUniqueCode string `json:"is_unique_code,omitempty"` 216 | OuterID int `json:"outer_id,omitempty"` 217 | OuterStr string `json:"outer_str,omitempty"` 218 | } `json:"card"` 219 | } `json:"action_info"` 220 | } 221 | 222 | type CreateCardQrcodeResp struct { 223 | utils.CommonError 224 | Ticket string `json:"ticket"` 225 | Url string `json:"url"` 226 | ShowQrcodeUrl string `json:"show_qrcode_url"` 227 | } 228 | 229 | func (this WechatApi) CreateCardQrcode(param *CreateCardQrcodeParam) (*CreateCardQrcodeResp, error) { 230 | var res CreateCardQrcodeResp 231 | if err := this.DoPostObject(createCardQrcode, param, &res); err == nil { 232 | return &res, nil 233 | } else { 234 | return nil, err 235 | } 236 | } 237 | 238 | type CreateCardResp struct { 239 | utils.CommonError 240 | CardID string `json:"card_id"` 241 | } 242 | 243 | func (this WechatApi) CreateCard(param *CreateCardParam) (*CreateCardResp, error) { 244 | var res CreateCardResp 245 | if err := this.DoPostObject(createCard, param, &res); err == nil { 246 | return &res, nil 247 | } else { 248 | return nil, err 249 | } 250 | } 251 | 252 | type SetCardWhitelistParam struct { 253 | OpenIDs []string `json:"openid"` 254 | Usernames []string `json:"username"` 255 | } 256 | 257 | func (this WechatApi) SetCardWhitelist(param *SetCardWhitelistParam) (*utils.CommonError, error) { 258 | var res utils.CommonError 259 | if err := this.DoPostObject(setCardWhitelist, param, &res); err == nil { 260 | return &res, nil 261 | } else { 262 | return nil, err 263 | } 264 | } 265 | 266 | const ( 267 | decryptCardCode = "https://api.weixin.qq.com/card/code/decrypt?access_token=%s" 268 | deleteCard = "https://api.weixin.qq.com/card/delete?access_token=%s" 269 | checkCardCode = "https://api.weixin.qq.com/card/code/get?access_token=%s" 270 | getCardUseList = "https://api.weixin.qq.com/card/user/getcardlist?access_token=%s" 271 | unavailableCardCode = "https://api.weixin.qq.com/card/code/unavailable?access_token=%s" 272 | // getcardbizuininfo = "https://api.weixin.qq.com/datacube/getcardbizuininfo?access_token=%s" 273 | ) 274 | 275 | type GetCardUseListParam struct { 276 | CardID string `json:"card_id,omitempty"` 277 | OpenID string `json:"openid" doc:"用户openid"` 278 | } 279 | 280 | type GetCardUseListResp struct { 281 | utils.CommonError 282 | CardList []struct { 283 | CardIDParam 284 | Code string `json:"code"` 285 | } `json:"card_list"` 286 | HasShareCard bool `json:"has_share_card" doc:"是否有可用的朋友的券"` 287 | } 288 | 289 | func (this WechatApi) GetCardUseList(param *GetCardUseListParam) (*GetCardUseListResp, error) { 290 | var res GetCardUseListResp 291 | if err := this.DoPostObject(getCardUseList, param, &res); err == nil { 292 | return &res, nil 293 | } else { 294 | return nil, err 295 | } 296 | } 297 | 298 | type CheckCardCodeParam struct { 299 | CardID string `json:"card_id,omitempty"` 300 | Code string `json:"code"` 301 | CheckConsume bool `json:"check_consume" doc:"是否校验code核销状态,填入true和false时的code异常状态返回数据不同。"` 302 | } 303 | 304 | type CheckCardCodeResp struct { 305 | utils.CommonError 306 | Card struct { 307 | CardID string `json:"card_id"` 308 | BeginTime int64 `json:"begin_time"` 309 | EndTime int64 `json:"end_time"` 310 | } `json:"card"` 311 | OpenID string `json:"openid" doc:"用户openid"` 312 | CanConsume bool `json:"can_consume" doc:"是否可以核销,true为可以核销,false为不可核销"` 313 | OuterStr string `json:"outer_str"` 314 | UserCardStatus string `json:"user_card_status" doc:"当前code对应卡券的状态NORMAL正常,CONSUMED已核销,EXPIRE已过期,GIFTING转赠中,GIFT_TIMEOUT转赠超时,DELETE已删除,UNAVAILABLE已失效"` 315 | } 316 | 317 | func (this WechatApi) CheckCardCode(param *CheckCardCodeParam) (*CheckCardCodeResp, error) { 318 | var res CheckCardCodeResp 319 | if err := this.DoPostObject(checkCardCode, param, &res); err == nil { 320 | return &res, nil 321 | } else { 322 | return nil, err 323 | } 324 | } 325 | 326 | type CardOuterParam struct { 327 | EncryptCode string `json:"encrypt_code" doc:"加密的code"` 328 | CardCode string `json:"code" doc:"实际的code"` 329 | CardID string `json:"card_id"` 330 | OpenID string `json:"openid"` 331 | OuterStr string `json:"outer_str"` 332 | OuterID int `json:"outer_id"` 333 | } 334 | 335 | 336 | type DecryptCardCodeParam struct { 337 | EncryptCode string `json:"encrypt_code" doc:"加密的code"` 338 | } 339 | type DecryptCardCodeResp struct { 340 | utils.CommonError 341 | CardCode string `json:"code" doc:"实际的code"` 342 | } 343 | 344 | func (this WechatApi) DecryptCardCode(param *DecryptCardCodeParam) (*DecryptCardCodeResp, error) { 345 | var res DecryptCardCodeResp 346 | if err := this.DoPostObject(decryptCardCode, param, &res); err == nil { 347 | return &res, nil 348 | } else { 349 | return nil, err 350 | } 351 | } 352 | 353 | type UnavailableCardCodeParam struct { 354 | CardID string `json:"card_id" doc:"自定义code卡券的请求 需要此字段"` 355 | Code string `json:"code"` 356 | Reason string `json:"reason"` 357 | } 358 | 359 | func (this WechatApi) UnavailableCardCode(param *UnavailableCardCodeParam) (*utils.CommonError, error) { 360 | var res utils.CommonError 361 | if err := this.DoPostObject(unavailableCardCode, param, &res); err == nil { 362 | return &res, nil 363 | } else { 364 | return nil, err 365 | } 366 | } 367 | 368 | type CardIDParam struct { 369 | CardID string `json:"card_id"` 370 | } 371 | 372 | /* 373 | 注意:如用户在商家删除卡券前已领取一张或多张该卡券依旧有效。即删除卡券不能删除已被用户领取,保存在微信客户端中的卡券。 374 | */ 375 | func (this WechatApi) DeleteCard(param *CardIDParam) (*utils.CommonError, error) { 376 | var res utils.CommonError 377 | if err := this.DoPostObject(deleteCard, param, &res); err == nil { 378 | return &res, nil 379 | } else { 380 | return nil, err 381 | } 382 | } 383 | 384 | const ( 385 | createCardLandingpage = "https://api.weixin.qq.com/card/landingpage/create?access_token=%s" 386 | consumeCardCode = "https://api.weixin.qq.com/card/code/consume?access_token=%s" 387 | ) 388 | 389 | type CreateCardLandingpageParam struct { 390 | Banner string `json:"banner" doc:"页面的banner图片链接,须调用,建议尺寸为640*300。"` 391 | PageTitle string `json:"page_title" doc:"页面的title。"` 392 | CanShare bool `json:"can_share" doc:"页面是否可以分享,填入true/false"` 393 | Scene string `json:"scene" doc:"投放页面的场景值;SCENE_NEAR_BY附近,SCENE_MENU自定义菜单,SCENE_QRCODE二维码,SCENE_ARTICLE公众号文章,SCENE_H5 h5页面,SCENE_IVR 自动回复,SCENE_CARD_CUSTOM_CELL 卡券自定义cell"` 394 | CardList []struct { 395 | CardID string `json:"card_id"` 396 | ThumbUrl string `json:"thumb_url" doc:"缩略图url"` 397 | } `json:"card_list"` 398 | } 399 | 400 | type CreateCardLandingpageResp struct { 401 | utils.CommonError 402 | Url string `json:"url" doc:"货架链接。"` 403 | PageID int64 `json:"page_id" doc:"货架ID。货架的唯一标识。"` 404 | } 405 | 406 | func (this WechatApi) CreateCardLandingpage(param *CreateCardLandingpageParam) (*CreateCardLandingpageResp, error) { 407 | var res CreateCardLandingpageResp 408 | if err := this.DoPostObject(createCardLandingpage, param, &res); err == nil { 409 | return &res, nil 410 | } else { 411 | return nil, err 412 | } 413 | } 414 | 415 | type ConsumeCardCodeParam struct { 416 | CardID string `json:"card_id,omitempty" doc:"自定义code卡券的请求 需要此字段"` 417 | Code string `json:"code"` 418 | } 419 | 420 | type ConsumeCardCodeResp struct { 421 | utils.CommonError 422 | Openid string `json:"openid" doc:"用户在该公众号内的唯一身份标识。"` 423 | Card struct { 424 | CardID string `json:"card_id" doc:"卡券ID。"` 425 | } `json:"card"` 426 | } 427 | 428 | func (this WechatApi) ConsumeCardCode(param *ConsumeCardCodeParam) (*ConsumeCardCodeResp, error) { 429 | var res ConsumeCardCodeResp 430 | if err := this.DoPostObject(consumeCardCode, param, &res); err == nil { 431 | return &res, nil 432 | } else { 433 | return nil, err 434 | } 435 | } 436 | -------------------------------------------------------------------------------- /mp/common.go: -------------------------------------------------------------------------------- 1 | package mp 2 | 3 | import ( 4 | "github.com/qjw/go-wx-sdk/utils" 5 | ) 6 | 7 | 8 | type Config struct { 9 | // 微信公众号app id 10 | AppID string 11 | // 微信公众号密钥 12 | AppSecret string 13 | // 微信公众号token 14 | Token string 15 | // 微信公众号消息加密密钥 16 | EncodingAESKey string 17 | } 18 | 19 | 20 | type WechatApi struct { 21 | utils.ApiTokenBase 22 | Context *Context 23 | } 24 | 25 | func NewWechatApi(context *Context) * WechatApi{ 26 | api := &WechatApi{ 27 | Context:context, 28 | } 29 | api.ContextToken = context 30 | return api 31 | } 32 | -------------------------------------------------------------------------------- /mp/context.go: -------------------------------------------------------------------------------- 1 | package mp 2 | 3 | import ( 4 | "github.com/qjw/go-wx-sdk/utils" 5 | "encoding/json" 6 | "fmt" 7 | "sync" 8 | "time" 9 | "github.com/qjw/go-wx-sdk/cache" 10 | ) 11 | 12 | const ( 13 | //AccessTokenURL 获取access_token的接口 14 | accessTokenURL = "https://api.weixin.qq.com/cgi-bin/token" 15 | jsTicketUrl = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=jsapi" 16 | cardTicketUrl = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=wx_card" 17 | ) 18 | 19 | const ( 20 | jsTickctTemp = "js_ticket_%s" 21 | cardTickctTemp = "card_ticket_%s" 22 | ) 23 | 24 | // Context struct 25 | type Context struct { 26 | // 配置 27 | Config *Config 28 | 29 | // 缓存处理器 30 | Cache cache.Cache 31 | 32 | //accessTokenLock 读写锁 同一个AppID一个 33 | accessTokenLock *sync.RWMutex 34 | 35 | //jsAPITicket 读写锁 同一个AppID一个 36 | jsAPITicketLock *sync.RWMutex 37 | } 38 | 39 | // SetJsAPITicketLock 设置jsAPITicket的lock 40 | func (ctx *Context) setJsAPITicketLock(lock *sync.RWMutex) { 41 | ctx.jsAPITicketLock = lock 42 | } 43 | 44 | //SetAccessTokenLock 设置读写锁(一个appID一个读写锁) 45 | func (ctx *Context) setAccessTokenLock(l *sync.RWMutex) { 46 | ctx.accessTokenLock = l 47 | } 48 | 49 | func NewContext(config *Config, cache cache.Cache) *Context { 50 | context := &Context{ 51 | Config: config, 52 | Cache: cache, 53 | } 54 | context.setAccessTokenLock(new(sync.RWMutex)) 55 | context.setJsAPITicketLock(new(sync.RWMutex)) 56 | return context 57 | } 58 | 59 | //GetAccessToken 获取access_token 60 | func (ctx *Context) GetAccessToken() (accessToken string, err error) { 61 | ctx.accessTokenLock.Lock() 62 | defer ctx.accessTokenLock.Unlock() 63 | 64 | accessTokenCacheKey := fmt.Sprintf("access_token_%s", ctx.Config.AppID) 65 | val := ctx.Cache.Get(accessTokenCacheKey) 66 | if val != nil { 67 | accessToken = val.(string) 68 | return 69 | } 70 | 71 | //从微信服务器获取 72 | var resAccessToken utils.ResAccessToken 73 | resAccessToken, err = ctx.GetAccessTokenFromServer() 74 | if err != nil { 75 | return 76 | } 77 | 78 | accessToken = resAccessToken.AccessToken 79 | return 80 | } 81 | 82 | //GetAccessTokenFromServer 强制从微信服务器获取token 83 | func (ctx *Context) GetAccessTokenFromServer() (resAccessToken utils.ResAccessToken, err error) { 84 | url := fmt.Sprintf("%s?grant_type=client_credential&appid=%s&secret=%s", accessTokenURL, 85 | ctx.Config.AppID, 86 | ctx.Config.AppSecret) 87 | 88 | body, _, err := utils.HTTPGet(url) 89 | err = json.Unmarshal(body, &resAccessToken) 90 | if err != nil { 91 | return 92 | } 93 | if resAccessToken.ErrMsg != "" { 94 | err = fmt.Errorf("get access_token error : errcode=%v , errormsg=%v", 95 | resAccessToken.ErrCode, resAccessToken.ErrMsg) 96 | return 97 | } 98 | 99 | accessTokenCacheKey := fmt.Sprintf("access_token_%s", ctx.Config.AppID) 100 | expires := resAccessToken.ExpiresIn - 1500 101 | err = ctx.Cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second) 102 | return 103 | } 104 | 105 | func (ctx *Context) GetJsTicket() (jsTicket string, err error) { 106 | return ctx.getTicket(jsTicketUrl,jsTickctTemp) 107 | /* 108 | ctx.jsAPITicketLock.Lock() 109 | defer ctx.jsAPITicketLock.Unlock() 110 | 111 | jsTicketCacheKey := fmt.Sprintf("js_ticket_%s", ctx.Config.AppID) 112 | val := ctx.Cache.Get(jsTicketCacheKey) 113 | if val != nil { 114 | jsTicket = val.(string) 115 | return 116 | } 117 | 118 | //从微信服务器获取 119 | var resJsTicket *utils.ResJsTicket 120 | resJsTicket, err = ctx.GetJsTicketFromServer() 121 | if err != nil { 122 | return 123 | } 124 | 125 | jsTicket = resJsTicket.Ticket 126 | return 127 | */ 128 | } 129 | 130 | func (ctx *Context) GetCardTicket() (jsTicket string, err error) { 131 | return ctx.getTicket(cardTicketUrl, cardTickctTemp) 132 | } 133 | 134 | /* 135 | func (ctx *Context) GetJsTicketFromServer() (resJsTicket *utils.ResJsTicket, err error) { 136 | var token string 137 | token, err = ctx.GetAccessToken() 138 | if err != nil { 139 | return 140 | } 141 | 142 | url := fmt.Sprintf(jsTicketUrl, token) 143 | var jsticket utils.ResJsTicket 144 | resJsTicket = &jsticket 145 | var body []byte 146 | body, err = utils.HTTPGet(url) 147 | err = json.Unmarshal(body, &jsticket) 148 | if err != nil { 149 | return 150 | } 151 | if resJsTicket.ErrCode != 0 || resJsTicket.ErrMsg != "ok" { 152 | err = fmt.Errorf("get access_token error : errcode=%v , errormsg=%v", 153 | resJsTicket.ErrCode, resJsTicket.ErrMsg) 154 | return 155 | } 156 | 157 | jsTicketCacheKey := fmt.Sprintf("js_ticket_%s", ctx.Config.AppID) 158 | expires := resJsTicket.ExpiresIn - 1500 159 | err = ctx.Cache.Set(jsTicketCacheKey, resJsTicket.Ticket, time.Duration(expires)*time.Second) 160 | return 161 | } 162 | */ 163 | 164 | 165 | func (ctx *Context) getTicket(urlTemp, keyTemp string) (jsTicket string, err error) { 166 | ctx.jsAPITicketLock.Lock() 167 | defer ctx.jsAPITicketLock.Unlock() 168 | 169 | jsTicketCacheKey := fmt.Sprintf(keyTemp, ctx.Config.AppID) 170 | val := ctx.Cache.Get(jsTicketCacheKey) 171 | if val != nil { 172 | jsTicket = val.(string) 173 | return 174 | } 175 | 176 | //从微信服务器获取 177 | var resJsTicket *utils.ResJsTicket 178 | resJsTicket, err = ctx.getTicketFromServer(urlTemp,keyTemp) 179 | if err != nil { 180 | return 181 | } 182 | 183 | jsTicket = resJsTicket.Ticket 184 | return 185 | } 186 | 187 | func (ctx *Context) getTicketFromServer(urlTemp, keyTemp string) (resJsTicket *utils.ResJsTicket, err error,) { 188 | var token string 189 | token, err = ctx.GetAccessToken() 190 | if err != nil { 191 | return 192 | } 193 | 194 | url := fmt.Sprintf(urlTemp, token) 195 | var jsticket utils.ResJsTicket 196 | resJsTicket = &jsticket 197 | 198 | body, _, err := utils.HTTPGet(url) 199 | err = json.Unmarshal(body, &jsticket) 200 | if err != nil { 201 | return 202 | } 203 | if resJsTicket.ErrCode != 0 || resJsTicket.ErrMsg != "ok" { 204 | err = fmt.Errorf("get access_token error : errcode=%v , errormsg=%v", 205 | resJsTicket.ErrCode, resJsTicket.ErrMsg) 206 | return 207 | } 208 | 209 | jsTicketCacheKey := fmt.Sprintf(keyTemp, ctx.Config.AppID) 210 | expires := resJsTicket.ExpiresIn - 1500 211 | err = ctx.Cache.Set(jsTicketCacheKey, resJsTicket.Ticket, time.Duration(expires)*time.Second) 212 | return 213 | } 214 | -------------------------------------------------------------------------------- /mp/mass.go: -------------------------------------------------------------------------------- 1 | package mp 2 | 3 | import ( 4 | "github.com/qjw/go-wx-sdk/utils" 5 | "bytes" 6 | "fmt" 7 | "html/template" 8 | ) 9 | 10 | //-----------------------------------群发-------------------------------------------------------------------------------- 11 | const ( 12 | mass_preview = "https://api.weixin.qq.com/cgi-bin/message/mass/preview?access_token=%s" 13 | mass_send = "https://api.weixin.qq.com/cgi-bin/message/mass/send?access_token=%s" 14 | mass_get = "https://api.weixin.qq.com/cgi-bin/message/mass/get?access_token=%s" 15 | mass_delete = "https://api.weixin.qq.com/cgi-bin/message/mass/delete?access_token=%s" 16 | mass_sendall = "https://api.weixin.qq.com/cgi-bin/message/mass/sendall?access_token=%s" 17 | ) 18 | 19 | const ( 20 | mass_preview_temp = `{ 21 | "touser":"%s", 22 | "%s":{ 23 | "media_id":"%s" 24 | }, 25 | "msgtype":"%s" 26 | }` 27 | mass_preview_txt_temp = `{ 28 | "touser":"%s", 29 | "text":{ 30 | "content":"%s" 31 | }, 32 | "msgtype":"text" 33 | }` 34 | mass_send_mpnews_temp = `{ 35 | "touser":[ 36 | {{range $index, $element := .OpenIDList}} 37 | {{if $index}},{{end}} 38 | "{{$element}}" 39 | {{end}} 40 | ], 41 | "mpnews":{ 42 | "media_id":"{{.MediaID}}" 43 | }, 44 | "msgtype":"mpnews", 45 | "send_ignore_reprint":{{.SendIgnoreReprint}} 46 | }` 47 | mass_send_txt_temp = `{ 48 | "touser":[ 49 | {{range $index, $element := .OpenIDList}} 50 | {{if $index}},{{end}} 51 | "{{$element}}" 52 | {{end}} 53 | ], 54 | "msgtype": "text", 55 | "text": { "content": "{{.Content}}"} 56 | }` 57 | mass_send_card_temp = `{ 58 | "touser":[ 59 | {{range $index, $element := .OpenIDList}} 60 | {{if $index}},{{end}} 61 | "{{$element}}" 62 | {{end}} 63 | ], 64 | "msgtype": "wxcard", 65 | "wxcard": { "card_id": "{{.CardID}}"} 66 | }` 67 | mass_send_msg_temp = `{ 68 | "touser":[ 69 | {{range $index, $element := .OpenIDList}} 70 | {{if $index}},{{end}} 71 | "{{$element}}" 72 | {{end}} 73 | ], 74 | "{{.Tp}}":{ 75 | "media_id":"{{.MediaID}}" 76 | }, 77 | "msgtype":"{{.Tp}}" 78 | }` 79 | mass_get_temp = `{ 80 | "msg_id": %d 81 | }` 82 | 83 | mass_sendall_mpnews_temp = `{ 84 | "filter":{ 85 | "is_to_all":%t, 86 | "tag_id":%d 87 | }, 88 | "mpnews":{ 89 | "media_id":"%s" 90 | }, 91 | "msgtype":"mpnews", 92 | "send_ignore_reprint":%d 93 | }` 94 | mass_sendall_text = `{ 95 | "filter":{ 96 | "is_to_all":%t, 97 | "tag_id":%d 98 | }, 99 | "text":{ 100 | "content":"%s" 101 | }, 102 | "msgtype":"text" 103 | }` 104 | mass_sendall_msg = `{ 105 | "filter":{ 106 | "is_to_all":%t, 107 | "tag_id":%d 108 | }, 109 | "%s":{ 110 | "media_id":"%s" 111 | }, 112 | "msgtype":"%s" 113 | }` 114 | mass_sendall_card = `{ 115 | "filter":{ 116 | "is_to_all":%t, 117 | "tag_id":%d 118 | }, 119 | "wxcard":{ 120 | "card_id":"%s" 121 | }, 122 | "msgtype":"wxcard" 123 | 124 | }` 125 | ) 126 | 127 | type MassPreviewRes struct { 128 | utils.CommonError 129 | MsgID string `json:"msg_id"` 130 | } 131 | 132 | func (this WechatApi) MassPreviewMsg(touser, media_id, tp string) (*MassPreviewRes, error) { 133 | var res MassPreviewRes 134 | body := fmt.Sprintf(mass_preview_temp, touser, tp, media_id, tp) 135 | if err := this.DoPost(mass_preview, body, &res); err == nil { 136 | return &res, nil 137 | } else { 138 | return nil, err 139 | } 140 | } 141 | 142 | func (this WechatApi) MassPreviewText(touser, content string) (*MassPreviewRes, error) { 143 | var res MassPreviewRes 144 | body := fmt.Sprintf(mass_preview_txt_temp, touser, content) 145 | if err := this.DoPost(mass_preview, body, &res); err == nil { 146 | return &res, nil 147 | } else { 148 | return nil, err 149 | } 150 | } 151 | 152 | type MassSendRes struct { 153 | utils.CommonError 154 | MsgID int64 `json:"msg_id"` 155 | MsgDataID int64 `json:"msg_data_id"` 156 | } 157 | 158 | func (this WechatApi) MassSendMpnews(tousers []string, media_id string, send_ignore_reprint int) (*MassSendRes, error) { 159 | var res MassSendRes 160 | ttt := template.New("MassSendMpnews") 161 | ttt.Parse(mass_send_mpnews_temp) 162 | var buf bytes.Buffer 163 | ttt.Execute(&buf, struct { 164 | OpenIDList []string 165 | MediaID string 166 | SendIgnoreReprint int 167 | }{ 168 | MediaID: media_id, 169 | OpenIDList: tousers, 170 | SendIgnoreReprint: send_ignore_reprint, 171 | }) 172 | 173 | if err := this.DoPost(mass_send, buf.String(), &res); err == nil { 174 | return &res, nil 175 | } else { 176 | return nil, err 177 | } 178 | } 179 | 180 | func (this WechatApi) MassSendText(tousers []string, content string) (*MassSendRes, error) { 181 | var res MassSendRes 182 | ttt := template.New("MassSendText") 183 | ttt.Parse(mass_send_txt_temp) 184 | var buf bytes.Buffer 185 | ttt.Execute(&buf, struct { 186 | OpenIDList []string 187 | Content string 188 | }{ 189 | Content: content, 190 | OpenIDList: tousers, 191 | }) 192 | 193 | if err := this.DoPost(mass_send, buf.String(), &res); err == nil { 194 | return &res, nil 195 | } else { 196 | return nil, err 197 | } 198 | } 199 | 200 | func (this WechatApi) MassSendCard(tousers []string, card_id string) (*MassSendRes, error) { 201 | var res MassSendRes 202 | ttt := template.New("MassSendText") 203 | ttt.Parse(mass_send_card_temp) 204 | var buf bytes.Buffer 205 | ttt.Execute(&buf, struct { 206 | OpenIDList []string 207 | CardID string 208 | }{ 209 | CardID: card_id, 210 | OpenIDList: tousers, 211 | }) 212 | 213 | if err := this.DoPost(mass_send, buf.String(), &res); err == nil { 214 | return &res, nil 215 | } else { 216 | return nil, err 217 | } 218 | } 219 | 220 | // voice/image 221 | func (this WechatApi) MassSendMsg(tousers []string, media_id string, tp string) (*MassSendRes, error) { 222 | var res MassSendRes 223 | ttt := template.New("MassSendMsg") 224 | ttt.Parse(mass_send_msg_temp) 225 | var buf bytes.Buffer 226 | ttt.Execute(&buf, struct { 227 | OpenIDList []string 228 | MediaID string 229 | Tp string 230 | }{ 231 | MediaID: media_id, 232 | OpenIDList: tousers, 233 | Tp: tp, 234 | }) 235 | 236 | if err := this.DoPost(mass_send, buf.String(), &res); err == nil { 237 | return &res, nil 238 | } else { 239 | return nil, err 240 | } 241 | } 242 | 243 | type MassGetResult struct { 244 | utils.CommonError 245 | MsgID int64 `json:"msg_id"` 246 | MsgStatus string `json:"msg_status"` 247 | } 248 | 249 | func (this WechatApi) MassGet(msg_id int64) (*MassGetResult, error) { 250 | var res MassGetResult 251 | body := fmt.Sprintf(mass_get_temp, msg_id) 252 | if err := this.DoPost(mass_get, body, &res); err == nil { 253 | return &res, nil 254 | } else { 255 | return nil, err 256 | } 257 | } 258 | 259 | func (this WechatApi) MassDelete(msg_id int64) (*utils.CommonError, error) { 260 | var res utils.CommonError 261 | body := fmt.Sprintf(mass_get_temp, msg_id) 262 | if err := this.DoPost(mass_delete, body, &res); err == nil { 263 | return &res, nil 264 | } else { 265 | return nil, err 266 | } 267 | } 268 | 269 | func (this WechatApi) MassSendAllMpnews(media_id string, tag_id int, 270 | is_to_all bool, send_ignore_reprint int) (*MassSendRes, error) { 271 | var res MassSendRes 272 | body := fmt.Sprintf(mass_sendall_mpnews_temp, is_to_all, tag_id, media_id, send_ignore_reprint) 273 | if err := this.DoPost(mass_sendall, body, &res); err == nil { 274 | return &res, nil 275 | } else { 276 | return nil, err 277 | } 278 | } 279 | 280 | func (this WechatApi) MassSendAllText(content string, tag_id int, is_to_all bool) (*MassSendRes, error) { 281 | var res MassSendRes 282 | body := fmt.Sprintf(mass_sendall_text, is_to_all, tag_id, content) 283 | if err := this.DoPost(mass_sendall, body, &res); err == nil { 284 | return &res, nil 285 | } else { 286 | return nil, err 287 | } 288 | } 289 | 290 | func (this WechatApi) MassSendAllCard(card_id string, tag_id int, is_to_all bool) (*MassSendRes, error) { 291 | var res MassSendRes 292 | body := fmt.Sprintf(mass_sendall_card, is_to_all, tag_id, card_id) 293 | if err := this.DoPost(mass_sendall, body, &res); err == nil { 294 | return &res, nil 295 | } else { 296 | return nil, err 297 | } 298 | } 299 | 300 | func (this WechatApi) MassSendAllMsg(media_id string, tag_id int, is_to_all bool, tp string) (*MassSendRes, error) { 301 | var res MassSendRes 302 | body := fmt.Sprintf(mass_sendall_msg, is_to_all, tag_id, tp, media_id, tp) 303 | if err := this.DoPost(mass_sendall, body, &res); err == nil { 304 | return &res, nil 305 | } else { 306 | return nil, err 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /mp/material.go: -------------------------------------------------------------------------------- 1 | package mp 2 | 3 | import ( 4 | "github.com/qjw/go-wx-sdk/utils" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | ) 9 | 10 | //-----------------------------------素材-------------------------------------------------------------------------------- 11 | 12 | const ( 13 | get_material_count = "https://api.weixin.qq.com/cgi-bin/material/get_materialcount?access_token=%s" 14 | batch_get_material = "https://api.weixin.qq.com/cgi-bin/material/batchget_material?access_token=%s" 15 | media_upload = "https://api.weixin.qq.com/cgi-bin/media/upload?access_token=%s&type=%s" 16 | media_get = "https://api.weixin.qq.com/cgi-bin/media/get?access_token=%s&media_id=%s" 17 | mediaVideoGet = "http://api.weixin.qq.com/cgi-bin/media/get?access_token=%s&media_id=%s" 18 | add_material = "https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=%s&type=%s" 19 | get_material = "https://api.weixin.qq.com/cgi-bin/material/get_material?access_token=%s" 20 | del_material = "https://api.weixin.qq.com/cgi-bin/material/del_material?access_token=%s" 21 | media_uploadimg = "https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token=%s" 22 | add_news = "https://api.weixin.qq.com/cgi-bin/material/add_news?access_token=%s" 23 | ) 24 | 25 | const ( 26 | get_material_temp = `{ 27 | "media_id":"%s" 28 | }` 29 | batchgetMaterialTemp = `{"type":"%s","offset":%d,"count":%d}` 30 | ) 31 | 32 | 33 | type MaterialCount struct { 34 | VoiceCount int `json:"voice_count"` 35 | VideoCount int `json:"video_count"` 36 | ImageCount int `json:"image_count"` 37 | NewsCount int `json:"news_count"` 38 | } 39 | 40 | func (this WechatApi) GetMaterialCount() (*MaterialCount, error) { 41 | var res MaterialCount 42 | if err := this.DoGet(get_material_count, &res); err == nil { 43 | return &res, nil 44 | } else { 45 | return nil, err 46 | } 47 | } 48 | 49 | type Material struct { 50 | TotalCount int `json:"total_count"` 51 | ItemCount int `json:"item_count"` 52 | Items []struct { 53 | MediaID string `json:"media_id"` 54 | Name string `json:"name"` 55 | UpdateTime int64 `json:"update_time"` 56 | Url string `json:"url"` 57 | } `json:"item"` 58 | } 59 | 60 | func (this WechatApi) GetMaterials(ty string, offset int, count int) (*Material, error) { 61 | var res Material 62 | body := fmt.Sprintf(batchgetMaterialTemp, ty, offset, count) 63 | if err := this.DoPost(batch_get_material, body, &res); err == nil { 64 | return &res, nil 65 | } else { 66 | return nil, err 67 | } 68 | } 69 | 70 | type NewsMaterial struct { 71 | TotalCount int `json:"total_count"` 72 | ItemCount int `json:"item_count"` 73 | Items []struct { 74 | MediaID string `json:"media_id"` 75 | UpdateTime int64 `json:"update_time"` 76 | Content struct { 77 | NewsItem []struct { 78 | Title string `json:"title"` 79 | ThumbMediaID string `json:"thumb_media_id"` 80 | ShowCoverPic int `json:"show_cover_pic"` 81 | Author string `json:"author"` 82 | Digest string `json:"digest"` 83 | Content string `json:"content"` 84 | Url string `json:"url"` 85 | ContentSourceUrl string `json:"content_source_url"` 86 | NeedOpenComment int `json:"need_open_comment"` 87 | OnlyFansCanComment int `json:"only_fans_can_comment"` 88 | ThumbUrl string `json:"thumb_url"` 89 | } `json:"news_item"` 90 | } `json:"content"` 91 | } `json:"item"` 92 | } 93 | 94 | func (this WechatApi) GetNewsMaterials(offset int, count int) (*NewsMaterial, error) { 95 | var res NewsMaterial 96 | body := fmt.Sprintf(batchgetMaterialTemp, "news", offset, count) 97 | if err := this.DoPost(batch_get_material, body, &res); err == nil { 98 | return &res, nil 99 | } else { 100 | return nil, err 101 | } 102 | } 103 | 104 | type UploadRes struct { 105 | utils.CommonError 106 | Type string `json:"type"` 107 | MediaID string `json:"media_id"` 108 | ThumbMediaID string `json:"thumb_media_id"` 109 | CreatedAt int64 `json:"created_at"` 110 | } 111 | 112 | func (this WechatApi) UploadTmpMaterial(reader io.Reader, filename, tp string) (*UploadRes, error) { 113 | var res UploadRes 114 | if err := this.DoPostFile(reader, "media", filename, &res, media_upload, tp); err == nil { 115 | return &res, nil 116 | } else { 117 | return nil, err 118 | } 119 | } 120 | 121 | func (this WechatApi) GetTmpMaterial(media_id string) (string, error) { 122 | accessToken, err := this.Context.GetAccessToken() 123 | if err != nil { 124 | return "", err 125 | } 126 | return string(fmt.Sprintf(media_get, accessToken, media_id)), nil 127 | } 128 | 129 | type VideoTmpMaterialRes struct { 130 | VideoUrl string `json:"video_url"` 131 | } 132 | 133 | func (this WechatApi) GetVideoTmpMaterial(media_id string) (*VideoTmpMaterialRes, error) { 134 | var res VideoTmpMaterialRes 135 | if err := this.DoGet(mediaVideoGet, &res, media_id); err == nil { 136 | return &res, nil 137 | } else { 138 | return nil, err 139 | } 140 | } 141 | 142 | func (this WechatApi) UploadMaterial(reader io.Reader, filename, tp string) (*UploadRes, error) { 143 | var res UploadRes 144 | if err := this.DoPostFile(reader, "media", filename, &res, add_material, tp); err == nil { 145 | return &res, nil 146 | } else { 147 | return nil, err 148 | } 149 | } 150 | 151 | type VideoMaterialInfo struct { 152 | Description string `json:"introduction"` 153 | Title string `json:"title"` 154 | } 155 | 156 | func (this WechatApi) UploadVideoMaterial(reader io.Reader, 157 | filename string, info *VideoMaterialInfo) (*UploadRes, error) { 158 | var res UploadRes 159 | if err := this.DoPostFileExtra(reader, "media", "description", filename, info, 160 | &res, add_material, "video"); err == nil { 161 | return &res, nil 162 | } else { 163 | return nil, err 164 | } 165 | } 166 | 167 | func (this WechatApi) GetMaterial() (string, error) { 168 | accessToken, err := this.Context.GetAccessToken() 169 | if err != nil { 170 | return "", err 171 | } 172 | return string(fmt.Sprintf(get_material, accessToken)), nil 173 | } 174 | 175 | type VideoMaterialRes struct { 176 | DownUrl string `json:"down_url"` 177 | Title string `json:"title"` 178 | Description string `json:"description"` 179 | } 180 | 181 | func (this WechatApi) GetVideoMaterial(media_id string) (*VideoMaterialRes, error) { 182 | var res VideoMaterialRes 183 | body := fmt.Sprintf(get_material_temp, media_id) 184 | if err := this.DoPost(get_material, body, &res); err == nil { 185 | return &res, nil 186 | } else { 187 | return nil, err 188 | } 189 | } 190 | 191 | func (this WechatApi) DeleteMaterial(media_id string) (*utils.CommonError, error) { 192 | var res utils.CommonError 193 | body := fmt.Sprintf(get_material_temp, media_id) 194 | if err := this.DoPost(del_material, body, &res); err == nil { 195 | return &res, nil 196 | } else { 197 | return nil, err 198 | } 199 | } 200 | 201 | type ArticleImageRes struct { 202 | Url string `json:"url"` 203 | } 204 | 205 | func (this WechatApi) UploadArticleImage(reader io.Reader, filename string) (*ArticleImageRes, error) { 206 | var res ArticleImageRes 207 | if err := this.DoPostFile(reader, "media", filename, &res, media_uploadimg); err == nil { 208 | return &res, nil 209 | } else { 210 | return nil, err 211 | } 212 | } 213 | 214 | type ArticleCreateEntry struct { 215 | Title string `json:"title" doc:"图文消息的标题"` 216 | ThumbMediaID string `json:"thumb_media_id" doc:"图文消息的封面图片素材id(必须是永久mediaID)"` 217 | Author string `json:"author" doc:"作者"` 218 | Digest string `json:"digest" doc:"图文消息的摘要,仅有单图文消息才有摘要,多图文此处为空"` 219 | ShowCoverPic int8 `json:"show_cover_pic" doc:"是否显示封面,0为false,即不显示,1为true,即显示"` 220 | Content string `json:"content"` 221 | ContentSourceUrl string `json:"content_source_url" doc:"图文消息的原文地址,即点击“阅读原文”后的URL"` 222 | Url string `json:"url"` 223 | } 224 | 225 | type ArticleCreate struct { 226 | Articles []ArticleCreateEntry `json:"articles"` 227 | } 228 | 229 | type ArticleCreateRes struct { 230 | MediaID string `json:"media_id"` 231 | } 232 | 233 | func (this WechatApi) CreateArticle(articles []ArticleCreateEntry) (*ArticleCreateRes, error) { 234 | var res ArticleCreateRes 235 | bodyObj := ArticleCreate{ 236 | Articles: articles, 237 | } 238 | body, _ := json.Marshal(&bodyObj) 239 | if err := this.DoPostRaw(add_news, body, &res); err == nil { 240 | return &res, nil 241 | } else { 242 | return nil, err 243 | } 244 | } 245 | 246 | 247 | type ArticleDetail struct { 248 | NewsItem []ArticleCreateEntry `json:"news_item"` 249 | } 250 | 251 | func (this WechatApi) GetArticleMaterial(media_id string) (*ArticleDetail, error) { 252 | var res ArticleDetail 253 | body := fmt.Sprintf(get_material_temp, media_id) 254 | if err := this.DoPost(get_material, body, &res); err == nil { 255 | return &res, nil 256 | } else { 257 | return nil, err 258 | } 259 | } -------------------------------------------------------------------------------- /mp/message.go: -------------------------------------------------------------------------------- 1 | package mp 2 | 3 | import ( 4 | "github.com/qjw/go-wx-sdk/utils" 5 | "encoding/xml" 6 | ) 7 | 8 | //MixMessage 存放所有微信发送过来的消息和事件 9 | type MixMessage struct { 10 | MixCommonToken 11 | 12 | //基本消息 13 | MsgID int64 `xml:"MsgId,omitempty" json:"msg_id,omitempty"` 14 | Content string `xml:"Content,omitempty" json:"content,omitempty"` 15 | PicURL string `xml:"PicUrl,omitempty" json:"pic_url,omitempty"` 16 | MediaID string `xml:"MediaId,omitempty" json:"media_id,omitempty"` 17 | Format string `xml:"Format,omitempty" json:"format,omitempty"` 18 | ThumbMediaID string `xml:"ThumbMediaId,omitempty" json:"thumb_media_id,omitempty"` 19 | LocationX float64 `xml:"Location_X,omitempty" json:"location_x,omitempty"` 20 | LocationY float64 `xml:"Location_Y,omitempty" json:"location_y,omitempty"` 21 | Scale float64 `xml:"Scale,omitempty" json:"scale,omitempty"` 22 | Label string `xml:"Label,omitempty" json:"label,omitempty"` 23 | Title string `xml:"Title,omitempty" json:"title,omitempty"` 24 | Description string `xml:"Description,omitempty" json:"description,omitempty"` 25 | URL string `xml:"Url,omitempty" json:"url,omitempty"` 26 | 27 | //事件相关 28 | Event utils.EventType `xml:"Event,omitempty" json:"event,omitempty"` 29 | EventKey string `xml:"EventKey,omitempty" json:"event_key,omitempty"` 30 | Ticket string `xml:"Ticket,omitempty" json:"ticket,omitempty"` 31 | Latitude string `xml:"Latitude,omitempty" json:"latitude,omitempty"` 32 | Longitude string `xml:"Longitude,omitempty" json:"longitude,omitempty"` 33 | Precision string `xml:"Precision,omitempty,omitempty" json:"precision,omitempty"` 34 | MenuID string `xml:"MenuId,omitempty,omitempty" json:"menu_id,omitempty"` 35 | 36 | ScanCodeInfo *struct { 37 | ScanType string `xml:"ScanType,omitempty" json:"scan_type,omitempty"` 38 | ScanResult string `xml:"ScanResult,omitempty" json:"scan_result,omitempty"` 39 | } `xml:"ScanCodeInfo,omitempty" json:"scan_code_info,omitempty"` 40 | 41 | //SendPicsInfo struct { 42 | // Count int32 `xml:"Count,omitempty" json:"count,omitempty"` 43 | // PicList []EventPic `xml:"PicList>item"` 44 | //} `xml:"SendPicsInfo"` 45 | 46 | //SendLocationInfo struct { 47 | // LocationX float64 `xml:"Location_X,omitempty" json:"location_x,omitempty"` 48 | // LocationY float64 `xml:"Location_Y,omitempty" json:"location_y,omitempty"` 49 | // Scale float64 `xml:"Scale,omitempty" json:"scale,omitempty"` 50 | // Label string `xml:"Label,omitempty" json:"label,omitempty"` 51 | // Poiname string `xml:"Poiname" json:"poiname,omitempty"` 52 | //} 53 | 54 | CardEvent 55 | } 56 | 57 | type CardEvent struct { 58 | // 卡券 审核事件推送 @card_pass_check/card_not_pass_check 59 | CardId string `xml:"CardId,omitempty" json:"card_id,omitempty"` 60 | RefuseReason string `xml:"RefuseReason,omitempty" json:"refuse_reason,omitempty"` 61 | 62 | // 卡券 领取事件推送 @user_get_card 63 | IsGiveByFriend int `xml:"IsGiveByFriend,omitempty" json:"is_give_by_friend,omitempty" doc:"是否为转赠领取,1代表是,0代表否。"` 64 | UserCardCode string `xml:"UserCardCode,omitempty" json:"user_card_code,omitempty" doc:"code序列号"` 65 | FriendUserName string `xml:"FriendUserName,omitempty" json:"friend_username,omitempty" doc:"当IsGiveByFriend为1时填入的字段,表示发起转赠用户的openid"` 66 | OuterId int `xml:"OuterId,omitempty" json:"outer_id,omitempty"` 67 | OldUserCardCode string `xml:"OldUserCardCode,omitempty" json:"old_user_card_code,omitempty" doc:"为保证安全,微信会在转赠发生后变更该卡券的code号,该字段表示转赠前的code。"` 68 | OuterStr string `xml:"OuterStr,omitempty" json:"outer_str,omitempty"` 69 | IsRestoreMemberCard int `xml:"IsRestoreMemberCard,omitempty" json:"is_restore_member_card,omitempty"` 70 | IsRecommendByFriend int `xml:"IsRecommendByFriend,omitempty" json:"is_recommend_by_friend,omitempty" doc:"用户删除会员卡后可重新找回,当用户本次操作为找回时,该值为1,否则为0"` 71 | 72 | // 卡券 转赠事件推送 @user_gifting_card 73 | IsReturnBack int `xml:"IsReturnBack,omitempty" json:"is_return_back,omitempty" doc:"是否转赠退回,0代表不是,1代表是。"` 74 | IsChatRoom int `xml:"IsChatRoom,omitempty" json:"is_chatroom,omitempty" doc:"是否是群转赠"` 75 | 76 | // 卡券 删除事件推送 @user_del_card 77 | // 卡券 核销事件推送 @user_consume_card 78 | ConsumeSource string `xml:"ConsumeSource,omitempty" json:"consume_source,omitempty" doc:"核销来源。支持开发者统计API核销(FROM_API)、公众平台核销(FROM_MP)、卡券商户助手核销(FROM_MOBILE_HELPER)(核销员微信号)"` 79 | LocationName string `xml:"LocationName,omitempty" json:"location_name,omitempty" doc:"门店名称,当前卡券核销的门店名称(只有通过自助核销和买单核销时才会出现该字段)"` 80 | StaffOpenId string `xml:"StaffOpenId,omitempty" json:"staff_openId,omitempty" doc:"核销该卡券核销员的openid(只有通过卡券商户助手核销时才会出现)"` 81 | VerifyCode string `xml:"VerifyCode,omitempty" json:"verify_code,omitempty" doc:"自助核销时,用户输入的验证码"` 82 | RemarkAmount string `xml:"RemarkAmount,omitempty" json:"remark_amount,omitempty" doc:"自助核销时,用户输入的备注金额"` 83 | 84 | // 卡券 买单事件推送 @user_pay_from_pay_cell 85 | TransId string `xml:"TransId,omitempty" json:"trans_id,omitempty" doc:"微信支付交易订单号(只有使用买单功能核销的卡券才会出现)"` 86 | LocationId int `xml:"LocationId,omitempty" json:"location_id,omitempty" doc:"门店ID,当前卡券核销的门店ID(只有通过卡券商户助手和买单核销时才会出现)"` 87 | Fee int `xml:"Fee" json:"fee,omitempty" doc:"实付金额,单位为分"` 88 | OriginalFee int `xml:"OriginalFee,omitempty" json:"original_fee,omitempty" doc:"应付金额,单位为分"` 89 | 90 | // 卡券 进入会员卡事件推送 @user_view_card 91 | // 从卡券进入公众号会话事件推送 @user_enter_session_from_card 92 | // 会员卡内容更新事件 @update_member_card 93 | ModifyBonus int `xml:"ModifyBonus,omitempty" json:"modify_bonus,omitempty" doc:"变动的积分值。"` 94 | ModifyBalance int `xml:"ModifyBalance,omitempty" json:"modify_balance,omitempty" doc:"变动的余额值。"` 95 | 96 | // 库存报警事件 @card_sku_remind 97 | Detail string `xml:"Detail,omitempty" json:"detail,omitempty" doc:"报警详细信息"` 98 | 99 | // 券点流水详情事件 @card_pay_order 100 | // 会员卡激活事件推送 @submit_membercard_user_info 101 | } 102 | 103 | //EventPic 发图事件推送 104 | type EventPic struct { 105 | PicMd5Sum string `xml:"PicMd5Sum"` 106 | } 107 | 108 | // CommonToken 消息中通用的结构 109 | type MixCommonToken struct { 110 | XMLName xml.Name `xml:"xml" json:"-"` 111 | ToUserName string `xml:"ToUserName" json:"to_username"` 112 | FromUserName string `xml:"FromUserName" json:"from_username"` 113 | CreateTime int64 `xml:"CreateTime" json:"create_time"` 114 | MsgType utils.MsgType `xml:"MsgType" json:"msg_type"` 115 | } 116 | -------------------------------------------------------------------------------- /mp/oauth2.go: -------------------------------------------------------------------------------- 1 | package mp 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "errors" 7 | "encoding/json" 8 | "github.com/qjw/go-wx-sdk/utils" 9 | ) 10 | 11 | const ( 12 | oauth2_authorize = "https://open.weixin.qq.com/connect/oauth2/authorize" 13 | oauth2_access_token = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code" 14 | oauth2_sns_userinfo = "https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s&lang=zh_CN" 15 | ) 16 | 17 | func (this WechatApi) authorizeUrl(redirectURI, scope, state string) string { 18 | return oauth2_authorize + "?appid=" + url.QueryEscape(this.Context.Config.AppID) + 19 | "&redirect_uri=" + url.QueryEscape(redirectURI) + 20 | "&response_type=code&scope=" + url.QueryEscape(scope) + 21 | "&state=" + url.QueryEscape(state) + 22 | "#wechat_redirect" 23 | } 24 | 25 | func (this WechatApi) AuthorizeUserinfo(redirectURI, state string) string { 26 | return this.authorizeUrl(redirectURI, "snsapi_userinfo", state) 27 | } 28 | 29 | func (this WechatApi) AuthorizeBase(redirectURI, state string) string { 30 | return this.authorizeUrl(redirectURI, "snsapi_base", state) 31 | } 32 | 33 | type SnsUserinfo struct { 34 | utils.CommonError 35 | Openid string `json:"openid"` 36 | Nickname string `json:"nickname"` 37 | Sex int `json:"sex"` 38 | Province string `json:"province"` 39 | City string `json:"city"` 40 | Country string `json:"country"` 41 | Headimgurl string `json:"headimgurl"` 42 | Unionid string `json:"unionid"` 43 | Privilege []string `json:"privilege"` 44 | } 45 | 46 | func (this WechatApi) GetAuthorizeSnsUserinfo(access_token, openid string) (*SnsUserinfo, error) { 47 | var res SnsUserinfo 48 | uri := fmt.Sprintf(oauth2_sns_userinfo, access_token, openid) 49 | 50 | response, _, err := utils.HTTPGet(uri) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | err = json.Unmarshal(response, &res) 56 | if err != nil { 57 | return nil, errors.New(string(response)) 58 | } 59 | return &res, nil 60 | } 61 | 62 | type AuthorizeAccessToken struct { 63 | utils.CommonError 64 | AccessToken string `json:"access_token"` 65 | ExpiresIn int64 `json:"expires_in"` 66 | RefreshToken string `json:"refresh_token"` 67 | OpenID string `json:"openid"` 68 | Scope string `json:"scope"` 69 | Unionid string `json:"unionid"` 70 | } 71 | 72 | func (this WechatApi) GetAuthorizeAccessToken(code string) (*AuthorizeAccessToken, error) { 73 | var res AuthorizeAccessToken 74 | uri := fmt.Sprintf(oauth2_access_token, 75 | this.Context.Config.AppID, 76 | this.Context.Config.AppSecret, 77 | code, 78 | ) 79 | 80 | response, _, err := utils.HTTPGet(uri) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | err = json.Unmarshal(response, &res) 86 | if err != nil { 87 | return nil, errors.New(string(response)) 88 | } 89 | return &res, nil 90 | } 91 | -------------------------------------------------------------------------------- /mp/poi.go: -------------------------------------------------------------------------------- 1 | package mp 2 | 3 | import "github.com/qjw/go-wx-sdk/utils" 4 | 5 | const ( 6 | getPoiList = "https://api.weixin.qq.com/cgi-bin/poi/getpoilist?access_token=%s" 7 | getPoi = "http://api.weixin.qq.com/cgi-bin/poi/getpoi?access_token=%s" 8 | getWxCategory = "http://api.weixin.qq.com/cgi-bin/poi/getwxcategory?access_token=%s" 9 | ) 10 | 11 | type PoiObj struct { 12 | Sid string `json:"sid,omitempty" doc:"商户自己的id,用于后续审核通过收到poi_id 的通知时,做对应关系。请商户自己保证唯一识别性"` 13 | Business_name string `json:"business_name" doc:"门店名称(仅为商户名,如:国美、麦当劳,不应包含地区、地址、分店名等信息,错误示例:北京国美),不能为空,15个汉字或30个英文字符内"` 14 | Branch_name string `json:"branch_name,omitempty" doc:"分店名称(不应包含地区信息,不应与门店名有重复,错误示例:北京王府井店)20个字以内"` 15 | Address string `json:"address" doc:"门店所在的详细街道地址(不要填写省市信息)"` 16 | Telephone string `json:"telephone" doc:"门店的电话(纯数字,区号、分机号均由“-”隔开)"` 17 | City string `json:"city" doc:"门店所在的城市,10个字以内"` 18 | Introduction string `json:"introduction" doc:"商户简介,主要介绍商户信息等 300字以内"` 19 | Province string `json:"province" doc:"门店所在的省份(直辖市填城市名,如:北京市)10个字以内"` 20 | District string `json:"district" doc:"门店所在地区,10个字以内"` 21 | 22 | Recommend string `json:"recommend" doc:"推荐品,餐厅可为推荐菜;酒店为推荐套房;景点为推荐游玩景点等,针对自己行业的推荐内容200字以内"` 23 | Special string `json:"special" doc:"特色服务,如免费wifi,免费停车,送货上门等商户能提供的特色功能或服务"` 24 | OpenTime string `json:"open_time" doc:"营业时间,24 小时制表示,用“-”连接,如 8:00-20:00"` 25 | PoiID string `json:"poi_id" doc:"微信内部的id"` 26 | Categories []string `json:"categories" doc:"门店的类型(不同级分类用“,”隔开,如:美食,川菜,火锅。详细分类参见附件:微信门店类目表)"` 27 | OffsetType int `json:"offset_type" doc:"坐标类型:1 为火星坐标,2 为sogou经纬度,3 为百度经纬度,4 为mapbar经纬度,5 为GPS坐标,6 为sogou墨卡托坐标,"` 28 | Longitude float64 `json:"longitude" doc:"门店所在地理位置的经度"` 29 | Latitude float64 `json:"latitude" doc:"门店所在地理位置的纬度(经纬度均为火星坐标,最好选用腾讯地图标记的坐标)"` 30 | PhotoList []struct { 31 | PhotoUrl string `json:"photo_url"` 32 | } `json:"photo_list" doc:"图片列表,url 形式,可以有多张图片,尺寸为640*340px。必须为上一接口生成的url。图片内容不允许与门店不相关,不允许为二维码、员工合照(或模特肖像)、营业执照、无门店正门的街景、地图截图、公交地铁站牌、菜单截图等"` 33 | AvgPrice int `json:"avg_price" doc:"人均价格,大于0 的整数"` 34 | AvailableState int `json:"available_state" doc:"门店是否可用状态。1 表示系统错误、2 表示审核中、3 审核通过、4 审核驳回。当该字段为1、2、4 状态时,poi_id 为空"` 35 | UpdateStatus int `json:"update_status" doc:"扩展字段是否正在更新中。1 表示扩展字段正在更新中,尚未生效,不允许再次更新; 0 表示扩展字段没有在更新中或更新已生效,可以再次更新"` 36 | } 37 | 38 | type GetPoiListParam struct { 39 | Begin int `json:"begin" doc:"开始位置,0 即为从第一条开始查询"` 40 | Limit int `json:"limit" doc:"返回数据条数,最大允许50,默认为20"` 41 | } 42 | 43 | type GetPoiListResp struct { 44 | utils.CommonError 45 | BusinessList []struct { 46 | BaseInfo PoiObj `json:"base_info"` 47 | } `json:"business_list"` 48 | } 49 | 50 | func (this WechatApi) GetPoiList(param *GetPoiListParam) (*GetPoiListResp, error) { 51 | var res GetPoiListResp 52 | if err := this.DoPostObject(getPoiList, param, &res); err == nil { 53 | return &res, nil 54 | } else { 55 | return nil, err 56 | } 57 | } 58 | 59 | type GetPoiDetailParam struct { 60 | PoiID string `json:"poi_id" doc:"微信内部的id"` 61 | } 62 | 63 | type GetPoiDetailResp struct { 64 | utils.CommonError 65 | Business struct { 66 | BaseInfo PoiObj `json:"base_info"` 67 | } `json:"business"` 68 | } 69 | 70 | func (this WechatApi) GetPoiDetail(param *GetPoiDetailParam) (*GetPoiDetailResp, error) { 71 | var res GetPoiDetailResp 72 | if err := this.DoPostObject(getPoi, param, &res); err == nil { 73 | return &res, nil 74 | } else { 75 | return nil, err 76 | } 77 | } -------------------------------------------------------------------------------- /mp/server.go: -------------------------------------------------------------------------------- 1 | package mp 2 | 3 | import ( 4 | "github.com/qjw/go-wx-sdk/utils" 5 | "encoding/xml" 6 | "fmt" 7 | "log" 8 | "io/ioutil" 9 | "net/http" 10 | "runtime/debug" 11 | "strconv" 12 | ) 13 | 14 | type MessageHandle func(*MixMessage) utils.Reply 15 | 16 | type Server struct { 17 | Request *http.Request 18 | Responce http.ResponseWriter 19 | MpContext *Context 20 | 21 | // 公众号的OpenID 22 | // openID string 23 | 24 | // 收到消息的回调 25 | MessageHandler MessageHandle 26 | } 27 | 28 | 29 | type ServerRequest struct { 30 | MixedMsg *MixMessage 31 | // 加密模式下才有 32 | RequestHttpBody *utils.RequestEncryptedXMLMsg 33 | // 收到的原始数据 34 | RequestRawXMLMsg []byte 35 | 36 | // 安全(加密)模式 37 | IsSafeMode bool 38 | Random []byte 39 | Nonce string 40 | Timestamp int64 41 | 42 | // 回复的消息 43 | ResponseMsg utils.Reply 44 | } 45 | 46 | //NewServer init 47 | func NewServer(request *http.Request, responce http.ResponseWriter, 48 | handle MessageHandle, mpwcontext *Context) *Server { 49 | return &Server{ 50 | Request: request, 51 | Responce: responce, 52 | MessageHandler: handle, 53 | MpContext: mpwcontext, 54 | } 55 | } 56 | 57 | //Serve 处理微信的请求消息 58 | func (srv *Server) Ping() { 59 | if !srv.validate(nil) { 60 | http.Error(srv.Responce, "", http.StatusForbidden) 61 | return 62 | } 63 | 64 | echostr:= srv.Request.URL.Query().Get("echostr") 65 | if echostr == "" { 66 | http.Error(srv.Responce, "", http.StatusForbidden) 67 | return 68 | } 69 | http.Error(srv.Responce, echostr, http.StatusOK) 70 | } 71 | 72 | //Serve 处理微信的请求消息 73 | func (srv *Server) Serve() error { 74 | var svrReq ServerRequest 75 | if !srv.validate(&svrReq) { 76 | return fmt.Errorf("请求校验失败") 77 | } 78 | 79 | err := srv.handleRequest(&svrReq) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | if err = srv.buildResponse(&svrReq);err != nil{ 85 | return err 86 | } 87 | if err = srv.send(&svrReq);err != nil { 88 | return err 89 | } 90 | return nil 91 | } 92 | 93 | //Validate 校验请求是否合法 94 | func (srv *Server) validate(svrReq *ServerRequest) bool { 95 | signature := srv.Request.URL.Query().Get("signature") 96 | if signature == "" { 97 | log.Print("invalid msg_signature") 98 | return false 99 | } 100 | timestamp := srv.Request.URL.Query().Get("timestamp") 101 | if timestamp == "" { 102 | log.Print("invalid timestamp") 103 | return false 104 | } 105 | 106 | timestampInt, err := strconv.ParseInt(timestamp, 10, 64) 107 | if err != nil { 108 | log.Print(err.Error()) 109 | return false 110 | } 111 | 112 | nonce := srv.Request.URL.Query().Get("nonce") 113 | if nonce == "" { 114 | log.Print("invalid nonce") 115 | return false 116 | } 117 | 118 | if signature == utils.Signature(srv.MpContext.Config.Token, timestamp, nonce){ 119 | if svrReq != nil{ 120 | svrReq.Timestamp = timestampInt 121 | svrReq.Nonce = nonce 122 | } 123 | return true 124 | }else{ 125 | return false 126 | } 127 | } 128 | 129 | //HandleRequest 处理微信的请求 130 | func (srv *Server) handleRequest(svrReq *ServerRequest) (err error) { 131 | //set isSafeMode 132 | svrReq.IsSafeMode = false 133 | encryptType := srv.Request.URL.Query().Get("encrypt_type") 134 | if encryptType == "aes" { 135 | svrReq.IsSafeMode = true 136 | } 137 | 138 | //set openID 139 | // srv.openID = srv.Context.Query("openid") 140 | err = srv.getMessage(svrReq) 141 | if err != nil { 142 | return 143 | } 144 | 145 | if srv.MessageHandler != nil { 146 | svrReq.ResponseMsg = srv.MessageHandler(svrReq.MixedMsg) 147 | } 148 | return 149 | } 150 | 151 | //getMessage 解析微信返回的消息 152 | func (srv *Server) getMessage(svrReq *ServerRequest) (error) { 153 | var rawXMLMsgBytes []byte 154 | var err error 155 | if svrReq.IsSafeMode { 156 | var encryptedXMLMsg utils.RequestEncryptedXMLMsg 157 | if err := xml.NewDecoder(srv.Request.Body).Decode(&encryptedXMLMsg); err != nil { 158 | return fmt.Errorf("从body中解析xml失败,err=%v", err) 159 | } 160 | svrReq.RequestHttpBody = &encryptedXMLMsg 161 | 162 | //解密 163 | svrReq.Random, rawXMLMsgBytes, _, err = utils.DecryptMsg(srv.MpContext.Config.AppID, 164 | encryptedXMLMsg.EncryptedMsg, 165 | srv.MpContext.Config.EncodingAESKey) 166 | if err != nil { 167 | return fmt.Errorf("消息解密失败, err=%v", err) 168 | } 169 | } else { 170 | rawXMLMsgBytes, err = ioutil.ReadAll(srv.Request.Body) 171 | if err != nil { 172 | return fmt.Errorf("从body中解析xml失败, err=%v", err) 173 | } 174 | } 175 | 176 | 177 | var msg MixMessage 178 | if err := xml.Unmarshal(rawXMLMsgBytes, &msg);err != nil{ 179 | return err 180 | } 181 | 182 | svrReq.RequestRawXMLMsg = rawXMLMsgBytes 183 | svrReq.MixedMsg = &msg 184 | return nil 185 | } 186 | 187 | func (srv *Server) buildResponse(svrReq *ServerRequest) (err error) { 188 | reply := svrReq.ResponseMsg 189 | if reply == nil { 190 | return 191 | } 192 | 193 | defer func() { 194 | if e := recover(); e != nil { 195 | err = fmt.Errorf("panic error: %v\n%s", e, debug.Stack()) 196 | } 197 | }() 198 | msgType := reply.GetMsgType() 199 | switch msgType { 200 | case utils.MsgTypeText: 201 | case utils.MsgTypeImage: 202 | case utils.MsgTypeVoice: 203 | case utils.MsgTypeVideo: 204 | case utils.MsgTypeMusic: 205 | case utils.MsgTypeNews: 206 | case utils.MsgTypeTransfer: 207 | default: 208 | err = utils.ErrUnsupportReply 209 | return 210 | } 211 | 212 | reply.SetToUserName(svrReq.MixedMsg.FromUserName) 213 | reply.SetFromUserName(svrReq.MixedMsg.ToUserName) 214 | reply.SetCreateTime(utils.GetCurrTs()) 215 | return 216 | } 217 | 218 | //Send 将自定义的消息发送 219 | func (srv *Server) send(svrReq *ServerRequest) (err error) { 220 | if svrReq.ResponseMsg == nil{ 221 | return 222 | } 223 | 224 | var replyMsg interface{} = svrReq.ResponseMsg 225 | if svrReq.IsSafeMode { 226 | responseRawXMLMsg, err := xml.Marshal(svrReq.ResponseMsg) 227 | if err != nil{ 228 | return err 229 | } 230 | 231 | //安全模式下对消息进行加密 232 | var encryptedMsg []byte 233 | encryptedMsg, err = utils.EncryptMsg(svrReq.Random, responseRawXMLMsg, 234 | srv.MpContext.Config.AppID, 235 | srv.MpContext.Config.EncodingAESKey) 236 | if err != nil { 237 | return err 238 | } 239 | // 如果获取不到timestamp nonce 则自己生成 240 | timestamp := svrReq.Timestamp 241 | timestampStr := strconv.FormatInt(timestamp, 10) 242 | msgSignature := utils.Signature(srv.MpContext.Config.Token, timestampStr, 243 | svrReq.Nonce, string(encryptedMsg)) 244 | replyMsg = utils.ResponseEncryptedXMLMsg{ 245 | EncryptedMsg: string(encryptedMsg), 246 | MsgSignature: msgSignature, 247 | Timestamp: timestamp, 248 | Nonce: svrReq.Nonce, 249 | } 250 | } 251 | if replyMsg != nil { 252 | data, _ := xml.MarshalIndent(replyMsg, "", "\t") 253 | 254 | srv.Responce.Header().Set("Content-Type", "application/xml; charset=utf-8") 255 | srv.Responce.WriteHeader(http.StatusOK) 256 | srv.Responce.Write(data) 257 | } 258 | return nil 259 | } 260 | -------------------------------------------------------------------------------- /small/api.go: -------------------------------------------------------------------------------- 1 | package small 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qjw/go-wx-sdk/utils" 6 | ) 7 | 8 | //-----------------------------------客服消息----------------------------------------------------------------------------- 9 | 10 | const ( 11 | sendKfMsg = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=%s" 12 | ) 13 | const ( 14 | sendKfTextMsgTemp = `{ 15 | "touser":"%s", 16 | "msgtype":"text", 17 | "text": 18 | { 19 | "content":"%s" 20 | } 21 | }` 22 | sendKfImageMsgTemp = `{ 23 | "touser":"%s", 24 | "msgtype":"image", 25 | "image": 26 | { 27 | "media_id":"%s" 28 | } 29 | }` 30 | ) 31 | 32 | func (this SmallApi) SendKfMessage(touser string, content string) (*utils.CommonError, error) { 33 | var res utils.CommonError 34 | body := fmt.Sprintf(sendKfTextMsgTemp, touser, content) 35 | if err := this.DoPost(sendKfMsg, body, &res); err == nil { 36 | return &res, nil 37 | } else { 38 | return nil, err 39 | } 40 | } 41 | 42 | func (this SmallApi) SendKfImageMessage(touser string, media_id string) (*utils.CommonError, error) { 43 | var res utils.CommonError 44 | body := fmt.Sprintf(sendKfImageMsgTemp, touser, media_id) 45 | if err := this.DoPost(sendKfMsg, body, &res); err == nil { 46 | return &res, nil 47 | } else { 48 | return nil, err 49 | } 50 | } 51 | 52 | //-----------------------------------登录换取session----------------------------------------------------------------------------- 53 | type UserSession struct { 54 | OpenID string `json:"openid"` 55 | SessionKey string `json:"session_key"` 56 | UnionID string `json:"unionid"` 57 | } 58 | 59 | const ( 60 | jscode2SessionApi = "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code" 61 | ) 62 | 63 | func (this SmallApi) Jscode2Session(code string) (*UserSession, error) { 64 | var res struct { 65 | utils.CommonError 66 | UserSession 67 | } 68 | 69 | if err := this.DoGetLite(jscode2SessionApi, 70 | &res, 71 | this.Context.Config.AppID, 72 | this.Context.Config.AppSecret, 73 | code); err == nil { 74 | if res.CommonError.IsOK() { 75 | return &(res.UserSession), nil 76 | } else { 77 | return nil, fmt.Errorf("%d - %s", res.ErrCode, res.ErrMsg) 78 | } 79 | } else { 80 | return nil, err 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /small/common.go: -------------------------------------------------------------------------------- 1 | package small 2 | 3 | import "github.com/qjw/go-wx-sdk/utils" 4 | 5 | type Config struct { 6 | // 小程序app id 7 | AppID string 8 | // 小程序密钥 9 | AppSecret string 10 | // 小程序token 11 | Token string 12 | // 小程序消息加密密钥 13 | EncodingAESKey string 14 | } 15 | 16 | type SmallApi struct { 17 | utils.ApiTokenBase 18 | Context *Context 19 | } 20 | 21 | func NewSmallApi(context *Context) * SmallApi{ 22 | api := &SmallApi{ 23 | Context:context, 24 | } 25 | api.ContextToken = context 26 | return api 27 | } 28 | -------------------------------------------------------------------------------- /small/context.go: -------------------------------------------------------------------------------- 1 | package small 2 | 3 | import ( 4 | "github.com/qjw/go-wx-sdk/cache" 5 | "sync" 6 | "fmt" 7 | "encoding/json" 8 | "time" 9 | "github.com/qjw/go-wx-sdk/utils" 10 | ) 11 | 12 | const ( 13 | //AccessTokenURL 获取access_token的接口 14 | accessTokenURL = "https://api.weixin.qq.com/cgi-bin/token" 15 | jsTicketUrl = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=jsapi" 16 | //cardTicketUrl = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=wx_card" 17 | ) 18 | 19 | const ( 20 | accessTokenKey = "sm_access_token_%s" 21 | jsTickctTemp = "sm_js_ticket_%s" 22 | //cardTickctTemp = "card_ticket_%s" 23 | ) 24 | 25 | const ( 26 | ) 27 | 28 | // Context struct 29 | type Context struct { 30 | // 配置 31 | Config *Config 32 | // 缓存处理器 33 | Cache cache.Cache 34 | 35 | //accessTokenLock 读写锁 同一个AppID一个 36 | accessTokenLock *sync.RWMutex 37 | 38 | //jsAPITicket 读写锁 同一个AppID一个 39 | jsAPITicketLock *sync.RWMutex 40 | } 41 | 42 | // SetJsAPITicketLock 设置jsAPITicket的lock 43 | func (ctx *Context) setJsAPITicketLock(lock *sync.RWMutex) { 44 | ctx.jsAPITicketLock = lock 45 | } 46 | 47 | //SetAccessTokenLock 设置读写锁(一个appID一个读写锁) 48 | func (ctx *Context) setAccessTokenLock(l *sync.RWMutex) { 49 | ctx.accessTokenLock = l 50 | } 51 | 52 | func NewContext(config *Config, cache cache.Cache) *Context { 53 | context := &Context{ 54 | Config: config, 55 | Cache: cache, 56 | } 57 | context.setAccessTokenLock(new(sync.RWMutex)) 58 | context.setJsAPITicketLock(new(sync.RWMutex)) 59 | return context 60 | } 61 | 62 | //GetAccessToken 获取access_token 63 | func (ctx *Context) GetAccessToken() (accessToken string, err error) { 64 | ctx.accessTokenLock.Lock() 65 | defer ctx.accessTokenLock.Unlock() 66 | 67 | accessTokenCacheKey := fmt.Sprintf(accessTokenKey, ctx.Config.AppID) 68 | val := ctx.Cache.Get(accessTokenCacheKey) 69 | if val != nil { 70 | accessToken = val.(string) 71 | return 72 | } 73 | 74 | //从微信服务器获取 75 | var resAccessToken utils.ResAccessToken 76 | resAccessToken, err = ctx.GetAccessTokenFromServer() 77 | if err != nil { 78 | return 79 | } 80 | 81 | accessToken = resAccessToken.AccessToken 82 | return 83 | } 84 | 85 | //GetAccessTokenFromServer 强制从微信服务器获取token 86 | func (ctx *Context) GetAccessTokenFromServer() (resAccessToken utils.ResAccessToken, err error) { 87 | url := fmt.Sprintf("%s?grant_type=client_credential&appid=%s&secret=%s", accessTokenURL, 88 | ctx.Config.AppID, 89 | ctx.Config.AppSecret) 90 | 91 | body, _, err := utils.HTTPGet(url) 92 | err = json.Unmarshal(body, &resAccessToken) 93 | if err != nil { 94 | return 95 | } 96 | if resAccessToken.ErrMsg != "" { 97 | err = fmt.Errorf("get access_token error : errcode=%v , errormsg=%v", 98 | resAccessToken.ErrCode, resAccessToken.ErrMsg) 99 | return 100 | } 101 | 102 | accessTokenCacheKey := fmt.Sprintf(accessTokenKey, ctx.Config.AppID) 103 | expires := resAccessToken.ExpiresIn - 1500 104 | err = ctx.Cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second) 105 | return 106 | } 107 | 108 | func (ctx *Context) GetJsTicket() (jsTicket string, err error) { 109 | return ctx.getTicket(jsTicketUrl,jsTickctTemp) 110 | } 111 | 112 | //func (ctx *Context) GetCardTicket() (jsTicket string, err error) { 113 | // return ctx.getTicket(cardTicketUrl, cardTickctTemp) 114 | //} 115 | 116 | 117 | func (ctx *Context) getTicket(urlTemp, keyTemp string) (jsTicket string, err error) { 118 | ctx.jsAPITicketLock.Lock() 119 | defer ctx.jsAPITicketLock.Unlock() 120 | 121 | jsTicketCacheKey := fmt.Sprintf(keyTemp, ctx.Config.AppID) 122 | val := ctx.Cache.Get(jsTicketCacheKey) 123 | if val != nil { 124 | jsTicket = val.(string) 125 | return 126 | } 127 | 128 | //从微信服务器获取 129 | var resJsTicket *utils.ResJsTicket 130 | resJsTicket, err = ctx.getTicketFromServer(urlTemp,keyTemp) 131 | if err != nil { 132 | return 133 | } 134 | 135 | jsTicket = resJsTicket.Ticket 136 | return 137 | } 138 | 139 | func (ctx *Context) getTicketFromServer(urlTemp, keyTemp string) (resJsTicket *utils.ResJsTicket, err error,) { 140 | var token string 141 | token, err = ctx.GetAccessToken() 142 | if err != nil { 143 | return 144 | } 145 | 146 | url := fmt.Sprintf(urlTemp, token) 147 | var jsticket utils.ResJsTicket 148 | resJsTicket = &jsticket 149 | 150 | body, _, err := utils.HTTPGet(url) 151 | err = json.Unmarshal(body, &jsticket) 152 | if err != nil { 153 | return 154 | } 155 | if resJsTicket.ErrCode != 0 || resJsTicket.ErrMsg != "ok" { 156 | err = fmt.Errorf("get access_token error : errcode=%v , errormsg=%v", 157 | resJsTicket.ErrCode, resJsTicket.ErrMsg) 158 | return 159 | } 160 | 161 | jsTicketCacheKey := fmt.Sprintf(keyTemp, ctx.Config.AppID) 162 | expires := resJsTicket.ExpiresIn - 1500 163 | err = ctx.Cache.Set(jsTicketCacheKey, resJsTicket.Ticket, time.Duration(expires)*time.Second) 164 | return 165 | } 166 | -------------------------------------------------------------------------------- /small/message.go: -------------------------------------------------------------------------------- 1 | package small 2 | 3 | import ( 4 | "encoding/xml" 5 | "github.com/qjw/go-wx-sdk/utils" 6 | ) 7 | 8 | //MixMessage 存放所有微信发送过来的消息和事件 9 | type MixMessage struct { 10 | MixCommonToken 11 | 12 | //基本消息 13 | MsgID int64 `xml:"MsgId,omitempty" json:"msg_id,omitempty"` 14 | Content string `xml:"Content,omitempty" json:"content,omitempty"` 15 | PicURL string `xml:"PicUrl,omitempty" json:"pic_url,omitempty"` 16 | MediaID string `xml:"MediaId,omitempty" json:"media_id,omitempty"` 17 | 18 | //事件相关 19 | Event utils.EventType `xml:"Event,omitempty" json:"event,omitempty"` 20 | // 进入会话事件 @user_enter_tempsession 21 | SessionFrom string `xml:"SessionFrom,omitempty" json:"session_from,omitempty"` 22 | } 23 | 24 | // CommonToken 消息中通用的结构 25 | type MixCommonToken struct { 26 | XMLName xml.Name `xml:"xml" json:"-"` 27 | ToUserName string `xml:"ToUserName" json:"to_username"` 28 | FromUserName string `xml:"FromUserName" json:"from_username"` 29 | CreateTime int64 `xml:"CreateTime" json:"create_time"` 30 | MsgType utils.MsgType `xml:"MsgType" json:"msg_type"` 31 | } 32 | -------------------------------------------------------------------------------- /small/server.go: -------------------------------------------------------------------------------- 1 | package small 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "log" 7 | "github.com/qjw/go-wx-sdk/utils" 8 | "io/ioutil" 9 | "net/http" 10 | "runtime/debug" 11 | "strconv" 12 | ) 13 | 14 | type MessageHandle func(*MixMessage) utils.Reply 15 | 16 | type Server struct { 17 | Request *http.Request 18 | Responce http.ResponseWriter 19 | SContext *Context 20 | 21 | // 公众号的OpenID 22 | // openID string 23 | 24 | // 收到消息的回调 25 | MessageHandler MessageHandle 26 | } 27 | 28 | type ServerRequest struct { 29 | MixedMsg *MixMessage 30 | // 加密模式下才有 31 | RequestHttpBody *utils.RequestEncryptedXMLMsg 32 | // 收到的原始数据 33 | RequestRawXMLMsg []byte 34 | 35 | // 安全(加密)模式 36 | IsSafeMode bool 37 | Random []byte 38 | Nonce string 39 | Timestamp int64 40 | 41 | // 回复的消息 42 | ResponseMsg utils.Reply 43 | } 44 | 45 | //NewServer init 46 | func NewServer(request *http.Request, responce http.ResponseWriter, 47 | handle MessageHandle, mpwcontext *Context) *Server { 48 | return &Server{ 49 | Request: request, 50 | Responce: responce, 51 | MessageHandler: handle, 52 | SContext: mpwcontext, 53 | } 54 | } 55 | 56 | //Serve 处理微信的请求消息 57 | func (srv *Server) Ping() { 58 | if !srv.validate(nil) { 59 | http.Error(srv.Responce, "", http.StatusForbidden) 60 | return 61 | } 62 | 63 | echostr := srv.Request.URL.Query().Get("echostr") 64 | if echostr == "" { 65 | http.Error(srv.Responce, "", http.StatusForbidden) 66 | return 67 | } 68 | http.Error(srv.Responce, echostr, http.StatusOK) 69 | } 70 | 71 | //Serve 处理微信的请求消息 72 | func (srv *Server) Serve() error { 73 | var svrReq ServerRequest 74 | if !srv.validate(&svrReq) { 75 | return fmt.Errorf("请求校验失败") 76 | } 77 | 78 | err := srv.handleRequest(&svrReq) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | if err = srv.buildResponse(&svrReq); err != nil { 84 | return err 85 | } 86 | if err = srv.send(&svrReq); err != nil { 87 | return err 88 | } 89 | return nil 90 | } 91 | 92 | //Validate 校验请求是否合法 93 | func (srv *Server) validate(svrReq *ServerRequest) bool { 94 | signature := srv.Request.URL.Query().Get("signature") 95 | if signature == "" { 96 | log.Print("invalid msg_signature") 97 | return false 98 | } 99 | timestamp := srv.Request.URL.Query().Get("timestamp") 100 | if timestamp == "" { 101 | log.Print("invalid timestamp") 102 | return false 103 | } 104 | 105 | timestampInt, err := strconv.ParseInt(timestamp, 10, 64) 106 | if err != nil { 107 | log.Print(err.Error()) 108 | return false 109 | } 110 | 111 | nonce := srv.Request.URL.Query().Get("nonce") 112 | if nonce == "" { 113 | log.Print("invalid nonce") 114 | return false 115 | } 116 | 117 | if signature == utils.Signature(srv.SContext.Config.Token, timestamp, nonce) { 118 | if svrReq != nil { 119 | svrReq.Timestamp = timestampInt 120 | svrReq.Nonce = nonce 121 | } 122 | return true 123 | } else { 124 | return false 125 | } 126 | } 127 | 128 | //HandleRequest 处理微信的请求 129 | func (srv *Server) handleRequest(svrReq *ServerRequest) (err error) { 130 | //set isSafeMode 131 | svrReq.IsSafeMode = false 132 | encryptType := srv.Request.URL.Query().Get("encrypt_type") 133 | if encryptType == "aes" { 134 | svrReq.IsSafeMode = true 135 | } 136 | 137 | //set openID 138 | // srv.openID = srv.Context.Query("openid") 139 | err = srv.getMessage(svrReq) 140 | if err != nil { 141 | return 142 | } 143 | 144 | if srv.MessageHandler != nil { 145 | svrReq.ResponseMsg = srv.MessageHandler(svrReq.MixedMsg) 146 | } 147 | return 148 | } 149 | 150 | //getMessage 解析微信返回的消息 151 | func (srv *Server) getMessage(svrReq *ServerRequest) error { 152 | var rawXMLMsgBytes []byte 153 | var err error 154 | if svrReq.IsSafeMode { 155 | var encryptedXMLMsg utils.RequestEncryptedXMLMsg 156 | if err := xml.NewDecoder(srv.Request.Body).Decode(&encryptedXMLMsg); err != nil { 157 | return fmt.Errorf("从body中解析xml失败,err=%v", err) 158 | } 159 | svrReq.RequestHttpBody = &encryptedXMLMsg 160 | 161 | //解密 162 | svrReq.Random, rawXMLMsgBytes, _, err = utils.DecryptMsg(srv.SContext.Config.AppID, 163 | encryptedXMLMsg.EncryptedMsg, 164 | srv.SContext.Config.EncodingAESKey) 165 | if err != nil { 166 | return fmt.Errorf("消息解密失败, err=%v", err) 167 | } 168 | } else { 169 | rawXMLMsgBytes, err = ioutil.ReadAll(srv.Request.Body) 170 | if err != nil { 171 | return fmt.Errorf("从body中解析xml失败, err=%v", err) 172 | } 173 | } 174 | 175 | var msg MixMessage 176 | if err := xml.Unmarshal(rawXMLMsgBytes, &msg); err != nil { 177 | return err 178 | } 179 | 180 | svrReq.RequestRawXMLMsg = rawXMLMsgBytes 181 | svrReq.MixedMsg = &msg 182 | return nil 183 | } 184 | 185 | func (srv *Server) buildResponse(svrReq *ServerRequest) (err error) { 186 | reply := svrReq.ResponseMsg 187 | if reply == nil { 188 | return 189 | } 190 | 191 | defer func() { 192 | if e := recover(); e != nil { 193 | err = fmt.Errorf("panic error: %v\n%s", e, debug.Stack()) 194 | } 195 | }() 196 | msgType := reply.GetMsgType() 197 | switch msgType { 198 | case utils.MsgTypeText: 199 | case utils.MsgTypeImage: 200 | case utils.MsgTypeVoice: 201 | case utils.MsgTypeVideo: 202 | case utils.MsgTypeMusic: 203 | case utils.MsgTypeNews: 204 | case utils.MsgTypeTransfer: 205 | default: 206 | err = utils.ErrUnsupportReply 207 | return 208 | } 209 | 210 | reply.SetToUserName(svrReq.MixedMsg.FromUserName) 211 | reply.SetFromUserName(svrReq.MixedMsg.ToUserName) 212 | reply.SetCreateTime(utils.GetCurrTs()) 213 | return 214 | } 215 | 216 | //Send 将自定义的消息发送 217 | func (srv *Server) send(svrReq *ServerRequest) (err error) { 218 | if svrReq.ResponseMsg == nil { 219 | return 220 | } 221 | 222 | var replyMsg interface{} = svrReq.ResponseMsg 223 | if svrReq.IsSafeMode { 224 | responseRawXMLMsg, err := xml.Marshal(svrReq.ResponseMsg) 225 | if err != nil { 226 | return err 227 | } 228 | 229 | //安全模式下对消息进行加密 230 | var encryptedMsg []byte 231 | encryptedMsg, err = utils.EncryptMsg(svrReq.Random, responseRawXMLMsg, 232 | srv.SContext.Config.AppID, 233 | srv.SContext.Config.EncodingAESKey) 234 | if err != nil { 235 | return err 236 | } 237 | // 如果获取不到timestamp nonce 则自己生成 238 | timestamp := svrReq.Timestamp 239 | timestampStr := strconv.FormatInt(timestamp, 10) 240 | msgSignature := utils.Signature(srv.SContext.Config.Token, timestampStr, 241 | svrReq.Nonce, string(encryptedMsg)) 242 | replyMsg = utils.ResponseEncryptedXMLMsg{ 243 | EncryptedMsg: string(encryptedMsg), 244 | MsgSignature: msgSignature, 245 | Timestamp: timestamp, 246 | Nonce: svrReq.Nonce, 247 | } 248 | } 249 | if replyMsg != nil { 250 | data, _ := xml.MarshalIndent(replyMsg, "", "\t") 251 | 252 | srv.Responce.Header().Set("Content-Type", "application/xml; charset=utf-8") 253 | srv.Responce.WriteHeader(http.StatusOK) 254 | srv.Responce.Write(data) 255 | } 256 | return nil 257 | } 258 | -------------------------------------------------------------------------------- /utils/api_base.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "reflect" 5 | "fmt" 6 | "encoding/json" 7 | "errors" 8 | "net/http" 9 | ) 10 | 11 | type ApiBase struct { 12 | } 13 | 14 | func (this ApiBase) DoPost(uri_pattern string, body string, res interface{}, a ...interface{}) error { 15 | return this.DoPostRaw(uri_pattern, []byte(body), res, a...) 16 | } 17 | 18 | func (this ApiBase) DoPostObject(uri_pattern string, body interface{}, res interface{}, a ...interface{}) error { 19 | tp := reflect.TypeOf(body) 20 | if tp.Kind() != reflect.Ptr || tp.Elem().Kind() != reflect.Struct{ 21 | panic("invalid body object type") 22 | } 23 | 24 | raw,err := json.Marshal(body) 25 | if err != nil{ 26 | return err 27 | } 28 | return this.DoPostRaw(uri_pattern, raw, res, a...) 29 | } 30 | 31 | func (this ApiBase) DoPostRaw(uri_pattern string, body []byte, res interface{}, a ...interface{}) (err error) { 32 | var uri string 33 | if len(a) == 0 { 34 | uri = uri_pattern 35 | } else { 36 | uri = fmt.Sprintf(uri_pattern, a...) 37 | } 38 | 39 | response, code, err := PostJSONRaw(uri, body) 40 | return this.doProcessResponse(response,code,err,res) 41 | } 42 | 43 | func (this ApiBase) DoGet(uri_pattern string, res interface{}, a ...interface{}) (err error) { 44 | return this.doHttpByUri(HTTPGet,uri_pattern,res,a...) 45 | } 46 | 47 | func (this ApiBase) DoDelete(uri_pattern string, res interface{}, a ...interface{}) (err error) { 48 | return this.doHttpByUri(HTTPDelete,uri_pattern,res,a...) 49 | } 50 | 51 | func (this ApiBase) doHttpByUri(httpProcessor func (string)([]byte, int, error), 52 | uri_pattern string, res interface{}, a ...interface{}) (err error) { 53 | var uri string 54 | if len(a) == 0 { 55 | uri = uri_pattern 56 | } else { 57 | uri = fmt.Sprintf(uri_pattern, a...) 58 | } 59 | 60 | response, code, err := httpProcessor(uri) 61 | return this.doProcessResponse(response,code,err,res) 62 | } 63 | 64 | func (this ApiBase) doProcessResponse(response []byte, code int, err error, res interface{}) error{ 65 | if err != nil { 66 | return err 67 | } 68 | 69 | if code == http.StatusNoContent && len(response) == 0{ 70 | response = []byte("{}") 71 | } 72 | 73 | err = json.Unmarshal(response, res) 74 | if err != nil { 75 | return errors.New(string(response)) 76 | } 77 | return nil 78 | } -------------------------------------------------------------------------------- /utils/api_base_xml.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "reflect" 5 | "fmt" 6 | "log" 7 | "errors" 8 | "encoding/xml" 9 | "net/http" 10 | ) 11 | 12 | type ApiBaseXml struct { 13 | } 14 | 15 | func (this ApiBaseXml) DoPost(uri_pattern string, body string, res interface{}, a ...interface{}) error { 16 | return this.DoPostRaw(uri_pattern, []byte(body), res, a...) 17 | } 18 | 19 | func (this ApiBaseXml) DoPostObject(uri_pattern string, body interface{}, res interface{}, a ...interface{}) error { 20 | tp := reflect.TypeOf(body) 21 | if tp.Kind() != reflect.Ptr || tp.Elem().Kind() != reflect.Struct{ 22 | panic("invalid body object type") 23 | } 24 | 25 | raw,err := xml.Marshal(body) 26 | if err != nil{ 27 | return err 28 | } 29 | return this.DoPostRaw(uri_pattern, raw, res, a...) 30 | } 31 | 32 | func (this ApiBaseXml) DoPostRaw(uri_pattern string, body []byte, res interface{}, a ...interface{}) (err error) { 33 | var uri string 34 | if len(a) == 0 { 35 | uri = uri_pattern 36 | } else { 37 | uri = fmt.Sprintf(uri_pattern, a...) 38 | } 39 | 40 | response, code, err := PostJSONRaw(uri, body) 41 | return this.doProcessResponse(response,code,err,res) 42 | } 43 | 44 | func (this ApiBaseXml) DoGet(uri_pattern string, res interface{}, a ...interface{}) (err error) { 45 | var uri string 46 | if len(a) == 0 { 47 | uri = uri_pattern 48 | } else { 49 | uri = fmt.Sprintf(uri_pattern, a...) 50 | } 51 | 52 | response, code, err := HTTPGet(uri) 53 | return this.doProcessResponse(response,code,err,res) 54 | } 55 | 56 | 57 | func (this ApiBaseXml) doProcessResponse(response []byte, code int, err error, res interface{}) error{ 58 | if err != nil { 59 | return err 60 | } 61 | 62 | tmp := string(response) 63 | log.Print(tmp) 64 | 65 | if code == http.StatusNoContent && len(response) == 0{ 66 | response = []byte("") 67 | } 68 | 69 | err = xml.Unmarshal(response, res) 70 | if err != nil { 71 | return errors.New(string(response)) 72 | } 73 | return nil 74 | } -------------------------------------------------------------------------------- /utils/char_data.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "encoding/xml" 4 | 5 | type CharData string 6 | func (n CharData) MarshalXML(e *xml.Encoder, start xml.StartElement) error { 7 | return e.EncodeElement(struct{ 8 | S string `xml:",innerxml"` 9 | }{ 10 | S: "", 11 | }, start) 12 | } 13 | -------------------------------------------------------------------------------- /utils/context.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "encoding/json" 7 | "errors" 8 | "reflect" 9 | "net/http" 10 | ) 11 | 12 | type ContextToken interface{ 13 | GetAccessToken() (accessToken string, err error); 14 | GetJsTicket() (jsTicket string, err error) 15 | } 16 | 17 | type ApiTokenBase struct { 18 | ContextToken ContextToken 19 | } 20 | 21 | func (this ApiTokenBase) DoPost(uri_pattern string, body string, res interface{}, a ...interface{}) error { 22 | return this.DoPostRaw(uri_pattern, []byte(body), res, a...) 23 | } 24 | 25 | func (this ApiTokenBase) DoPostObject(uri_pattern string, body interface{}, res interface{}, a ...interface{}) error { 26 | tp := reflect.TypeOf(body) 27 | if tp.Kind() != reflect.Ptr || tp.Elem().Kind() != reflect.Struct{ 28 | panic("invalid body object type") 29 | } 30 | 31 | raw,err := json.Marshal(body) 32 | if err != nil{ 33 | return err 34 | } 35 | return this.DoPostRaw(uri_pattern, raw, res, a...) 36 | } 37 | 38 | func (this ApiTokenBase) DoPostRaw(uri_pattern string, body []byte, res interface{}, a ...interface{}) error { 39 | accessToken, err := this.ContextToken.GetAccessToken() 40 | if err != nil { 41 | return err 42 | } 43 | 44 | var uri string 45 | if len(a) == 0 { 46 | uri = fmt.Sprintf(uri_pattern, accessToken) 47 | } else { 48 | //todo 性能 49 | a = append([]interface{}{accessToken}, a...) 50 | uri = fmt.Sprintf(uri_pattern, a...) 51 | } 52 | 53 | response, code, err := PostJSONRaw(uri, body) 54 | return this.doProcessResponse(response,code,err,res) 55 | } 56 | 57 | func (this ApiTokenBase) DoGet(uri_pattern string, res interface{}, a ...interface{}) error { 58 | accessToken, err := this.ContextToken.GetAccessToken() 59 | if err != nil { 60 | return err 61 | } 62 | 63 | var uri string 64 | if len(a) == 0 { 65 | uri = fmt.Sprintf(uri_pattern, accessToken) 66 | } else { 67 | //todo 性能 68 | a = append([]interface{}{accessToken}, a...) 69 | uri = fmt.Sprintf(uri_pattern, a...) 70 | } 71 | 72 | response, code, err := HTTPGet(uri) 73 | return this.doProcessResponse(response,code,err,res) 74 | } 75 | 76 | func (this ApiTokenBase) DoGetLite(uri_pattern string, res interface{}, a ...interface{}) error { 77 | var uri string 78 | uri = fmt.Sprintf(uri_pattern, a...) 79 | response, code, err := HTTPGet(uri) 80 | return this.doProcessResponse(response,code,err,res) 81 | } 82 | 83 | func (this ApiTokenBase) DoPostFile(reader io.Reader, fieldname, filename string, res interface{}, 84 | uri_pattern string, a ...interface{}) error { 85 | accessToken, err := this.ContextToken.GetAccessToken() 86 | if err != nil { 87 | return err 88 | } 89 | 90 | var uri string 91 | if len(a) == 0 { 92 | uri = fmt.Sprintf(uri_pattern, accessToken) 93 | } else { 94 | //todo 性能 95 | a = append([]interface{}{accessToken}, a...) 96 | uri = fmt.Sprintf(uri_pattern, a...) 97 | } 98 | 99 | response, code, err := PostReader(fieldname, filename, uri, reader) 100 | return this.doProcessResponse(response,code,err,res) 101 | } 102 | 103 | func (this ApiTokenBase) DoPostFileExtra(reader io.Reader, fieldname, extraFieldname, filename string, 104 | extra interface{}, res interface{}, 105 | uri_pattern string, a ...interface{}) error { 106 | accessToken, err := this.ContextToken.GetAccessToken() 107 | if err != nil { 108 | return err 109 | } 110 | 111 | descBytes, err := json.Marshal(extra) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | var uri string 117 | if len(a) == 0 { 118 | uri = fmt.Sprintf(uri_pattern, accessToken) 119 | } else { 120 | //todo 性能 121 | a = append([]interface{}{accessToken}, a...) 122 | uri = fmt.Sprintf(uri_pattern, a...) 123 | } 124 | 125 | fields := []MultipartFormField{ 126 | { 127 | IsFile: false, 128 | Fieldname: fieldname, 129 | Filename: filename, 130 | Reader: reader, 131 | }, 132 | { 133 | IsFile: false, 134 | Fieldname: extraFieldname, 135 | Filename: filename, 136 | Value: descBytes, 137 | }, 138 | } 139 | 140 | response, code, err := PostMultipartForm(fields, uri) 141 | return this.doProcessResponse(response,code,err,res) 142 | } 143 | 144 | func (this ApiTokenBase) doProcessResponse(response []byte, code int, err error, res interface{}) error{ 145 | if err != nil { 146 | return err 147 | } 148 | 149 | if code == http.StatusNoContent && len(response) == 0{ 150 | response = []byte("{}") 151 | } 152 | 153 | err = json.Unmarshal(response, res) 154 | if err != nil { 155 | return errors.New(string(response)) 156 | } 157 | return nil 158 | } 159 | //--------------------------------------------------------------------------------------------------------------------- 160 | 161 | -------------------------------------------------------------------------------- /utils/crypto.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "encoding/base64" 7 | "fmt" 8 | ) 9 | 10 | //EncryptMsg 加密消息 11 | func EncryptMsg(random, rawXMLMsg []byte, appID, aesKey string) (encrtptMsg []byte, err error) { 12 | defer func() { 13 | if e := recover(); e != nil { 14 | err = fmt.Errorf("panic error: err=%v", e) 15 | return 16 | } 17 | }() 18 | var key []byte 19 | key, err = aesKeyDecode(aesKey) 20 | if err != nil { 21 | panic(err) 22 | } 23 | ciphertext := AESEncryptMsg(random, rawXMLMsg, appID, key) 24 | encrtptMsg = []byte(base64.StdEncoding.EncodeToString(ciphertext)) 25 | return 26 | } 27 | 28 | //AESEncryptMsg ciphertext = AES_Encrypt[random(16B) + msg_len(4B) + rawXMLMsg + appId] 29 | //参考:github.com/chanxuehong/wechat.v2 30 | func AESEncryptMsg(random, rawXMLMsg []byte, appID string, aesKey []byte) (ciphertext []byte) { 31 | const ( 32 | BlockSize = 32 // PKCS#7 33 | BlockMask = BlockSize - 1 // BLOCK_SIZE 为 2^n 时, 可以用 mask 获取针对 BLOCK_SIZE 的余数 34 | ) 35 | 36 | appIDOffset := 20 + len(rawXMLMsg) 37 | contentLen := appIDOffset + len(appID) 38 | amountToPad := BlockSize - contentLen&BlockMask 39 | plaintextLen := contentLen + amountToPad 40 | 41 | plaintext := make([]byte, plaintextLen) 42 | 43 | // 拼接 44 | copy(plaintext[:16], random) 45 | encodeNetworkByteOrder(plaintext[16:20], uint32(len(rawXMLMsg))) 46 | copy(plaintext[20:], rawXMLMsg) 47 | copy(plaintext[appIDOffset:], appID) 48 | 49 | // PKCS#7 补位 50 | for i := contentLen; i < plaintextLen; i++ { 51 | plaintext[i] = byte(amountToPad) 52 | } 53 | 54 | // 加密 55 | block, err := aes.NewCipher(aesKey[:]) 56 | if err != nil { 57 | panic(err) 58 | } 59 | mode := cipher.NewCBCEncrypter(block, aesKey[:16]) 60 | mode.CryptBlocks(plaintext, plaintext) 61 | 62 | ciphertext = plaintext 63 | return 64 | } 65 | 66 | //DecryptMsg 消息解密 67 | func DecryptMsg(appID, encryptedMsg, aesKey string) (random, rawMsgXMLBytes []byte, appIDRes string, err error) { 68 | defer func() { 69 | if e := recover(); e != nil { 70 | err = fmt.Errorf("panic error: err=%v", e) 71 | return 72 | } 73 | }() 74 | var encryptedMsgBytes, key, getAppIDBytes []byte 75 | encryptedMsgBytes, err = base64.StdEncoding.DecodeString(encryptedMsg) 76 | if err != nil { 77 | return 78 | } 79 | key, err = aesKeyDecode(aesKey) 80 | if err != nil { 81 | panic(err) 82 | } 83 | random, rawMsgXMLBytes, getAppIDBytes, err = AESDecryptMsg(encryptedMsgBytes, key) 84 | if err != nil { 85 | err = fmt.Errorf("消息解密失败,%v", err) 86 | return 87 | } 88 | appIDRes = string(getAppIDBytes) 89 | if appID != "" && appID != appIDRes { 90 | err = fmt.Errorf("消息解密校验APPID失败") 91 | return 92 | } 93 | return 94 | } 95 | 96 | func aesKeyDecode(encodedAESKey string) (key []byte, err error) { 97 | if len(encodedAESKey) != 43 { 98 | err = fmt.Errorf("the length of encodedAESKey must be equal to 43") 99 | return 100 | } 101 | key, err = base64.StdEncoding.DecodeString(encodedAESKey + "=") 102 | if err != nil { 103 | return 104 | } 105 | if len(key) != 32 { 106 | err = fmt.Errorf("encodingAESKey invalid") 107 | return 108 | } 109 | return 110 | } 111 | 112 | // AESDecryptMsg ciphertext = AES_Encrypt[random(16B) + msg_len(4B) + rawXMLMsg + appId] 113 | //参考:github.com/chanxuehong/wechat.v2 114 | func AESDecryptMsg(ciphertext []byte, aesKey []byte) (random, rawXMLMsg, appID []byte, err error) { 115 | const ( 116 | BlockSize = 32 // PKCS#7 117 | BlockMask = BlockSize - 1 // BLOCK_SIZE 为 2^n 时, 可以用 mask 获取针对 BLOCK_SIZE 的余数 118 | ) 119 | 120 | if len(ciphertext) < BlockSize { 121 | err = fmt.Errorf("the length of ciphertext too short: %d", len(ciphertext)) 122 | return 123 | } 124 | if len(ciphertext)&BlockMask != 0 { 125 | err = fmt.Errorf("ciphertext is not a multiple of the block size, the length is %d", len(ciphertext)) 126 | return 127 | } 128 | 129 | plaintext := make([]byte, len(ciphertext)) // len(plaintext) >= BLOCK_SIZE 130 | 131 | // 解密 132 | block, err := aes.NewCipher(aesKey) 133 | if err != nil { 134 | panic(err) 135 | } 136 | mode := cipher.NewCBCDecrypter(block, aesKey[:16]) 137 | mode.CryptBlocks(plaintext, ciphertext) 138 | 139 | // PKCS#7 去除补位 140 | amountToPad := int(plaintext[len(plaintext)-1]) 141 | if amountToPad < 1 || amountToPad > BlockSize { 142 | err = fmt.Errorf("the amount to pad is incorrect: %d", amountToPad) 143 | return 144 | } 145 | plaintext = plaintext[:len(plaintext)-amountToPad] 146 | 147 | // 反拼接 148 | // len(plaintext) == 16+4+len(rawXMLMsg)+len(appId) 149 | if len(plaintext) <= 20 { 150 | err = fmt.Errorf("plaintext too short, the length is %d", len(plaintext)) 151 | return 152 | } 153 | rawXMLMsgLen := int(decodeNetworkByteOrder(plaintext[16:20])) 154 | if rawXMLMsgLen < 0 { 155 | err = fmt.Errorf("incorrect msg length: %d", rawXMLMsgLen) 156 | return 157 | } 158 | appIDOffset := 20 + rawXMLMsgLen 159 | if len(plaintext) <= appIDOffset { 160 | err = fmt.Errorf("msg length too large: %d", rawXMLMsgLen) 161 | return 162 | } 163 | 164 | random = plaintext[:16:20] 165 | rawXMLMsg = plaintext[20:appIDOffset:appIDOffset] 166 | appID = plaintext[appIDOffset:] 167 | return 168 | } 169 | 170 | // 把整数 n 格式化成 4 字节的网络字节序 171 | func encodeNetworkByteOrder(orderBytes []byte, n uint32) { 172 | orderBytes[0] = byte(n >> 24) 173 | orderBytes[1] = byte(n >> 16) 174 | orderBytes[2] = byte(n >> 8) 175 | orderBytes[3] = byte(n) 176 | } 177 | 178 | // 从 4 字节的网络字节序里解析出整数 179 | func decodeNetworkByteOrder(orderBytes []byte) (n uint32) { 180 | return uint32(orderBytes[0])<<24 | 181 | uint32(orderBytes[1])<<16 | 182 | uint32(orderBytes[2])<<8 | 183 | uint32(orderBytes[3]) 184 | } 185 | -------------------------------------------------------------------------------- /utils/error.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // CommonError 微信返回的通用错误json 4 | type CommonError struct { 5 | ErrCode int64 `json:"errcode,omitempty"` 6 | ErrMsg string `json:"errmsg,omitempty"` 7 | } 8 | 9 | func (this CommonError) IsOK() bool { 10 | return this.ErrCode == 0 11 | } 12 | 13 | type Pagination struct { 14 | Offset int `json:"offset"` 15 | Count int `json:"count"` 16 | } -------------------------------------------------------------------------------- /utils/helper.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // Bool is a helper routine that allocates a new bool value 4 | // to store v and returns a pointer to it. 5 | func Bool(v bool) *bool { 6 | return &v 7 | } 8 | 9 | // Int is a helper routine that allocates a new int value 10 | // to store v and returns a pointer to it. 11 | func Int(v int) *int { 12 | return &v 13 | } 14 | 15 | // Int32 is a helper routine that allocates a new int32 value 16 | // to store v and returns a pointer to it. 17 | func Int32(v int32) *int32 { 18 | return &v 19 | } 20 | 21 | // Int64 is a helper routine that allocates a new int64 value 22 | // to store v and returns a pointer to it. 23 | func Int64(v int64) *int64 { 24 | return &v 25 | } 26 | 27 | // Float32 is a helper routine that allocates a new float32 value 28 | // to store v and returns a pointer to it. 29 | func Float32(v float32) *float32 { 30 | return &v 31 | } 32 | 33 | // Float64 is a helper routine that allocates a new float64 value 34 | // to store v and returns a pointer to it. 35 | func Float64(v float64) *float64 { 36 | return &v 37 | } 38 | 39 | // Uint32 is a helper routine that allocates a new uint32 value 40 | // to store v and returns a pointer to it. 41 | func Uint32(v uint32) *uint32 { 42 | return &v 43 | } 44 | 45 | // Uint64 is a helper routine that allocates a new uint64 value 46 | // to store v and returns a pointer to it. 47 | func Uint64(v uint64) *uint64 { 48 | return &v 49 | } 50 | 51 | // String is a helper routine that allocates a new string value 52 | // to store v and returns a pointer to it. 53 | func String(v string) *string { 54 | return &v 55 | } 56 | 57 | 58 | // String is a helper routine that allocates a new string value 59 | // to store v and returns a pointer to it. 60 | func NewCharData(v CharData) *CharData { 61 | return &v 62 | } -------------------------------------------------------------------------------- /utils/http.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "mime/multipart" 10 | "net/http" 11 | "os" 12 | ) 13 | 14 | //HTTPGet get 请求 15 | func HTTPGet(uri string) ([]byte, int, error) { 16 | response, err := http.Get(uri) 17 | if err != nil { 18 | return nil, http.StatusBadRequest, err 19 | } 20 | 21 | defer response.Body.Close() 22 | if response.StatusCode < http.StatusOK || 23 | response.StatusCode >= http.StatusMultipleChoices { 24 | return nil, response.StatusCode, 25 | fmt.Errorf("http get error : uri=%v , statusCode=%v", uri, response.StatusCode) 26 | } 27 | body,err := ioutil.ReadAll(response.Body) 28 | return body,response.StatusCode,err 29 | } 30 | 31 | func HTTPDelete(uri string) ([]byte, int, error) { 32 | req, err := http.NewRequest("DELETE", uri, nil) 33 | if err != nil { 34 | return nil, http.StatusBadRequest, err 35 | } 36 | 37 | client := &http.Client{} 38 | response, err := client.Do(req) 39 | if err != nil { 40 | return nil, response.StatusCode, err 41 | } 42 | 43 | defer response.Body.Close() 44 | if response.StatusCode < http.StatusOK || 45 | response.StatusCode >= http.StatusMultipleChoices { 46 | return nil, response.StatusCode, 47 | fmt.Errorf("http get error : uri=%v , statusCode=%v", uri, response.StatusCode) 48 | } 49 | 50 | body, err := ioutil.ReadAll(response.Body) 51 | return body,response.StatusCode,err 52 | } 53 | 54 | 55 | //PostJSON post json 数据请求 56 | func PostJSON(uri string, obj interface{}) ([]byte, int, error) { 57 | jsonData, err := json.Marshal(obj) 58 | if err != nil { 59 | return nil, http.StatusBadRequest, err 60 | } 61 | body := bytes.NewBuffer(jsonData) 62 | response, err := http.Post(uri, "application/json;charset=utf-8", body) 63 | if err != nil { 64 | return nil, response.StatusCode, err 65 | } 66 | defer response.Body.Close() 67 | 68 | if response.StatusCode < http.StatusOK || 69 | response.StatusCode >= http.StatusMultipleChoices { 70 | return nil, http.StatusBadRequest, fmt.Errorf("http get error : uri=%v , statusCode=%v", uri, response.StatusCode) 71 | } 72 | body2, err := ioutil.ReadAll(response.Body) 73 | return body2,response.StatusCode,err 74 | } 75 | 76 | //PostJSON post json 数据请求 77 | func PostJSONRaw(uri string, obj []byte) ([]byte, int, error) { 78 | body := bytes.NewBuffer(obj) 79 | response, err := http.Post(uri, "application/json;charset=utf-8", body) 80 | if err != nil { 81 | return nil, http.StatusBadRequest, err 82 | } 83 | defer response.Body.Close() 84 | 85 | if response.StatusCode < http.StatusOK || 86 | response.StatusCode >= http.StatusMultipleChoices { 87 | return nil, response.StatusCode, fmt.Errorf("http get error : uri=%v , statusCode=%v", uri, response.StatusCode) 88 | } 89 | body2, err := ioutil.ReadAll(response.Body) 90 | return body2,response.StatusCode,err 91 | } 92 | 93 | //PostFile 上传文件 94 | func PostFile(fieldname, filename, uri string) ([]byte, int, error) { 95 | fields := []MultipartFormField{ 96 | { 97 | IsFile: true, 98 | Fieldname: fieldname, 99 | Filename: filename, 100 | }, 101 | } 102 | return PostMultipartForm(fields, uri) 103 | } 104 | 105 | //PostFile 上传文件 106 | func PostReader(fieldname, filename, uri string, reader io.Reader) ([]byte, int, error) { 107 | fields := []MultipartFormField{ 108 | { 109 | IsFile: false, 110 | Fieldname: fieldname, 111 | Filename: filename, 112 | Reader: reader, 113 | }, 114 | } 115 | return PostMultipartForm(fields, uri) 116 | } 117 | 118 | //MultipartFormField 保存文件或其他字段信息 119 | type MultipartFormField struct { 120 | IsFile bool 121 | Fieldname string 122 | Value []byte 123 | Filename string 124 | Reader io.Reader 125 | } 126 | 127 | //PostMultipartForm 上传文件或其他多个字段 128 | func PostMultipartForm(fields []MultipartFormField, uri string) (respBody []byte, code int, err error) { 129 | bodyBuf := &bytes.Buffer{} 130 | bodyWriter := multipart.NewWriter(bodyBuf) 131 | code = http.StatusBadRequest 132 | 133 | for _, field := range fields { 134 | if field.IsFile { 135 | fileWriter, e := bodyWriter.CreateFormFile(field.Fieldname, field.Filename) 136 | if e != nil { 137 | err = fmt.Errorf("error writing to buffer , err=%v", e) 138 | return 139 | } 140 | 141 | fh, e := os.Open(field.Filename) 142 | if e != nil { 143 | err = fmt.Errorf("error opening file , err=%v", e) 144 | return 145 | } 146 | defer fh.Close() 147 | 148 | if _, err = io.Copy(fileWriter, fh); err != nil { 149 | return 150 | } 151 | } else { 152 | var partWriter io.Writer = nil 153 | var e error = nil 154 | // 155 | if field.Reader == nil { 156 | field.Reader = bytes.NewReader(field.Value) 157 | partWriter, e = bodyWriter.CreateFormField(field.Fieldname) 158 | } else { 159 | partWriter, e = bodyWriter.CreateFormFile(field.Fieldname, field.Filename) 160 | } 161 | if e != nil { 162 | err = e 163 | return 164 | } 165 | 166 | if _, err = io.Copy(partWriter, field.Reader); err != nil { 167 | return 168 | } 169 | } 170 | } 171 | 172 | contentType := bodyWriter.FormDataContentType() 173 | bodyWriter.Close() 174 | 175 | response, e := http.Post(uri, contentType, bodyBuf) 176 | if e != nil { 177 | err = e 178 | return 179 | } 180 | 181 | code = response.StatusCode 182 | defer response.Body.Close() 183 | if response.StatusCode < http.StatusOK || 184 | response.StatusCode >= http.StatusMultipleChoices { 185 | return 186 | } 187 | respBody, err = ioutil.ReadAll(response.Body) 188 | return 189 | } -------------------------------------------------------------------------------- /utils/message.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/xml" 5 | "errors" 6 | ) 7 | 8 | 9 | //ErrInvalidReply 无效的回复 10 | var ErrInvalidReply = errors.New("无效的回复消息") 11 | 12 | //ErrUnsupportReply 不支持的回复类型 13 | var ErrUnsupportReply = errors.New("不支持的回复消息") 14 | 15 | // MsgType 基本消息类型 16 | type MsgType string 17 | 18 | // EventType 事件类型 19 | type EventType string 20 | 21 | const ( 22 | //MsgTypeText 表示文本消息 23 | MsgTypeText MsgType = "text" 24 | //MsgTypeImage 表示图片消息 25 | MsgTypeImage = "image" 26 | //MsgTypeVoice 表示语音消息 27 | MsgTypeVoice = "voice" 28 | //MsgTypeVideo 表示视频消息 29 | MsgTypeVideo = "video" 30 | //MsgTypeShortVideo 表示短视频消息[限接收] 31 | MsgTypeShortVideo = "shortvideo" 32 | //MsgTypeLocation 表示坐标消息[限接收] 33 | MsgTypeLocation = "location" 34 | //MsgTypeLink 表示链接消息[限接收] 35 | MsgTypeLink = "link" 36 | //MsgTypeMusic 表示音乐消息[限回复] 37 | MsgTypeMusic = "music" 38 | //MsgTypeNews 表示图文消息[限回复] 39 | MsgTypeNews = "news" 40 | //MsgTypeTransfer 表示消息消息转发到客服 41 | MsgTypeTransfer = "transfer_customer_service" 42 | //MsgTypeEvent 表示事件推送消息 43 | MsgTypeEvent = "event" 44 | ) 45 | 46 | const ( 47 | //EventSubscribe 订阅 48 | EventSubscribe EventType = "subscribe" 49 | //EventUnsubscribe 取消订阅 50 | EventUnsubscribe = "unsubscribe" 51 | //EventScan 用户已经关注公众号,则微信会将带场景值扫描事件推送给开发者 52 | EventScan = "SCAN" 53 | //EventLocation 上报地理位置事件 54 | EventLocation = "LOCATION" 55 | //EventClick 点击菜单拉取消息时的事件推送 56 | EventClick = "CLICK" 57 | //EventView 点击菜单跳转链接时的事件推送 58 | EventView = "VIEW" 59 | //EventScancodePush 扫码推事件的事件推送 60 | EventScancodePush = "scancode_push" 61 | //EventScancodeWaitmsg 扫码推事件且弹出“消息接收中”提示框的事件推送 62 | EventScancodeWaitmsg = "scancode_waitmsg" 63 | //EventPicSysphoto 弹出系统拍照发图的事件推送 64 | EventPicSysphoto = "pic_sysphoto" 65 | //EventPicPhotoOrAlbum 弹出拍照或者相册发图的事件推送 66 | EventPicPhotoOrAlbum = "pic_photo_or_album" 67 | //EventPicWeixin 弹出微信相册发图器的事件推送 68 | EventPicWeixin = "pic_weixin" 69 | //EventLocationSelect 弹出地理位置选择器的事件推送 70 | EventLocationSelect = "location_select" 71 | ) 72 | 73 | 74 | // 微信服务器请求 http body 75 | type RequestEncryptedXMLMsg struct { 76 | XMLName struct{} `xml:"xml" json:"-"` 77 | 78 | ToUserName string `xml:"ToUserName"` 79 | EncryptedMsg string `xml:"Encrypt"` 80 | } 81 | 82 | //ResponseEncryptedXMLMsg 需要返回的消息体 83 | type ResponseEncryptedXMLMsg struct { 84 | XMLName struct{} `xml:"xml" json:"-"` 85 | EncryptedMsg string `xml:"Encrypt" json:"Encrypt"` 86 | MsgSignature string `xml:"MsgSignature" json:"MsgSignature"` 87 | Timestamp int64 `xml:"TimeStamp" json:"TimeStamp"` 88 | Nonce string `xml:"Nonce" json:"Nonce"` 89 | } 90 | 91 | type Reply interface { 92 | SetToUserName(toUserName string)() 93 | SetFromUserName(fromUserName string)() 94 | SetCreateTime(createTime int64)() 95 | SetMsgType(msgType MsgType)() 96 | GetMsgType()(MsgType) 97 | } 98 | 99 | // CommonToken 消息中通用的结构 100 | type CommonToken struct { 101 | XMLName xml.Name `xml:"xml"` 102 | ToUserName CharData `xml:"ToUserName"` 103 | FromUserName CharData `xml:"FromUserName"` 104 | CreateTime int64 `xml:"CreateTime"` 105 | MsgType CharData `xml:"MsgType"` 106 | } 107 | 108 | //SetToUserName set ToUserName 109 | func (msg *CommonToken) SetToUserName(toUserName string) { 110 | msg.ToUserName = CharData(toUserName) 111 | } 112 | 113 | //SetFromUserName set FromUserName 114 | func (msg *CommonToken) SetFromUserName(fromUserName string) { 115 | msg.FromUserName = CharData(fromUserName) 116 | } 117 | 118 | //SetCreateTime set createTime 119 | func (msg *CommonToken) SetCreateTime(createTime int64) { 120 | msg.CreateTime = createTime 121 | } 122 | 123 | //SetMsgType set MsgType 124 | func (msg *CommonToken) SetMsgType(msgType MsgType) { 125 | msg.MsgType = CharData(msgType) 126 | } 127 | 128 | func (msg CommonToken) GetMsgType() MsgType { 129 | return MsgType(msg.MsgType) 130 | } 131 | 132 | 133 | //Text 文本消息 134 | type TextMessage struct { 135 | CommonToken 136 | Content CharData `xml:"Content"` 137 | } 138 | 139 | //NewText 初始化文本消息 140 | func NewText(content string) *TextMessage { 141 | msg := &TextMessage{ 142 | Content:CharData(content), 143 | } 144 | msg.MsgType = "text" 145 | return msg 146 | } 147 | 148 | //Text 文本消息 149 | type VideoMessage struct { 150 | CommonToken 151 | Video struct { 152 | MediaId CharData `xml:"MediaId"` 153 | Title CharData `xml:"-"` 154 | Description CharData `xml:"-"` 155 | } `xml:"Video"` 156 | } 157 | 158 | //NewText 初始化图片消息 159 | func NewVideo(media_id string,title string,desc string) *VideoMessage { 160 | msg := &VideoMessage{ 161 | Video:struct{ 162 | MediaId CharData `xml:"MediaId"` 163 | Title CharData `xml:"-"` 164 | Description CharData `xml:"-"` 165 | }{ 166 | MediaId:CharData(media_id), 167 | Title:CharData(title), 168 | Description:CharData(desc), 169 | }, 170 | } 171 | msg.MsgType = "video" 172 | return msg 173 | } 174 | 175 | 176 | //Text 文本消息 177 | type VoiceMessage struct { 178 | CommonToken 179 | Voice struct { 180 | MediaId CharData `xml:"MediaId"` 181 | } `xml:"Voice"` 182 | } 183 | 184 | //NewText 初始化图片消息 185 | func NewVoice(media_id string) *VoiceMessage { 186 | msg := &VoiceMessage{ 187 | Voice:struct{ 188 | MediaId CharData `xml:"MediaId"` 189 | }{ 190 | MediaId:CharData(media_id), 191 | }, 192 | } 193 | msg.MsgType = "voice" 194 | return msg 195 | } 196 | 197 | //Text 文本消息 198 | type ImageMessage struct { 199 | CommonToken 200 | Image struct { 201 | MediaId CharData `xml:"MediaId"` 202 | } `xml:"Image"` 203 | } 204 | 205 | //NewText 初始化图片消息 206 | func NewImage(media_id string) *ImageMessage { 207 | msg := new(ImageMessage) 208 | msg.Image.MediaId = CharData(media_id) 209 | msg.MsgType = "image" 210 | return msg 211 | } 212 | 213 | //NewText 初始化转发消息 214 | func NewTransfer() *CommonToken { 215 | msg := &CommonToken{} 216 | msg.MsgType = "transfer_customer_service" 217 | return msg 218 | } 219 | 220 | //ResAccessToken struct 221 | type ResAccessToken struct { 222 | CommonError 223 | 224 | AccessToken string `json:"access_token"` 225 | ExpiresIn int64 `json:"expires_in"` 226 | } 227 | 228 | type ResJsTicket struct { 229 | CommonError 230 | 231 | Ticket string `json:"ticket"` 232 | ExpiresIn int64 `json:"expires_in"` 233 | } -------------------------------------------------------------------------------- /utils/nonce.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "time" 5 | "math/rand" 6 | ) 7 | 8 | const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 9 | const ( 10 | letterIdxBits = 6 // 6 bits to represent a letter index 11 | letterIdxMask = 1<= 0; { 20 | if remain == 0 { 21 | cache, remain = src.Int63(), letterIdxMax 22 | } 23 | if idx := int(cache & letterIdxMask); idx < len(letterBytes) { 24 | b[i] = letterBytes[idx] 25 | i-- 26 | } 27 | cache >>= letterIdxBits 28 | remain-- 29 | } 30 | 31 | return string(b) 32 | } -------------------------------------------------------------------------------- /utils/sign_struct.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | "hash" 10 | "io" 11 | "log" 12 | "reflect" 13 | "sort" 14 | "strconv" 15 | "strings" 16 | ) 17 | 18 | type SignStruct struct { 19 | Elements map[string]string `json:"elements"` 20 | Keys []string `json:"keys"` 21 | ToLower bool `doc:"是否自动将key小写"` 22 | Tag string `json:"tag" doc:"使用xml/json或者直接字段名"` 23 | } 24 | 25 | func (this SignStruct) isValidType(kind reflect.Kind) bool { 26 | switch kind { 27 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8: 28 | return true 29 | case reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64, reflect.String, reflect.Bool: 30 | return true 31 | case reflect.Ptr: 32 | return true 33 | } 34 | return false 35 | } 36 | 37 | func (this *SignStruct) fetchElem(tp reflect.StructField, value reflect.Value, tag string) { 38 | name := tag 39 | if len(name) < 1 { 40 | name = tp.Name 41 | } 42 | if this.ToLower { 43 | name = strings.ToLower(name) 44 | } 45 | 46 | // 排除空值 47 | if value.Interface() == reflect.Zero(value.Type()).Interface() { 48 | return 49 | } 50 | 51 | switch value.Type().Kind() { 52 | case reflect.Int: 53 | this.Elements[name] = strconv.FormatInt(value.Int(), 10) 54 | case reflect.Int8: 55 | this.Elements[name] = strconv.FormatInt(value.Int(), 10) 56 | case reflect.Int16: 57 | this.Elements[name] = strconv.FormatInt(value.Int(), 10) 58 | case reflect.Int32: 59 | this.Elements[name] = strconv.FormatInt(value.Int(), 10) 60 | case reflect.Int64: 61 | this.Elements[name] = strconv.FormatInt(value.Int(), 10) 62 | case reflect.Uint: 63 | this.Elements[name] = strconv.FormatUint(value.Uint(), 10) 64 | case reflect.Uint8: 65 | this.Elements[name] = strconv.FormatUint(value.Uint(), 10) 66 | case reflect.Uint16: 67 | this.Elements[name] = strconv.FormatUint(value.Uint(), 10) 68 | case reflect.Uint32: 69 | this.Elements[name] = strconv.FormatUint(value.Uint(), 10) 70 | case reflect.Uint64: 71 | this.Elements[name] = strconv.FormatUint(value.Uint(), 10) 72 | case reflect.Float32: 73 | this.Elements[name] = strconv.FormatFloat(value.Float(), 'f', -1, 32) 74 | case reflect.Float64: 75 | this.Elements[name] = strconv.FormatFloat(value.Float(), 'f', -1, 64) 76 | case reflect.String: 77 | this.Elements[name] = value.String() 78 | case reflect.Bool: 79 | this.Elements[name] = strconv.FormatBool(value.Bool()) 80 | default: 81 | panic("invalid type") 82 | } 83 | this.Keys = append(this.Keys, name) 84 | } 85 | 86 | func (this *SignStruct) genSignData(variable interface{}) (err error) { 87 | value := reflect.ValueOf(variable) 88 | if value.Kind() != reflect.Ptr { 89 | err = errors.New("invalid varibal to sign (need tobe pointer)") 90 | return 91 | } 92 | value = value.Elem() 93 | // 分配Map 94 | this.Elements = make(map[string]string) 95 | this.Keys = make([]string, 0) 96 | return this.genSignDataImp(value) 97 | } 98 | 99 | func (this SignStruct) getTag(field *reflect.StructField) (tag string, tags []string) { 100 | if strings.ToLower(this.Tag) == "xml" { 101 | tag = field.Tag.Get("xml") 102 | } else if strings.ToLower(this.Tag) == "json" { 103 | tag = field.Tag.Get("json") 104 | } 105 | 106 | tags = strings.Split(tag, ",") 107 | if len(tags) > 0 { 108 | tag = tags[0] 109 | } 110 | return 111 | } 112 | 113 | func (this *SignStruct) genSignDataImp(value reflect.Value) (err error) { 114 | if value.Kind() != reflect.Struct { 115 | err = errors.New("invalid varibal to sign (need tobe struct)") 116 | return 117 | } 118 | tp := value.Type() 119 | count := tp.NumField() 120 | if count == 0 { 121 | return 122 | } 123 | 124 | for i := 0; i < count; i++ { 125 | field := tp.Field(i) 126 | 127 | if field.Anonymous { 128 | this.genSignDataImp(value.Field(i)) 129 | continue 130 | } 131 | 132 | if "-" == field.Tag.Get("sign") { 133 | continue 134 | } 135 | tag, _ := this.getTag(&field) 136 | if tag == "-" { 137 | tag = "" 138 | } 139 | 140 | fieldTp := field.Type 141 | if !this.isValidType(fieldTp.Kind()) { 142 | err = fmt.Errorf("unsupport type %s", fieldTp.String()) 143 | return 144 | } 145 | 146 | if fieldTp.Kind() == reflect.Ptr { 147 | tmpVar := value.Field(i) 148 | if tmpVar.IsNil() { 149 | continue 150 | } 151 | this.fetchElem(field, tmpVar.Elem(), tag) 152 | 153 | } else { 154 | this.fetchElem(field, value.Field(i), tag) 155 | } 156 | } 157 | return 158 | } 159 | 160 | func (this SignStruct) Sign(variable interface{}, fn func() hash.Hash, apiKey string) (sign string, err error) { 161 | if err = this.genSignData(variable); err != nil { 162 | return 163 | } 164 | sort.Strings(this.Keys) 165 | if fn == nil { 166 | fn = md5.New 167 | } 168 | h := fn() 169 | 170 | firstFlag := true 171 | for _, k := range this.Keys { 172 | if !firstFlag { 173 | io.WriteString(h, "&") 174 | } else { 175 | firstFlag = false 176 | } 177 | v := this.Elements[k] 178 | 179 | log.Printf("key %s value %s\n", k, v) 180 | 181 | io.WriteString(h, k) 182 | io.WriteString(h, "=") 183 | io.WriteString(h, v) 184 | } 185 | 186 | if len(apiKey) > 0 { 187 | log.Printf("key %s value %s\n", "key", apiKey) 188 | io.WriteString(h, "&key=") 189 | io.WriteString(h, apiKey) 190 | } 191 | 192 | signature := make([]byte, h.Size()*2) 193 | hex.Encode(signature, h.Sum(nil)) 194 | return string(bytes.ToUpper(signature)), nil 195 | 196 | } 197 | -------------------------------------------------------------------------------- /utils/sign_struct_value.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "errors" 7 | "fmt" 8 | "hash" 9 | "io" 10 | "reflect" 11 | "sort" 12 | "strconv" 13 | "crypto/sha1" 14 | "log" 15 | ) 16 | 17 | type SignStructValue struct { 18 | Keys []string `json:"keys"` 19 | } 20 | 21 | func (this SignStructValue) isValidType(kind reflect.Kind) bool { 22 | switch kind { 23 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8: 24 | return true 25 | case reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64, reflect.String, reflect.Bool: 26 | return true 27 | case reflect.Ptr: 28 | return true 29 | } 30 | return false 31 | } 32 | 33 | func (this *SignStructValue) fetchElem(tp reflect.StructField, value reflect.Value) { 34 | // 排除空值 35 | if value.Interface() == reflect.Zero(value.Type()).Interface() { 36 | return 37 | } 38 | 39 | var varStr string 40 | switch value.Type().Kind() { 41 | case reflect.Int: 42 | varStr = strconv.FormatInt(value.Int(), 10) 43 | case reflect.Int8: 44 | varStr = strconv.FormatInt(value.Int(), 10) 45 | case reflect.Int16: 46 | varStr = strconv.FormatInt(value.Int(), 10) 47 | case reflect.Int32: 48 | varStr = strconv.FormatInt(value.Int(), 10) 49 | case reflect.Int64: 50 | varStr = strconv.FormatInt(value.Int(), 10) 51 | case reflect.Uint: 52 | varStr = strconv.FormatUint(value.Uint(), 10) 53 | case reflect.Uint8: 54 | varStr = strconv.FormatUint(value.Uint(), 10) 55 | case reflect.Uint16: 56 | varStr = strconv.FormatUint(value.Uint(), 10) 57 | case reflect.Uint32: 58 | varStr = strconv.FormatUint(value.Uint(), 10) 59 | case reflect.Uint64: 60 | varStr = strconv.FormatUint(value.Uint(), 10) 61 | case reflect.Float32: 62 | varStr = strconv.FormatFloat(value.Float(), 'f', -1, 32) 63 | case reflect.Float64: 64 | varStr = strconv.FormatFloat(value.Float(), 'f', -1, 64) 65 | case reflect.String: 66 | varStr = value.String() 67 | case reflect.Bool: 68 | varStr = strconv.FormatBool(value.Bool()) 69 | default: 70 | panic("invalid type") 71 | } 72 | this.Keys = append(this.Keys, varStr) 73 | } 74 | 75 | func (this *SignStructValue) genSignData(variable interface{}) (err error) { 76 | value := reflect.ValueOf(variable) 77 | if value.Kind() != reflect.Ptr { 78 | err = errors.New("invalid varibal to sign (need tobe pointer)") 79 | return 80 | } 81 | value = value.Elem() 82 | // 分配Map 83 | this.Keys = make([]string, 0) 84 | return this.genSignDataImp(value) 85 | } 86 | 87 | func (this *SignStructValue) genSignDataImp(value reflect.Value) (err error) { 88 | if value.Kind() != reflect.Struct { 89 | err = errors.New("invalid varibal to sign (need tobe struct)") 90 | return 91 | } 92 | tp := value.Type() 93 | count := tp.NumField() 94 | if count == 0 { 95 | return 96 | } 97 | 98 | for i := 0; i < count; i++ { 99 | field := tp.Field(i) 100 | 101 | if field.Anonymous { 102 | this.genSignDataImp(value.Field(i)) 103 | continue 104 | } 105 | 106 | if "-" == field.Tag.Get("sign") { 107 | continue 108 | } 109 | 110 | fieldTp := field.Type 111 | if !this.isValidType(fieldTp.Kind()) { 112 | err = fmt.Errorf("unsupport type %s", fieldTp.String()) 113 | return 114 | } 115 | 116 | if fieldTp.Kind() == reflect.Ptr { 117 | tmpVar := value.Field(i) 118 | if tmpVar.IsNil() { 119 | continue 120 | } 121 | this.fetchElem(field, tmpVar.Elem()) 122 | 123 | } else { 124 | this.fetchElem(field, value.Field(i)) 125 | } 126 | } 127 | return 128 | } 129 | 130 | func (this SignStructValue) Sign(variable interface{}, fn func() hash.Hash) (sign string, err error) { 131 | if err = this.genSignData(variable); err != nil { 132 | return 133 | } 134 | sort.Strings(this.Keys) 135 | if fn == nil { 136 | fn = sha1.New 137 | } 138 | h := fn() 139 | for _, k := range this.Keys { 140 | log.Printf("key: %s",k) 141 | io.WriteString(h, k) 142 | } 143 | 144 | signature := make([]byte, h.Size()*2) 145 | hex.Encode(signature, h.Sum(nil)) 146 | return string(bytes.ToUpper(signature)), nil 147 | 148 | } 149 | -------------------------------------------------------------------------------- /utils/signature.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/sha1" 5 | "fmt" 6 | "io" 7 | "sort" 8 | "encoding/hex" 9 | ) 10 | 11 | //Signature sha1签名 12 | func Signature(params ...string) string { 13 | sort.Strings(params) 14 | h := sha1.New() 15 | for _, s := range params { 16 | io.WriteString(h, s) 17 | } 18 | return fmt.Sprintf("%x", h.Sum(nil)) 19 | } 20 | 21 | // 微信 js-sdk wx.config 的参数签名. 22 | func WXConfigSign(jsapiTicket, nonceStr, timestamp, url string) (signature string) { 23 | n := len("jsapi_ticket=") + len(jsapiTicket) + 24 | len("&noncestr=") + len(nonceStr) + 25 | len("×tamp=") + len(timestamp) + 26 | len("&url=") + len(url) 27 | 28 | buf := make([]byte, 0, n) 29 | 30 | buf = append(buf, "jsapi_ticket="...) 31 | buf = append(buf, jsapiTicket...) 32 | buf = append(buf, "&noncestr="...) 33 | buf = append(buf, nonceStr...) 34 | buf = append(buf, "×tamp="...) 35 | buf = append(buf, timestamp...) 36 | buf = append(buf, "&url="...) 37 | buf = append(buf, url...) 38 | 39 | hashsum := sha1.Sum(buf) 40 | return hex.EncodeToString(hashsum[:]) 41 | } 42 | -------------------------------------------------------------------------------- /utils/time.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "time" 4 | 5 | //GetCurrTs return current timestamps 6 | func GetCurrTs() int64 { 7 | return time.Now().Unix() 8 | } 9 | -------------------------------------------------------------------------------- /vendor/vendor.json: -------------------------------------------------------------------------------- 1 | { 2 | "comment": "", 3 | "ignore": "test", 4 | "package": [ 5 | { 6 | "path": "github.com/qjw/go-wx-sdk", 7 | "revision": "", 8 | "tree": true 9 | }, 10 | { 11 | "checksumSHA1": "OU/wHTJqhyQfyRnXMVWx1Ox06kQ=", 12 | "path": "gopkg.in/redis.v5", 13 | "revision": "a16aeec10ff407b1e7be6dd35797ccf5426ef0f0", 14 | "revisionTime": "2017-03-04T11:38:25Z" 15 | }, 16 | { 17 | "checksumSHA1": "efyYmNqK7vcPhXW4KXfwbdA1wr4=", 18 | "path": "gopkg.in/redis.v5/internal", 19 | "revision": "a16aeec10ff407b1e7be6dd35797ccf5426ef0f0", 20 | "revisionTime": "2017-03-04T11:38:25Z" 21 | }, 22 | { 23 | "checksumSHA1": "2Ek4SixeRSKOX3mUiBMs3Aw+Guc=", 24 | "path": "gopkg.in/redis.v5/internal/consistenthash", 25 | "revision": "a16aeec10ff407b1e7be6dd35797ccf5426ef0f0", 26 | "revisionTime": "2017-03-04T11:38:25Z" 27 | }, 28 | { 29 | "checksumSHA1": "rJYVKcBrwYUGl7nuuusmZGrt8mY=", 30 | "path": "gopkg.in/redis.v5/internal/hashtag", 31 | "revision": "a16aeec10ff407b1e7be6dd35797ccf5426ef0f0", 32 | "revisionTime": "2017-03-04T11:38:25Z" 33 | }, 34 | { 35 | "checksumSHA1": "zsH5BF9qc31R7eEEVYLsjbIigDQ=", 36 | "path": "gopkg.in/redis.v5/internal/pool", 37 | "revision": "a16aeec10ff407b1e7be6dd35797ccf5426ef0f0", 38 | "revisionTime": "2017-03-04T11:38:25Z" 39 | }, 40 | { 41 | "checksumSHA1": "EqPdu5g8NhzxQOMCvzbreTQlzVE=", 42 | "path": "gopkg.in/redis.v5/internal/proto", 43 | "revision": "a16aeec10ff407b1e7be6dd35797ccf5426ef0f0", 44 | "revisionTime": "2017-03-04T11:38:25Z" 45 | } 46 | ], 47 | "rootPath": "github.com/qjw/go-wx-sdk" 48 | } 49 | --------------------------------------------------------------------------------