feat(channel): 新增银联商务支付配置功能

- 新增银联商务支付配置接口定义
- 实现银联商务支付配置编辑页面
- 支持子商户配置和微信认证配置
- 添加表单校验和数据初始化逻辑
- 集成到渠道配置编辑页面中
- 支持配置的保存、更新和获取功能
This commit is contained in:
bootx
2025-11-26 20:51:08 +08:00
parent 277ceea631
commit ef44d700a8
7 changed files with 436 additions and 24 deletions

View File

@@ -0,0 +1,51 @@
import { defHttp } from '@/utils/http/axios'
import { Result } from '#/axios'
import { MchEntity } from '#/web'
/**
* 获取单条
*/
export function getConfig(appId) {
return defHttp.get<Result<UmsPayConfig>>({
url: '/ums/config/findByAppId',
params: { appId },
})
}
/**
* 保存或更新
*/
export function update(obj: UmsPayConfig) {
return defHttp.post({
url: '/ums/config/update',
data: obj,
})
}
/**
* 银联商务配置
*/
export interface UmsPayConfig extends MchEntity {
// 应用ID
umsAppId?: string
// 应用密钥
appKey?: string
// 商户号
merchantNo?: string
// 终端号
terminalNo?: string
// 订单号前缀
orderPrefix?: string
// 密钥
secretKey?: string
// 是否启用
enable: boolean
// 是否沙箱环境
sandbox?: boolean
// 微信AppId
wxAppId?: string
// 微信AppSecret
wxAppSecret?: string
// 微信授权认证地址
wxAuthUrl?: string
}

View File

