Files
devstar/services/wechat/qr-code.go
孟宁 0557a744c0 !52 通过场景码SceneStr区分用户登录不同的部署版本,优化了代码
* 修改了微信公众号关注事件和文本消息的响应
* 通过场景码SceneStr区分用户登录不同的部署版本,优化了代码
* 兼容线上/api/wechat/official-account相关API
* 对第三方依赖中的PowerWeChat和officialAccount等字符串进行了本地化抽象,以Wechat和wechat_sdk命名可读性更好
* Merge branch 'dev' into refactoring-wechat-qr-code
* 梳理了routers到auth_service的代码,wechat_service还需要进一步整理
* 对代码文件目录结构进行了重构,内在逻辑还没有重构
* wechat相关路径中删掉了无意义的official-account字符串
2025-02-14 10:41:45 +00:00

195 lines
6.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"
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 中 <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 := 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)
}