feat(支付): 新增网联客支付渠道支持

新增网联客小程序支付和定额聚合支付两种支付渠道,包括相关常量定义、图标资源、支付页面展示及后台配置表单。支持在支付页面选择网联客支付方式,并在后台管理页面进行渠道配置。
pull/766/head
HuangShilin 2025-04-21 17:52:31 +08:00
parent 05393006c1
commit d2fe81d97d
7 changed files with 21194 additions and 0 deletions

20840
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1676209433089" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2990" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M608.6 290.3c67.1 0 121.7 50.5 121.7 112.9 0 19.4-5.6 38.4-15.7 55.5-15.3 25-39.8 43.5-69.4 52.3-7.9 2.3-13.9 3.2-19.4 3.2-13 0-23.1-10.2-23.1-23.1 0-13 10.2-23.1 23.1-23.1 0.9 0 2.8 0 5.1-0.9 19.9-5.6 35.6-17.1 44.4-32.4 6-9.7 8.8-20.4 8.8-31.5 0-36.6-33.8-66.6-75-66.6-14.4 0-28.2 3.7-40.7 10.6-21.8 12.5-34.7 33.3-34.7 56v193.9c0 39.3-21.8 75.4-57.9 95.8-19.4 11.1-41.2 16.7-63.4 16.7-67.1 0-121.7-50.5-121.7-112.9 0-19.4 5.6-38.4 15.7-55.5 15.3-25 39.8-43.5 69.4-52.3 8.3-2.3 13.9-3.2 19.4-3.2 13 0 23.1 10.2 23.1 23.1 0 13-10.2 23.1-23.1 23.1-0.9 0-2.8 0-5.1 0.9-19.9 6-35.6 17.6-44.4 32.4-6 9.7-8.8 20.4-8.8 31.5 0 36.6 33.8 66.6 75.4 66.6 14.4 0 28.2-3.7 40.7-10.6 21.8-12.5 34.7-33.3 34.7-56V403.3c0-39.3 21.8-75.4 57.9-95.8 19-11.6 40.7-17.2 63-17.2zM510.8 929c231.1 0 418.4-187.3 418.4-418.4S741.9 92.1 510.8 92.1 92.4 279.5 92.4 510.6 279.7 929 510.8 929z m0 22C267.5 951 70.3 753.8 70.3 510.6S267.5 70.1 510.8 70.1s440.5 197.2 440.5 440.5S754.1 951 510.8 951z" p-id="2991" fill="#4a90e2"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="200px" height="200px" viewBox="0 0 200 200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>网联客定额聚合支付</title>
<g id="网联客定额聚合支付" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect id="背景" fill="#1890FF" x="0" y="0" width="200" height="200" rx="20"></rect>
<g id="图标" transform="translate(40.000000, 40.000000)" fill="#FFFFFF">
<path d="M100,20 C100,8.954305 91.045695,0 80,0 L20,0 C8.954305,0 0,8.954305 0,20 L0,80 C0,91.045695 8.954305,100 20,100 L80,100 C91.045695,100 100,91.045695 100,80 L100,20 Z M90,20 L90,80 C90,85.5228475 85.5228475,90 80,90 L20,90 C14.4771525,90 10,85.5228475 10,80 L10,20 C10,14.4771525 14.4771525,10 20,10 L80,10 C85.5228475,10 90,14.4771525 90,20 Z" id="外框" fill-rule="nonzero"></path>
<rect id="中心方块" x="30" y="30" width="40" height="40" rx="5"></rect>
<circle id="左上圆点" cx="25" cy="25" r="10"></circle>
<circle id="右下圆点" cx="75" cy="75" r="10"></circle>
</g>
<text id="WLK" font-family="Arial-BoldMT, Arial" font-size="24" font-weight="bold" fill="#FFFFFF">
<tspan x="75" y="110">WLK</tspan>
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -148,6 +148,14 @@ export const PayChannelEnum = {
MOCK: { MOCK: {
code: 'mock', code: 'mock',
name: '模拟支付' name: '模拟支付'
},
WLK_LITE: {
code: 'wlk_lite',
name: '网联客小程序支付'
},
WLK_OPEN_QR_CODE: {
code: 'wlk_open_qr_code',
name: '网联客定额聚合支付'
} }
} }

View File

