payment_config_service.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  1. package service
  2. import (
  3. "context"
  4. "fmt"
  5. "math"
  6. "os"
  7. "strconv"
  8. "strings"
  9. dbent "github.com/Wei-Shaw/sub2api/ent"
  10. "github.com/Wei-Shaw/sub2api/ent/paymentproviderinstance"
  11. "github.com/Wei-Shaw/sub2api/internal/payment"
  12. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  13. )
  14. const (
  15. SettingPaymentEnabled = "payment_enabled"
  16. SettingMinRechargeAmount = "MIN_RECHARGE_AMOUNT"
  17. SettingMaxRechargeAmount = "MAX_RECHARGE_AMOUNT"
  18. SettingDailyRechargeLimit = "DAILY_RECHARGE_LIMIT"
  19. SettingOrderTimeoutMinutes = "ORDER_TIMEOUT_MINUTES"
  20. SettingMaxPendingOrders = "MAX_PENDING_ORDERS"
  21. SettingEnabledPaymentTypes = "ENABLED_PAYMENT_TYPES"
  22. SettingLoadBalanceStrategy = "LOAD_BALANCE_STRATEGY"
  23. SettingBalancePayDisabled = "BALANCE_PAYMENT_DISABLED"
  24. SettingBalanceRechargeMult = "BALANCE_RECHARGE_MULTIPLIER"
  25. // SettingSubscriptionUSDToCNYRate 是订阅 CNY 换算汇率(1 USD = X CNY)。
  26. // 0/未配置 = 关闭换算(订阅按 price 数值直付),显式配置后 CNY 通道订阅按 price × rate 收款。
  27. SettingSubscriptionUSDToCNYRate = "SUBSCRIPTION_USD_TO_CNY_RATE"
  28. SettingRechargeFeeRate = "RECHARGE_FEE_RATE"
  29. SettingProductNamePrefix = "PRODUCT_NAME_PREFIX"
  30. SettingProductNameSuffix = "PRODUCT_NAME_SUFFIX"
  31. SettingHelpImageURL = "PAYMENT_HELP_IMAGE_URL"
  32. SettingHelpText = "PAYMENT_HELP_TEXT"
  33. SettingCancelRateLimitOn = "CANCEL_RATE_LIMIT_ENABLED"
  34. SettingCancelRateLimitMax = "CANCEL_RATE_LIMIT_MAX"
  35. SettingCancelWindowSize = "CANCEL_RATE_LIMIT_WINDOW"
  36. SettingCancelWindowUnit = "CANCEL_RATE_LIMIT_UNIT"
  37. SettingCancelWindowMode = "CANCEL_RATE_LIMIT_WINDOW_MODE"
  38. SettingAlipayForceQRCode = "ALIPAY_FORCE_QRCODE"
  39. SettingAlipayMobilePrecreateDeepLink = "ALIPAY_MOBILE_PRECREATE_DEEP_LINK"
  40. )
  41. // Default values for payment configuration settings.
  42. const (
  43. defaultOrderTimeoutMin = 30
  44. defaultMaxPendingOrders = 3
  45. )
  46. // PaymentConfig holds the payment system configuration.
  47. type PaymentConfig struct {
  48. Enabled bool `json:"enabled"`
  49. MinAmount float64 `json:"min_amount"`
  50. MaxAmount float64 `json:"max_amount"`
  51. DailyLimit float64 `json:"daily_limit"`
  52. OrderTimeoutMin int `json:"order_timeout_minutes"`
  53. MaxPendingOrders int `json:"max_pending_orders"`
  54. EnabledTypes []string `json:"enabled_payment_types"`
  55. BalanceDisabled bool `json:"balance_disabled"`
  56. BalanceRechargeMultiplier float64 `json:"balance_recharge_multiplier"`
  57. // SubscriptionUSDToCNYRate 为 0 时订阅换算关闭(兼容存量行为)。
  58. SubscriptionUSDToCNYRate float64 `json:"subscription_usd_to_cny_rate"`
  59. RechargeFeeRate float64 `json:"recharge_fee_rate"`
  60. LoadBalanceStrategy string `json:"load_balance_strategy"`
  61. ProductNamePrefix string `json:"product_name_prefix"`
  62. ProductNameSuffix string `json:"product_name_suffix"`
  63. HelpImageURL string `json:"help_image_url"`
  64. HelpText string `json:"help_text"`
  65. StripePublishableKey string `json:"stripe_publishable_key,omitempty"`
  66. // Cancel rate limit settings
  67. CancelRateLimitEnabled bool `json:"cancel_rate_limit_enabled"`
  68. CancelRateLimitMax int `json:"cancel_rate_limit_max"`
  69. CancelRateLimitWindow int `json:"cancel_rate_limit_window"`
  70. CancelRateLimitUnit string `json:"cancel_rate_limit_unit"`
  71. CancelRateLimitMode string `json:"cancel_rate_limit_window_mode"`
  72. // Force Alipay mobile users to use QR code instead of mobile redirect
  73. AlipayForceQRCode bool `json:"alipay_force_qrcode"`
  74. // Use Alipay face-to-face precreate and an app deep link on mobile clients.
  75. AlipayMobilePrecreateDeepLink bool `json:"alipay_mobile_precreate_deep_link"`
  76. }
  77. // UpdatePaymentConfigRequest contains fields to update payment configuration.
  78. type UpdatePaymentConfigRequest struct {
  79. Enabled *bool `json:"enabled"`
  80. MinAmount *float64 `json:"min_amount"`
  81. MaxAmount *float64 `json:"max_amount"`
  82. DailyLimit *float64 `json:"daily_limit"`
  83. OrderTimeoutMin *int `json:"order_timeout_minutes"`
  84. MaxPendingOrders *int `json:"max_pending_orders"`
  85. EnabledTypes []string `json:"enabled_payment_types"`
  86. BalanceDisabled *bool `json:"balance_disabled"`
  87. BalanceRechargeMultiplier *float64 `json:"balance_recharge_multiplier"`
  88. SubscriptionUSDToCNYRate *float64 `json:"subscription_usd_to_cny_rate"`
  89. RechargeFeeRate *float64 `json:"recharge_fee_rate"`
  90. LoadBalanceStrategy *string `json:"load_balance_strategy"`
  91. ProductNamePrefix *string `json:"product_name_prefix"`
  92. ProductNameSuffix *string `json:"product_name_suffix"`
  93. HelpImageURL *string `json:"help_image_url"`
  94. HelpText *string `json:"help_text"`
  95. // Cancel rate limit settings
  96. CancelRateLimitEnabled *bool `json:"cancel_rate_limit_enabled"`
  97. CancelRateLimitMax *int `json:"cancel_rate_limit_max"`
  98. CancelRateLimitWindow *int `json:"cancel_rate_limit_window"`
  99. CancelRateLimitUnit *string `json:"cancel_rate_limit_unit"`
  100. CancelRateLimitMode *string `json:"cancel_rate_limit_window_mode"`
  101. // Force Alipay mobile users to use QR code instead of mobile redirect
  102. AlipayForceQRCode *bool `json:"alipay_force_qrcode"`
  103. // Use Alipay face-to-face precreate and an app deep link on mobile clients.
  104. AlipayMobilePrecreateDeepLink *bool `json:"alipay_mobile_precreate_deep_link"`
  105. VisibleMethodAlipaySource *string `json:"payment_visible_method_alipay_source"`
  106. VisibleMethodWxpaySource *string `json:"payment_visible_method_wxpay_source"`
  107. VisibleMethodAlipayEnabled *bool `json:"payment_visible_method_alipay_enabled"`
  108. VisibleMethodWxpayEnabled *bool `json:"payment_visible_method_wxpay_enabled"`
  109. }
  110. // MethodLimits holds per-payment-type limits.
  111. type MethodLimits struct {
  112. PaymentType string `json:"payment_type"`
  113. DisplayName string `json:"display_name,omitempty"`
  114. Currency string `json:"currency"`
  115. FeeRate float64 `json:"fee_rate"`
  116. DailyLimit float64 `json:"daily_limit"`
  117. SingleMin float64 `json:"single_min"`
  118. SingleMax float64 `json:"single_max"`
  119. }
  120. // MethodLimitsResponse is the full response for the user-facing /limits API.
  121. // It includes per-method limits and the global widest range (union of all methods).
  122. type MethodLimitsResponse struct {
  123. Methods map[string]MethodLimits `json:"methods"`
  124. GlobalMin float64 `json:"global_min"` // 0 = no minimum
  125. GlobalMax float64 `json:"global_max"` // 0 = no maximum
  126. }
  127. type CreateProviderInstanceRequest struct {
  128. ProviderKey string `json:"provider_key"`
  129. Name string `json:"name"`
  130. Config map[string]string `json:"config"`
  131. SupportedTypes []string `json:"supported_types"`
  132. Enabled bool `json:"enabled"`
  133. PaymentMode string `json:"payment_mode"`
  134. SortOrder int `json:"sort_order"`
  135. Limits string `json:"limits"`
  136. RefundEnabled bool `json:"refund_enabled"`
  137. AllowUserRefund bool `json:"allow_user_refund"`
  138. }
  139. type UpdateProviderInstanceRequest struct {
  140. Name *string `json:"name"`
  141. Config map[string]string `json:"config"`
  142. SupportedTypes []string `json:"supported_types"`
  143. Enabled *bool `json:"enabled"`
  144. PaymentMode *string `json:"payment_mode"`
  145. SortOrder *int `json:"sort_order"`
  146. Limits *string `json:"limits"`
  147. RefundEnabled *bool `json:"refund_enabled"`
  148. AllowUserRefund *bool `json:"allow_user_refund"`
  149. }
  150. type CreatePlanRequest struct {
  151. GroupID int64 `json:"group_id"`
  152. Name string `json:"name"`
  153. Description string `json:"description"`
  154. Price float64 `json:"price"`
  155. OriginalPrice *float64 `json:"original_price"`
  156. Currency string `json:"currency"`
  157. ValidityDays int `json:"validity_days"`
  158. ValidityUnit string `json:"validity_unit"`
  159. Features string `json:"features"`
  160. ProductName string `json:"product_name"`
  161. ForSale bool `json:"for_sale"`
  162. SortOrder int `json:"sort_order"`
  163. }
  164. type UpdatePlanRequest struct {
  165. GroupID *int64 `json:"group_id"`
  166. Name *string `json:"name"`
  167. Description *string `json:"description"`
  168. Price *float64 `json:"price"`
  169. OriginalPrice *float64 `json:"original_price"`
  170. Currency *string `json:"currency"`
  171. ValidityDays *int `json:"validity_days"`
  172. ValidityUnit *string `json:"validity_unit"`
  173. Features *string `json:"features"`
  174. ProductName *string `json:"product_name"`
  175. ForSale *bool `json:"for_sale"`
  176. SortOrder *int `json:"sort_order"`
  177. }
  178. // PaymentConfigService manages payment configuration and CRUD for
  179. // provider instances, channels, and subscription plans.
  180. type PaymentConfigService struct {
  181. entClient *dbent.Client
  182. settingRepo SettingRepository
  183. encryptionKey []byte
  184. }
  185. // NewPaymentConfigService creates a new PaymentConfigService.
  186. func NewPaymentConfigService(entClient *dbent.Client, settingRepo SettingRepository, encryptionKey []byte) *PaymentConfigService {
  187. return &PaymentConfigService{entClient: entClient, settingRepo: settingRepo, encryptionKey: encryptionKey}
  188. }
  189. // IsPaymentEnabled returns whether the payment system is enabled.
  190. func (s *PaymentConfigService) IsPaymentEnabled(ctx context.Context) bool {
  191. val, err := s.settingRepo.GetValue(ctx, SettingPaymentEnabled)
  192. if err != nil {
  193. return false
  194. }
  195. return val == "true"
  196. }
  197. // GetPaymentConfig returns the full payment configuration.
  198. func (s *PaymentConfigService) GetPaymentConfig(ctx context.Context) (*PaymentConfig, error) {
  199. keys := []string{
  200. SettingPaymentEnabled, SettingMinRechargeAmount, SettingMaxRechargeAmount,
  201. SettingDailyRechargeLimit, SettingOrderTimeoutMinutes, SettingMaxPendingOrders,
  202. SettingEnabledPaymentTypes, SettingBalancePayDisabled, SettingBalanceRechargeMult, SettingSubscriptionUSDToCNYRate, SettingRechargeFeeRate, SettingLoadBalanceStrategy,
  203. SettingProductNamePrefix, SettingProductNameSuffix,
  204. SettingHelpImageURL, SettingHelpText,
  205. SettingCancelRateLimitOn, SettingCancelRateLimitMax,
  206. SettingCancelWindowSize, SettingCancelWindowUnit, SettingCancelWindowMode,
  207. SettingAlipayForceQRCode, SettingAlipayMobilePrecreateDeepLink,
  208. SettingPaymentVisibleMethodAlipayEnabled, SettingPaymentVisibleMethodAlipaySource,
  209. SettingPaymentVisibleMethodWxpayEnabled, SettingPaymentVisibleMethodWxpaySource,
  210. }
  211. vals, err := s.settingRepo.GetMultiple(ctx, keys)
  212. if err != nil {
  213. return nil, fmt.Errorf("get payment config settings: %w", err)
  214. }
  215. cfg := s.parsePaymentConfig(vals)
  216. // Load Stripe publishable key from the first enabled Stripe provider instance
  217. cfg.StripePublishableKey = s.getStripePublishableKey(ctx)
  218. return cfg, nil
  219. }
  220. func (s *PaymentConfigService) parsePaymentConfig(vals map[string]string) *PaymentConfig {
  221. cfg := &PaymentConfig{
  222. Enabled: vals[SettingPaymentEnabled] == "true",
  223. MinAmount: pcParseFloat(vals[SettingMinRechargeAmount], 1),
  224. MaxAmount: pcParseFloat(vals[SettingMaxRechargeAmount], 0),
  225. DailyLimit: pcParseFloat(vals[SettingDailyRechargeLimit], 0),
  226. OrderTimeoutMin: pcParseInt(vals[SettingOrderTimeoutMinutes], defaultOrderTimeoutMin),
  227. MaxPendingOrders: pcParseInt(vals[SettingMaxPendingOrders], defaultMaxPendingOrders),
  228. BalanceDisabled: vals[SettingBalancePayDisabled] == "true",
  229. BalanceRechargeMultiplier: normalizeBalanceRechargeMultiplier(pcParseFloat(vals[SettingBalanceRechargeMult], defaultBalanceRechargeMultiplier)),
  230. SubscriptionUSDToCNYRate: normalizeSubscriptionUSDToCNYRate(pcParseFloat(vals[SettingSubscriptionUSDToCNYRate], 0)),
  231. RechargeFeeRate: pcParseFloat(vals[SettingRechargeFeeRate], 0),
  232. LoadBalanceStrategy: vals[SettingLoadBalanceStrategy],
  233. ProductNamePrefix: vals[SettingProductNamePrefix],
  234. ProductNameSuffix: vals[SettingProductNameSuffix],
  235. HelpImageURL: vals[SettingHelpImageURL],
  236. HelpText: vals[SettingHelpText],
  237. CancelRateLimitEnabled: vals[SettingCancelRateLimitOn] == "true",
  238. CancelRateLimitMax: pcParseInt(vals[SettingCancelRateLimitMax], 10),
  239. CancelRateLimitWindow: pcParseInt(vals[SettingCancelWindowSize], 1),
  240. CancelRateLimitUnit: vals[SettingCancelWindowUnit],
  241. CancelRateLimitMode: vals[SettingCancelWindowMode],
  242. AlipayForceQRCode: vals[SettingAlipayForceQRCode] == "true",
  243. AlipayMobilePrecreateDeepLink: vals[SettingAlipayMobilePrecreateDeepLink] == "true",
  244. }
  245. cfg.AlipayMobilePrecreateDeepLink = pcEnvBoolOverride(
  246. SettingAlipayMobilePrecreateDeepLink,
  247. cfg.AlipayMobilePrecreateDeepLink,
  248. )
  249. if cfg.LoadBalanceStrategy == "" {
  250. cfg.LoadBalanceStrategy = payment.DefaultLoadBalanceStrategy
  251. }
  252. if raw := vals[SettingEnabledPaymentTypes]; raw != "" {
  253. types := make([]string, 0, len(strings.Split(raw, ",")))
  254. for _, t := range strings.Split(raw, ",") {
  255. t = strings.TrimSpace(t)
  256. if t != "" {
  257. types = append(types, t)
  258. }
  259. }
  260. cfg.EnabledTypes = NormalizeVisibleMethods(types)
  261. }
  262. return cfg
  263. }
  264. func pcEnvBoolOverride(key string, fallback bool) bool {
  265. raw, ok := os.LookupEnv(key)
  266. if !ok || strings.TrimSpace(raw) == "" {
  267. return fallback
  268. }
  269. value, err := strconv.ParseBool(strings.TrimSpace(raw))
  270. if err != nil {
  271. return fallback
  272. }
  273. return value
  274. }
  275. // getStripePublishableKey finds the publishable key from the first enabled Stripe provider instance.
  276. func (s *PaymentConfigService) getStripePublishableKey(ctx context.Context) string {
  277. if s.entClient == nil {
  278. return ""
  279. }
  280. instances, err := s.entClient.PaymentProviderInstance.Query().
  281. Where(
  282. paymentproviderinstance.EnabledEQ(true),
  283. paymentproviderinstance.ProviderKeyEQ(payment.TypeStripe),
  284. ).Limit(1).All(ctx)
  285. if err != nil || len(instances) == 0 {
  286. return ""
  287. }
  288. cfg, err := s.decryptConfig(instances[0].Config)
  289. if err != nil || cfg == nil {
  290. return ""
  291. }
  292. return cfg[payment.ConfigKeyPublishableKey]
  293. }
  294. // UpdatePaymentConfig updates the payment configuration settings.
  295. // NOTE: This function exceeds 30 lines because each field requires an independent
  296. // nil-check before serialisation — this is inherent to patch-style update patterns
  297. // and cannot be meaningfully decomposed without introducing unnecessary abstraction.
  298. func (s *PaymentConfigService) UpdatePaymentConfig(ctx context.Context, req UpdatePaymentConfigRequest) error {
  299. if req.BalanceRechargeMultiplier != nil {
  300. if math.IsNaN(*req.BalanceRechargeMultiplier) || math.IsInf(*req.BalanceRechargeMultiplier, 0) || *req.BalanceRechargeMultiplier <= 0 {
  301. return infraerrors.BadRequest("INVALID_BALANCE_RECHARGE_MULTIPLIER", "balance recharge multiplier must be greater than 0")
  302. }
  303. }
  304. if req.SubscriptionUSDToCNYRate != nil {
  305. v := *req.SubscriptionUSDToCNYRate
  306. if math.IsNaN(v) || math.IsInf(v, 0) || v < 0 {
  307. return infraerrors.BadRequest("INVALID_SUBSCRIPTION_USD_TO_CNY_RATE", "subscription USD to CNY rate must be 0 (disabled) or a positive number")
  308. }
  309. }
  310. if req.RechargeFeeRate != nil {
  311. v := *req.RechargeFeeRate
  312. if math.IsNaN(v) || math.IsInf(v, 0) || v < 0 || v > 100 {
  313. return infraerrors.BadRequest("INVALID_RECHARGE_FEE_RATE", "recharge fee rate must be between 0 and 100")
  314. }
  315. // Enforce max 2 decimal places
  316. if math.Round(v*100) != v*100 {
  317. return infraerrors.BadRequest("INVALID_RECHARGE_FEE_RATE", "recharge fee rate allows at most 2 decimal places")
  318. }
  319. }
  320. m := make(map[string]string)
  321. if req.Enabled != nil {
  322. m[SettingPaymentEnabled] = formatBoolOrEmpty(req.Enabled)
  323. }
  324. if req.MinAmount != nil {
  325. m[SettingMinRechargeAmount] = formatPositiveFloat(req.MinAmount)
  326. }
  327. if req.MaxAmount != nil {
  328. m[SettingMaxRechargeAmount] = formatPositiveFloat(req.MaxAmount)
  329. }
  330. if req.DailyLimit != nil {
  331. m[SettingDailyRechargeLimit] = formatPositiveFloat(req.DailyLimit)
  332. }
  333. if req.OrderTimeoutMin != nil {
  334. m[SettingOrderTimeoutMinutes] = formatPositiveInt(req.OrderTimeoutMin)
  335. }
  336. if req.MaxPendingOrders != nil {
  337. m[SettingMaxPendingOrders] = formatPositiveInt(req.MaxPendingOrders)
  338. }
  339. if req.EnabledTypes != nil {
  340. m[SettingEnabledPaymentTypes] = strings.Join(req.EnabledTypes, ",")
  341. }
  342. if req.BalanceDisabled != nil {
  343. m[SettingBalancePayDisabled] = formatBoolOrEmpty(req.BalanceDisabled)
  344. }
  345. if req.BalanceRechargeMultiplier != nil {
  346. m[SettingBalanceRechargeMult] = formatPositiveFloat(req.BalanceRechargeMultiplier)
  347. }
  348. if req.SubscriptionUSDToCNYRate != nil {
  349. m[SettingSubscriptionUSDToCNYRate] = formatPositiveFloatExact(req.SubscriptionUSDToCNYRate)
  350. }
  351. if req.RechargeFeeRate != nil {
  352. m[SettingRechargeFeeRate] = formatNonNegativeFloat(req.RechargeFeeRate)
  353. }
  354. if req.LoadBalanceStrategy != nil {
  355. m[SettingLoadBalanceStrategy] = derefStr(req.LoadBalanceStrategy)
  356. }
  357. if req.ProductNamePrefix != nil {
  358. m[SettingProductNamePrefix] = derefStr(req.ProductNamePrefix)
  359. }
  360. if req.ProductNameSuffix != nil {
  361. m[SettingProductNameSuffix] = derefStr(req.ProductNameSuffix)
  362. }
  363. if req.HelpImageURL != nil {
  364. m[SettingHelpImageURL] = derefStr(req.HelpImageURL)
  365. }
  366. if req.HelpText != nil {
  367. m[SettingHelpText] = derefStr(req.HelpText)
  368. }
  369. if req.CancelRateLimitEnabled != nil {
  370. m[SettingCancelRateLimitOn] = formatBoolOrEmpty(req.CancelRateLimitEnabled)
  371. }
  372. if req.CancelRateLimitMax != nil {
  373. m[SettingCancelRateLimitMax] = formatPositiveInt(req.CancelRateLimitMax)
  374. }
  375. if req.CancelRateLimitWindow != nil {
  376. m[SettingCancelWindowSize] = formatPositiveInt(req.CancelRateLimitWindow)
  377. }
  378. if req.CancelRateLimitUnit != nil {
  379. m[SettingCancelWindowUnit] = derefStr(req.CancelRateLimitUnit)
  380. }
  381. if req.CancelRateLimitMode != nil {
  382. m[SettingCancelWindowMode] = derefStr(req.CancelRateLimitMode)
  383. }
  384. if req.AlipayForceQRCode != nil {
  385. m[SettingAlipayForceQRCode] = formatBoolOrEmpty(req.AlipayForceQRCode)
  386. }
  387. if req.AlipayMobilePrecreateDeepLink != nil {
  388. m[SettingAlipayMobilePrecreateDeepLink] = formatBoolOrEmpty(req.AlipayMobilePrecreateDeepLink)
  389. }
  390. if req.VisibleMethodAlipaySource != nil {
  391. m[SettingPaymentVisibleMethodAlipaySource] = derefStr(req.VisibleMethodAlipaySource)
  392. }
  393. if req.VisibleMethodWxpaySource != nil {
  394. m[SettingPaymentVisibleMethodWxpaySource] = derefStr(req.VisibleMethodWxpaySource)
  395. }
  396. if req.VisibleMethodAlipayEnabled != nil {
  397. m[SettingPaymentVisibleMethodAlipayEnabled] = formatBoolOrEmpty(req.VisibleMethodAlipayEnabled)
  398. }
  399. if req.VisibleMethodWxpayEnabled != nil {
  400. m[SettingPaymentVisibleMethodWxpayEnabled] = formatBoolOrEmpty(req.VisibleMethodWxpayEnabled)
  401. }
  402. return s.settingRepo.SetMultiple(ctx, m)
  403. }
  404. func formatBoolOrEmpty(v *bool) string {
  405. if v == nil {
  406. return ""
  407. }
  408. return strconv.FormatBool(*v)
  409. }
  410. func formatPositiveFloat(v *float64) string {
  411. if v == nil || *v <= 0 {
  412. return "" // empty → parsePaymentConfig uses default
  413. }
  414. return strconv.FormatFloat(*v, 'f', 2, 64)
  415. }
  416. // formatPositiveFloatExact 保留完整精度,用于汇率等对小数位敏感的配置。
  417. func formatPositiveFloatExact(v *float64) string {
  418. if v == nil || *v <= 0 {
  419. return "" // empty → parsePaymentConfig 视为未配置(换算关闭)
  420. }
  421. return strconv.FormatFloat(*v, 'f', -1, 64)
  422. }
  423. func formatNonNegativeFloat(v *float64) string {
  424. if v == nil || *v < 0 {
  425. return ""
  426. }
  427. return strconv.FormatFloat(*v, 'f', 2, 64)
  428. }
  429. func formatPositiveInt(v *int) string {
  430. if v == nil || *v <= 0 {
  431. return ""
  432. }
  433. return strconv.Itoa(*v)
  434. }
  435. func derefStr(v *string) string {
  436. if v == nil {
  437. return ""
  438. }
  439. return *v
  440. }
  441. func splitTypes(s string) []string {
  442. if s == "" {
  443. return nil
  444. }
  445. parts := strings.Split(s, ",")
  446. result := make([]string, 0, len(parts))
  447. for _, p := range parts {
  448. p = strings.TrimSpace(p)
  449. if p != "" {
  450. result = append(result, p)
  451. }
  452. }
  453. return result
  454. }
  455. func joinTypes(types []string) string {
  456. return strings.Join(types, ",")
  457. }
  458. func pcParseFloat(s string, defaultVal float64) float64 {
  459. if s == "" {
  460. return defaultVal
  461. }
  462. v, err := strconv.ParseFloat(s, 64)
  463. if err != nil {
  464. return defaultVal
  465. }
  466. return v
  467. }
  468. func pcParseInt(s string, defaultVal int) int {
  469. if s == "" {
  470. return defaultVal
  471. }
  472. v, err := strconv.Atoi(s)
  473. if err != nil {
  474. return defaultVal
  475. }
  476. return v
  477. }
  478. func buildVisibleMethodSourceAvailability(instances []*dbent.PaymentProviderInstance) map[string]bool {
  479. available := make(map[string]bool, 4)
  480. for _, inst := range instances {
  481. switch inst.ProviderKey {
  482. case payment.TypeAlipay:
  483. if inst.SupportedTypes == "" || payment.InstanceSupportsType(inst.SupportedTypes, payment.TypeAlipay) || payment.InstanceSupportsType(inst.SupportedTypes, payment.TypeAlipayDirect) {
  484. available[VisibleMethodSourceOfficialAlipay] = true
  485. }
  486. case payment.TypeWxpay:
  487. if inst.SupportedTypes == "" || payment.InstanceSupportsType(inst.SupportedTypes, payment.TypeWxpay) || payment.InstanceSupportsType(inst.SupportedTypes, payment.TypeWxpayDirect) {
  488. available[VisibleMethodSourceOfficialWechat] = true
  489. }
  490. case payment.TypeEasyPay:
  491. for _, supportedType := range splitTypes(inst.SupportedTypes) {
  492. switch NormalizeVisibleMethod(supportedType) {
  493. case payment.TypeAlipay:
  494. available[VisibleMethodSourceEasyPayAlipay] = true
  495. case payment.TypeWxpay:
  496. available[VisibleMethodSourceEasyPayWechat] = true
  497. }
  498. }
  499. }
  500. }
  501. return available
  502. }
  503. func applyVisibleMethodRoutingToEnabledTypes(base []string, vals map[string]string, available map[string]bool) []string {
  504. shouldExpose := map[string]bool{
  505. payment.TypeAlipay: visibleMethodShouldBeExposed(payment.TypeAlipay, vals, available),
  506. payment.TypeWxpay: visibleMethodShouldBeExposed(payment.TypeWxpay, vals, available),
  507. }
  508. seen := make(map[string]struct{}, len(base)+2)
  509. out := make([]string, 0, len(base)+2)
  510. appendType := func(paymentType string) {
  511. paymentType = NormalizeVisibleMethod(paymentType)
  512. if paymentType == "" {
  513. return
  514. }
  515. if _, ok := seen[paymentType]; ok {
  516. return
  517. }
  518. seen[paymentType] = struct{}{}
  519. out = append(out, paymentType)
  520. }
  521. for _, paymentType := range base {
  522. visibleMethod := NormalizeVisibleMethod(paymentType)
  523. switch visibleMethod {
  524. case payment.TypeAlipay, payment.TypeWxpay:
  525. if shouldExpose[visibleMethod] {
  526. appendType(visibleMethod)
  527. }
  528. default:
  529. appendType(visibleMethod)
  530. }
  531. }
  532. for _, visibleMethod := range []string{payment.TypeAlipay, payment.TypeWxpay} {
  533. if shouldExpose[visibleMethod] {
  534. appendType(visibleMethod)
  535. }
  536. }
  537. return out
  538. }
  539. func visibleMethodShouldBeExposed(method string, vals map[string]string, available map[string]bool) bool {
  540. enabledKey := visibleMethodEnabledSettingKey(method)
  541. sourceKey := visibleMethodSourceSettingKey(method)
  542. if enabledKey == "" || sourceKey == "" || vals[enabledKey] != "true" {
  543. return false
  544. }
  545. source := NormalizeVisibleMethodSource(method, vals[sourceKey])
  546. return source != "" && available[source]
  547. }