package service import ( "context" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "time" "wx_service/config" ) var ( ErrWeChatOANotConfigured = errors.New("wechat official account is not configured") ErrWeChatOACodeRequired = errors.New("code is required") ) type WeChatOAService struct { cfg config.WeChatOfficialConfig client *http.Client } func NewWeChatOAService(cfg config.WeChatOfficialConfig) *WeChatOAService { timeout := cfg.RequestTimeout if timeout <= 0 { timeout = 5 * time.Second } return &WeChatOAService{ cfg: cfg, client: &http.Client{ Timeout: timeout, }, } } type OAuthToken struct { OpenID string `json:"openid"` UnionID string `json:"unionid,omitempty"` Scope string `json:"scope,omitempty"` ExpiresIn int `json:"expires_in,omitempty"` AccessToken string `json:"access_token,omitempty"` } type UserInfo struct { OpenID string `json:"openid"` NickName string `json:"nickname,omitempty"` Sex int `json:"sex,omitempty"` Province string `json:"province,omitempty"` City string `json:"city,omitempty"` Country string `json:"country,omitempty"` HeadImgURL string `json:"headimgurl,omitempty"` Privilege []string `json:"privilege,omitempty"` UnionID string `json:"unionid,omitempty"` } type wechatError struct { ErrCode int `json:"errcode"` ErrMsg string `json:"errmsg"` } type WeChatOAError struct { Code int Msg string } func (e *WeChatOAError) Error() string { return fmt.Sprintf("wechat oa error: errcode=%d errmsg=%s", e.Code, e.Msg) } func (s *WeChatOAService) ExchangeCode(ctx context.Context, code string) (OAuthToken, error) { if s.cfg.AppID == "" || s.cfg.AppSecret == "" { return OAuthToken{}, ErrWeChatOANotConfigured } if code == "" { return OAuthToken{}, ErrWeChatOACodeRequired } params := url.Values{} params.Set("appid", s.cfg.AppID) params.Set("secret", s.cfg.AppSecret) params.Set("code", code) params.Set("grant_type", "authorization_code") endpoint := fmt.Sprintf("https://api.weixin.qq.com/sns/oauth2/access_token?%s", params.Encode()) body, err := s.get(ctx, endpoint) if err != nil { return OAuthToken{}, err } var apiErr wechatError _ = json.Unmarshal(body, &apiErr) if apiErr.ErrCode != 0 { return OAuthToken{}, &WeChatOAError{Code: apiErr.ErrCode, Msg: apiErr.ErrMsg} } var token OAuthToken if err := json.Unmarshal(body, &token); err != nil { return OAuthToken{}, fmt.Errorf("decode oauth token: %w", err) } return token, nil } func (s *WeChatOAService) FetchUserInfo(ctx context.Context, accessToken string, openid string) (UserInfo, error) { if accessToken == "" || openid == "" { return UserInfo{}, fmt.Errorf("missing access_token or openid") } params := url.Values{} params.Set("access_token", accessToken) params.Set("openid", openid) params.Set("lang", "zh_CN") endpoint := fmt.Sprintf("https://api.weixin.qq.com/sns/userinfo?%s", params.Encode()) body, err := s.get(ctx, endpoint) if err != nil { return UserInfo{}, err } var apiErr wechatError _ = json.Unmarshal(body, &apiErr) if apiErr.ErrCode != 0 { return UserInfo{}, &WeChatOAError{Code: apiErr.ErrCode, Msg: apiErr.ErrMsg} } var info UserInfo if err := json.Unmarshal(body, &info); err != nil { return UserInfo{}, fmt.Errorf("decode user info: %w", err) } return info, nil } func (s *WeChatOAService) get(ctx context.Context, url string) ([]byte, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("build request: %w", err) } resp, err := s.client.Do(req) if err != nil { return nil, fmt.Errorf("call wechat api: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read response: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("wechat api status=%d body=%s", resp.StatusCode, string(body)) } return body, nil }