Files
2025-08-25 15:46:12 +08:00

284 lines
9.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}