tokenFactory/model/route_slug.go

159 lines
4.9 KiB
Go
Raw Permalink 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 model
import (
"fmt"
"regexp"
"strings"
"github.com/QuantumNous/new-api/common"
"gorm.io/gorm"
)
// channelNoRoutePattern 与旧版三段式里 channel_noc1、c2…同形route_slug 禁止使用该形态以免解析歧义。
var channelNoRoutePattern = regexp.MustCompile(`^c\d+$`)
// DefaultRouteSlugFromChannelID 返回渠道默认全局路由后缀(与 channels.id 一一对应)。
// 前缀 "u" 避免与旧 channel_no 段 c\d+ 混淆。
func DefaultRouteSlugFromChannelID(id int64) string {
return "u" + EncodeBase62(id)
}
// IsValidRouteSlug 判断字符串是否可作为全局 route_slug232 位 base62且不能为 c+数字(旧 channel_no 形态)。
func IsValidRouteSlug(s string) bool {
s = strings.TrimSpace(s)
if len(s) < 2 || len(s) > 32 {
return false
}
for _, c := range s {
if !((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) {
return false
}
}
if channelNoRoutePattern.MatchString(s) {
return false
}
return true
}
// ResolveChannelIDByRouteSlugAndModel 按 route_slug 查找已启用渠道,并校验 models 列表包含 modelName。
// 未命中、已禁用或模型不在列表中时返回 0供分发器静默降级为普通路由
func ResolveChannelIDByRouteSlugAndModel(slug, modelName string) int {
slug = strings.TrimSpace(slug)
if slug == "" || !IsValidRouteSlug(slug) {
return 0
}
var ch Channel
err := DB.Select("id", "models", "status").Where("route_slug = ?", slug).First(&ch).Error
if err != nil {
return 0
}
if ch.Status != common.ChannelStatusEnabled {
return 0
}
if !ChannelModelsRawContains(ch.Models, modelName) {
return 0
}
return ch.Id
}
// GetRouteSlugsByChannelIDs 批量返回 channel_id → route_slug定价等场景
func GetRouteSlugsByChannelIDs(channelIDs []int) map[int]string {
if len(channelIDs) == 0 {
return nil
}
var rows []Channel
if err := DB.Select("id", "route_slug").Where("id IN ?", channelIDs).Find(&rows).Error; err != nil {
return nil
}
out := make(map[int]string, len(rows))
for i := range rows {
s := strings.TrimSpace(rows[i].RouteSlug)
if s != "" {
out[rows[i].Id] = s
}
}
if len(out) == 0 {
return nil
}
return out
}
// assignRouteSlugInTx 在事务内为新建渠道写入 route_slug空则按 id 生成;非空则校验格式与唯一性)。
func assignRouteSlugInTx(tx *gorm.DB, channelID int, requested string) (assigned string, err error) {
if channelID <= 0 {
return "", nil
}
req := strings.TrimSpace(requested)
slug := req
if slug == "" {
slug = DefaultRouteSlugFromChannelID(int64(channelID))
} else if !IsValidRouteSlug(slug) {
return "", fmt.Errorf("route_slug 无效")
}
var cnt int64
if err := tx.Model(&Channel{}).Where("route_slug = ? AND id <> ?", slug, channelID).Count(&cnt).Error; err != nil {
return "", err
}
if cnt > 0 {
return "", fmt.Errorf("route_slug 已被占用")
}
if err := tx.Model(&Channel{}).Where("id = ?", channelID).Update("route_slug", slug).Error; err != nil {
return "", err
}
return slug, nil
}
// BackfillChannelRouteSlugs 为缺少 route_slug 的渠道写入默认值(幂等)。
func BackfillChannelRouteSlugs() error {
if DB == nil || DB.Migrator() == nil {
return nil
}
if !DB.Migrator().HasColumn(&Channel{}, "route_slug") {
return nil
}
var ids []int
if err := DB.Model(&Channel{}).Where("route_slug IS NULL OR route_slug = ?", "").Pluck("id", &ids).Error; err != nil {
return err
}
for _, id := range ids {
slug := DefaultRouteSlugFromChannelID(int64(id))
if err := DB.Model(&Channel{}).Where("id = ?", id).Update("route_slug", slug).Error; err != nil {
return fmt.Errorf("backfill route_slug channel_id=%d: %w", id, err)
}
}
return nil
}
// ensureRouteSlugLookupIndex 创建 route_slug 普通索引(非唯一:批量插入时须先落库再逐行赋值 slug避免空串唯一冲突
func ensureRouteSlugLookupIndex() error {
sql := "CREATE INDEX IF NOT EXISTS idx_channels_route_slug ON channels (route_slug)"
if common.UsingMySQL {
sql = "CREATE INDEX idx_channels_route_slug ON channels (route_slug)"
}
err := DB.Exec(sql).Error
if err == nil {
return nil
}
msg := strings.ToLower(err.Error())
if strings.Contains(msg, "duplicate") || strings.Contains(msg, "already exists") || strings.Contains(msg, "exist") {
return nil
}
return fmt.Errorf("ensure route_slug lookup index: %w", err)
}
// MigrateChannelRouteSlugAndDropLegacy 删除未上线的旧 route_index 表、补全 route_slug、建查询索引。
func MigrateChannelRouteSlugAndDropLegacy() error {
if DB == nil || DB.Migrator() == nil {
return nil
}
if DB.Migrator().HasTable("channel_model_route_indices") {
if err := DB.Migrator().DropTable("channel_model_route_indices"); err != nil {
return fmt.Errorf("drop channel_model_route_indices: %w", err)
}
}
if err := BackfillChannelRouteSlugs(); err != nil {
return err
}
return ensureRouteSlugLookupIndex()
}