fix: 修复收入统计 404,并隔离支付测试/生产配置
Dashboard 收入接口路径对齐;登录页去掉「授权管理平台」;Waffo 测试/生产密钥与 Product 映射分环境保存。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
76bba729f1
commit
9b79eabd34
9 files changed
+391
-80
No files matched your search
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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 配置失败")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user