fix: 包年 CNY / 永久 USD 分币种结账

按档位分别拼 Waffo currency,避免永久商品用 CNY 报不可用。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Logan.LiandCursor committed 2026-08-12 16:18:05 +08:00
1 parent d4c5637168
commit e8d98c993f
3 files changed
+53 -13

No files matched your search

+4 -2
View File
@@ -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 可覆盖)
+36 -3
View File
@@ -14,7 +14,9 @@ type WaffoConfig struct {
ProductID string
ProductIDYearly string
ProductIDPerpetual string
Currency string
Currency string // fallback / default
CurrencyYearly string
CurrencyPerpetual string
Environment string
SuccessURL string
}
@@ -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")
}
+13 -8
View File
@@ -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"))
}
}