@ -0,0 +1,261 @@
<template>
<div>
<Dialog v-model="dialogVisible" :title="dialogTitle" width="800px">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="120px"
>
<el-form-item label="渠道费率" label-width="180px" prop="feeRate">
<el-input
v-model="formData.feeRate"
:style="{ width: '100%' }"
clearable
placeholder="请输入渠道费率"
>
<template #append>%</template>
</el-input>
</el-form-item>
<el-form-item label="请求地址前缀" label-width="180px" prop="config.prefixUrl">
<el-input
v-model="formData.config.prefixUrl"
:style="{ width: '100%' }"
clearable
placeholder="请输入请求地址前缀"
/>
</el-form-item>
<el-form-item label="公众号APPID" label-width="180px" prop="config.wxAppId">
<el-input
v-model="formData.config.wxAppId"
:style="{ width: '100%' }"
clearable
placeholder="请输入公众号APPID"
/>
</el-form-item>
<el-form-item label-width="180px">
<a
href="https://pay.weixin.qq.com/index.php/extend/merchant_appid/mapay_platform/account_manage"
target="_blank"
>
前往微信商户平台查看 APPID
</a>
</el-form-item>
<el-form-item label="服务商编号" label-width="180px" prop="config.isvNo">
<el-input v-model="formData.config.isvNo" :style="{ width: '100%' }" placeholder="请输入服务商编号" />
</el-form-item>
<el-form-item label="银行编号" label-width="180px" prop="config.bankCode">
<el-input v-model="formData.config.bankCode" :style="{ width: '100%' }" placeholder="请输入银行编号" />
</el-form-item>
<el-form-item label="商户号" label-width="180px" prop="config.bankMerNo">
<el-input v-model="formData.config.bankMerNo" :style="{ width: '100%' }" placeholder="请输入商户号" />
</el-form-item>
<el-form-item label="门店号" label-width="180px" prop="config.bankShopNo">
<el-input v-model="formData.config.bankShopNo" :style="{ width: '100%' }" placeholder="请输入门店号" />
</el-form-item>
<el-form-item label="签名Key" label-width="180px" prop="config.signKey">
<el-input v-model="formData.config.signKey" clearable placeholder="请输入签名Key" />
</el-form-item>
<el-form-item label="系统公钥" label-width="180px" prop="config.sysPublicKey">
<el-input v-model="formData.config.sysPublicKey" clearable placeholder="请输入系统公钥" />
</el-form-item>
<el-form-item label="系统私钥" label-width="180px" prop="config.platPrivateKey">
<el-input v-model="formData.config.platPrivateKey" clearable placeholder="请输入系统私钥" />
</el-form-item>
<el-form-item label="平台公钥" label-width="180px" prop="config.platPublicKey">
<el-input v-model="formData.config.platPublicKey" clearable placeholder="请输入平台公钥" />
</el-form-item>
<el-form-item label="MAC密钥" label-width="180px" prop="config.macKey">
<el-input v-model="formData.config.macKey" clearable placeholder="请输入MAC密钥" />
</el-form-item>
<el-form-item label="机构号" label-width="180px" prop="config.macKey">
<el-input v-model="formData.config.bankOrgNo" clearable placeholder="请输入机构号" />
</el-form-item>
<el-form-item label="渠道状态" label-width="180px" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="parseInt(dict.value)"
:value="parseInt(dict.value)"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" label-width="180px" prop="remark">
<el-input v-model="formData.remark" :style="{ width: '100%' }" />
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</div>
</template>
<script lang="ts" setup>
import { CommonStatusEnum } from '@/utils/constants'
import { DICT_TYPE, getDictOptions } from '@/utils/dict'
import * as ChannelApi from '@/api/pay/channel'
defineOptions({ name: 'WlkChannelForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formData = ref<any>({
appId: '',
code: '',
status: undefined,
feeRate: undefined,
remark: '',
config: {
appId: '',
mchId: '',
apiVersion: '',
mchKey: '',
keyContent: '',
privateKeyContent: '',
certSerialNo: '',
apiV3Key: '',
bank_shop_no: ''
}
})
const formRules = {
feeRate: [{ required: true, message: '请输入渠道费率', trigger: 'blur' }],
status: [{ required: true, message: '渠道状态不能为空', trigger: 'blur' }],
'config.mchId': [{ required: true, message: '请传入商户号', trigger: 'blur' }],
'config.appId': [{ required: true, message: '请输入公众号APPID', trigger: 'blur' }],
'config.apiVersion': [{ required: true, message: 'API版本不能为空', trigger: 'blur' }],
'config.mchKey': [{ required: true, message: '请输入商户密钥', trigger: 'blur' }],
'config.keyContent': [
{ required: true, message: '请上传 apiclient_cert.p12 证书', trigger: 'blur' }
],
'config.privateKeyContent': [
{ required: true, message: '请上传 apiclient_key.pem 证书', trigger: 'blur' }
],
'config.certSerialNo': [{ required: true, message: '请输入证书序列号', trigger: 'blur' }],
'config.apiV3Key': [{ required: true, message: '请上传 api V3 密钥值', trigger: 'blur' }],
'config.bank_shop_no': [{ required: false, message: '请输入门店号', trigger: 'blur' }]
}
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (appId, code) => {
dialogVisible.value = true
formLoading.value = true
resetForm(appId, code)
//
try {
const data = await ChannelApi.getChannel(appId, code)
if (data && data.id) {
formData.value = data
formData.value.config = JSON.parse(data.config)
}
dialogTitle.value = !formData.value.id ? '创建支付渠道' : '编辑支付渠道'
} finally {
formLoading.value = false
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
//
formLoading.value = true
try {
const data = { ...formData.value } as unknown as ChannelApi.ChannelVO
data.config = JSON.stringify(formData.value.config)
if (!data.id) {
await ChannelApi.createChannel(data)
message.success(t('common.createSuccess'))
} else {
await ChannelApi.updateChannel(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = (appId, code) => {
formData.value = {
appId: appId,
code: code,
status: CommonStatusEnum.ENABLE,
feeRate: undefined,
remark: '',
config: {
appId: '',
mchId: '',
apiVersion: '',
mchKey: '',
keyContent: '',
privateKeyContent: '',
certSerialNo: '',
bank_shop_no: '',
apiV3Key: ''
}
}
formRef.value?.resetFields()
}
/**
* apiclient_cert.p12apiclient_key.pem 上传前的校验
*/
const fileBeforeUpload = (file, fileAccept) => {
let format = '.' + file.name.split('.')[1]
if (format !== fileAccept) {
message.error('请上传指定格式"' + fileAccept + '"文件')
return false
}
let isRightSize = file.size / 1024 / 1024 < 2
if (!isRightSize) {
message.error('文件大小超过 2MB')
}
return isRightSize
}
const p12FileBeforeUpload = (file) => {
fileBeforeUpload(file, '.p12')
}
const pemFileBeforeUpload = (file) => {
fileBeforeUpload(file, '.pem')
}
/**
* 读取 apiclient_key.pem privateKeyContent 字段
*/
const privateKeyContentUpload = async (event) => {
const readFile = new FileReader()
readFile.onload = (e: any) => {
formData.value.config.privateKeyContent = e.target.result
}
readFile.readAsText(event.file)
}
/**
* 读取 apiclient_cert.p12 keyContent 字段
*/
const keyContentUpload = async (event) => {
const readFile = new FileReader()
readFile.onload = (e: any) => {
formData.value.config.keyContent = e.target.result.split(',')[1]
}
readFile.readAsDataURL(event.file) // base64
}
</script>

View File

@ -76,6 +76,36 @@
/> />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column align="center" label="网联客配置">
<el-table-column
v-for="channel in wlkChannels"
:key="channel.code"
:label="channel.name.replace('网联客', '')"
align="center"
>
<template #default="scope">
<el-button
v-if="isChannelExists(scope.row.channelCodes, channel.code)"
circle
size="small"
type="success"
@click="openChannelForm(scope.row, channel.code)"
>
<Icon icon="ep:check" />
</el-button>
<el-button
v-else
circle
size="small"
type="danger"
@click="openChannelForm(scope.row, channel.code)"
>
<Icon icon="ep:close" />
</el-button>
</template>
</el-table-column>
</el-table-column>
<el-table-column align="center" label="支付宝配置"> <el-table-column align="center" label="支付宝配置">
<el-table-column <el-table-column
v-for="channel in alipayChannels" v-for="channel in alipayChannels"
@ -134,6 +164,7 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table-column> </el-table-column>
<el-table-column align="center" label="钱包支付配置"> <el-table-column align="center" label="钱包支付配置">
<el-table-column :label="PayChannelEnum.WALLET.name" align="center"> <el-table-column :label="PayChannelEnum.WALLET.name" align="center">
<template #default="scope"> <template #default="scope">
@ -218,6 +249,7 @@
<WeixinChannelForm ref="weixinFormRef" @success="getList" /> <WeixinChannelForm ref="weixinFormRef" @success="getList" />
<MockChannelForm ref="mockFormRef" @success="getList" /> <MockChannelForm ref="mockFormRef" @success="getList" />
<WalletChannelForm ref="walletFormRef" @success="getList" /> <WalletChannelForm ref="walletFormRef" @success="getList" />
<WlkChannelForm ref="wlkFormRef" @success="getList" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
@ -228,6 +260,7 @@ import AlipayChannelForm from './components/channel/AlipayChannelForm.vue'
import WeixinChannelForm from './components/channel/WeixinChannelForm.vue' import WeixinChannelForm from './components/channel/WeixinChannelForm.vue'
import MockChannelForm from './components/channel/MockChannelForm.vue' import MockChannelForm from './components/channel/MockChannelForm.vue'
import WalletChannelForm from './components/channel/WalletChannelForm.vue' import WalletChannelForm from './components/channel/WalletChannelForm.vue'
import WlkChannelForm from './components/channel/WlkChannelForm.vue'
defineOptions({ name: 'PayApp' }) defineOptions({ name: 'PayApp' })
@ -266,6 +299,10 @@ const wxChannels = [
PayChannelEnum.WX_BAR PayChannelEnum.WX_BAR
] ]
const wlkChannels = [
PayChannelEnum.WLK_LITE,
PayChannelEnum.WLK_OPEN_QR_CODE
]
/** 查询列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
@ -342,6 +379,7 @@ const alipayFormRef = ref()
const weixinFormRef = ref() const weixinFormRef = ref()
const mockFormRef = ref() const mockFormRef = ref()
const walletFormRef = ref() const walletFormRef = ref()
const wlkFormRef = ref()
const channelParam = reactive({ const channelParam = reactive({
appId: null, // ID appId: null, // ID
payCode: null // payCode: null //
@ -363,6 +401,9 @@ const openChannelForm = async (row, payCode) => {
if (payCode.indexOf('wallet') === 0) { if (payCode.indexOf('wallet') === 0) {
mockFormRef.value.open(row.id, payCode) mockFormRef.value.open(row.id, payCode)
} }
if (payCode.indexOf('wlk_') === 0) {
wlkFormRef.value.open(row.id, payCode)
}
} }
/** 初始化 **/ /** 初始化 **/

View File

@ -19,6 +19,19 @@
<!-- 支付选择框 --> <!-- 支付选择框 -->
<el-card style="margin-top: 10px" v-loading="submitLoading" element-loading-text="提交支付中..."> <el-card style="margin-top: 10px" v-loading="submitLoading" element-loading-text="提交支付中...">
<!-- 网联客支付 -->
<el-descriptions title="选择网联客支付" style="margin-top: 20px" />
<div class="pay-channel-container">
<div
class="box"
v-for="channel in channelsWlkPay"
:key="channel.code"
@click="submit(channel.code)"
>
<img :src="channel.icon" />
<div class="title">{{ channel.name }}</div>
</div>
</div>
<!-- 支付宝 --> <!-- 支付宝 -->
<el-descriptions title="选择支付宝支付" /> <el-descriptions title="选择支付宝支付" />
<div class="pay-channel-container"> <div class="pay-channel-container">
@ -133,6 +146,8 @@ import svg_wx_app from '@/assets/svgs/pay/icon/wx_app.svg'
import svg_wx_native from '@/assets/svgs/pay/icon/wx_native.svg' import svg_wx_native from '@/assets/svgs/pay/icon/wx_native.svg'
import svg_wx_bar from '@/assets/svgs/pay/icon/wx_bar.svg' import svg_wx_bar from '@/assets/svgs/pay/icon/wx_bar.svg'
import svg_mock from '@/assets/svgs/pay/icon/mock.svg' import svg_mock from '@/assets/svgs/pay/icon/mock.svg'
import svg_wlk_lite from '@/assets/svgs/pay/icon/wlk_lite.svg'
import svg_wlk_open_qr_code from '@/assets/svgs/pay/icon/wlk_open_qr_code.svg'
defineOptions({ name: 'PayCashier' }) defineOptions({ name: 'PayCashier' })
@ -145,6 +160,18 @@ const id = ref(undefined) // 支付单号
const returnUrl = ref<string | undefined>(undefined) // const returnUrl = ref<string | undefined>(undefined) //
const loading = ref(false) // loading const loading = ref(false) // loading
const payOrder = ref({}) // const payOrder = ref({}) //
const channelsWlkPay = [
{
name: '网联客小程序支付',
icon: svg_wlk_lite,
code: 'wlk_lite'
},
{
name: '网联客定额聚合支付',
icon: svg_wlk_open_qr_code,
code: 'wlk_open_qr_code'
}
]
const channelsAlipay = [ const channelsAlipay = [
{ {
name: '支付宝 PC 网站支付', name: '支付宝 PC 网站支付',