Files
huangguo_server/web/service/officialWebsiteser/slug.go
T
rootandClaude Opus 5 8679200f41 Initial commit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:57:10 +08:00

114 lines
3.3 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 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 生成同表唯一的 slugexists 判断某 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)
}