2026-06-11 01:02:16 +08:00

366 lines
12 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 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)
}