mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4510,8 +4510,8 @@ export default {
|
||||
description: '配置支付系统选项',
|
||||
enabled: '启用支付',
|
||||
enabledHint: '启用或禁用支付系统',
|
||||
enabledPaymentTypes: '服务商',
|
||||
enabledPaymentTypesHint: '选择要启用的服务商',
|
||||
enabledPaymentTypes: '支持的支付方式',
|
||||
enabledPaymentTypesHint: '选择要启用的支付方式',
|
||||
minAmount: '最低金额',
|
||||
maxAmount: '最高金额',
|
||||
dailyLimit: '每日限额',
|
||||
|
||||
@@ -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('')
|
||||
|
||||
Reference in New Issue
Block a user