修复订阅无法正常获取的错误
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)
log.Printf("server starting on port %s", config.AppConfig.AppPort)
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,24 +4,26 @@ import (
"log"
"os"
"strconv"
"strings"
"github.com/joho/godotenv"
)
type Config struct {
DBHost string
DBPort string
DBUser string
DBPass string
DBName string
RedisHost string
RedisPort string
RedisPass string
RedisDB int
JWTSecret string
AppPort string
AppURL string
PluginRoot string
DBHost string
DBPort string
DBUser string
DBPass string
DBName string
RedisHost string
RedisPort string
RedisPass string
RedisDB int
JWTSecret string
AppPort string
AppURL string
LogLevel string
PluginRoot string
}
var AppConfig *Config
@@ -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 == "" {
log.Printf("Redis host not configured, using in-memory cache")
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 {
log.Printf("Redis unavailable at %s, falling back to in-memory: %v", addr, err)
if config.IsLogLevelEnabled(config.AppConfig.LogLevel, "warn") {
log.Printf("Redis unavailable at %s, falling back to in-memory: %v", addr, err)
}
return
}
Redis = client
log.Printf("Redis connection established at %s", addr)
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)
}
log.Println("Database connection established")
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

@@ -206,10 +206,10 @@ func UserTrafficLog(c *gin.Context) {
items := make([]gin.H, 0, len(records))
for _, record := range records {
items = append(items, gin.H{
"user_id": record.UserID,
"u": record.U,
"d": record.D,
"record_at": record.RecordAt,
"user_id": record.UserID,
"u": record.U,
"d": record.D,
"record_at": record.RecordAt,
"server_rate": 1,
})
}
@@ -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, "/")