修复订阅无法正常获取的错误
Some checks failed
build / build (api, amd64, linux) (push) Failing after -50s
build / build (api, arm64, linux) (push) Failing after -51s
build / build (api.exe, amd64, windows) (push) Failing after -51s

This commit is contained in:
CN-JS-HuiBai
2026-04-17 23:07:47 +08:00
parent 9f6a4515c0
commit 97f0672729
11 changed files with 273 additions and 95 deletions

View File

@@ -12,6 +12,5 @@ REDIS_DB=0
JWT_SECRET=change_me_to_a_long_random_secret
APP_PORT=8080
APP_URL=http://127.0.0.1:8080
LOG_LEVEL=info
# Plugin source reference directory, only needed during development.
PLUGIN_ROOT=reference/LDNET-GA-Theme/plugin

View File

@@ -1,6 +1,7 @@
package main
import (
"io"
"log"
"os"
"path/filepath"
@@ -16,23 +17,42 @@ import (
func main() {
config.LoadConfig()
configureRuntimeLogging()
database.InitDB()
database.InitCache()
router := gin.New()
router.Use(gin.Logger(), gin.Recovery())
if config.IsLogLevelEnabled(config.AppConfig.LogLevel, "info") {
router.Use(gin.Logger())
}
router.Use(gin.Recovery())
api := router.Group("/api")
registerV1(api.Group("/v1"))
registerV2(api.Group("/v2"))
registerWebRoutes(router)
if config.IsLogLevelEnabled(config.AppConfig.LogLevel, "info") {
log.Printf("server starting on port %s", config.AppConfig.AppPort)
}
if err := router.Run(":" + config.AppConfig.AppPort); err != nil {
log.Fatalf("failed to start server: %v", err)
}
}
func configureRuntimeLogging() {
if config.NormalizeLogLevel(config.AppConfig.LogLevel) == "debug" {
gin.SetMode(gin.DebugMode)
} else {
gin.SetMode(gin.ReleaseMode)
}
if config.NormalizeLogLevel(config.AppConfig.LogLevel) == "silent" {
gin.DefaultWriter = io.Discard
gin.DefaultErrorWriter = io.Discard
}
}
func registerV1(v1 *gin.RouterGroup) {
registerPassportRoutes(v1)
registerGuestRoutes(v1)
@@ -288,8 +308,10 @@ func registerWebRoutes(router *gin.Engine) {
}
securePath := "/" + service.GetAdminSecurePath()
subscribePath := "/" + service.GetSubscribePath()
router.GET("/", handler.UserThemePage)
router.GET("/dashboard", handler.UserThemePage)
router.GET(subscribePath+"/:token", handler.Subscribe)
router.GET(securePath, handler.AdminAppPage)
router.GET(securePath+"/", handler.AdminAppPage)
router.GET(securePath+"/plugin-panel/:kind", handler.AdminPluginPanelPage)

View File

@@ -4,6 +4,7 @@ import (
"log"
"os"
"strconv"
"strings"
"github.com/joho/godotenv"
)
@@ -21,6 +22,7 @@ type Config struct {
JWTSecret string
AppPort string
AppURL string
LogLevel string
PluginRoot string
}
@@ -45,10 +47,47 @@ func LoadConfig() {
JWTSecret: getEnv("JWT_SECRET", "secret"),
AppPort: getEnv("APP_PORT", "8080"),
AppURL: getEnv("APP_URL", ""),
LogLevel: NormalizeLogLevel(getEnv("LOG_LEVEL", "info")),
PluginRoot: getEnv("PLUGIN_ROOT", "reference\\LDNET-GA-Theme\\plugin"),
}
}
func NormalizeLogLevel(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "debug":
return "debug"
case "warn", "warning":
return "warn"
case "error":
return "error"
case "silent":
return "silent"
default:
return "info"
}
}
func IsLogLevelEnabled(currentLevel, targetLevel string) bool {
logLevelOrder := map[string]int{
"debug": 0,
"info": 1,
"warn": 2,
"error": 3,
"silent": 4,
}
currentRank, ok := logLevelOrder[NormalizeLogLevel(currentLevel)]
if !ok {
currentRank = logLevelOrder["info"]
}
targetRank, ok := logLevelOrder[NormalizeLogLevel(targetLevel)]
if !ok {
targetRank = logLevelOrder["info"]
}
return currentRank <= targetRank
}
func getEnv(key, defaultValue string) string {
if value, exists := os.LookupEnv(key); exists {
return value

View File

@@ -30,7 +30,9 @@ var fallbackCache = &memoryCache{
func InitCache() {
if config.AppConfig.RedisHost == "" {
if config.IsLogLevelEnabled(config.AppConfig.LogLevel, "warn") {
log.Printf("Redis host not configured, using in-memory cache")
}
return
}
@@ -46,12 +48,16 @@ func InitCache() {
defer cancel()
if err := client.Ping(ctx).Err(); err != nil {
if config.IsLogLevelEnabled(config.AppConfig.LogLevel, "warn") {
log.Printf("Redis unavailable at %s, falling back to in-memory: %v", addr, err)
}
return
}
Redis = client
if config.IsLogLevelEnabled(config.AppConfig.LogLevel, "info") {
log.Printf("Redis connection established at %s", addr)
}
}
func CacheSet(key string, value any, ttl time.Duration) error {

View File

@@ -24,7 +24,7 @@ func InitDB() {
var err error
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
Logger: logger.Default.LogMode(gormLogLevel(config.AppConfig.LogLevel)),
DisableForeignKeyConstraintWhenMigrating: true,
})
@@ -40,5 +40,22 @@ func InitDB() {
log.Fatalf("Failed to migrate database tables: %v", err)
}
if config.IsLogLevelEnabled(config.AppConfig.LogLevel, "info") {
log.Println("Database connection established")
}
}
func gormLogLevel(level string) logger.LogLevel {
switch config.NormalizeLogLevel(level) {
case "debug", "info":
return logger.Info
case "warn":
return logger.Warn
case "error":
return logger.Error
case "silent":
return logger.Silent
default:
return logger.Info
}
}

View File

@@ -155,7 +155,7 @@ func getAllConfigMappings() gin.H {
"show_protocol_to_server_enable": service.MustGetBool("show_protocol_to_server_enable", false),
"default_remind_expire": service.MustGetBool("default_remind_expire", true),
"default_remind_traffic": service.MustGetBool("default_remind_traffic", true),
"subscribe_path": service.MustGetString("subscribe_path", "s"),
"subscribe_path": service.GetSubscribePath(),
},
"subscribe_template": service.GetAllSubscribeTemplates(),
"frontend": gin.H{

View File

@@ -0,0 +1,147 @@
package handler
import (
"net"
"net/http"
"net/url"
"strings"
"xboard-go/internal/service"
"github.com/gin-gonic/gin"
)
func requestBaseURL(c *gin.Context) string {
if c != nil {
if origin := requestOrigin(c.Request); origin != "" {
return origin
}
}
if appURL := service.GetAppURL(); appURL != "" {
return strings.TrimRight(appURL, "/")
}
return ""
}
func requestOrigin(r *http.Request) string {
if r == nil {
return ""
}
scheme, host := forwardedOrigin(r)
if scheme != "" && host != "" {
return scheme + "://" + host
}
if host == "" {
host = firstHeaderValue(r.Header.Get("X-Forwarded-Host"))
}
if host == "" {
host = strings.TrimSpace(r.Host)
}
if host == "" {
host = originHostFallback(r)
}
if host == "" {
return ""
}
if scheme == "" {
scheme = firstHeaderValue(r.Header.Get("X-Forwarded-Proto"))
}
if scheme == "" {
scheme = firstHeaderValue(r.Header.Get("X-Forwarded-Scheme"))
}
if scheme == "" {
scheme = firstHeaderValue(r.Header.Get("X-Scheme"))
}
if scheme == "" && strings.EqualFold(strings.TrimSpace(r.Header.Get("Front-End-Https")), "on") {
scheme = "https"
}
if scheme == "" && strings.EqualFold(strings.TrimSpace(r.Header.Get("X-Forwarded-Ssl")), "on") {
scheme = "https"
}
if scheme == "" {
switch strings.TrimSpace(firstHeaderValue(r.Header.Get("X-Forwarded-Port"))) {
case "443":
scheme = "https"
case "80":
scheme = "http"
}
}
if scheme == "" {
scheme = originSchemeFallback(r)
}
if scheme == "" {
if r.TLS != nil {
scheme = "https"
} else {
scheme = "http"
}
}
if forwardedPort := strings.TrimSpace(firstHeaderValue(r.Header.Get("X-Forwarded-Port"))); forwardedPort != "" && !strings.Contains(host, ":") {
defaultPort := map[string]string{
"http": "80",
"https": "443",
}[scheme]
if forwardedPort != defaultPort {
host = net.JoinHostPort(host, forwardedPort)
}
}
return scheme + "://" + host
}
func forwardedOrigin(r *http.Request) (scheme string, host string) {
forwarded := strings.TrimSpace(firstHeaderValue(r.Header.Get("Forwarded")))
if forwarded == "" {
return "", ""
}
for _, segment := range strings.Split(forwarded, ";") {
parts := strings.SplitN(strings.TrimSpace(segment), "=", 2)
if len(parts) != 2 {
continue
}
key := strings.ToLower(strings.TrimSpace(parts[0]))
value := strings.Trim(strings.TrimSpace(parts[1]), "\"")
switch key {
case "proto":
if value != "" {
scheme = value
}
case "host":
if value != "" {
host = value
}
}
}
return scheme, host
}
func firstHeaderValue(value string) string {
if value == "" {
return ""
}
parts := strings.Split(value, ",")
return strings.TrimSpace(parts[0])
}
func originHostFallback(r *http.Request) string {
for _, raw := range []string{r.Header.Get("Origin"), r.Header.Get("Referer")} {
if parsed, err := url.Parse(strings.TrimSpace(raw)); err == nil && parsed.Host != "" {
return parsed.Host
}
}
return ""
}
func originSchemeFallback(r *http.Request) string {
for _, raw := range []string{r.Header.Get("Origin"), r.Header.Get("Referer")} {
if parsed, err := url.Parse(strings.TrimSpace(raw)); err == nil && parsed.Scheme != "" {
return parsed.Scheme
}
}
return ""
}

View File

@@ -74,14 +74,8 @@ func UserGetSubscribe(c *gin.Context) {
}
}
baseURL := strings.TrimRight(service.GetAppURL(), "/")
if baseURL == "" {
scheme := "http"
if c.Request.TLS != nil {
scheme = "https"
}
baseURL = scheme + "://" + c.Request.Host
}
baseURL := requestBaseURL(c)
subscribePath := service.GetSubscribePath()
Success(c, gin.H{
"plan_id": user.PlanID,
@@ -96,7 +90,7 @@ func UserGetSubscribe(c *gin.Context) {
"speed_limit": user.SpeedLimit,
"next_reset_at": user.NextResetAt,
"plan": user.Plan,
"subscribe_url": strings.TrimRight(baseURL, "/") + "/s/" + user.Token,
"subscribe_url": strings.TrimRight(baseURL, "/") + "/" + subscribePath + "/" + user.Token,
"reset_day": nil,
})
}
@@ -129,15 +123,8 @@ func UserResetSecurity(c *gin.Context) {
return
}
baseURL := strings.TrimRight(service.GetAppURL(), "/")
if baseURL == "" {
scheme := "http"
if c.Request.TLS != nil {
scheme = "https"
}
baseURL = scheme + "://" + c.Request.Host
}
Success(c, strings.TrimRight(baseURL, "/")+"/s/"+newToken)
baseURL := requestBaseURL(c)
Success(c, strings.TrimRight(baseURL, "/")+"/"+service.GetSubscribePath()+"/"+newToken)
}
func UserUpdate(c *gin.Context) {

View File

@@ -726,14 +726,10 @@ func quickLoginURL(c *gin.Context, userID int, redirectValues ...string) string
}
func baseURL(c *gin.Context) string {
if appURL := service.GetAppURL(); appURL != "" {
return strings.TrimRight(appURL, "/")
if baseURL := requestBaseURL(c); baseURL != "" {
return baseURL
}
scheme := "http"
if c.Request.TLS != nil {
scheme = "https"
}
return scheme + "://" + c.Request.Host
return ""
}
func generateTradeNo() string {

View File

@@ -4,11 +4,9 @@ import (
"bytes"
"encoding/json"
"html/template"
"net"
"net/http"
"path/filepath"
"strconv"
"strings"
"time"
"xboard-go/internal/service"
@@ -65,15 +63,10 @@ func UserThemePage(c *gin.Context) {
func AdminAppPage(c *gin.Context) {
securePath := service.GetAdminSecurePath()
baseURL := service.GetAppURL()
if origin := requestOrigin(c.Request); origin != "" {
baseURL = origin
}
title := service.MustGetString("app_name", "XBoard") + " Admin"
settings := map[string]string{
"base_url": baseURL,
"base_url": requestBaseURL(c),
"title": title,
"version": service.MustGetString("app_version", "1.0.0"),
"logo": service.MustGetString("logo", ""),
@@ -113,38 +106,3 @@ func mustJSON(value any) template.JS {
}
return template.JS(payload)
}
func requestOrigin(r *http.Request) string {
if r == nil {
return ""
}
scheme := r.Header.Get("X-Forwarded-Proto")
if scheme == "" {
if r.TLS != nil {
scheme = "https"
} else {
scheme = "http"
}
}
host := r.Header.Get("X-Forwarded-Host")
if host == "" {
host = r.Host
}
if host == "" {
return ""
}
if forwardedPort := r.Header.Get("X-Forwarded-Port"); forwardedPort != "" && !strings.Contains(host, ":") {
defaultPort := map[string]string{
"http": "80",
"https": "443",
}[scheme]
if forwardedPort != "" && forwardedPort != defaultPort {
host = net.JoinHostPort(host, forwardedPort)
}
}
return scheme + "://" + host
}

View File

@@ -79,6 +79,13 @@ func GetAdminSecurePath() string {
return "admin"
}
func GetSubscribePath() string {
if subscribePath := strings.Trim(normalizeWrappedString(MustGetString("subscribe_path", "")), "/"); subscribePath != "" {
return subscribePath
}
return "s"
}
func GetAppURL() string {
if appURL := normalizeWrappedString(MustGetString("app_url", "")); appURL != "" {
return strings.TrimRight(appURL, "/")