* 修改了微信公众号关注事件和文本消息的响应 * 通过场景码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字符串
195 lines
6.0 KiB
Go
195 lines
6.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"
|
||
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)
|
||
}
|