2025-06-19 08:57:34 +08:00
|
|
|
|
package controller
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2025-08-14 20:05:06 +08:00
|
|
|
|
"context"
|
|
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"net/http"
|
|
|
|
|
|
"one-api/logger"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
"sync"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"one-api/dto"
|
|
|
|
|
|
"one-api/model"
|
|
|
|
|
|
"one-api/setting/ratio_setting"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
2025-06-19 08:57:34 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
🚀 chore(controller, dto): elevate ratio-sync feature to production readiness
WHAT’S NEW
• controller/ratio_sync.go
– Deleted unused local structs (TestResult, DifferenceItem, SyncableChannel).
– Centralised config with constants: defaultTimeoutSeconds, defaultEndpoint, maxConcurrentFetches, ratioTypes.
– Replaced magic numbers; added semaphore-based concurrency limit and shared http.Client (with TLS & Expect-Continue timeouts).
– Added comprehensive error handling and context-aware logging via common.Log* helpers.
– Checked DB errors from GetChannelsByIds; early-return on failures or empty upstream list.
– Removed custom-channel support; logic now relies solely on ChannelIDs.
– Minor clean-ups: import grouping, string trimming, endpoint normalisation.
• dto/ratio_sync.go
– Simplified UpstreamRequest: dropped unused CustomChannels field.
WHY
These improvements harden the ratio-sync endpoint for production use by preventing silent failures, controlling resource usage, and making behaviour configurable and observable.
HOW
No business logic change—only structural refactor, logging, and safeguards—so existing API contracts (aside from removed custom_channels) remain intact.
2025-06-19 19:55:51 +08:00
|
|
|
|
const (
|
2025-08-14 20:05:06 +08:00
|
|
|
|
defaultTimeoutSeconds = 10
|
|
|
|
|
|
defaultEndpoint = "/api/ratio_config"
|
|
|
|
|
|
maxConcurrentFetches = 8
|
🚀 chore(controller, dto): elevate ratio-sync feature to production readiness
WHAT’S NEW
• controller/ratio_sync.go
– Deleted unused local structs (TestResult, DifferenceItem, SyncableChannel).
– Centralised config with constants: defaultTimeoutSeconds, defaultEndpoint, maxConcurrentFetches, ratioTypes.
– Replaced magic numbers; added semaphore-based concurrency limit and shared http.Client (with TLS & Expect-Continue timeouts).
– Added comprehensive error handling and context-aware logging via common.Log* helpers.
– Checked DB errors from GetChannelsByIds; early-return on failures or empty upstream list.
– Removed custom-channel support; logic now relies solely on ChannelIDs.
– Minor clean-ups: import grouping, string trimming, endpoint normalisation.
• dto/ratio_sync.go
– Simplified UpstreamRequest: dropped unused CustomChannels field.
WHY
These improvements harden the ratio-sync endpoint for production use by preventing silent failures, controlling resource usage, and making behaviour configurable and observable.
HOW
No business logic change—only structural refactor, logging, and safeguards—so existing API contracts (aside from removed custom_channels) remain intact.
2025-06-19 19:55:51 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
var ratioTypes = []string{"model_ratio", "completion_ratio", "cache_ratio", "model_price"}
|
|
|
|
|
|
|
2025-06-19 08:57:34 +08:00
|
|
|
|
type upstreamResult struct {
|
2025-08-14 20:05:06 +08:00
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
|
Data map[string]any `json:"data,omitempty"`
|
|
|
|
|
|
Err string `json:"err,omitempty"`
|
2025-06-19 08:57:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func FetchUpstreamRatios(c *gin.Context) {
|
2025-08-14 20:05:06 +08:00
|
|
|
|
var req dto.UpstreamRequest
|
|
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if req.Timeout <= 0 {
|
|
|
|
|
|
req.Timeout = defaultTimeoutSeconds
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var upstreams []dto.UpstreamDTO
|
|
|
|
|
|
|
|
|
|
|
|
if len(req.Upstreams) > 0 {
|
|
|
|
|
|
for _, u := range req.Upstreams {
|
|
|
|
|
|
if strings.HasPrefix(u.BaseURL, "http") {
|
|
|
|
|
|
if u.Endpoint == "" {
|
|
|
|
|
|
u.Endpoint = defaultEndpoint
|
|
|
|
|
|
}
|
|
|
|
|
|
u.BaseURL = strings.TrimRight(u.BaseURL, "/")
|
|
|
|
|
|
upstreams = append(upstreams, u)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if len(req.ChannelIDs) > 0 {
|
|
|
|
|
|
intIds := make([]int, 0, len(req.ChannelIDs))
|
|
|
|
|
|
for _, id64 := range req.ChannelIDs {
|
|
|
|
|
|
intIds = append(intIds, int(id64))
|
|
|
|
|
|
}
|
|
|
|
|
|
dbChannels, err := model.GetChannelsByIds(intIds)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
logger.LogError(c.Request.Context(), "failed to query channels: "+err.Error())
|
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "查询渠道失败"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, ch := range dbChannels {
|
|
|
|
|
|
if base := ch.GetBaseURL(); strings.HasPrefix(base, "http") {
|
|
|
|
|
|
upstreams = append(upstreams, dto.UpstreamDTO{
|
|
|
|
|
|
ID: ch.Id,
|
|
|
|
|
|
Name: ch.Name,
|
|
|
|
|
|
BaseURL: strings.TrimRight(base, "/"),
|
|
|
|
|
|
Endpoint: "",
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(upstreams) == 0 {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "message": "无有效上游渠道"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
|
ch := make(chan upstreamResult, len(upstreams))
|
|
|
|
|
|
|
|
|
|
|
|
sem := make(chan struct{}, maxConcurrentFetches)
|
|
|
|
|
|
|
|
|
|
|
|
client := &http.Client{Transport: &http.Transport{MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second}}
|
|
|
|
|
|
|
|
|
|
|
|
for _, chn := range upstreams {
|
|
|
|
|
|
wg.Add(1)
|
|
|
|
|
|
go func(chItem dto.UpstreamDTO) {
|
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
|
|
|
|
|
|
|
sem <- struct{}{}
|
|
|
|
|
|
defer func() { <-sem }()
|
|
|
|
|
|
|
|
|
|
|
|
endpoint := chItem.Endpoint
|
|
|
|
|
|
if endpoint == "" {
|
|
|
|
|
|
endpoint = defaultEndpoint
|
|
|
|
|
|
} else if !strings.HasPrefix(endpoint, "/") {
|
|
|
|
|
|
endpoint = "/" + endpoint
|
|
|
|
|
|
}
|
|
|
|
|
|
fullURL := chItem.BaseURL + endpoint
|
|
|
|
|
|
|
|
|
|
|
|
uniqueName := chItem.Name
|
|
|
|
|
|
if chItem.ID != 0 {
|
|
|
|
|
|
uniqueName = fmt.Sprintf("%s(%d)", chItem.Name, chItem.ID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), time.Duration(req.Timeout)*time.Second)
|
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
|
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
logger.LogWarn(c.Request.Context(), "build request failed: "+err.Error())
|
|
|
|
|
|
ch <- upstreamResult{Name: uniqueName, Err: err.Error()}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resp, err := client.Do(httpReq)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
logger.LogWarn(c.Request.Context(), "http error on "+chItem.Name+": "+err.Error())
|
|
|
|
|
|
ch <- upstreamResult{Name: uniqueName, Err: err.Error()}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
|
|
logger.LogWarn(c.Request.Context(), "non-200 from "+chItem.Name+": "+resp.Status)
|
|
|
|
|
|
ch <- upstreamResult{Name: uniqueName, Err: resp.Status}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
// 兼容两种上游接口格式:
|
|
|
|
|
|
// type1: /api/ratio_config -> data 为 map[string]any,包含 model_ratio/completion_ratio/cache_ratio/model_price
|
|
|
|
|
|
// type2: /api/pricing -> data 为 []Pricing 列表,需要转换为与 type1 相同的 map 格式
|
|
|
|
|
|
var body struct {
|
|
|
|
|
|
Success bool `json:"success"`
|
|
|
|
|
|
Data json.RawMessage `json:"data"`
|
|
|
|
|
|
Message string `json:"message"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
|
|
|
|
logger.LogWarn(c.Request.Context(), "json decode failed from "+chItem.Name+": "+err.Error())
|
|
|
|
|
|
ch <- upstreamResult{Name: uniqueName, Err: err.Error()}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if !body.Success {
|
|
|
|
|
|
ch <- upstreamResult{Name: uniqueName, Err: body.Message}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试按 type1 解析
|
|
|
|
|
|
var type1Data map[string]any
|
|
|
|
|
|
if err := json.Unmarshal(body.Data, &type1Data); err == nil {
|
|
|
|
|
|
// 如果包含至少一个 ratioTypes 字段,则认为是 type1
|
|
|
|
|
|
isType1 := false
|
|
|
|
|
|
for _, rt := range ratioTypes {
|
|
|
|
|
|
if _, ok := type1Data[rt]; ok {
|
|
|
|
|
|
isType1 = true
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if isType1 {
|
|
|
|
|
|
ch <- upstreamResult{Name: uniqueName, Data: type1Data}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果不是 type1,则尝试按 type2 (/api/pricing) 解析
|
|
|
|
|
|
var pricingItems []struct {
|
|
|
|
|
|
ModelName string `json:"model_name"`
|
|
|
|
|
|
QuotaType int `json:"quota_type"`
|
|
|
|
|
|
ModelRatio float64 `json:"model_ratio"`
|
|
|
|
|
|
ModelPrice float64 `json:"model_price"`
|
|
|
|
|
|
CompletionRatio float64 `json:"completion_ratio"`
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := json.Unmarshal(body.Data, &pricingItems); err != nil {
|
|
|
|
|
|
logger.LogWarn(c.Request.Context(), "unrecognized data format from "+chItem.Name+": "+err.Error())
|
|
|
|
|
|
ch <- upstreamResult{Name: uniqueName, Err: "无法解析上游返回数据"}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
modelRatioMap := make(map[string]float64)
|
|
|
|
|
|
completionRatioMap := make(map[string]float64)
|
|
|
|
|
|
modelPriceMap := make(map[string]float64)
|
|
|
|
|
|
|
|
|
|
|
|
for _, item := range pricingItems {
|
|
|
|
|
|
if item.QuotaType == 1 {
|
|
|
|
|
|
modelPriceMap[item.ModelName] = item.ModelPrice
|
|
|
|
|
|
} else {
|
|
|
|
|
|
modelRatioMap[item.ModelName] = item.ModelRatio
|
|
|
|
|
|
// completionRatio 可能为 0,此时也直接赋值,保持与上游一致
|
|
|
|
|
|
completionRatioMap[item.ModelName] = item.CompletionRatio
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
converted := make(map[string]any)
|
|
|
|
|
|
|
|
|
|
|
|
if len(modelRatioMap) > 0 {
|
|
|
|
|
|
ratioAny := make(map[string]any, len(modelRatioMap))
|
|
|
|
|
|
for k, v := range modelRatioMap {
|
|
|
|
|
|
ratioAny[k] = v
|
|
|
|
|
|
}
|
|
|
|
|
|
converted["model_ratio"] = ratioAny
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(completionRatioMap) > 0 {
|
|
|
|
|
|
compAny := make(map[string]any, len(completionRatioMap))
|
|
|
|
|
|
for k, v := range completionRatioMap {
|
|
|
|
|
|
compAny[k] = v
|
|
|
|
|
|
}
|
|
|
|
|
|
converted["completion_ratio"] = compAny
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(modelPriceMap) > 0 {
|
|
|
|
|
|
priceAny := make(map[string]any, len(modelPriceMap))
|
|
|
|
|
|
for k, v := range modelPriceMap {
|
|
|
|
|
|
priceAny[k] = v
|
|
|
|
|
|
}
|
|
|
|
|
|
converted["model_price"] = priceAny
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ch <- upstreamResult{Name: uniqueName, Data: converted}
|
|
|
|
|
|
}(chn)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
wg.Wait()
|
|
|
|
|
|
close(ch)
|
|
|
|
|
|
|
|
|
|
|
|
localData := ratio_setting.GetExposedData()
|
|
|
|
|
|
|
|
|
|
|
|
var testResults []dto.TestResult
|
|
|
|
|
|
var successfulChannels []struct {
|
|
|
|
|
|
name string
|
|
|
|
|
|
data map[string]any
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for r := range ch {
|
|
|
|
|
|
if r.Err != "" {
|
|
|
|
|
|
testResults = append(testResults, dto.TestResult{
|
|
|
|
|
|
Name: r.Name,
|
|
|
|
|
|
Status: "error",
|
|
|
|
|
|
Error: r.Err,
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
testResults = append(testResults, dto.TestResult{
|
|
|
|
|
|
Name: r.Name,
|
|
|
|
|
|
Status: "success",
|
|
|
|
|
|
})
|
|
|
|
|
|
successfulChannels = append(successfulChannels, struct {
|
|
|
|
|
|
name string
|
|
|
|
|
|
data map[string]any
|
|
|
|
|
|
}{name: r.Name, data: r.Data})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
differences := buildDifferences(localData, successfulChannels)
|
|
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"data": gin.H{
|
|
|
|
|
|
"differences": differences,
|
|
|
|
|
|
"test_results": testResults,
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
2025-06-19 08:57:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func buildDifferences(localData map[string]any, successfulChannels []struct {
|
2025-08-14 20:05:06 +08:00
|
|
|
|
name string
|
|
|
|
|
|
data map[string]any
|
2025-06-19 08:57:34 +08:00
|
|
|
|
}) map[string]map[string]dto.DifferenceItem {
|
2025-08-14 20:05:06 +08:00
|
|
|
|
differences := make(map[string]map[string]dto.DifferenceItem)
|
|
|
|
|
|
|
|
|
|
|
|
allModels := make(map[string]struct{})
|
|
|
|
|
|
|
|
|
|
|
|
for _, ratioType := range ratioTypes {
|
|
|
|
|
|
if localRatioAny, ok := localData[ratioType]; ok {
|
|
|
|
|
|
if localRatio, ok := localRatioAny.(map[string]float64); ok {
|
|
|
|
|
|
for modelName := range localRatio {
|
|
|
|
|
|
allModels[modelName] = struct{}{}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for _, channel := range successfulChannels {
|
|
|
|
|
|
for _, ratioType := range ratioTypes {
|
|
|
|
|
|
if upstreamRatio, ok := channel.data[ratioType].(map[string]any); ok {
|
|
|
|
|
|
for modelName := range upstreamRatio {
|
|
|
|
|
|
allModels[modelName] = struct{}{}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
confidenceMap := make(map[string]map[string]bool)
|
|
|
|
|
|
|
|
|
|
|
|
// 预处理阶段:检查pricing接口的可信度
|
|
|
|
|
|
for _, channel := range successfulChannels {
|
|
|
|
|
|
confidenceMap[channel.name] = make(map[string]bool)
|
|
|
|
|
|
|
|
|
|
|
|
modelRatios, hasModelRatio := channel.data["model_ratio"].(map[string]any)
|
|
|
|
|
|
completionRatios, hasCompletionRatio := channel.data["completion_ratio"].(map[string]any)
|
|
|
|
|
|
|
|
|
|
|
|
if hasModelRatio && hasCompletionRatio {
|
|
|
|
|
|
// 遍历所有模型,检查是否满足不可信条件
|
|
|
|
|
|
for modelName := range allModels {
|
|
|
|
|
|
// 默认为可信
|
|
|
|
|
|
confidenceMap[channel.name][modelName] = true
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否满足不可信条件:model_ratio为37.5且completion_ratio为1
|
|
|
|
|
|
if modelRatioVal, ok := modelRatios[modelName]; ok {
|
|
|
|
|
|
if completionRatioVal, ok := completionRatios[modelName]; ok {
|
|
|
|
|
|
// 转换为float64进行比较
|
|
|
|
|
|
if modelRatioFloat, ok := modelRatioVal.(float64); ok {
|
|
|
|
|
|
if completionRatioFloat, ok := completionRatioVal.(float64); ok {
|
|
|
|
|
|
if modelRatioFloat == 37.5 && completionRatioFloat == 1.0 {
|
|
|
|
|
|
confidenceMap[channel.name][modelName] = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 如果不是从pricing接口获取的数据,则全部标记为可信
|
|
|
|
|
|
for modelName := range allModels {
|
|
|
|
|
|
confidenceMap[channel.name][modelName] = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for modelName := range allModels {
|
|
|
|
|
|
for _, ratioType := range ratioTypes {
|
|
|
|
|
|
var localValue interface{} = nil
|
|
|
|
|
|
if localRatioAny, ok := localData[ratioType]; ok {
|
|
|
|
|
|
if localRatio, ok := localRatioAny.(map[string]float64); ok {
|
|
|
|
|
|
if val, exists := localRatio[modelName]; exists {
|
|
|
|
|
|
localValue = val
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
upstreamValues := make(map[string]interface{})
|
|
|
|
|
|
confidenceValues := make(map[string]bool)
|
|
|
|
|
|
hasUpstreamValue := false
|
|
|
|
|
|
hasDifference := false
|
|
|
|
|
|
|
|
|
|
|
|
for _, channel := range successfulChannels {
|
|
|
|
|
|
var upstreamValue interface{} = nil
|
|
|
|
|
|
|
|
|
|
|
|
if upstreamRatio, ok := channel.data[ratioType].(map[string]any); ok {
|
|
|
|
|
|
if val, exists := upstreamRatio[modelName]; exists {
|
|
|
|
|
|
upstreamValue = val
|
|
|
|
|
|
hasUpstreamValue = true
|
|
|
|
|
|
|
|
|
|
|
|
if localValue != nil && localValue != val {
|
|
|
|
|
|
hasDifference = true
|
|
|
|
|
|
} else if localValue == val {
|
|
|
|
|
|
upstreamValue = "same"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if upstreamValue == nil && localValue == nil {
|
|
|
|
|
|
upstreamValue = "same"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if localValue == nil && upstreamValue != nil && upstreamValue != "same" {
|
|
|
|
|
|
hasDifference = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
upstreamValues[channel.name] = upstreamValue
|
|
|
|
|
|
|
|
|
|
|
|
confidenceValues[channel.name] = confidenceMap[channel.name][modelName]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
shouldInclude := false
|
|
|
|
|
|
|
|
|
|
|
|
if localValue != nil {
|
|
|
|
|
|
if hasDifference {
|
|
|
|
|
|
shouldInclude = true
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if hasUpstreamValue {
|
|
|
|
|
|
shouldInclude = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if shouldInclude {
|
|
|
|
|
|
if differences[modelName] == nil {
|
|
|
|
|
|
differences[modelName] = make(map[string]dto.DifferenceItem)
|
|
|
|
|
|
}
|
|
|
|
|
|
differences[modelName][ratioType] = dto.DifferenceItem{
|
|
|
|
|
|
Current: localValue,
|
|
|
|
|
|
Upstreams: upstreamValues,
|
|
|
|
|
|
Confidence: confidenceValues,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
channelHasDiff := make(map[string]bool)
|
|
|
|
|
|
for _, ratioMap := range differences {
|
|
|
|
|
|
for _, item := range ratioMap {
|
|
|
|
|
|
for chName, val := range item.Upstreams {
|
|
|
|
|
|
if val != nil && val != "same" {
|
|
|
|
|
|
channelHasDiff[chName] = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for modelName, ratioMap := range differences {
|
|
|
|
|
|
for ratioType, item := range ratioMap {
|
|
|
|
|
|
for chName := range item.Upstreams {
|
|
|
|
|
|
if !channelHasDiff[chName] {
|
|
|
|
|
|
delete(item.Upstreams, chName)
|
|
|
|
|
|
delete(item.Confidence, chName)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
allSame := true
|
|
|
|
|
|
for _, v := range item.Upstreams {
|
|
|
|
|
|
if v != "same" {
|
|
|
|
|
|
allSame = false
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(item.Upstreams) == 0 || allSame {
|
|
|
|
|
|
delete(ratioMap, ratioType)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
differences[modelName][ratioType] = item
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(ratioMap) == 0 {
|
|
|
|
|
|
delete(differences, modelName)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return differences
|
2025-06-19 08:57:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func GetSyncableChannels(c *gin.Context) {
|
2025-08-14 20:05:06 +08:00
|
|
|
|
channels, err := model.GetAllChannels(0, 0, true, false)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"message": err.Error(),
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var syncableChannels []dto.SyncableChannel
|
|
|
|
|
|
for _, channel := range channels {
|
|
|
|
|
|
if channel.GetBaseURL() != "" {
|
|
|
|
|
|
syncableChannels = append(syncableChannels, dto.SyncableChannel{
|
|
|
|
|
|
ID: channel.Id,
|
|
|
|
|
|
Name: channel.Name,
|
|
|
|
|
|
BaseURL: channel.GetBaseURL(),
|
|
|
|
|
|
Status: channel.Status,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"message": "",
|
|
|
|
|
|
"data": syncableChannels,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|