| @@ -136,6 +136,7 @@ func init() { | |||
| new(AiModelManage), | |||
| new(OfficialTag), | |||
| new(OfficialTagRepos), | |||
| new(WechatBindLog), | |||
| ) | |||
| tablesStatistic = append(tablesStatistic, | |||
| @@ -177,6 +177,9 @@ type User struct { | |||
| //BlockChain | |||
| PublicKey string `xorm:"INDEX"` | |||
| PrivateKey string `xorm:"INDEX"` | |||
| WechatOpenId string `xorm:"INDEX"` | |||
| } | |||
| // SearchOrganizationsOptions options to filter organizations | |||
| @@ -0,0 +1,74 @@ | |||
| package models | |||
| import ( | |||
| "code.gitea.io/gitea/modules/log" | |||
| "time" | |||
| ) | |||
| type WechatBindAction int | |||
| const ( | |||
| WECHAT_BIND WechatBindAction = iota + 1 | |||
| WECHAT_UNBIND | |||
| ) | |||
| type WechatBindLog struct { | |||
| ID int64 `xorm:"pk autoincr"` | |||
| UserID int64 `xorm:"INDEX"` | |||
| WechatOpenId string `xorm:"INDEX"` | |||
| Action int | |||
| CreateTime time.Time `xorm:"INDEX created"` | |||
| } | |||
| func BindWechatOpenId(userId int64, wechatOpenId string) error { | |||
| sess := x.NewSession() | |||
| defer sess.Close() | |||
| if err := sess.Begin(); err != nil { | |||
| return err | |||
| } | |||
| param := &User{WechatOpenId: wechatOpenId} | |||
| n, err := sess.Where("ID = ?", userId).Update(param) | |||
| if err != nil { | |||
| log.Error("update wechat_open_id failed,e=%v", err) | |||
| return err | |||
| } | |||
| if n == 0 { | |||
| log.Error("update wechat_open_id failed,user not exist,userId=%d", userId) | |||
| return nil | |||
| } | |||
| logParam := &WechatBindLog{ | |||
| UserID: userId, | |||
| WechatOpenId: wechatOpenId, | |||
| Action: int(WECHAT_BIND), | |||
| } | |||
| sess.Insert(logParam) | |||
| return sess.Commit() | |||
| } | |||
| func UnbindWechatOpenId(userId int64) error { | |||
| sess := x.NewSession() | |||
| defer sess.Close() | |||
| if err := sess.Begin(); err != nil { | |||
| return err | |||
| } | |||
| param := &User{WechatOpenId: ""} | |||
| n, err := x.Where("ID = ?", userId).Update(param) | |||
| if err != nil { | |||
| log.Error("update wechat_open_id failed,e=%v", err) | |||
| return err | |||
| } | |||
| if n == 0 { | |||
| log.Error("update wechat_open_id failed,user not exist,userId=%d", userId) | |||
| return nil | |||
| } | |||
| //todo 是否记录原有微信openId | |||
| logParam := &WechatBindLog{ | |||
| UserID: userId, | |||
| Action: int(WECHAT_UNBIND), | |||
| } | |||
| sess.Insert(logParam) | |||
| return sess.Commit() | |||
| } | |||
| @@ -14,7 +14,7 @@ func GetWechatAccessToken() string { | |||
| if token == EMPTY_REDIS_VAL { | |||
| return "" | |||
| } | |||
| live, _ := redis_client.TTL(token) | |||
| live, _ := redis_client.TTL(redis_key.WechatAccessTokenKey()) | |||
| //refresh wechat access token when expire time less than 5 minutes | |||
| if live > 0 && live < 300 { | |||
| refreshAccessTokenCache() | |||
| @@ -0,0 +1,11 @@ | |||
| package wechat | |||
| import "code.gitea.io/gitea/models" | |||
| func BindWechat(userId int64, wechatOpenId string) error { | |||
| return models.BindWechatOpenId(userId, wechatOpenId) | |||
| } | |||
| func UnbindWechat(userId int64) error { | |||
| return models.UnbindWechatOpenId(userId) | |||
| } | |||
| @@ -0,0 +1,5 @@ | |||
| package wechat | |||
| type WechatCall interface { | |||
| call() | |||
| } | |||
| @@ -4,7 +4,9 @@ import ( | |||
| "code.gitea.io/gitea/modules/log" | |||
| "code.gitea.io/gitea/modules/setting" | |||
| "encoding/json" | |||
| "fmt" | |||
| "github.com/go-resty/resty/v2" | |||
| "strconv" | |||
| "time" | |||
| ) | |||
| @@ -17,6 +19,9 @@ const ( | |||
| ACCESS_TOKEN_PATH = "/cgi-bin/token" | |||
| QR_CODE_Path = "/cgi-bin/qrcode/create" | |||
| ACTION_QR_STR_SCENE = "QR_STR_SCENE" | |||
| ERR_CODE_ACCESSTOKEN_EXPIRE = 42001 | |||
| ERR_CODE_ACCESSTOKEN_INVALID = 40001 | |||
| ) | |||
| type AccessTokenResponse struct { | |||
| @@ -44,6 +49,11 @@ type Scene struct { | |||
| Scene_str string | |||
| } | |||
| type ErrorResponse struct { | |||
| Errcode int | |||
| Errmsg string | |||
| } | |||
| func getWechatRestyClient() *resty.Client { | |||
| if client == nil { | |||
| client = resty.New() | |||
| @@ -69,7 +79,7 @@ func callAccessToken() *AccessTokenResponse { | |||
| return &result | |||
| } | |||
| func callQRCodeCreate(sceneStr string) *QRCodeResponse { | |||
| func callQRCodeCreate(sceneStr string) (*QRCodeResponse, bool) { | |||
| client := getWechatRestyClient() | |||
| body := &QRCodeRequest{ | |||
| @@ -85,11 +95,29 @@ func callQRCodeCreate(sceneStr string) *QRCodeResponse { | |||
| SetBody(bodyJson). | |||
| SetResult(&result). | |||
| Post(setting.WechatApiHost + QR_CODE_Path) | |||
| if err != nil || result.Url == "" { | |||
| if err != nil { | |||
| log.Error("create QR code failed,e=%v", err) | |||
| return nil | |||
| return nil, false | |||
| } | |||
| errCode := getErrorCodeFromResponse(r) | |||
| if errCode == ERR_CODE_ACCESSTOKEN_EXPIRE || errCode == ERR_CODE_ACCESSTOKEN_INVALID { | |||
| return nil, true | |||
| } | |||
| if result.Url == "" { | |||
| return nil, false | |||
| } | |||
| //todo 识别token失效的错误码,重试机制 | |||
| log.Info("%v", r) | |||
| return &result | |||
| return &result, false | |||
| } | |||
| func getErrorCodeFromResponse(r *resty.Response) int { | |||
| a := r.Body() | |||
| resultMap := make(map[string]interface{}, 0) | |||
| json.Unmarshal(a, &resultMap) | |||
| code := resultMap["errcode"] | |||
| if code == nil { | |||
| return -1 | |||
| } | |||
| c, _ := strconv.Atoi(fmt.Sprint(code)) | |||
| return c | |||
| } | |||
| @@ -1,6 +1,10 @@ | |||
| package wechat | |||
| func GetWechatQRCode4Bind(sceneStr string) *QRCodeResponse { | |||
| r := callQRCodeCreate(sceneStr) | |||
| return r | |||
| result, retryFlag := callQRCodeCreate(sceneStr) | |||
| if retryFlag { | |||
| refreshAccessTokenCache() | |||
| result, _ = callQRCodeCreate(sceneStr) | |||
| } | |||
| return result | |||
| } | |||
| @@ -3,6 +3,7 @@ package redis_client | |||
| import ( | |||
| "code.gitea.io/gitea/modules/labelmsg" | |||
| "fmt" | |||
| "github.com/gomodule/redigo/redis" | |||
| "math" | |||
| "strconv" | |||
| "time" | |||
| @@ -36,7 +37,24 @@ func Get(key string) (string, error) { | |||
| if reply == nil { | |||
| return "", err | |||
| } | |||
| return fmt.Sprint(reply), nil | |||
| s, _ := redis.String(reply, nil) | |||
| return s, nil | |||
| } | |||
| func Del(key string) (int, error) { | |||
| redisClient := labelmsg.Get() | |||
| defer redisClient.Close() | |||
| reply, err := redisClient.Do("DEL", key) | |||
| if err != nil { | |||
| return 0, err | |||
| } | |||
| if reply == nil { | |||
| return 0, err | |||
| } | |||
| s, _ := redis.Int(reply, nil) | |||
| return s, nil | |||
| } | |||
| @@ -59,6 +59,7 @@ | |||
| package v1 | |||
| import ( | |||
| "code.gitea.io/gitea/routers/authentication" | |||
| "net/http" | |||
| "strings" | |||
| @@ -995,6 +996,9 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
| m.Group("/topics", func() { | |||
| m.Get("/search", repo.TopicSearch) | |||
| }) | |||
| m.Group("/from_wechat", func() { | |||
| m.Get("/event", authentication.AcceptWechatEvent) | |||
| }) | |||
| }, securityHeaders(), context.APIContexter(), sudo()) | |||
| } | |||
| @@ -27,14 +27,14 @@ func GetQRCode4Bind(ctx *context.Context) { | |||
| r, err := createQRCode4Bind(userId) | |||
| if err != nil { | |||
| ctx.JSON(200, map[string]interface{}{ | |||
| "code": 9, | |||
| "code": "9999", | |||
| "msg": "Get QR code failed", | |||
| }) | |||
| return | |||
| } | |||
| ctx.JSON(200, map[string]interface{}{ | |||
| "code": 0, | |||
| "code": "00", | |||
| "msg": "success", | |||
| "data": r, | |||
| }) | |||
| @@ -0,0 +1,68 @@ | |||
| package authentication | |||
| import ( | |||
| "code.gitea.io/gitea/modules/auth/wechat" | |||
| "code.gitea.io/gitea/modules/context" | |||
| "code.gitea.io/gitea/modules/log" | |||
| "code.gitea.io/gitea/modules/redis/redis_client" | |||
| "code.gitea.io/gitea/modules/redis/redis_key" | |||
| "encoding/xml" | |||
| "io/ioutil" | |||
| "strconv" | |||
| "time" | |||
| ) | |||
| //<xml> | |||
| // <ToUserName><![CDATA[toUser]]></ToUserName> | |||
| // <FromUserName><![CDATA[FromUser]]></FromUserName> | |||
| // <CreateTime>123456789</CreateTime> | |||
| // <MsgType><![CDATA[event]]></MsgType> | |||
| // <Event><![CDATA[SCAN]]></Event> | |||
| // <EventKey><![CDATA[SCENE_VALUE]]></EventKey> | |||
| // <Ticket><![CDATA[TICKET]]></Ticket> | |||
| //</xml> | |||
| type WechatEvent struct { | |||
| ToUserName string | |||
| FromUserName string | |||
| CreateTime int64 | |||
| MsgType string | |||
| Event string | |||
| EventKey string | |||
| Ticket string | |||
| } | |||
| type Xml struct { | |||
| ToUserName string | |||
| FromUserName string | |||
| CreateTime int64 | |||
| MsgType string | |||
| Content string | |||
| } | |||
| // AcceptWechatEvent | |||
| func AcceptWechatEvent(ctx *context.Context) { | |||
| b, _ := ioutil.ReadAll(ctx.Req.Request.Body) | |||
| we := WechatEvent{} | |||
| xml.Unmarshal(b, &we) | |||
| log.Info("accept wechat event= %v", b) | |||
| key := redis_key.WechatBindingUserIdKey(we.EventKey) | |||
| val, _ := redis_client.Get(key) | |||
| if val == "" { | |||
| log.Error("sceneStr is not exist,sceneStr=%s", we.EventKey) | |||
| ctx.XML(200, "") | |||
| return | |||
| } | |||
| userId, _ := strconv.ParseInt(val, 10, 64) | |||
| //更新微信openId和流水 | |||
| wechat.BindWechat(userId, we.EventKey) | |||
| reply := &Xml{ | |||
| ToUserName: we.FromUserName, | |||
| FromUserName: we.ToUserName, | |||
| CreateTime: time.Now().Unix(), | |||
| MsgType: "text", | |||
| Content: "启智账号认证微信成功", | |||
| } | |||
| redis_client.Del(key) | |||
| ctx.XML(200, reply) | |||
| } | |||