fix: subscription billing, alipay redirect + H5, payment secrets, 128MB response limit#1731
Open
touwaeriol wants to merge 8 commits intoWei-Shaw:mainfrom
Open
fix: subscription billing, alipay redirect + H5, payment secrets, 128MB response limit#1731touwaeriol wants to merge 8 commits intoWei-Shaw:mainfrom
touwaeriol wants to merge 8 commits intoWei-Shaw:mainfrom
Conversation
…hertext fallback Without TOTP_ENCRYPTION_KEY, saved payment configs were lost on restart because the AES round-trip failed silently. Write new records as plaintext JSON; read path tries JSON first, falls back to legacy AES decrypt when a key is present, and treats unreadable values as empty so admins can re-enter them via the UI.
Subscription-mode billing was consuming quota at TotalCost (raw) instead of ActualCost (TotalCost * RateMultiplier), so per-group rate multipliers — including free subscriptions (multiplier = 0) — were silently ignored. Switch the three subscription cost writes in buildUsageBillingCommand, finalizePostUsageBilling, and the legacy postUsageBilling fallback to ActualCost, and add a table-driven test covering 2x / 0.5x / free multipliers plus a balance-mode regression check.
… API key Chrome's password manager matched the apikey-type account's Base URL + API Key inputs as a login form and autofilled the last saved password by domain, so editing a Gemini account could overwrite its apikey with a Claude key that shared the same Base URL. Add autocomplete="new-password" plus data-*-ignore attributes for 1Password / LastPass / Bitwarden to opt the field out of every major password manager's autofill.
… 0 in compute 分组倍率和用户专属倍率在保存时没有校验,0 会触发计费层的 `<=0 → 1.0` 防御条款,结果订阅/余额分组按标准价扣费;完全是沉默地绕过了业务规则。 - 保存校验(admin_service):CreateGroup / UpdateGroup / BatchSetGroupRateMultipliers / UpdateUser.SyncUserGroupRates 全部要求 > 0 - 计算层(billing_service):三处 `<=0 → 1.0` 改为 `<0 → 0`;负数按 0 结算, 避免配置异常被静默按 1x 收费 - 前端:分组倍率 / 用户专属倍率输入 min 统一到 0.001 - 删除未使用的 IsFreeSubscription 方法 测试:新增 billing_service_rate_multiplier_test.go 端到端验证;更新原有锁定 旧 `<=0 → 1.0` 行为的测试。
…gurable) 图片生成 API 返回的 base64 内联图响应经常超过 8MB 单次读取上限,被 ReadUpstreamResponseBody 拦截成 502 upstream_error。 单张 4K PNG base64 最坏约 67MB,多张候选图或 imageSize=4K 的 image_generation 一次请求能轻松到 30MB+。把默认上限提到 128MB 能覆盖 2-3 张 4K 图,相对 请求体上限 256MB 仍有缓冲;同时抽出 config.DefaultUpstreamResponseReadMaxBytes 共享常量,viper 默认值和 service 层兜底共用,消除两处同步魔法数字。 仍可通过 gateway.upstream_response_read_max_bytes 配置项覆盖。
明文 JSON 已经是新写入的默认格式;保留 AES 密文读取仅为兼容迁移期间的旧 记录,一旦所有部署通过管理后台重存过一次即可删除。标记为 deprecated 并加 TODO,几个版本后统一清理掉:payment.Encrypt / payment.Decrypt、两处 decryptConfig 的 AES 分支、PaymentConfigService.encryptionKey 和 DefaultLoadBalancer.encryptionKey 字段。
20baf26 to
61a008f
Compare
The native Alipay provider previously tried to embed the payment page URL into a QR code on the client — the URL is not a scannable payload so the QR never worked. Merchants also hit a H5 detection mismatch whenever the backend UA sniffer missed iPadOS 13+ or embedded browsers, and the popup window was too small for Alipay's standard checkout layout (QR + account-login panel on the right), forcing the user to scroll horizontally and vertically. Changes: Backend - alipay.go: drop QR-on-URL path. Use redirect-only flow — alipay.trade.page.pay for PC (returns a gateway URL the browser opens in a new window) and alipay.trade.wap.pay for H5 (returns a URL the browser jumps to). Both flows produce pages on openapi.alipaydev.com / excashier.alipay.com; the client never renders a QR itself. - payment_handler.go: add optional is_mobile bool to CreateOrderRequest so the frontend can declare the device explicitly. Server still falls back to UA sniffing when absent. Frontend - types/payment.ts, PaymentView.vue: declare is_mobile in CreateOrderRequest and pass the computed isMobileDevice() value. - providerConfig.ts: replace the two fixed POPUP_WINDOW_FEATURES constants with getPaymentPopupFeatures(), which prefers 1250×900 (Alipay's checkout footprint), clamps to window.screen.avail* and centers the popup so it never overflows on smaller laptops. - PaymentQRDialog.vue, PaymentStatusPanel.vue, StripePaymentInline.vue, PaymentView.vue: use the new helper at all popup call sites.
Admin GET /api/v1/admin/payment/providers previously returned every
config value — including privateKey / apiV3Key / secretKey etc. —
verbatim. Any future XSS on the admin UI would hand attackers the
full set of production payment credentials, and the plaintext values
sat unnecessarily in browser memory for every operator.
Treat those fields as write-only from the admin surface:
- decryptAndMaskConfig() strips sensitive keys from the GET response.
The authoritative list is an explicit per-provider registry that
mirrors the frontend's PROVIDER_CONFIG_FIELDS sensitive flag:
alipay → privateKey, publicKey, alipayPublicKey
wxpay → privateKey, apiV3Key, publicKey
stripe → secretKey, webhookSecret (publishableKey stays plain)
easypay → pkey
Payment runtime still reads the full config via decryptConfig, so
nothing at the gateway changes.
- mergeConfig() treats an empty value for a sensitive key as "leave
unchanged" — the admin UI omits unchanged secrets so operators can
tweak non-sensitive settings without re-entering credentials.
- Admin dialog (PaymentProviderDialog.vue):
* secret inputs get autocomplete="new-password", data-1p-ignore,
data-lpignore and data-bwignore so password managers do not
offer to save provider credentials
* in edit mode the required-field check skips sensitive fields
(empty is the "keep existing" signal) and the placeholder shows
"leave empty to keep" instead of the default example value
* create mode still requires every non-optional field, including
secrets, since there is nothing to preserve
- Unit test renamed to TestIsSensitiveProviderConfigField, covers
the per-provider registry and specifically asserts that Stripe's
publishableKey is NOT treated as a secret.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1711
Closes #1718
Closes #1719
背景 / Background
生产环境发现数个独立但都影响日常使用的 bug:订阅倍率被静默按 1x 扣费;Chrome 密码管理器误填账号 API Key 和支付 provider 凭证;支付配置在
TOTP_ENCRYPTION_KEY未配时丢失;4K 图片生成响应超 8MB 被直接拒绝;Alipay 的 client-side QR 一直是坏的(URL 不是可扫描载荷);admin 读取支付配置接口把所有凭证明文返回;popup 窗口太小装不下 Alipay 收银台。Several independent production issues: subscription billing silently charging 1x; Chrome autofilling the wrong API keys and payment provider credentials; payment configs lost after save when
TOTP_ENCRYPTION_KEYwas unset; 4K image generation responses rejected as too large; Alipay's client-side QR never worked (the URL is not a scannable payload); admin config API returned payment credentials verbatim; popup too small for Alipay's checkout layout.目的 / Purpose
把上述问题一次性修到可发布状态:订阅倍率忠实落账;密码管理器在所有敏感输入框禁用;支付配置以明文 JSON 存储并保留旧密文回退;Alipay 改为原生跳转流(PC page-pay、H5 wap-pay),popup 尺寸按收银台实际占位确定;admin 响应脱敏所有敏感字段,编辑态将空值视为"保持不变";上游响应读取上限放宽至 128MB。
Land all of the above in one shippable PR: subscription multiplier faithfully billed; password managers disabled on every sensitive input; payment configs stored as plaintext JSON with legacy ciphertext fallback; Alipay switched to native redirect flows (PC page-pay, H5 wap-pay) with popup sized to the real checkout footprint; admin responses redact sensitive fields and treat empty values as "leave unchanged" on edit; upstream response read limit raised to 128MB.
改动 / Changes
后端 / Backend
gateway_service.go:三处订阅扣费写入改用ActualCost,订阅额度按倍率消耗(修 bug: 订阅分组统计费率不对,还是按照原消耗计费【计费问题 v0.1.113 版本】 #1711 [BUG] 订阅套餐定价为 0 的话,会影响订单和订阅套餐的显示 #1718 关于订阅bug 当前订阅分组倍率 不影响额度。请尽快修复 #1719)billing_service.go:三处<=0 → 1.0改为<0 → 0;admin_service.go四处加> 0校验;删除死代码IsFreeSubscriptionconfig.DefaultUpstreamResponseReadMaxBytes共享常量Deprecated:+TODO(deprecated-legacy-ciphertext),几个版本后统一清理alipay.go:去掉 QR-on-URL 路径,PC 用alipay.trade.page.pay,H5 用alipay.trade.wap.pay,返回网关 URL 由浏览器打开/跳转payment_handler.go:CreateOrderRequest新增可选is_mobile,允许前端显式声明设备;缺省回退 UA 嗅探payment_config_providers.go:按 provider 列举的敏感字段注册表(alipay:privateKey/publicKey/alipayPublicKey;wxpay:privateKey/apiV3Key/publicKey;stripe:secretKey/webhookSecret;easypay:pkey)。decryptAndMaskConfig脱敏 admin GET 响应;mergeConfig将敏感字段的空值视为"保持不变";支付运行时仍走decryptConfig,gateway 行为不变gateway_service.gonow useActualCostinstead ofTotalCost(fixes bug: 订阅分组统计费率不对,还是按照原消耗计费【计费问题 v0.1.113 版本】 #1711 [BUG] 订阅套餐定价为 0 的话,会影响订单和订阅套餐的显示 #1718 关于订阅bug 当前订阅分组倍率 不影响额度。请尽快修复 #1719)billing_service.go: three<=0 → 1.0clamps become<0 → 0;admin_service.gorejectsrate_multiplier <= 0on group/user-rate save; deadIsFreeSubscriptionremovedconfig.DefaultUpstreamResponseReadMaxBytesshared constantDeprecated:+TODO(deprecated-legacy-ciphertext)for future cleanupalipay.go: drop the QR-on-URL path. Usealipay.trade.page.payfor PC andalipay.trade.wap.payfor H5 — both return a gateway URL the browser opens in a new window or navigates topayment_handler.go: add optionalis_mobiletoCreateOrderRequestso the frontend can declare the device explicitly; server falls back to UA sniffing when absentpayment_config_providers.go: per-provider registry of sensitive fields (alipay:privateKey/publicKey/alipayPublicKey; wxpay:privateKey/apiV3Key/publicKey; stripe:secretKey/webhookSecret; easypay:pkey).decryptAndMaskConfigstrips them from the admin GET response;mergeConfigtreats an empty value on these keys as "leave unchanged". Payment runtime still reads the full config viadecryptConfig— gateway behavior unchanged前端 / Frontend
EditAccountModal.vue:API Key input 补autocomplete=new-password+data-1p-ignore/data-lpignore/data-bwignoremin从 0 收紧到 0.001,匹配后端校验types/payment.ts/views/user/PaymentView.vue:CreateOrderRequest新增is_mobile,下单时传isMobileDevice()计算值providerConfig.ts:用getPaymentPopupFeatures()替换两个固定POPUP_WINDOW_FEATURES常量,优先 1250×900(Alipay 收银台尺寸),按window.screen.avail*夹紧并居中PaymentQRDialog.vue/PaymentStatusPanel.vue/StripePaymentInline.vue/PaymentView.vue:所有 popup 调用点统一走新 helperPaymentProviderDialog.vue:secret 输入框加autocomplete="new-password"/data-1p-ignore/data-lpignore/data-bwignore,避免密码管理器保存;编辑态必填校验跳过敏感字段(空=保留原值),占位符显示"留空保持不变";新建态仍要求所有非可选字段EditAccountModal.vue: addautocomplete=new-password+ threedata-*-ignoreattrs on API Key inputmintightened from 0 to 0.001 to match backend validationtypes/payment.ts/views/user/PaymentView.vue: addis_mobiletoCreateOrderRequest; pass the computedisMobileDevice()value when creating ordersproviderConfig.ts: replace the two fixedPOPUP_WINDOW_FEATURESconstants withgetPaymentPopupFeatures()— prefers 1250×900 (Alipay checkout footprint), clamps towindow.screen.avail*, centers the popupPaymentQRDialog.vue/PaymentStatusPanel.vue/StripePaymentInline.vue/PaymentView.vue: use the new helper at all popup call sitesPaymentProviderDialog.vue: secret inputs getautocomplete="new-password"/data-1p-ignore/data-lpignore/data-bwignoreso password managers do not offer to save provider credentials. In edit mode the required-field check skips sensitive fields (empty = keep existing) and the placeholder shows "leave empty to keep"; create mode still requires every non-optional field测试 / Tests
新增
billing_service_rate_multiplier_test.go(负数/0/正数)和gateway_service_subscription_billing_test.go(2x/0.5x/免费订阅/余额回归);payment_config_providers_test.go改名TestIsSensitiveProviderConfigField,覆盖 provider 敏感字段注册表并断言 StripepublishableKey不被脱敏。go test -tags=unit ./...全绿;pnpm typecheck通过。Added
billing_service_rate_multiplier_test.goandgateway_service_subscription_billing_test.go;payment_config_providers_test.gorenamed toTestIsSensitiveProviderConfigFieldcovering the provider sensitive-field registry (explicitly asserts StripepublishableKeyis NOT redacted). Backend unit tests and frontend typecheck both pass.