payx: auto re-login on upstream token expiry
merchantPost now retries once after clearing the cached token and re-authenticating when the upstream signals an invalid session: HTML login page, HTTP 401, or business msgs containing login-expiry keywords. Previously a stale token left stock queries returning empty until manual intervention. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
@@ -24,6 +25,9 @@ type apiError struct{ msg string }
|
|||||||
|
|
||||||
func (e *apiError) Error() string { return e.msg }
|
func (e *apiError) Error() string { return e.msg }
|
||||||
|
|
||||||
|
// errNeedRelogin 上游登录态失效的信号(HTML 登录页 / HTTP 401),merchantPost 捕获后自动重登重试。
|
||||||
|
var errNeedRelogin = errors.New("upstream re-login required")
|
||||||
|
|
||||||
// envelope 上游统一响应信封 {code,msg,data};code==1 为成功。
|
// envelope 上游统一响应信封 {code,msg,data};code==1 为成功。
|
||||||
type envelope struct {
|
type envelope struct {
|
||||||
Code int64 `json:"code"`
|
Code int64 `json:"code"`
|
||||||
@@ -134,13 +138,38 @@ func (c *Client) ensureSession() (string, error) {
|
|||||||
return c.login()
|
return c.login()
|
||||||
}
|
}
|
||||||
|
|
||||||
// merchantPost 商户接口调用:带 Merchant-Token;失败原样透出,不自动重登。
|
// merchantPost 商户接口调用:带 Merchant-Token;令牌失效(登录页/401)时自动重登并重试一次。
|
||||||
func (c *Client) merchantPost(ctx context.Context, path string, body, out any) error {
|
func (c *Client) merchantPost(ctx context.Context, path string, body, out any) error {
|
||||||
tok, err := c.ensureSession()
|
tok, err := c.ensureSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return c.postOnce(ctx, path, body, tok, out)
|
err = c.postOnce(ctx, path, body, tok, out)
|
||||||
|
if !errors.Is(err, errNeedRelogin) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 旧令牌失效:清缓存强制重登,再原样重试一次;仍失败则透出
|
||||||
|
c.mu.Lock()
|
||||||
|
c.token = ""
|
||||||
|
c.mu.Unlock()
|
||||||
|
tok, err = c.login()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("上游令牌失效且重登失败: %w", err)
|
||||||
|
}
|
||||||
|
if err = c.postOnce(ctx, path, body, tok, out); errors.Is(err, errNeedRelogin) {
|
||||||
|
return &apiError{"上游令牌失效,重登后仍被拒绝"}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// reloginMsg 上游业务错误文案是否属于登录态失效。
|
||||||
|
func reloginMsg(msg string) bool {
|
||||||
|
for _, kw := range []string{"未登录", "请先登录", "登录已过期", "登录已失效", "重新登录", "登录状态"} {
|
||||||
|
if strings.Contains(msg, kw) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// post 通用调用:merchantToken 为空=买家接口;data 解码到 out。
|
// post 通用调用:merchantToken 为空=买家接口;data 解码到 out。
|
||||||
@@ -156,13 +185,17 @@ func (c *Client) postOnce(ctx context.Context, path string, body any, merchantTo
|
|||||||
trimmed := bytes.TrimSpace(raw)
|
trimmed := bytes.TrimSpace(raw)
|
||||||
// 令牌失效/被风控时上游会回 HTML 登录页,统一归为鉴权类错误触发重登
|
// 令牌失效/被风控时上游会回 HTML 登录页,统一归为鉴权类错误触发重登
|
||||||
if len(trimmed) > 0 && trimmed[0] == '<' {
|
if len(trimmed) > 0 && trimmed[0] == '<' {
|
||||||
return &apiError{"上游返回登录页(token 已失效)"}
|
return fmt.Errorf("上游返回登录页(token 已失效): %w", errNeedRelogin)
|
||||||
}
|
}
|
||||||
var env envelope
|
var env envelope
|
||||||
if err := json.Unmarshal(trimmed, &env); err != nil {
|
if err := json.Unmarshal(trimmed, &env); err != nil {
|
||||||
return fmt.Errorf("上游响应解析失败: %w", err)
|
return fmt.Errorf("上游响应解析失败: %w", err)
|
||||||
}
|
}
|
||||||
if env.Code != 1 {
|
if env.Code != 1 {
|
||||||
|
// 业务错误里带登录态文案(非 HTML/401 形态)同样触发重登
|
||||||
|
if reloginMsg(env.Msg) {
|
||||||
|
return fmt.Errorf("%s: %w", env.Msg, errNeedRelogin)
|
||||||
|
}
|
||||||
return &apiError{env.Msg}
|
return &apiError{env.Msg}
|
||||||
}
|
}
|
||||||
if out != nil && len(env.Data) > 0 {
|
if out != nil && len(env.Data) > 0 {
|
||||||
@@ -193,6 +226,9 @@ func (c *Client) postRawOnce(ctx context.Context, path string, body any, merchan
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if resp.StatusCode == http.StatusUnauthorized {
|
||||||
|
return nil, fmt.Errorf("上游返回 HTTP 401: %w", errNeedRelogin)
|
||||||
|
}
|
||||||
// 5xx/HTML 网关故障单独报错;200 + HTML = 令牌失效被踢回登录页
|
// 5xx/HTML 网关故障单独报错;200 + HTML = 令牌失效被踢回登录页
|
||||||
if resp.StatusCode >= 500 {
|
if resp.StatusCode >= 500 {
|
||||||
return nil, &apiError{fmt.Sprintf("上游网关故障(HTTP %d)", resp.StatusCode)}
|
return nil, &apiError{fmt.Sprintf("上游网关故障(HTTP %d)", resp.StatusCode)}
|
||||||
|
|||||||
Reference in New Issue
Block a user