@@ -0,0 +1,209 @@
<template>
<basic-drawer
showFooter
v-bind="$attrs"
width="60%"
title="银联商务配置"
:open="visible"
:maskClosable="false"
@close="handleCancel"
>
<a-spin :spinning="confirmLoading">
<a-form
class="small-from-item"
ref="formRef"
:model="form"
:rules="rules"
:validate-trigger="['blur', 'change']"
:label-col="labelCol"
:wrapper-col="wrapperCol"
>
<a-divider>子商户配置</a-divider>
<a-form-item label="主键" name="id" :hidden="true">
<a-input v-model:value="form.id" />
</a-form-item>
<a-form-item label="是否启用" name="enable">
<a-switch
checked-children="启用"
un-checked-children="停用"
v-model:checked="form.enable"
/>
</a-form-item>
<a-form-item label="沙箱环境" name="sandbox">
<a-switch checked-children="" un-checked-children="" v-model:checked="form.sandbox" />
</a-form-item>
<a-form-item label="应用ID(appId)" name="umsAppId">
<a-input v-model:value="form.umsAppId" placeholder="请输入应用ID" />
</a-form-item>
<a-form-item label="应用密钥(appKey)" name="appKey">
<a-input v-model:value="form.appKey" placeholder="请输入应用密钥" />
</a-form-item>
<a-form-item label="商户号" name="merchantNo">
<a-input v-model:value="form.merchantNo" placeholder="请输入商户号" />
</a-form-item>
<a-form-item label="终端号" name="terminalNo">
<a-input v-model:value="form.terminalNo" placeholder="请输入终端号" />
</a-form-item>
<a-form-item
label="订单号前缀"
name="orderPrefix"
tooltip="需要以银商分配的4位来源编号msgSrcId作为订单号的前4位相当于订单号前缀"
>
<a-input v-model:value="form.orderPrefix" placeholder="请输入订单号前缀" />
</a-form-item>
<a-form-item label="通信密钥" name="secretKey" tooltip="用于回调消息验签">
<a-input v-model:value="form.secretKey" placeholder="请输入通信密钥" />
</a-form-item>
<a-divider>微信认证配置</a-divider>
<a-form-item label="微信AppId" name="wxAppId">
<a-input
v-model:value="form.wxAppId"
:disabled="showable"
placeholder="请输入微信应用AppId"
/>
</a-form-item>
<a-form-item label="微信AppSecret" name="wxAppSecret">
<a-input
v-model:value="form.wxAppSecret"
:disabled="showable"
placeholder="请输入微信应用wxAppSecret"
/>
</a-form-item>
<a-form-item
name="wxAuthUrl"
label="微信授权认证地址"
tooltip="该地址需要重定向或转发到网关前端的地址,用于进行微信认证(置空将读取平台配置中的网关前端地址)"
>
<a-input
v-model:value="form.wxAuthUrl"
:disabled="showable"
placeholder="请输入微信OAuth2认证地址"
/>
</a-form-item>
</a-form>
</a-spin>
<template #footer>
<a-space>
<a-button key="cancel" @click="handleCancel">取消</a-button>
<a-button
v-if="!showable"
key="forward"
:loading="confirmLoading"
type="primary"
@click="handleOk"
>保存</a-button
>
</a-space>
</template>
</basic-drawer>
</template>
<script lang="ts" setup>
import { computed, nextTick, ref } from 'vue'
import useFormEdit from '@/hooks/bootx/useFormEdit'
import { update, getConfig, UmsPayConfig } from './UmsPayConfig.api'
import { FormInstance, Rule } from 'ant-design-vue/lib/form'
import { useMessage } from '@/hooks/web/useMessage'
import { BasicDrawer } from '@/components/Drawer'
import { ChannelConfig } from '@/views/daxpay/common/merchant/config/ChannelConfig.api'
import { dropdownByMchAndChannel } from '@/views/daxpay/common/onboarded/mch/OnbMchInfo.api'
import { LabeledValue } from 'ant-design-vue/lib/select'
const { handleCancel, diffForm, labelCol, wrapperCol, confirmLoading, visible, showable } =
useFormEdit()
const { createMessage } = useMessage()
const formRef = ref<FormInstance>()
const channelConfig = ref<ChannelConfig>({})
const umsAppIdList = ref<LabeledValue[]>([])
const form = ref<UmsPayConfig>({
enable: true,
sandbox: false,
})
let rawForm: any = {}
// 校验
const rules = computed(() => {
return {
umsAppId: [{ required: true, message: '请输入应用ID' }],
appKey: [{ required: true, message: '请输入应用密钥' }],
merchantNo: [{ required: true, message: '请输入商户号' }],
terminalNo: [{ required: true, message: '请输入终端号' }],
orderPrefix: [{ required: true, message: '请输入订单号前缀' }],
enable: [{ required: true, message: '请选择是否启用' }],
sandbox: [{ required: true, message: '请选择是否为沙箱环境' }],
secretKey: [{ required: true, message: '请输入通信密钥' }],
} as Record<string, Rule[]>
})
// 事件
const emits = defineEmits(['ok'])
/**
* 入口
*/
function init(config: ChannelConfig) {
channelConfig.value = config
initData()
resetForm()
visible.value = true
getInfo()
}
/**
* 初始化数据
*/
function initData() {
dropdownByMchAndChannel(channelConfig.value.mchNo, channelConfig.value.channel).then(
({ data }) => {
umsAppIdList.value = data
},
)
}
/**
* 获取信息
*/
function getInfo() {
getConfig(channelConfig.value.appId).then(({ data }) => {
confirmLoading.value = true
rawForm = { ...data }
form.value = data
confirmLoading.value = false
})
}
/**
* 更新
*/
function handleOk() {
formRef.value?.validate().then(() => {
confirmLoading.value = true
update({
...form.value,
...diffForm(rawForm, form.value, 'appKey', 'secretKey'),
mchNo: channelConfig.value.mchNo,
appId: channelConfig.value.appId,
})
.then(() => {
createMessage.success('保存成功')
handleCancel()
emits('ok')
})
.finally(() => {
confirmLoading.value = false
})
})
}
/**
* 重置表单
*/
function resetForm() {
nextTick(() => {
formRef.value?.resetFields()
})
}
defineExpose({
init,
})
</script>
<style lang="less" scoped></style>

