- Add POST /payment/orders/:id/sync endpoint that queries upstream
provider on each poll, complementing webhooks for timely payment
detection when webhooks are delayed or unreachable
- Fix Stripe payment countdown: pass expires_at to PaymentStatusPanel
instead of empty string (was falling back to hardcoded 30 minutes)
- Idempotent: toPaid uses atomic UPDATE WHERE status=PENDING, so
concurrent webhook + sync calls won't double-credit
Cherry-picked from PR branch (feat/payment-system-v2).
- EasyPay: parse payurl2 for H5 mobile links, prefer on mobile
- EasyPay: add device=mobile for popup mode on mobile
- Backend: expand isMobile() to detect iPad/iPod
- Frontend: auto-redirect on mobile instead of popup
- Frontend: fallback to redirect when popup blocked
- Stripe: use mobile_web client for WeChat Pay on mobile
- StripePopup: typed interface, extractApiErrorMessage
The db139191 audit incorrectly removed the second URL decode pass.
EasyPay providers send values that need decoding after url.ParseQuery,
causing signature verification to fail on .147+.
Production .146 worked because it still had decodeURLValue.
The PaymentResultView only checked COMPLETED and PAID status, but orders
in RECHARGING state (balance being applied after payment) were incorrectly
shown as failed.
- Replace gradient header in subscription confirm with clean card layout
matching sub2apipay design (platform accent text instead of full gradient)
- Add renewal plan selection modal: when group has multiple plans show
picker, single plan skips directly to payment method selection
- Restyle SubscriptionsView with platform-specific colors (badge, border,
button) instead of hardcoded purple
- Update platformColors to match sub2apipay style (transparent badges,
subtle borders with /20 opacity)
EasyPay callbacks arrive with double-encoded query values (e.g. %25E5 instead
of %E5) due to redirect chains. url.ParseQuery decodes once; decodeURLValue
applies a second safe decode so the sign matches what EasyPay computed.
Also removes temporary debug logging.
When multiple provider instances exist (e.g. 3 EasyPay accounts), the webhook
handler now extracts out_trade_no from the callback, looks up the order, and
uses the order's original provider instance for verification instead of picking
an arbitrary instance from the registry.
- Add btn-alipay/btn-wxpay CSS classes; confirm button color follows payment method
- Subscription confirm header uses platform gradient (Anthropic orange, Gemini blue, etc.)
- Subscription confirm page shows plan details (rate, limits, validity)
- SubscriptionPlanCard: show "Renew" button when user has active subscription
- SubscriptionsView: add renewal button on active subscription cards
- QR code display: brand-color border + center logo overlay (Alipay blue, WeChat green)
- StripePaymentView: WeChat QR green border + logo, Alipay spinner brand color
- Backend: fix subscription refund to deduct days (ExtendSubscription -days or Revoke)
- Backend: rollback subscription days on gateway failure
UpdateProvider API returns raw DB entity (encrypted config, string types),
but frontend needs ProviderInstanceResponse (decrypted, array types).
Reload full list after save to ensure correct data and sort order.
- Create OrderTable.vue with shared cell rendering (ID, order number,
amount, payment method, status badge, created time)
- Accepts showUser prop to conditionally show user_email column
- Actions column uses scoped slot for view-specific buttons
- UserOrdersView and AdminOrdersView both use OrderTable
- Removes duplicated DataTable cell templates from both views
chore: bump version to 0.1.108.144
Backend:
- Add Keyword field to OrderListParams
- AdminListOrders supports keyword search on out_trade_no, user_email, user_name
- PaymentOrder already has user_email/user_name/user_notes fields (no join needed)
Frontend:
- Replace inline status badge with OrderStatusBadge component
- Replace user_id column with user_email (shows email, fallback to username, with notes)
- Keyword search matches order number and user info
chore: bump version to 0.1.108.143
Previously gwRefund used registry.GetProvider(paymentType) which returns
an arbitrary instance for that type. When multiple instances share the
same payment type (e.g., two EasyPay merchants both supporting alipay),
the refund would be sent to the wrong merchant.
Now getRefundProvider() reads the order's ProviderInstanceID, loads that
instance's config, and creates the correct provider. Falls back to
registry lookup for legacy orders without an instance ID.
chore: bump version to 0.1.108.142
- Upgrade Go from 1.26.1 to 1.26.2 in go.mod and Dockerfile to fix
govulncheck findings (GO-2026-4947, GO-2026-4946, GO-2026-4870, etc.)
- Fix flaky PassthroughModeRelaysByCaddyAdapter test: tolerate
StatusNormalClosure error from server after client closes connection
- Don't treat refund_amount as "already refunded" in REFUND_REQUESTED status
(it's the requested amount, not actually refunded)
- Pre-fill refund amount with user's requested amount
- Show "already refunded" only for PARTIALLY_REFUNDED/REFUNDED orders
- Change action buttons from stacked icon+text to compact inline style
Admin order list:
- Add REFUND_REQUESTED "approve refund" button with amount badge
- Add REFUND_FAILED "retry refund" button
- Add missing status filters (REFUND_REQUESTED, REFUND_FAILED)
- Order detail dialog: show refund request info + audit logs
Admin refund dialog:
- Show refund request info card (violet) when user requested
- Display user balance with insufficient balance warning
- Add "no deduction" info when deduct_balance unchecked
- Add force refund checkbox (shown when requireForce=true)
- Add warning display prop
- Pre-fill reason from user's refund request
- Default deduct_balance to true
chore: bump version to 0.1.108.141
Backend:
- Define OrderTypeBalance/Subscription, EntityStatusActive, DeductionType*,
NotificationStatus* constants in payment/types.go
- Replace all magic strings in payment_order, payment_fulfillment, payment_refund
- Add local constants in easypay.go (tradeStatusSuccess, signTypeMD5)
- Add 27 unit tests for load balancer (filterByLimits, pickLeastAmount,
getInstanceChannelLimits, startOfDay)
Frontend:
- Remove all `any` types in SettingsView.vue (18 catch blocks + 1 payload)
- Fix bare catch blocks in PaymentResultView, PaymentView
- Add `unknown` type annotation to all catch blocks
chore: bump version to 0.1.108.140
- Batch-query daily usage in one SQL (GroupBy) instead of N queries
- Include PENDING orders in daily usage to prevent over-committing
- Check remaining capacity (used + orderAmount > limit) not just used >= limit
- Check SingleMin/SingleMax per-channel limits
- Pre-attach usage to candidates, reuse across filter and strategy pick
- Fallback to full list with warn log when all instances exhausted
chore: bump version to 0.1.108.139
- Replace all ¥ / ¥ with $ across all payment views and components
- Add out_trade_no column to admin order list table
- Add order number field to admin order detail dialog
chore: bump version to 0.1.108.138
Check used + orderAmount > dailyLimit (not just used >= limit) and
also filter by SingleMin/SingleMax per-channel limits.
chore: bump version to 0.1.108.137
Previously SelectInstance always used round-robin regardless of the
configured strategy. Now it reads the strategy from payment config
and selects the instance with the lowest daily transaction amount
when "least-amount" is configured.
chore: bump version to 0.1.108.136