按档位分别拼 Waffo currency,避免永久商品用 CNY 报不可用。 Co-authored-by: Cursor <cursoragent@cursor.com>
184 lines
4.9 KiB
Go
184 lines
4.9 KiB
Go
package payment
|
||
|
||
import (
|
||
"fmt"
|
||
"net/url"
|
||
"os"
|
||
"strings"
|
||
)
|
||
|
||
// 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 // fallback / default
|
||
CurrencyYearly string
|
||
CurrencyPerpetual string
|
||
Environment string
|
||
SuccessURL string
|
||
}
|
||
|
||
// LoadWaffoConfig reads WAFFO_* env vars (same names as OpsXShell legacy license-server). // Logan.Li
|
||
func LoadWaffoConfig() WaffoConfig {
|
||
env := strings.TrimSpace(os.Getenv("WAFFO_ENVIRONMENT"))
|
||
if env == "" {
|
||
env = "test"
|
||
}
|
||
success := strings.TrimSpace(os.Getenv("SUCCESS_URL"))
|
||
if success == "" {
|
||
success = "opsxshell://pro/activated"
|
||
}
|
||
currency := strings.TrimSpace(os.Getenv("WAFFO_CURRENCY"))
|
||
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"))
|
||
if yearly == "" {
|
||
yearly = defaultProduct
|
||
}
|
||
if perpetual == "" {
|
||
perpetual = defaultProduct
|
||
}
|
||
return WaffoConfig{
|
||
CheckoutURL: strings.TrimSpace(os.Getenv("WAFFO_CHECKOUT_URL")),
|
||
StoreSlug: strings.TrimSpace(os.Getenv("WAFFO_STORE_SLUG")),
|
||
ProductID: defaultProduct,
|
||
ProductIDYearly: yearly,
|
||
ProductIDPerpetual: perpetual,
|
||
Currency: currency,
|
||
CurrencyYearly: currencyYearly,
|
||
CurrencyPerpetual: currencyPerpetual,
|
||
Environment: env,
|
||
SuccessURL: success,
|
||
}
|
||
}
|
||
|
||
// BuildCheckoutURL returns a Waffo hosted checkout URL with order/device/email metadata. // Logan.Li
|
||
func BuildCheckoutURL(cfg WaffoConfig, orderNo, deviceID, email, returnURL, planType string) (string, error) {
|
||
success := cfg.SuccessURL
|
||
if strings.TrimSpace(returnURL) != "" {
|
||
success = strings.TrimSpace(returnURL)
|
||
}
|
||
u, err := resolveBaseCheckoutURL(cfg, planType)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
q := u.Query()
|
||
if q.Get("successUrl") == "" {
|
||
q.Set("successUrl", appendQuery(success, map[string]string{
|
||
"orderNo": orderNo,
|
||
"deviceId": deviceID,
|
||
"email": email,
|
||
"planType": planType,
|
||
}))
|
||
}
|
||
if deviceID != "" {
|
||
q.Set("deviceId", deviceID)
|
||
}
|
||
if orderNo != "" {
|
||
q.Set("orderNo", orderNo)
|
||
}
|
||
if email != "" {
|
||
q.Set("email", email)
|
||
}
|
||
if planType != "" {
|
||
q.Set("planType", planType)
|
||
}
|
||
u.RawQuery = q.Encode()
|
||
return u.String(), nil
|
||
}
|
||
|
||
func productIDForPlan(cfg WaffoConfig, planType string) string {
|
||
switch strings.TrimSpace(planType) {
|
||
case "yearly", "monthly":
|
||
return cfg.ProductIDYearly
|
||
case "perpetual", "lifetime":
|
||
return cfg.ProductIDPerpetual
|
||
default:
|
||
if cfg.ProductIDPerpetual != "" {
|
||
return cfg.ProductIDPerpetual
|
||
}
|
||
return cfg.ProductID
|
||
}
|
||
}
|
||
|
||
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
|
||
return "onetime"
|
||
}
|
||
|
||
func resolveBaseCheckoutURL(cfg WaffoConfig, planType string) (*url.URL, error) {
|
||
// Prefer store + per-plan product IDs so yearly/perpetual map to different Waffo SKUs.
|
||
productID := productIDForPlan(cfg, planType)
|
||
if cfg.StoreSlug != "" && productID != "" {
|
||
raw := fmt.Sprintf("https://pancake.waffo.ai/store/%s/product/%s", cfg.StoreSlug, productID)
|
||
u, err := url.Parse(raw)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
q := u.Query()
|
||
q.Set("type", checkoutTypeForPlan(planType))
|
||
q.Set("currency", currencyForPlan(cfg, planType))
|
||
if cfg.Environment == "test" {
|
||
q.Set("test", "true")
|
||
}
|
||
u.RawQuery = q.Encode()
|
||
return u, nil
|
||
}
|
||
if cfg.CheckoutURL != "" {
|
||
return url.Parse(cfg.CheckoutURL)
|
||
}
|
||
return nil, fmt.Errorf("未配置 WAFFO_STORE_SLUG 与 WAFFO_PRODUCT_ID_YEARLY / WAFFO_PRODUCT_ID_PERPETUAL(或 WAFFO_CHECKOUT_URL)")
|
||
}
|
||
|
||
func appendQuery(raw string, values map[string]string) string {
|
||
u, err := url.Parse(raw)
|
||
if err != nil {
|
||
return raw
|
||
}
|
||
q := u.Query()
|
||
for k, v := range values {
|
||
if v != "" {
|
||
q.Set(k, v)
|
||
}
|
||
}
|
||
u.RawQuery = q.Encode()
|
||
return u.String()
|
||
}
|