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.
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
- 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[]`
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.
Upstream proxy double-encodes GET query params. url.ParseQuery only
decodes once, leaving values like %E5%BF%AB instead of actual UTF-8.
Add fullyDecodeURL to repeatedly decode until stable before signing.
RawQuery from GET callbacks can be double-URL-encoded by upstream
proxies. Use Query().Encode() to rebuild from fully decoded params,
ensuring VerifyNotification computes the signature on correct values.
- 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
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 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
- Skip UpdatePaymentConfig when no payment fields provided (prevents wipe)
- Remove unused defaultMinRechargeAmount/defaultMaxRechargeAmount constants
- Fix mergeConfig comment to match actual behavior
- Backend: payment fields added to GET/PUT /admin/settings (full replace)
- Frontend: single API call for all settings (no separate payment config API)
- Payment page: show "充值未开放" when no payment methods available
- Pending order check when disabling provider
- Backend: add ListProviderInstancesWithConfig that decrypts config
and masks sensitive fields (keys, secrets) as "••••••••"
- Frontend: pre-fill non-sensitive config values when editing provider
(PID, API base URL, notify URL, return URL, channel IDs etc.)
- Sensitive fields left empty for user to re-enter if needed
- Add PurchaseRouter.vue to dynamically render PaymentView or
PurchaseSubscriptionView based on payment_enabled vs
purchase_subscription_enabled settings
- Fix route guard to allow access when either payment system is enabled
- Split sidebar menu: /purchase shows for both systems, /orders only
for built-in payment
- Remove auto-migration that forcibly disabled legacy purchase system
on first startup (MigrateLegacyPurchaseURL)
- Add payment_enabled to PublicSettings API response so frontend can
check both systems from a single endpoint
- Restore legacy purchase link config UI controls in admin Payment tab
- Split payment settings save to use dedicated PUT /admin/payment/config
API instead of mixing into general settings endpoint
- Add admin payment config API functions (getConfig/updateConfig)
- payment_service: split GetDashboardStats (131→35 lines) into 4 sub-functions,
extract applyPagination helper, replace bubble sort with sort.Slice,
use sync.Once for EnsureProviders, define magic number constants
- providers: merge duplicate PC/Mobile functions in alipay/wxpay,
extract loadKeyPair/queryOrderTotalFen in wxpay, add centsToYuan in stripe,
define constants for success codes, currencies, event types
- handlers: extract requireAuth and parseIDParam helpers to eliminate
repeated auth/ID-parsing boilerplate
- registry: remove dead seen map in GetProviderByKey
- types: centralize order status constants in payment package
- load_balancer+config_service: unify duplicated containsType into
exported InstanceSupportsType function
Critical:
- Refund idempotency keys now include UnixNano timestamp (alipay/wxpay)
- PaymentView passes qr/pay_url params to QR code page
- Fix dead code branch in order result handling
Quality:
- Currency symbol $ → ¥ in admin stats and refund dialog
- StripePaymentView setTimeout cleanup on unmount
- Webhook body size limited to 1MB (io.LimitReader)
- SubscriptionPlan features type documented
- Run gofmt on user schema, config test, group handler
- Remove unused mergeGroupIDs function
- Restore shared test helpers (newJSONResponse, queuedHTTPUpstream)
that were in deleted Sora test file
- Add int64(0) param to SelectAccountWithLoadAwareness callers (signature change from channel scheduling refactor)
- Add UsageMapHook type and struct field to StreamingProcessor
- Revert Claude Max cache billing code to upstream/main (not part of channel feature)
- Revert credits overages logic to upstream/main (non-channel change)
- Remove Instructions field reference (non-channel OpenAI feature)
- Restore sora_client_handler_test.go from upstream + add channel service nil params
- Fix 7 stale comments still mentioning "限制检查" in handlers/services
- Make billingModelForRestriction explicitly list channel_mapped case
- Add slog.Warn for error swallowing in ResolveChannelMapping and
needsUpstreamChannelRestrictionCheck
- Document sticky session upstream check exemption
Move the model pricing restriction check from 8 handler entry points
to the account scheduling phase (SelectAccountForModelWithExclusions /
SelectAccountWithLoadAwareness), aligning restriction with billing:
- requested: check original request model against pricing list
- channel_mapped: check channel-mapped model against pricing list
- upstream: per-account check using account-mapped model
Handler layer now only resolves channel mapping (no restriction).
Scheduling layer performs pre-check for requested/channel_mapped,
and per-account filtering for upstream billing source.
Replace response.BadRequest with response.ErrorFrom + infraerrors.BadRequest
to provide machine-readable reason codes (VALIDATION_ERROR, INVALID_CHANNEL_ID,
MISSING_PARAMETER) for frontend i18n support.
- PricingSourceChannel/LiteLLM/Fallback for resolver source
- MediaTypeImage/Video/Prompt for result.MediaType
- Reuse BillingModeToken/BillingModeImage for billing mode
- Reuse BillingModelSourceChannelMapped/PlatformAnthropic in handler
- Backend: reject intervals with all-null price fields on save
- Backend: filterValidIntervals skips empty intervals in pricing resolver
- Frontend: red border + asterisk on empty interval rows
- Backend: antigravity groups now match anthropic/gemini channel pricing
- Fix errcheck: defer rows.Close() with nolint
- Fix errcheck: type assertion with ok check in channel cache
- Fix staticcheck ST1005: lowercase error string
- Fix staticcheck SA5011: nil check cost before use in openai gateway
- Fix gofmt: format chatcompletions_to_responses.go
- Parse candidatesTokensDetails from Gemini API to separate image/text output tokens
- Add image_output_tokens and image_output_cost to usage_log (migration 089)
- Support per-image-token pricing via output_cost_per_image_token from model pricing data
- Channel pricing ImageOutputPrice override works in token billing mode
- Auto-fill image_output_price in channel pricing form from model defaults
- Add "channel_mapped" billing model source as new default (migration 088)
- Bills by model name after channel mapping, before account mapping
- Fix channel cache error TTL sign error (115s → 5s)
- Fix Update channel only invalidating new groups, not removed groups
- Fix frontend model_mapping clearing sending undefined instead of {}
- Credits balance precheck via shared AccountUsageService cache before injection
- Skip credits injection for accounts with insufficient balance
- Don't mark credits exhausted for "exhausted your capacity on this model" 429s