feat(daxpay): 新增收银台支付功能

- 添加收银台支付相关的 API 接口和请求参数
- 实现收银台支付页面组件,包括表单验证、签名生成和请求提交等功能
- 在开发交易页面中集成收银台支付选项
- 更新支付状态枚举,增加待支付状态
- 优化支付订单列表的支付状态显示
This commit is contained in:
DaxPay
2024-12-04 15:12:18 +08:00
parent 43efc2316b
commit 2546c3fc82
7 changed files with 334 additions and 1 deletions

View File

@@ -2,6 +2,10 @@
* 支付状态
*/
export enum PayStatusEnum {
/**
* 待支付
*/
WAIT = 'wait',
/**
* 支付中
*/

View File

@@ -43,6 +43,20 @@ export function tradeTransfer(params) {
return defHttp.post<Result<TransferResult>>({ url: '/develop/trade/transfer', data: params })
}
/**
* 创建收银台链接签名
*/
export function checkoutSign(param: CheckoutCreatParam) {
return defHttp.post<Result<string>>({ url: '/develop/trade/sign/checkout', data: param })
}
/**
* 创建收银台链接
*/
export function checkoutUrl(params) {
return defHttp.post<Result<CheckoutUrlResult>>({ url: '/develop/trade/checkoutUrl', data: params })
}
/**
* 支付请求参数
*/
@@ -213,3 +227,57 @@ export interface TransferResult {
/** 签名 */
sign?: string
}
/**
* 收银台创建参数
*/
export interface CheckoutCreatParam {
/** 收银台类型 */
checkoutType?: string
/** 商户订单号 */
bizOrderNo?: string
/** 支付标题 */
title?: string
/** 支付描述 */
description?: string
/** 是否开启分账 */
allocation?: boolean
/** 自动分账 */
autoAllocation?: boolean
/** 过期时间 */
expiredTime?: string
/** 支付通道编码 */
channel?: string
/** 支付方式编码 */
method?: string
/** 支付金额 */
amount?: number
/** 支付扩展参数 */
extraParam?: string
/** 商户扩展参数 */
attach?: string
/** 同步跳转地址 */
returnUrl?: string
/** 用户付款中途退出返回商户网站的地址 */
quitUrl?: string
/** 异步通知地址 */
notifyUrl?: string
/** 终端IP */
clientIp?: string
/** 应用号 */
appId?: string
/** 随机数 */
nonceStr?: string
/** 签名 */
sign?: string
/** 请求时间 */
reqTime?: string
}
/**
* 收银台响应参数
*/
export interface CheckoutUrlResult {
/** 收银台链接 */
url?: string
}

View File

@@ -11,6 +11,7 @@
<develop-trade-pay v-if="tradeType === TradeTypeEnum.PAY" />
<develop-trade-refund v-if="tradeType === TradeTypeEnum.REFUND" />
<develop-trade-transfer v-if="tradeType === TradeTypeEnum.TRANSFER" />
<develop-trade-check v-if="tradeType === 'checkout'" />
</div>
</div>
</div>
@@ -24,6 +25,7 @@
import DevelopTradePay from './DevelopTradePay.vue'
import DevelopTradeRefund from './DevelopTradeRefund.vue'
import DevelopTradeTransfer from './DevelopTradeTransfer.vue'
import DevelopTradeCheck from './DevelopTradeCheck.vue'
const { dictDropDown } = useDict()
@@ -37,6 +39,7 @@
*/
async function initData() {
tradeTypes.value = await dictDropDown('trade_type')
tradeTypes.value.push({ label: '收银台', value: 'checkout' })
}
</script>

View File

@@ -0,0 +1,255 @@
<template>
<div>
<a-spin :spinning="confirmLoading">
<a-form
class="small-from-item w-40vw"
ref="formRef"
:model="form"
:rules="rules"
:labelCol="{ span: 6 }"
:wrapperCol="{ span: 18 }"
:validate-trigger="['blur', 'change']"
>
<a-form-item label="应用号" name="appId">
<a-select
:filter-option="search"
:options="mchAppList"
v-model:value="form.appId"
placeholder="请选择商户应用"
/>
</a-form-item>
<a-form-item label="收银台类型" name="checkoutType">
<a-select
allow-clear
v-model:value="form.checkoutType"
:options="checkoutOptions"
placeholder="请选择收银台类型"
/>
</a-form-item>
<a-form-item label="订单号" name="bizOrderNo">
<a-input-group compact>
<a-input
v-model:value="form.bizOrderNo"
placeholder="请输入订单号"
style="width: calc(100% - 60px)"
/>
<a-button @click="genBizOrderNo">生成</a-button>
</a-input-group>
</a-form-item>
<a-form-item label="支付标题" name="title">
<a-input v-model:value="form.title" placeholder="请输入支付标题" />
</a-form-item>
<a-form-item label="支付金额" name="amount">
<a-input-number
v-model:value="form.amount"
:min="0.01"
:precision="2"
placeholder="请输入支付金额"
/>
</a-form-item>
<a-form-item label="是否分账" name="allocation">
<a-switch
checked-children=""
un-checked-children=""
v-model:checked="form.allocation"
/>
</a-form-item>
<a-form-item label="支付描述" name="description">
<a-input v-model:value="form.description" placeholder="请输入支付描述" />
</a-form-item>
<a-form-item label="支付扩展参数" name="extraParam">
<a-textarea v-model:value="form.extraParam" :rows="3" placeholder="请输入支付扩展参数" />
</a-form-item>
<a-form-item label="商户扩展参数" name="attach">
<a-textarea
v-model:value="form.attach"
:rows="3"
placeholder="请输入商户扩展参数, 会原样回调返回"
/>
</a-form-item>
<a-form-item label="异步通知地址" name="notifyUrl">
<a-input v-model:value="form.notifyUrl" placeholder="请输入异步通知地址" />
</a-form-item>
<a-form-item label="支付超时时间" name="expiredTime">
<!-- 日期时间 -->
<a-date-picker
allowClear
showTime
style="width: 100%"
placeholder="请选择订单过期时间"
:valueFormat="'YYYY-MM-DD HH:mm:ss'"
v-model:value="form.expiredTime"
/>
</a-form-item>
<a-form-item label="终端IP" name="clientIp">
<a-input v-model:value="form.clientIp" placeholder="请输入终端IP地址" />
</a-form-item>
<a-form-item label="随机数" name="nonceStr">
<a-input-group compact>
<a-input
v-model:value="form.nonceStr"
style="width: calc(100% - 60px)"
placeholder="请输入随机数"
/>
<a-button @click="genNonceStr">生成</a-button>
</a-input-group>
</a-form-item>
<a-form-item label="请求时间" name="reqTime">
<a-input-group compact>
<a-input disabled v-model:value="form.reqTime" style="width: calc(100% - 60px)" />
<a-button @click="updateReqTime">更新</a-button>
</a-input-group>
</a-form-item>
<a-form-item label="签名" name="sign">
<a-input-group compact>
<a-input
placeholder="请输入签名"
v-model:value="form.sign"
style="width: calc(100% - 60px)"
/>
<a-button @click="getSign">生成</a-button>
</a-input-group>
</a-form-item>
<a-space style="display: flex; justify-content: center">
<a-button @click="handleReset">重置表单</a-button>
<a-button type="primary" @click="handleSubmit">提交请求</a-button>
</a-space>
</a-form>
</a-spin>
</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 { CheckoutCreatParam, checkoutSign, checkoutUrl } from './DevelopTrade.api'
import { LabeledValue } from 'ant-design-vue/lib/select'
import useFormEdit from '@/hooks/bootx/useFormEdit'
import { mchAppDropdown } from '@/views/daxpay/common/merchant/app/MchApp.api'
import { useDict } from '@/hooks/bootx/useDict'
import XEUtils from 'xe-utils'
import { buildShortUUID, buildUUID } from '@/utils/uuid'
import { copyText } from '@/utils/copyTextToClipboard'
const { search } = useFormEdit()
const { dictDropDown } = useDict()
const confirmLoading = ref(false)
const formRef = ref<FormInstance>()
const form = reactive<CheckoutCreatParam>({
title: '测试收银台支付',
clientIp: '127.0.0.1',
amount: 0.1,
allocation: false,
})
const rules = computed(() => {
return {
checkoutType: [{ required: true, message: '应用号不可为空' }],
appId: [{ required: true, message: '应用号不可为空' }],
bizOrderNo: [{ required: true, message: '订单号不可为空' }],
title: [{ required: true, message: '支付标题不可为空' }],
amount: [{ required: true, message: '支付金额不可为空' }],
allocation: [{ required: true, message: '分账不可为空' }],
clientIp: [{ required: true, message: '终端IP不可为空' }],
nonceStr: [{ required: true, message: '随机数不可为空' }],
reqTime: [{ required: true, message: '请求时间不可为空' }],
} as Record<string, Rule[]>
})
const mchAppList = ref<LabeledValue[]>([])
const checkoutOptions = ref<LabeledValue[]>([])
onMounted(() => {
initData()
})
/**
* 初始化数据
*/
async function initData() {
confirmLoading.value = false
checkoutOptions.value = await dictDropDown('checkout_type')
// 时间默认30M后
form.expiredTime = XEUtils.toDateString(
new Date(new Date().getTime() + 30 * 60 * 1000),
'yyyy-MM-dd HH:mm:ss',
)
initMchApp()
genNonceStr()
genBizOrderNo()
updateReqTime()
}
/**
* 商户变动时刷新应用列表
*/
function initMchApp() {
mchAppDropdown().then(({ data }) => {
mchAppList.value = data
})
}
/**
* 生成订单号
*/
function genBizOrderNo() {
form.bizOrderNo = buildShortUUID('PAY')
}
/**
* 生成随机数
*/
function genNonceStr() {
form.nonceStr = buildUUID()
}
/**
* 更新请求时间
*/
function updateReqTime() {
form.reqTime = XEUtils.toDateString(new Date(), 'yyyy-MM-dd HH:mm:ss')
}
/**
* 获取签名
*/
function getSign() {
formRef.value?.validate().then(() => {
checkoutSign(form).then(({ data }) => {
form.sign = data
})
})
}
/**
* 重置
*/
function handleReset() {
formRef.value?.resetFields()
initData()
}
/**
* 提交
*/
function handleSubmit() {
formRef.value?.validate().then(() => {
confirmLoading.value = true
checkoutUrl(form)
.then(({ data }) => {
Modal.info({
title: '响应结果',
content: data.url,
okText: '复制链接',
onOk() {
copyText(data.url as string)
},
})
})
.finally(() => {
confirmLoading.value = false
})
})
}
</script>
<style scoped lang="less"></style>

View File

@@ -148,6 +148,7 @@
title: '测试支付',
clientIp: '127.0.0.1',
amount: 0.01,
allocation: false,
})
const rules = computed(() => {
return {

View File

@@ -26,7 +26,7 @@
placeholder="请选择转账通道"
/>
</a-form-item>
<a-form-item label="转账号" name="bizRefundNo">
<a-form-item label="转账号" name="bizTransferNo">
<a-input-group compact>
<a-input
v-model:value="form.bizTransferNo"

View File

@@ -94,6 +94,8 @@ export function cellStyle({ row, column }) {
// 支付状态
if (column.field == 'status') {
switch (row.status) {
case PayStatusEnum.WAIT:
return { color: 'orange' }
case PayStatusEnum.SUCCESS:
return { color: 'green' }
case PayStatusEnum.FAIL: