// Command import-generated-test-users validates a 100-person generated fixture // set, publishes original/display/thumbnail objects to the configured Tencent // COS bucket, and atomically creates explicitly labelled non-loginable users. // It is operator-only and is never called during application startup. package main import ( "bytes" "context" "crypto/aes" "crypto/cipher" "crypto/sha256" "database/sql" "encoding/base64" "encoding/hex" "encoding/json" "errors" "flag" "fmt" "image" "image/color" "image/draw" "image/jpeg" "image/png" "io" "net/http" "net/url" "os" "path" "path/filepath" "regexp" "sort" "strings" "time" "github.com/disintegration/imaging" "github.com/go-sql-driver/mysql" cos "github.com/tencentyun/cos-go-sdk-v5" ) const ( batchName = "cn-realistic-18-30-20260902-v1" recordCount = 100 ) type manifestRecord struct { ID int `json:"id"` Gender string `json:"gender"` Age int `json:"age"` } type assetPlan struct { ID int `json:"id"` Gender string `json:"gender"` Age int `json:"age"` File string `json:"file"` SHA256 string `json:"sha256"` OriginalKey string `json:"originalKey"` DisplayKey string `json:"displayKey"` ThumbnailKey string `json:"thumbnailKey"` OriginalURL string `json:"originalUrl"` DisplayURL string `json:"displayUrl"` ThumbnailURL string `json:"thumbnailUrl"` } type uploadResult struct { OriginalUploaded bool `json:"originalUploaded"` DisplayUploaded bool `json:"displayUploaded"` ThumbnailUploaded bool `json:"thumbnailUploaded"` } type city struct { Code string Name string Lat float64 Lng float64 } var cities = []city{ {"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}, } func decrypt(value, configKey, jwtKey string) (string, error) { if !strings.HasPrefix(value, "enc:v1:") { return value, nil } payload, err := base64.RawStdEncoding.DecodeString(strings.TrimPrefix(value, "enc:v1:")) if err != nil { return "", errors.New("invalid encrypted COS credential") } for _, raw := range []string{configKey, jwtKey + ":integration-config"} { if raw == "" { continue } key := sha256.Sum256([]byte(raw)) block, _ := aes.NewCipher(key[:]) gcm, _ := cipher.NewGCM(block) if len(payload) < gcm.NonceSize() { continue } plain, openErr := gcm.Open(nil, payload[:gcm.NonceSize()], payload[gcm.NonceSize():], nil) if openErr == nil { return string(plain), nil } } return "", errors.New("cannot decrypt COS credential with server encryption key") } func validHTTPS(raw string) (*url.URL, error) { u, err := url.Parse(strings.TrimSpace(raw)) if err != nil || u.Scheme != "https" || u.Host == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" { return nil, errors.New("COS endpoint/public base must be HTTPS without credentials, query, or fragment") } return u, nil } func loadManifest(file string) ([]manifestRecord, error) { data, err := os.ReadFile(file) if err != nil { return nil, err } var records []manifestRecord if err = json.Unmarshal(data, &records); err != nil { return nil, errors.New("manifest is not valid JSON") } if len(records) != recordCount { return nil, fmt.Errorf("manifest must contain exactly %d records", recordCount) } sort.Slice(records, func(i, j int) bool { return records[i].ID < records[j].ID }) female := 0 for index, item := range records { if item.ID != index+1 || (item.Gender != "male" && item.Gender != "female") || item.Age < 18 || item.Age > 30 { return nil, fmt.Errorf("invalid manifest record at position %d", index+1) } if item.Gender == "female" { female++ } } if female <= recordCount/2 { return nil, errors.New("manifest must contain more female than male accounts") } return records, nil } func avatarFilename(item manifestRecord) string { return fmt.Sprintf("avatar-%03d-%s.png", item.ID, item.Gender) } func planAssets(records []manifestRecord, directory, prefix, publicBase string) ([]assetPlan, error) { if !filepath.IsAbs(directory) { return nil, errors.New("avatars directory must be absolute") } if !regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9/_-]{0,119}$`).MatchString(prefix) || strings.Contains(prefix, "//") || path.Clean(prefix) != prefix { return nil, errors.New("invalid configured object prefix") } if _, err := validHTTPS(publicBase); err != nil { return nil, err } base := strings.TrimRight(publicBase, "/") plans := make([]assetPlan, 0, len(records)) for _, item := range records { name := avatarFilename(item) file := filepath.Join(directory, name) info, err := os.Lstat(file) if err != nil || !info.Mode().IsRegular() || info.Size() <= 0 || info.Size() > 12<<20 { return nil, fmt.Errorf("missing or invalid avatar %s", name) } data, err := os.ReadFile(file) if err != nil { return nil, fmt.Errorf("cannot read avatar %s", name) } cfg, err := png.DecodeConfig(bytes.NewReader(data)) if err != nil || cfg.Width < 640 || cfg.Height < 640 || int64(cfg.Width)*int64(cfg.Height) > 24_000_000 { return nil, fmt.Errorf("avatar must be a valid PNG between 640px and 24MP: %s", name) } hash := sha256.Sum256(data) digest := hex.EncodeToString(hash[:]) stem := path.Join(prefix, "test-users", batchName, fmt.Sprintf("%03d-%s-%s", item.ID, item.Gender, digest[:20])) plan := assetPlan{ ID: item.ID, Gender: item.Gender, Age: item.Age, File: name, SHA256: digest, OriginalKey: stem + "-original.png", DisplayKey: stem + "-av1.jpg", ThumbnailKey: stem + "-av1-thumb.jpg", } plan.OriginalURL = base + "/" + plan.OriginalKey plan.DisplayURL = base + "/" + plan.DisplayKey plan.ThumbnailURL = base + "/" + plan.ThumbnailKey plans = append(plans, plan) } return plans, nil } func jpegBytes(src image.Image, quality int) ([]byte, error) { opaque := image.NewRGBA(src.Bounds()) draw.Draw(opaque, opaque.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src) draw.Draw(opaque, opaque.Bounds(), src, src.Bounds().Min, draw.Over) var out bytes.Buffer err := jpeg.Encode(&out, opaque, &jpeg.Options{Quality: quality}) return out.Bytes(), err } func renderVariants(file string) (original, display, thumbnail []byte, err error) { original, err = os.ReadFile(file) if err != nil { return nil, nil, nil, err } src, err := png.Decode(bytes.NewReader(original)) if err != nil { return nil, nil, nil, err } displayImage := imaging.Fit(src, 640, 640, imaging.Lanczos) thumbImage := imaging.Fit(displayImage, 256, 256, imaging.Lanczos) display, err = jpegBytes(displayImage, 82) if err == nil { thumbnail, err = jpegBytes(thumbImage, 78) } return } func cosFailure(operation string, err error) error { var apiErr *cos.ErrorResponse if errors.As(err, &apiErr) { return fmt.Errorf("COS %s failed: %s (request %s)", operation, apiErr.Code, apiErr.RequestID) } return fmt.Errorf("COS %s failed; credentials were not logged", operation) } func verifyPublic(ctx context.Context, client *http.Client, objectURL, contentType string, expected []byte) (int, error) { req, _ := http.NewRequestWithContext(ctx, http.MethodGet, objectURL, nil) res, err := client.Do(req) if err != nil { return 0, errors.New("public COS request failed") } defer res.Body.Close() if res.StatusCode != http.StatusOK { return res.StatusCode, fmt.Errorf("public COS URL returned HTTP %d", res.StatusCode) } actual, err := io.ReadAll(io.LimitReader(res.Body, int64(len(expected))+1)) if err != nil || !bytes.Equal(actual, expected) || !strings.HasPrefix(res.Header.Get("Content-Type"), contentType) { return res.StatusCode, errors.New("public COS object content mismatch") } return res.StatusCode, nil } func uploadObject(ctx context.Context, client *cos.Client, public *http.Client, key, objectURL, contentType string, data []byte) (bool, error) { uploaded := false res, err := client.Object.Get(ctx, key, nil) if err == nil { actual, readErr := io.ReadAll(io.LimitReader(res.Body, int64(len(data))+1)) res.Body.Close() if readErr != nil || !bytes.Equal(actual, data) { return false, errors.New("existing COS object differs; no overwrite performed") } } else { var apiErr *cos.ErrorResponse if !errors.As(err, &apiErr) || apiErr.Code != "NoSuchKey" { return false, cosFailure("lookup", err) } headers := http.Header{} headers.Set("x-cos-forbid-overwrite", "true") _, err = client.Object.Put(ctx, key, bytes.NewReader(data), &cos.ObjectPutOptions{ObjectPutHeaderOptions: &cos.ObjectPutHeaderOptions{ ContentType: contentType, ContentLength: int64(len(data)), CacheControl: "public, max-age=31536000, immutable", XOptionHeader: &headers, }}) if err != nil { return false, cosFailure("upload", err) } uploaded = true } status, err := verifyPublic(ctx, public, objectURL, contentType, data) if err != nil && status == http.StatusForbidden && uploaded { _, aclErr := client.Object.PutACL(ctx, key, &cos.ObjectPutACLOptions{Header: &cos.ACLHeaderOptions{XCosACL: "public-read"}}) if aclErr != nil { return uploaded, cosFailure("object public-read ACL", aclErr) } _, err = verifyPublic(ctx, public, objectURL, contentType, data) } return uploaded, err } func writeJSON(file string, value any) error { data, err := json.MarshalIndent(value, "", " ") if err != nil { return err } f, err := os.OpenFile(file, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) if err != nil { return err } defer f.Close() if _, err = f.Write(append(data, '\n')); err != nil { return err } return f.Sync() } func nickname(item manifestRecord) string { surnames := []string{"陈", "林", "周", "许", "苏", "沈", "陆", "顾", "方", "季", "唐", "宋", "夏", "叶", "温", "乔", "江", "程", "简", "安"} female := []string{"可欣", "清禾", "知夏", "若宁", "语桐", "星遥", "念安", "晚晴", "雨眠", "予柔"} male := []string{"知远", "景行", "沐川", "星河", "予安", "亦辰", "云舟", "修远", "言澈", "嘉树"} given := male if item.Gender == "female" { given = female } return "测试·" + surnames[(item.ID-1)%len(surnames)] + given[((item.ID-1)/len(surnames))%len(given)] } func createUsers(ctx context.Context, db *sql.DB, records []manifestRecord, plans []assetPlan, confirmDatabase string) (created, skipped int, ids []int64, err error) { conn, err := db.Conn(ctx) if err != nil { return 0, 0, nil, err } defer conn.Close() var database string if err = conn.QueryRowContext(ctx, `SELECT DATABASE()`).Scan(&database); err != nil || confirmDatabase == "" || database != confirmDatabase { return 0, 0, nil, errors.New("database confirmation does not match connected database") } var locked int if err = conn.QueryRowContext(ctx, `SELECT GET_LOCK(?,10)`, "xingyu:testusers:"+batchName).Scan(&locked); err != nil || locked != 1 { return 0, 0, nil, errors.New("could not acquire generated test-user batch lock") } defer func() { _, _ = conn.ExecContext(context.Background(), `DO RELEASE_LOCK(?)`, "xingyu:testusers:"+batchName) }() tx, err := conn.BeginTx(ctx, nil) if err != nil { return 0, 0, nil, err } defer tx.Rollback() var batchCount int if err = tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM users WHERE test_batch=?`, batchName).Scan(&batchCount); err != nil { return 0, 0, nil, errors.New("test-user schema unavailable; migration 028 is required") } if batchCount != 0 && batchCount != recordCount { return 0, 0, nil, fmt.Errorf("batch has %d users; refusing to modify partial data", batchCount) } jobs := []string{"设计师", "工程师", "教师", "摄影师", "产品经理", "运营", "编辑", "自由职业"} hobbies := []string{"城市漫步", "阅读与咖啡", "跑步与音乐", "旅行与美食", "电影与绘画", "徒步与摄影"} now := time.Now() ids = make([]int64, 0, recordCount) for index, item := range records { publicID := fmt.Sprintf("TESTIM%06d", item.ID) gender := 1 if item.Gender == "female" { gender = 2 } var id int64 var existingTest bool var existingBatch string var deleted sql.NullTime queryErr := tx.QueryRowContext(ctx, `SELECT id,is_test,test_batch,deleted_at FROM users WHERE public_id=? FOR UPDATE`, publicID).Scan(&id, &existingTest, &existingBatch, &deleted) if queryErr == nil { if batchCount != recordCount || !existingTest || existingBatch != batchName || deleted.Valid { return 0, 0, nil, fmt.Errorf("public ID %s conflicts with an existing user", publicID) } var actualGender int var actualAvatar string if err = tx.QueryRowContext(ctx, `SELECT gender,avatar_url FROM user_profiles WHERE user_id=?`, id).Scan(&actualGender, &actualAvatar); err != nil || actualGender != gender || actualAvatar != plans[index].DisplayURL { return 0, 0, nil, fmt.Errorf("existing generated profile %s was modified or is incomplete", publicID) } skipped++ } else if queryErr == sql.ErrNoRows && batchCount == 0 { result, insertErr := tx.ExecContext(ctx, `INSERT INTO users(public_id,password_hash,is_test,test_batch,status) VALUES(?,'!GENERATED_TEST_ACCOUNT_NO_LOGIN',1,?,1)`, publicID, batchName) if insertErr != nil { return 0, 0, nil, insertErr } id, err = result.LastInsertId() if err != nil { return 0, 0, nil, err } place := cities[(item.ID-1)%len(cities)] birthday := now.AddDate(-item.Age, 0, -(item.ID%180 + 1)).Format("2006-01-02") height := 168 + item.ID%12 if gender == 2 { height = 158 + item.ID%11 } bio := "【测试数据】虚构资料与AI生成头像,仅用于产品功能测试,非真实用户。兴趣示例:" + hobbies[(item.ID-1)%len(hobbies)] + "。" _, err = tx.ExecContext(ctx, `INSERT INTO user_profiles(user_id,nickname,avatar_url,cover_url,gender,birthday,height_cm,city_code,city_name,occupation,bio,profile_score,last_active_at) VALUES(?,?,?,'',?,?,?,?,?,?,?,80,NOW(3))`, id, nickname(item), plans[index].DisplayURL, gender, birthday, height, place.Code, place.Name, jobs[(item.ID-1)%len(jobs)], bio) if err != nil { return 0, 0, nil, err } _, err = tx.ExecContext(ctx, `INSERT INTO user_privacy_settings(user_id,nearby_visible,distance_visible,online_visible,last_active_visible,allow_stranger_message,allow_profile_visit_record,allow_search) VALUES(?,1,1,1,0,1,0,1)`, id) if err != nil { return 0, 0, nil, err } lat := place.Lat + float64((item.ID%7)-3)*0.003 lng := place.Lng + float64((item.ID%9)-4)*0.003 _, err = tx.ExecContext(ctx, `INSERT INTO user_location_states(user_id,city_code,location_cell,latitude,longitude,source) VALUES(?,?,'fixture',?,?,'fixture')`, id, place.Code, lat, lng) if err != nil { return 0, 0, nil, err } created++ } else { if queryErr == sql.ErrNoRows { queryErr = errors.New("batch identity mismatch") } return 0, 0, nil, queryErr } ids = append(ids, id) } if err = tx.Commit(); err != nil { return 0, 0, nil, errors.New("user transaction commit result uncertain; verify database before retry") } return created, skipped, ids, nil } func run() error { apply := flag.Bool("apply", false, "upload objects and create the test-user batch") confirmDB := flag.String("confirm-database", "", "exact production database name") confirmBucket := flag.String("confirm-bucket", "", "exact configured COS bucket") manifestFile := flag.String("manifest", "", "absolute path to manifest.json") avatarsDir := flag.String("avatars-dir", "", "absolute directory containing generated PNG files") backupDir := flag.String("backup-dir", "", "new scoped backup directory under /www/backup; required with --apply") flag.Parse() if os.Getenv("IM_ENV") != "production" { return errors.New("this scoped importer requires IM_ENV=production") } if !filepath.IsAbs(*manifestFile) || !filepath.IsAbs(*avatarsDir) { return errors.New("manifest and avatars directory must be absolute paths") } dsn := os.Getenv("IM_DB_DSN") dsnConfig, err := mysql.ParseDSN(dsn) if err != nil || dsn == "" || *confirmDB != "xim" || dsnConfig.DBName != *confirmDB || dsnConfig.Net != "tcp" || dsnConfig.Addr != "127.0.0.1:3306" { return errors.New("database target confirmation failed") } dsnConfig.ParseTime = true db, err := sql.Open("mysql", dsnConfig.FormatDSN()) if err != nil { return errors.New("cannot open database") } defer db.Close() ctx, cancel := context.WithTimeout(context.Background(), 45*time.Minute) defer cancel() if err = db.PingContext(ctx); err != nil { return errors.New("database connection failed") } settings := map[string]string{} rows, err := db.QueryContext(ctx, `SELECT config_key,config_value,value_type FROM system_configs WHERE config_key LIKE 'storage.tencent_cos.%' OR config_key IN ('storage.provider','storage.object_prefix')`) if err != nil { return errors.New("cannot read storage configuration") } for rows.Next() { var key, value, kind string if err = rows.Scan(&key, &value, &kind); err != nil { rows.Close() return err } if kind == "secret" { value, err = decrypt(value, os.Getenv("IM_CONFIG_ENCRYPTION_KEY"), os.Getenv("IM_JWT_SECRET")) if err != nil { rows.Close() return err } } settings[key] = strings.TrimSpace(value) } err = rows.Err() rows.Close() if err != nil { return err } bucket := settings["storage.tencent_cos.bucket"] if settings["storage.provider"] != "tencent_cos" || bucket == "" || bucket != *confirmBucket { return errors.New("configured storage provider/bucket does not match confirmation") } endpoint, err := validHTTPS(settings["storage.tencent_cos.endpoint"]) if err != nil { return err } if endpoint.Port() != "" || !regexp.MustCompile(`^`+regexp.QuoteMeta(bucket)+`\.cos\.[a-z0-9-]+\.myqcloud\.com$`).MatchString(endpoint.Hostname()) || strings.Trim(endpoint.Path, "/") != "" { return errors.New("authenticated endpoint is not the configured Tencent COS bucket hostname") } if settings["storage.tencent_cos.secret_id"] == "" || settings["storage.tencent_cos.secret_key"] == "" { return errors.New("COS credentials are not configured") } records, err := loadManifest(*manifestFile) if err != nil { return err } plans, err := planAssets(records, *avatarsDir, settings["storage.object_prefix"], settings["storage.tencent_cos.public_base_url"]) if err != nil { return err } female := 0 for _, item := range records { if item.Gender == "female" { female++ } } if !*apply { return json.NewEncoder(os.Stdout).Encode(map[string]any{"dryRun": true, "batch": batchName, "bucket": bucket, "users": len(records), "female": female, "male": len(records) - female, "objects": len(plans) * 3}) } cleanBackup := filepath.Clean(*backupDir) if filepath.Dir(cleanBackup) != "/www/backup" || !strings.HasPrefix(filepath.Base(cleanBackup), "xim-generated-users-") { return errors.New("backup directory must be a new /www/backup/xim-generated-users-* directory") } if err = os.Mkdir(cleanBackup, 0700); err != nil { return err } if err = writeJSON(filepath.Join(cleanBackup, "plan.json"), map[string]any{"batch": batchName, "bucket": bucket, "records": records, "objects": plans}); err != nil { return err } fmt.Println("BACKUP=" + cleanBackup) noRedirect := func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } public := &http.Client{Timeout: 60 * time.Second, CheckRedirect: noRedirect} endpoint.Path = "" cosClient := cos.NewClient(&cos.BaseURL{BucketURL: endpoint}, &http.Client{Timeout: 90 * time.Second, CheckRedirect: noRedirect, Transport: &cos.AuthorizationTransport{SecretID: settings["storage.tencent_cos.secret_id"], SecretKey: settings["storage.tencent_cos.secret_key"]}}) uploads := make([]uploadResult, 0, len(plans)) for _, plan := range plans { original, display, thumbnail, renderErr := renderVariants(filepath.Join(*avatarsDir, plan.File)) if renderErr != nil { return fmt.Errorf("%s: cannot render avatar variants; database unchanged", plan.File) } originalUploaded, uploadErr := uploadObject(ctx, cosClient, public, plan.OriginalKey, plan.OriginalURL, "image/png", original) if uploadErr != nil { return fmt.Errorf("%s original: %w; database unchanged", plan.File, uploadErr) } displayUploaded, uploadErr := uploadObject(ctx, cosClient, public, plan.DisplayKey, plan.DisplayURL, "image/jpeg", display) if uploadErr != nil { return fmt.Errorf("%s display: %w; database unchanged", plan.File, uploadErr) } thumbUploaded, uploadErr := uploadObject(ctx, cosClient, public, plan.ThumbnailKey, plan.ThumbnailURL, "image/jpeg", thumbnail) if uploadErr != nil { return fmt.Errorf("%s thumbnail: %w; database unchanged", plan.File, uploadErr) } uploads = append(uploads, uploadResult{originalUploaded, displayUploaded, thumbUploaded}) fmt.Printf("VERIFIED %03d/100 original=%t display=%t thumbnail=%t\n", plan.ID, originalUploaded, displayUploaded, thumbUploaded) } if err = writeJSON(filepath.Join(cleanBackup, "uploaded.json"), map[string]any{"plans": plans, "results": uploads}); err != nil { return err } created, skipped, ids, err := createUsers(ctx, db, records, plans, *confirmDB) if err != nil { return err } result := map[string]any{"batch": batchName, "bucket": bucket, "created": created, "skipped": skipped, "female": female, "male": len(records) - female, "objectsVerified": len(plans) * 3, "ids": ids, "backup": cleanBackup, "loginDisabled": true, "globalConfigUnchanged": true} if err = writeJSON(filepath.Join(cleanBackup, "result.json"), result); err != nil { return errors.New("users committed but result record could not be written; inspect database") } return json.NewEncoder(os.Stdout).Encode(result) } func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } }