package officialwebsiteser import ( "regexp" "strings" "91porn-server/common/stderr" "github.com/mozillazg/go-pinyin" ) var ( seoSlugValidRe = regexp.MustCompile(`^[a-z]+$`) // 纯小写英文字母 seoSlugStripRe = regexp.MustCompile(`[^a-z]`) // 非 a-z 一律剔除 ) // seoSlugMaxLen 自动生成 slug 的最大长度(按拼音音节边界截断,避免长标题生成超长 slug) const seoSlugMaxLen = 20 // GenerateSeoSlug 从标题生成纯小写字母 slug:中文转拼音、英文保留、其余字符剔除, // 并按音节边界累加到 seoSlugMaxLen 为止(不从音节中间截断)。 // 例: "优雅Summer 2023" -> "youyasummer" func GenerateSeoSlug(title string) string { if title == "" { return "" } args := pinyin.NewArgs() // 非汉字(英文/数字/符号)原样返回,后续再统一剔除非 a-z args.Fallback = func(r rune, a pinyin.Args) []string { return []string{string(r)} } rows := pinyin.Pinyin(title, args) var b strings.Builder for _, row := range rows { if len(row) == 0 { continue } // 单个音节先剔除非 a-z、转小写 syl := seoSlugStripRe.ReplaceAllString(strings.ToLower(row[0]), "") if syl == "" { continue } // 到长度上限则在音节边界停止,避免 slug 过长 if b.Len()+len(syl) > seoSlugMaxLen { break } b.WriteString(syl) } return b.String() } // ValidateSeoSlug 校验 slug 格式:非空且纯小写英文字母。 func ValidateSeoSlug(s string) bool { return seoSlugValidRe.MatchString(s) } // ResolveSeoSlug 统一处理 seoSlug: // - 手填(provided 非空):校验纯小写字母格式,并查重(冲突报错) // - 留空:由 title 生成拼音雏形,冲突时追加后缀去重 // // exists 由各表提供(查各自表是否已占用该 slug)。 func ResolveSeoSlug(provided, title string, exists func(slug string) (bool, error)) (string, error) { if provided != "" { if !ValidateSeoSlug(provided) { return "", stderr.ErrParamError } used, err := exists(provided) if err != nil { return "", err } if used { return "", stderr.ErrParamError } return provided, nil } return UniqueSeoSlug(GenerateSeoSlug(title), exists) } // UniqueSeoSlug 基于 base 生成同表唯一的 slug;exists 判断某 slug 是否已被占用。 // 冲突时依次追加纯小写字母后缀 a、b…z、aa、ab…zz、aaa… 直到命中未占用值; // base 为空则返回空(走稀疏索引,不参与唯一约束)。 func UniqueSeoSlug(base string, exists func(slug string) (bool, error)) (string, error) { if base == "" { return "", nil } // n=0 先试 base 原值,之后依次追加 a、b…z、aa… 后缀(双射 26 进制递增)。 // 表内记录有限且各候选互不相同,必能在有限次内命中未占用值。 for n := 0; ; n++ { candidate := base if n > 0 { candidate += seoSlugSuffix(n) } used, err := exists(candidate) if err != nil { return "", err } if !used { return candidate, nil } } } // seoSlugSuffix 返回第 n 个纯小写字母后缀(n>=1),按双射 26 进制递增: // 1->a 2->b … 26->z 27->aa 28->ab … 52->az 53->ba … 702->zz 703->aaa func seoSlugSuffix(n int) string { var buf []byte for n > 0 { n-- buf = append([]byte{byte('a' + n%26)}, buf...) n /= 26 } return string(buf) }