fix: 修复支付页测试/生产 Product 映射串用与无法保存
切换环境时先暂存再切换;后端 Profile 不再串用另一侧商品;支持一键填充 OpsXshell 与跨环境复制,调试使用当前页映射。
This commit is contained in:
1 parent
9b79eabd34
commit
7c146cb639
5 files changed
+317
-71
No files matched your search
@@ -89,6 +89,7 @@ type WaffoProductMapping struct {
|
||||
PlanKey string `json:"plan_key"`
|
||||
Label string `json:"label,omitempty"`
|
||||
ProductID string `json:"product_id"`
|
||||
Currency string `json:"currency,omitempty"` // 如 CNY / USD,空则用全局默认 // Logan.Li
|
||||
}
|
||||
|
||||
// WaffoSettingsResponse 管理端 Waffo 配置(私钥不回显) // Logan.Li
|
||||
@@ -130,6 +131,8 @@ type WaffoSettingsUpdate struct {
|
||||
StoreSlug string `json:"store_slug"`
|
||||
Environment string `json:"environment"` // test | live | prod
|
||||
Currency string `json:"currency"`
|
||||
CurrencyYearly string `json:"currency_yearly"`
|
||||
CurrencyPerpetual string `json:"currency_perpetual"`
|
||||
ProductID string `json:"product_id"`
|
||||
ProductIDYearly string `json:"product_id_yearly"`
|
||||
ProductIDPerpetual string `json:"product_id_perpetual"`
|
||||
@@ -142,9 +145,16 @@ type WaffoSettingsUpdate struct {
|
||||
|
||||
// WaffoDebugRequest 用当前配置探测 create-session(可覆盖 plan/product) // Logan.Li
|
||||
type WaffoDebugRequest struct {
|
||||
PlanType string `json:"plan_type"` // yearly | perpetual | …,默认 yearly
|
||||
ProductID string `json:"product_id"` // 可选覆盖
|
||||
BuyerEmail string `json:"buyer_email"` // 可选
|
||||
PlanType string `json:"plan_type"` // yearly | perpetual | …,默认 yearly
|
||||
ProductID string `json:"product_id"` // 可选覆盖
|
||||
BuyerEmail string `json:"buyer_email"` // 可选
|
||||
Environment string `json:"environment"` // 可选:用页面当前环境覆盖 DB
|
||||
ProductIDYearly string `json:"product_id_yearly"`
|
||||
ProductIDPerpetual string `json:"product_id_perpetual"`
|
||||
ProductMappings []WaffoProductMapping `json:"product_mappings"`
|
||||
Currency string `json:"currency"`
|
||||
CurrencyYearly string `json:"currency_yearly"`
|
||||
CurrencyPerpetual string `json:"currency_perpetual"`
|
||||
}
|
||||
|
||||
// WaffoDebugResponse 调试结果摘要(绝不含私钥) // Logan.Li
|
||||
|
||||
@@ -37,6 +37,7 @@ type WaffoProductMapping struct {
|
||||
PlanKey string `json:"plan_key"`
|
||||
Label string `json:"label,omitempty"`
|
||||
ProductID string `json:"product_id"`
|
||||
Currency string `json:"currency,omitempty"`
|
||||
}
|
||||
|
||||
// WaffoPublicMeta 存入 payment_gateway_configs.public_key 的 JSON(无私钥) // Logan.Li
|
||||
@@ -416,6 +417,11 @@ func ResolveProductID(cfg WaffoConfig, planType string) string {
|
||||
return productIDForPlan(cfg, planType)
|
||||
}
|
||||
|
||||
// ResolveCurrency 按 plan_type 解析结算货币 // Logan.Li
|
||||
func ResolveCurrency(cfg WaffoConfig, planType string) string {
|
||||
return currencyForPlan(cfg, planType)
|
||||
}
|
||||
|
||||
func productIDForPlan(cfg WaffoConfig, planType string) string {
|
||||
plan := strings.TrimSpace(strings.ToLower(planType))
|
||||
if plan == "" {
|
||||
|
||||
@@ -86,7 +86,11 @@ func CreateCheckoutSession(cfg WaffoConfig, req CreateSessionRequest) (*CreateSe
|
||||
if env == "" {
|
||||
env = strings.TrimSpace(cfg.Environment)
|
||||
}
|
||||
if env == "" {
|
||||
// Waffo Store Slug 鉴权要求 X-Environment 为 test|prod(不是 live) // Logan.Li
|
||||
switch strings.ToLower(env) {
|
||||
case "live", "prod", "production":
|
||||
env = "prod"
|
||||
default:
|
||||
env = "test"
|
||||
}
|
||||
httpReq.Header.Set("X-Store-Slug", slug)
|
||||
|
||||
@@ -122,10 +122,17 @@ func (s *SettingService) GetWaffo() (*dto.WaffoSettingsResponse, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// buildEnvUI 组装单环境预览 // Logan.Li
|
||||
// buildEnvUI 组装单环境预览(不得串用另一环境顶层映射) // Logan.Li
|
||||
func buildEnvUI(meta payment.WaffoPublicMeta, bag payment.WaffoSecretBag, env string) dto.WaffoEnvUI {
|
||||
cp := meta
|
||||
// 先清空商品字段,再叠该环境 Profile,避免未配置环境显示成另一侧数据 // Logan.Li
|
||||
cp.ProductID = ""
|
||||
cp.ProductIDYearly = ""
|
||||
cp.ProductIDPerpetual = ""
|
||||
cp.ProductIDs = nil
|
||||
cp.ProductMappings = nil
|
||||
cp.ApplyEnvProfile(env)
|
||||
|
||||
cfgLike := payment.WaffoConfig{
|
||||
ProductID: cp.ProductID,
|
||||
ProductIDYearly: cp.ProductIDYearly,
|
||||
@@ -146,9 +153,9 @@ func buildEnvUI(meta payment.WaffoPublicMeta, bag payment.WaffoSecretBag, env st
|
||||
}
|
||||
return dto.WaffoEnvUI{
|
||||
ProductMappings: buildProductMappings(cfgLike, cp.ProductMappings),
|
||||
ProductID: cp.ProductID,
|
||||
ProductIDYearly: cp.ProductIDYearly,
|
||||
ProductIDPerpetual: cp.ProductIDPerpetual,
|
||||
ProductID: firstNonEmpty(cp.ProductID, cfgLike.ProductIDs["perpetual"], cfgLike.ProductIDs["lifetime"], cfgLike.ProductIDs["yearly"]),
|
||||
ProductIDYearly: firstNonEmpty(cp.ProductIDYearly, cfgLike.ProductIDs["yearly"]),
|
||||
ProductIDPerpetual: firstNonEmpty(cp.ProductIDPerpetual, cfgLike.ProductIDs["perpetual"], cfgLike.ProductIDs["lifetime"]),
|
||||
PrivateKeyConfigured: bag.SecretForEnv(env) != "",
|
||||
}
|
||||
}
|
||||
@@ -188,8 +195,8 @@ func (s *SettingService) UpdateWaffo(req dto.WaffoSettingsUpdate) (*dto.WaffoSet
|
||||
if meta.Currency == "" {
|
||||
meta.Currency = "CNY"
|
||||
}
|
||||
meta.CurrencyYearly = meta.Currency
|
||||
meta.CurrencyPerpetual = meta.Currency
|
||||
meta.CurrencyYearly = firstNonEmpty(strings.TrimSpace(req.CurrencyYearly), currencyFromMappings(mappings, "yearly"), meta.Currency)
|
||||
meta.CurrencyPerpetual = firstNonEmpty(strings.TrimSpace(req.CurrencyPerpetual), currencyFromMappings(mappings, "perpetual"), currencyFromMappings(mappings, "lifetime"), meta.Currency)
|
||||
meta.Environment = newEnv
|
||||
meta.ProductID = strings.TrimSpace(req.ProductID)
|
||||
meta.ProductIDYearly = strings.TrimSpace(req.ProductIDYearly)
|
||||
@@ -258,6 +265,18 @@ func (s *SettingService) UpdateWaffo(req dto.WaffoSettingsUpdate) (*dto.WaffoSet
|
||||
// DebugWaffo 用当前 DB/env 配置轻量探测 create-session(不回显私钥) // Logan.Li
|
||||
func (s *SettingService) DebugWaffo(req dto.WaffoDebugRequest) *dto.WaffoDebugResponse {
|
||||
cfg := payment.LoadWaffoConfig(s.db)
|
||||
// 页面未保存的环境 / 映射优先覆盖,避免「填了映射却仍用旧 Product」 // Logan.Li
|
||||
if strings.TrimSpace(req.Environment) != "" {
|
||||
env := payment.NormalizeWaffoEnvironment(req.Environment)
|
||||
cfg.Environment = env
|
||||
var gw model.PaymentGatewayConfig
|
||||
if err := s.db.Where("gateway = ?", "waffo").First(&gw).Error; err == nil {
|
||||
bag := payment.ParseWaffoSecretBag(gw.SecretKey, env)
|
||||
cfg.PrivateKey = bag.SecretForEnv(env)
|
||||
}
|
||||
}
|
||||
applyDebugProductOverrides(&cfg, req)
|
||||
|
||||
planType := strings.TrimSpace(strings.ToLower(req.PlanType))
|
||||
if planType == "" {
|
||||
planType = "yearly"
|
||||
@@ -286,7 +305,7 @@ func (s *SettingService) DebugWaffo(req dto.WaffoDebugRequest) *dto.WaffoDebugRe
|
||||
|
||||
if productID == "" {
|
||||
out.OK = false
|
||||
out.ErrorSummary = "未配置可用的 Waffo Product ID"
|
||||
out.ErrorSummary = "未配置可用的 Waffo Product ID(请在当前环境映射表填写后先保存,或点「填充 OpsXshell 商品」)"
|
||||
return out
|
||||
}
|
||||
if authMode == "none" {
|
||||
@@ -304,11 +323,15 @@ func (s *SettingService) DebugWaffo(req dto.WaffoDebugRequest) *dto.WaffoDebugRe
|
||||
if successURL == "" {
|
||||
successURL = payment.DefaultSuccessURL
|
||||
}
|
||||
currency := payment.ResolveCurrency(cfg, planType)
|
||||
if c := strings.TrimSpace(req.Currency); c != "" && planType != "yearly" && planType != "perpetual" && planType != "lifetime" {
|
||||
currency = c
|
||||
}
|
||||
|
||||
session, err := payment.CreateCheckoutSession(cfg, payment.CreateSessionRequest{
|
||||
ProductID: productID,
|
||||
ProductType: "onetime",
|
||||
Currency: firstNonEmpty(cfg.Currency, "CNY"),
|
||||
Currency: currency,
|
||||
BuyerEmail: buyer,
|
||||
SuccessURL: successURL,
|
||||
OrderMerchantExternalID: orderNo,
|
||||
@@ -325,6 +348,9 @@ func (s *SettingService) DebugWaffo(req dto.WaffoDebugRequest) *dto.WaffoDebugRe
|
||||
out.OK = false
|
||||
out.ErrorSummary = sanitizeDebugError(err.Error())
|
||||
out.HTTPStatus = extractHTTPStatus(err.Error())
|
||||
if strings.Contains(out.ErrorSummary, "not approved for production") {
|
||||
out.ErrorSummary += "(Waffo 门店尚未开通生产收款:请先在测试环境验证;生产需在 Waffo Dashboard 完成 Go-Live)"
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -337,6 +363,55 @@ func (s *SettingService) DebugWaffo(req dto.WaffoDebugRequest) *dto.WaffoDebugRe
|
||||
return out
|
||||
}
|
||||
|
||||
// applyDebugProductOverrides 把调试请求中的映射叠到运行时配置 // Logan.Li
|
||||
func applyDebugProductOverrides(cfg *payment.WaffoConfig, req dto.WaffoDebugRequest) {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
if cfg.ProductIDs == nil {
|
||||
cfg.ProductIDs = map[string]string{}
|
||||
}
|
||||
for _, m := range req.ProductMappings {
|
||||
k := strings.TrimSpace(strings.ToLower(m.PlanKey))
|
||||
v := strings.TrimSpace(m.ProductID)
|
||||
if k == "" || v == "" {
|
||||
continue
|
||||
}
|
||||
cfg.ProductIDs[k] = v
|
||||
switch k {
|
||||
case "yearly", "monthly":
|
||||
cfg.ProductIDYearly = v
|
||||
if c := strings.TrimSpace(m.Currency); c != "" {
|
||||
cfg.CurrencyYearly = c
|
||||
}
|
||||
case "perpetual", "lifetime":
|
||||
cfg.ProductIDPerpetual = v
|
||||
cfg.ProductID = v
|
||||
if c := strings.TrimSpace(m.Currency); c != "" {
|
||||
cfg.CurrencyPerpetual = c
|
||||
}
|
||||
}
|
||||
}
|
||||
if y := strings.TrimSpace(req.ProductIDYearly); y != "" {
|
||||
cfg.ProductIDYearly = y
|
||||
cfg.ProductIDs["yearly"] = y
|
||||
}
|
||||
if p := strings.TrimSpace(req.ProductIDPerpetual); p != "" {
|
||||
cfg.ProductIDPerpetual = p
|
||||
cfg.ProductID = p
|
||||
cfg.ProductIDs["perpetual"] = p
|
||||
}
|
||||
if c := strings.TrimSpace(req.Currency); c != "" {
|
||||
cfg.Currency = c
|
||||
}
|
||||
if c := strings.TrimSpace(req.CurrencyYearly); c != "" {
|
||||
cfg.CurrencyYearly = c
|
||||
}
|
||||
if c := strings.TrimSpace(req.CurrencyPerpetual); c != "" {
|
||||
cfg.CurrencyPerpetual = c
|
||||
}
|
||||
}
|
||||
|
||||
func mergeProductIDInputs(req dto.WaffoSettingsUpdate) (map[string]string, []dto.WaffoProductMapping) {
|
||||
ids := map[string]string{}
|
||||
var mappings []dto.WaffoProductMapping
|
||||
@@ -352,6 +427,7 @@ func mergeProductIDInputs(req dto.WaffoSettingsUpdate) (map[string]string, []dto
|
||||
PlanKey: k,
|
||||
Label: strings.TrimSpace(m.Label),
|
||||
ProductID: v,
|
||||
Currency: strings.ToUpper(strings.TrimSpace(m.Currency)),
|
||||
})
|
||||
if v != "" {
|
||||
ids[k] = v
|
||||
@@ -387,7 +463,7 @@ func mergeProductIDInputs(req dto.WaffoSettingsUpdate) (map[string]string, []dto
|
||||
}
|
||||
|
||||
if len(mappings) == 0 {
|
||||
mappings = mappingsFromIDs(ids, nil)
|
||||
mappings = mappingsFromIDs(ids, nil, nil, payment.WaffoConfig{})
|
||||
} else {
|
||||
// 用最终 ids 回填表格行 product_id,避免只改了兼容字段 // Logan.Li
|
||||
for i := range mappings {
|
||||
@@ -401,6 +477,7 @@ func mergeProductIDInputs(req dto.WaffoSettingsUpdate) (map[string]string, []dto
|
||||
|
||||
func buildProductMappings(cfg payment.WaffoConfig, stored []payment.WaffoProductMapping) []dto.WaffoProductMapping {
|
||||
labelHint := map[string]string{}
|
||||
currencyHint := map[string]string{}
|
||||
for _, m := range stored {
|
||||
k := strings.TrimSpace(strings.ToLower(m.PlanKey))
|
||||
if k == "" {
|
||||
@@ -409,15 +486,55 @@ func buildProductMappings(cfg payment.WaffoConfig, stored []payment.WaffoProduct
|
||||
if l := strings.TrimSpace(m.Label); l != "" {
|
||||
labelHint[k] = l
|
||||
}
|
||||
if c := strings.TrimSpace(m.Currency); c != "" {
|
||||
currencyHint[k] = strings.ToUpper(c)
|
||||
}
|
||||
}
|
||||
return mappingsFromIDs(cfg.ProductIDs, labelHint)
|
||||
// 若 stored 有完整行(含空 product 也应展示),优先按 stored 顺序回显 // Logan.Li
|
||||
if len(stored) > 0 {
|
||||
out := make([]dto.WaffoProductMapping, 0, len(stored))
|
||||
seen := map[string]bool{}
|
||||
for _, m := range stored {
|
||||
k := strings.TrimSpace(strings.ToLower(m.PlanKey))
|
||||
if k == "" || seen[k] {
|
||||
continue
|
||||
}
|
||||
seen[k] = true
|
||||
pid := strings.TrimSpace(m.ProductID)
|
||||
if pid == "" {
|
||||
pid = strings.TrimSpace(cfg.ProductIDs[k])
|
||||
}
|
||||
cur := strings.TrimSpace(m.Currency)
|
||||
if cur == "" {
|
||||
cur = currencyHint[k]
|
||||
}
|
||||
if cur == "" {
|
||||
switch k {
|
||||
case "yearly", "monthly":
|
||||
cur = cfg.CurrencyYearly
|
||||
case "perpetual", "lifetime":
|
||||
cur = cfg.CurrencyPerpetual
|
||||
}
|
||||
}
|
||||
out = append(out, dto.WaffoProductMapping{
|
||||
PlanKey: k,
|
||||
Label: firstNonEmpty(strings.TrimSpace(m.Label), labelHint[k]),
|
||||
ProductID: pid,
|
||||
Currency: strings.ToUpper(strings.TrimSpace(cur)),
|
||||
})
|
||||
}
|
||||
if len(out) > 0 {
|
||||
return out
|
||||
}
|
||||
}
|
||||
return mappingsFromIDs(cfg.ProductIDs, labelHint, currencyHint, cfg)
|
||||
}
|
||||
|
||||
func mappingsFromIDs(ids map[string]string, labels map[string]string) []dto.WaffoProductMapping {
|
||||
func mappingsFromIDs(ids map[string]string, labels, currencies map[string]string, cfg payment.WaffoConfig) []dto.WaffoProductMapping {
|
||||
if len(ids) == 0 {
|
||||
return []dto.WaffoProductMapping{
|
||||
{PlanKey: "yearly", Label: "包年"},
|
||||
{PlanKey: "perpetual", Label: "永久"},
|
||||
{PlanKey: "yearly", Label: "包年", Currency: "CNY"},
|
||||
{PlanKey: "perpetual", Label: "永久", Currency: "USD"},
|
||||
}
|
||||
}
|
||||
defaultLabels := map[string]string{
|
||||
@@ -445,7 +562,26 @@ func mappingsFromIDs(ids map[string]string, labels map[string]string) []dto.Waff
|
||||
if label == "" {
|
||||
label = defaultLabels[k]
|
||||
}
|
||||
out = append(out, dto.WaffoProductMapping{PlanKey: k, Label: label, ProductID: v})
|
||||
cur := ""
|
||||
if currencies != nil {
|
||||
cur = currencies[k]
|
||||
}
|
||||
if cur == "" {
|
||||
switch k {
|
||||
case "yearly", "monthly":
|
||||
cur = cfg.CurrencyYearly
|
||||
case "perpetual", "lifetime":
|
||||
cur = cfg.CurrencyPerpetual
|
||||
default:
|
||||
cur = cfg.Currency
|
||||
}
|
||||
}
|
||||
out = append(out, dto.WaffoProductMapping{
|
||||
PlanKey: k,
|
||||
Label: label,
|
||||
ProductID: v,
|
||||
Currency: strings.ToUpper(strings.TrimSpace(cur)),
|
||||
})
|
||||
}
|
||||
for _, k := range order {
|
||||
appendOne(k)
|
||||
@@ -470,8 +606,8 @@ func mappingsFromIDs(ids map[string]string, labels map[string]string) []dto.Waff
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return []dto.WaffoProductMapping{
|
||||
{PlanKey: "yearly", Label: "包年"},
|
||||
{PlanKey: "perpetual", Label: "永久"},
|
||||
{PlanKey: "yearly", Label: "包年", Currency: "CNY"},
|
||||
{PlanKey: "perpetual", Label: "永久", Currency: "USD"},
|
||||
}
|
||||
}
|
||||
return out
|
||||
@@ -488,11 +624,23 @@ func toPaymentMappings(in []dto.WaffoProductMapping) []payment.WaffoProductMappi
|
||||
PlanKey: k,
|
||||
Label: strings.TrimSpace(m.Label),
|
||||
ProductID: strings.TrimSpace(m.ProductID),
|
||||
Currency: strings.ToUpper(strings.TrimSpace(m.Currency)),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// currencyFromMappings 从映射表取指定 plan 的货币 // Logan.Li
|
||||
func currencyFromMappings(mappings []dto.WaffoProductMapping, plan string) string {
|
||||
plan = strings.TrimSpace(strings.ToLower(plan))
|
||||
for _, m := range mappings {
|
||||
if strings.TrimSpace(strings.ToLower(m.PlanKey)) == plan {
|
||||
return strings.ToUpper(strings.TrimSpace(m.Currency))
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func cloneProductIDs(in map[string]string) map[string]string {
|
||||
if len(in) == 0 {
|
||||
return map[string]string{}
|
||||
|
||||
Reference in new issue
Block a user