425 lines
13 KiB
Vue
425 lines
13 KiB
Vue
<script setup lang="ts">
|
||
import { ElMessage } from 'element-plus'
|
||
import { computed, ref, watch } from 'vue'
|
||
|
||
import {
|
||
createPaymentConfig,
|
||
updatePaymentConfig,
|
||
fetchPaymentConfig,
|
||
type PaymentConfig,
|
||
type CreatePaymentConfigRequest,
|
||
type UpdatePaymentConfigRequest,
|
||
} from '@/features/admin/api/paymentConfig'
|
||
import { readError } from '@/shared/utils/error'
|
||
|
||
const props = defineProps<{
|
||
modelValue: boolean
|
||
mode: 'create' | 'edit' | 'view'
|
||
config: PaymentConfig | null
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'update:modelValue', val: boolean): void
|
||
(e: 'close'): void
|
||
(e: 'saved'): void
|
||
}>()
|
||
|
||
const submitting = ref(false)
|
||
const showSecrets = ref(false)
|
||
const loadingSecrets = ref(false)
|
||
const advancedOpen = ref<string[]>([])
|
||
|
||
const defaultGateways: Record<string, string> = {
|
||
leshua: 'https://paygate.leshuazf.com/cgi-bin/lepos_pay_gateway.cgi',
|
||
lakala: 'https://test.wsmsd.cn/sit',
|
||
mock: '',
|
||
}
|
||
|
||
function defaultExtraConfig(provider: string) {
|
||
if (provider === 'lakala') {
|
||
return {
|
||
app_id: '',
|
||
serial_no: '',
|
||
term_no: '',
|
||
lakala_cert: '',
|
||
pay_mode: 'AUTO',
|
||
order_expire_minutes: 15,
|
||
}
|
||
}
|
||
return {}
|
||
}
|
||
|
||
function makeDefaultForm(provider = 'leshua'): CreatePaymentConfigRequest {
|
||
return {
|
||
name: '',
|
||
provider,
|
||
merchant_id: '',
|
||
gateway_url: defaultGateways[provider] || '',
|
||
sign_key: '',
|
||
notify_key: '',
|
||
notify_url: '',
|
||
jump_url: '',
|
||
pay_way: 'ZFBZF',
|
||
jspay_flag: '2',
|
||
sign_type: provider === 'lakala' ? 'SHA256withRSA' : 'MD5',
|
||
extra_config: defaultExtraConfig(provider),
|
||
is_default: false,
|
||
status: 'active',
|
||
environment: provider === 'lakala' ? 'sandbox' : 'production',
|
||
business_tags: [],
|
||
}
|
||
}
|
||
|
||
const formData = ref<CreatePaymentConfigRequest>(makeDefaultForm())
|
||
|
||
const dialogTitle = computed(() => {
|
||
const titles = {
|
||
create: '新增支付配置',
|
||
edit: '编辑支付配置',
|
||
view: '查看支付配置',
|
||
}
|
||
return titles[props.mode]
|
||
})
|
||
|
||
const isReadonly = computed(() => props.mode === 'view')
|
||
const isLakala = computed(() => formData.value.provider === 'lakala')
|
||
const isMock = computed(() => formData.value.provider === 'mock')
|
||
const signKeyLabel = computed(() => (isLakala.value ? '商户私钥' : '签名密钥'))
|
||
const notifyKeyLabel = computed(() => (isLakala.value ? '通知证书' : '通知密钥'))
|
||
const extraConfig = computed<Record<string, any>>(() => formData.value.extra_config || {})
|
||
|
||
function normalizeExtraConfig(provider: string, extraConfig?: Record<string, any> | null) {
|
||
const extra = {
|
||
...defaultExtraConfig(provider),
|
||
...(extraConfig || {}),
|
||
}
|
||
if (provider === 'lakala' && !extra.pay_mode) {
|
||
extra.pay_mode = 'AUTO'
|
||
}
|
||
return extra
|
||
}
|
||
|
||
watch(
|
||
() => props.modelValue,
|
||
val => {
|
||
if (val && props.config) {
|
||
formData.value = {
|
||
name: props.config.name,
|
||
provider: props.config.provider,
|
||
merchant_id: props.config.merchant_id,
|
||
gateway_url: props.config.gateway_url,
|
||
notify_url: props.config.notify_url,
|
||
jump_url: props.config.jump_url,
|
||
pay_way: props.config.pay_way,
|
||
jspay_flag: props.config.jspay_flag,
|
||
sign_type: props.config.sign_type,
|
||
extra_config: normalizeExtraConfig(props.config.provider, props.config.extra_config),
|
||
is_default: props.config.is_default,
|
||
status: props.config.status,
|
||
environment: props.config.environment,
|
||
business_tags: props.config.business_tags || [],
|
||
}
|
||
showSecrets.value = false
|
||
advancedOpen.value = []
|
||
} else if (val && !props.config) {
|
||
// 重置表单
|
||
formData.value = makeDefaultForm()
|
||
showSecrets.value = false
|
||
advancedOpen.value = []
|
||
}
|
||
},
|
||
{ immediate: true }
|
||
)
|
||
|
||
function handleProviderChange(provider: string) {
|
||
if (props.mode !== 'create') return
|
||
const currentName = formData.value.name
|
||
const currentNotifyURL = formData.value.notify_url
|
||
formData.value = {
|
||
...makeDefaultForm(provider),
|
||
name: currentName,
|
||
notify_url: currentNotifyURL,
|
||
}
|
||
}
|
||
|
||
async function handleViewSecrets() {
|
||
if (!props.config) return
|
||
loadingSecrets.value = true
|
||
try {
|
||
const config = await fetchPaymentConfig(props.config.id, true)
|
||
formData.value.sign_key = config.sign_key || ''
|
||
formData.value.notify_key = config.notify_key || ''
|
||
showSecrets.value = true
|
||
ElMessage.success('密钥已加载')
|
||
} catch (error) {
|
||
ElMessage.error(readError(error, '加载密钥失败'))
|
||
} finally {
|
||
loadingSecrets.value = false
|
||
}
|
||
}
|
||
|
||
async function handleSave() {
|
||
submitting.value = true
|
||
try {
|
||
if (props.mode === 'create') {
|
||
await createPaymentConfig(formData.value)
|
||
ElMessage.success('创建成功')
|
||
} else if (props.mode === 'edit') {
|
||
if (!props.config) return
|
||
const payload: UpdatePaymentConfigRequest = {
|
||
name: formData.value.name,
|
||
merchant_id: formData.value.merchant_id,
|
||
gateway_url: formData.value.gateway_url,
|
||
notify_url: formData.value.notify_url,
|
||
jump_url: formData.value.jump_url,
|
||
pay_way: formData.value.pay_way,
|
||
jspay_flag: formData.value.jspay_flag,
|
||
sign_type: formData.value.sign_type,
|
||
extra_config: formData.value.extra_config,
|
||
is_default: formData.value.is_default,
|
||
status: formData.value.status,
|
||
environment: formData.value.environment,
|
||
business_tags: formData.value.business_tags,
|
||
}
|
||
// 只有在修改了密钥时才传递
|
||
if (formData.value.sign_key) {
|
||
payload.sign_key = formData.value.sign_key
|
||
}
|
||
if (formData.value.notify_key) {
|
||
payload.notify_key = formData.value.notify_key
|
||
}
|
||
await updatePaymentConfig(props.config.id, payload)
|
||
ElMessage.success('更新成功')
|
||
}
|
||
emit('saved')
|
||
} catch (error) {
|
||
ElMessage.error(readError(error, '保存失败'))
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
function handleClose() {
|
||
emit('update:modelValue', false)
|
||
emit('close')
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<el-dialog :model-value="modelValue" :title="dialogTitle" width="700px" @close="handleClose">
|
||
<el-form :model="formData" label-width="120px">
|
||
<el-form-item label="配置名称" required>
|
||
<el-input v-model="formData.name" placeholder="如:乐刷生产商户1" :disabled="isReadonly" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="支付服务商" required>
|
||
<el-select
|
||
v-model="formData.provider"
|
||
:disabled="mode !== 'create'"
|
||
@change="handleProviderChange"
|
||
>
|
||
<el-option label="乐刷支付" value="leshua" />
|
||
<el-option label="拉卡拉支付" value="lakala" />
|
||
<el-option label="模拟支付" value="mock" />
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="商户号" required>
|
||
<el-input v-model="formData.merchant_id" placeholder="渠道商户号" :disabled="isReadonly" />
|
||
</el-form-item>
|
||
|
||
<el-form-item v-if="!isMock" label="网关地址" required>
|
||
<el-input v-model="formData.gateway_url" placeholder="支付网关URL" :disabled="isReadonly" />
|
||
</el-form-item>
|
||
|
||
<el-form-item v-if="isLakala" label="App ID" required>
|
||
<el-input
|
||
v-model="extraConfig.app_id"
|
||
placeholder="拉卡拉开放平台 appId"
|
||
:disabled="isReadonly"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item v-if="isLakala" label="证书序列号" required>
|
||
<el-input
|
||
v-model="extraConfig.serial_no"
|
||
placeholder="商户证书序列号"
|
||
:disabled="isReadonly"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item v-if="isLakala" label="终端号" required>
|
||
<el-input v-model="extraConfig.term_no" placeholder="拉卡拉终端号" :disabled="isReadonly" />
|
||
</el-form-item>
|
||
|
||
<el-form-item v-if="!isMock" :label="signKeyLabel" required>
|
||
<el-input
|
||
v-model="formData.sign_key"
|
||
:type="isLakala ? 'textarea' : 'password'"
|
||
:rows="isLakala ? 5 : undefined"
|
||
:show-password="!isLakala"
|
||
:placeholder="
|
||
mode === 'edit' ? '留空则不修改' : isLakala ? '请输入商户私钥 PEM' : '请输入签名密钥'
|
||
"
|
||
:disabled="isReadonly"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item v-if="!isMock" :label="notifyKeyLabel" required>
|
||
<el-input
|
||
v-model="formData.notify_key"
|
||
:type="isLakala ? 'textarea' : 'password'"
|
||
:rows="isLakala ? 5 : undefined"
|
||
:show-password="!isLakala"
|
||
:placeholder="
|
||
mode === 'edit'
|
||
? '留空则不修改'
|
||
: isLakala
|
||
? '请输入拉卡拉通知验签证书 PEM'
|
||
: '请输入通知密钥'
|
||
"
|
||
:disabled="isReadonly"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item v-if="isLakala" label="平台证书">
|
||
<el-input
|
||
v-model="extraConfig.lakala_cert"
|
||
type="textarea"
|
||
:rows="4"
|
||
placeholder="拉卡拉平台响应验签证书 PEM,可与通知证书一致"
|
||
:disabled="isReadonly"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item v-if="mode === 'view' && !showSecrets" label="">
|
||
<el-button :loading="loadingSecrets" @click="handleViewSecrets">查看密钥</el-button>
|
||
</el-form-item>
|
||
|
||
<el-form-item v-if="!isMock" label="回调地址" required>
|
||
<el-input
|
||
v-model="formData.notify_url"
|
||
placeholder="异步通知回调地址"
|
||
:disabled="isReadonly"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="是否默认">
|
||
<el-switch v-model="formData.is_default" :disabled="isReadonly" />
|
||
<span class="form-hint">全局只能启用一个配置,启用后会自动设为默认</span>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="状态">
|
||
<el-select v-model="formData.status" :disabled="isReadonly">
|
||
<el-option label="启用" value="active" />
|
||
<el-option label="禁用" value="disabled" />
|
||
<el-option label="测试中" value="testing" />
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="环境">
|
||
<el-select v-model="formData.environment" :disabled="isReadonly">
|
||
<el-option label="生产环境" value="production" />
|
||
<el-option label="测试环境" value="sandbox" />
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-collapse v-model="advancedOpen" class="advanced-config">
|
||
<el-collapse-item name="advanced">
|
||
<template #title>
|
||
<span class="advanced-title">高级配置</span>
|
||
</template>
|
||
|
||
<el-form-item label="跳转地址">
|
||
<el-input
|
||
v-model="formData.jump_url"
|
||
placeholder="支付完成跳转地址"
|
||
:disabled="isReadonly"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="支付方式">
|
||
<el-select v-model="formData.pay_way" :disabled="isReadonly">
|
||
<el-option label="支付宝" value="ZFBZF" />
|
||
<el-option label="微信" value="WXZF" />
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item v-if="isLakala" label="拉卡拉模式">
|
||
<el-select v-model="extraConfig.pay_mode" :disabled="isReadonly">
|
||
<el-option label="不限制,由收银台选择" value="AUTO" />
|
||
<el-option label="支付宝" value="ALIPAY" />
|
||
<el-option label="微信" value="WECHAT" />
|
||
<el-option label="银联" value="UQRCODEPAY" />
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item v-if="isLakala" label="有效期分钟">
|
||
<el-input-number
|
||
v-model="extraConfig.order_expire_minutes"
|
||
:min="1"
|
||
:max="1440"
|
||
:disabled="isReadonly"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="支付形态">
|
||
<el-select v-model="formData.jspay_flag" :disabled="isReadonly">
|
||
<el-option label="H5 / JSAPI" value="1" />
|
||
<el-option label="简易支付 / 收银台" value="2" />
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="签名类型">
|
||
<el-select v-model="formData.sign_type" :disabled="isReadonly">
|
||
<el-option label="MD5" value="MD5" />
|
||
<el-option label="SHA256withRSA" value="SHA256withRSA" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-collapse-item>
|
||
</el-collapse>
|
||
</el-form>
|
||
|
||
<template #footer>
|
||
<el-button @click="handleClose">{{ isReadonly ? '关闭' : '取消' }}</el-button>
|
||
<el-button v-if="!isReadonly" type="primary" :loading="submitting" @click="handleSave">
|
||
保存
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.advanced-config {
|
||
margin-top: 4px;
|
||
border-top: 1px solid #edf0f5;
|
||
border-bottom: 0;
|
||
}
|
||
|
||
.advanced-config :deep(.el-collapse-item__header) {
|
||
height: 44px;
|
||
padding-left: 120px;
|
||
border-bottom: 0;
|
||
color: #4b5563;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.advanced-config :deep(.el-collapse-item__wrap) {
|
||
border-bottom: 0;
|
||
}
|
||
|
||
.advanced-config :deep(.el-collapse-item__content) {
|
||
padding-bottom: 0;
|
||
}
|
||
|
||
.advanced-title {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.form-hint {
|
||
margin-left: 8px;
|
||
color: #6b7280;
|
||
font-size: 12px;
|
||
}
|
||
</style>
|