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" context2 "code.gitea.io/gitea/services/context" "github.com/google/uuid" ) // ResultType 定义了响应格式 type ResultType struct { Code int `json:"code"` Msg string `json:"msg"` Data interface{} `json:"data,omitempty"` // Data 字段可选 } // RespondJson2HttpResponseWriter // 将 ResultType 响应转换为 JSON 并写入响应 func (t *ResultType) RespondJson2HttpResponseWriter(w http.ResponseWriter) { responseBytes, err := json.Marshal(*t) if err != nil { // 只有序列化 ResultType 失败时候, 才返回 HTTP 500 Internal Server Error http.Error(w, http.StatusText(http.StatusInternalServerError)+": failed to marshal JSON", http.StatusInternalServerError) return } // 序列化 ResultType 成功,无论成功或者失败,统一返回 HTTP 200 OK if setting.CORSConfig.Enabled { AllowOrigin := setting.CORSConfig.AllowDomain[0] if AllowOrigin == "" { AllowOrigin = "*" } w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Access-Control-Allow-Origin", AllowOrigin) w.Header().Set("Access-Control-Allow-Methods", strings.Join(setting.CORSConfig.Methods, ",")) w.Header().Set("Access-Control-Allow-Headers", strings.Join(setting.CORSConfig.Headers, ",")) } else { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") } w.WriteHeader(http.StatusOK) _, _ = w.Write(responseBytes) } // RespSuccess 操作成功常量 var RespSuccess = ResultType{ Code: 0, Msg: "操作成功", } // RespUnauthorizedFailure 获取 devContainer 信息失败:用户未授权 var RespUnauthorizedFailure = ResultType{ Code: 00001, Msg: "未登录,禁止访问", } // 错误码 100xx 表示微信相关错误信息 // RespFailedWechatMalconfigured 微信配置错误,功能不可用 var RespFailedWechatMalconfigured = ResultType{ Code: 10000, Msg: "微信配置错误,功能不可用", //Data: map[string]string{ // "ErrorMsg": //}, } // RespPendingQrNotScanned 用户未扫码返回结果常量 var RespPendingQrNotScanned = ResultType{ Code: 10001, Msg: "用户未扫描微信公众号带参数二维码", } // RespFailedIllegalWechatQrTicket 二维码凭证Ticket参数无效 var RespFailedIllegalWechatQrTicket = ResultType{ Code: 10002, Msg: "提交的微信公众号带参数二维码凭证Ticket参数无效", } // RespFailedGenerateWechatOfficialAccountTempQR 微信公众号临时二维码生成失败 var RespFailedGenerateWechatOfficialAccountTempQR = ResultType{ Code: 10003, Msg: "微信公众号临时二维码生成失败", //Data: map[string]string{ // "ErrorMsg": err.Error(), //}, } // RespFailedGetWechatOfficialAccountTempQRStatus 获取微信公众号临时二维码扫码状态出错 var RespFailedGetWechatOfficialAccountTempQRStatus = ResultType{ Code: 10004, Msg: "获取微信公众号临时二维码扫码状态出错", //Data: map[string]string{ // "ErrorMsg": err.Error(), //}, } // 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 := ResultType{ Code: RespFailedWechatMalconfigured.Code, Msg: 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 := ResultType{ Code: RespFailedGenerateWechatOfficialAccountTempQR.Code, Msg: RespFailedGenerateWechatOfficialAccountTempQR.Msg, Data: map[string]string{ "ErrorMsg": err.Error(), }, } respFailed.RespondJson2HttpResponseWriter(ctx.Resp) return } // 3. 返回 (自动对VO对象进行JSON序列化) repsSuccessGenerateQRCode := ResultType{ Code: RespSuccess.Code, Msg: 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 == "" { RespFailedIllegalWechatQrTicket.RespondJson2HttpResponseWriter(responseWriter) return } qrStatus, err := GetWechatQrStatusByTicket(ticket) if err != nil { respFailed := ResultType{ Code: RespFailedGetWechatOfficialAccountTempQRStatus.Code, Msg: RespFailedGetWechatOfficialAccountTempQRStatus.Msg, Data: map[string]string{ "ErrorMsg": err.Error(), }, } respFailed.RespondJson2HttpResponseWriter(responseWriter) return } // 将扫码信息返回 result := ResultType{ Code: RespSuccess.Code, Msg: RespSuccess.Msg, Data: qrStatus, } result.RespondJson2HttpResponseWriter(responseWriter) }