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:
@@ -29,3 +29,8 @@ QINIU_CDN_DOMAIN=
|
|||||||
QINIU_KEY_PREFIX=uploads/
|
QINIU_KEY_PREFIX=uploads/
|
||||||
# token 有效期(秒)
|
# token 有效期(秒)
|
||||||
QINIU_TOKEN_EXPIRE_SECONDS=300
|
QINIU_TOKEN_EXPIRE_SECONDS=300
|
||||||
|
|
||||||
|
# 微信公众号(网页授权 OAuth2)
|
||||||
|
WECHAT_OA_APP_ID=replace-with-oa-appid
|
||||||
|
WECHAT_OA_APP_SECRET=replace-with-oa-appsecret
|
||||||
|
WECHAT_OA_TIMEOUT_SECONDS=5
|
||||||
|
|||||||
+6
-1
@@ -10,6 +10,8 @@ import (
|
|||||||
authservice "wx_service/internal/common/auth/service"
|
authservice "wx_service/internal/common/auth/service"
|
||||||
qiniuhandler "wx_service/internal/common/qiniu/handler"
|
qiniuhandler "wx_service/internal/common/qiniu/handler"
|
||||||
qiniuservice "wx_service/internal/common/qiniu/service"
|
qiniuservice "wx_service/internal/common/qiniu/service"
|
||||||
|
oahandler "wx_service/internal/common/wechat_official/handler"
|
||||||
|
oaservice "wx_service/internal/common/wechat_official/service"
|
||||||
"wx_service/internal/database"
|
"wx_service/internal/database"
|
||||||
"wx_service/internal/model"
|
"wx_service/internal/model"
|
||||||
rmhandler "wx_service/internal/remove_watermark/handler"
|
rmhandler "wx_service/internal/remove_watermark/handler"
|
||||||
@@ -60,8 +62,11 @@ func main() {
|
|||||||
qiniuService := qiniuservice.NewQiniuService(config.AppConfig.Qiniu)
|
qiniuService := qiniuservice.NewQiniuService(config.AppConfig.Qiniu)
|
||||||
uploadHandler := qiniuhandler.NewUploadHandler(qiniuService)
|
uploadHandler := qiniuhandler.NewUploadHandler(qiniuService)
|
||||||
|
|
||||||
|
oaService := oaservice.NewWeChatOAService(config.AppConfig.WeChatOA)
|
||||||
|
oaOAuthHandler := oahandler.NewOAuthHandler(oaService)
|
||||||
|
|
||||||
// 6) 注册路由:把 URL 映射到 handler
|
// 6) 注册路由:把 URL 映射到 handler
|
||||||
routes.Register(router, database.DB, authHandler, videoHandler, smokeHandler, uploadHandler)
|
routes.Register(router, database.DB, authHandler, videoHandler, smokeHandler, uploadHandler, oaOAuthHandler)
|
||||||
|
|
||||||
// 7) 启动监听端口
|
// 7) 启动监听端口
|
||||||
addr := ":" + config.AppConfig.Server.Port
|
addr := ":" + config.AppConfig.Server.Port
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type Config struct {
|
|||||||
JWT JWTConfig
|
JWT JWTConfig
|
||||||
ShortVideo ShortVideoConfig
|
ShortVideo ShortVideoConfig
|
||||||
Qiniu QiniuConfig
|
Qiniu QiniuConfig
|
||||||
|
WeChatOA WeChatOfficialConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
@@ -53,6 +54,13 @@ type QiniuConfig struct {
|
|||||||
TokenExpireSeconds int
|
TokenExpireSeconds int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WeChatOfficialConfig 用于微信公众号网页授权(OAuth2)相关接口。
|
||||||
|
type WeChatOfficialConfig struct {
|
||||||
|
AppID string
|
||||||
|
AppSecret string
|
||||||
|
RequestTimeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
var AppConfig *Config
|
var AppConfig *Config
|
||||||
|
|
||||||
func LoadConfig() {
|
func LoadConfig() {
|
||||||
@@ -91,6 +99,11 @@ func LoadConfig() {
|
|||||||
KeyPrefix: getEnv("QINIU_KEY_PREFIX", "uploads/"),
|
KeyPrefix: getEnv("QINIU_KEY_PREFIX", "uploads/"),
|
||||||
TokenExpireSeconds: getEnvAsInt("QINIU_TOKEN_EXPIRE_SECONDS", 300),
|
TokenExpireSeconds: getEnvAsInt("QINIU_TOKEN_EXPIRE_SECONDS", 300),
|
||||||
},
|
},
|
||||||
|
WeChatOA: WeChatOfficialConfig{
|
||||||
|
AppID: getEnv("WECHAT_OA_APP_ID", ""),
|
||||||
|
AppSecret: getEnv("WECHAT_OA_APP_SECRET", ""),
|
||||||
|
RequestTimeout: time.Duration(getEnvAsInt("WECHAT_OA_TIMEOUT_SECONDS", 5)) * time.Second,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
- `docs/common/auth.md`
|
- `docs/common/auth.md`
|
||||||
- `docs/common/response.md`
|
- `docs/common/response.md`
|
||||||
- `docs/common/upload_qiniu.md`
|
- `docs/common/upload_qiniu.md`
|
||||||
|
- `docs/common/wechat_official.md`
|
||||||
|
|
||||||
## 去水印小程序
|
## 去水印小程序
|
||||||
|
|
||||||
|
|||||||
@@ -26,3 +26,7 @@
|
|||||||
## 上传(七牛直传)
|
## 上传(七牛直传)
|
||||||
|
|
||||||
- `docs/common/upload_qiniu.md`
|
- `docs/common/upload_qiniu.md`
|
||||||
|
|
||||||
|
## 微信公众号
|
||||||
|
|
||||||
|
- `docs/common/wechat_official.md`
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# 微信公众号:code 换取 openid/用户信息
|
||||||
|
|
||||||
|
本接口用于微信公众号网页授权(OAuth2):前端拿到微信回调的 `code` 后,调用后端换取 `openid`,并可选获取用户信息(昵称/头像/unionid 等)。
|
||||||
|
https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx65d83a301fc84068&redirect_uri=https%3A%2F%2Fwww.baidu.com&response_type=code&scope=&state=1#wechat_redirect
|
||||||
|
https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx65d83a301fc84068&redirect_uri=https%3A%2F%2Fwww.baidu.com&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
在 `.env` 中配置(示例见:`.env.example`):
|
||||||
|
|
||||||
|
- `WECHAT_OA_APP_ID`
|
||||||
|
- `WECHAT_OA_APP_SECRET`
|
||||||
|
- `WECHAT_OA_TIMEOUT_SECONDS`(可选)
|
||||||
|
|
||||||
|
## 接口
|
||||||
|
|
||||||
|
`POST /api/v1/wechat/official/oauth/code2user`
|
||||||
|
|
||||||
|
说明:
|
||||||
|
- 不需要登录(无需 Bearer Token),因为该 `code` 本身来自微信网页授权流程。
|
||||||
|
|
||||||
|
请求体:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "微信回调参数 code",
|
||||||
|
"with_userinfo": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `with_userinfo` 可选,默认 `true`。
|
||||||
|
- 若网页授权 scope 仅为 `snsapi_base`,通常只能拿到 `openid`,获取 `userinfo` 可能失败(接口会返回 `userinfo_error` 提示)。
|
||||||
|
|
||||||
|
curl 示例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://127.0.0.1:8080/api/v1/wechat/official/oauth/code2user' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"code":"YOUR_CODE","with_userinfo":true}'
|
||||||
|
```
|
||||||
|
|
||||||
|
成功响应示例(节选):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"openid": "oXXXX",
|
||||||
|
"unionid": "oXXXX",
|
||||||
|
"scope": "snsapi_userinfo",
|
||||||
|
"expires_in": 7200,
|
||||||
|
"userinfo": {
|
||||||
|
"openid": "oXXXX",
|
||||||
|
"nickname": "昵称",
|
||||||
|
"headimgurl": "https://...",
|
||||||
|
"sex": 1,
|
||||||
|
"province": "Guangdong",
|
||||||
|
"city": "Shenzhen",
|
||||||
|
"country": "CN",
|
||||||
|
"unionid": "oXXXX"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
authhandler "wx_service/internal/common/auth/handler"
|
authhandler "wx_service/internal/common/auth/handler"
|
||||||
qiniuhandler "wx_service/internal/common/qiniu/handler"
|
qiniuhandler "wx_service/internal/common/qiniu/handler"
|
||||||
|
oahandler "wx_service/internal/common/wechat_official/handler"
|
||||||
"wx_service/internal/middleware"
|
"wx_service/internal/middleware"
|
||||||
rmhandler "wx_service/internal/remove_watermark/handler"
|
rmhandler "wx_service/internal/remove_watermark/handler"
|
||||||
smokehandler "wx_service/internal/smoke/handler"
|
smokehandler "wx_service/internal/smoke/handler"
|
||||||
@@ -20,6 +21,7 @@ func Register(
|
|||||||
videoHandler *rmhandler.VideoHandler,
|
videoHandler *rmhandler.VideoHandler,
|
||||||
smokeHandler *smokehandler.SmokeHandler,
|
smokeHandler *smokehandler.SmokeHandler,
|
||||||
uploadHandler *qiniuhandler.UploadHandler,
|
uploadHandler *qiniuhandler.UploadHandler,
|
||||||
|
oaOAuthHandler *oahandler.OAuthHandler,
|
||||||
) {
|
) {
|
||||||
// Register 用来集中注册所有 HTTP 路由,便于工程结构更清晰:
|
// Register 用来集中注册所有 HTTP 路由,便于工程结构更清晰:
|
||||||
// - main 只负责初始化(配置/DB/依赖注入)
|
// - main 只负责初始化(配置/DB/依赖注入)
|
||||||
@@ -29,6 +31,9 @@ func Register(
|
|||||||
// 登录接口:用微信 code 换取/创建用户并返回 session_key(作为后续 Bearer Token)
|
// 登录接口:用微信 code 换取/创建用户并返回 session_key(作为后续 Bearer Token)
|
||||||
api.POST("/auth/login", authHandler.LoginWithWeChat)
|
api.POST("/auth/login", authHandler.LoginWithWeChat)
|
||||||
|
|
||||||
|
// 公众号网页授权:不需要登录(code 本身来自微信授权回调)
|
||||||
|
registerWeChatOfficialRoutes(api, oaOAuthHandler)
|
||||||
|
|
||||||
// 需要登录的接口组:统一挂载鉴权中间件
|
// 需要登录的接口组:统一挂载鉴权中间件
|
||||||
protected := api.Group("")
|
protected := api.Group("")
|
||||||
protected.Use(middleware.AuthMiddleware(db))
|
protected.Use(middleware.AuthMiddleware(db))
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
oahandler "wx_service/internal/common/wechat_official/handler"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerWeChatOfficialRoutes(api *gin.RouterGroup, oauthHandler *oahandler.OAuthHandler) {
|
||||||
|
// 微信公众号(网页授权 OAuth2)
|
||||||
|
oa := api.Group("/wechat/official")
|
||||||
|
{
|
||||||
|
oa.POST("/oauth/code2user", oauthHandler.CodeToUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user