654 lines
20 KiB
Go
654 lines
20 KiB
Go
package controller
|
||
|
||
import (
|
||
"fmt"
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/QuantumNous/new-api/common"
|
||
"github.com/QuantumNous/new-api/constant"
|
||
"github.com/QuantumNous/new-api/model"
|
||
"github.com/QuantumNous/new-api/setting/system_setting"
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// ─── 导出字段键常量 ────────────────────────────────────────────────────────────
|
||
|
||
const (
|
||
chFieldName = "name"
|
||
chFieldDiscountRate = "discountRate"
|
||
chFieldMarkupDiscount = "markupDiscountRate"
|
||
chFieldRouteSlug = "routeSlug"
|
||
chFieldQuota = "quota"
|
||
chFieldDisabled = "disabled"
|
||
chFieldSupplierName = "supplierName"
|
||
chFieldType = "type"
|
||
chFieldLogo = "logo"
|
||
chFieldProviderType = "providerType"
|
||
chFieldApiKey = "apiKey"
|
||
chFieldApiBaseUrl = "apiBaseUrl"
|
||
chFieldModels = "models"
|
||
chFieldGroups = "groups"
|
||
chFieldModelRedirect = "modelRedirect"
|
||
chFieldOtherInfo = "otherInfo"
|
||
)
|
||
|
||
// chAllowedExportFields 允许导出的合法字段集合,防止非法字段注入。
|
||
var chAllowedExportFields = map[string]bool{
|
||
chFieldName: true, chFieldDiscountRate: true, chFieldRouteSlug: true,
|
||
chFieldQuota: true, chFieldDisabled: true,
|
||
chFieldSupplierName: true, chFieldType: true, chFieldLogo: true,
|
||
chFieldProviderType: true, chFieldApiKey: true, chFieldApiBaseUrl: true,
|
||
chFieldModels: true, chFieldGroups: true, chFieldModelRedirect: true,
|
||
chFieldOtherInfo: true,
|
||
}
|
||
|
||
// ─── DTO 定义 ──────────────────────────────────────────────────────────────────
|
||
|
||
// ChannelExportRequest 渠道导出请求体。
|
||
type ChannelExportRequest struct {
|
||
ChannelIDs []int `json:"channel_ids"` // 需要导出的渠道 ID 列表
|
||
Fields []string `json:"fields"` // 用户选择的字段列表
|
||
Mode string `json:"mode"` // 导出模式: "standard"(默认) | "site_builder"(建站用户导出)
|
||
}
|
||
|
||
// ChannelExportPayload 导出响应的数据结构(可直接用于后续导入)。
|
||
type ChannelExportPayload struct {
|
||
Version string `json:"version"`
|
||
ExportTime string `json:"exportTime"`
|
||
Channels []map[string]interface{} `json:"channels"`
|
||
}
|
||
|
||
// ChannelImportRequest 导入请求结构(与导出结构兼容)。
|
||
type ChannelImportRequest struct {
|
||
Version string `json:"version"`
|
||
ExportTime string `json:"exportTime"`
|
||
Channels []map[string]interface{} `json:"channels"`
|
||
SiteBuilderApiKey string `json:"site_builder_api_key,omitempty"` // 建站模式统一密钥,导入 type=60 渠道时若 apiKey 为空则原样写入渠道 Key
|
||
}
|
||
|
||
// ChannelImportResult 导入操作的结果统计。
|
||
type ChannelImportResult struct {
|
||
Added int `json:"added"`
|
||
Updated int `json:"updated"`
|
||
Failed int `json:"failed"`
|
||
Failures []ChannelImportFailure `json:"failures"`
|
||
}
|
||
|
||
// ChannelImportFailure 单条导入失败的详情。
|
||
type ChannelImportFailure struct {
|
||
Name string `json:"name"`
|
||
Reason string `json:"reason"`
|
||
}
|
||
|
||
// ─── 导出接口 ─────────────────────────────────────────────────────────────────
|
||
|
||
// ExportChannels 按渠道 ID 列表导出指定字段。
|
||
// POST /api/channel/export
|
||
// mode=standard (默认): 原样导出渠道数据
|
||
// mode=site_builder: 建站用户导出,type 强制为 60,apiKey 置空(由导入方指定建站密钥),
|
||
// apiBaseUrl 为本平台 ServerAddress,otherInfo 中标记来源与路由信息。
|
||
func ExportChannels(c *gin.Context) {
|
||
var req ChannelExportRequest
|
||
if err := common.DecodeJson(c.Request.Body, &req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "请求格式错误"})
|
||
return
|
||
}
|
||
if len(req.ChannelIDs) == 0 {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "请先选择需要导出的渠道"})
|
||
return
|
||
}
|
||
|
||
// 过滤非法字段,只保留允许导出的合法字段
|
||
fieldSet := make(map[string]bool)
|
||
for _, f := range req.Fields {
|
||
if chAllowedExportFields[f] {
|
||
fieldSet[f] = true
|
||
}
|
||
}
|
||
if len(fieldSet) == 0 {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "请至少选择一个导出字段"})
|
||
return
|
||
}
|
||
// 始终包含 name,以便导入时做名称匹配
|
||
fieldSet[chFieldName] = true
|
||
|
||
channels, err := model.GetChannelsByIDs(req.ChannelIDs)
|
||
if err != nil {
|
||
common.ApiError(c, err)
|
||
return
|
||
}
|
||
|
||
isSiteBuilder := req.Mode == "site_builder"
|
||
|
||
items := make([]map[string]interface{}, 0, len(channels))
|
||
for _, ch := range channels {
|
||
if isSiteBuilder {
|
||
items = append(items, buildSiteBuilderExportItem(c, ch, fieldSet))
|
||
} else {
|
||
items = append(items, buildChannelExportItem(ch, fieldSet))
|
||
}
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"data": ChannelExportPayload{
|
||
Version: "1.0",
|
||
ExportTime: time.Now().UTC().Format(time.RFC3339),
|
||
Channels: items,
|
||
},
|
||
})
|
||
}
|
||
|
||
// buildChannelExportItem 根据字段集合构建单个渠道的导出 map(未选字段不出现在结果中)。
|
||
func buildChannelExportItem(ch *model.Channel, fields map[string]bool) map[string]interface{} {
|
||
item := make(map[string]interface{})
|
||
|
||
if fields[chFieldName] {
|
||
item[chFieldName] = ch.Name
|
||
}
|
||
if fields[chFieldDiscountRate] {
|
||
item[chFieldDiscountRate] = ch.PriceDiscountPercent
|
||
}
|
||
if fields[chFieldMarkupDiscount] {
|
||
item[chFieldMarkupDiscount] = ch.MarkupDiscountRate
|
||
}
|
||
if fields[chFieldRouteSlug] {
|
||
item[chFieldRouteSlug] = ch.RouteSlug
|
||
}
|
||
if fields[chFieldQuota] {
|
||
item[chFieldQuota] = ch.Balance
|
||
}
|
||
if fields[chFieldDisabled] {
|
||
// Status=2 表示禁用,其他值表示启用
|
||
item[chFieldDisabled] = ch.Status == 2
|
||
}
|
||
if fields[chFieldSupplierName] {
|
||
item[chFieldSupplierName] = ch.SupplierName
|
||
}
|
||
if fields[chFieldType] {
|
||
item[chFieldType] = ch.Type
|
||
}
|
||
if fields[chFieldLogo] {
|
||
item[chFieldLogo] = ch.CompanyLogoURL
|
||
}
|
||
if fields[chFieldProviderType] {
|
||
item[chFieldProviderType] = ch.SupplierType
|
||
}
|
||
if fields[chFieldApiKey] {
|
||
item[chFieldApiKey] = ch.Key
|
||
}
|
||
if fields[chFieldApiBaseUrl] {
|
||
baseURL := ""
|
||
if ch.BaseURL != nil {
|
||
baseURL = *ch.BaseURL
|
||
}
|
||
item[chFieldApiBaseUrl] = baseURL
|
||
}
|
||
if fields[chFieldModels] {
|
||
// Models 字段存储为逗号分隔字符串,导出时转换为数组
|
||
item[chFieldModels] = ch.GetModels()
|
||
}
|
||
if fields[chFieldGroups] {
|
||
// Group 字段存储为逗号分隔字符串,导出时转换为数组
|
||
item[chFieldGroups] = ch.GetGroups()
|
||
}
|
||
if fields[chFieldModelRedirect] {
|
||
redirect := map[string]string{}
|
||
if ch.ModelMapping != nil && *ch.ModelMapping != "" {
|
||
_ = common.UnmarshalJsonStr(*ch.ModelMapping, &redirect)
|
||
}
|
||
item[chFieldModelRedirect] = redirect
|
||
}
|
||
if fields[chFieldOtherInfo] {
|
||
otherInfo := ch.GetOtherInfo()
|
||
if len(otherInfo) > 0 {
|
||
item[chFieldOtherInfo] = otherInfo
|
||
}
|
||
}
|
||
|
||
return item
|
||
}
|
||
|
||
// buildSiteBuilderExportItem 构建建站用户导出项。
|
||
// 核心差异:type 固定为 60 (TokenFactoryOpen),apiKey 置空(由导入方在导入时指定建站密钥),
|
||
// apiBaseUrl 为本平台 ServerAddress,otherInfo 中标记来源和路由信息。
|
||
func buildSiteBuilderExportItem(c *gin.Context, ch *model.Channel, fields map[string]bool) map[string]interface{} {
|
||
item := buildChannelExportItem(ch, fields)
|
||
|
||
// 强制覆盖 type = 60 (TokenFactoryOpen)
|
||
if fields[chFieldType] {
|
||
item[chFieldType] = constant.ChannelTypeTokenFactoryOpen
|
||
}
|
||
|
||
// 强制覆盖 apiBaseUrl 为本平台 ServerAddress
|
||
serverAddr := strings.TrimRight(system_setting.ServerAddress, "/")
|
||
if serverAddr == "" {
|
||
// fallback: 从请求中推导
|
||
scheme := "http"
|
||
if c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https" {
|
||
scheme = "https"
|
||
}
|
||
serverAddr = fmt.Sprintf("%s://%s", scheme, c.Request.Host)
|
||
}
|
||
item[chFieldApiBaseUrl] = serverAddr
|
||
// 确保字段集合中包含 apiBaseUrl,即使原先未勾选
|
||
fields[chFieldApiBaseUrl] = true
|
||
|
||
// 建站模式下 apiKey 置空,由导入方通过 site_builder_api_key 参数统一指定密钥。
|
||
// 导入时:若渠道 type=60 且 apiKey 为空,则使用导入请求中的 site_builder_api_key。
|
||
item[chFieldApiKey] = ""
|
||
// 确保字段集合中包含 apiKey
|
||
fields[chFieldApiKey] = true
|
||
|
||
// 对于建站导出,在 otherInfo 中标记来源为 tokenfactory_open,
|
||
// 并保留上游路由信息(route_slug 等),以便导入方可正确路由请求到上游渠道。
|
||
otherInfo := ch.GetOtherInfo()
|
||
if otherInfo == nil {
|
||
otherInfo = make(map[string]interface{})
|
||
}
|
||
otherInfo["source"] = "tokenfactory_open"
|
||
// 保留原渠道的 route_slug 作为 upstream_route_slug
|
||
if ch.RouteSlug != "" {
|
||
otherInfo["upstream_route_slug"] = ch.RouteSlug
|
||
}
|
||
// 保留原渠道的 supplier 信息
|
||
if ch.SupplierName != "" {
|
||
otherInfo["upstream_supplier_alias"] = ch.SupplierName
|
||
}
|
||
item[chFieldOtherInfo] = otherInfo
|
||
fields[chFieldOtherInfo] = true
|
||
|
||
return item
|
||
}
|
||
|
||
// ─── 导入接口 ──────────────────────────────────────────────────────────────
|
||
|
||
// ImportChannels 按名称匹配导入渠道配置。
|
||
// 核心规则:仅通过 name 匹配;同名则更新(仅更新 JSON 中存在的字段);不存在则新增;
|
||
// 绝对禁止清空/覆盖未传字段;绝对禁止删除已有渠道。
|
||
// POST /api/channel/import
|
||
func ImportChannels(c *gin.Context) {
|
||
var req ChannelImportRequest
|
||
if err := common.DecodeJson(c.Request.Body, &req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "JSON 格式错误,请上传合法的导出文件"})
|
||
return
|
||
}
|
||
if req.Channels == nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "channels 字段不能为空"})
|
||
return
|
||
}
|
||
|
||
result := &ChannelImportResult{Failures: []ChannelImportFailure{}}
|
||
|
||
for _, item := range req.Channels {
|
||
// 校验 name 字段
|
||
name, ok := chGetStr(item, "name")
|
||
if !ok || strings.TrimSpace(name) == "" {
|
||
result.Failed++
|
||
result.Failures = append(result.Failures, ChannelImportFailure{Name: "(未知)", Reason: "缺少或无效的 name 字段"})
|
||
continue
|
||
}
|
||
name = strings.TrimSpace(name)
|
||
|
||
// 校验字段类型合法性(models/groups 必须为数组,modelRedirect 必须为对象)
|
||
if err := chValidateItem(item); err != nil {
|
||
result.Failed++
|
||
result.Failures = append(result.Failures, ChannelImportFailure{Name: name, Reason: err.Error()})
|
||
continue
|
||
}
|
||
|
||
// 按名称查询是否存在同名渠道
|
||
existing, err := model.GetChannelByName(name)
|
||
if err != nil {
|
||
result.Failed++
|
||
result.Failures = append(result.Failures, ChannelImportFailure{Name: name, Reason: "查询渠道失败: " + err.Error()})
|
||
continue
|
||
}
|
||
|
||
if existing != nil {
|
||
// 同名渠道已存在:仅更新 JSON 中存在的字段,不清空其他字段
|
||
if err := chApplyToExisting(existing, item, req.SiteBuilderApiKey); err != nil {
|
||
result.Failed++
|
||
result.Failures = append(result.Failures, ChannelImportFailure{Name: name, Reason: "更新失败: " + err.Error()})
|
||
continue
|
||
}
|
||
result.Updated++
|
||
} else {
|
||
// 不存在同名渠道:新增
|
||
newCh := &model.Channel{}
|
||
if err := chApplyToNew(newCh, item, req.SiteBuilderApiKey); err != nil {
|
||
result.Failed++
|
||
result.Failures = append(result.Failures, ChannelImportFailure{Name: name, Reason: "构建新增数据失败: " + err.Error()})
|
||
continue
|
||
}
|
||
if err := newCh.Insert(); err != nil {
|
||
result.Failed++
|
||
result.Failures = append(result.Failures, ChannelImportFailure{Name: name, Reason: "创建渠道失败: " + err.Error()})
|
||
continue
|
||
}
|
||
result.Added++
|
||
}
|
||
}
|
||
|
||
common.SysLog(fmt.Sprintf("渠道导入完成:新增 %d,更新 %d,失败 %d", result.Added, result.Updated, result.Failed))
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"message": "渠道导入完成",
|
||
"data": result,
|
||
})
|
||
}
|
||
|
||
// ─── 内部工具函数 ──────────────────────────────────────────────────────────────
|
||
|
||
// chGetStr 从 map 中安全读取字符串值。
|
||
func chGetStr(m map[string]interface{}, key string) (string, bool) {
|
||
v, ok := m[key]
|
||
if !ok {
|
||
return "", false
|
||
}
|
||
s, ok := v.(string)
|
||
return s, ok
|
||
}
|
||
|
||
// chToFloat64 将 JSON 数字(float64)或其他数值类型统一转为 float64。
|
||
func chToFloat64(v interface{}) float64 {
|
||
switch val := v.(type) {
|
||
case float64:
|
||
return val
|
||
case float32:
|
||
return float64(val)
|
||
case int:
|
||
return float64(val)
|
||
case int64:
|
||
return float64(val)
|
||
case string:
|
||
f, _ := strconv.ParseFloat(val, 64)
|
||
return f
|
||
}
|
||
return 0
|
||
}
|
||
|
||
// chValidateItem 校验导入条目中各字段的类型合法性。
|
||
// 非法字段跳过,不影响其他条目继续处理。
|
||
func chValidateItem(item map[string]interface{}) error {
|
||
if v, ok := item["models"]; ok && v != nil {
|
||
if _, ok := v.([]interface{}); !ok {
|
||
return fmt.Errorf("models 字段必须为数组")
|
||
}
|
||
}
|
||
if v, ok := item["groups"]; ok && v != nil {
|
||
if _, ok := v.([]interface{}); !ok {
|
||
return fmt.Errorf("groups 字段必须为数组")
|
||
}
|
||
}
|
||
if v, ok := item["modelRedirect"]; ok && v != nil {
|
||
if _, ok := v.(map[string]interface{}); !ok {
|
||
return fmt.Errorf("modelRedirect 字段必须为对象")
|
||
}
|
||
}
|
||
if v, ok := item["otherInfo"]; ok && v != nil {
|
||
if _, ok := v.(map[string]interface{}); !ok {
|
||
return fmt.Errorf("otherInfo 字段必须为对象")
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// chApplyToExisting 将导入数据应用到已存在的渠道(精确更新,仅更新 JSON 中存在的字段)。
|
||
// 通过 GORM Select+Updates 确保只写入指定列,不影响其他列。
|
||
// siteBuilderApiKey: 建站模式统一密钥,当渠道 type=60 且 apiKey 为空时使用此值。
|
||
func chApplyToExisting(ch *model.Channel, item map[string]interface{}, siteBuilderApiKey string) error {
|
||
cols := make([]string, 0, len(item))
|
||
updates := &model.Channel{}
|
||
|
||
if v, ok := item["discountRate"]; ok {
|
||
f := chToFloat64(v)
|
||
updates.PriceDiscountPercent = &f
|
||
cols = append(cols, "price_discount_percent")
|
||
}
|
||
if v, ok := item["markupDiscountRate"]; ok {
|
||
f := chToFloat64(v)
|
||
updates.MarkupDiscountRate = &f
|
||
cols = append(cols, "markup_discount_rate")
|
||
}
|
||
if v, ok := item["disabled"]; ok {
|
||
if b, isBool := v.(bool); isBool {
|
||
if b {
|
||
updates.Status = 2 // 禁用
|
||
} else {
|
||
updates.Status = 1 // 启用
|
||
}
|
||
cols = append(cols, "status")
|
||
}
|
||
}
|
||
if v, ok := item["type"]; ok {
|
||
updates.Type = int(chToFloat64(v))
|
||
cols = append(cols, "type")
|
||
}
|
||
if v, ok := item["logo"]; ok {
|
||
if s, ok := v.(string); ok {
|
||
updates.CompanyLogoURL = s
|
||
cols = append(cols, "company_logo_url")
|
||
}
|
||
}
|
||
if v, ok := item["providerType"]; ok {
|
||
if s, ok := v.(string); ok {
|
||
updates.SupplierType = s
|
||
cols = append(cols, "supplier_type")
|
||
}
|
||
}
|
||
if v, ok := item["apiKey"]; ok {
|
||
if s, ok := v.(string); ok {
|
||
// 建站模式:当 apiKey 为空且渠道 type=60 时,使用导入请求中的统一密钥
|
||
if strings.TrimSpace(s) == "" && siteBuilderApiKey != "" {
|
||
channelType := 0
|
||
if t, ok := item["type"]; ok {
|
||
channelType = int(chToFloat64(t))
|
||
} else {
|
||
channelType = ch.Type
|
||
}
|
||
if channelType == constant.ChannelTypeTokenFactoryOpen {
|
||
s = strings.TrimSpace(siteBuilderApiKey)
|
||
}
|
||
}
|
||
updates.Key = s
|
||
cols = append(cols, "key")
|
||
}
|
||
}
|
||
if v, ok := item["apiBaseUrl"]; ok {
|
||
if s, ok := v.(string); ok {
|
||
updates.BaseURL = &s
|
||
cols = append(cols, "base_url")
|
||
}
|
||
}
|
||
if v, ok := item["models"]; ok {
|
||
if arr, ok := v.([]interface{}); ok {
|
||
parts := make([]string, 0, len(arr))
|
||
for _, m := range arr {
|
||
if s, ok := m.(string); ok && strings.TrimSpace(s) != "" {
|
||
parts = append(parts, strings.TrimSpace(s))
|
||
}
|
||
}
|
||
updates.Models = strings.Join(parts, ",")
|
||
cols = append(cols, "models")
|
||
}
|
||
}
|
||
if v, ok := item["groups"]; ok {
|
||
if arr, ok := v.([]interface{}); ok {
|
||
parts := make([]string, 0, len(arr))
|
||
for _, g := range arr {
|
||
if s, ok := g.(string); ok && strings.TrimSpace(s) != "" {
|
||
parts = append(parts, strings.TrimSpace(s))
|
||
}
|
||
}
|
||
updates.Group = strings.Join(parts, ",")
|
||
cols = append(cols, "group")
|
||
}
|
||
}
|
||
if v, ok := item["modelRedirect"]; ok {
|
||
if m, ok := v.(map[string]interface{}); ok {
|
||
redirect := make(map[string]string, len(m))
|
||
for k, val := range m {
|
||
if s, ok := val.(string); ok {
|
||
redirect[k] = s
|
||
}
|
||
}
|
||
b, err := common.Marshal(redirect)
|
||
if err != nil {
|
||
return fmt.Errorf("序列化 modelRedirect 失败: %w", err)
|
||
}
|
||
s := string(b)
|
||
updates.ModelMapping = &s
|
||
cols = append(cols, "model_mapping")
|
||
}
|
||
}
|
||
if v, ok := item["quota"]; ok {
|
||
updates.Balance = chToFloat64(v)
|
||
cols = append(cols, "balance")
|
||
}
|
||
if v, ok := item["routeSlug"]; ok {
|
||
if s, ok := v.(string); ok {
|
||
updates.RouteSlug = s
|
||
cols = append(cols, "route_slug")
|
||
}
|
||
}
|
||
if v, ok := item["otherInfo"]; ok {
|
||
if m, ok := v.(map[string]interface{}); ok {
|
||
b, err := common.Marshal(m)
|
||
if err != nil {
|
||
return fmt.Errorf("序列化 otherInfo 失败: %w", err)
|
||
}
|
||
s := string(b)
|
||
updates.OtherInfo = s
|
||
cols = append(cols, "other_info")
|
||
}
|
||
}
|
||
|
||
if len(cols) == 0 {
|
||
// 没有可更新的字段,直接跳过(不报错)
|
||
return nil
|
||
}
|
||
|
||
// 使用精确列选择更新,确保只写入指定列
|
||
return model.PartialUpdateChannelFields(ch.Id, cols, updates)
|
||
}
|
||
|
||
// chApplyToNew 将导入数据写入新渠道对象,用于新增场景。
|
||
// siteBuilderApiKey: 建站模式统一密钥,当渠道 type=60 且 apiKey 为空时使用此值。
|
||
func chApplyToNew(ch *model.Channel, item map[string]interface{}, siteBuilderApiKey string) error {
|
||
name, ok := chGetStr(item, "name")
|
||
if !ok || strings.TrimSpace(name) == "" {
|
||
return fmt.Errorf("name 字段缺失")
|
||
}
|
||
ch.Name = strings.TrimSpace(name)
|
||
|
||
// 默认启用;若 JSON 中 disabled=true 则禁用
|
||
ch.Status = 1
|
||
if v, ok := item["disabled"]; ok {
|
||
if b, isBool := v.(bool); isBool && b {
|
||
ch.Status = 2
|
||
}
|
||
}
|
||
|
||
if v, ok := item["discountRate"]; ok {
|
||
f := chToFloat64(v)
|
||
ch.PriceDiscountPercent = &f
|
||
}
|
||
if v, ok := item["markupDiscountRate"]; ok {
|
||
f := chToFloat64(v)
|
||
ch.MarkupDiscountRate = &f
|
||
}
|
||
if v, ok := item["routeSlug"]; ok {
|
||
if s, ok := v.(string); ok {
|
||
ch.RouteSlug = s
|
||
}
|
||
}
|
||
if v, ok := item["quota"]; ok {
|
||
ch.Balance = chToFloat64(v)
|
||
}
|
||
if v, ok := item["type"]; ok {
|
||
ch.Type = int(chToFloat64(v))
|
||
}
|
||
if v, ok := item["logo"]; ok {
|
||
if s, ok := v.(string); ok {
|
||
ch.CompanyLogoURL = s
|
||
}
|
||
}
|
||
if v, ok := item["providerType"]; ok {
|
||
if s, ok := v.(string); ok {
|
||
ch.SupplierType = s
|
||
}
|
||
}
|
||
if v, ok := item["apiKey"]; ok {
|
||
if s, ok := v.(string); ok {
|
||
// 建站模式:当 apiKey 为空且渠道 type=60 时,使用导入请求中的统一密钥
|
||
if strings.TrimSpace(s) == "" && siteBuilderApiKey != "" {
|
||
if ch.Type == constant.ChannelTypeTokenFactoryOpen {
|
||
s = strings.TrimSpace(siteBuilderApiKey)
|
||
}
|
||
}
|
||
ch.Key = s
|
||
}
|
||
}
|
||
if v, ok := item["apiBaseUrl"]; ok {
|
||
if s, ok := v.(string); ok {
|
||
ch.BaseURL = &s
|
||
}
|
||
}
|
||
if v, ok := item["models"]; ok {
|
||
if arr, ok := v.([]interface{}); ok {
|
||
parts := make([]string, 0, len(arr))
|
||
for _, m := range arr {
|
||
if s, ok := m.(string); ok && strings.TrimSpace(s) != "" {
|
||
parts = append(parts, strings.TrimSpace(s))
|
||
}
|
||
}
|
||
ch.Models = strings.Join(parts, ",")
|
||
}
|
||
}
|
||
if v, ok := item["groups"]; ok {
|
||
if arr, ok := v.([]interface{}); ok {
|
||
parts := make([]string, 0, len(arr))
|
||
for _, g := range arr {
|
||
if s, ok := g.(string); ok && strings.TrimSpace(s) != "" {
|
||
parts = append(parts, strings.TrimSpace(s))
|
||
}
|
||
}
|
||
ch.Group = strings.Join(parts, ",")
|
||
}
|
||
} else {
|
||
ch.Group = "default" // 新增渠道默认分组
|
||
}
|
||
if v, ok := item["modelRedirect"]; ok {
|
||
if m, ok := v.(map[string]interface{}); ok {
|
||
redirect := make(map[string]string, len(m))
|
||
for k, val := range m {
|
||
if s, ok := val.(string); ok {
|
||
redirect[k] = s
|
||
}
|
||
}
|
||
b, err := common.Marshal(redirect)
|
||
if err != nil {
|
||
return fmt.Errorf("序列化 modelRedirect 失败: %w", err)
|
||
}
|
||
s := string(b)
|
||
ch.ModelMapping = &s
|
||
}
|
||
}
|
||
if v, ok := item["otherInfo"]; ok {
|
||
if m, ok := v.(map[string]interface{}); ok {
|
||
b, err := common.Marshal(m)
|
||
if err != nil {
|
||
return fmt.Errorf("序列化 otherInfo 失败: %w", err)
|
||
}
|
||
ch.OtherInfo = string(b)
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|