workweixin.go 12 KB

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