fix: order expiry cancels upstream, subscription layout, popup & label fixes

- ExpireTimedOutOrders now cancels upstream payment (Stripe PaymentIntent)
  before marking order as expired
- Active subscriptions moved below plan list with compact inline style
- StripePaymentView uses minimal layout (no AppLayout) in popup mode
- Fix misleading "服务商" label → "支持的支付方式" for payment type toggles
This commit is contained in:
erio
2026-04-09 02:29:18 +08:00
parent 302e6acbd9
commit a8a98cc5a1
4 changed files with 51 additions and 42 deletions
+19
View File
@@ -436,6 +436,8 @@ func (s *PaymentService) ExpireTimedOutOrders(ctx context.Context) (int, error)
}
n := 0
for _, o := range orders {
// Cancel upstream payment (e.g. Stripe PaymentIntent) before marking expired
s.cancelUpstreamPayment(ctx, o)
c, e := s.entClient.PaymentOrder.Update().Where(paymentorder.IDEQ(o.ID), paymentorder.StatusEQ(OrderStatusPending)).SetStatus(OrderStatusExpired).Save(ctx)
if e != nil {
slog.Warn("expire failed", "orderID", o.ID, "error", e)
@@ -448,3 +450,20 @@ func (s *PaymentService) ExpireTimedOutOrders(ctx context.Context) (int, error)
}
return n, nil
}
// cancelUpstreamPayment attempts to cancel the upstream provider payment (e.g. Stripe PaymentIntent).
func (s *PaymentService) cancelUpstreamPayment(ctx context.Context, o *dbent.PaymentOrder) {
if o.PaymentTradeNo == "" || o.PaymentType == "" {
return
}
s.EnsureProviders(ctx)
prov, err := s.registry.GetProvider(o.PaymentType)
if err != nil {
return
}
if cp, ok := prov.(payment.CancelableProvider); ok {
if err := cp.CancelPayment(ctx, o.PaymentTradeNo); err != nil {
slog.Warn("cancel upstream payment failed", "orderID", o.ID, "tradeNo", o.PaymentTradeNo, "error", err)
}
}
}
+2 -2
View File
@@ -4510,8 +4510,8 @@ export default {
description: '配置支付系统选项',
enabled: '启用支付',
enabledHint: '启用或禁用支付系统',
enabledPaymentTypes: '服务商',
enabledPaymentTypesHint: '选择要启用的服务商',
enabledPaymentTypes: '支持的支付方式',
enabledPaymentTypesHint: '选择要启用的支付方式',
minAmount: '最低金额',
maxAmount: '最高金额',
dailyLimit: '每日限额',
+23 -36
View File
@@ -96,42 +96,6 @@
</template>
<!-- Subscribe Tab -->
<template v-else-if="activeTab === 'subscription'">
<!-- Active Subscription Card -->
<div v-if="activeSubscriptions.length > 0" class="space-y-3">
<p class="text-xs font-medium text-gray-400 dark:text-gray-500">{{ t('payment.activeSubscription') }}</p>
<div v-for="sub in activeSubscriptions" :key="sub.id"
class="card overflow-hidden">
<div :class="['h-1', platformAccentBarClass(sub.group?.platform || '')]" />
<div class="p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">
{{ sub.group?.name || `Group #${sub.group_id}` }}
</h4>
<span :class="['rounded-full px-2 py-0.5 text-[10px] font-medium', platformBadgeLightClass(sub.group?.platform || '')]">
{{ platformLabel(sub.group?.platform || '') }}
</span>
</div>
<span class="badge badge-success text-xs">{{ t('userSubscriptions.status.active') }}</span>
</div>
<div class="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs text-gray-500 dark:text-gray-400">
<span>{{ t('payment.planCard.rate') }}: ×{{ sub.group?.rate_multiplier ?? 1 }}</span>
<span v-if="sub.group?.daily_limit_usd != null">{{ t('payment.planCard.dailyLimit') }}: ${{ sub.group.daily_limit_usd }}</span>
<span v-if="sub.group?.weekly_limit_usd != null">{{ t('payment.planCard.weeklyLimit') }}: ${{ sub.group.weekly_limit_usd }}</span>
<span v-if="sub.group?.monthly_limit_usd != null">{{ t('payment.planCard.monthlyLimit') }}: ${{ sub.group.monthly_limit_usd }}</span>
<span v-if="!sub.group?.daily_limit_usd && !sub.group?.weekly_limit_usd && !sub.group?.monthly_limit_usd">{{ t('payment.planCard.quota') }}: {{ t('payment.planCard.unlimited') }}</span>
</div>
<div class="mt-2 text-xs text-gray-500 dark:text-gray-400">
<span v-if="sub.expires_at">{{ t('userSubscriptions.daysRemaining', { days: getDaysRemaining(sub.expires_at) }) }}</span>
<span v-else>{{ t('userSubscriptions.noExpiration') }}</span>
</div>
</div>
</div>
</div>
<div v-else class="card p-5">
<p class="text-xs font-medium text-gray-400 dark:text-gray-500">{{ t('payment.activeSubscription') }}</p>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ t('payment.noActiveSubscription') }}</p>
</div>
<!-- Subscription confirm (inline, replaces plan list) -->
<template v-if="selectedPlan">
<div class="card overflow-hidden">
@@ -188,6 +152,29 @@
<div v-else :class="planGridClass">
<SubscriptionPlanCard v-for="plan in checkout.plans" :key="plan.id" :plan="plan" @select="selectPlan" />
</div>
<!-- Active subscriptions (compact, below plan list) -->
<div v-if="activeSubscriptions.length > 0">
<p class="mb-2 text-xs font-medium text-gray-400 dark:text-gray-500">{{ t('payment.activeSubscription') }}</p>
<div class="space-y-2">
<div v-for="sub in activeSubscriptions" :key="sub.id"
class="flex items-center gap-3 rounded-xl border border-gray-100 bg-white px-3 py-2 dark:border-dark-700 dark:bg-dark-800">
<div :class="['h-6 w-1 shrink-0 rounded-full', platformAccentBarClass(sub.group?.platform || '')]" />
<div class="min-w-0 flex-1">
<div class="flex items-center gap-1.5">
<span class="truncate text-xs font-semibold text-gray-900 dark:text-white">{{ sub.group?.name || `Group #${sub.group_id}` }}</span>
<span :class="['shrink-0 rounded-full px-1.5 py-0.5 text-[9px] font-medium', platformBadgeLightClass(sub.group?.platform || '')]">{{ platformLabel(sub.group?.platform || '') }}</span>
</div>
<div class="flex flex-wrap gap-x-3 text-[11px] text-gray-400 dark:text-gray-500">
<span>{{ t('payment.planCard.rate') }}: ×{{ sub.group?.rate_multiplier ?? 1 }}</span>
<span v-if="sub.group?.daily_limit_usd == null && sub.group?.weekly_limit_usd == null && sub.group?.monthly_limit_usd == null">{{ t('payment.planCard.quota') }}: {{ t('payment.planCard.unlimited') }}</span>
<span v-if="sub.expires_at">{{ t('userSubscriptions.daysRemaining', { days: getDaysRemaining(sub.expires_at) }) }}</span>
<span v-else>{{ t('userSubscriptions.noExpiration') }}</span>
</div>
</div>
<span class="badge badge-success shrink-0 text-[10px]">{{ t('userSubscriptions.status.active') }}</span>
</div>
</div>
</div>
</template>
</template>
</template>
@@ -1,6 +1,6 @@
<template>
<AppLayout>
<div class="mx-auto max-w-lg space-y-6 py-8">
<component :is="isPopup ? 'div' : AppLayout" :class="isPopup ? 'min-h-screen bg-gray-50 dark:bg-dark-900' : ''">
<div class="mx-auto max-w-lg space-y-6 py-8" :class="isPopup ? 'px-4' : ''">
<div v-if="loading" class="flex items-center justify-center py-20">
<div class="h-8 w-8 animate-spin rounded-full border-4 border-primary-500 border-t-transparent"></div>
</div>
@@ -85,11 +85,11 @@
</div>
</template>
</div>
</AppLayout>
</component>
</template>
<script setup lang="ts">
import { ref, nextTick, onMounted, onUnmounted } from 'vue'
import { ref, computed, nextTick, onMounted, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import { usePaymentStore } from '@/stores/payment'
@@ -105,6 +105,9 @@ const route = useRoute()
const router = useRouter()
const paymentStore = usePaymentStore()
// Popup mode: skip AppLayout when opened with a specific method (alipay/wechat_pay)
const isPopup = computed(() => !!route.query.method)
const loading = ref(true)
const initError = ref('')
const stripeError = ref('')