114 lines
3.3 KiB
Go
114 lines
3.3 KiB
Go
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)
|
||
}
|