284 lines
9.0 KiB
Go
284 lines
9.0 KiB
Go
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 中 <img src=`${QrImageUrl}` ... >
|
||
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)
|
||
}
|