workweixin.go 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. package workweixin
  2. import (
  3. "context"
  4. "crypto/tls"
  5. // "encoding/json"
  6. "net/http"
  7. "time"
  8. "github.com/beego/beego/v2/client/httplib"
  9. "github.com/beego/beego/v2/core/logs"
  10. "github.com/mindoc-org/mindoc/cache"
  11. "github.com/mindoc-org/mindoc/conf"
  12. )
  13. // doc
  14. // - 全局错误码: https://work.weixin.qq.com/api/doc/90000/90139/90313
  15. const (
  16. AccessTokenCacheKey = "access-token-cache-key"
  17. ContactAccessTokenCacheKey = "contact-access-token-cache-key"
  18. )
  19. // 获取访问凭据-请求响应结构
  20. type AccessTokenResponse struct {
  21. ErrCode int `json:"errcode"`
  22. ErrMsg string `json:"errmsg"`
  23. AccessToken string `json:"access_token"` // 获取到的凭证,最长为512字节
  24. ExpiresIn int `json:"expires_in"` // 凭证的有效时间(秒)
  25. }
  26. // 获取用户Id-请求响应结构
  27. type UserIdResponse struct {
  28. ErrCode int `json:"errcode"`
  29. ErrMsg string `json:"errmsg"`
  30. UserId string `json:"UserId"` // 企业成员UserID
  31. OpenId string `json:"OpenId"` // 非企业成员的标识,对当前企业唯一
  32. DeviceId string `json:"DeviceId"` // 设备号
  33. }
  34. // 获取成员ID列表-请求响应结构
  35. type UserListIdResponse struct {
  36. ErrCode int `json:"errcode"`
  37. ErrMsg string `json:"errmsg"`
  38. NextCursor string `json:"next_cursor"` // 分页游标,下次请求时填写以获取之后分页的记录
  39. DeptUser []WorkWeixinDeptUserInfo `json:"dept_user"` // 用户-部门关系列表
  40. }
  41. // 获取用户信息-请求响应结构
  42. type UserInfoResponse struct {
  43. ErrCode int `json:"errcode"`
  44. ErrMsg string `json:"errmsg"`
  45. UserId string `json:"UserId"` // 企业成员UserID
  46. Name string `json:"name"` // 成员名称
  47. HideMobile int `json:"hide_mobile"` // 是否隐藏了手机号码
  48. Mobile string `json:"mobile"` // 手机号码
  49. Department []int `json:"department"` // 成员所属部门id列表
  50. Email string `json:"email"` // 邮箱
  51. IsLeaderInDept []int `json:"is_leader_in_dept"` // 表示在所在的部门内是否为上级
  52. IsLeader int `json:"isleader"` // 是否是部门上级(领导)
  53. Avatar string `json:"avatar"` // 头像url
  54. Alias string `json:"alias"` // 别名
  55. Status int `json:"status"` // 激活状态: 1=已激活,2=已禁用,4=未激活,5=退出企业
  56. MainDepartment int `json:"main_department"` // 主部门
  57. }
  58. // 访问凭据缓存-结构
  59. type AccessTokenCache struct {
  60. AccessToken string `json:"access_token"`
  61. ExpiresIn int `json:"expires_in"`
  62. UpdateTime time.Time `json:"update_time"`
  63. }
  64. // 企业微信用户信息-结构
  65. type WorkWeixinDeptUserInfo struct {
  66. UserId string `json:"UserId"` // 企业成员UserID
  67. Department int `json:"department"` // 成员所属部门id列表
  68. }
  69. // 企业微信用户信息-结构
  70. type WorkWeixinUserInfo struct {
  71. UserId string `json:"UserId"` // 企业成员UserID
  72. Name string `json:"name"` // 成员名称
  73. HideMobile int `json:"hide_mobile"` // 是否隐藏了手机号码
  74. Mobile string `json:"mobile"` // 手机号码
  75. Department []int `json:"department"` // 成员所属部门id列表
  76. Email string `json:"email"` // 邮箱
  77. IsLeaderInDept []int `json:"is_leader_in_dept"` // 表示在所在的部门内是否为上级
  78. IsLeader int `json:"isleader"` // 是否是部门上级(领导)
  79. Avatar string `json:"avatar"` // 头像url
  80. Alias string `json:"alias"` // 别名
  81. Status int `json:"status"` // 激活状态: 1=已激活,2=已禁用,4=未激活,5=退出企业
  82. MainDepartment int `json:"main_department"` // 主部门
  83. }
  84. func httpFilter(next httplib.Filter) httplib.Filter {
  85. return func(ctx context.Context, req *httplib.BeegoHTTPRequest) (*http.Response, error) {
  86. r := req.GetRequest()
  87. logs.Info("filter-url: ", r.URL)
  88. // Never forget invoke this. Or the request will not be sent
  89. return next(ctx, req)
  90. }
  91. }
  92. // 获取访问凭据-请求
  93. func RequestAccessToken(corpid string, secret string) (cache_token AccessTokenCache, ok bool) {
  94. url := "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
  95. req := httplib.Get(url)
  96. req.Param("corpid", corpid) // 企业ID
  97. req.Param("corpsecret", secret) // 应用的凭证密钥
  98. req.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: false})
  99. req.AddFilters(httpFilter)
  100. resp, err := req.Response()
  101. _ = resp
  102. var token AccessTokenCache
  103. if err != nil {
  104. logs.Error(err)
  105. return token, false
  106. }
  107. var atr AccessTokenResponse
  108. err = req.ToJSON(&atr)
  109. if err != nil {
  110. logs.Error(err)
  111. return token, false
  112. }
  113. token = AccessTokenCache{
  114. AccessToken: atr.AccessToken,
  115. ExpiresIn: atr.ExpiresIn,
  116. UpdateTime: time.Now(),
  117. }
  118. return token, true
  119. }
  120. // 获取访问凭据
  121. func GetAccessToken(is_contact bool) (access_token string, ok bool) {
  122. var cache_token AccessTokenCache
  123. cache_key := AccessTokenCacheKey
  124. if is_contact {
  125. cache_key = ContactAccessTokenCacheKey
  126. }
  127. err := cache.Get(cache_key, &cache_token)
  128. if err == nil {
  129. logs.Info("AccessToken从缓存读取成功")
  130. // TODO: access_token有效期判断, 刷新
  131. return cache_token.AccessToken, true
  132. } else {
  133. logs.Warning(err)
  134. workweixinConfig := conf.GetWorkWeixinConfig()
  135. logs.Debug("corp_id: ", workweixinConfig.CorpId)
  136. logs.Debug("agent_id: ", workweixinConfig.AgentId)
  137. logs.Debug("secret: ", workweixinConfig.Secret)
  138. logs.Debug("contact_secret: ", workweixinConfig.ContactSecret)
  139. secret := workweixinConfig.Secret
  140. if is_contact {
  141. secret = workweixinConfig.ContactSecret
  142. }
  143. new_token, ok := RequestAccessToken(workweixinConfig.CorpId, secret)
  144. if ok {
  145. logs.Debug(new_token)
  146. if err = cache.Put(cache_key, new_token, time.Second*time.Duration(new_token.ExpiresIn)); err == nil {
  147. logs.Info("AccessToken缓存写入成功")
  148. return new_token.AccessToken, true
  149. }
  150. logs.Warning("AccessToken缓存写入失败")
  151. return "", false
  152. }
  153. logs.Warning("AccessToken请求失败")
  154. return "", false
  155. }
  156. }
  157. // 获取用户id-请求
  158. func RequestUserId(access_token string, code string) (user_id string, ok bool) {
  159. url := "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo"
  160. req := httplib.Get(url)
  161. req.Param("access_token", access_token) // 应用调用接口凭证
  162. req.Param("code", code) // 通过成员授权获取到的code
  163. req.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: false})
  164. req.AddFilters(httpFilter)
  165. resp, err := req.Response()
  166. _ = resp
  167. if err != nil {
  168. logs.Error(err)
  169. return "", false
  170. }
  171. var uir UserIdResponse
  172. err = req.ToJSON(&uir)
  173. if err != nil {
  174. logs.Error(err)
  175. return "", false
  176. }
  177. return uir.UserId, true
  178. }
  179. /*
  180. 获取用户详细信息-请求
  181. 从2022年8月15日10点开始,“企业管理后台 - 管理工具 - 通讯录同步”的新增IP将不能再调用此接口
  182. url:https://developer.work.weixin.qq.com/document/path/96079
  183. */
  184. func RequestUserInfo(contact_access_token string, userid string) (user_info WorkWeixinUserInfo, error_msg string, ok bool) {
  185. url := "https://qyapi.weixin.qq.com/cgi-bin/user/get"
  186. req := httplib.Get(url)
  187. req.Param("access_token", contact_access_token) // 通讯录应用调用接口凭证
  188. req.Param("userid", userid) // 成员UserID
  189. req.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: false})
  190. req.AddFilters(httpFilter)
  191. resp_str, err := req.String()
  192. _ = resp_str
  193. var info WorkWeixinUserInfo
  194. if err != nil {
  195. logs.Error(err)
  196. return info, "请求失败", false
  197. } else {
  198. logs.Debug(resp_str)
  199. }
  200. var uir UserInfoResponse
  201. err = req.ToJSON(&uir)
  202. if err != nil {
  203. logs.Error(err)
  204. return info, "请求数据结果错误", false
  205. }
  206. if uir.ErrCode != 0 {
  207. return info, uir.ErrMsg, false
  208. }
  209. info = WorkWeixinUserInfo{
  210. UserId: uir.UserId,
  211. Name: uir.Name,
  212. HideMobile: uir.HideMobile,
  213. Mobile: uir.Mobile,
  214. Department: uir.Department,
  215. Email: uir.Email,
  216. IsLeaderInDept: uir.IsLeaderInDept,
  217. IsLeader: uir.IsLeader,
  218. Avatar: uir.Avatar,
  219. Alias: uir.Alias,
  220. Status: uir.Status,
  221. MainDepartment: uir.MainDepartment,
  222. }
  223. return info, "", true
  224. }
  225. /*
  226. 获取成员ID列表
  227. */
  228. func GetUserListId(contact_access_token string, userid string) (user_info WorkWeixinDeptUserInfo, error_msg string, ok bool) {
  229. url := "https://qyapi.weixin.qq.com/cgi-bin/user/list_id"
  230. req := httplib.Get(url)
  231. req.Param("access_token", contact_access_token) // 通讯录应用调用接口凭证
  232. req.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: false})
  233. req.AddFilters(httpFilter)
  234. respStr, err := req.String()
  235. _ = respStr
  236. var info WorkWeixinDeptUserInfo
  237. if err != nil {
  238. logs.Error(err)
  239. return info, "请求失败", false
  240. } else {
  241. logs.Debug(respStr)
  242. }
  243. // 返回响应
  244. var uir UserListIdResponse
  245. //获取用户信息失败: 请求数据结果错误
  246. err = req.ToJSON(&uir)
  247. if err != nil {
  248. logs.Error(err)
  249. return info, "请求数据结果错误", false
  250. }
  251. if uir.ErrCode != 0 {
  252. return info, uir.ErrMsg, false
  253. }
  254. // 判断userid 中是否还有当前用户id
  255. for i := 0; i < len(uir.DeptUser); i++ {
  256. if uir.DeptUser[i].UserId == userid {
  257. info = WorkWeixinDeptUserInfo{
  258. UserId: uir.DeptUser[i].UserId,
  259. }
  260. return info, "", true
  261. }
  262. }
  263. return info, uir.ErrMsg, false
  264. }