mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-23 03:36:30 +00:00
fix(payment): split callback URL into base URL + fixed path, wider dialog
- Callback URLs now split: editable base URL + read-only fixed path - Base URL defaults to current domain, user can change it - notifyUrl/returnUrl computed as baseUrl + fixed webhook path - Edit loads existing base URL by extracting it from stored notifyUrl - Dialog width set to wide for more space - Strict validation: all required fields must be filled on both create/edit
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
<BaseDialog
|
||||
:show="show"
|
||||
:title="editing ? t('admin.settings.payment.editProvider') : t('admin.settings.payment.createProvider')"
|
||||
width="wide"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<form id="provider-form" @submit.prevent="handleSave" class="space-y-4">
|
||||
@@ -90,6 +91,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Callback URL config (base URL + fixed paths) -->
|
||||
<div v-if="callbackPaths" class="mt-4 space-y-3">
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.settings.payment.callbackBaseUrl') }} <span class="text-red-500">*</span></label>
|
||||
<input v-model="callbackBaseUrl" type="text" class="input" :placeholder="defaultBaseUrl" />
|
||||
</div>
|
||||
<div v-if="callbackPaths.notifyUrl" class="flex items-center gap-2">
|
||||
<span class="shrink-0 text-xs text-gray-500 dark:text-gray-400">{{ t('admin.settings.payment.field_notifyUrl') }}:</span>
|
||||
<code class="min-w-0 flex-1 truncate rounded bg-gray-100 px-2 py-1 text-xs text-gray-700 dark:bg-dark-700 dark:text-gray-300">{{ callbackBaseUrl || defaultBaseUrl }}{{ callbackPaths.notifyUrl }}</code>
|
||||
</div>
|
||||
<div v-if="callbackPaths.returnUrl" class="flex items-center gap-2">
|
||||
<span class="shrink-0 text-xs text-gray-500 dark:text-gray-400">{{ t('admin.settings.payment.field_returnUrl') }}:</span>
|
||||
<code class="min-w-0 flex-1 truncate rounded bg-gray-100 px-2 py-1 text-xs text-gray-700 dark:bg-dark-700 dark:text-gray-300">{{ callbackBaseUrl || defaultBaseUrl }}{{ callbackPaths.returnUrl }}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stripe webhook hint -->
|
||||
<div v-if="stripeWebhookUrl" class="mt-3 rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-800/50 dark:bg-blue-900/20">
|
||||
<p class="text-xs text-blue-700 dark:text-blue-300">
|
||||
@@ -160,7 +177,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, computed } from 'vue'
|
||||
import { reactive, computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import Select from '@/components/common/Select.vue'
|
||||
@@ -169,9 +186,11 @@ import type { TypeOption } from './providerConfig'
|
||||
import {
|
||||
PROVIDER_CONFIG_FIELDS,
|
||||
PROVIDER_SUPPORTED_TYPES,
|
||||
PROVIDER_CALLBACK_PATHS,
|
||||
WEBHOOK_PATHS,
|
||||
parseTypes,
|
||||
getAvailableTypes,
|
||||
extractBaseUrl,
|
||||
} from './providerConfig'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -209,14 +228,17 @@ const form = reactive({
|
||||
})
|
||||
const config = reactive<Record<string, string>>({})
|
||||
const limits = reactive<Record<string, Record<string, number>>>({})
|
||||
const callbackBaseUrl = ref('')
|
||||
|
||||
// --- Computed ---
|
||||
const baseURL = typeof window !== 'undefined' ? window.location.origin : ''
|
||||
const defaultBaseUrl = typeof window !== 'undefined' ? window.location.origin : ''
|
||||
|
||||
const stripeWebhookUrl = computed(() =>
|
||||
form.provider_key === 'stripe' ? baseURL + WEBHOOK_PATHS.stripe : '',
|
||||
form.provider_key === 'stripe' ? (callbackBaseUrl.value || defaultBaseUrl) + WEBHOOK_PATHS.stripe : '',
|
||||
)
|
||||
|
||||
const callbackPaths = computed(() => PROVIDER_CALLBACK_PATHS[form.provider_key] || null)
|
||||
|
||||
const availableTypes = computed(() =>
|
||||
getAvailableTypes(form.provider_key, props.allPaymentTypes, props.redirectLabel),
|
||||
)
|
||||
@@ -258,6 +280,7 @@ function onKeyChange() {
|
||||
function clearConfig() {
|
||||
Object.keys(config).forEach(k => delete config[k])
|
||||
Object.keys(limits).forEach(k => delete limits[k])
|
||||
callbackBaseUrl.value = ''
|
||||
}
|
||||
|
||||
function applyDefaults() {
|
||||
@@ -317,6 +340,14 @@ function handleSave() {
|
||||
filteredConfig[k] = v
|
||||
}
|
||||
|
||||
// Inject computed callback URLs from base URL + fixed paths
|
||||
const paths = PROVIDER_CALLBACK_PATHS[form.provider_key]
|
||||
if (paths) {
|
||||
const base = callbackBaseUrl.value || defaultBaseUrl
|
||||
if (paths.notifyUrl) filteredConfig['notifyUrl'] = base + paths.notifyUrl
|
||||
if (paths.returnUrl) filteredConfig['returnUrl'] = base + paths.returnUrl
|
||||
}
|
||||
|
||||
emit('save', {
|
||||
providerKey: form.provider_key,
|
||||
name: form.name,
|
||||
@@ -355,10 +386,16 @@ function loadProvider(provider: ProviderInstance) {
|
||||
// Pre-fill config from API response (non-sensitive in cleartext, sensitive masked as ••••••••)
|
||||
if (provider.config) {
|
||||
for (const [k, v] of Object.entries(provider.config)) {
|
||||
// Skip notifyUrl/returnUrl — they are derived from callbackBaseUrl
|
||||
if (k === 'notifyUrl' || k === 'returnUrl') continue
|
||||
config[k] = v
|
||||
}
|
||||
// Extract base URL from existing notifyUrl
|
||||
const paths = PROVIDER_CALLBACK_PATHS[provider.provider_key]
|
||||
if (paths?.notifyUrl && provider.config['notifyUrl']) {
|
||||
callbackBaseUrl.value = extractBaseUrl(provider.config['notifyUrl'], paths.notifyUrl)
|
||||
}
|
||||
}
|
||||
// Apply defaults for any empty fields with defaultValue
|
||||
applyDefaults()
|
||||
// Parse existing limits
|
||||
if (provider.limits) {
|
||||
|
||||
@@ -17,6 +17,12 @@ export interface TypeOption {
|
||||
label: string
|
||||
}
|
||||
|
||||
/** Callback URL paths for a provider. */
|
||||
export interface CallbackPaths {
|
||||
notifyUrl?: string
|
||||
returnUrl?: string
|
||||
}
|
||||
|
||||
// --- Constants ---
|
||||
|
||||
/** Maps provider key → available payment types. */
|
||||
@@ -37,16 +43,20 @@ export const WEBHOOK_PATHS: Record<string, string> = {
|
||||
|
||||
export const RETURN_PATH = '/payment/result'
|
||||
|
||||
const baseURL = typeof window !== 'undefined' ? window.location.origin : ''
|
||||
/** Fixed callback paths per provider — displayed as read-only after base URL. */
|
||||
export const PROVIDER_CALLBACK_PATHS: Record<string, CallbackPaths> = {
|
||||
easypay: { notifyUrl: WEBHOOK_PATHS.easypay, returnUrl: RETURN_PATH },
|
||||
alipay: { notifyUrl: WEBHOOK_PATHS.alipay, returnUrl: RETURN_PATH },
|
||||
wxpay: { notifyUrl: WEBHOOK_PATHS.wxpay },
|
||||
// stripe: no callback URL config needed (webhook is separate)
|
||||
}
|
||||
|
||||
/** Per-provider config fields with defaults. */
|
||||
/** Per-provider config fields (excludes notifyUrl/returnUrl which are handled separately). */
|
||||
export const PROVIDER_CONFIG_FIELDS: Record<string, ConfigFieldDef[]> = {
|
||||
easypay: [
|
||||
{ key: 'pid', label: 'PID', sensitive: false },
|
||||
{ key: 'pkey', label: 'PKey', sensitive: true },
|
||||
{ key: 'apiBase', label: '', sensitive: false },
|
||||
{ key: 'notifyUrl', label: '', sensitive: false, defaultValue: baseURL + WEBHOOK_PATHS.easypay },
|
||||
{ key: 'returnUrl', label: '', sensitive: false, defaultValue: baseURL + RETURN_PATH },
|
||||
{ key: 'cidAlipay', label: '', sensitive: false, optional: true },
|
||||
{ key: 'cidWxpay', label: '', sensitive: false, optional: true },
|
||||
],
|
||||
@@ -54,8 +64,6 @@ export const PROVIDER_CONFIG_FIELDS: Record<string, ConfigFieldDef[]> = {
|
||||
{ key: 'appId', label: 'App ID', sensitive: false },
|
||||
{ key: 'privateKey', label: '', sensitive: true },
|
||||
{ key: 'publicKey', label: '', sensitive: true },
|
||||
{ key: 'notifyUrl', label: '', sensitive: false, defaultValue: baseURL + WEBHOOK_PATHS.alipay },
|
||||
{ key: 'returnUrl', label: '', sensitive: false, defaultValue: baseURL + RETURN_PATH },
|
||||
],
|
||||
wxpay: [
|
||||
{ key: 'appId', label: 'App ID', sensitive: false },
|
||||
@@ -65,7 +73,6 @@ export const PROVIDER_CONFIG_FIELDS: Record<string, ConfigFieldDef[]> = {
|
||||
{ key: 'publicKey', label: '', sensitive: true },
|
||||
{ key: 'publicKeyId', label: '', sensitive: false, optional: true },
|
||||
{ key: 'certSerial', label: '', sensitive: false, optional: true },
|
||||
{ key: 'notifyUrl', label: '', sensitive: false, defaultValue: baseURL + WEBHOOK_PATHS.wxpay },
|
||||
],
|
||||
stripe: [
|
||||
{ key: 'secretKey', label: '', sensitive: true },
|
||||
@@ -103,3 +110,11 @@ export function getAvailableTypes(
|
||||
const types = PROVIDER_SUPPORTED_TYPES[providerKey] || []
|
||||
return types.map(t => resolveTypeLabel(t, providerKey, allTypes, redirectLabel))
|
||||
}
|
||||
|
||||
/** Extract base URL from a full callback URL by removing the known path suffix. */
|
||||
export function extractBaseUrl(fullUrl: string, path: string): string {
|
||||
if (!fullUrl) return ''
|
||||
if (fullUrl.endsWith(path)) return fullUrl.slice(0, -path.length)
|
||||
// Fallback: try to extract origin
|
||||
try { return new URL(fullUrl).origin } catch { return fullUrl }
|
||||
}
|
||||
|
||||
@@ -4363,6 +4363,7 @@ export default {
|
||||
field_apiBase: 'API Base URL',
|
||||
field_notifyUrl: 'Notify URL',
|
||||
field_returnUrl: 'Return URL',
|
||||
callbackBaseUrl: 'Callback Base URL',
|
||||
field_privateKey: 'Private Key',
|
||||
field_publicKey: 'Public Key',
|
||||
field_mchId: 'Merchant ID',
|
||||
|
||||
@@ -4536,6 +4536,7 @@ export default {
|
||||
field_apiBase: 'API 基础地址',
|
||||
field_notifyUrl: '异步通知地址',
|
||||
field_returnUrl: '同步跳转地址',
|
||||
callbackBaseUrl: '回调基础地址',
|
||||
field_privateKey: '私钥',
|
||||
field_publicKey: '公钥',
|
||||
field_mchId: '商户号',
|
||||
|
||||
Reference in New Issue
Block a user