Files
Logan.Li 83a3630404
CI / backend (push) Canceled after 0s
feat(settings): 运行配置中心——.env 业务参数统一到后台页面维护
- 新增 app_configs 表与 appconfig 注册表:读取优先级 页面 → .env → 默认,删除即回退
- 页面可配 7 项:支付回调基址、成功回跳、Webhook 密钥(加密入库)、未验签放行开关、CORS、可信代理、GeoIP 路径
- 引导密钥(JWT_SECRET / OPSLIC_MASTER_KEY / OFFLINE_LICENSE_SEED)与部署参数只读展示掩码+指纹,永不入库
- 校验:URL 格式、密钥长度≥16、CIDR、release 下禁止开启未验签回调;变更写审计日志
- 消费点改造:支付回调地址、Waffo 回跳、webhook 验签、可信代理、CORS、GeoIP 统一走配置中心
- 前端新增「审计与设置 → 运行配置」页(分组表格 + 来源标记 + 编辑弹窗 + 恢复默认)
- .env.example 补注释:哪些必须留在 .env、哪些已收敛到页面
2026-11-01 06:17:58 +08:00

314 lines
13 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package router
import (
"backend/config"
"backend/internal/controller/analytics"
appconfigctl "backend/internal/controller/appconfig"
"backend/internal/controller/auth"
"backend/internal/controller/customer"
"backend/internal/controller/entitlement"
"backend/internal/controller/license"
"backend/internal/controller/order"
paymentctl "backend/internal/controller/payment"
"backend/internal/controller/plan"
"backend/internal/controller/product"
"backend/internal/controller/setting"
synccontroller "backend/internal/controller/sync"
"backend/internal/service/appconfig"
"backend/middleware"
"strings"
"github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// register 同时注册带和不带 trailing slash 的路由 // Logan.Li
func register(r gin.IRoutes, method, relativePath string, handlers ...gin.HandlerFunc) {
isParamRoute := strings.Contains(relativePath, ":")
switch method {
case "GET":
r.GET(relativePath, handlers...)
if !isParamRoute {
r.GET(relativePath+"/", handlers...)
}
case "POST":
r.POST(relativePath, handlers...)
if !isParamRoute {
r.POST(relativePath+"/", handlers...)
}
case "PUT":
r.PUT(relativePath, handlers...)
if !isParamRoute {
r.PUT(relativePath+"/", handlers...)
}
case "DELETE":
r.DELETE(relativePath, handlers...)
if !isParamRoute {
r.DELETE(relativePath+"/", handlers...)
}
case "PATCH":
r.PATCH(relativePath, handlers...)
if !isParamRoute {
r.PATCH(relativePath+"/", handlers...)
}
}
}
// SetupRouter 初始化路由 // Logan.Li
func SetupRouter(db *gorm.DB, cfg *config.Config) *gin.Engine {
gin.SetMode(cfg.Mode)
r := gin.Default()
r.RedirectTrailingSlash = false
r.RedirectFixedPath = false
// 信任代理:默认不信任任何代理头(X-Forwarded-For 可伪造,会绕过按 IP 限流);
// 经 nginx 反代的节点用「可信反代网段」指定其内网 CIDR(后台可配,重启生效) // Logan.Li
if proxies := appconfig.List("TRUSTED_PROXIES"); len(proxies) > 0 {
if err := r.SetTrustedProxies(proxies); err != nil {
panic("TRUSTED_PROXIES 配置非法: " + err.Error())
}
} else if err := r.SetTrustedProxies(nil); err != nil {
panic("SetTrustedProxies 失败: " + err.Error())
}
// Gzip 压缩中间件 // Logan.Li
r.Use(gzip.Gzip(gzip.DefaultCompression))
// CORS 中间件 // Logan.Li
r.Use(middleware.CORSMiddleware(cfg))
// 存活探针(不查库) // Logan.Li
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"code": 0, "message": "ok", "data": nil})
})
r.GET("/healthz", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "alive"})
})
// 就绪探针:检查数据库可达 // Logan.Li
r.GET("/readyz", func(c *gin.Context) {
sqlDB, err := db.DB()
if err != nil || sqlDB.Ping() != nil {
c.JSON(503, gin.H{"status": "db unreachable"})
return
}
c.JSON(200, gin.H{"status": "ready"})
})
// 初始化控制器 // Logan.Li
authController := auth.NewAuthController(db, cfg)
productController := product.NewProductController(db)
customerController := customer.NewCustomerController(db)
licenseController := license.NewLicenseController(db, cfg)
planController := plan.NewPlanController(db)
orderController := order.NewOrderController(db)
entitlementController := entitlement.NewEntitlementController(db)
analyticsController := analytics.NewAnalyticsController(db)
settingController := setting.NewSettingController(db)
syncController := synccontroller.NewController(db, cfg)
paymentConfigController := paymentctl.NewPaymentConfigController(db, cfg)
appConfigController := appconfigctl.NewController(db)
api := r.Group("/api")
{
// ============ 节点互联(对端调用,HMAC 签名认证,非 JWT) ============
syncGroup := api.Group("/v1/sync")
{
syncGroup.POST("/handshake", middleware.RateLimit(1, 30), syncController.HandshakeIn)
syncGroup.POST("/exchange", middleware.RateLimit(2, 60), syncController.ExchangeIn)
}
// ============ 认证路由(无需 JWT) ============
authGroup := api.Group("/auth")
{
register(authGroup, "POST", "/login", middleware.RateLimit(0.2, 5), authController.Login)
}
// ============ 客户端授权契约(无需认证,供外部工具调用) ============
clientAPI := api.Group("/v1")
{
licLimit := middleware.RateLimit(2, 30)
checkoutLimit := middleware.RateLimit(0.5, 10)
webhookLimit := middleware.RateLimit(1, 20)
// License 验证 / 激活 / 停用 / 心跳
register(clientAPI, "POST", "/license/verify", licLimit, licenseController.Verify)
register(clientAPI, "POST", "/license/activate", licLimit, licenseController.ActivateDevice)
register(clientAPI, "POST", "/license/deactivate", licLimit, licenseController.Deactivate)
register(clientAPI, "POST", "/license/heartbeat", licLimit, licenseController.Heartbeat)
register(clientAPI, "POST", "/license/devices", licLimit, licenseController.ListDevices)
// 产品定价查询(展示用)
register(clientAPI, "GET", "/products/code/:code", productController.GetByCode)
clientAPI.GET("/products/:product_id/plans", planController.GetByProduct)
// 订单与支付(客户端购买入口 + 支付回调)
register(clientAPI, "POST", "/orders", checkoutLimit, orderController.CreateOrder)
register(clientAPI, "POST", "/checkout", checkoutLimit, orderController.Checkout)
register(clientAPI, "POST", "/purchase/claim", checkoutLimit, orderController.ClaimPurchase)
register(clientAPI, "POST", "/webhooks/payment", webhookLimit, orderController.PaymentWebhook)
register(clientAPI, "GET", "/webhooks/waffo", orderController.WaffoWebhookReady)
register(clientAPI, "POST", "/webhooks/waffo", webhookLimit, orderController.WaffoWebhook)
register(clientAPI, "GET", "/purchase/success", orderController.PurchaseSuccess)
register(clientAPI, "POST", "/purchase/check", checkoutLimit, orderController.CheckDuplicatePurchase)
register(clientAPI, "POST", "/purchase/status", checkoutLimit, orderController.CheckPurchaseStatus)
// 客户权益查询(通过邮箱)
register(clientAPI, "GET", "/entitlements/my", checkoutLimit, entitlementController.GetByEmail)
}
// ============ 需要认证的管理路由 ============
protected := api.Group("", middleware.AuthMiddleware(cfg, db))
{
// 认证相关
register(protected, "GET", "/auth/me", authController.Me)
register(protected, "PUT", "/auth/password", authController.ChangePassword)
// 产品管理
products := protected.Group("/products")
{
register(products, "GET", "", productController.List)
register(products, "GET", "/all", productController.GetAll)
register(products, "POST", "", productController.Create)
register(products, "GET", "/:id", productController.Get)
register(products, "PUT", "/:id", productController.Update)
register(products, "DELETE", "/:id", productController.Delete)
register(products, "POST", "/batch-delete", productController.BatchDelete)
register(products, "PATCH", "/:id/status", productController.UpdateStatus)
}
// 客户管理
customers := protected.Group("/customers")
{
register(customers, "GET", "", customerController.List)
register(customers, "GET", "/all", customerController.GetAll)
register(customers, "POST", "", customerController.Create)
register(customers, "GET", "/:id", customerController.Get)
register(customers, "PUT", "/:id", customerController.Update)
register(customers, "DELETE", "/:id", customerController.Delete)
register(customers, "POST", "/batch-delete", customerController.BatchDelete)
register(customers, "PATCH", "/:id/status", customerController.UpdateStatus)
}
// License 管理
licenses := protected.Group("/licenses")
{
register(licenses, "GET", "", licenseController.List)
register(licenses, "POST", "", licenseController.Create)
register(licenses, "POST", "/offline", licenseController.IssueOffline)
register(licenses, "GET", "/offline/public-key", licenseController.OfflinePublicKey)
register(licenses, "GET", "/export", licenseController.Export)
register(licenses, "POST", "/batch-delete", licenseController.BatchDelete)
register(licenses, "GET", "/:id", licenseController.Get)
register(licenses, "PUT", "/:id", licenseController.Update)
register(licenses, "DELETE", "/:id", licenseController.Delete)
register(licenses, "POST", "/:id/renew", licenseController.Renew)
register(licenses, "POST", "/:id/revoke", licenseController.Revoke)
register(licenses, "POST", "/:id/suspend", licenseController.Suspend)
register(licenses, "POST", "/:id/activate", licenseController.Activate)
register(licenses, "GET", "/:id/activations", licenseController.GetActivations)
register(licenses, "POST", "/:id/activations/:activationId/offline", licenseController.ForceOffline)
}
// 定价计划管理
plans := protected.Group("/plans")
{
register(plans, "GET", "", planController.List)
register(plans, "POST", "", planController.Create)
register(plans, "PUT", "/:id", planController.Update)
register(plans, "DELETE", "/:id", planController.Delete)
register(plans, "PATCH", "/:id/status", planController.ToggleStatus)
}
// 订单管理
orders := protected.Group("/orders")
{
register(orders, "GET", "", orderController.List)
register(orders, "GET", "/:id", orderController.GetDetail)
register(orders, "POST", "/manual-grant", orderController.ManualGrant)
register(orders, "POST", "/batch-delete", orderController.BatchDelete)
}
// 客户权益管理
entitlements := protected.Group("/entitlements")
{
register(entitlements, "GET", "", entitlementController.List)
register(entitlements, "GET", "/:id", entitlementController.Get)
register(entitlements, "POST", "/:id/revoke", entitlementController.Revoke)
register(entitlements, "POST", "/:id/renew", entitlementController.Renew)
}
// 仪表板与数据分析
dashboard := protected.Group("/dashboard")
{
register(dashboard, "GET", "/stats", analyticsController.GetDashboardStats)
register(dashboard, "GET", "/expiring", analyticsController.GetExpiringLicenses)
register(dashboard, "GET", "/activities", analyticsController.GetRecentActivities)
register(dashboard, "GET", "/online-stats", analyticsController.GetOnlineStats)
register(dashboard, "GET", "/revenue", analyticsController.GetRevenueStats)
}
analyticsGroup := protected.Group("/analytics")
{
register(analyticsGroup, "GET", "/license-growth", analyticsController.GetLicenseGrowth)
register(analyticsGroup, "GET", "/license-growth-detail", analyticsController.GetLicenseGrowthDetail)
register(analyticsGroup, "GET", "/product-distribution", analyticsController.GetProductDistribution)
register(analyticsGroup, "GET", "/revenue", analyticsController.GetRevenueStats)
register(analyticsGroup, "GET", "/geo-distribution", analyticsController.GetGeoDistribution)
register(analyticsGroup, "GET", "/platform-distribution", analyticsController.GetPlatformDistribution)
}
// 日志审计与系统设置
logs := protected.Group("/logs")
{
register(logs, "GET", "", settingController.ListAuditLogs)
register(logs, "DELETE", "/clear", settingController.ClearAuditLogs)
}
system := protected.Group("/settings")
{
register(system, "GET", "", settingController.Get)
register(system, "PUT", "", settingController.Update)
register(system, "GET", "/waffo", settingController.GetWaffo)
register(system, "PUT", "/waffo", settingController.UpdateWaffo)
register(system, "POST", "/waffo/test", settingController.DebugWaffo)
// 运行配置中心(统一在页面维护 .env 里的业务参数) // Logan.Li
register(system, "GET", "/runtime", appConfigController.Overview)
register(system, "PUT", "/runtime", appConfigController.Update)
register(system, "POST", "/runtime/reset", appConfigController.Reset)
}
// 节点互联管理(双向授权同步) // Logan.Li
syncAdmin := protected.Group("/sync")
{
register(syncAdmin, "GET", "/peers", syncController.ListPeers)
register(syncAdmin, "POST", "/peers", syncController.SavePeer)
register(syncAdmin, "DELETE", "/peers/:id", syncController.DeletePeer)
register(syncAdmin, "POST", "/peers/:id/handshake", syncController.TestHandshake)
register(syncAdmin, "POST", "/peers/:id/sync", syncController.SyncNow)
register(syncAdmin, "GET", "/generate-key", syncController.GenerateKey)
}
// 支付调试(与 settings/waffo/test 同一 handler) // Logan.Li
paymentGroup := protected.Group("/payment")
{
register(paymentGroup, "POST", "/waffo/debug", settingController.DebugWaffo)
}
// 支付配置(微信支付 / 支付宝,仅管理员) // Logan.Li
paymentConfig := protected.Group("/admin/payment")
{
register(paymentConfig, "GET", "/config", paymentConfigController.GetConfig)
register(paymentConfig, "PUT", "/config", paymentConfigController.UpdateConfig)
register(paymentConfig, "POST", "/upload", paymentConfigController.Upload)
register(paymentConfig, "POST", "/test", paymentConfigController.Test)
register(paymentConfig, "POST", "/wechat/platform-certs", paymentConfigController.FetchWechatPlatformCerts)
}
}
}
return r
}