feat: 对齐 Waffo 包年/永久结账与客户端席位 API
同步 OpsXShell 档位定价与 Waffo 商品映射,补齐 checkout/webhook/设备列表契约,便于桌面端购买与满席释放。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
4ae318335c
commit
95405de119
14 files changed
+958
-155
No files matched your search
@@ -0,0 +1,153 @@
|
||||
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
|
||||
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"
|
||||
}
|
||||
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,
|
||||
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 checkoutTypeForPlan(planType string) string {
|
||||
switch strings.TrimSpace(planType) {
|
||||
case "yearly", "monthly":
|
||||
return "subscription"
|
||||
default:
|
||||
return "onetime"
|
||||
}
|
||||
}
|
||||
|
||||
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 != "" {
|
||||
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", cfg.Currency)
|
||||
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()
|
||||
}
|
||||
Reference in new issue
Block a user