package dingtalk import ( "ai_scheduler/internal/config" "bytes" "context" "encoding/json" "errors" "io" "net/http" "net/url" ) // Client 使用官方接口获取 AccessToken,并通过 HTTP 直接调用 TopAPI type Client struct { cfg *config.Config } // NewDingTalkClient 基于配置创建钉钉客户端(无 fastwego 依赖) func NewDingTalkClient(cfg *config.Config) *Client { return &Client{cfg: cfg} } type UserDetail struct { UserID string `json:"userid"` Name string `json:"name"` UnionID string `json:"unionid"` } // getAccessToken 通过官方接口获取应用 AccessToken(简单实现,每次请求刷新,不做缓存) func (c *Client) getAccessToken(ctx context.Context) (string, error) { // 调用旧版 OAPI 获取 access_token,供 /topapi 使用 appKey := c.cfg.Tools.DingTalkBot.APIKey appSecret := c.cfg.Tools.DingTalkBot.APISecret params := url.Values{} params.Add("appkey", appKey) params.Add("appsecret", appSecret) req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://oapi.dingtalk.com/gettoken?"+params.Encode(), nil) if err != nil { return "", err } resp, err := http.DefaultClient.Do(req) if err != nil { return "", err } defer resp.Body.Close() b, err := io.ReadAll(resp.Body) if err != nil { return "", err } var r struct { ErrCode int `json:"errcode"` ErrMsg string `json:"errmsg"` AccessToken string `json:"access_token"` ExpiresIn int `json:"expires_in"` } if err := json.Unmarshal(b, &r); err != nil { return "", err } if r.ErrCode != 0 || r.AccessToken == "" { if r.ErrMsg == "" { r.ErrMsg = "gettoken failed" } return "", errors.New(r.ErrMsg) } return r.AccessToken, nil } // QueryUserDetails 按 userId 查询用户详情 func (c *Client) QueryUserDetails(ctx context.Context, userId string) (*UserDetail, error) { accessToken, err := c.getAccessToken(ctx) if err != nil { return nil, err } body := struct { UserId string `json:"userid"` Language string `json:"language,omitempty"` }{UserId: userId, Language: "zh_CN"} b, _ := json.Marshal(body) // 直接调用 TopAPI(oapi.dingtalk.com),通过 query 参数携带 access_token // 官方 SDK 无 topapi 封装,此处保留原接口以保证行为一致 params := url.Values{} params.Add("access_token", accessToken) req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://oapi.dingtalk.com/topapi/v2/user/get?"+params.Encode(), bytes.NewReader(b)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") httpResp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer httpResp.Body.Close() res, err := io.ReadAll(httpResp.Body) if err != nil { return nil, err } var resp struct { Code int `json:"errcode"` Msg string `json:"errmsg"` Result UserDetail `json:"result"` } if err := json.Unmarshal(res, &resp); err != nil { return nil, err } if resp.Code != 0 { return nil, errors.New(resp.Msg) } return &resp.Result, nil } // QueryUserDetailsByMobile 按手机号查询用户详情 func (c *Client) QueryUserDetailsByMobile(ctx context.Context, mobile string) (*UserDetail, error) { accessToken, err := c.getAccessToken(ctx) if err != nil { return nil, err } body := struct { Mobile string `json:"mobile"` }{Mobile: mobile} b, _ := json.Marshal(body) // 直接调用 TopAPI(oapi.dingtalk.com),通过 query 参数携带 access_token params := url.Values{} params.Add("access_token", accessToken) req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://oapi.dingtalk.com/topapi/v2/user/getbymobile?"+params.Encode(), bytes.NewReader(b)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") httpResp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer httpResp.Body.Close() res, err := io.ReadAll(httpResp.Body) if err != nil { return nil, err } var resp struct { Code int `json:"errcode"` Msg string `json:"errmsg"` Result UserDetail `json:"result"` } if err := json.Unmarshal(res, &resp); err != nil { return nil, err } if resp.Code != 0 { return nil, errors.New(resp.Msg) } return &resp.Result, nil }