View File

@@ -10,7 +10,15 @@
:wrapperCol="{ span: 18 }"
:validate-trigger="['blur', 'change']"
>
<a-form-item label="商户" name="mchNo">
<a-form-item label="商户私钥" required>
<a-space>
<a-button size="small" @click="showPrivateKeyModal" type="primary">设置私钥</a-button>
<a-button size="small" v-if="privateKey" @click="clearPrivateKey" danger
>清除私钥</a-button
>
</a-space>
</a-form-item>
<a-form-item label="商户" name="mchNo">
<a-select
show-search
:filter-option="search"
@@ -20,7 +28,7 @@
@change="merchantChange"
/>
</a-form-item>
<a-form-item label="应用" name="appId">
<a-form-item label="应用" name="appId">
<a-select
show-search
:filter-option="search"
@@ -125,12 +133,27 @@
</a-space>
</a-form>
</a-spin>
<!-- 设置私钥弹窗 -->
<a-modal
v-model:visible="privateKeyVisible"
title="设置商户私钥"
@ok="savePrivateKey"
:maskClosable="false"
>
<a-textarea
v-model:value="privateKeyInput"
placeholder="请输入商户私钥"
:rows="6"
allow-clear
/>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { FormInstance, Rule } from 'ant-design-vue/lib/form'
import { Modal } from 'ant-design-vue'
import { Modal, message } from 'ant-design-vue'
import { GatewayPayParam, gatewaySign, gateway } from './DevelopTrade.api'
import { LabeledValue } from 'ant-design-vue/lib/select'
import useFormEdit from '@/hooks/bootx/useFormEdit'
@@ -144,6 +167,9 @@
const { search } = useFormEdit()
const { dictDropDown } = useDict()
// 商户私钥存储在localStorage中的键名
const PRIVATE_KEY_STORAGE_KEY = 'daxpay_gateway_private_key'
const confirmLoading = ref(false)
const formRef = ref<FormInstance>()
const form = reactive<GatewayPayParam>({
@@ -151,10 +177,16 @@
clientIp: '127.0.0.1',
amount: 0.1,
})
// 商户私钥相关状态
const privateKey = ref<string>('')
const privateKeyVisible = ref<boolean>(false)
const privateKeyInput = ref<string>('')
const rules = computed(() => {
return {
mchNo: [{ required: true, message: '商户不可为空' }],
appId: [{ required: true, message: '应用不可为空' }],
mchNo: [{ required: true, message: '商户不可为空' }],
appId: [{ required: true, message: '应用不可为空' }],
gatewayPayType: [{ required: true, message: '网关支付方式不可为空' }],
bizOrderNo: [{ required: true, message: '订单号不可为空' }],
title: [{ required: true, message: '支付标题不可为空' }],
@@ -170,6 +202,11 @@
const gatewayTypeOptions = ref<LabeledValue[]>([])
onMounted(() => {
// 从localStorage中读取私钥
const savedPrivateKey = localStorage.getItem(PRIVATE_KEY_STORAGE_KEY)
if (savedPrivateKey) {
privateKey.value = savedPrivateKey
}
initData()
})
@@ -227,12 +264,45 @@
*/
function getSign() {
formRef.value?.validate().then(() => {
gatewaySign(form).then(({ data }) => {
gatewaySign(form, privateKey.value).then(({ data }) => {
form.sign = data
})
})
}
/**
* 显示设置私钥弹窗
*/
function showPrivateKeyModal() {
privateKeyInput.value = privateKey.value || ''
privateKeyVisible.value = true
}
/**
* 保存私钥
*/
function savePrivateKey() {
privateKey.value = privateKeyInput.value
// 保存到localStorage
if (privateKey.value) {
localStorage.setItem(PRIVATE_KEY_STORAGE_KEY, privateKey.value)
} else {
localStorage.removeItem(PRIVATE_KEY_STORAGE_KEY)
}
privateKeyVisible.value = false
message.success('私钥保存成功')
}
/**
* 清除私钥
*/
function clearPrivateKey() {
privateKey.value = ''
// 从localStorage中移除
localStorage.removeItem(PRIVATE_KEY_STORAGE_KEY)
message.success('私钥已清除')
}
/**
* 重置
*/
@@ -247,7 +317,7 @@
function handleSubmit() {
formRef.value?.validate().then(() => {
confirmLoading.value = true
gateway(form)
gateway(form, privateKey.value)
.then(({ data }) => {
Modal.info({
title: '响应结果',

View File

@@ -13,12 +13,12 @@
<a-form-item label="商户私钥" required>
<a-space>
<a-button size="small" @click="showPrivateKeyModal" type="primary">设置私钥</a-button>
<a-button size="small" v-if="privateKey" @click="privateKey = ''" danger
<a-button size="small" v-if="privateKey" @click="clearPrivateKey" danger
>清除私钥</a-button
>
</a-space>
</a-form-item>
<a-form-item label="商户" name="mchNo">
<a-form-item label="商户" name="mchNo">
<a-select
show-search
:filter-option="search"
@@ -28,7 +28,7 @@
@change="merchantChange"
/>
</a-form-item>
<a-form-item label="应用" name="appId">
<a-form-item label="应用" name="appId">
<a-select
show-search
:filter-option="search"
@@ -178,7 +178,7 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { FormInstance, Rule } from 'ant-design-vue/lib/form'
import { Modal } from 'ant-design-vue'
import { Modal, message } from 'ant-design-vue'
import { PayParam, paySign, tradePay } from './DevelopTrade.api'
import { LabeledValue } from 'ant-design-vue/lib/select'
import { dropdownByEnable as dropdownByEnable } from '@/views/daxpay/common/assist/basic/MerchantQuery.api'
@@ -193,6 +193,9 @@
const { search } = useFormEdit()
const { dictDropDown } = useDict()
// 商户私钥存储在localStorage中的键名
const PRIVATE_KEY_STORAGE_KEY = 'daxpay_pay_private_key'
// 添加商户私钥相关状态
const privateKey = ref<string>('')
const privateKeyVisible = ref<boolean>(false)
@@ -207,7 +210,7 @@
})
const rules = computed(() => {
return {
mchNo: [{ required: true, message: '商户不可为空' }],
mchNo: [{ required: true, message: '商户不可为空' }],
channel: [{ required: true, message: '支付通道不可为空' }],
bizOrderNo: [{ required: true, message: '订单号不可为空' }],
title: [{ required: true, message: '支付标题不可为空' }],
@@ -229,6 +232,11 @@
const methodOptions = ref<LabeledValue[]>([])
onMounted(() => {
// 从localStorage中读取私钥
const savedPrivateKey = localStorage.getItem(PRIVATE_KEY_STORAGE_KEY)
if (savedPrivateKey) {
privateKey.value = savedPrivateKey
}
initData()
})
@@ -306,7 +314,24 @@
*/
function savePrivateKey() {
privateKey.value = privateKeyInput.value
// 保存到localStorage
if (privateKey.value) {
localStorage.setItem(PRIVATE_KEY_STORAGE_KEY, privateKey.value)
} else {
localStorage.removeItem(PRIVATE_KEY_STORAGE_KEY)
}
privateKeyVisible.value = false
message.success('私钥保存成功')
}
/**
* 清除私钥
*/
function clearPrivateKey() {
privateKey.value = ''
// 从localStorage中移除
localStorage.removeItem(PRIVATE_KEY_STORAGE_KEY)
message.success('私钥已清除')
}
/**

View File

@@ -13,12 +13,12 @@
<a-form-item label="商户私钥" required>
<a-space>
<a-button size="small" @click="showPrivateKeyModal" type="primary">设置私钥</a-button>
<a-button size="small" v-if="privateKey" @click="privateKey = ''" danger
<a-button size="small" v-if="privateKey" @click="clearPrivateKey" danger
>清除私钥</a-button
>
</a-space>
</a-form-item>
<a-form-item label="商户" name="mchNo">
<a-form-item label="商户" name="mchNo">
<a-select
show-search
:filter-option="search"
@@ -28,7 +28,7 @@
@change="merchantChange"
/>
</a-form-item>
<a-form-item label="应用" name="appId">
<a-form-item label="应用" name="appId">
<a-select
show-search
:filter-option="search"
@@ -128,7 +128,7 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { FormInstance, Rule } from 'ant-design-vue/lib/form'
import { Modal } from 'ant-design-vue'
import { Modal, message } from 'ant-design-vue'
import { refundSign, RefundParam, tradeRefund } from './DevelopTrade.api'
import { LabeledValue } from 'ant-design-vue/lib/select'
import { dropdownByEnable as dropdownByEnable } from '@/views/daxpay/common/assist/basic/MerchantQuery.api'
@@ -139,6 +139,9 @@
const { search } = useFormEdit()
// 商户私钥存储在localStorage中的键名
const PRIVATE_KEY_STORAGE_KEY = 'daxpay_refund_private_key'
// 添加商户私钥相关状态
const privateKey = ref<string>('')
const privateKeyVisible = ref<boolean>(false)
@@ -152,8 +155,8 @@
})
const rules = computed(() => {
return {
mchNo: [{ required: true, message: '商户不可为空' }],
appId: [{ required: true, message: '应用不可为空' }],
mchNo: [{ required: true, message: '商户不可为空' }],
appId: [{ required: true, message: '应用不可为空' }],
bizRefundNo: [{ required: true, message: '商户退款号不可为空' }],
title: [{ required: true, message: '退款标题不可为空' }],
amount: [{ required: true, message: '退款金额不可为空' }],
@@ -167,6 +170,11 @@
const mchAppOptions = ref<LabeledValue[]>([])
onMounted(() => {
// 从localStorage中读取私钥
const savedPrivateKey = localStorage.getItem(PRIVATE_KEY_STORAGE_KEY)
if (savedPrivateKey) {
privateKey.value = savedPrivateKey
}
initData()
})
@@ -238,7 +246,24 @@
*/
function savePrivateKey() {
privateKey.value = privateKeyInput.value
// 保存到localStorage
if (privateKey.value) {
localStorage.setItem(PRIVATE_KEY_STORAGE_KEY, privateKey.value)
} else {
localStorage.removeItem(PRIVATE_KEY_STORAGE_KEY)
}
privateKeyVisible.value = false
message.success('私钥保存成功')
}
/**
* 清除私钥
*/
function clearPrivateKey() {
privateKey.value = ''
// 从localStorage中移除
localStorage.removeItem(PRIVATE_KEY_STORAGE_KEY)
message.success('私钥已清除')
}
/**

View File

@@ -13,12 +13,12 @@
<a-form-item label="商户私钥" required>
<a-space>
<a-button size="small" @click="showPrivateKeyModal" type="primary">设置私钥</a-button>
<a-button size="small" v-if="privateKey" @click="privateKey = ''" danger
<a-button size="small" v-if="privateKey" @click="clearPrivateKey" danger
>清除私钥</a-button
>
</a-space>
</a-form-item>
<a-form-item label="商户" name="mchNo">
<a-form-item label="商户" name="mchNo">
<a-select
show-search
:filter-option="search"
@@ -28,7 +28,7 @@
@change="merchantChange"
/>
</a-form-item>
<a-form-item label="应用" name="appId">
<a-form-item label="应用" name="appId">
<a-select
show-search
:filter-option="search"
@@ -144,7 +144,7 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { FormInstance, Rule } from 'ant-design-vue/lib/form'
import { Modal } from 'ant-design-vue'
import { Modal, message } from 'ant-design-vue'
import { transferSign, TransferParam, tradeTransfer } from './DevelopTrade.api'
import { LabeledValue } from 'ant-design-vue/lib/select'
import { dropdownByEnable as dropdownByEnable } from '@/views/daxpay/common/assist/basic/MerchantQuery.api'
@@ -157,6 +157,9 @@
const { search } = useFormEdit()
const { dictDropDown } = useDict()
// 商户私钥存储在localStorage中的键名
const PRIVATE_KEY_STORAGE_KEY = 'daxpay_transfer_private_key'
// 添加商户私钥相关状态
const privateKey = ref<string>('')
const privateKeyVisible = ref<boolean>(false)
@@ -170,8 +173,8 @@
})
const rules = computed(() => {
return {
mchNo: [{ required: true, message: '商户不可为空' }],
appId: [{ required: true, message: '应用不可为空' }],
mchNo: [{ required: true, message: '商户不可为空' }],
appId: [{ required: true, message: '应用不可为空' }],
channel: [{ required: true, message: '支付通道不可为空' }],
bizTransferNo: [{ required: true, message: '商户转账号不可为空' }],
title: [{ required: true, message: '转账标题不可为空' }],
@@ -190,6 +193,11 @@
const payeeTypeOptions = ref<LabeledValue[]>([])
onMounted(() => {
// 从localStorage中读取私钥
const savedPrivateKey = localStorage.getItem(PRIVATE_KEY_STORAGE_KEY)
if (savedPrivateKey) {
privateKey.value = savedPrivateKey
}
initData()
})
@@ -262,7 +270,24 @@
*/
function savePrivateKey() {
privateKey.value = privateKeyInput.value
// 保存到localStorage
if (privateKey.value) {
localStorage.setItem(PRIVATE_KEY_STORAGE_KEY, privateKey.value)
} else {
localStorage.removeItem(PRIVATE_KEY_STORAGE_KEY)
}
privateKeyVisible.value = false
message.success('私钥保存成功')
}
/**
* 清除私钥
*/
function clearPrivateKey() {
privateKey.value = ''
// 从localStorage中移除
localStorage.removeItem(PRIVATE_KEY_STORAGE_KEY)
message.success('私钥已清除')
}
/**

View File

@@ -29,6 +29,7 @@
ref="dougongSub"
@ok="ok"
/>
<ums-pay-config-edit v-if="record.channel === ChannelEnum.UMS_PAY" ref="umsPay" @ok="ok" />
</template>
<script setup lang="ts">
import { nextTick, ref } from 'vue'
@@ -49,6 +50,7 @@
import SandSubConfigEdit from '@/views/daxpay/common/channel/sand/config/payment/SandSubConfigEdit.vue'
import YeePaySubConfigEdit from '@/views/daxpay/common/channel/yeepay/config/payment/YeePaySubConfigEdit.vue'
import DougongSubConfigEdit from '@/views/daxpay/common/channel/dougong/config/payment/DougongSubConfigEdit.vue'
import UmsPayConfigEdit from '@/views/daxpay/common/channel/ums/config/UmsPayConfigEdit.vue'
const { createMessage } = useMessage()
@@ -67,6 +69,7 @@
const sandSub = ref<any>()
const yeePaySub = ref<any>()
const dougongSub = ref<any>()
const umsPay = ref<any>()
// 事件
const emits = defineEmits(['ok'])
@@ -133,6 +136,10 @@
dougongSub.value.init(record.value)
break
}
case ChannelEnum.UMS_PAY: {
umsPay.value.init(record.value)
break
}
default: {
createMessage.info('暂未支持, 请期待...')
}