优化小程序
This commit is contained in:
1 parent
2e2b837a20
commit
727319a6eb
24 files changed
+1509
-200
No files matched your search
+112
-2
@@ -6,9 +6,15 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/core/logs"
|
||||
)
|
||||
|
||||
// =============================================================
|
||||
@@ -48,6 +54,106 @@ func (e *APIError) Explain() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// ============================ 出口 IP 探测(40164 IP 白名单排查用) ============================
|
||||
|
||||
var (
|
||||
outboundIPMu sync.Mutex
|
||||
outboundIPCache string
|
||||
outboundIPAt time.Time
|
||||
)
|
||||
|
||||
// outboundIPTTL 出口 IP 缓存时长(探测失败也按此负缓存,避免每次请求都外网探测)
|
||||
const outboundIPTTL = 5 * time.Minute
|
||||
|
||||
// ipDetectServices 出口 IP 探测服务,按顺序尝试,任一返回 IPv4 即采用
|
||||
var ipDetectServices = []string{"https://ifconfig.me/ip", "https://api.ipify.org", "https://myip.ipip.net"}
|
||||
|
||||
var ipv4Re = regexp.MustCompile(`\d{1,3}(?:\.\d{1,3}){3}`)
|
||||
|
||||
// OutboundIP 返回本服务器访问公网时的出口 IP(微信后台「IP白名单」需填入该 IP)。
|
||||
// 结果内存缓存 5 分钟;服务器无法访问探测服务时返回空串,不影响主流程。
|
||||
func OutboundIP() string {
|
||||
outboundIPMu.Lock()
|
||||
if time.Since(outboundIPAt) < outboundIPTTL {
|
||||
ip := outboundIPCache
|
||||
outboundIPMu.Unlock()
|
||||
return ip
|
||||
}
|
||||
outboundIPMu.Unlock()
|
||||
|
||||
ip := detectOutboundIP()
|
||||
outboundIPMu.Lock()
|
||||
outboundIPCache = ip
|
||||
outboundIPAt = time.Now()
|
||||
outboundIPMu.Unlock()
|
||||
return ip
|
||||
}
|
||||
|
||||
func detectOutboundIP() string {
|
||||
client := &http.Client{Timeout: 3 * time.Second}
|
||||
for _, svc := range ipDetectServices {
|
||||
resp, err := client.Get(svc)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
body, rerr := io.ReadAll(io.LimitReader(resp.Body, 256))
|
||||
resp.Body.Close()
|
||||
if rerr != nil {
|
||||
continue
|
||||
}
|
||||
if ip := firstIPv4(string(body)); ip != "" {
|
||||
return ip
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// firstIPv4 取文本中第一个合法 IPv4(形如 1.2.3.4),无则返回空串。
|
||||
func firstIPv4(s string) string {
|
||||
m := ipv4Re.FindString(s)
|
||||
if m == "" || net.ParseIP(m) == nil {
|
||||
return ""
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// ErrMsgIPs 从微信 errmsg 中提取 IP(如 "invalid ip 1.2.3.4 ipv6 ::ffff:1.2.3.4")。
|
||||
func ErrMsgIPs(errmsg string) []string {
|
||||
var out []string
|
||||
seen := make(map[string]bool)
|
||||
for _, m := range ipv4Re.FindAllString(errmsg, -1) {
|
||||
if net.ParseIP(m) == nil || seen[m] {
|
||||
continue
|
||||
}
|
||||
seen[m] = true
|
||||
out = append(out, m)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// logAPIError 记录微信接口错误详情:
|
||||
// errcode / errmsg / 微信提示的 IP / 本服务器出口 IP / 排查建议只落服务端日志。
|
||||
// 注册页、登录页等匿名可访问页面不应回显这些信息(暴露服务器出口 IP 与内部错误)。
|
||||
func logAPIError(scene string, err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
detail := err.Error()
|
||||
var ae *APIError
|
||||
if errors.As(err, &ae) && ae != nil {
|
||||
if hint := ae.Explain(); hint != "" {
|
||||
detail += ";" + hint
|
||||
}
|
||||
if ips := ErrMsgIPs(ae.Msg); len(ips) > 0 {
|
||||
detail += ";微信提示的 IP:" + strings.Join(ips, ", ")
|
||||
}
|
||||
}
|
||||
if ip := OutboundIP(); ip != "" {
|
||||
detail += ";本服务器出口 IP:" + ip
|
||||
}
|
||||
logs.Warning("[wechat-mp] %s 失败:%s", scene, detail)
|
||||
}
|
||||
|
||||
// ExplainError 提取微信接口错误的中文排查建议;无可用建议时返回 ("", false)。
|
||||
func ExplainError(err error) (string, bool) {
|
||||
var ae *APIError
|
||||
@@ -84,7 +190,9 @@ func GetAccessToken(cfg *Config) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
if resp.AccessToken == "" {
|
||||
return "", &APIError{API: "获取 access_token", Code: resp.ErrCode, Msg: resp.ErrMsg}
|
||||
apiErr := &APIError{API: "获取 access_token", Code: resp.ErrCode, Msg: resp.ErrMsg}
|
||||
logAPIError("获取 access_token", apiErr)
|
||||
return "", apiErr
|
||||
}
|
||||
ttl := time.Duration(resp.ExpiresIn) * time.Second
|
||||
if ttl <= tokenMinTTL {
|
||||
@@ -125,7 +233,9 @@ func CreateQRCode(cfg *Config, scene string, expireSeconds int) (string, error)
|
||||
return "", err
|
||||
}
|
||||
if resp.Ticket == "" {
|
||||
return "", &APIError{API: "创建带参二维码", Code: resp.ErrCode, Msg: resp.ErrMsg}
|
||||
apiErr := &APIError{API: "创建带参二维码", Code: resp.ErrCode, Msg: resp.ErrMsg}
|
||||
logAPIError("创建带参二维码", apiErr)
|
||||
return "", apiErr
|
||||
}
|
||||
return "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" + url.QueryEscape(resp.Ticket), nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/core/logs"
|
||||
|
||||
"server/models"
|
||||
)
|
||||
|
||||
@@ -18,6 +20,13 @@ import (
|
||||
// WechatBindTypeRegister 注册验证码会话类型(yz_platform_wechat_mp_verify_code.bind_type)
|
||||
const WechatBindTypeRegister = "register"
|
||||
|
||||
// ErrRegisterCodeInvalid 账号注册:验证码无效 / 已使用(引导语指向「注册」而非绑定用的「验证码」)
|
||||
var ErrRegisterCodeInvalid = errors.New("验证码不正确或已过期:请在公众号内重新发送「注册」获取")
|
||||
|
||||
// RegisterQRDownNotice 带参二维码不可用(IP 白名单 40164 / 未认证 48001 等)时,
|
||||
// 回显给注册页的用户级引导:详细原因见服务端日志,不暴露服务器出口 IP。
|
||||
const RegisterQRDownNotice = "二维码暂不可用:请在公众号内直接发送「注册」获取验证码"
|
||||
|
||||
// StartRegisterVerify 创建注册验证码会话并生成带参二维码。
|
||||
// 带参二维码生成失败(未认证公众号 48001 等)时回退:qrURL 为空、
|
||||
// notice 返回引导(用户在公众号内发送「注册」获取验证码),scene 仍有效可轮询。
|
||||
@@ -48,24 +57,43 @@ func StartRegisterVerify(ttl time.Duration) (scene, qrURL, notice string, expire
|
||||
}
|
||||
qrURL, err = CreateQRCode(cfg, scene, int(ttl.Seconds())+120)
|
||||
if err != nil {
|
||||
// 不吞真实错误:与扫码登录 StartScanLogin 一致,把微信返回的
|
||||
// errcode 与排查建议(IP 白名单 / 48001 未认证等)透出,便于定位;
|
||||
// 同时保留「公众号内发送注册」兜底路径,会话仍有效可轮询核销。
|
||||
notice := "请在公众号内直接发送「注册」获取验证码"
|
||||
var apiErr *APIError
|
||||
if errors.As(err, &apiErr) {
|
||||
notice = "生成带参二维码失败(" + apiErr.Error() + ")。" + notice
|
||||
if hint := apiErr.Explain(); hint != "" {
|
||||
notice += ":" + hint
|
||||
}
|
||||
} else {
|
||||
notice = "生成带参二维码失败:" + err.Error() + "。" + notice
|
||||
}
|
||||
return scene, "", notice, expireAt, nil
|
||||
// 注册页匿名可访问:errcode / errmsg / 服务器出口 IP 等排查信息已在
|
||||
// CreateQRCode 内落到服务端日志(见 logAPIError),此处只回显用户级引导,
|
||||
// 避免向访客泄露服务器出口 IP 与内部错误细节;
|
||||
// 会话仍然有效,「公众号内发送注册」这条兜底路径可正常核销。
|
||||
logs.Warning("[wechat-mp] 注册二维码不可用,已回退公众号发送「注册」:%v", err)
|
||||
return scene, "", RegisterQRDownNotice, expireAt, nil
|
||||
}
|
||||
return scene, qrURL, "", expireAt, nil
|
||||
}
|
||||
|
||||
// ConfirmAccountRegisterVerify 账号注册的验证码核销:
|
||||
// 优先按 scene 核销二维码会话;该会话尚未扫码(用户没扫码,改为在公众号内发送
|
||||
// 「注册」取码)、scene 失效或验证码不属于该会话时,直接按验证码认领会话。
|
||||
//
|
||||
// 账号注册不绑定手机号,服务号无法按手机号把「发送者」与网页会话关联起来,
|
||||
// 因此不要求网页会话必须先被扫码:验证码有效即视为公众号内本人操作。
|
||||
func ConfirmAccountRegisterVerify(scene, code string) (string, error) {
|
||||
code = strings.TrimSpace(code)
|
||||
if code == "" {
|
||||
return "", ErrCodeNotPending
|
||||
}
|
||||
if strings.TrimSpace(scene) != "" {
|
||||
if openID, err := ConfirmRegisterVerify(scene, code); err == nil {
|
||||
return openID, nil
|
||||
}
|
||||
}
|
||||
openID, err := ConfirmRegisterVerifyByCode(code)
|
||||
if err != nil {
|
||||
// 按码认领失败的提示要指向注册场景(发送「注册」),而非绑定场景的「验证码」
|
||||
if errors.Is(err, ErrCodeNotPending) || errors.Is(err, ErrCodeUsed) {
|
||||
return "", ErrRegisterCodeInvalid
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return openID, nil
|
||||
}
|
||||
|
||||
// GetRegisterVerifyStatus 查询注册会话扫码状态(网页轮询展示「已扫码」)
|
||||
func GetRegisterVerifyStatus(scene string) (scanned bool, expireAt time.Time, err error) {
|
||||
row, err := GetVerifyCode(scene)
|
||||
|
||||
Reference in new issue
Block a user