Files
opsx-licence/backend/internal/service/payment/waffo.go
T
Logan.LiandCursor 0966375076 feat: 支付成功页与友好 webhook 落地页
浏览器回跳展示许可证并深链回 App;补齐包年订单因未收到 Waffo POST 未发码的体验。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-12 16:39:13 +08:00

179 lines
4.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = "http://l123.91ai.io/api/v1/purchase/success"
}
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 == "" {
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()
}