feat(daxpay): 支持支付宝和微信特约商户

- 新增支付宝和微信特约商户相关的配置项和功能
- 更新了相应的API接口和UI界面以支持特约商户功能
-优化了部分现有功能,如支付限额和证书上传等
This commit is contained in:
DaxPay
2025-01-03 10:25:37 +08:00
parent 13234b5fb8
commit 73ce82fa4e
14 changed files with 192 additions and 82 deletions

View File

@@ -3,7 +3,9 @@
*/
export enum ChannelEnum {
ALI = 'ali_pay',
ALI_ISV = 'alipay_isv',
WECHAT = 'wechat_pay',
WECHAT_ISV = 'wechat_pay_isv',
UNION_PAY = 'union_pay',
}
@@ -63,7 +65,6 @@ export enum ChannelAuthStatusEnum {
NOT_EXIST = 'not_exist',
}
/**
* 收银台类型
*/
@@ -78,7 +79,6 @@ export enum CheckoutTypeEnum {
* 支付调起类型
*/
export enum CheckoutCallTypeEnum {
/** 扫码支付 */
QR_CODE = 'qr_code',
/** 条码支付 */
@@ -93,6 +93,4 @@ export enum CheckoutCallTypeEnum {
JSAPI = 'jsapi',
/** 表单方式 */
FROM = 'from',
}

View File

@@ -33,6 +33,10 @@ export interface AlipayConfig extends MchEntity {
enable: boolean
// 支付限额
limitAmount?: number
// 是否为特约商户
isv?: boolean
// 特约商户Token
appAuthToken?: string
// 商户账号ID
alipayUserId?: string
// 授权回调地址

View File

@@ -34,7 +34,7 @@
<a-form-item name="limitAmount">
<template #label>
<basic-title
helpMessage="每次发起支付的金额不能超过该值,如果同时配置了应用支付限额,则以额度低的为准"
helpMessage="每次发起支付的金额不能超过该值,如果同时配置了全局支付限额,则以额度低的为准"
>
支付限额()
</basic-title>
@@ -55,6 +55,14 @@
</template>
<a-input v-model:value="form.alipayUserId" placeholder="请输入合作者身份ID" />
</a-form-item>
<a-form-item name="appAuthToken" v-if="isIsv">
<template #label>
<basic-title helpMessage="商家授权给服务商的应用授权凭证, 用于代调用接口"
>应用授权Token
</basic-title>
</template>
<a-input v-model:value="form.appAuthToken" placeholder="请输入特约商户AppAuthToken" />
</a-form-item>
<a-form-item label="沙箱环境" name="sandbox">
<a-switch checked-children="" un-checked-children="" v-model:checked="form.sandbox" />
</a-form-item>
@@ -195,7 +203,7 @@
import { BasicDrawer } from '@/components/Drawer'
import { Icon } from '@/components/Icon'
import BasicTitle from '@/components/Basic/src/BasicTitle.vue'
import { ChannelConfig } from '@/views/daxpay/common/merchant/channel/ChannelConfig.api'
import { ChannelConfig } from "@/views/daxpay/common/merchant/channel/ChannelConfig.api";
const { handleCancel, diffForm, labelCol, wrapperCol, confirmLoading, visible, showable } =
useFormEdit()
@@ -205,8 +213,9 @@
const formRef = ref<FormInstance>()
const channelConfig = ref<ChannelConfig>({})
const isIsv = ref<boolean>(false)
const form = ref({
const form = ref<AlipayConfig>({
aliAppId: '',
enable: false,
limitAmount: 20000,
@@ -220,8 +229,7 @@
alipayRootCert: '',
privateKey: '',
sandbox: false,
remark: '',
} as AlipayConfig)
})
let rawForm: any = {}
// 校验
const rules = computed(() => {
@@ -230,6 +238,7 @@
enable: [{ required: true, message: '请选择是否启用' }],
limitAmount: [{ required: true, message: '请输入单次支付限额' }],
authType: [{ required: true, message: '请选择认证方式' }],
appAuthToken: [{ required: isIsv.value, message: '请输入特约商户AppAuthToken' }],
signType: [{ required: true, message: '请选择加密类型' }],
alipayPublicKey: [{ required: form.value.authType === 'key', message: '请输入支付宝公钥' }],
privateKey: [{ required: true, message: '请输入应用私钥' }],
@@ -240,7 +249,6 @@
],
sandbox: [{ required: true, message: '请选择是否为沙箱环境' }],
expireTime: [{ required: true, message: '请输入默认超时配置' }],
payWays: [{ required: true, message: '请选择支持的支付类型' }],
} as Record<string, Rule[]>
})
// 事件
@@ -248,8 +256,9 @@
/**
* 入口
*/
function init(config: ChannelConfig) {
function init(config: ChannelConfig, isv: boolean) {
channelConfig.value = config
isIsv.value = isv
resetForm()
visible.value = true
getInfo()
@@ -284,6 +293,7 @@
'alipayRootCert',
'privateKey',
),
isv: isIsv.value,
appId: channelConfig.value.appId,
})
.then(() => {

View File

@@ -30,6 +30,12 @@ export interface WechatPayConfig extends MchEntity {
wxAppId?: string
// 微信商户号
wxMchId?: string
// 微信子应用AppId
subAppId?: string
// 微信子商户号
subMchId?: string
// 是否特约商户
isv?: boolean
// 是否启用
enable: boolean
// 授权认证地址
@@ -44,13 +50,17 @@ export interface WechatPayConfig extends MchEntity {
apiKeyV3?: string
// APPID对应的接口密码用于获取接口调用凭证access_token时使用
appSecret?: string
// 私钥Key
privateKey?: string
// 私钥证书
// 支付公钥
publicKey?: string
// 支付公钥ID
publicKeyId?: string
// 商户API证书
privateCert?: string
// 证书序列号
// 商户API证书私钥
privateKey?: string
// 商户API证书序列号
certSerialNo?: string
// p12的文件id
// p12的文件
p12?: string | null
// 应用域名,回调中会使用此参数
domain?: string
@@ -63,3 +73,17 @@ export interface WechatPayConfig extends MchEntity {
// 备注
remark?: string
}
/**
* 特约商户服务商配置
*/
export interface WechatPaySimpleConfig extends MchEntity {
// 服务商应用号
isvAppId?: string
// 子商户应用AppId
subAppId?: string
// 子商户商户号
subMchId?: string
// 是否启用
enable?: boolean
}

View File

@@ -24,13 +24,27 @@
<a-form-item label="商户号" name="wxMchId">
<a-input v-model:value="form.wxMchId" :disabled="showable" placeholder="请输入商户号" />
</a-form-item>
<a-form-item label="应用号(AppId)" name="wxAppId">
<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="子商户号" name="subMchId" v-if="isIsv">
<a-input
v-model:value="form.subMchId"
:disabled="showable"
placeholder="请输入子商户号"
/>
</a-form-item>
<a-form-item label="子应用号(subAppId)" name="subAppId" v-if="isIsv">
<a-input
v-model:value="form.subAppId"
:disabled="showable"
placeholder="请输入微信子应用AppId"
/>
</a-form-item>
<a-form-item label="公众号AppSecret" name="appSecret">
<a-input
v-model:value="form.appSecret"
@@ -62,7 +76,7 @@
<a-form-item name="limitAmount">
<template #label>
<basic-title
helpMessage="每次发起支付的金额不能超过该值,如果同时配置了应用支付限额,则以额度低的为准"
helpMessage="每次发起支付的金额不能超过该值,如果同时配置了全局支付限额,则以额度低的为准"
>
支付限额()
</basic-title>
@@ -96,53 +110,10 @@
placeholder="请输入APIv3密钥"
/>
</a-form-item>
<a-form-item name="certSerialNo">
<template #label>
<basic-title helpMessage="登录微信支付平台进入到账户中心 API安全 查看证书, 查看证书序列号">
证书序列号
</basic-title>
</template>
<a-input
:disabled="showable"
v-model:value="form.certSerialNo"
placeholder="请输入证书序列号"
/>
</a-form-item>
<a-form-item name="privateKey">
<template #label>
<basic-title helpMessage="微信商户平台Api接口中的apiclient_key.pem证书">
私钥Key
</basic-title>
</template>
<a-upload
v-if="!form.privateKey"
:disabled="showable"
name="file"
:multiple="false"
:action="uploadAction"
:headers="tokenHeader"
:showUploadList="false"
@change="(info) => handleChange(info, 'privateKey')"
>
<a-button type="primary" preIcon="carbon:cloud-upload"> 私钥Key上传 </a-button>
</a-upload>
<a-input v-else defaultValue="apiclient_key.pem" disabled>
<template #addonAfter v-if="!showable">
<a-tooltip>
<template #title> 删除上传的证书文件 </template>
<icon
@click="form.privateKey = ''"
icon="ant-design:close-circle-outlined"
:size="20"
/>
</a-tooltip>
</template>
</a-input>
</a-form-item>
<a-form-item name="privateCert">
<template #label>
<basic-title helpMessage="微信商户平台Api接口中的apiclient_cert.pem证书">
私钥证书
<basic-title helpMessage="微信平台中的商户API证书(apiclient_cert.pem)">
商户API证书
</basic-title>
</template>
<a-upload
@@ -155,12 +126,12 @@
:showUploadList="false"
@change="(info) => handleChange(info, 'privateCert')"
>
<a-button type="primary" preIcon="carbon:cloud-upload"> 私钥Key上传 </a-button>
<a-button type="primary" preIcon="carbon:cloud-upload"> API证书上传 </a-button>
</a-upload>
<a-input v-else defaultValue="apiclient_cert.pem" disabled>
<template #addonAfter v-if="!showable">
<a-tooltip>
<template #title> 删除上传的证书文件 </template>
<template #title> 删除上传的商户API证书 </template>
<icon
@click="form.privateCert = ''"
icon="ant-design:close-circle-outlined"
@@ -170,6 +141,80 @@
</template>
</a-input>
</a-form-item>
<a-form-item name="privateKey">
<template #label>
<basic-title helpMessage="微信平台中的商户API证书私钥(apiclient_key.pem)">
商户API证书私钥
</basic-title>
</template>
<a-upload
v-if="!form.privateKey"
:disabled="showable"
name="file"
:multiple="false"
:action="uploadAction"
:headers="tokenHeader"
:showUploadList="false"
@change="(info) => handleChange(info, 'privateKey')"
>
<a-button type="primary" preIcon="carbon:cloud-upload"> API证书私钥上传 </a-button>
</a-upload>
<a-input v-else defaultValue="apiclient_key.pem" disabled>
<template #addonAfter v-if="!showable">
<a-tooltip>
<template #title> 删除上传的API证书私钥 </template>
<icon
@click="form.privateKey = ''"
icon="ant-design:close-circle-outlined"
:size="20"
/>
</a-tooltip>
</template>
</a-input>
</a-form-item>
<a-form-item label="商户API证书序列号" name="certSerialNo">
<a-input
:disabled="showable"
v-model:value="form.certSerialNo"
placeholder="请输入商户API证书序列号"
/>
</a-form-item>
<a-form-item name="publicKey">
<template #label>
<basic-title helpMessage="微信平台中的支付公钥(pub_key.pem)"> 支付公钥 </basic-title>
</template>
<a-upload
v-if="!form.publicKey"
:disabled="showable"
name="file"
:multiple="false"
:action="uploadAction"
:headers="tokenHeader"
:showUploadList="false"
@change="(info) => handleChange(info, 'publicKey')"
>
<a-button type="primary" preIcon="carbon:cloud-upload"> 支付公钥上传 </a-button>
</a-upload>
<a-input v-else defaultValue="pub_key.pem" disabled>
<template #addonAfter v-if="!showable">
<a-tooltip>
<template #title> 删除上传的支付公钥 </template>
<icon
@click="form.publicKey = ''"
icon="ant-design:close-circle-outlined"
:size="20"
/>
</a-tooltip>
</template>
</a-input>
</a-form-item>
<a-form-item label="支付公钥ID" name="publicKeyId">
<a-input
:disabled="showable"
v-model:value="form.publicKeyId"
placeholder="请输入支付公钥ID"
/>
</a-form-item>
<a-form-item name="p12">
<template #label>
<basic-title helpMessage="V2版本中例如退款、转账时必须要配置p12证书才可以执行">
@@ -225,13 +270,14 @@
import { useUpload } from '@/hooks/bootx/useUpload'
import { useMessage } from '@/hooks/web/useMessage'
import BasicTitle from '@/components/Basic/src/BasicTitle.vue'
import { ChannelConfig } from '@/views/daxpay/common/merchant/channel/ChannelConfig.api'
import { ChannelConfig } from "@/views/daxpay/common/merchant/channel/ChannelConfig.api";
const { handleCancel, diffForm, labelCol, wrapperCol, confirmLoading, visible, showable } =
useFormEdit()
// 文件上传
const { tokenHeader, uploadAction } = useUpload('/readBase64')
const { createMessage } = useMessage()
const isIsv = ref<boolean>(false)
// 表单
const formRef = ref<FormInstance>()
@@ -258,13 +304,12 @@
const rules = computed(() => {
return {
wxMchId: [{ required: true, message: '请输入商户号' }],
limitAmount: [{ required: true, message: '请输入单次支付限额' }],
wxAppId: [{ required: true, message: '请输入应用编号' }],
// appSecret: [{ required: true, message: '请输入AppSecret' }],
subMchId: [{ required: isIsv.value, message: '请输入子商户号' }],
limitAmount: [{ required: true, message: '请输入单次支付限额' }],
enable: [{ required: true, message: '请选择是否启用' }],
notifyUrl: [{ required: true, message: '请输入异步通知页面地址' }],
returnUrl: [{ required: true, message: '请输入同步通知页面地址' }],
sandbox: [{ required: true, message: '请选择是否为沙箱环境' }],
apiVersion: [{ required: true, message: '请选择支付API版本' }],
apiKeyV2: [{ required: form.value.apiVersion === 'apiV2', message: '请输入V2秘钥' }],
apiKeyV3: [{ required: form.value.apiVersion === 'apiV3', message: '请输入V3秘钥' }],
@@ -281,7 +326,6 @@
message: '请上传私钥Key(apiclient_cert.pem)',
},
],
payWays: [{ required: true, message: '请选择支持的支付类型' }],
} as Record<string, Rule[]>
})
@@ -290,8 +334,9 @@
/**
* 入口
*/
function init(config: ChannelConfig) {
function init(config: ChannelConfig, isv: boolean) {
channelConfig.value = config
isIsv.value = isv
visible.value = true
resetForm()
getInfo()
@@ -324,9 +369,13 @@
'appSecret',
'apiKeyV2',
'apiKeyV3',
'privateKey',
'privateCert',
'privateKey',
'certSerialNo',
'publicKey',
'publicKeyId',
),
isv: isIsv.value,
appId: channelConfig.value.appId,
})
.then(() => {

View File

@@ -156,6 +156,7 @@
clientIp: '127.0.0.1',
amount: 0.01,
allocation: false,
autoAllocation: false,
})
const rules = computed(() => {
return {
@@ -166,6 +167,7 @@
amount: [{ required: true, message: '支付金额不可为空' }],
method: [{ required: true, message: '支付方式不可为空' }],
allocation: [{ required: true, message: '分账不可为空' }],
autoAllocation: [{ required: true, message: '自动分账不可为空' }],
clientIp: [{ required: true, message: '终端IP不可为空' }],
nonceStr: [{ required: true, message: '随机数不可为空' }],
reqTime: [{ required: true, message: '请求时间不可为空' }],

View File

@@ -27,6 +27,12 @@
<a-form-item label="应用名称" name="appName">
<a-input v-model:value="form.appName" :disabled="showable" placeholder="请输入应用名称" />
</a-form-item>
<a-form-item label="应用状态" name="status">
<a-radio-group v-model:value="form.status" :disabled="showable" button-style="solid">
<a-radio-button value="disabled">禁用</a-radio-button>
<a-radio-button value="enable">启用</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item label="支付限额(元)" name="limitAmount">
<a-input-number
v-model:value="form.limitAmount"
@@ -145,6 +151,7 @@
const rules = reactive({
appName: [{ required: true, message: '请输入应用名称' }],
signType: [{ required: true, message: '请选择签名方式' }],
status: [{ required: true, message: '请选择应用状态' }],
signSecret: [{ required: true, message: '请输入签名秘钥' }],
reqSign: [{ required: true, message: '请选择是否验签' }],
limitAmount: [{ required: true, message: '请输入支付限额(元)' }],

View File

@@ -44,6 +44,11 @@
<a-tag>{{ dictConvert('merchant_notify_type', row.notifyType) || '空' }}</a-tag>
</template>
</vxe-column>
<vxe-column field="status" title="应用状态" :min-width="100">
<template #default="{ row }">
<a-tag>{{ dictConvert('mch_app_status', row.status) || '空' }}</a-tag>
</template>
</vxe-column>
<vxe-column field="notifyUrl" title="通知地址" :min-width="220" />
<vxe-column field="createTime" title="创建时间" :min-width="120" />
<vxe-column fixed="right" :width="220" :showOverflow="false" title="操作">

View File

@@ -25,15 +25,23 @@
function show(record: ChannelConfig) {
switch (record.channel) {
case ChannelEnum.ALI: {
alipay.value.init(record)
alipay.value.init(record, false)
break
}
case ChannelEnum.ALI_ISV: {
alipay.value.init(record, true)
break
}
case ChannelEnum.UNION_PAY: {
union.value.init(record)
break
}
case ChannelEnum.WECHAT: {
wechat.value.init(record)
break
}
case ChannelEnum.UNION_PAY: {
union.value.init(record)
case ChannelEnum.WECHAT_ISV: {
wechat.value.init(record, true)
break
}
default: {

View File

@@ -113,8 +113,12 @@
switch (type) {
case ChannelEnum.ALI:
return alipay
case ChannelEnum.ALI_ISV:
return alipay
case ChannelEnum.WECHAT:
return wechat
case ChannelEnum.WECHAT_ISV:
return wechat
case ChannelEnum.UNION_PAY:
return unionPay
default:

View File

@@ -46,7 +46,7 @@ export function transfer(params) {
/**
* 转账信息同步
*/
export function syncByTransferNo(id) {
export function syncByTransferId(id) {
return defHttp.post<Result<void>>({
url: '/order/transfer/sync',
params: { id },

View File

@@ -105,13 +105,13 @@
</template>
<script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue'
import { computed, onMounted, ref } from 'vue'
import {
closeTransfer,
getTotalAmount,
page,
retryTransfer,
syncByTransferNo,
syncByTransferId,
cellStyle,
} from './TransferOrder.api'
import useTablePage from '@/hooks/bootx/useTablePage'
@@ -125,7 +125,6 @@
import ALink from '/@/components/Link/Link.vue'
import { TransferStatusEnum } from '@/enums/daxpay/tradeStatusEnum'
import { Icon } from '@/components/Icon'
import { merchantDropdown } from '@/views/daxpay/admin/merchant/info/Merchant.api'
import { mchAppDropdown } from '@/views/daxpay/common/merchant/app/MchApp.api'
// 使用hooks
@@ -233,10 +232,10 @@
createConfirm({
iconType: 'warning',
title: '警告',
content: '是否同步退款信息',
content: '是否同步转账信息',
onOk: () => {
loading.value = true
syncByTransferNo(record.transferNo).then(() => {
syncByTransferId(record.id).then(() => {
createMessage.success('同步成功')
queryPage()
})

View File

@@ -88,6 +88,7 @@
import { Rule } from 'ant-design-vue/lib/form'
import { getAppEnvConfig } from '@/utils/env'
import { imgCaptcha } from '@/api/common/Captcha'
import { LoginType } from '@/api/common/LoginAssist'
const { notification } = useMessage()
// 用户信息存储

1
types/store.d.ts vendored
View File

@@ -1,6 +1,5 @@
import { ErrorTypeEnum } from '@/enums/exceptionEnum'
import { MenuModeEnum, MenuTypeEnum } from '@/enums/menuEnum'
import { RoleInfo } from '@/api/sys/model/userModel'
export interface ApiAddress {
key: string