149 lines
4.2 KiB
Go
149 lines
4.2 KiB
Go
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
|
||
}
|