Integrate WeChat Official Account support and update configuration
- Added WeChat Official Account configuration options to .env.example and config.go for OAuth2 integration. - Updated main.go to initialize WeChat OAuth handler and register routes for handling OAuth requests. - Enhanced documentation to include references for WeChat Official Account functionality. - Updated route registration to accommodate the new OAuth handler for improved API structure.
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"wx_service/internal/common/wechat_official/service"
|
||||
"wx_service/internal/model"
|
||||
)
|
||||
|
||||
type OAuthHandler struct {
|
||||
oaService *service.WeChatOAService
|
||||
}
|
||||
|
||||
func NewOAuthHandler(oaService *service.WeChatOAService) *OAuthHandler {
|
||||
return &OAuthHandler{oaService: oaService}
|
||||
}
|
||||
|
||||
type codeToUserRequest struct {
|
||||
Code string `json:"code" binding:"required"`
|
||||
WithUserInfo *bool `json:"with_userinfo"`
|
||||
}
|
||||
|
||||
// CodeToUser 使用微信公众号网页授权 code 换取 openid,并可选拉取用户信息(需要 snsapi_userinfo 授权)。
|
||||
func (h *OAuthHandler) CodeToUser(c *gin.Context) {
|
||||
var req codeToUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "请求参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
withUserInfo := true
|
||||
if req.WithUserInfo != nil {
|
||||
withUserInfo = *req.WithUserInfo
|
||||
}
|
||||
|
||||
token, err := h.oaService.ExchangeCode(c.Request.Context(), req.Code)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, service.ErrWeChatOANotConfigured):
|
||||
c.JSON(http.StatusServiceUnavailable, model.Error(http.StatusServiceUnavailable, "未配置公众号服务,请联系管理员"))
|
||||
case errors.Is(err, service.ErrWeChatOACodeRequired):
|
||||
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "code 不能为空"))
|
||||
default:
|
||||
var apiErr *service.WeChatOAError
|
||||
if errors.As(err, &apiErr) {
|
||||
c.JSON(http.StatusBadGateway, model.Error(http.StatusBadGateway, "微信接口异常,请稍后重试"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "获取 openid 失败,请稍后重试"))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
payload := gin.H{
|
||||
"openid": token.OpenID,
|
||||
"unionid": token.UnionID,
|
||||
"scope": token.Scope,
|
||||
"expires_in": token.ExpiresIn,
|
||||
}
|
||||
|
||||
if withUserInfo {
|
||||
info, err := h.oaService.FetchUserInfo(c.Request.Context(), token.AccessToken, token.OpenID)
|
||||
if err != nil {
|
||||
// 可能是 scope 不够(snsapi_base),此时仍返回 openid,并给出提示。
|
||||
payload["userinfo_error"] = "未获取到用户信息(可能未授权 snsapi_userinfo)"
|
||||
} else {
|
||||
// 统一从 userinfo 里回填 unionid(如果有)
|
||||
if info.UnionID != "" {
|
||||
payload["unionid"] = info.UnionID
|
||||
}
|
||||
payload["userinfo"] = info
|
||||
}
|
||||
}
|
||||
|
||||
// 为安全起见,这里不向前端返回 access_token/refresh_token。
|
||||
c.JSON(http.StatusOK, model.Success(payload))
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user