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

195 lines
5.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package regionblock
import (
"context"
"database/sql"
"errors"
"regexp"
"strings"
"time"
"unicode/utf8"
"hyapp-admin-server/internal/appctx"
)
const (
maxRegionBlockKeywords = 200
maxRegionBlockKeywordLength = 128
)
var (
countryCodePattern = regexp.MustCompile(`^[A-Za-z]{2,3}$`)
errInvalidPayload = errors.New("region block payload invalid")
)
type Service struct {
userDB *sql.DB
}
type RegionBlock struct {
BlockID int64 `json:"block_id"`
Keyword string `json:"keyword"`
CountryCode string `json:"country_code"`
Enabled bool `json:"enabled"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
type normalizedKeyword struct {
Keyword string
CountryCode string
}
type countryCodeQuerier interface {
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}
func NewService(userDB *sql.DB) *Service {
return &Service{userDB: userDB}
}
// List 返回当前 App 已启用的登录地区屏蔽词。
// 表属于 user-service 登录风控事实admin-server 只作为后台维护入口直接写 user DB。
func (s *Service) List(ctx context.Context) ([]RegionBlock, error) {
if s.userDB == nil {
return nil, errors.New("user db is not configured")
}
return queryEnabledBlocks(ctx, s.userDB, appctx.FromContext(ctx))
}
// Replace 用页面提交的词表整体替换当前 App 的生效配置。
// 整体替换比逐条增删更适合配置页保存语义,也能避免客户端和服务端出现半旧半新的屏蔽词集合。
func (s *Service) Replace(ctx context.Context, actorID int64, keywords []string) ([]RegionBlock, error) {
if s.userDB == nil {
return nil, errors.New("user db is not configured")
}
normalized, err := normalizeKeywords(ctx, s.userDB, appctx.FromContext(ctx), keywords)
if err != nil {
return nil, err
}
tx, err := s.userDB.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer tx.Rollback()
nowMS := time.Now().UTC().UnixMilli()
appCode := appctx.FromContext(ctx)
if _, err := tx.ExecContext(ctx, `
UPDATE login_risk_country_blocks
SET enabled = 0, updated_by_user_id = ?, updated_at_ms = ?
WHERE app_code = ? AND enabled = 1
`, nullableActor(actorID), nowMS, appCode); err != nil {
return nil, err
}
for _, keyword := range normalized {
if _, err := tx.ExecContext(ctx, `
INSERT INTO login_risk_country_blocks (
app_code, keyword, country_code, enabled,
created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
)
VALUES (?, ?, ?, 1, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
country_code = VALUES(country_code),
enabled = 1,
updated_by_user_id = VALUES(updated_by_user_id),
updated_at_ms = VALUES(updated_at_ms)
`, appCode, keyword.Keyword, keyword.CountryCode, nullableActor(actorID), nullableActor(actorID), nowMS, nowMS); err != nil {
return nil, err
}
}
if err := tx.Commit(); err != nil {
return nil, err
}
return s.List(ctx)
}
func normalizeKeywords(ctx context.Context, querier countryCodeQuerier, appCode string, raw []string) ([]normalizedKeyword, error) {
if len(raw) > maxRegionBlockKeywords {
return nil, errInvalidPayload
}
result := make([]normalizedKeyword, 0, len(raw))
seen := map[string]struct{}{}
for _, item := range raw {
keyword := strings.TrimSpace(item)
if keyword == "" || utf8.RuneCountInString(keyword) > maxRegionBlockKeywordLength {
return nil, errInvalidPayload
}
if countryCodePattern.MatchString(keyword) {
keyword = strings.ToUpper(keyword)
}
key := strings.ToLower(keyword)
if _, ok := seen[key]; ok {
continue
}
countryCode, err := resolveCountryCode(ctx, querier, appCode, keyword)
if err != nil {
return nil, err
}
seen[key] = struct{}{}
result = append(result, normalizedKeyword{Keyword: keyword, CountryCode: countryCode})
}
return result, nil
}
func resolveCountryCode(ctx context.Context, querier countryCodeQuerier, appCode string, keyword string) (string, error) {
if countryCodePattern.MatchString(keyword) {
return strings.ToUpper(keyword), nil
}
upperKeyword := strings.ToUpper(keyword)
var countryCode string
err := querier.QueryRowContext(ctx, `
SELECT country_code
FROM countries
WHERE app_code = ?
AND (
country_name = ?
OR country_display_name = ?
OR UPPER(country_code) = ?
OR UPPER(COALESCE(iso_alpha3, '')) = ?
)
ORDER BY enabled DESC, sort_order ASC, country_id ASC
LIMIT 1
`, appCode, keyword, keyword, upperKeyword, upperKeyword).Scan(&countryCode)
if errors.Is(err, sql.ErrNoRows) {
return "", nil
}
if err != nil {
return "", err
}
return strings.ToUpper(strings.TrimSpace(countryCode)), nil
}
func queryEnabledBlocks(ctx context.Context, db *sql.DB, appCode string) ([]RegionBlock, error) {
rows, err := db.QueryContext(ctx, `
SELECT 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)
if err != nil {
return nil, err
}
defer rows.Close()
items := make([]RegionBlock, 0)
for rows.Next() {
var item RegionBlock
if err := rows.Scan(&item.BlockID, &item.Keyword, &item.CountryCode, &item.Enabled, &item.CreatedAtMS, &item.UpdatedAtMS); err != nil {
return nil, err
}
items = append(items, item)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
func nullableActor(actorID int64) any {
if actorID <= 0 {
return nil
}
return actorID
}