Extend checkout API to return group name, rate multiplier, daily/weekly/monthly
limits and supported model scopes. Redesign SubscriptionPlanCard to display
platform badge, group quota info, and model scope tags.
- EasyPay: remove redirect mode, keep only qrcode and popup
- Stripe: remove mode config entirely, always use inline Payment Element
- Subscription: replace dialog with inline confirm UI (plan summary +
method selector + fee breakdown), same flow as top-up
- Move PaymentStatusPanel/StripePaymentInline to shared section for
both recharge and subscription payments
- Hide tabs and help card during payment/subscription confirm phases
When user selects a redirect-based payment method (e.g. Alipay) in the
inline Stripe Element and clicks pay, the payment page opens in a new
window instead of navigating the current page away. The main page
switches to waiting/polling mode. Inline methods (card, WeChat) still
complete directly on the page.
Stripe payment now shows the Payment Element directly on the purchase
page. User selects payment method (Alipay/WeChat/Card) inline, then
confirms. Redirect-based methods (Alipay) navigate to the payment
page; non-redirect methods (Card/WeChat) complete inline.
Quick amount buttons and custom input always use the global min/max
range (union of all methods). Payment method availability is
determined by whether the selected amount fits each method's limits.
Payment waiting/QR/success states now render inline on the page
instead of as a modal overlay dialog. The page content switches
between selection mode and payment status mode.
Backend:
- Extract YuanToFen/FenToYuan to payment/amount.go using shopspring/decimal
- Require alipay publicKey in config validation
- Fix wxpay webhook response to return JSON per V3 spec
- Remove wxpay certSerial fallback to publicKeyId
- Define magic strings as named constants in wxpay/alipay providers
- Add slog warning for wxpay H5→Native payment downgrade
- Make EncryptionKey validation return error on invalid (non-empty) key
- Make decryptConfig propagate errors instead of returning nil
- Add idempotency check in doBalance to prevent stuck FAILED retries
Frontend:
- Fix dashboard currency symbol from $ to ¥
- Fix AdminPaymentPlansView any type to proper SubscriptionPlan type
- Make quick amount buttons follow selected payment method limits
- Center help image with larger height and text below
Instead of navigating the main page away during payment, open Stripe
and EasyPay redirect payments in a popup window while showing a
waiting dialog on the purchase page. Extract POPUP_WINDOW_FEATURES
constant to providerConfig.ts.
QR code payments now display in an inline dialog on the payment page
with countdown and status polling. Success is shown directly without
page navigation. Redirect mode (pay_url only) still navigates to the
polling page as before.
- Fix features field sent as raw string causing character-by-character iteration
- Redesign SubscriptionPlanCard with gradient accent bar, checkmark feature list,
discount badge, and polished hover/button effects
- Adaptive plan grid layout (1/2/3 columns based on plan count)
- Improve subscription confirm dialog with better price display
Backend: new GET /payment/checkout-info returns methods (with limits),
global_min/max, plans (with platform), balance_disabled, help_text,
and stripe_publishable_key in one response.
Frontend: PaymentView now calls getCheckoutInfo() once instead of
fetchConfig + getLimits + fetchPlans separately. Removed plansLoading
state and loadPlans watcher. Reduced from 317 to 301 lines.
Backend:
- Add ProviderStatus* constants (pending/paid/success/failed/refunded)
- Add DefaultLoadBalanceStrategy, ConfigKeyPublishableKey constants
- GetBasePaymentType: use Type* constants instead of raw strings
- Webhook handler: use payment.Type* for provider keys, extract
webhookLogTruncateLen constant
- All 4 providers: replace status string literals with ProviderStatus*
Frontend:
- All catch blocks: add `: unknown` type annotation
- stores/payment.ts: replace `any` with proper type for plan parsing
- PaymentProviderDialog: replace `as any` with `as SelectOption[]`
Root cause: Stripe instance had supported_types="card,alipay,link,wxpay"
but only card/link were mapped to "stripe" group. Stripe's alipay/wxpay
leaked into independent groups, and since Stripe had no limits configured,
it triggered the "any unlimited → all zeros" early return, making ALL
method limits show as zero.
Fix: pcGroupByPaymentType now routes ALL types from Stripe provider
instances to the "stripe" group (by checking ProviderKey, not sub-type).
Frontend: Stripe provider dialog shows single "Stripe" limits entry
instead of per-sub-type entries.
Backend: change limits aggregation from intersection (most restrictive) to
union (least restrictive) across provider instances. Since the load balancer
can route to any instance, users should see the widest possible range.
Add global_min/global_max precomputed by backend for quick amount buttons.
Frontend: auto-disable payment methods that can't handle the entered amount,
auto-switch to first available method, show warning when no method fits.
Refactor: extract ChannelLimits named type, unionFloat helper to eliminate
repeated min/max/daily logic, split limits code to payment_config_limits.go,
replace magic strings with payment.Type* constants.
- Payment result page is now a standalone page without business layout
- Support EasyPay return_url params (out_trade_no, trade_status)
- Payment methods sorted: EasyPay → Alipay → WeChat → Stripe
- Alipay/WeChat use official brand SVG icons
- QR code displays Alipay/WeChat logo in center with scan prompt
- After opening pay_url in new window, navigate to order status page
with countdown timer and order polling (reuses QRCode view)
- QRCode view now shows "pay in new window" UI when no QR code present
- Add verbose logging for webhook signature verification failures
- EasyPay redirect passes all configured CIDs (cidAlipay, cidWxpay) comma-separated
- Payment redirect opens in new window instead of current window redirect
- Order action column uses styled icon buttons with tooltip
- Pending order limit error returns i18n-friendly message with metadata
- Cancel order rate limit feature (ported from sub2apipay) with rolling/fixed window modes
- Admin settings UI for cancel rate limit configuration
- Fix provider dialog triggering unintended settings save on open
- Provider save now correctly calls saveSettings after provider update
- API client interceptor forwards reason and metadata fields for structured error handling
- Plan table: group column shows GroupBadge with platform color instead
of raw ID, column header changed to "分组"
- Plan dialog: group info preview uses GroupBadge, removes redundant
platform/rate fields
- Action buttons: all payment admin tables (plans, orders, providers)
now use vertically stacked icon+text buttons matching the app style
- Added i18n keys: payment.admin.group, common.view
Backend request/response structs used camelCase JSON tags while the
rest of the codebase uses snake_case (from Ent ORM entities). This
caused field name mismatches — notably the admin refund deduct_balance
field was silently ignored.
Backend: changed all camelCase JSON tags in payment handler/service
structs to snake_case (CreateOrderRequest, CreateOrderResponse,
CreatePlanRequest, UpdatePlanRequest, CreateProviderInstanceRequest,
UpdateProviderInstanceRequest, AdminProcessRefundRequest, RefundResult).
Frontend: updated all API call payloads and response field accesses
to use snake_case, removed manual camelCase conversion in
buildPlanPayload, toggleForSale, provider dialog emit, etc.
- EasyPay VerifyNotification was not parsing the 'money' field, causing
Amount=0 in PaymentNotification → amount mismatch error on callback
- Cancel button on QR page now shows "取消订单" instead of generic "取消"
- Fix plan create/update: send camelCase keys (groupId, validityDays,
forSale, etc.) matching backend JSON tags instead of snake_case
- Show GroupBadge with platform color instead of raw group ID in table
- Replace checkbox with toggle switch for for_sale, support quick
toggle directly from the plan list
- Parse features string to array when loading plans (both admin and user)
- Fix price display to use ¥ (CNY) instead of $
- EasyPay webhook: add GET route + read params from URL query (fix 404)
- Stripe: expose as single "stripe" method to users, sub-types (card/link/
alipay/wxpay) passed to PaymentIntent internally via instance config
- QR code page: use order expiresAt for countdown, add cancel button
- InstanceSelection carries SupportedTypes for provider-specific routing
- Fix CreateOrderRequest field name mismatch (payment_type → paymentType)
- Payment page now discovers available types from providers (not global config)
- Backend GetLimits returns map keyed by payment type
- EasyPay shows 3 buttons (跳转/支付宝/微信), Stripe shows card/alipay/wxpay/link
- Auto-select first method after limits loaded