package identity import ( "encoding/json" "fmt" "strings" "time" "unicode/utf8" "hyapp/pkg/xerr" userdomain "hyapp/services/user-service/internal/domain/user" ) type prettyRuleConfig struct { // Digits 限制数字规则可使用的数字,例如 ["6","8","9"];为空时使用 0-9。 Digits []string `json:"digits"` // ExcludeDigits 用于排除 4 等运营常见不投放数字,排除后再进入规则组合。 ExcludeDigits []string `json:"exclude_digits"` // Values 是 custom_values 的固定候选,适合导入运营已经挑好的靓号。 Values []string `json:"values"` // Pattern 是 custom_pattern 的模式,例如 AABBC、AAAABBBB;同一字母代表同一数字。 Pattern string `json:"pattern"` // Prefix/Suffix 可把数字规则包装成 VIP888、888HY 等字符串靓号。 Prefix string `json:"prefix"` Suffix string `json:"suffix"` // YearFrom/YearTo 控制日期号范围,未填写时覆盖常见生日和纪念日区间。 YearFrom int `json:"year_from"` YearTo int `json:"year_to"` // StartDate/EndDate 用于精确限制日期号范围,格式固定为 yyyy-mm-dd。 StartDate string `json:"start_date"` EndDate string `json:"end_date"` } func generatePrettyDisplayIDCandidates(ruleType string, configJSON string) ([]string, error) { config, err := parsePrettyRuleConfig(configJSON) if err != nil { return nil, err } digits, err := config.availableDigits() if err != nil { return nil, err } var raw []string switch ruleType { case userdomain.PrettyDisplayIDRuleAA: raw = repeatDigitCandidates(digits, 2) case userdomain.PrettyDisplayIDRuleAAA: raw = repeatDigitCandidates(digits, 3) case userdomain.PrettyDisplayIDRuleAAAA: raw = repeatDigitCandidates(digits, 4) case userdomain.PrettyDisplayIDRuleAAAAA: raw = repeatDigitCandidates(digits, 5) case userdomain.PrettyDisplayIDRuleAAAAAA: raw = repeatDigitCandidates(digits, 6) case userdomain.PrettyDisplayIDRuleAAAAAAA: raw = repeatDigitCandidates(digits, 7) case userdomain.PrettyDisplayIDRuleAABB: raw = patternDigitCandidates("aabb", digits) case userdomain.PrettyDisplayIDRuleAABBCC: // 旧规则按 000000-999999 生成 1000 个候选,保留 A/B/C 可相同的兼容语义。 raw = patternCandidates("aabbcc", digits, false) case userdomain.PrettyDisplayIDRuleAABBCCDD: raw = patternDigitCandidates("aabbccdd", digits) case userdomain.PrettyDisplayIDRuleABAB: raw = patternDigitCandidates("abab", digits) case userdomain.PrettyDisplayIDRuleABABAB: raw = patternDigitCandidates("ababab", digits) case userdomain.PrettyDisplayIDRuleABBA: raw = patternDigitCandidates("abba", digits) case userdomain.PrettyDisplayIDRuleABCBA: raw = patternDigitCandidates("abcba", digits) case userdomain.PrettyDisplayIDRuleABCCBA: raw = patternDigitCandidates("abccba", digits) case userdomain.PrettyDisplayIDRuleAAAB: raw = patternDigitCandidates("aaab", digits) case userdomain.PrettyDisplayIDRuleAAAAB: raw = patternDigitCandidates("aaaab", digits) case userdomain.PrettyDisplayIDRuleAAAAAB: raw = patternDigitCandidates("aaaaab", digits) case userdomain.PrettyDisplayIDRuleAAABBB: raw = patternDigitCandidates("aaabbb", digits) case userdomain.PrettyDisplayIDRuleAAAABBBB: raw = patternDigitCandidates("aaaabbbb", digits) case userdomain.PrettyDisplayIDRuleABC: raw = sequenceDigitCandidates(digits, 3, false) case userdomain.PrettyDisplayIDRuleABCD: raw = sequenceDigitCandidates(digits, 4, false) case userdomain.PrettyDisplayIDRuleABCDE: raw = sequenceDigitCandidates(digits, 5, false) case userdomain.PrettyDisplayIDRuleABCDEF: raw = sequenceDigitCandidates(digits, 6, false) case userdomain.PrettyDisplayIDRuleCBA: raw = sequenceDigitCandidates(digits, 3, true) case userdomain.PrettyDisplayIDRuleDCBA: raw = sequenceDigitCandidates(digits, 4, true) case userdomain.PrettyDisplayIDRuleEDCBA: raw = sequenceDigitCandidates(digits, 5, true) case userdomain.PrettyDisplayIDRuleFEDCBA: raw = sequenceDigitCandidates(digits, 6, true) case userdomain.PrettyDisplayIDRuleABCABC: raw = repeatSequenceCandidates(sequenceDigitCandidates(digits, 3, false), 2) case userdomain.PrettyDisplayIDRuleDateYYYYMMDD: raw, err = dateCandidates(config, "20060102") case userdomain.PrettyDisplayIDRuleDateYYMMDD: raw, err = dateCandidates(config, "060102") case userdomain.PrettyDisplayIDRuleLove: raw = []string{"520", "521", "1314", "920", "921", "1711", "9421", "3344", "5201314", "5211314", "1314520", "1314521"} case userdomain.PrettyDisplayIDRuleCustomValues: raw = config.Values case userdomain.PrettyDisplayIDRuleCustomPattern: raw, err = customPatternCandidates(config.Pattern, digits) default: return nil, xerr.New(xerr.InvalidArgument, "rule_type is invalid") } if err != nil { return nil, err } candidates := finalizePrettyCandidates(raw, config) if len(candidates) == 0 { return nil, xerr.New(xerr.InvalidArgument, "pretty display id rule has no candidates") } return candidates, nil } func parsePrettyRuleConfig(configJSON string) (prettyRuleConfig, error) { var config prettyRuleConfig if strings.TrimSpace(configJSON) == "" { return config, nil } if err := json.Unmarshal([]byte(configJSON), &config); err != nil { return prettyRuleConfig{}, xerr.New(xerr.InvalidArgument, "rule_config_json is invalid") } config.Pattern = strings.TrimSpace(config.Pattern) config.Prefix = strings.TrimSpace(config.Prefix) config.Suffix = strings.TrimSpace(config.Suffix) config.StartDate = strings.TrimSpace(config.StartDate) config.EndDate = strings.TrimSpace(config.EndDate) return config, nil } func (c prettyRuleConfig) availableDigits() ([]string, error) { digits := c.Digits if len(digits) == 0 { digits = []string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"} } excluded := make(map[string]struct{}, len(c.ExcludeDigits)) for _, digit := range c.ExcludeDigits { digit = strings.TrimSpace(digit) if digit != "" { excluded[digit] = struct{}{} } } result := make([]string, 0, len(digits)) seen := make(map[string]struct{}, len(digits)) for _, digit := range digits { digit = strings.TrimSpace(digit) if !validASCIIDigit(digit) { return nil, xerr.New(xerr.InvalidArgument, "rule_config_json digits is invalid") } if _, skip := excluded[digit]; skip { continue } if _, ok := seen[digit]; ok { continue } seen[digit] = struct{}{} result = append(result, digit) } if len(result) == 0 { return nil, xerr.New(xerr.InvalidArgument, "rule_config_json digits is empty") } return result, nil } func validASCIIDigit(value string) bool { return len(value) == 1 && value[0] >= '0' && value[0] <= '9' } func repeatDigitCandidates(digits []string, count int) []string { candidates := make([]string, 0, len(digits)) for _, digit := range digits { candidates = append(candidates, strings.Repeat(digit, count)) } return candidates } func patternDigitCandidates(pattern string, digits []string) []string { return patternCandidates(pattern, digits, true) } func patternCandidates(pattern string, digits []string, distinct bool) []string { variables := make([]rune, 0) seenVariable := make(map[rune]struct{}) for _, r := range pattern { if _, ok := seenVariable[r]; ok { continue } seenVariable[r] = struct{}{} variables = append(variables, r) } assignments := make(map[rune]string, len(variables)) usedDigits := make(map[string]struct{}, len(variables)) candidates := make([]string, 0) var visit func(index int) visit = func(index int) { if index == len(variables) { var builder strings.Builder for _, r := range pattern { builder.WriteString(assignments[r]) } candidates = append(candidates, builder.String()) return } variable := variables[index] for _, digit := range digits { if distinct { if _, used := usedDigits[digit]; used { continue } } assignments[variable] = digit usedDigits[digit] = struct{}{} visit(index + 1) delete(assignments, variable) delete(usedDigits, digit) } } visit(0) return candidates } func sequenceDigitCandidates(digits []string, length int, descending bool) []string { allowed := make(map[string]struct{}, len(digits)) for _, digit := range digits { allowed[digit] = struct{}{} } candidates := make([]string, 0) for start := 0; start <= 9; start++ { end := start + length - 1 if end > 9 { break } var builder strings.Builder valid := true for offset := 0; offset < length; offset++ { value := start + offset if descending { value = end - offset } digit := fmt.Sprintf("%d", value) if _, ok := allowed[digit]; !ok { valid = false break } builder.WriteString(digit) } if valid { candidates = append(candidates, builder.String()) } } return candidates } func repeatSequenceCandidates(values []string, count int) []string { candidates := make([]string, 0, len(values)) for _, value := range values { candidates = append(candidates, strings.Repeat(value, count)) } return candidates } func dateCandidates(config prettyRuleConfig, layout string) ([]string, error) { start, end, err := dateRange(config) if err != nil { return nil, err } candidates := make([]string, 0) for current := start; !current.After(end); current = current.AddDate(0, 0, 1) { candidates = append(candidates, current.Format(layout)) } return candidates, nil } func dateRange(config prettyRuleConfig) (time.Time, time.Time, error) { if config.StartDate != "" || config.EndDate != "" { start, err := parseDateOrDefault(config.StartDate, time.Date(1950, 1, 1, 0, 0, 0, 0, time.UTC)) if err != nil { return time.Time{}, time.Time{}, err } end, err := parseDateOrDefault(config.EndDate, time.Date(2035, 12, 31, 0, 0, 0, 0, time.UTC)) if err != nil { return time.Time{}, time.Time{}, err } if end.Before(start) { return time.Time{}, time.Time{}, xerr.New(xerr.InvalidArgument, "rule_config_json date range is invalid") } return start, end, nil } yearFrom := config.YearFrom yearTo := config.YearTo if yearFrom == 0 { yearFrom = 1950 } if yearTo == 0 { yearTo = 2035 } if yearFrom < 1900 || yearTo > 2099 || yearTo < yearFrom { return time.Time{}, time.Time{}, xerr.New(xerr.InvalidArgument, "rule_config_json year range is invalid") } return time.Date(yearFrom, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(yearTo, 12, 31, 0, 0, 0, 0, time.UTC), nil } func parseDateOrDefault(value string, fallback time.Time) (time.Time, error) { if value == "" { return fallback, nil } parsed, err := time.ParseInLocation("2006-01-02", value, time.UTC) if err != nil { return time.Time{}, xerr.New(xerr.InvalidArgument, "rule_config_json date is invalid") } return parsed, nil } func customPatternCandidates(pattern string, digits []string) ([]string, error) { if pattern == "" || utf8.RuneCountInString(pattern) > 16 { return nil, xerr.New(xerr.InvalidArgument, "rule_config_json pattern is invalid") } normalized := strings.ToLower(pattern) for _, r := range normalized { if r >= 'a' && r <= 'z' { continue } return nil, xerr.New(xerr.InvalidArgument, "rule_config_json pattern is invalid") } return patternCandidates(normalized, digits, true), nil } func finalizePrettyCandidates(raw []string, config prettyRuleConfig) []string { candidates := make([]string, 0, len(raw)) seen := make(map[string]struct{}, len(raw)) for _, value := range raw { value = strings.TrimSpace(value) if value == "" { continue } value = config.Prefix + value + config.Suffix if !userdomain.ValidAdminPrettyDisplayUserID(value) { continue } if _, ok := seen[value]; ok { continue } seen[value] = struct{}{} candidates = append(candidates, value) } return candidates } func generateAABBCCCandidates() []string { // AABBCC 历史规则一共 10*10*10=1000 个候选,包含 00、01 等前导零组合,靓号内容允许字符串形式保留前导零。 return patternCandidates("aabbcc", []string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}, false) }