diff --git a/gin/router-gin.go b/gin/router-gin.go index 16595bb..41aef1b 100644 --- a/gin/router-gin.go +++ b/gin/router-gin.go @@ -10,6 +10,7 @@ func routerSetup(router *gin.Engine) { router.Use(gin.Recovery()) api := router.Group("/api") api.POST("/login", handler.AuthLogin) + api.GET(`/wechat/qr/:id`, handler.WechatQrGet) protected := router.Group("/api") protected.Use(auth(), authorize()) diff --git a/go.mod b/go.mod index 2fbe25c..995f6e5 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 github.com/sirupsen/logrus v1.9.4 - golang.org/x/crypto v0.48.0 + golang.org/x/crypto v0.49.0 gorm.io/driver/mysql v1.6.0 gorm.io/gorm v1.31.1 gorm.io/plugin/dbresolver v1.6.2 @@ -17,15 +17,20 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect + github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/fatih/structs v1.1.0 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect @@ -43,12 +48,17 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect + github.com/silenceper/wechat/v2 v2.1.12 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.2.0 // indirect + github.com/tidwall/pretty v1.2.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect golang.org/x/arch v0.22.0 // indirect golang.org/x/net v0.51.0 // indirect - golang.org/x/sys v0.41.0 // indirect - golang.org/x/text v0.34.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect google.golang.org/protobuf v1.36.10 // indirect ) diff --git a/handler/wechat-handler.go b/handler/wechat-handler.go new file mode 100644 index 0000000..6a4160f --- /dev/null +++ b/handler/wechat-handler.go @@ -0,0 +1,52 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "myschools.me/heritage/heritage-api/service" +) + +func WechatQrGet(c *gin.Context) { + reqid := c.Param("id") + if reqid == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "reqid is required", + }) + return + } + + resp, err := service.WechatQrGet(&reqid) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + }) + return + } + c.JSON(http.StatusOK, resp) +} + +func WechatAuth(c *gin.Context) { + reqid := c.Param("id") + if reqid == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "reqid is required", + }) + return + } + code := c.Query("code") + if code == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "code is required", + }) + return + } + resp, err := service.WechatAuth(&reqid, &code) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + }) + return + } + c.JSON(http.StatusOK, resp) +} diff --git a/service/base-service.go b/service/base-service.go index e93b318..3c6f632 100644 --- a/service/base-service.go +++ b/service/base-service.go @@ -1,13 +1,17 @@ package service import ( + "bytes" + "io" + "net/http" "strings" "github.com/google/uuid" + "github.com/sirupsen/logrus" ) func newID() string { - i := uuid.Must(uuid.NewV7()).String() + i := uuid.Must(uuid.NewUUID()).String() return strings.ReplaceAll(i, "-", "") } @@ -33,3 +37,60 @@ func newToken() string { i := uuid.Must(uuid.NewUUID()).String() return strings.ReplaceAll(i, "-", "") } + +// httpDO 通用的HTTP请求函数 +func httpDO(url, method string, headers map[string]string, body []byte) ([]byte, error) { + var reqBody io.Reader + if body != nil { + reqBody = bytes.NewBuffer(body) + } + + req, err := http.NewRequest(method, url, reqBody) + if err != nil { + logrus.WithFields(logrus.Fields{ + "func": "httpDO", + "url": url, + "method": method, + }).Errorf("http.NewRequest: %v", err) + return nil, err + } + + for k, v := range headers { + req.Header.Set(k, v) + } + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + logrus.WithFields(logrus.Fields{ + "func": "httpDO", + "url": url, + "method": method, + }).Errorf("client.Do failed: %v", err) + return nil, err + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + logrus.WithFields(logrus.Fields{ + "func": "httpDO", + "url": url, + "method": method, + }).Errorf("io.ReadAll failed: %v", err) + return nil, err + } + + if resp.StatusCode >= 400 { + logrus.WithFields(logrus.Fields{ + "func": "httpDO", + "url": url, + "method": method, + "statusCode": resp.StatusCode, + "response": string(respBody), + }).Errorf("HTTP request failed with status code: %d", resp.StatusCode) + return respBody, err + } + + return respBody, nil +} diff --git a/service/wechat-service.go b/service/wechat-service.go new file mode 100644 index 0000000..6cb8394 --- /dev/null +++ b/service/wechat-service.go @@ -0,0 +1,123 @@ +package service + +import ( + "encoding/json" + "fmt" + "os" + "strconv" + + "github.com/silenceper/wechat/v2" + "github.com/silenceper/wechat/v2/cache" + "github.com/silenceper/wechat/v2/officialaccount" + "github.com/silenceper/wechat/v2/officialaccount/config" + "github.com/sirupsen/logrus" +) + +type ticketRequest struct { + ExpireSeconds int `json:"expire_seconds"` + ActionName string `json:"action_name"` + ActionInfo struct { + Scene struct { + SceneStr string `json:"scene_str"` + } `json:"scene"` + } `json:"action_info"` +} + +type ticketResponse struct { + ErrCode int `json:"errcode"` + ErrMsg string `json:"errmsg"` + Ticket string `json:"ticket"` + ExpireSeconds int `json:"expire_seconds"` + Url string `json:"url"` +} + +type WechatGetQrCodeResponse struct { + Ticket string `json:"ticket"` + ExpireSeconds string `json:"expire_seconds"` + Url string `json:"url"` +} + +var ( + mp *officialaccount.OfficialAccount +) + +func init() { + wx := wechat.NewWechat() + wx.SetCache(cache.NewRedis(nil, &cache.RedisOpts{ + Host: os.Getenv("REDIS_HOST"), + Password: os.Getenv("REDIS_PWD"), + Database: func() int { + db := os.Getenv("REDIS_DB") + n, err := strconv.Atoi(db) + if err != nil { + logrus.WithFields(logrus.Fields{ + "func": "init", + }).Warnf("strconv.Atoi failed: %v", err) + n = 0 + } + return n + }(), + MaxIdle: 3, + MaxActive: 300, + IdleTimeout: 600, + })) + + mp = wx.GetOfficialAccount(&config.Config{ + AppID: os.Getenv("APPID"), + AppSecret: os.Getenv("APPSECRET"), + Token: os.Getenv("TOKEN"), + EncodingAESKey: os.Getenv("EncodingAESKey"), + Cache: nil, + }) +} + +// 通过ID获取临时二维码 +func WechatQrGet(reqid *string) (any, error) { + token, err := mp.GetAccessToken() + if err != nil { + logrus.WithFields(logrus.Fields{ + "func": "WechatQrGet", + }).Errorf("mp.GetAccessToken: %v", err) + return nil, err + } + + accurl := "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=" + token + data := ticketRequest{ + ExpireSeconds: 180, + ActionName: "QR_STR_SCENE", + ActionInfo: struct { + Scene struct { + SceneStr string `json:"scene_str"` + } `json:"scene"` + }{ + Scene: struct { + SceneStr string `json:"scene_str"` + }{ + SceneStr: *reqid, + }, + }, + } + marshal, err := json.Marshal(&data) + if err != nil { + return nil, err + } + body, err := httpDO(accurl, "post", nil, marshal) + if err != nil { + return nil, err + } + var result *ticketResponse + err = json.Unmarshal(body, &result) + if err != nil { + return nil, err + } + resp := &WechatGetQrCodeResponse{ + Ticket: result.Ticket, + ExpireSeconds: fmt.Sprintf("%d", result.ExpireSeconds), + Url: result.Url, + } + return resp, nil +} + +func WechatAuth(reqid, code *string) (any, error) { + return nil, nil +}