This commit is contained in:
dzc committed 2026-08-29 17:30:07 +08:00
1 parent fc8a9e4a84
commit 44f47fe5bc
4 files changed
+906 -19

No files matched your search

+21
View File
@@ -35,3 +35,24 @@ export function retry(id: string | number, mchId: string | number) {
params: { mchId },
})
}
/** 提交支付宝直付通二级商户全量资料修改申请;支付宝处理完成后才会在账单和支付结果页生效。 */
export function modifyAlipayAlias(id: string | number, data: Record<string, any>) {
return request({
url: `/third/third-mch-fz-relation/${id}/alipay-alias`,
method: "post",
data,
})
}
/** 将资料图片上传到支付宝,返回可填入资料表单的 image_id。 */
export function uploadAlipayImage(id: string | number, file: File) {
const formData = new FormData()
formData.append("file", file)
return request({
url: `/third/third-mch-fz-relation/${id}/alipay-image`,
method: "post",
headers: { "Content-Type": "multipart/form-data" },
data: formData,
})
}
@@ -0,0 +1,81 @@
<template>
<div class="flex w-full min-w-0 items-start gap-2">
<el-select v-if="multiple" :model-value="normalizedValues" class="min-w-0 flex-1" multiple filterable
allow-create default-first-option placeholder="上传图片或粘贴支付宝 image_id"
@update:model-value="updateMultiple">
<el-option v-for="value in normalizedValues" :key="value" :label="value" :value="value" />
</el-select>
<el-input v-else :model-value="singleValue" class="min-w-0 flex-1" clearable
placeholder="上传图片或粘贴支付宝 image_id" @update:model-value="updateSingle" />
<el-upload :show-file-list="false" :accept="acceptedTypes" :http-request="uploadImage"
:before-upload="beforeUpload" :disabled="uploading">
<el-button :icon="Upload" :loading="uploading">上传到支付宝</el-button>
</el-upload>
</div>
</template>
<script setup lang="ts">
import { Upload } from '@element-plus/icons-vue'
import type { UploadProps, UploadRequestOptions } from 'element-plus'
import { useMessage } from '/@/hooks/message'
import { uploadAlipayImage } from '/@/api/thirdparty/third-mch-fz-relation'
const props = withDefaults(defineProps<{
modelValue?: string | string[]
relationId: string | number
multiple?: boolean
}>(), {
modelValue: '',
multiple: false,
})
const emit = defineEmits<{
(event: 'update:modelValue', value: string | string[]): void
}>()
const acceptedTypes = '.bmp,.jpg,.jpeg,.png,.gif,image/bmp,image/jpeg,image/png,image/gif'
const uploading = ref(false)
const normalizedValues = computed(() => Array.isArray(props.modelValue)
? props.modelValue.filter(Boolean)
: String(props.modelValue || '').split(',').map(item => item.trim()).filter(Boolean))
const singleValue = computed(() => Array.isArray(props.modelValue)
? (props.modelValue[0] || '')
: String(props.modelValue || ''))
const updateMultiple = (value: string[]) => emit('update:modelValue', value.map(item => item.trim()).filter(Boolean))
const updateSingle = (value: string) => emit('update:modelValue', value.trim())
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
const extension = file.name.split('.').pop()?.toLowerCase()
if (!extension || !['bmp', 'jpg', 'jpeg', 'png', 'gif'].includes(extension)) {
useMessage().warning('只支持 bmp、jpg、jpeg、png、gif 图片')
return false
}
if (file.size > 10 * 1024 * 1024) {
useMessage().warning('图片不能超过10MB')
return false
}
return true
}
const uploadImage = async (options: UploadRequestOptions) => {
try {
uploading.value = true
const { data, code } = await uploadAlipayImage(props.relationId, options.file)
if (code !== 0 || !data) throw new Error('支付宝未返回图片标识')
if (props.multiple) {
emit('update:modelValue', Array.from(new Set([...normalizedValues.value, String(data)])))
} else {
emit('update:modelValue', String(data))
}
options.onSuccess({ data })
useMessage().success('图片已上传到支付宝')
} catch (error: any) {
options.onError(error)
useMessage().error(error?.msg || error?.message || '支付宝图片上传失败')
} finally {
uploading.value = false
}
}
</script>
@@ -0,0 +1,745 @@
<template>
<el-dialog :model-value="modelValue" title="修改支付宝商户资料" width="92%" top="4vh" style="max-width: 1180px"
:close-on-click-modal="false" :close-on-press-escape="!loading" :show-close="!loading" append-to-body
@update:model-value="closeDialog">
<el-alert class="mb-4" type="warning" :closable="false" show-icon
title="支付宝会按全量资料重新审核。请按当前已生效的进件资料填写,图片必须使用本表单上传后返回的支付宝 image_id。" />
<div class="mb-4 flex items-center gap-3">
<el-alert class="min-w-0 flex-1"
:type="profileStatus === 'success' ? 'success' : profileStatus === 'warning' ? 'warning' : 'info'"
:closable="false" show-icon :title="profileMessage" />
<el-button :icon="Refresh" :loading="profileLoading" :disabled="loading || !relationId"
@click="loadAlipayProfile">重新读取支付宝资料</el-button>
</div>
<el-form ref="formRef" :model="form" :rules="rules" label-width="150px" v-loading="loading || profileLoading"
class="max-h-[72vh] overflow-y-auto pr-2">
<el-tabs v-model="activeTab">
<el-tab-pane label="商户主体" name="basic">
<el-row :gutter="20">
<el-col :xs="24" :md="12">
<el-form-item label="直付通商户标识">
<el-input :model-value="relation?.channelSubMchId" disabled />
</el-form-item>
</el-col>
<el-col :xs="24" :md="12">
<el-form-item prop="name">
<template #label>
<span>商户名称</span>
<tip content="填写支付宝直付通进件登记的主体名称,不是分账接收方名称或联系人姓名。" />
</template>
<el-input v-model="form.name" maxlength="128" show-word-limit
placeholder="与营业执照或身份证名称一致" />
</el-form-item>
</el-col>
<el-col :xs="24" :md="12">
<el-form-item label="商户简称" prop="aliasName">
<el-input v-model="form.aliasName" maxlength="128" show-word-limit
placeholder="账单和支付结果页展示名称" />
</el-form-item>
</el-col>
<el-col :xs="24" :md="12">
<el-form-item label="商户类型" prop="merchantType">
<el-select v-model="form.merchantType" class="w-full">
<el-option v-for="item in merchantTypeOptions" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :md="12">
<el-form-item label="商户证件类型" prop="certType">
<el-select v-model="form.certType" class="w-full" disabled>
<el-option v-for="item in certTypeOptions" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :md="12">
<el-form-item prop="mcc">
<template #label>
<span>MCC</span>
<tip content="支付宝进件商户类别码。特殊行业还需在“资质材料”中上传行业资质。" />
</template>
<el-input v-model="form.mcc" maxlength="16" placeholder="例如 5812" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="当前已开通服务" prop="services">
<el-select v-model="form.services" class="w-full" multiple filterable
placeholder="请选择该二级商户当前已开通的全部服务">
<el-option v-for="item in serviceOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :md="12">
<el-form-item label="客服电话" prop="servicePhone">
<el-input v-model="form.servicePhone" maxlength="32" />
</el-form-item>
</el-col>
<el-col :xs="24" :md="12">
<el-form-item label="服务商签约日期" prop="signTimeWithIsv">
<el-date-picker v-model="form.signTimeWithIsv" class="w-full" type="date"
value-format="YYYY-MM-DD" format="YYYY-MM-DD" placeholder="选择原签约日期" />
</el-form-item>
</el-col>
<template v-if="form.merchantType !== '06'">
<el-col :xs="24" :md="12">
<el-form-item label="法人名称" prop="legalName">
<el-input v-model="form.legalName" maxlength="128" />
</el-form-item>
</el-col>
<el-col :xs="24" :md="12">
<el-form-item label="法人证件类型" prop="legalCertType">
<el-select v-model="form.legalCertType" class="w-full">
<el-option v-for="item in legalCertTypeOptions" :key="item.value"
:label="item.label" :value="item.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="法人证件号码" prop="legalCertNo">
<el-input v-model="form.legalCertNo" maxlength="64" />
</el-form-item>
</el-col>
</template>
</el-row>
</el-tab-pane>
<el-tab-pane label="联系人与结算" name="settlement">
<div class="mb-4 border-b border-[var(--el-border-color)] pb-2 text-sm font-medium">商户联系人</div>
<el-row :gutter="20">
<el-col :xs="24" :md="12">
<el-form-item label="联系人姓名" prop="contact.name">
<el-input v-model="form.contact.name" maxlength="64" />
</el-form-item>
</el-col>
<el-col :xs="24" :md="12">
<el-form-item label="联系人手机号" prop="contact.mobile">
<el-input v-model="form.contact.mobile" maxlength="32" />
</el-form-item>
</el-col>
<el-col :xs="24" :md="12">
<el-form-item label="联系人类型">
<el-select v-model="form.contact.type" class="w-full" clearable>
<el-option v-for="item in contactTypeOptions" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :md="12">
<el-form-item label="联系人身份证号">
<el-input v-model="form.contact.idCardNo" maxlength="64" />
</el-form-item>
</el-col>
<el-col :xs="24" :md="12">
<el-form-item label="联系人电话">
<el-input v-model="form.contact.phone" maxlength="32" />
</el-form-item>
</el-col>
<el-col :xs="24" :md="12">
<el-form-item label="联系人邮箱">
<el-input v-model="form.contact.email" maxlength="128" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="联系人职责标识">
<el-select v-model="form.contact.tag" class="w-full" multiple filterable allow-create
default-first-option placeholder="选填;按原进件资料填写职责标识" />
</el-form-item>
</el-col>
</el-row>
<div class="mb-4 mt-2 border-b border-[var(--el-border-color)] pb-2 text-sm font-medium">签约与结算</div>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="签约支付宝账号">
<el-input v-model="form.bindingAlipayLogonId" maxlength="128"
placeholder="需要协议确认或增值服务签约时填写" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="结算方式" prop="settlementMode">
<el-radio-group v-model="form.settlementMode">
<el-radio-button label="alipayAccount">支付宝账号</el-radio-button>
<el-radio-button label="bankCard">银行卡</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
<el-col v-if="form.settlementMode === 'alipayAccount'" :span="24">
<el-form-item label="结算支付宝账号" prop="alipayLogonId">
<el-input v-model="form.alipayLogonId" maxlength="128" placeholder="需与商户名称同名且已实名认证" />
</el-form-item>
</el-col>
<template v-else>
<el-col :xs="24" :md="12">
<el-form-item label="银行卡号" prop="settleCard.accountNo">
<el-input v-model="form.settleCard.accountNo" maxlength="64" />
</el-form-item>
</el-col>
<el-col :xs="24" :md="12">
<el-form-item label="银行卡户名" prop="settleCard.accountHolderName">
<el-input v-model="form.settleCard.accountHolderName" maxlength="128" />
</el-form-item>
</el-col>
<el-col :xs="24" :md="12">
<el-form-item label="银行卡类型">
<el-select v-model="form.settleCard.accountType" class="w-full">
<el-option label="借记卡" value="DC" />
<el-option label="信用卡" value="CC" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :md="12">
<el-form-item label="账号使用类型">
<el-select v-model="form.settleCard.usageType" class="w-full">
<el-option label="对公" value="01" />
<el-option label="对私" value="02" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :md="12">
<el-form-item label="银行名称">
<el-input v-model="form.settleCard.accountInstName" maxlength="128" />
</el-form-item>
</el-col>
<el-col :xs="24" :md="12">
<el-form-item label="开户行简称">
<el-input v-model="form.settleCard.accountInstId" maxlength="32" />
</el-form-item>
</el-col>
<el-col :xs="24" :md="12">
<el-form-item label="开户行所在省">
<el-input v-model="form.settleCard.accountInstProvince" maxlength="64" />
</el-form-item>
</el-col>
<el-col :xs="24" :md="12">
<el-form-item label="开户行所在市">
<el-input v-model="form.settleCard.accountInstCity" maxlength="64" />
</el-form-item>
</el-col>
<el-col :xs="24" :md="12">
<el-form-item label="开户支行名称">
<el-input v-model="form.settleCard.accountBranchName" maxlength="128" />
</el-form-item>
</el-col>
<el-col :xs="24" :md="12">
<el-form-item label="联行号(选填)">
<el-input v-model="form.settleCard.bankCode" maxlength="32" />
</el-form-item>
</el-col>
</template>
<el-col :span="24">
<el-form-item label="设置默认结算规则">
<el-switch v-model="form.useDefaultSettleRule" />
</el-form-item>
</el-col>
</el-row>
</el-tab-pane>
<el-tab-pane label="经营地址与图片" name="images">
<template v-if="usesFaceToFace">
<div class="mb-4 border-b border-[var(--el-border-color)] pb-2 text-sm font-medium">经营地址</div>
<el-row :gutter="20">
<el-col :xs="24" :md="8">
<el-form-item label="省份编码">
<el-input v-model="form.businessAddress.provinceCode" maxlength="12" />
</el-form-item>
</el-col>
<el-col :xs="24" :md="8">
<el-form-item label="城市编码">
<el-input v-model="form.businessAddress.cityCode" maxlength="12" />
</el-form-item>
</el-col>
<el-col :xs="24" :md="8">
<el-form-item label="区县编码">
<el-input v-model="form.businessAddress.districtCode" maxlength="12" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="经营详细地址">
<el-input v-model="form.businessAddress.address" maxlength="256" show-word-limit />
</el-form-item>
</el-col>
<el-col :xs="24" :md="8">
<el-form-item label="高德 POI ID">
<el-input v-model="form.businessAddress.poiid" maxlength="64" />
</el-form-item>
</el-col>
<el-col :xs="24" :md="8">
<el-form-item label="经度">
<el-input v-model="form.businessAddress.longitude" maxlength="32" />
</el-form-item>
</el-col>
<el-col :xs="24" :md="8">
<el-form-item label="纬度">
<el-input v-model="form.businessAddress.latitude" maxlength="32" />
</el-form-item>
</el-col>
</el-row>
</template>
<div class="mb-4 mt-2 border-b border-[var(--el-border-color)] pb-2 text-sm font-medium">商户材料</div>
<el-row :gutter="20">
<el-col v-if="usesFaceToFace" :span="24">
<el-form-item label="门头照">
<alipay-image-upload v-model="form.outDoorImages" :relation-id="relationId" multiple />
</el-form-item>
</el-col>
<el-col v-if="usesFaceToFace" :span="24">
<el-form-item label="内景照">
<alipay-image-upload v-model="form.inDoorImages" :relation-id="relationId" multiple />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="商户证件正面/主页">
<alipay-image-upload v-model="form.certImage" :relation-id="relationId" />
</el-form-item>
</el-col>
<el-col v-if="form.merchantType === '06'" :span="24">
<el-form-item label="个人证件反面">
<alipay-image-upload v-model="form.certImageBack" :relation-id="relationId" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="法人证件正面">
<alipay-image-upload v-model="form.legalCertFrontImage" :relation-id="relationId" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="法人证件反面">
<alipay-image-upload v-model="form.legalCertBackImage" :relation-id="relationId" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="授权函/说明函">
<alipay-image-upload v-model="form.licenseAuthLetterImage" :relation-id="relationId" />
</el-form-item>
</el-col>
<template v-if="form.merchantType === '06'">
<el-col :xs="24" :md="12">
<el-form-item label="补充证件类型">
<el-select v-model="form.additionalCertType" class="w-full" clearable>
<el-option
v-for="item in certTypeOptions.filter(option => option.value !== '100')"
:key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :md="12">
<el-form-item label="补充证件号码">
<el-input v-model="form.additionalCertNo" maxlength="64" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="补充证件图片">
<alipay-image-upload v-model="form.additionalCertImage" :relation-id="relationId" />
</el-form-item>
</el-col>
</template>
</el-row>
</el-tab-pane>
<el-tab-pane label="站点与应用" name="sites">
<div class="mb-4 flex items-center justify-between gap-3">
<el-select v-model="form.tradeScene" class="max-w-[520px] flex-1" multiple clearable
placeholder="交易场景(选填;WAP、APP、PC 需站点截图)">
<el-option label="小程序支付(TINY_APP)" value="TINY_APP" />
<el-option label="H5(WAP)" value="WAP" />
<el-option label="线下当面付(OFFLINE)" value="OFFLINE" />
<el-option label="APP" value="APP" />
<el-option label="网站支付(PC)" value="PC" />
</el-select>
<el-button :icon="Plus" @click="addSite()">添加站点</el-button>
</div>
<el-empty v-if="!form.sites.length" description="当前服务不需要站点资料" :image-size="72" />
<div v-for="(site, index) in form.sites" :key="site.key"
class="border-t border-[var(--el-border-color)] py-4 first:border-t-0 first:pt-0">
<div class="mb-3 flex items-center justify-between">
<span class="text-sm font-medium">站点 {{ index + 1 }}</span>
<el-tooltip content="删除站点">
<el-button circle text type="danger" :icon="Delete" @click="removeSite(index)" />
</el-tooltip>
</div>
<el-row :gutter="20">
<el-col :xs="24" :md="8">
<el-form-item label="站点类型">
<el-select v-model="site.siteType" class="w-full">
<el-option v-for="item in siteTypeOptions" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :md="8">
<el-form-item label="站点名称">
<el-input v-model="site.siteName" maxlength="128" />
</el-form-item>
</el-col>
<el-col v-if="site.siteType === '06'" :xs="24" :md="8">
<el-form-item label="支付宝小程序AppID">
<el-input v-model="site.tinyAppId" maxlength="64" />
</el-form-item>
</el-col>
<el-col :xs="24" :md="8">
<el-form-item label="站点地址">
<el-input v-model="site.siteUrl" maxlength="512" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="站点截图">
<alipay-image-upload v-model="site.screenshotImage" :relation-id="relationId" />
</el-form-item>
</el-col>
</el-row>
<el-collapse>
<el-collapse-item title="更多站点资料">
<el-row :gutter="20">
<el-col :xs="24" :md="12"><el-form-item label="下载地址"><el-input
v-model="site.download" /></el-form-item></el-col>
<el-col :xs="24" :md="12"><el-form-item label="应用市场"><el-input
v-model="site.market" /></el-form-item></el-col>
<el-col :xs="24" :md="12"><el-form-item label="上架状态"><el-input
v-model="site.status" /></el-form-item></el-col>
<el-col :xs="24" :md="12"><el-form-item label="网站域名"><el-input
v-model="site.siteDomain" /></el-form-item></el-col>
<el-col :xs="24" :md="12"><el-form-item label="测试账号"><el-input
v-model="site.account" autocomplete="off" /></el-form-item></el-col>
<el-col :xs="24" :md="12"><el-form-item label="测试密码"><el-input
v-model="site.password" type="password" autocomplete="new-password"
show-password /></el-form-item></el-col>
<el-col :xs="24" :md="12"><el-form-item label="ICP信息服务名称"><el-input
v-model="site.icpServiceName" /></el-form-item></el-col>
<el-col :xs="24" :md="12"><el-form-item label="ICP号"><el-input
v-model="site.icpNo" /></el-form-item></el-col>
<el-col :span="24"><el-form-item label="ICP主办单位"><el-input
v-model="site.icpOrgName" /></el-form-item></el-col>
<el-col :span="24"><el-form-item label="站点授权函"><alipay-image-upload
v-model="site.authLetterImage"
:relation-id="relationId" /></el-form-item></el-col>
<el-col :span="24"><el-form-item label="备注"><el-input v-model="site.remark"
type="textarea" /></el-form-item></el-col>
<el-col :span="24"><el-form-item label="备注图片"><alipay-image-upload
v-model="site.remarkImage"
:relation-id="relationId" /></el-form-item></el-col>
</el-row>
</el-collapse-item>
</el-collapse>
</div>
</el-tab-pane>
<el-tab-pane label="资质材料" name="qualifications">
<div class="mb-4 flex justify-end">
<el-button :icon="Plus" @click="addQualification">添加行业资质</el-button>
</div>
<el-alert class="mb-4" type="info" :closable="false" show-icon
title="仅特殊行业 MCC 需要行业资质;每项的资质类型与图片必须同时填写。法人证件照片在“经营地址与图片”中上传。" />
<el-empty v-if="!form.qualifications.length" description="没有行业资质" :image-size="72" />
<div v-for="(item, index) in form.qualifications" :key="item.key"
class="border-t border-[var(--el-border-color)] py-4 first:border-t-0 first:pt-0">
<div class="mb-3 flex items-center justify-between">
<span class="text-sm font-medium">行业资质 {{ index + 1 }}</span>
<el-tooltip content="删除资质">
<el-button circle text type="danger" :icon="Delete"
@click="form.qualifications.splice(index, 1)" />
</el-tooltip>
</div>
<el-row :gutter="20">
<el-col :xs="24" :md="10">
<el-form-item label="行业资质类型">
<el-input v-model="item.industryQualificationType" maxlength="128" />
</el-form-item>
</el-col>
<el-col :xs="24" :md="14">
<el-form-item label="行业资质图片">
<alipay-image-upload v-model="item.industryQualificationImage"
:relation-id="relationId" />
</el-form-item>
</el-col>
</el-row>
</div>
</el-tab-pane>
</el-tabs>
</el-form>
<template #footer>
<el-button :disabled="loading" @click="closeDialog(false)">取消</el-button>
<el-button type="primary" :loading="loading" :disabled="profileLoading" @click="submit">提交修改申请</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { Delete, Plus, Refresh } from '@element-plus/icons-vue'
import { useMessage } from '/@/hooks/message'
import { getAlipayMerchantProfile, modifyAlipayAlias } from '/@/api/thirdparty/third-mch-fz-relation'
import AlipayImageUpload from './alipay-image-upload.vue'
interface RelationInfo {
id?: string | number
channelSubMchId?: string
}
const props = defineProps<{
modelValue: boolean
relation: RelationInfo | null
}>()
const emit = defineEmits(['update:modelValue', 'loading-change', 'submitted'])
const serviceOptions = ['当面付', 'jsapi支付', 'app支付', 'wap支付', '电脑支付', '预授权支付', '商户代扣', '小程序支付', '订单码支付']
const merchantTypeOptions = [
{ value: '01', label: '企业' }, { value: '02', label: '事业单位' },
{ value: '03', label: '民办非企业组织' }, { value: '04', label: '社会团体' },
{ value: '05', label: '党政及国家机关' }, { value: '06', label: '个人商户' },
{ value: '07', label: '个体工商户' },
]
const certTypeOptions = [
{ value: '201', label: '营业执照' }, { value: '218', label: '事业单位法人证书' },
{ value: '204', label: '民办非企业登记证书' }, { value: '206', label: '社会团体法人登记证书' },
{ value: '219', label: '党政机关批准设立文件' }, { value: '100', label: '大陆身份证' },
]
const legalCertTypeOptions = [
{ value: '100', label: '大陆身份证' }, { value: '105', label: '港澳居民往来内地通行证' },
{ value: '106', label: '台湾同胞往来大陆通行证' }, { value: '108', label: '外国人居留证' },
]
const contactTypeOptions = [
{ value: 'LEGAL_PERSON', label: '法人' }, { value: 'CONTROLLER', label: '实际控制人' },
{ value: 'AGENT', label: '代理人' }, { value: 'OTHER', label: '其他' },
]
const siteTypeOptions = [
{ value: '01', label: '网站' }, { value: '02', label: 'APP' }, { value: '06', label: '支付宝小程序' },
]
const merchantCertTypes: Record<string, string> = {
'01': '201', '02': '218', '03': '204', '04': '206', '05': '219', '06': '100', '07': '201',
}
const createSite = (siteType = '01') => ({
key: `${Date.now()}-${Math.random()}`, siteType, siteName: '', siteUrl: '', screenshotImage: '', tinyAppId: '',
account: '', password: '', status: '', market: '', download: '', authLetterImage: '', remark: '',
remarkImage: '', siteDomain: '', icpServiceName: '', icpNo: '', icpOrgName: '',
})
const createForm = () => ({
name: '', aliasName: '', merchantType: '01', mcc: '', services: [] as string[], servicePhone: '',
signTimeWithIsv: '', certType: '201', legalName: '', legalCertNo: '', legalCertType: '100',
bindingAlipayLogonId: '', alipayLogonId: '', settlementMode: 'alipayAccount', useDefaultSettleRule: true,
contact: { name: '', mobile: '', phone: '', email: '', idCardNo: '', tag: [] as string[], type: 'LEGAL_PERSON' },
businessAddress: { provinceCode: '', cityCode: '', districtCode: '', address: '', poiid: '', longitude: '', latitude: '', type: 'BUSINESS_ADDRESS' },
settleCard: { accountBranchName: '', accountHolderName: '', accountInstCity: '', accountInstId: '', accountInstName: '', accountInstProvince: '', accountNo: '', accountType: 'DC', usageType: '01', bankCode: '' },
outDoorImages: [] as string[], inDoorImages: [] as string[], certImage: '', certImageBack: '',
legalCertFrontImage: '', legalCertBackImage: '', licenseAuthLetterImage: '',
additionalCertType: '', additionalCertNo: '', additionalCertImage: '',
sites: [] as ReturnType<typeof createSite>[], tradeScene: [] as string[],
qualifications: [] as Array<{ key: string, industryQualificationType: string, industryQualificationImage: string }>,
})
const formRef = ref()
const loading = ref(false)
const profileLoading = ref(false)
const profileStatus = ref<'idle' | 'success' | 'warning'>('idle')
const profileMessage = ref('打开弹窗后会自动从支付宝读取二级商户基础资料。')
const activeTab = ref('basic')
const form = reactive(createForm())
const relationId = computed(() => props.relation?.id || '')
const usesFaceToFace = computed(() => form.services.includes('当面付'))
let profileRequestSerial = 0
const requiredRule = (message: string, trigger = 'blur') => ({ required: true, message, trigger })
const rules = {
name: [requiredRule('商户名称不能为空')], aliasName: [requiredRule('商户简称不能为空')],
merchantType: [requiredRule('请选择商户类型', 'change')], certType: [requiredRule('请选择商户证件类型', 'change')],
mcc: [requiredRule('MCC不能为空')], services: [{ type: 'array', required: true, min: 1, message: '请选择全部已开通服务', trigger: 'change' }],
servicePhone: [requiredRule('客服电话不能为空')], signTimeWithIsv: [requiredRule('请选择服务商签约日期', 'change')],
legalName: [requiredRule('法人名称不能为空')], legalCertNo: [requiredRule('法人证件号码不能为空')],
legalCertType: [requiredRule('请选择法人证件类型', 'change')],
'contact.name': [requiredRule('联系人姓名不能为空')], 'contact.mobile': [requiredRule('联系人手机号不能为空')],
settlementMode: [requiredRule('请选择结算方式', 'change')], alipayLogonId: [requiredRule('结算支付宝账号不能为空')],
'settleCard.accountNo': [requiredRule('银行卡号不能为空')],
'settleCard.accountHolderName': [requiredRule('银行卡户名不能为空')],
}
watch(() => props.modelValue, (visible) => {
profileRequestSerial += 1
profileLoading.value = false
if (!visible) return
Object.assign(form, createForm())
profileStatus.value = 'idle'
profileMessage.value = '正在从支付宝读取二级商户基础资料...'
activeTab.value = 'basic'
nextTick(() => formRef.value?.clearValidate())
void loadAlipayProfile()
})
watch(() => form.merchantType, (value) => {
form.certType = merchantCertTypes[value] || ''
if (value === '06') {
form.legalName = ''
form.legalCertNo = ''
}
})
watch(() => [...form.services], (services) => {
if ((services.includes('电脑支付') || services.includes('wap支付')) && !form.sites.some(item => item.siteType === '01')) addSite('01')
if (services.includes('app支付') && !form.sites.some(item => ['02', '06'].includes(item.siteType))) addSite('02')
if ((services.includes('jsapi支付') || services.includes('小程序支付')) && !form.sites.some(item => item.siteType === '06')) addSite('06')
})
const addSite = (siteType = '01') => form.sites.push(createSite(siteType))
const removeSite = (index: number) => form.sites.splice(index, 1)
const addQualification = () => form.qualifications.push({
key: `${Date.now()}-${Math.random()}`, industryQualificationType: '', industryQualificationImage: '',
})
const closeDialog = (visible: boolean) => {
if (!loading.value) emit('update:modelValue', visible)
}
const applyAlipayProfile = (data: Record<string, any>) => {
const textFields = ['name', 'aliasName', 'mcc', 'servicePhone', 'merchantType', 'certType', 'alipayLogonId']
textFields.forEach((field) => {
if (String(data?.[field] || '').trim()) (form as any)[field] = data[field]
})
if (Array.isArray(data?.services) && data.services.length) form.services = [...data.services]
if (data?.contact) Object.assign(form.contact, data.contact)
if (data?.businessAddress) Object.assign(form.businessAddress, data.businessAddress)
if (data?.settleCard) Object.assign(form.settleCard, data.settleCard)
if (Array.isArray(data?.sites) && data.sites.length) {
form.sites = data.sites.map((site: Record<string, any>) => ({
...createSite(site.siteType || '01'),
...site,
}))
}
if (String(data?.alipayLogonId || '').trim()) {
form.settlementMode = 'alipayAccount'
} else if (String(data?.settleCard?.accountNo || '').trim()) {
form.settlementMode = 'bankCard'
}
}
const loadAlipayProfile = async () => {
if (profileLoading.value || !relationId.value) return
const requestSerial = ++profileRequestSerial
const requestedRelationId = relationId.value
try {
profileLoading.value = true
profileStatus.value = 'idle'
profileMessage.value = '正在从支付宝读取二级商户基础资料...'
const { data, code } = await getAlipayMerchantProfile(requestedRelationId)
if (requestSerial !== profileRequestSerial || requestedRelationId !== relationId.value || !props.modelValue) return
if (code !== 0 || !data) throw new Error('支付宝未返回商户资料')
applyAlipayProfile(data)
profileStatus.value = 'success'
profileMessage.value = '已回填支付宝可查询的基础资料;法人证件、原签约日期、审核图片、行业资质及未返回字段仍需核对补充。'
nextTick(() => formRef.value?.clearValidate())
} catch (error: any) {
if (requestSerial !== profileRequestSerial || !props.modelValue) return
profileStatus.value = 'warning'
profileMessage.value = error?.msg || error?.message || '支付宝资料读取失败,请手工填写或稍后重试。'
} finally {
if (requestSerial === profileRequestSerial) profileLoading.value = false
}
}
const conditionalError = (): { message: string, tab: string } | null => {
const missing = (value: unknown) => !String(value || '').trim()
if (form.merchantType !== '06' && (missing(form.legalName) || missing(form.legalCertNo))) {
return { message: '非个人商户必须填写法人名称和法人证件号码', tab: 'basic' }
}
if (form.settlementMode === 'bankCard') {
const card = form.settleCard
if ([card.accountNo, card.accountHolderName, card.accountType, card.usageType, card.accountInstName,
card.accountInstId, card.accountInstProvince, card.accountInstCity, card.accountBranchName].some(missing)) {
return { message: '请完整填写结算银行卡资料', tab: 'settlement' }
}
if (form.name.trim() !== card.accountHolderName.trim() && missing(form.licenseAuthLetterImage)) {
return { message: '结算卡户名与商户名称不一致时必须上传授权函或说明函', tab: 'images' }
}
}
if (form.merchantType !== '06' && form.legalCertType !== '100' && missing(form.licenseAuthLetterImage)) {
return { message: '外籍或非大陆身份证法人必须上传身份说明材料', tab: 'images' }
}
if (usesFaceToFace.value) {
const address = form.businessAddress
if ([address.provinceCode, address.cityCode, address.districtCode, address.address].some(missing)) {
return { message: '当面付必须完整填写经营地址', tab: 'images' }
}
if (!form.outDoorImages.length || !form.inDoorImages.length) {
return { message: '当面付必须上传门头照和内景照', tab: 'images' }
}
if ((form.merchantType !== '06' || form.settlementMode === 'bankCard') && missing(form.certImage)) {
return { message: '当前商户必须上传商户证件图片', tab: 'images' }
}
if (form.merchantType === '06' && missing(form.certImageBack)) {
return { message: '个人商户使用当面付必须上传证件反面图片', tab: 'images' }
}
}
for (const site of form.sites) {
if (missing(site.siteType) || missing(site.siteName)) return { message: '每个站点都必须填写类型和名称', tab: 'sites' }
}
if ((form.services.includes('电脑支付') || form.services.includes('wap支付'))
&& !form.sites.some(item => item.siteType === '01' && !missing(item.siteUrl))) {
return { message: '电脑支付或wap支付必须填写网站名称和地址', tab: 'sites' }
}
if (form.services.includes('app支付') && !form.sites.some(item => ['02', '06'].includes(item.siteType))) {
return { message: 'app支付必须填写APP或支付宝小程序资料', tab: 'sites' }
}
if ((form.services.includes('jsapi支付') || form.services.includes('小程序支付'))
&& !form.sites.some(item => item.siteType === '06' && !missing(item.tinyAppId) && !missing(item.siteUrl))) {
return { message: 'jsapi支付或小程序支付必须填写支付宝小程序AppID和站点地址', tab: 'sites' }
}
for (const scene of form.tradeScene) {
if (!['WAP', 'APP', 'PC'].includes(scene)) continue
const targetTypes = scene === 'APP' ? ['02', '06'] : ['01']
if (!form.sites.some(item => targetTypes.includes(item.siteType) && !missing(item.screenshotImage))) {
return { message: `${scene}交易场景必须上传对应站点截图`, tab: 'sites' }
}
}
if (form.qualifications.some(item => missing(item.industryQualificationType) !== missing(item.industryQualificationImage))) {
return { message: '行业资质类型和资质图片必须同时填写', tab: 'qualifications' }
}
if (form.qualifications.some(item => !missing(item.industryQualificationType) && !missing(item.industryQualificationImage))
&& (missing(form.legalCertFrontImage) || missing(form.legalCertBackImage))) {
return { message: '特殊行业必须上传法人证件正面和反面图片', tab: 'images' }
}
const additionalCertValues = [form.additionalCertType, form.additionalCertNo, form.additionalCertImage]
if (additionalCertValues.some(value => !missing(value)) && additionalCertValues.some(missing)) {
return { message: '补充证件类型、证件号和图片必须同时填写', tab: 'images' }
}
return null
}
const submit = async () => {
if (loading.value || !relationId.value) return
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
const error = conditionalError()
if (error) {
activeTab.value = error.tab
useMessage().warning(error.message)
return
}
const payload: Record<string, any> = JSON.parse(JSON.stringify(form))
delete payload.settlementMode
delete payload.useDefaultSettleRule
payload.sites.forEach((site: any) => delete site.key)
payload.qualifications.forEach((item: any) => delete item.key)
if (form.settlementMode === 'alipayAccount') {
payload.settleCard = null
} else {
payload.alipayLogonId = null
}
payload.defaultSettleRule = form.useDefaultSettleRule ? {
defaultSettleType: form.settlementMode,
defaultSettleTarget: form.settlementMode === 'alipayAccount' ? form.alipayLogonId : form.settleCard.accountNo,
} : null
try {
loading.value = true
emit('loading-change', true)
const { data, code } = await modifyAlipayAlias(relationId.value, payload)
if (code !== 0 || !data?.orderId) throw new Error('支付宝未返回申请单号')
useMessage().success(`申请已提交,等待支付宝处理。申请单号:${data.orderId}`)
emit('submitted', data.orderId)
emit('update:modelValue', false)
} catch (error: any) {
useMessage().error(error?.msg || error?.message || '支付宝商户资料修改申请提交失败')
} finally {
loading.value = false
emit('loading-change', false)
}
}
</script>
@@ -19,8 +19,8 @@
<dict-select v-model="form.platformType" dict-code="platform_pay" />
</el-form-item>
</el-col>
<el-alert v-if="form.platformType === '10'" class="mt-[4px]" type="info" show-icon
:closable="false" title="微信子商户号和分账开关统一由下方关系配置;关闭分账后该关系只提供服务商支付路由。" />
<el-alert v-if="form.platformType === '10'" class="mt-[4px]" type="info" show-icon :closable="false"
title="微信子商户号和分账开关统一由下方关系配置;关闭分账后该关系只提供服务商支付路由。" />
<el-alert v-else-if="form.platformType === '20'" class="mt-[4px]" type="info" show-icon
:closable="false" title="支付宝未配置关系或默认关系关闭分账时按普通支付处理;直付通商户、授权和抽佣统一由下方关系配置。" />
</el-row>
@@ -29,15 +29,16 @@
<div class="mb-3 flex items-center justify-between gap-3">
<div>
<div class="text-base font-medium text-[var(--el-text-color-primary)]">分账关系</div>
<div class="mt-1 text-xs text-[var(--el-text-color-secondary)]">默认关系负责支付渠道商户路由;只有“开启分账”且渠道已绑定时才执行平台抽佣。
<div class="mt-1 text-xs text-[var(--el-text-color-secondary)]">
默认关系负责支付渠道商户路由;只有“开启分账”且渠道已绑定时才执行平台抽佣。
</div>
</div>
<div class="flex shrink-0 items-center gap-2">
<el-button icon="Refresh" :disabled="!form.id || relationActionId !== null || relationLoading"
v-auth="'wallet_mchManage_setting'" @click="loadRelations">刷新</el-button>
<el-button type="primary" icon="Plus" :disabled="!form.id || relationActionId !== null || relationLoading"
v-auth="'wallet_mchManage_setting'"
@click="openRelationDialog()">新增关系</el-button>
<el-button type="primary" icon="Plus"
:disabled="!form.id || relationActionId !== null || relationLoading"
v-auth="'wallet_mchManage_setting'" @click="openRelationDialog()">新增关系</el-button>
</div>
</div>
<el-alert v-if="!form.id" type="warning" :closable="false" show-icon title="请先保存支付配置,再维护一对多分账关系。" />
@@ -73,32 +74,41 @@
<el-tag v-if="!isRelationFzEnabled(row)" type="info">分账已关闭</el-tag>
<el-tag v-else :type="relationStatusMeta(row.status).type">{{
relationStatusMeta(row.status).label
}}</el-tag>
}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="boundTime" label="绑定时间" min-width="170">
<template #default="{ row }">{{ isRelationFzEnabled(row) ? (row.boundTime || '-') : '-' }}</template>
<template #default="{ row }">{{ isRelationFzEnabled(row) ? (row.boundTime || '-') : '-'
}}</template>
</el-table-column>
<el-table-column prop="bindError" label="最近错误" min-width="200" show-overflow-tooltip>
<template #default="{ row }">{{ isRelationFzEnabled(row) ? (row.bindError || '-') : '-' }}</template>
<template #default="{ row }">{{ isRelationFzEnabled(row) ? (row.bindError || '-') : '-'
}}</template>
</el-table-column>
<el-table-column label="操作" width="350" fixed="right" align="center">
<el-table-column label="操作" width="460" fixed="right" align="center">
<template #default="{ row }">
<el-button v-if="form.platformType === '20' && row.status !== 'SUPERSEDED' && false" text
type="warning" v-auth="'wallet_mchManage_setting'"
:disabled="relationActionId !== null || relationLoading || aliasLoading"
@click="openAlipayAliasDialog(row)">修改商户简称</el-button>
<el-button v-if="row.status !== 'SUPERSEDED'" text type="primary"
v-auth="'wallet_mchManage_setting'" :disabled="relationActionId !== null || relationLoading"
@click="openRelationDialog(row)">编辑</el-button>
<el-button v-if="row.isDefault !== '1'" text type="primary"
v-auth="'wallet_mchManage_setting'" :loading="relationActionId === row.id"
<el-button v-if="row.isDefault !== '1'" text type="primary" v-auth="'wallet_mchManage_setting'"
:loading="relationActionId === row.id"
:disabled="relationActionId !== null || relationLoading" @click="setRelationDefault(row)">
{{ row.status === 'SUPERSEDED' ? '恢复' : '设为默认' }}
</el-button>
<el-button
v-if="isRelationFzEnabled(row) && row.fzRatio > 0 && ['PENDING_BIND', 'BINDING', 'BIND_FAILED'].includes(row.status)"
text type="warning" v-auth="'wallet_mchManage_setting'" :loading="relationActionId === row.id"
:disabled="relationActionId !== null || relationLoading" @click="retryRelation(row)">重试</el-button>
text type="warning" v-auth="'wallet_mchManage_setting'"
:loading="relationActionId === row.id"
:disabled="relationActionId !== null || relationLoading"
@click="retryRelation(row)">重试</el-button>
<el-button v-if="row.status !== 'SUPERSEDED'" text type="danger"
v-auth="'wallet_mchManage_setting'" :loading="relationActionId === row.id"
:disabled="relationActionId !== null || relationLoading" @click="removeRelation(row)">删除</el-button>
:disabled="relationActionId !== null || relationLoading"
@click="removeRelation(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -121,8 +131,8 @@
<span>是否开启分账</span>
<tip content="关闭后仅使用当前关系的渠道商户路由,不绑定接收方、不抽佣、不退分账。" />
</template>
<el-switch v-model="relationForm.enableFz" active-value="1" inactive-value="0"
active-text="开启" inactive-text="关闭" />
<el-switch v-model="relationForm.enableFz" active-value="1" inactive-value="0" active-text="开启"
inactive-text="关闭" />
</el-form-item>
<el-form-item v-if="form.platformType === '20'" label="平台账号键" prop="accountKey">
<el-input v-model="relationForm.accountKey" maxlength="64" placeholder="为空使用平台默认支付宝账号" />
@@ -153,7 +163,8 @@
<span class="ml-2 text-[var(--el-text-color-secondary)]">%</span>
</div>
</el-form-item>
<el-form-item v-if="form.platformType === '10' && relationForm.enableFz === '1'" label="微信关系类型" prop="channelRelationType">
<el-form-item v-if="form.platformType === '10' && relationForm.enableFz === '1'" label="微信关系类型"
prop="channelRelationType">
<el-select v-model="relationForm.channelRelationType" class="w-full">
<el-option v-for="item in wechatRelationOptions" :key="item.value" :label="item.label"
:value="item.value" />
@@ -166,7 +177,8 @@
<el-alert type="info" :closable="false" show-icon>
<template #title>{{ form.platformType === '20'
? '支付宝关系只允许填写 userId、loginName、openId 或 cardAliasNo 中的一种。'
: '微信接收方类型和关系类型在开启分账后提交微信分账关系接口。' }} 修改二级商户、平台账号或接收方身份时会生成新关系版本;仅开启分账的关系会进入渠道绑定任务。历史订单仍保留原关系。</template>
: '微信接收方类型和关系类型在开启分账后提交微信分账关系接口。' }}
修改二级商户、平台账号或接收方身份时会生成新关系版本;仅开启分账的关系会进入渠道绑定任务。历史订单仍保留原关系。</template>
</el-alert>
</el-form>
<template #footer>
@@ -176,6 +188,9 @@
</el-button>
</template>
</el-dialog>
<alipay-merchant-profile-dialog v-model="aliasVisible" :relation="aliasRelation"
@loading-change="aliasLoading = $event" />
</template>
<script setup lang="ts" name="PaySettingForm">
@@ -183,6 +198,7 @@ import { useMessage, useMessageBox } from "/@/hooks/message"
import * as mchPaySettingApi from "/@/api/wallet/mch-pay-setting";
import * as mchFzRelationApi from "/@/api/thirdparty/third-mch-fz-relation";
import AlipayMerchantProfileDialog from './alipay-merchant-profile-dialog.vue'
const emit = defineEmits(["refresh"])
@@ -196,6 +212,12 @@ const relationActionId = ref<string | number | null>(null)
const relationVisible = ref(false)
const relationFormRef = ref()
const relations = ref<any[]>([])
/** 支付宝商户简称修改弹窗是否显示。 */
const aliasVisible = ref(false)
/** 支付宝商户简称修改申请是否正在提交。 */
const aliasLoading = ref(false)
/** 当前正在修改支付宝全量资料的关系。 */
const aliasRelation = ref<any>(null)
const relationForm = reactive<any>({
id: undefined,
mchId: '',
@@ -383,6 +405,24 @@ const openRelationDialog = (row?: any) => {
relationVisible.value = true
}
/** 打开支付宝商户简称修改弹窗;简称属于支付宝二级商户资料,不并入分账关系编辑表单。 */
const openAlipayAliasDialog = (row: any) => {
if (form.platformType !== '20') {
useMessage().warning('只有支付宝支付配置支持修改商户简称')
return
}
if (!row?.id || row.status === 'SUPERSEDED') {
useMessage().warning('当前关系不能修改支付宝商户简称')
return
}
if (!String(row.channelSubMchId || '').trim()) {
useMessage().warning('请先配置支付宝直付通二级商户标识')
return
}
aliasRelation.value = row
aliasVisible.value = true
}
/** 保存关系配置;渠道身份变更由后端创建新版本,其余渠道字段变更会唤醒稳定的 Quartz 绑定任务。 */
const saveRelation = async () => {
const valid = await relationFormRef.value?.validate().catch(() => false)