diff --git a/.env.example b/.env.example index 42e5e37..1076e28 100644 --- a/.env.example +++ b/.env.example @@ -42,14 +42,16 @@ DB_NAME=db.sqlite3 # Waffo 支付(OpsXShell 收银台 · pancake.waffo.ai) # ============================================ # 店铺: https://pancake.waffo.ai/store/opsxshell-pwsi6m4n -# 包年一次性: PROD_1BIezCi932IbgCzRIV8hcJ (type=onetime) -# 永久买断: PROD_0pcnzzT4nalEkkcdyIUoD5 (type=onetime) +# 包年一次性: PROD_1BIezCi932IbgCzRIV8hcJ (type=onetime, currency=CNY) +# 永久买断: PROD_0pcnzzT4nalEkkcdyIUoD5 (type=onetime, currency=USD) # 勿设置单一 WAFFO_CHECKOUT_URL,否则无法按档位分流商品 WAFFO_STORE_SLUG=opsxshell-pwsi6m4n WAFFO_PRODUCT_ID=PROD_0pcnzzT4nalEkkcdyIUoD5 WAFFO_PRODUCT_ID_YEARLY=PROD_1BIezCi932IbgCzRIV8hcJ WAFFO_PRODUCT_ID_PERPETUAL=PROD_0pcnzzT4nalEkkcdyIUoD5 WAFFO_CURRENCY=CNY +WAFFO_CURRENCY_YEARLY=CNY +WAFFO_CURRENCY_PERPETUAL=USD # test | live(test 时自动附加 test=true) WAFFO_ENVIRONMENT=test # 支付成功回跳(桌面端 deep link;CreateOrder/Checkout 的 return_url 可覆盖) diff --git a/backend/internal/service/payment/waffo.go b/backend/internal/service/payment/waffo.go index 8d462d0..4840979 100644 --- a/backend/internal/service/payment/waffo.go +++ b/backend/internal/service/payment/waffo.go @@ -9,14 +9,16 @@ import ( // WaffoConfig hosted checkout settings for pancake.waffo.ai // Logan.Li type WaffoConfig struct { - CheckoutURL string - StoreSlug string - ProductID string - ProductIDYearly string - ProductIDPerpetual string - Currency string - Environment string - SuccessURL string + CheckoutURL string + StoreSlug string + ProductID string + ProductIDYearly string + ProductIDPerpetual string + Currency string // fallback / default + CurrencyYearly string + CurrencyPerpetual string + Environment string + SuccessURL string } // LoadWaffoConfig reads WAFFO_* env vars (same names as OpsXShell legacy license-server). // Logan.Li @@ -33,6 +35,19 @@ func LoadWaffoConfig() WaffoConfig { if currency == "" { currency = "CNY" } + currencyYearly := strings.TrimSpace(os.Getenv("WAFFO_CURRENCY_YEARLY")) + if currencyYearly == "" { + currencyYearly = currency + } + currencyPerpetual := strings.TrimSpace(os.Getenv("WAFFO_CURRENCY_PERPETUAL")) + if currencyPerpetual == "" { + // 永久默认 USD(与 pancake 商品一致);可用环境变量覆盖。 // Logan.Li + if currency == "CNY" { + currencyPerpetual = "USD" + } else { + currencyPerpetual = currency + } + } defaultProduct := strings.TrimSpace(os.Getenv("WAFFO_PRODUCT_ID")) yearly := strings.TrimSpace(os.Getenv("WAFFO_PRODUCT_ID_YEARLY")) perpetual := strings.TrimSpace(os.Getenv("WAFFO_PRODUCT_ID_PERPETUAL")) @@ -49,6 +64,8 @@ func LoadWaffoConfig() WaffoConfig { ProductIDYearly: yearly, ProductIDPerpetual: perpetual, Currency: currency, + CurrencyYearly: currencyYearly, + CurrencyPerpetual: currencyPerpetual, Environment: env, SuccessURL: success, } @@ -103,6 +120,23 @@ func productIDForPlan(cfg WaffoConfig, planType string) string { } } +func currencyForPlan(cfg WaffoConfig, planType string) string { + switch strings.TrimSpace(planType) { + case "yearly", "monthly": + if cfg.CurrencyYearly != "" { + return cfg.CurrencyYearly + } + case "perpetual", "lifetime": + if cfg.CurrencyPerpetual != "" { + return cfg.CurrencyPerpetual + } + } + if cfg.Currency != "" { + return cfg.Currency + } + return "CNY" +} + func checkoutTypeForPlan(planType string) string { // OpsXShell 包年 / 永久均为 Waffo onetime 商品(包年不再走 subscription)。 // Logan.Li _ = planType @@ -110,7 +144,6 @@ func checkoutTypeForPlan(planType string) string { } func resolveBaseCheckoutURL(cfg WaffoConfig, planType string) (*url.URL, error) { - // Single WAFFO_CHECKOUT_URL only as absolute fallback when no store/products configured. // Prefer store + per-plan product IDs so yearly/perpetual map to different Waffo SKUs. productID := productIDForPlan(cfg, planType) if cfg.StoreSlug != "" && productID != "" { @@ -121,7 +154,7 @@ func resolveBaseCheckoutURL(cfg WaffoConfig, planType string) (*url.URL, error) } q := u.Query() q.Set("type", checkoutTypeForPlan(planType)) - q.Set("currency", cfg.Currency) + q.Set("currency", currencyForPlan(cfg, planType)) if cfg.Environment == "test" { q.Set("test", "true") } diff --git a/backend/internal/service/payment/waffo_test.go b/backend/internal/service/payment/waffo_test.go index 02723e1..c7574a1 100644 --- a/backend/internal/service/payment/waffo_test.go +++ b/backend/internal/service/payment/waffo_test.go @@ -6,12 +6,14 @@ import ( "testing" ) -func TestBuildCheckoutURL_YearlyOnetime(t *testing.T) { +func TestBuildCheckoutURL_YearlyOnetimeCNY(t *testing.T) { cfg := WaffoConfig{ StoreSlug: "opsxshell-pwsi6m4n", ProductIDYearly: "PROD_1BIezCi932IbgCzRIV8hcJ", ProductIDPerpetual: "PROD_0pcnzzT4nalEkkcdyIUoD5", Currency: "CNY", + CurrencyYearly: "CNY", + CurrencyPerpetual: "USD", Environment: "test", SuccessURL: "opsxshell://pro/activated", } @@ -31,19 +33,18 @@ func TestBuildCheckoutURL_YearlyOnetime(t *testing.T) { t.Fatalf("type=%s want onetime", q.Get("type")) } if q.Get("currency") != "CNY" { - t.Fatalf("currency=%s", q.Get("currency")) - } - if q.Get("test") != "true" { - t.Fatalf("test=%s", q.Get("test")) + t.Fatalf("currency=%s want CNY", q.Get("currency")) } } -func TestBuildCheckoutURL_PerpetualOnetime(t *testing.T) { +func TestBuildCheckoutURL_PerpetualOnetimeUSD(t *testing.T) { cfg := WaffoConfig{ StoreSlug: "opsxshell-pwsi6m4n", ProductIDYearly: "PROD_1BIezCi932IbgCzRIV8hcJ", ProductIDPerpetual: "PROD_0pcnzzT4nalEkkcdyIUoD5", Currency: "CNY", + CurrencyYearly: "CNY", + CurrencyPerpetual: "USD", Environment: "test", SuccessURL: "opsxshell://pro/activated", } @@ -58,7 +59,11 @@ func TestBuildCheckoutURL_PerpetualOnetime(t *testing.T) { if !strings.Contains(u.Path, "PROD_0pcnzzT4nalEkkcdyIUoD5") { t.Fatalf("perpetual product missing: %s", raw) } - if u.Query().Get("type") != "onetime" { - t.Fatalf("type=%s want onetime", u.Query().Get("type")) + q := u.Query() + if q.Get("type") != "onetime" { + t.Fatalf("type=%s want onetime", q.Get("type")) + } + if q.Get("currency") != "USD" { + t.Fatalf("currency=%s want USD", q.Get("currency")) } }