diff --git a/backend/internal/service/payment/waffo.go b/backend/internal/service/payment/waffo.go index 808a1da..23c4845 100644 --- a/backend/internal/service/payment/waffo.go +++ b/backend/internal/service/payment/waffo.go @@ -456,22 +456,36 @@ func productIDForPlan(cfg WaffoConfig, planType string) string { } func currencyForPlan(cfg WaffoConfig, planType string) string { + // 已知商品 ID 优先(测试包年不支持 CNY) // Logan.Li + if pid := productIDForPlan(cfg, planType); pid != "" { + switch pid { + case "PROD_1BIezCi932IbgCzRIV8hcJ", // test yearly + "PROD_0pcnzzT4nalEkkcdyIUoD5", // test perpetual + "PROD_3BSJqDRkVBoYNgO78NsTAU": // live perpetual + return "USD" + case "PROD_5PqVJyEiCM4nDgVoA0RKjN": // live yearly + return "CNY" + } + } switch strings.TrimSpace(strings.ToLower(planType)) { case "yearly", "monthly": if cfg.CurrencyYearly != "" { return cfg.CurrencyYearly } - return "CNY" // OpsXshell 包年默认人民币 // Logan.Li + if NormalizeWaffoEnvironment(cfg.Environment) == "live" { + return "CNY" + } + return "USD" case "perpetual", "lifetime": if cfg.CurrencyPerpetual != "" { return cfg.CurrencyPerpetual } - return "USD" // OpsXshell 永久默认美元 // Logan.Li + return "USD" } if cfg.Currency != "" { return cfg.Currency } - return "CNY" + return "USD" } func checkoutTypeForPlan(planType string) string { diff --git a/backend/internal/service/payment/waffo_test.go b/backend/internal/service/payment/waffo_test.go index bfafc1f..cb34710 100644 --- a/backend/internal/service/payment/waffo_test.go +++ b/backend/internal/service/payment/waffo_test.go @@ -6,13 +6,13 @@ import ( "testing" ) -func TestResolveBaseCheckoutURL_YearlyOnetimeCNY(t *testing.T) { +func TestResolveBaseCheckoutURL_YearlyOnetimeUSD(t *testing.T) { cfg := WaffoConfig{ StoreSlug: "opsxshell-pwsi6m4n", ProductIDYearly: "PROD_1BIezCi932IbgCzRIV8hcJ", ProductIDPerpetual: "PROD_0pcnzzT4nalEkkcdyIUoD5", Currency: "CNY", - CurrencyYearly: "CNY", + CurrencyYearly: "CNY", // 即使配置写 CNY,已知测试商品仍强制 USD // Logan.Li CurrencyPerpetual: "USD", Environment: "test", SuccessURL: "opsxshell://pro/activated", @@ -28,12 +28,12 @@ func TestResolveBaseCheckoutURL_YearlyOnetimeCNY(t *testing.T) { if q.Get("type") != "onetime" { t.Fatalf("type=%s want onetime", q.Get("type")) } - if q.Get("currency") != "CNY" { - t.Fatalf("currency=%s want CNY", q.Get("currency")) + if q.Get("currency") != "USD" { + t.Fatalf("currency=%s want USD", q.Get("currency")) } } -func TestResolveBaseCheckoutURL_PerpetualOnetimeCNY(t *testing.T) { +func TestResolveBaseCheckoutURL_PerpetualOnetimeUSD(t *testing.T) { cfg := WaffoConfig{ StoreSlug: "opsxshell-pwsi6m4n", ProductIDYearly: "PROD_1BIezCi932IbgCzRIV8hcJ", @@ -55,8 +55,8 @@ func TestResolveBaseCheckoutURL_PerpetualOnetimeCNY(t *testing.T) { if q.Get("type") != "onetime" { t.Fatalf("type=%s want onetime", q.Get("type")) } - if q.Get("currency") != "CNY" { - t.Fatalf("currency=%s want CNY", q.Get("currency")) + if q.Get("currency") != "USD" { + t.Fatalf("currency=%s want USD", q.Get("currency")) } } diff --git a/frontend/src/views/Payments.vue b/frontend/src/views/Payments.vue index b3c6dff..6367210 100644 --- a/frontend/src/views/Payments.vue +++ b/frontend/src/views/Payments.vue @@ -107,7 +107,7 @@ type="warning" :closable="false" show-icon - title="下单时按计划选用对应 PROD_xxx:包年→yearly,永久→perpetual。货币由后端按计划自动带(包年 CNY / 永久 USD),无需手填。" + title="下单时按计划选用对应 PROD_xxx。币种按商品自动选择(测试包年 USD;生产包年 CNY;永久 USD)。" style="margin-bottom: 12px" /> @@ -157,16 +157,24 @@ import { getWaffoSettings, updateWaffoSettings, testWaffoConnection } from '@/ap const planKeyOptions = ['trial', 'yearly', 'perpetual', 'monthly', 'lifetime'] -// OpsXshell 商品:测试 / 生产各一套 // Logan.Li +// OpsXshell 商品:测试 / 生产各一套(币种跟 Waffo 商品实际支持走) // Logan.Li const OPSXSHELL_PRODUCTS_LIVE = [ - { plan_key: 'yearly', label: 'OpsXshell Yearly', product_id: 'PROD_5PqVJyEiCM4nDgVoA0RKjN' }, - { plan_key: 'perpetual', label: 'OpsXshell Perpetual', product_id: 'PROD_3BSJqDRkVBoYNgO78NsTAU' }, + { plan_key: 'yearly', label: 'OpsXshell Yearly', product_id: 'PROD_5PqVJyEiCM4nDgVoA0RKjN', currency: 'CNY' }, + { plan_key: 'perpetual', label: 'OpsXshell Perpetual', product_id: 'PROD_3BSJqDRkVBoYNgO78NsTAU', currency: 'USD' }, ] const OPSXSHELL_PRODUCTS_TEST = [ - { plan_key: 'yearly', label: 'OpsXshell Yearly', product_id: 'PROD_1BIezCi932IbgCzRIV8hcJ' }, - { plan_key: 'perpetual', label: 'OpsXshell Perpetual', product_id: 'PROD_0pcnzzT4nalEkkcdyIUoD5' }, + // 测试包年商品不支持 CNY,须用 USD // Logan.Li + { plan_key: 'yearly', label: 'OpsXshell Yearly', product_id: 'PROD_1BIezCi932IbgCzRIV8hcJ', currency: 'USD' }, + { plan_key: 'perpetual', label: 'OpsXshell Perpetual', product_id: 'PROD_0pcnzzT4nalEkkcdyIUoD5', currency: 'USD' }, ] +const PRODUCT_CURRENCY = { + PROD_5PqVJyEiCM4nDgVoA0RKjN: 'CNY', + PROD_3BSJqDRkVBoYNgO78NsTAU: 'USD', + PROD_1BIezCi932IbgCzRIV8hcJ: 'USD', + PROD_0pcnzzT4nalEkkcdyIUoD5: 'USD', +} + const loading = ref(false) const saving = ref(false) const debugging = ref(false) @@ -226,19 +234,24 @@ function normalizeMappings(data = {}) { return rows.length ? rows : defaultMappings() } -// 按计划自动带货币,不再展示币种列 // Logan.Li -function currencyForKey(planKey) { - const k = (planKey || '').toLowerCase() +// 按 Product ID / 计划推断币种(测试包年是 USD,生产包年才是 CNY) // Logan.Li +function currencyForRow(row = {}) { + const pid = (row.product_id || '').trim() + if (pid && PRODUCT_CURRENCY[pid]) return PRODUCT_CURRENCY[pid] + if ((row.currency || '').trim()) return row.currency.trim().toUpperCase() + const k = (row.plan_key || '').toLowerCase() if (k === 'perpetual' || k === 'lifetime') return 'USD' - return 'CNY' + return isLive.value ? 'CNY' : 'USD' } function mappingsToCompatFields(rows) { const map = {} + const currencyMap = {} for (const row of rows) { const key = (row.plan_key || '').trim() if (!key) continue map[key] = (row.product_id || '').trim() + currencyMap[key] = currencyForRow(row) } return { product_mappings: rows @@ -247,13 +260,13 @@ function mappingsToCompatFields(rows) { plan_key: r.plan_key.trim(), label: r.label || '', product_id: (r.product_id || '').trim(), - currency: currencyForKey(r.plan_key), + currency: currencyForRow(r), })), product_id_yearly: map.yearly || '', product_id_perpetual: map.perpetual || map.lifetime || '', product_id: map.perpetual || map.lifetime || map.yearly || '', - currency_yearly: 'CNY', - currency_perpetual: 'USD', + currency_yearly: currencyMap.yearly || (isLive.value ? 'CNY' : 'USD'), + currency_perpetual: currencyMap.perpetual || currencyMap.lifetime || 'USD', } } @@ -302,6 +315,7 @@ function fillOpsxshellProducts() { if (byKey[item.plan_key]) { byKey[item.plan_key].product_id = item.product_id byKey[item.plan_key].label = item.label + byKey[item.plan_key].currency = item.currency } else { productMappings.value.push({ ...item }) }