mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 05:02:17 +00:00
Add plan-level quota reset periods and display/reset cadence in admin/UI Enforce natural reset alignment with background reset task and cleanup job Make subscription pre-consume/refund idempotent with request-scoped records and retries Use database time for consistent resets across multi-instance deployments Harden payment callbacks with locking and idempotent order completion Record subscription purchases in topup history and billing logs Optimize subscription queries and add critical composite indexes
279 lines
7.6 KiB
Go
279 lines
7.6 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"embed"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/constant"
|
|
"github.com/QuantumNous/new-api/controller"
|
|
"github.com/QuantumNous/new-api/logger"
|
|
"github.com/QuantumNous/new-api/middleware"
|
|
"github.com/QuantumNous/new-api/model"
|
|
"github.com/QuantumNous/new-api/router"
|
|
"github.com/QuantumNous/new-api/service"
|
|
_ "github.com/QuantumNous/new-api/setting/performance_setting" // 注册性能设置
|
|
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
|
|
|
"github.com/bytedance/gopkg/util/gopool"
|
|
"github.com/gin-contrib/sessions"
|
|
"github.com/gin-contrib/sessions/cookie"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/joho/godotenv"
|
|
|
|
_ "net/http/pprof"
|
|
)
|
|
|
|
//go:embed web/dist
|
|
var buildFS embed.FS
|
|
|
|
//go:embed web/dist/index.html
|
|
var indexPage []byte
|
|
|
|
func main() {
|
|
startTime := time.Now()
|
|
|
|
err := InitResources()
|
|
if err != nil {
|
|
common.FatalLog("failed to initialize resources: " + err.Error())
|
|
return
|
|
}
|
|
|
|
common.SysLog("New API " + common.Version + " started")
|
|
if os.Getenv("GIN_MODE") != "debug" {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
}
|
|
if common.DebugEnabled {
|
|
common.SysLog("running in debug mode")
|
|
}
|
|
|
|
defer func() {
|
|
err := model.CloseDB()
|
|
if err != nil {
|
|
common.FatalLog("failed to close database: " + err.Error())
|
|
}
|
|
}()
|
|
|
|
if common.RedisEnabled {
|
|
// for compatibility with old versions
|
|
common.MemoryCacheEnabled = true
|
|
}
|
|
if common.MemoryCacheEnabled {
|
|
common.SysLog("memory cache enabled")
|
|
common.SysLog(fmt.Sprintf("sync frequency: %d seconds", common.SyncFrequency))
|
|
|
|
// Add panic recovery and retry for InitChannelCache
|
|
func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
common.SysLog(fmt.Sprintf("InitChannelCache panic: %v, retrying once", r))
|
|
// Retry once
|
|
_, _, fixErr := model.FixAbility()
|
|
if fixErr != nil {
|
|
common.FatalLog(fmt.Sprintf("InitChannelCache failed: %s", fixErr.Error()))
|
|
}
|
|
}
|
|
}()
|
|
model.InitChannelCache()
|
|
}()
|
|
|
|
go model.SyncChannelCache(common.SyncFrequency)
|
|
}
|
|
|
|
// 热更新配置
|
|
go model.SyncOptions(common.SyncFrequency)
|
|
|
|
// 数据看板
|
|
go model.UpdateQuotaData()
|
|
|
|
if os.Getenv("CHANNEL_UPDATE_FREQUENCY") != "" {
|
|
frequency, err := strconv.Atoi(os.Getenv("CHANNEL_UPDATE_FREQUENCY"))
|
|
if err != nil {
|
|
common.FatalLog("failed to parse CHANNEL_UPDATE_FREQUENCY: " + err.Error())
|
|
}
|
|
go controller.AutomaticallyUpdateChannels(frequency)
|
|
}
|
|
|
|
go controller.AutomaticallyTestChannels()
|
|
|
|
// Codex credential auto-refresh check every 10 minutes, refresh when expires within 1 day
|
|
service.StartCodexCredentialAutoRefreshTask()
|
|
|
|
// Subscription quota reset task (daily/weekly/monthly/custom)
|
|
service.StartSubscriptionQuotaResetTask()
|
|
|
|
if common.IsMasterNode && constant.UpdateTask {
|
|
gopool.Go(func() {
|
|
controller.UpdateMidjourneyTaskBulk()
|
|
})
|
|
gopool.Go(func() {
|
|
controller.UpdateTaskBulk()
|
|
})
|
|
}
|
|
if os.Getenv("BATCH_UPDATE_ENABLED") == "true" {
|
|
common.BatchUpdateEnabled = true
|
|
common.SysLog("batch update enabled with interval " + strconv.Itoa(common.BatchUpdateInterval) + "s")
|
|
model.InitBatchUpdater()
|
|
}
|
|
|
|
if os.Getenv("ENABLE_PPROF") == "true" {
|
|
gopool.Go(func() {
|
|
log.Println(http.ListenAndServe("0.0.0.0:8005", nil))
|
|
})
|
|
go common.Monitor()
|
|
common.SysLog("pprof enabled")
|
|
}
|
|
|
|
err = common.StartPyroScope()
|
|
if err != nil {
|
|
common.SysError(fmt.Sprintf("start pyroscope error : %v", err))
|
|
}
|
|
|
|
// Initialize HTTP server
|
|
server := gin.New()
|
|
server.Use(gin.CustomRecovery(func(c *gin.Context, err any) {
|
|
common.SysLog(fmt.Sprintf("panic detected: %v", err))
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": gin.H{
|
|
"message": fmt.Sprintf("Panic detected, error: %v. Please submit a issue here: https://github.com/Calcium-Ion/new-api", err),
|
|
"type": "new_api_panic",
|
|
},
|
|
})
|
|
}))
|
|
// This will cause SSE not to work!!!
|
|
//server.Use(gzip.Gzip(gzip.DefaultCompression))
|
|
server.Use(middleware.RequestId())
|
|
server.Use(middleware.PoweredBy())
|
|
middleware.SetUpLogger(server)
|
|
// Initialize session store
|
|
store := cookie.NewStore([]byte(common.SessionSecret))
|
|
store.Options(sessions.Options{
|
|
Path: "/",
|
|
MaxAge: 2592000, // 30 days
|
|
HttpOnly: true,
|
|
Secure: false,
|
|
SameSite: http.SameSiteStrictMode,
|
|
})
|
|
server.Use(sessions.Sessions("session", store))
|
|
|
|
InjectUmamiAnalytics()
|
|
InjectGoogleAnalytics()
|
|
|
|
// 设置路由
|
|
router.SetRouter(server, buildFS, indexPage)
|
|
var port = os.Getenv("PORT")
|
|
if port == "" {
|
|
port = strconv.Itoa(*common.Port)
|
|
}
|
|
|
|
// Log startup success message
|
|
common.LogStartupSuccess(startTime, port)
|
|
|
|
err = server.Run(":" + port)
|
|
if err != nil {
|
|
common.FatalLog("failed to start HTTP server: " + err.Error())
|
|
}
|
|
}
|
|
|
|
func InjectUmamiAnalytics() {
|
|
analyticsInjectBuilder := &strings.Builder{}
|
|
if os.Getenv("UMAMI_WEBSITE_ID") != "" {
|
|
umamiSiteID := os.Getenv("UMAMI_WEBSITE_ID")
|
|
umamiScriptURL := os.Getenv("UMAMI_SCRIPT_URL")
|
|
if umamiScriptURL == "" {
|
|
umamiScriptURL = "https://analytics.umami.is/script.js"
|
|
}
|
|
analyticsInjectBuilder.WriteString("<script defer src=\"")
|
|
analyticsInjectBuilder.WriteString(umamiScriptURL)
|
|
analyticsInjectBuilder.WriteString("\" data-website-id=\"")
|
|
analyticsInjectBuilder.WriteString(umamiSiteID)
|
|
analyticsInjectBuilder.WriteString("\"></script>")
|
|
}
|
|
analyticsInjectBuilder.WriteString("<!--Umami QuantumNous-->\n")
|
|
analyticsInject := analyticsInjectBuilder.String()
|
|
indexPage = bytes.ReplaceAll(indexPage, []byte("<!--umami-->\n"), []byte(analyticsInject))
|
|
}
|
|
|
|
func InjectGoogleAnalytics() {
|
|
analyticsInjectBuilder := &strings.Builder{}
|
|
if os.Getenv("GOOGLE_ANALYTICS_ID") != "" {
|
|
gaID := os.Getenv("GOOGLE_ANALYTICS_ID")
|
|
// Google Analytics 4 (gtag.js)
|
|
analyticsInjectBuilder.WriteString("<script async src=\"https://www.googletagmanager.com/gtag/js?id=")
|
|
analyticsInjectBuilder.WriteString(gaID)
|
|
analyticsInjectBuilder.WriteString("\"></script>")
|
|
analyticsInjectBuilder.WriteString("<script>")
|
|
analyticsInjectBuilder.WriteString("window.dataLayer = window.dataLayer || [];")
|
|
analyticsInjectBuilder.WriteString("function gtag(){dataLayer.push(arguments);}")
|
|
analyticsInjectBuilder.WriteString("gtag('js', new Date());")
|
|
analyticsInjectBuilder.WriteString("gtag('config', '")
|
|
analyticsInjectBuilder.WriteString(gaID)
|
|
analyticsInjectBuilder.WriteString("');")
|
|
analyticsInjectBuilder.WriteString("</script>")
|
|
}
|
|
analyticsInjectBuilder.WriteString("<!--Google Analytics QuantumNous-->\n")
|
|
analyticsInject := analyticsInjectBuilder.String()
|
|
indexPage = bytes.ReplaceAll(indexPage, []byte("<!--Google Analytics-->\n"), []byte(analyticsInject))
|
|
}
|
|
|
|
func InitResources() error {
|
|
// Initialize resources here if needed
|
|
// This is a placeholder function for future resource initialization
|
|
err := godotenv.Load(".env")
|
|
if err != nil {
|
|
if common.DebugEnabled {
|
|
common.SysLog("No .env file found, using default environment variables. If needed, please create a .env file and set the relevant variables.")
|
|
}
|
|
}
|
|
|
|
// 加载环境变量
|
|
common.InitEnv()
|
|
|
|
logger.SetupLogger()
|
|
|
|
// Initialize model settings
|
|
ratio_setting.InitRatioSettings()
|
|
|
|
service.InitHttpClient()
|
|
|
|
service.InitTokenEncoders()
|
|
|
|
// Initialize SQL Database
|
|
err = model.InitDB()
|
|
if err != nil {
|
|
common.FatalLog("failed to initialize database: " + err.Error())
|
|
return err
|
|
}
|
|
|
|
model.CheckSetup()
|
|
|
|
// Initialize options, should after model.InitDB()
|
|
model.InitOptionMap()
|
|
|
|
// 清理旧的磁盘缓存文件
|
|
common.CleanupOldCacheFiles()
|
|
|
|
// 初始化模型
|
|
model.GetPricing()
|
|
|
|
// Initialize SQL Database
|
|
err = model.InitLogDB()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Initialize Redis
|
|
err = common.InitRedisClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|