2026-06-16 20:47:26 +08:00

149 lines
4.2 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 gamemanagement
import (
"context"
"crypto/rand"
"database/sql"
"fmt"
"math/big"
"net/url"
"strings"
"unicode/utf8"
)
type RobotProfile struct {
Nickname string
Avatar string
Gender string
}
type RobotProfileSource interface {
RandomRobotProfiles(ctx context.Context, count int) ([]RobotProfile, error)
}
type likeiRobotProfileSource struct {
db *sql.DB
}
func NewLikeiRobotProfileSource(db *sql.DB) RobotProfileSource {
if db == nil {
return nil
}
return &likeiRobotProfileSource{db: db}
}
// WithRobotProfileSource 注入机器人资料池;创建机器人优先消费线上 likei 用户昵称和头像,未配置时再走本地随机资料兜底。
func WithRobotProfileSource(source RobotProfileSource) Option {
return func(h *Handler) {
h.robotProfiles = source
}
}
func (s *likeiRobotProfileSource) RandomRobotProfiles(ctx context.Context, count int) ([]RobotProfile, error) {
if s == nil || s.db == nil {
return nil, fmt.Errorf("likei robot profile source is not configured")
}
if count <= 0 {
return nil, nil
}
rows, err := s.db.QueryContext(ctx, `
SELECT TRIM(user_nickname), TRIM(user_avatar)
FROM user_base_info
WHERE account_status = 'NORMAL'
AND is_del = 0
AND origin_sys = 'LIKEI'
AND user_avatar IS NOT NULL AND TRIM(user_avatar) <> ''
AND user_nickname IS NOT NULL AND TRIM(user_nickname) <> ''
AND CHAR_LENGTH(TRIM(user_nickname)) <= 64
AND CHAR_LENGTH(TRIM(user_avatar)) <= 512
AND (TRIM(user_avatar) LIKE 'http://%' OR TRIM(user_avatar) LIKE 'https://%')
ORDER BY RAND()
LIMIT ?
`, count)
if err != nil {
return nil, fmt.Errorf("query likei robot profiles: %w", err)
}
defer rows.Close()
profiles := make([]RobotProfile, 0, count)
for rows.Next() {
var profile RobotProfile
if err := rows.Scan(&profile.Nickname, &profile.Avatar); err != nil {
return nil, fmt.Errorf("scan likei robot profile: %w", err)
}
if normalized, ok := normalizeRobotProfile(profile); ok {
profiles = append(profiles, normalized)
}
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate likei robot profiles: %w", err)
}
if len(profiles) < count {
return nil, fmt.Errorf("likei robot profile pool returned %d profiles, need %d", len(profiles), count)
}
return profiles, nil
}
func robotProfilesForGenerate(ctx context.Context, source RobotProfileSource, count int) ([]RobotProfile, error) {
if source != nil {
// 已配置 likei 时继续使用真实资料池;查询失败不能静默降级,否则线上数据源故障会被批量造号掩盖。
return source.RandomRobotProfiles(ctx, count)
}
profiles := make([]RobotProfile, 0, count)
for index := 0; index < count; index++ {
profiles = append(profiles, randomRobotProfile())
}
return profiles, nil
}
func randomRobotProfile() RobotProfile {
seed := randomRobotHex(8)
nameSeed := randomRobotHex(3)
if seed == "fallback" {
seed = fmt.Sprintf("fallback-%d", randomRobotInt(1000000))
}
// 本地随机头像只保存绝对 URL不在 admin-server 里下载或转存图片user-service 仍负责资料字段校验。
return RobotProfile{
Nickname: fmt.Sprintf("Robot %s", strings.ToUpper(nameSeed)),
Avatar: "https://api.dicebear.com/9.x/adventurer/png?seed=hyapp-robot-" + url.QueryEscape(seed),
Gender: randomRobotGender(),
}
}
func randomRobotGender() string {
if randomRobotInt(2) == 0 {
return "male"
}
return "female"
}
func randomRobotInt(max int64) int64 {
if max <= 0 {
return 0
}
value, err := rand.Int(rand.Reader, big.NewInt(max))
if err != nil {
return 0
}
return value.Int64()
}
func normalizeRobotProfile(profile RobotProfile) (RobotProfile, bool) {
profile.Nickname = strings.TrimSpace(profile.Nickname)
profile.Avatar = strings.TrimSpace(profile.Avatar)
if profile.Nickname == "" || profile.Avatar == "" {
return RobotProfile{}, false
}
if utf8.RuneCountInString(profile.Nickname) > 64 || utf8.RuneCountInString(profile.Avatar) > 512 {
return RobotProfile{}, false
}
parsed, err := url.Parse(profile.Avatar)
if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
return RobotProfile{}, false
}
if strings.ContainsAny(profile.Avatar, " \t\r\n") {
return RobotProfile{}, false
}
return profile, true
}