// Package testusers creates explicitly labelled, non-loginable test profiles. // It is only used by an operator-invoked CLI, never during server startup. package testusers import ( "fmt" "net/url" "strings" ) const Batch = "cn-adults-20260831-v1" const Count = 100 type Profile struct { PublicID string `json:"publicId"` IsTest bool `json:"isTest"` TestBatch string `json:"testBatch"` Label string `json:"label"` Nickname string `json:"nickname"` Gender int `json:"gender"` Birthday string `json:"birthday"` Height int `json:"height"` CityCode string `json:"cityCode"` City string `json:"city"` Occupation string `json:"occupation"` Bio string `json:"bio"` AvatarFile string `json:"avatarFile"` Avatar string `json:"avatar"` Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` } type fixtureCity struct { Code string Name string Latitude float64 Longitude float64 } // Avatars contains five independently generated adult portraits per gender. // Reuse is intentional for fixtures; these are not 100 real identities. func Avatars() []string { files := make([]string, 0, 10) for gender := 1; gender <= 2; gender++ { for n := 1; n <= 5; n++ { files = append(files, fmt.Sprintf("900028%d%d-1.png", gender, n)) } } return files } func Generate(publicBase string) ([]Profile, error) { base, err := url.Parse(strings.TrimRight(publicBase, "/")) if err != nil || base.Host == "" || base.User != nil || base.RawQuery != "" || base.Fragment != "" || (base.Scheme != "http" && base.Scheme != "https") { return nil, fmt.Errorf("public base must be an absolute HTTP(S) uploads URL without credentials, query or fragment") } if base.Scheme == "http" && base.Hostname() != "127.0.0.1" && base.Hostname() != "localhost" && base.Hostname() != "::1" { return nil, fmt.Errorf("non-local avatar URLs require HTTPS") } surnames := []string{"陈", "林", "周", "许", "苏", "沈", "陆", "顾", "方", "季"} given := [][]string{{"沐川", "知远", "星河", "景行", "予安"}, {"晚晴", "知夏", "语桐", "清禾", "若宁"}} cities := []fixtureCity{ {"310100", "上海", 31.2304, 121.4737}, {"440100", "广州", 23.1291, 113.2644}, {"440300", "深圳", 22.5431, 114.0579}, {"330100", "杭州", 30.2741, 120.1551}, {"510100", "成都", 30.5728, 104.0668}, {"420100", "武汉", 30.5928, 114.3055}, {"320100", "南京", 32.0603, 118.7969}, {"350200", "厦门", 24.4798, 118.0894}, {"610100", "西安", 34.3416, 108.9398}, {"370200", "青岛", 36.0671, 120.3826}, } jobs := []string{"设计师", "工程师", "教师", "摄影师", "产品经理"} hobbies := []string{"摄影与城市漫步", "阅读与咖啡", "跑步与音乐", "旅行与美食", "电影与绘画"} ages := [2][5]int{{27, 30, 24, 33, 28}, {26, 29, 24, 32, 27}} items := make([]Profile, 0, Count) for gender := 1; gender <= 2; gender++ { for i := 0; i < 50; i++ { avatarFile := fmt.Sprintf("900028%d%d-1.png", gender, i%5+1) city := cities[i%len(cities)] ring := i / len(cities) latitude := city.Latitude + float64(ring-2)*0.012 + float64(gender-1)*0.006 longitude := city.Longitude + float64((ring*2+gender)%5-2)*0.012 items = append(items, Profile{ PublicID: fmt.Sprintf("TESTCN%06d", (gender-1)*50+i+1), IsTest: true, TestBatch: Batch, Label: "测试用户", Nickname: "测试·" + surnames[i/5] + given[gender-1][i%5], Gender: gender, Birthday: fmt.Sprintf("%d-%02d-%02d", 2026-ages[gender-1][i%5], i%6+1, i%27+1), Height: 160 + (2-gender)*12 + i%12, CityCode: city.Code, City: city.Name, Occupation: jobs[i%len(jobs)], Bio: "【测试数据】虚构资料,AI生成中国成年人头像,仅供功能测试,非真实交友用户。兴趣示例:" + hobbies[i%len(hobbies)] + "。", AvatarFile: avatarFile, Avatar: base.String() + "/" + avatarFile, Latitude: latitude, Longitude: longitude, }) } } return items, nil }