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