@@ -0,0 +1,113 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user