fix(t236): retry rejected ERP captchas
This commit is contained in:
@@ -23,6 +23,9 @@ import (
|
||||
const (
|
||||
defaultSessionTimeout = 30 * time.Second
|
||||
defaultCaptchaTTL = 5 * time.Minute
|
||||
authenticationBudget = 20 * time.Second
|
||||
maxCaptchaRetries = 6
|
||||
maxCaptchaAttempts = maxCaptchaRetries + 1
|
||||
maxCaptchaBytes = 2 << 20
|
||||
maxERPResponseBytes = 4 << 20
|
||||
maxDiagnosticBytes = 4 << 10
|
||||
@@ -30,6 +33,7 @@ const (
|
||||
|
||||
var (
|
||||
ErrCaptchaTicketInvalid = errors.New("ERP captcha ticket is invalid")
|
||||
ErrCaptchaRejected = errors.New("ERP captcha was rejected")
|
||||
ErrLoginRejected = errors.New("ERP login was rejected")
|
||||
)
|
||||
|
||||
@@ -143,7 +147,9 @@ func NewSessionManager(config SessionConfig) (*SessionManager, error) {
|
||||
func (manager *SessionManager) EnsureAuthenticated(ctx context.Context) error {
|
||||
manager.authMu.Lock()
|
||||
defer manager.authMu.Unlock()
|
||||
if _, err := manager.Validate(ctx); err == nil {
|
||||
authCtx, cancel := context.WithTimeout(ctx, authenticationBudget)
|
||||
defer cancel()
|
||||
if _, err := manager.Validate(authCtx); err == nil {
|
||||
return nil
|
||||
} else if !errors.Is(err, domain.ErrFreightSourceSessionNeeded) {
|
||||
return err
|
||||
@@ -151,6 +157,30 @@ func (manager *SessionManager) EnsureAuthenticated(ctx context.Context) error {
|
||||
if manager.recognizer == nil {
|
||||
return domain.ErrFreightSourceOCRInvalid
|
||||
}
|
||||
for attempt := 1; attempt <= maxCaptchaAttempts; attempt++ {
|
||||
err := manager.authenticateOnce(authCtx)
|
||||
switch {
|
||||
case err == nil:
|
||||
manager.logERPLoginAttempt(attempt, "success")
|
||||
return nil
|
||||
case errors.Is(err, ErrCaptchaRejected):
|
||||
manager.logERPLoginAttempt(attempt, "captcha_rejected")
|
||||
if attempt < maxCaptchaAttempts {
|
||||
continue
|
||||
}
|
||||
return domain.ErrFreightSourceLoginRejected
|
||||
case errors.Is(err, ErrLoginRejected):
|
||||
manager.logERPLoginAttempt(attempt, "login_rejected")
|
||||
return domain.ErrFreightSourceLoginRejected
|
||||
default:
|
||||
manager.logERPLoginAttempt(attempt, "failed")
|
||||
return err
|
||||
}
|
||||
}
|
||||
return domain.ErrFreightSourceLoginRejected
|
||||
}
|
||||
|
||||
func (manager *SessionManager) authenticateOnce(ctx context.Context) error {
|
||||
status, err := manager.FetchCaptcha(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -171,9 +201,6 @@ func (manager *SessionManager) EnsureAuthenticated(ctx context.Context) error {
|
||||
}
|
||||
manager.logOCRResult(code)
|
||||
_, err = manager.Login(ctx, status.CaptchaTicket, code)
|
||||
if errors.Is(err, ErrLoginRejected) {
|
||||
return domain.ErrFreightSourceLoginRejected
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -393,9 +420,10 @@ func (manager *SessionManager) requestJSONLocked(
|
||||
}
|
||||
manager.logERPResponse(request, response, contentBytes, false)
|
||||
var envelope struct {
|
||||
Status *bool `json:"status"`
|
||||
Code json.RawMessage `json:"code"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Status *bool `json:"status"`
|
||||
Code json.RawMessage `json:"code"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Message *string `json:"msg"`
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(contentBytes))
|
||||
if err := decoder.Decode(&envelope); err != nil || envelope.Status == nil ||
|
||||
@@ -408,6 +436,9 @@ func (manager *SessionManager) requestJSONLocked(
|
||||
}
|
||||
if !*envelope.Status {
|
||||
if loginRequest {
|
||||
if envelope.Message != nil && captchaRejectedMessage(*envelope.Message) {
|
||||
return nil, ErrCaptchaRejected
|
||||
}
|
||||
return nil, ErrLoginRejected
|
||||
}
|
||||
if unauthenticatedCode(envelope.Code) {
|
||||
@@ -428,6 +459,10 @@ func (manager *SessionManager) requestJSONLocked(
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func captchaRejectedMessage(message string) bool {
|
||||
return strings.TrimSpace(message) == "图片验证码不正确"
|
||||
}
|
||||
|
||||
func unauthenticatedCode(raw json.RawMessage) bool {
|
||||
var value any
|
||||
decoder := json.NewDecoder(bytes.NewReader(raw))
|
||||
@@ -538,6 +573,17 @@ func (manager *SessionManager) logOCRResultFailed() {
|
||||
manager.diagnosticLog("erp_ocr_result class=failed")
|
||||
}
|
||||
|
||||
func (manager *SessionManager) logERPLoginAttempt(attempt int, result string) {
|
||||
if !manager.diagnosticsOn {
|
||||
return
|
||||
}
|
||||
manager.diagnosticLog(
|
||||
"erp_login_attempt attempt=" + strconv.Itoa(attempt) +
|
||||
" max_attempts=" + strconv.Itoa(maxCaptchaAttempts) +
|
||||
" result=" + result,
|
||||
)
|
||||
}
|
||||
|
||||
func (manager *SessionManager) logERPResponsePreview(
|
||||
request *http.Request,
|
||||
response *http.Response,
|
||||
|
||||
Reference in New Issue
Block a user