package wechat import ( "context" "encoding/base64" binaryUtils "encoding/binary" "encoding/json" "fmt" "net/http" "strings" "time" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" Result "code.gitea.io/gitea/routers/entity" context2 "code.gitea.io/gitea/services/context" "github.com/google/uuid" ) // ErrorWechatTempQRStatus 获取微信公众号临时二维码出错 type ErrorWechatTempQRStatus struct { Action string Message string } func (err ErrorWechatTempQRStatus) Error() string { return fmt.Sprintf("Failed to %s in Wechat Service: %s", err.Action, err.Message, ) } // WechatTempQRStatus 获取微信公众号临时二维码扫码状态 type WechatTempQRStatus struct { IsScanned bool `json:"is_scanned"` // 微信公众号二维码是否已被扫描 // 下列3个参数如果为空,则在 JSON 中不出现 SceneStr string `json:"scene_str,omitempty"` // 微信公众号二维码场景值 OpenId string `json:"openid,omitempty"` // 微信公众号二维码扫码人 OpenID IsBinded bool `json:"is_binded,omitempty"` // 微信公众号二维码扫码人 OpenID 是否绑定到了 DevStar UserID } // Marshal2JSONString 将结构体解析为 JSON字符串 func (qrStatus WechatTempQRStatus) Marshal2JSONString() (string, error) { voJSONBytes, err := json.Marshal(qrStatus) if err != nil { return "", err } return string(voJSONBytes), nil } // WechatTempQRData 封装微信公众号临时带参数二维码返回值 type WechatTempQRData struct { Ticket string `json:"ticket"` ExpireSeconds int64 `json:"expire_seconds"` // Url 是指微信端扫码跳转的URL Url string `json:"url"` // QrImageSrcUrl 是指网页端显示微信二维码图片二维码地址,也即 HTML 中 QrImageSrcUrl string `json:"qr_image_url"` } // GenerateTempQR 生成微信公众号临时二维码 func GenerateTempQR( ctx context.Context, sceneStr string, qrExpireSeconds int, ) (*WechatTempQRData, error) { // 1. 检查参数 qrExpireSecondsOverride 和 sceneStrOverride: 若用户未指定,则从配置文件 app.ini 读取 if qrExpireSeconds <= 0 { qrExpireSeconds = setting.Wechat.TempQrExpireSeconds } if len(sceneStr) == 0 { // 生成随机 sceneStr 场景值 // sceneStr生成规则:UUIDv4后边拼接 当前UnixNano时间戳转为byte数组后的Base64 // e.g, sceneStr == "1c78e8d914fb4307a3588ac0f6bc092a@yPXAm+ve5hc=" bytesArrayUnit64 := make([]byte, 8) binaryUtils.LittleEndian.PutUint64(bytesArrayUnit64, uint64(time.Now().UnixNano())) currentTimestampNanoBase64 := base64.StdEncoding.EncodeToString(bytesArrayUnit64) sceneStr = strings.ReplaceAll(uuid.New().String(), "-", "") + "@" + currentTimestampNanoBase64 } // 2. 调用 Wechat.SDK 生成微信公众号临时二维码 qrData, err := setting.Wechat.SDK.QRCode.Temporary(ctx, sceneStr, qrExpireSeconds) if err != nil { return nil, err } // 3. 封装 VO 返回 wechatQr := &WechatTempQRData{ Ticket: qrData.Ticket, ExpireSeconds: qrData.ExpireSeconds, Url: qrData.Url, QrImageSrcUrl: "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" + qrData.Ticket, } return wechatQr, nil } // GenerateWechatQrCode 生成微信公众号临时带参数二维码 // // GET /api/wechat/login/qr/generate?qrExpireSeconds=${qrExpireSeconds}&sceneStr=${sceneStr} func GenerateWechatQrCode(ctx *context2.APIContext) { // 1. 检查微信功能是否启用 if setting.Wechat.SDK == nil { errorMsg := "微信公众号功能禁用, 不会生成公众号带参数二维码" log.Warn(errorMsg) respFailed := Result.ResultType{ Code: Result.RespFailedWechatMalconfigured.Code, Msg: Result.RespFailedWechatMalconfigured.Msg, Data: map[string]string{ "ErrorMsg": errorMsg, }, } respFailed.RespondJson2HttpResponseWriter(ctx.Resp) return } // 2. 解析 HTTP GET 请求参数,调用 service 层生成二维码 qrExpireSeconds := ctx.FormInt("qrExpireSeconds") sceneStr := ctx.FormString("sceneStr") log.Info("sceneStr:" + sceneStr) qrCode, err := GenerateTempQR(ctx, sceneStr, qrExpireSeconds) if err != nil { respFailed := Result.ResultType{ Code: Result.RespFailedGenerateWechatOfficialAccountTempQR.Code, Msg: Result.RespFailedGenerateWechatOfficialAccountTempQR.Msg, Data: map[string]string{ "ErrorMsg": err.Error(), }, } respFailed.RespondJson2HttpResponseWriter(ctx.Resp) return } // 3. 返回 (自动对VO对象进行JSON序列化) repsSuccessGenerateQRCode := Result.ResultType{ Code: Result.RespSuccess.Code, Msg: Result.RespSuccess.Msg, Data: qrCode, } repsSuccessGenerateQRCode.RespondJson2HttpResponseWriter(ctx.Resp) return } // QrCheckCodeStatus 检查二维码扫描状态 /** - 微信服务器验证消息 - GET /api/wechat/login/qr/check-status - 请求参数: - ticket: 微信公众号带参数临时二维码 ticket - _: UNIX时间戳,仅用作防止GET请求被缓存,保证每次GET 请求都能够到达服务器 - 响应参数:(请使用VAR定义) - { Code: , // 状态码,只有在 HTTP 200 OK 后,该字段才有意义 Msg: , // Data: } */ func QrCheckCodeStatus(responseWriter http.ResponseWriter, request *http.Request) { // 设置响应头为 JSON 格式 responseWriter.Header().Set("Content-Type", "application/json") // 从请求中提取 ticket 参数 ticket := request.URL.Query().Get("ticket") if ticket == "" { Result.RespFailedIllegalWechatQrTicket.RespondJson2HttpResponseWriter(responseWriter) return } qrStatus, err := GetWechatQrStatusByTicket(ticket) if err != nil { respFailed := Result.ResultType{ Code: Result.RespFailedGetWechatOfficialAccountTempQRStatus.Code, Msg: Result.RespFailedGetWechatOfficialAccountTempQRStatus.Msg, Data: map[string]string{ "ErrorMsg": err.Error(), }, } respFailed.RespondJson2HttpResponseWriter(responseWriter) return } // 将扫码信息返回 result := Result.ResultType{ Code: Result.RespSuccess.Code, Msg: Result.RespSuccess.Msg, Data: qrStatus, } result.RespondJson2HttpResponseWriter(responseWriter) }