新增微信公众平台对接

This commit is contained in:
wucongxing8150 2024-03-05 14:50:01 +08:00
parent b58489a053
commit b4039aab81
12 changed files with 367 additions and 16 deletions

3
.gitignore vendored
View File

@ -14,4 +14,5 @@
*.log
# Dependency directories (remove the comment below to include it)
# vendor/
# vendor/
.DS_Store/

BIN
api/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -10,4 +10,5 @@ type Api struct {
// version1 v1版本
type version1 struct {
UserDoctor v1.UserDoctor // 角色数据
WeChat v1.WeChat // 微信数据
}

View File

@ -0,0 +1,79 @@
package v1
import (
"fmt"
"github.com/gin-gonic/gin"
v1 "hospital-open-api/api/requests/v1"
"hospital-open-api/api/responses"
"hospital-open-api/config"
"hospital-open-api/extend/weChat/Scheme"
"hospital-open-api/global"
"hospital-open-api/utils"
"regexp"
"strings"
)
type WeChat struct{}
// GetScheme 获取页面加密scheme码
func (r *WeChat) GetScheme(c *gin.Context) {
WeChatRequest := v1.WeChatRequest{}
req := WeChatRequest.GetScheme
if err := c.ShouldBind(&req); err != nil {
responses.FailWithMessage(err.Error(), c)
return
}
// 参数验证
if err := global.Validate.Struct(req); err != nil {
responses.FailWithMessage(utils.Translate(err), c)
return
}
// 验证path参数
if strings.Count(req.Path, "/") != 3 || !strings.HasPrefix(req.Path, "/") || strings.HasSuffix(req.Path, "/") {
responses.FailWithMessage("path格式错误", c)
return
}
// 验证query参数
// 定义正则表达式,匹配 k1=v1&k2=v2 这种格式
pattern := `^([a-zA-Z0-9_-]+=[a-zA-Z0-9_-]+&)*([a-zA-Z0-9_-]+=[a-zA-Z0-9_-]+)$`
reg := regexp.MustCompile(pattern)
res := reg.MatchString(req.Query)
if !res {
responses.FailWithMessage("query格式错误", c)
return
}
// 判断环境
envVersion := "trial"
if config.C.Env == "prod" {
envVersion = "release"
}
fmt.Println(req.Query)
// 构建请求数据
JumpWxaData := &Scheme.GetSchemeJumpWxaRequest{
Path: req.Path,
Query: req.Query,
EnvVersion: envVersion,
}
GetSchemeRequest := &Scheme.GetSchemeRequest{
JumpWxa: JumpWxaData,
ExpireType: 1,
ExpireTime: 0,
ExpireInterval: req.ExpireInterval,
}
openLink, err := GetSchemeRequest.GetScheme(1)
if err != nil {
responses.FailWithMessage(err.Error(), c)
return
}
responses.OkWithData(openLink, c)
}

12
api/requests/v1/WeChat.go Normal file
View File

@ -0,0 +1,12 @@
package v1
type WeChatRequest struct {
GetScheme // 获取页面加密scheme码
}
// GetScheme 获取页面加密scheme码
type GetScheme struct {
Path string `json:"path" form:"path" validate:"required" label:"页面路径"`
Query string `json:"query" form:"query" validate:"required" label:"参数"`
ExpireInterval int `json:"expire_interval" form:"expire_interval" validate:"required" label:"有效天数"`
}

View File

@ -80,8 +80,10 @@ func basicRouter(r *gin.Engine, api controller.Api) {
func privateRouter(r *gin.Engine, api controller.Api) {
v1Group := r.Group("/v1")
{
// 用户
userGroup := v1Group.Group("/user")
{
// 医生
doctorGroup := userGroup.Group("/doctor")
{
// 获取医生详情
@ -91,5 +93,12 @@ func privateRouter(r *gin.Engine, api controller.Api) {
doctorGroup.GET("/multi", api.V1.UserDoctor.GetMultiDoctor)
}
}
// 微信
wechatGroup := v1Group.Group("/wechat")
{
// 生成页面加密scheme码
wechatGroup.POST("/scheme", api.V1.WeChat.GetScheme)
}
}
}

View File

@ -0,0 +1,78 @@
package Scheme
import (
"encoding/json"
"errors"
"fmt"
"hospital-open-api/extend/weChat"
"strings"
)
// GetSchemeJumpWxaRequest 跳转到的目标小程序信息
type GetSchemeJumpWxaRequest struct {
Path string `json:"path"`
Query string `json:"query"`
// envVersion 要打开的小程序版本。正式版为 "release",体验版为 "trial",开发版为 "develop"
EnvVersion string `json:"env_version"`
}
// GetSchemeRequest 请求参数
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.generate.html#请求参数
type GetSchemeRequest struct {
JumpWxa *GetSchemeJumpWxaRequest `json:"jump_wxa"`
ExpireType int `json:"expire_type"`
ExpireTime int64 `json:"expire_time"`
ExpireInterval int `json:"expire_interval"`
}
// GetSchemeResponse 获取加密scheme码返回数据
type GetSchemeResponse struct {
OpenLink string `json:"openlink"`
weChat.CommonError
}
// GetScheme 获取加密scheme码
func (r *GetSchemeRequest) GetScheme(userType int) (string, error) {
// 初始化配置
weChatConfig := weChat.GetConfig(userType)
// 获取token
accessToken, err := weChatConfig.GetAccessToken()
if err != nil {
return "", err
}
url := fmt.Sprintf("%s?access_token=%s", "https://api.weixin.qq.com/wxa/generatescheme", accessToken)
// 将请求数据转换为 JSON
requestBody, err := json.Marshal(r)
if err != nil {
return "", err
}
// 去除json的转义
requestBody = []byte(strings.Replace(string(requestBody), "\\u0026", "&", -1))
body, err := weChat.PostRequest(url, requestBody)
if err != nil {
return "", errors.New(err.Error())
}
var GetSchemeResponse GetSchemeResponse
err = json.Unmarshal(body, &GetSchemeResponse)
if err != nil {
// json解析失败
return "", err
}
if GetSchemeResponse.ErrCode != 0 || GetSchemeResponse.ErrMsg != "ok" {
return "", errors.New(GetSchemeResponse.ErrMsg)
}
if GetSchemeResponse.OpenLink == "" {
return "", errors.New("失败")
}
return GetSchemeResponse.OpenLink, nil
}

1
extend/weChat/WeChat.go Normal file
View File

@ -0,0 +1 @@
package weChat

149
extend/weChat/base.go Normal file
View File

@ -0,0 +1,149 @@
package weChat
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/leeqvip/gophp"
"hospital-open-api/config"
"hospital-open-api/global"
"io"
"net/http"
"time"
)
// CommonError 微信返回的通用错误 json
type CommonError struct {
ErrCode int64 `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
type GetAccessTokenResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int64 `json:"expires_in"`
CommonError
}
type WeChatConfig struct {
AppId string `json:"app_id"`
AppSecret string `json:"app_secret"`
}
// GetConfig 初始化配置
func GetConfig(userType int) *WeChatConfig {
weChatConfig := &WeChatConfig{}
if userType == 1 {
// 患者
weChatConfig.AppId = config.C.Wechat.PatientAppId
weChatConfig.AppSecret = config.C.Wechat.PatientAppSecret
} else if userType == 2 {
// 医生/药师
weChatConfig.AppId = config.C.Wechat.DoctorAppId
weChatConfig.AppSecret = config.C.Wechat.DoctorAppSecret
}
return weChatConfig
}
// GetAccessToken 获取access_token
func (c WeChatConfig) GetAccessToken() (string, error) {
// 检测缓存是否存在access_token
redisKey := "c:official_account.access_token." + c.AppId + "." + c.AppSecret
accessToken, _ := global.Redis.Get(context.Background(), redisKey).Result()
if accessToken != "" {
// 存在缓存,反序列化
result, err := gophp.Unserialize([]byte(accessToken))
if err != nil {
return "", err
}
var ok bool
accessToken, ok = result.(string)
if !ok {
return "", errors.New("失败")
}
} else {
// 不存在缓存,重新获取
var body []byte
grantType := "client_credential"
// 构建 URL
url := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/token?grant_type=%s&appid=%s&secret=%s", grantType, c.AppId,
c.AppSecret)
body, err := GetRequest(url)
if err != nil {
return "", errors.New(err.Error())
}
var GetAccessTokenResponse GetAccessTokenResponse
err = json.Unmarshal(body, &GetAccessTokenResponse)
if err != nil {
return "", errors.New(err.Error())
}
if GetAccessTokenResponse.ErrCode != 0 {
return "", errors.New(GetAccessTokenResponse.ErrMsg)
}
accessToken = GetAccessTokenResponse.AccessToken
expiresIn := GetAccessTokenResponse.ExpiresIn
// 序列化
redisValue, _ := gophp.Serialize(accessToken)
// 增加缓存
global.Redis.Set(context.Background(), redisKey, string(redisValue),
time.Duration(expiresIn-1500)*time.Second)
}
if accessToken == "" {
return "", errors.New("失败")
}
return accessToken, nil
}
// GetRequest 发送 GET 请求并返回响应内容和错误(如果有)
func GetRequest(url string) ([]byte, error) {
// 发送 GET 请求
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
// 读取响应内容
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, nil
}
// PostRequest 统一请求
func PostRequest(url string, requestBody []byte) ([]byte, error) {
// 发起 POST 请求
resp, err := http.Post(url, "application/json", bytes.NewBuffer(requestBody))
if err != nil {
return nil, err
}
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, nil
}

6
go.mod
View File

@ -15,9 +15,7 @@ require (
github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.15.1
github.com/go-redis/redis/v8 v8.11.5
github.com/golang-jwt/jwt/v5 v5.0.0
github.com/google/uuid v1.3.1
github.com/mojocn/base64Captcha v1.3.5
github.com/leeqvip/gophp v1.1.1
github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.16.0
github.com/wechatpay-apiv3/wechatpay-go v0.2.17
@ -50,7 +48,6 @@ require (
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
@ -73,7 +70,6 @@ require (
github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.10.0 // indirect
golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect
golang.org/x/net v0.11.0 // indirect
golang.org/x/sys v0.9.0 // indirect
golang.org/x/text v0.10.0 // indirect

12
go.sum
View File

@ -145,10 +145,6 @@ github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -204,8 +200,6 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
@ -237,6 +231,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/leeqvip/gophp v1.1.1 h1:+HNBayDVEKnaON0+qj/goDjlZ4Bos7hrdy6/Cb5v5M8=
github.com/leeqvip/gophp v1.1.1/go.mod h1:DRoO5E9Sk+t4/3LGPCH4QZ/arcASXk9VsqdeTXLgYC4=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
@ -252,8 +248,6 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mojocn/base64Captcha v1.3.5 h1:Qeilr7Ta6eDtG4S+tQuZ5+hO+QHbiGAJdi4PfoagaA0=
github.com/mojocn/base64Captcha v1.3.5/go.mod h1:/tTTXn4WTpX9CfrmipqRytCpJ27Uw3G6I7NcP2WwcmY=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
@ -346,8 +340,6 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190501045829-6d32002ffd75/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=

33
utils/unserialize.go Normal file
View File

@ -0,0 +1,33 @@
package utils
import (
"errors"
"regexp"
"strconv"
)
// Unserialize 反序列化 PHP 序列化的字符串
func Unserialize(data string) (string, error) {
// 使用正则表达式找到字符串值
re := regexp.MustCompile(`s:(\d+):"(.*?)";`)
match := re.FindStringSubmatch(data)
if len(match) != 3 {
return "", errors.New("invalid input format")
}
// 解析字符串长度
length, err := strconv.Atoi(match[1])
if err != nil {
return "", err
}
// 解析字符串值
value := match[2]
// 检查字符串长度是否与值长度匹配
if len(value) != length {
return "", errors.New("string length does not match")
}
return value, nil
}