2026-05-16 08:12:42 +08:00

176 lines
6.0 KiB
Go

package auth
import (
"context"
"database/sql"
"strings"
"time"
"hyapp/pkg/appcode"
authdomain "hyapp/services/user-service/internal/domain/auth"
)
func (r *Repository) CreateLoginIPRiskJob(ctx context.Context, job authdomain.LoginIPRiskJob) error {
_, err := r.db.ExecContext(ctx, `
INSERT INTO login_ip_risk_jobs (
app_code, job_id, session_id, user_id, client_ip, client_ip_hash, channel, platform, language, timezone,
request_id, edge_country_code, status, decision, country_code, failure_reason, locked_by, locked_until_ms,
attempt_count, created_at_ms, updated_at_ms
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', '', '', NULL, NULL, 0, ?, ?)
`, appcode.Normalize(job.AppCode), job.JobID, job.SessionID, job.UserID, job.ClientIP, job.ClientIPHash, job.Channel, job.Platform, job.Language, job.Timezone, job.RequestID, job.EdgeCountryCode, job.Status, job.CreatedAtMs, job.UpdatedAtMs)
return err
}
func (r *Repository) ClaimLoginIPRiskJobs(ctx context.Context, workerID string, nowMs int64, lockTTL time.Duration, batchSize int) ([]authdomain.LoginIPRiskJob, error) {
if batchSize <= 0 {
batchSize = 100
}
rows, err := r.db.QueryContext(ctx, `
SELECT app_code, job_id, session_id, user_id, client_ip, client_ip_hash, channel, platform, language, timezone,
request_id, edge_country_code, status, decision, country_code, failure_reason,
COALESCE(locked_by, ''), COALESCE(locked_until_ms, 0), attempt_count, created_at_ms, updated_at_ms
FROM login_ip_risk_jobs
WHERE app_code = ?
AND status IN (?, ?)
AND (locked_until_ms IS NULL OR locked_until_ms <= ?)
ORDER BY updated_at_ms ASC, job_id ASC
LIMIT ?
`, appcode.FromContext(ctx), authdomain.LoginIPRiskStatusPending, authdomain.LoginIPRiskStatusFailed, nowMs, batchSize)
if err != nil {
return nil, err
}
defer rows.Close()
candidates := make([]authdomain.LoginIPRiskJob, 0, batchSize)
for rows.Next() {
job, err := scanLoginIPRiskJob(rows)
if err != nil {
return nil, err
}
candidates = append(candidates, job)
}
if err := rows.Err(); err != nil {
return nil, err
}
claimed := make([]authdomain.LoginIPRiskJob, 0, len(candidates))
lockUntilMs := nowMs + lockTTL.Milliseconds()
for _, job := range candidates {
result, err := r.db.ExecContext(ctx, `
UPDATE login_ip_risk_jobs
SET status = ?, locked_by = ?, locked_until_ms = ?, attempt_count = attempt_count + 1, updated_at_ms = ?
WHERE app_code = ? AND job_id = ?
AND status IN (?, ?)
AND (locked_until_ms IS NULL OR locked_until_ms <= ?)
`, authdomain.LoginIPRiskStatusRunning, workerID, lockUntilMs, nowMs, job.AppCode, job.JobID, authdomain.LoginIPRiskStatusPending, authdomain.LoginIPRiskStatusFailed, nowMs)
if err != nil {
return nil, err
}
affected, err := result.RowsAffected()
if err != nil {
return nil, err
}
if affected == 1 {
job.Status = authdomain.LoginIPRiskStatusRunning
job.LockedBy = workerID
job.LockedUntilMs = lockUntilMs
job.AttemptCount++
job.UpdatedAtMs = nowMs
claimed = append(claimed, job)
}
}
return claimed, nil
}
func (r *Repository) CompleteLoginIPRiskJob(ctx context.Context, job authdomain.LoginIPRiskJob) error {
_, err := r.db.ExecContext(ctx, `
UPDATE login_ip_risk_jobs
SET status = ?, decision = ?, country_code = ?, failure_reason = ?, locked_by = NULL, locked_until_ms = NULL, updated_at_ms = ?
WHERE app_code = ? AND job_id = ?
`, job.Status, job.Decision, job.CountryCode, job.FailureReason, job.UpdatedAtMs, appcode.Normalize(job.AppCode), job.JobID)
return err
}
func (r *Repository) CreateLoginIPRiskDecision(ctx context.Context, decision authdomain.LoginIPRiskDecision) error {
_, err := r.db.ExecContext(ctx, `
INSERT INTO login_ip_risk_decisions (
app_code, decision_id, client_ip, client_ip_hash, decision, country_code, confidence,
provider_attempts_json, decided_at_ms, expires_at_ms, created_at_ms
)
VALUES (?, ?, ?, ?, ?, ?, ?, CAST(? AS JSON), ?, ?, ?)
`, appcode.Normalize(decision.AppCode), decision.DecisionID, decision.ClientIP, decision.ClientIPHash, decision.Decision, decision.CountryCode, decision.Confidence, decision.ProviderAttemptsJSON, decision.DecidedAtMs, decision.ExpiresAtMs, decision.CreatedAtMs)
return err
}
func (r *Repository) ListEnabledLoginRiskCountryBlocks(ctx context.Context) ([]authdomain.LoginRiskCountryBlock, error) {
rows, err := r.db.QueryContext(ctx, `
SELECT app_code, block_id, keyword, country_code, enabled, created_at_ms, updated_at_ms
FROM login_risk_country_blocks
WHERE app_code = ? AND enabled = 1
ORDER BY country_code ASC, keyword ASC, block_id ASC
`, appcode.FromContext(ctx))
if err != nil {
return nil, err
}
defer rows.Close()
blocks := make([]authdomain.LoginRiskCountryBlock, 0)
for rows.Next() {
var block authdomain.LoginRiskCountryBlock
if err := rows.Scan(&block.AppCode, &block.BlockID, &block.Keyword, &block.CountryCode, &block.Enabled, &block.CreatedAtMs, &block.UpdatedAtMs); err != nil {
return nil, err
}
block.CountryCode = normalizeCountryCode(block.CountryCode)
blocks = append(blocks, block)
}
if err := rows.Err(); err != nil {
return nil, err
}
return blocks, nil
}
type loginIPRiskJobScanner interface {
Scan(dest ...any) error
}
func normalizeCountryCode(country string) string {
country = strings.ToUpper(strings.TrimSpace(country))
if len(country) < 2 || len(country) > 3 {
return ""
}
return country
}
func scanLoginIPRiskJob(scanner loginIPRiskJobScanner) (authdomain.LoginIPRiskJob, error) {
var job authdomain.LoginIPRiskJob
err := scanner.Scan(
&job.AppCode,
&job.JobID,
&job.SessionID,
&job.UserID,
&job.ClientIP,
&job.ClientIPHash,
&job.Channel,
&job.Platform,
&job.Language,
&job.Timezone,
&job.RequestID,
&job.EdgeCountryCode,
&job.Status,
&job.Decision,
&job.CountryCode,
&job.FailureReason,
&job.LockedBy,
&job.LockedUntilMs,
&job.AttemptCount,
&job.CreatedAtMs,
&job.UpdatedAtMs,
)
if err == sql.ErrNoRows {
return authdomain.LoginIPRiskJob{}, err
}
return job, err
}