From 9b79eabd342e695734ac6ade5a96f0bb3390ddd8 Mon Sep 17 00:00:00 2001 From: "Logan.Li" Date: Wed, 12 Aug 2026 22:37:22 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=94=B6=E5=85=A5?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1=20404=EF=BC=8C=E5=B9=B6=E9=9A=94=E7=A6=BB?= =?UTF-8?q?=E6=94=AF=E4=BB=98=E6=B5=8B=E8=AF=95/=E7=94=9F=E4=BA=A7?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dashboard 收入接口路径对齐;登录页去掉「授权管理平台」;Waffo 测试/生产密钥与 Product 映射分环境保存。 Co-authored-by: Cursor --- backend/internal/dto/analytics.go | 11 ++ backend/internal/service/payment/waffo.go | 34 +++- backend/internal/service/payment/waffo_env.go | 132 ++++++++++++++++ .../service/setting/setting_service.go | 115 ++++++++++---- backend/router/router.go | 1 + frontend/index.html | 2 +- frontend/src/api/analytics.js | 2 +- frontend/src/views/Login.vue | 27 ++-- frontend/src/views/Payments.vue | 147 ++++++++++++++---- 9 files changed, 391 insertions(+), 80 deletions(-) create mode 100644 backend/internal/service/payment/waffo_env.go diff --git a/backend/internal/dto/analytics.go b/backend/internal/dto/analytics.go index 1dfdfad..19211e5 100644 --- a/backend/internal/dto/analytics.go +++ b/backend/internal/dto/analytics.go @@ -108,9 +108,20 @@ type WaffoSettingsResponse struct { SuccessURL string `json:"success_url"` WebhookURL string `json:"webhook_url"` PrivateKeyConfigured bool `json:"private_key_configured"` + PrivateKeyByEnv map[string]bool `json:"private_key_configured_by_env"` + Profiles map[string]WaffoEnvUI `json:"profiles"` // test/live 隔离的映射预览 // Logan.Li SourceHint string `json:"source_hint"` // db | env | mixed } +// WaffoEnvUI 单环境前端预览(无私钥) // Logan.Li +type WaffoEnvUI struct { + ProductMappings []WaffoProductMapping `json:"product_mappings"` + ProductID string `json:"product_id"` + ProductIDYearly string `json:"product_id_yearly"` + ProductIDPerpetual string `json:"product_id_perpetual"` + PrivateKeyConfigured bool `json:"private_key_configured"` +} + // WaffoSettingsUpdate 管理端更新 Waffo 配置;private_key 空=保留原值 // Logan.Li type WaffoSettingsUpdate struct { Enabled *bool `json:"enabled"` diff --git a/backend/internal/service/payment/waffo.go b/backend/internal/service/payment/waffo.go index 374ae2b..9583069 100644 --- a/backend/internal/service/payment/waffo.go +++ b/backend/internal/service/payment/waffo.go @@ -49,6 +49,7 @@ type WaffoPublicMeta struct { ProductIDPerpetual string `json:"product_id_perpetual"` ProductIDs map[string]string `json:"product_ids,omitempty"` // 可扩展映射,兼容旧字段 // Logan.Li ProductMappings []WaffoProductMapping `json:"product_mappings,omitempty"` // 前端表格(含 label) // Logan.Li + EnvProfiles map[string]WaffoEnvProfile `json:"env_profiles,omitempty"` // test/live 隔离映射 // Logan.Li SuccessURL string `json:"success_url"` Currency string `json:"currency"` CurrencyYearly string `json:"currency_yearly"` @@ -193,11 +194,40 @@ func mergeWaffoFromDB(db *gorm.DB, cfg *WaffoConfig) { } else { cfg.Environment = "live" } + cfg.Environment = NormalizeWaffoEnvironment(cfg.Environment) + meta.ApplyEnvProfile(cfg.Environment) + // ApplyEnvProfile 后同步产品字段到 cfg // Logan.Li + if meta.ProductID != "" { + cfg.ProductID = meta.ProductID + } + if meta.ProductIDYearly != "" { + cfg.ProductIDYearly = meta.ProductIDYearly + } + if meta.ProductIDPerpetual != "" { + cfg.ProductIDPerpetual = meta.ProductIDPerpetual + } + if len(meta.ProductIDs) > 0 { + cfg.ProductIDs = cloneStringMap(meta.ProductIDs) + } + if len(meta.ProductMappings) > 0 { + if cfg.ProductIDs == nil { + cfg.ProductIDs = map[string]string{} + } + for _, m := range meta.ProductMappings { + k := strings.TrimSpace(strings.ToLower(m.PlanKey)) + v := strings.TrimSpace(m.ProductID) + if k == "" || v == "" { + continue + } + cfg.ProductIDs[k] = v + } + } if strings.TrimSpace(gw.Currency) != "" && cfg.Currency == "" { cfg.Currency = gw.Currency } - if strings.TrimSpace(gw.SecretKey) != "" { - cfg.PrivateKey = strings.TrimSpace(gw.SecretKey) + bag := ParseWaffoSecretBag(gw.SecretKey, cfg.Environment) + if pk := bag.SecretForEnv(cfg.Environment); pk != "" { + cfg.PrivateKey = pk } } diff --git a/backend/internal/service/payment/waffo_env.go b/backend/internal/service/payment/waffo_env.go new file mode 100644 index 0000000..ddfba9a --- /dev/null +++ b/backend/internal/service/payment/waffo_env.go @@ -0,0 +1,132 @@ +package payment + +import ( + "encoding/json" + "strings" +) + +// WaffoEnvProfile 单环境下的 Product 映射(测试/生产隔离) // Logan.Li +type WaffoEnvProfile struct { + ProductID string `json:"product_id,omitempty"` + ProductIDYearly string `json:"product_id_yearly,omitempty"` + ProductIDPerpetual string `json:"product_id_perpetual,omitempty"` + ProductIDs map[string]string `json:"product_ids,omitempty"` + ProductMappings []WaffoProductMapping `json:"product_mappings,omitempty"` +} + +// WaffoSecretBag 测试/生产私钥分存(写入 SecretKey 字段的 JSON) // Logan.Li +type WaffoSecretBag struct { + Test string `json:"test,omitempty"` + Live string `json:"live,omitempty"` +} + +// NormalizeWaffoEnvironment 归一化环境名 // Logan.Li +func NormalizeWaffoEnvironment(env string) string { + switch strings.ToLower(strings.TrimSpace(env)) { + case "live", "prod", "production": + return "live" + default: + return "test" + } +} + +// ParseWaffoSecretBag 解析 SecretKey:JSON 双钥或旧版单 PEM // Logan.Li +func ParseWaffoSecretBag(raw, fallbackEnv string) WaffoSecretBag { + raw = strings.TrimSpace(raw) + if raw == "" { + return WaffoSecretBag{} + } + if strings.HasPrefix(raw, "{") { + var bag WaffoSecretBag + if err := json.Unmarshal([]byte(raw), &bag); err == nil { + return bag + } + } + env := NormalizeWaffoEnvironment(fallbackEnv) + if env == "live" { + return WaffoSecretBag{Live: raw} + } + return WaffoSecretBag{Test: raw} +} + +// EncodeWaffoSecretBag 序列化双环境私钥 // Logan.Li +func EncodeWaffoSecretBag(bag WaffoSecretBag) (string, error) { + bag.Test = strings.TrimSpace(bag.Test) + bag.Live = strings.TrimSpace(bag.Live) + b, err := json.Marshal(bag) + if err != nil { + return "", err + } + return string(b), nil +} + +// SecretForEnv 取指定环境私钥 // Logan.Li +func (b WaffoSecretBag) SecretForEnv(env string) string { + if NormalizeWaffoEnvironment(env) == "live" { + return strings.TrimSpace(b.Live) + } + return strings.TrimSpace(b.Test) +} + +// SetSecretForEnv 写入指定环境私钥 // Logan.Li +func (b *WaffoSecretBag) SetSecretForEnv(env, secret string) { + secret = strings.TrimSpace(secret) + if NormalizeWaffoEnvironment(env) == "live" { + b.Live = secret + return + } + b.Test = secret +} + +// ClearSecretForEnv 清除指定环境私钥 // Logan.Li +func (b *WaffoSecretBag) ClearSecretForEnv(env string) { + b.SetSecretForEnv(env, "") +} + +// ApplyEnvProfile 将当前环境的 Profile 叠到顶层字段供运行时使用 // Logan.Li +func (m *WaffoPublicMeta) ApplyEnvProfile(env string) { + if m == nil { + return + } + env = NormalizeWaffoEnvironment(env) + if m.EnvProfiles == nil { + return + } + p, ok := m.EnvProfiles[env] + if !ok { + return + } + if len(p.ProductMappings) > 0 { + m.ProductMappings = p.ProductMappings + } + if len(p.ProductIDs) > 0 { + m.ProductIDs = p.ProductIDs + } + if p.ProductID != "" { + m.ProductID = p.ProductID + } + if p.ProductIDYearly != "" { + m.ProductIDYearly = p.ProductIDYearly + } + if p.ProductIDPerpetual != "" { + m.ProductIDPerpetual = p.ProductIDPerpetual + } +} + +// CaptureEnvProfile 把顶层映射写入指定环境 Profile // Logan.Li +func (m *WaffoPublicMeta) CaptureEnvProfile(env string) { + if m == nil { + return + } + env = NormalizeWaffoEnvironment(env) + if m.EnvProfiles == nil { + m.EnvProfiles = map[string]WaffoEnvProfile{} + } + m.EnvProfiles[env] = WaffoEnvProfile{ + ProductID: m.ProductID, + ProductIDYearly: m.ProductIDYearly, + ProductIDPerpetual: m.ProductIDPerpetual, + ProductIDs: m.ProductIDs, + ProductMappings: m.ProductMappings, + } +} diff --git a/backend/internal/service/setting/setting_service.go b/backend/internal/service/setting/setting_service.go index e086bf9..02d3698 100644 --- a/backend/internal/service/setting/setting_service.go +++ b/backend/internal/service/setting/setting_service.go @@ -86,29 +86,73 @@ func (s *SettingService) GetWaffo() (*dto.WaffoSettingsResponse, error) { } meta := payment.ParseWaffoPublicMeta(gw.PublicKey) + env := payment.NormalizeWaffoEnvironment(cfg.Environment) + meta.ApplyEnvProfile(env) mappings := buildProductMappings(cfg, meta.ProductMappings) productIDs := cloneProductIDs(cfg.ProductIDs) + bag := payment.ParseWaffoSecretBag(gw.SecretKey, env) + + profiles := map[string]dto.WaffoEnvUI{ + "test": buildEnvUI(meta, bag, "test"), + "live": buildEnvUI(meta, bag, "live"), + } return &dto.WaffoSettingsResponse{ Enabled: enabled, MerchantID: cfg.MerchantID, StoreID: cfg.StoreID, StoreSlug: cfg.StoreSlug, - Environment: cfg.Environment, - SandboxMode: cfg.Environment == "test", + Environment: env, + SandboxMode: env == "test", Currency: cfg.Currency, - ProductID: cfg.ProductID, - ProductIDYearly: cfg.ProductIDYearly, - ProductIDPerpetual: cfg.ProductIDPerpetual, + ProductID: firstNonEmpty(meta.ProductID, cfg.ProductID), + ProductIDYearly: firstNonEmpty(meta.ProductIDYearly, cfg.ProductIDYearly), + ProductIDPerpetual: firstNonEmpty(meta.ProductIDPerpetual, cfg.ProductIDPerpetual), ProductIDs: productIDs, ProductMappings: mappings, SuccessURL: cfg.SuccessURL, WebhookURL: payment.DefaultWebhookURL, - PrivateKeyConfigured: strings.TrimSpace(cfg.PrivateKey) != "", - SourceHint: hint, + PrivateKeyConfigured: bag.SecretForEnv(env) != "", + PrivateKeyByEnv: map[string]bool{ + "test": bag.SecretForEnv("test") != "", + "live": bag.SecretForEnv("live") != "", + }, + Profiles: profiles, + SourceHint: hint, }, nil } +// buildEnvUI 组装单环境预览 // Logan.Li +func buildEnvUI(meta payment.WaffoPublicMeta, bag payment.WaffoSecretBag, env string) dto.WaffoEnvUI { + cp := meta + cp.ApplyEnvProfile(env) + cfgLike := payment.WaffoConfig{ + ProductID: cp.ProductID, + ProductIDYearly: cp.ProductIDYearly, + ProductIDPerpetual: cp.ProductIDPerpetual, + ProductIDs: cloneProductIDs(cp.ProductIDs), + } + if len(cp.ProductMappings) > 0 { + if cfgLike.ProductIDs == nil { + cfgLike.ProductIDs = map[string]string{} + } + for _, m := range cp.ProductMappings { + k := strings.TrimSpace(strings.ToLower(m.PlanKey)) + v := strings.TrimSpace(m.ProductID) + if k != "" && v != "" { + cfgLike.ProductIDs[k] = v + } + } + } + return dto.WaffoEnvUI{ + ProductMappings: buildProductMappings(cfgLike, cp.ProductMappings), + ProductID: cp.ProductID, + ProductIDYearly: cp.ProductIDYearly, + ProductIDPerpetual: cp.ProductIDPerpetual, + PrivateKeyConfigured: bag.SecretForEnv(env) != "", + } +} + // UpdateWaffo 保存 Waffo 配置到 payment_gateway_configs(后台优先于 .env) // Logan.Li func (s *SettingService) UpdateWaffo(req dto.WaffoSettingsUpdate) (*dto.WaffoSettingsResponse, error) { var gw model.PaymentGatewayConfig @@ -123,25 +167,35 @@ func (s *SettingService) UpdateWaffo(req dto.WaffoSettingsUpdate) (*dto.WaffoSet } } - productIDs, mappings := mergeProductIDInputs(req) - - meta := payment.WaffoPublicMeta{ - MerchantID: strings.TrimSpace(req.MerchantID), - StoreID: strings.TrimSpace(req.StoreID), - StoreSlug: strings.TrimSpace(req.StoreSlug), - ProductID: strings.TrimSpace(req.ProductID), - ProductIDYearly: strings.TrimSpace(req.ProductIDYearly), - ProductIDPerpetual: strings.TrimSpace(req.ProductIDPerpetual), - ProductIDs: productIDs, - ProductMappings: toPaymentMappings(mappings), - SuccessURL: strings.TrimSpace(req.SuccessURL), - Currency: strings.TrimSpace(req.Currency), + oldMeta := payment.ParseWaffoPublicMeta(gw.PublicKey) + oldEnv := payment.NormalizeWaffoEnvironment(oldMeta.Environment) + if oldEnv == "test" && !gw.SandboxMode { + oldEnv = "live" } + + productIDs, mappings := mergeProductIDInputs(req) + newEnv := payment.NormalizeWaffoEnvironment(req.Environment) + + meta := oldMeta + if meta.EnvProfiles == nil { + meta.EnvProfiles = map[string]payment.WaffoEnvProfile{} + } + meta.MerchantID = strings.TrimSpace(req.MerchantID) + meta.StoreID = strings.TrimSpace(req.StoreID) + meta.StoreSlug = strings.TrimSpace(req.StoreSlug) + meta.SuccessURL = strings.TrimSpace(req.SuccessURL) + meta.Currency = strings.TrimSpace(req.Currency) if meta.Currency == "" { meta.Currency = "CNY" } meta.CurrencyYearly = meta.Currency meta.CurrencyPerpetual = meta.Currency + meta.Environment = newEnv + meta.ProductID = strings.TrimSpace(req.ProductID) + meta.ProductIDYearly = strings.TrimSpace(req.ProductIDYearly) + meta.ProductIDPerpetual = strings.TrimSpace(req.ProductIDPerpetual) + meta.ProductIDs = productIDs + meta.ProductMappings = toPaymentMappings(mappings) // 兼容字段与可扩展映射对齐 // Logan.Li if meta.ProductIDYearly == "" { @@ -154,13 +208,12 @@ func (s *SettingService) UpdateWaffo(req dto.WaffoSettingsUpdate) (*dto.WaffoSet meta.ProductID = firstNonEmpty(meta.ProductIDPerpetual, meta.ProductIDYearly, productIDs["default"]) } - env := strings.ToLower(strings.TrimSpace(req.Environment)) - switch env { - case "live", "prod", "production": - meta.Environment = "live" + // 当前环境映射写入 Profile,保留另一环境配置 // Logan.Li + meta.CaptureEnvProfile(newEnv) + + if newEnv == "live" { gw.SandboxMode = false - default: - meta.Environment = "test" + } else { gw.SandboxMode = true } @@ -177,14 +230,20 @@ func (s *SettingService) UpdateWaffo(req dto.WaffoSettingsUpdate) (*dto.WaffoSet gw.Enabled = true } + bag := payment.ParseWaffoSecretBag(gw.SecretKey, oldEnv) if req.ClearPrivateKey { - gw.SecretKey = "" + bag.ClearSecretForEnv(newEnv) } else if pk := strings.TrimSpace(req.PrivateKey); pk != "" { if _, err := payment.ParseMerchantPrivateKey(pk); err != nil { return nil, errors.New("私钥无法解析: " + err.Error()) } - gw.SecretKey = pk + bag.SetSecretForEnv(newEnv, pk) } + encodedSecret, err := payment.EncodeWaffoSecretBag(bag) + if err != nil { + return nil, errors.New("保存私钥失败") + } + gw.SecretKey = encodedSecret if err := s.db.Save(&gw).Error; err != nil { return nil, errors.New("保存 Waffo 配置失败") diff --git a/backend/router/router.go b/backend/router/router.go index 99da68b..5ff9aed 100644 --- a/backend/router/router.go +++ b/backend/router/router.go @@ -228,6 +228,7 @@ func SetupRouter(db *gorm.DB, cfg *config.Config) *gin.Engine { 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) } diff --git a/frontend/index.html b/frontend/index.html index 4965a42..52fa257 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6,7 +6,7 @@ - opsx-licence 授权管理平台 + opsx-licence
diff --git a/frontend/src/api/analytics.js b/frontend/src/api/analytics.js index 9a331b4..4034ebc 100644 --- a/frontend/src/api/analytics.js +++ b/frontend/src/api/analytics.js @@ -31,7 +31,7 @@ export function getProductDistribution() { } export function getRevenueAnalytics() { - return request({ url: '/analytics/revenue', method: 'get' }) + return request({ url: '/dashboard/revenue', method: 'get' }) } export function getGeoDistribution() { diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue index 4f22cf8..8258855 100644 --- a/frontend/src/views/Login.vue +++ b/frontend/src/views/Login.vue @@ -2,9 +2,7 @@