| | |
| | | for (let index = 0; index < records.length; index++) { |
| | | const item: any = records[index] |
| | | const rawType = item.merchantType != null ? item.merchantType as string : 'food' |
| | | const hasConfiguredTags = item.benefitTags instanceof Array |
| | | const hasConfiguredTags = item.benefitTags instanceof Array && item.benefitTags.length > 0 |
| | | const configuredTags: any[] = hasConfiguredTags ? item.benefitTags as any[] : [] |
| | | const tags: string[] = [] |
| | | for (let tagIndex = 0; tagIndex < configuredTags.length; tagIndex++) { |
| | | const tag = String(configuredTags[tagIndex]).trim() |
| | | if (tag != '' && tags.indexOf(tag) < 0) tags.push(tag) |
| | | } |
| | | const discount = item.discountText != null ? item.discountText as string : formatRemoteDiscount(item) |
| | | const discount = item.discountText != null && String(item.discountText).trim() != '' |
| | | ? String(item.discountText).trim() |
| | | : formatRemoteDiscount(item) |
| | | if (!hasConfiguredTags && discount != '') tags.push(discount) |
| | | mapped.push({ |
| | | id: '' + item.id, |
| | |
| | | <view v-else class="content"> |
| | | <view class="service-card"> |
| | | <view class="service-title-row"> |
| | | <view class="service-icon"><image src="/static/merchant-assets/car-wash-orange.svg" mode="aspectFit"></image></view> |
| | | <view class="service-icon"><image src="/static/merchant-assets/car-wash-service-orange.svg" mode="aspectFit"></image></view> |
| | | <text class="service-title">{{merchantName}}</text> |
| | | </view> |
| | | <view class="talent-tag"><text>{{applicableCards}}</text></view> |
| | |
| | | .school-table-header { display: flex; min-height: 67rpx; background-color: #EEF4FF; flex-direction: row; align-items: center; } |
| | | .school-head-name { display: flex; min-width: 0; height: 67rpx; flex: 1; box-sizing: border-box; padding: 0 22rpx; justify-content: flex-start; align-items: center; } |
| | | .school-head-action { width: 126rpx; height: 67rpx; flex-shrink: 0; } |
| | | .school-head-name text { width: 100%; color: #26374F; font-size: 23rpx; font-weight: 700; text-align: left; } |
| | | .school-head-name text { display:block; width:100%; height:67rpx; color:#26374F; font-size:23rpx; line-height:67rpx; font-weight:700; text-align:left; } |
| | | .school-row { display: flex; height: 92rpx; min-height: 92rpx; border-top-width: 1rpx; border-top-style: solid; border-top-color: #E7ECF4; flex-direction: row; align-items: center; } |
| | | .school-name-cell { display: flex; min-width: 0; height: 92rpx; flex: 1; box-sizing: border-box; padding: 0 22rpx; align-items: center; justify-content: flex-start; } |
| | | .school-name-text { display:block; width:100%; min-width:0; height:92rpx; flex:1; color:#48566A; font-size:21rpx; line-height:92rpx; text-align:left; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } |
| | |
| | | const session = getSession() |
| | | if (session != null && session.talentId != null) talentId.value = session.talentId as string |
| | | if (talentId.value == '' && apiEnvironment == 'development') talentId.value = '2' |
| | | if (isParkingMerchant.value && talentId.value != '') { |
| | | if ((isFuelMerchant.value || isParkingMerchant.value) && talentId.value != '') { |
| | | getTalentCardProfile(talentId.value, (data: any) => { |
| | | const card = data != null ? data.card : null |
| | | talentCardId.value = card != null && card.id != null ? '' + card.id : '' |
| | |
| | | listLoading.value = true |
| | | listLoaded.value = false |
| | | listError.value = false |
| | | getFuelMerchantDetail(merchantId.value, (data: any) => { |
| | | getFuelMerchantDetail(merchantId.value, talentCardId.value, (data: any) => { |
| | | merchantName.value = data != null && data.merchantName != null ? data.merchantName as string : '' |
| | | merchantAddress.value = data != null && data.address != null ? data.address as string : '' |
| | | merchantPhone.value = data != null && data.contactMobile != null ? data.contactMobile as string : '' |
| | |
| | | .rights-panel { margin: 16rpx 25rpx 0; padding: 28rpx 26rpx; border-radius: 15rpx; background-color: #FFFFFF; } |
| | | .rights-text { display: block; margin-top: 0; color: #777F8B; font-size: 27rpx; line-height: 43rpx; } |
| | | .store-panel { margin: 16rpx 25rpx 0; overflow: hidden; border-width: 2rpx; border-style: solid; border-color: #DCE7FA; border-radius: 16rpx; background-color: #FFFFFF; } |
| | | .store-table-header { display: flex; min-height: 67rpx; background-color: #EEF4FF; flex-direction: row; align-items: center; } |
| | | .store-head-name, .store-head-address { display: flex; height: 67rpx; box-sizing: border-box; padding: 0 13rpx; justify-content: flex-start; align-items: center; } |
| | | .store-table-header { display: flex; height: 67rpx; background-color: #EEF4FF; flex-direction: row; align-items: center; } |
| | | .store-head-name, .store-head-address { display: flex; height: 67rpx; position: relative; box-sizing: border-box; padding: 0; justify-content: flex-start; align-items: center; } |
| | | .store-head-name { width: 250rpx; } |
| | | .store-head-address { min-width: 0; flex: 1; } |
| | | .store-head-action { width: 72rpx; height: 67rpx; flex-shrink: 0; } |
| | | .store-head-name text, .store-head-address text { width: 100%; color: #26374F; font-size: 23rpx; font-weight: 700; text-align: left; } |
| | | .store-head-name text, .store-head-address text { display: block; position: absolute; left: 13rpx; top: 0; right: 0; height: 67rpx; color: #26374F; font-size: 23rpx; font-weight: 700; line-height: 67rpx; text-align: left; } |
| | | .store-row { display: flex; min-height: 92rpx; border-top-width: 1rpx; border-top-style: solid; border-top-color: #E7ECF4; flex-direction: row; align-items: stretch; } |
| | | .store-name-cell { display: flex; width: 250rpx; min-height: 92rpx; box-sizing: border-box; border-right-width: 1rpx; border-right-style: solid; border-right-color: #E7ECF4; align-items: stretch; justify-content: flex-start; } |
| | | .store-address-cell { display: flex; min-width: 0; min-height: 92rpx; flex: 1; box-sizing: border-box; align-items: stretch; justify-content: flex-start; } |
| | |
| | | .medical-open-value { min-width: 0; flex: 1; margin-left: 14rpx; color: #FF7C2B; font-size: 26rpx; font-weight: 600; } |
| | | .medical-view-button { display: flex; width: 132rpx; height: 57rpx; border-radius: 29rpx; background-image: linear-gradient(90deg, #FF5733 0%, #FF8A20 100%); flex-shrink: 0; justify-content: center; align-items: center; } |
| | | .medical-view-button text { color: #FFFFFF; font-size: 27rpx; } |
| | | .medical-usage-panel { min-height: 548rpx; margin: 16rpx 25rpx 0; box-sizing: border-box; padding: 31rpx 27rpx; border-radius: 15rpx; background-color: #EFF1F4; } |
| | | .medical-usage-panel { margin: 16rpx 25rpx 0; box-sizing: border-box; padding: 28rpx 26rpx; border-radius: 15rpx; background-color: #FFFFFF; } |
| | | .medical-usage-panel text { color: #777F8B; font-size: 27rpx; line-height: 43rpx; white-space: pre-wrap; } |
| | | .action-bar { display: flex; height: 150rpx; height: calc(150rpx + env(safe-area-inset-bottom)); box-sizing: border-box; padding: 20rpx 34rpx 2rpx; padding-bottom: calc(2rpx + env(safe-area-inset-bottom)); background-color: #FFFFFF; box-shadow: 0 -5rpx 15rpx rgba(0,0,0,.06); flex-direction: column; justify-content: flex-end; } |
| | | .talent-code-button { display: flex; width: 100%; height: 90rpx; flex-shrink: 0; border-radius: 46rpx; background-image: linear-gradient(90deg, #FF4D32 0%, #FF8A20 100%); flex-direction: row; justify-content: center; align-items: center; } |
| | |
| | | export const getMedicalPackage = (packageId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appMedicalPackages') + encodeURIComponent(packageId), method: 'GET', skipAuth: true }, onSuccess, onError) |
| | | export const getMedicalPackageItems = (packageId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appMedicalPackages') + encodeURIComponent(packageId) + '/items', method: 'GET', skipAuth: true }, onSuccess, onError) |
| | | export const submitServiceAppointment = (payload: any, idempotencyKey: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appServiceAppointmentApplications'), method: 'POST', skipAuth: true, data: payload, idempotencyKey: idempotencyKey }, onSuccess, onError) |
| | | export const getFuelMerchantDetail = (merchantId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appFuelMerchantDetail') + encodeURIComponent(merchantId), method: 'GET', skipAuth: true }, onSuccess, onError) |
| | | export const getFuelMerchantDetail = (merchantId: string, talentCardId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => { |
| | | const data: any = {} |
| | | if (talentCardId != '') data.talentCardId = Number(talentCardId) |
| | | return request<any>({ path: getApiEndpoint('appFuelMerchantDetail') + encodeURIComponent(merchantId), method: 'GET', skipAuth: true, data: data }, onSuccess, onError) |
| | | } |
| | | export const submitFuelMobileRegistration = (merchantId: string, talentId: string, benefitId: string, mobile: string, idempotencyKey: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appFuelMerchantDetail') + encodeURIComponent(merchantId) + '/registrations', method: 'POST', skipAuth: true, data: { talentId: Number(talentId), benefitId: Number(benefitId), mobile: mobile }, idempotencyKey: idempotencyKey }, onSuccess, onError) |
| | | export const getParkingMerchantDetail = (merchantId: string, talentCardId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => { |
| | | const data: any = {} |
| New file |
| | |
| | | <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#E9924D" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> |
| | | <path d="M6.3 11.1 7.6 8a2 2 0 0 1 1.84-1.24h5.12A2 2 0 0 1 16.4 8l1.3 3.1"/> |
| | | <path d="M5.4 11.1h13.2a1.8 1.8 0 0 1 1.8 1.8v4.2a1.8 1.8 0 0 1-1.8 1.8H5.4a1.8 1.8 0 0 1-1.8-1.8v-4.2a1.8 1.8 0 0 1 1.8-1.8Z"/> |
| | | <path d="M6.6 18.9v1.2M17.4 18.9v1.2M7.3 14.4h.01M16.7 14.4h.01"/> |
| | | <path d="M18.6.8c-.9 1.1-1.45 1.9-1.45 2.62a1.45 1.45 0 0 0 2.9 0c0-.72-.55-1.52-1.45-2.62Z"/> |
| | | <path d="M21.5 3.9c-.56.68-.9 1.17-.9 1.61a.9.9 0 0 0 1.8 0c0-.44-.34-.93-.9-1.61Z"/> |
| | | </svg> |
| | |
| | | </view> |
| | | |
| | | <view class="result-content"> |
| | | <view class="status-icon-wrap"><image class="status-icon" src="/static/icons/lucide/clock-3.svg" mode="aspectFit"></image></view> |
| | | <text class="result-title">进件信息审核中</text> |
| | | <view class="return-button" @tap="goBack"><text>返回提现</text></view> |
| | | <view class="status-icon-wrap"><image class="status-icon" :src="statusIcon" mode="aspectFit"></image></view> |
| | | <text class="result-title">{{statusTitle}}</text> |
| | | <text v-if="statusReason.length > 0" class="result-reason">{{statusReason}}</text> |
| | | <view v-if="onboardingStatus == 'rejected'" class="return-button" @tap="resubmit"><text>重新提交</text></view> |
| | | <view v-else class="return-button" @tap="goBack"><text>返回商户中心</text></view> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup lang="uts"> |
| | | import { loadOnboardingStatus, SettlementOnboarding } from '../../services/settlement.uts' |
| | | |
| | | const onboardingStatus = ref('pending') |
| | | const statusReason = ref('') |
| | | const statusTitle = computed((): string => { |
| | | if (onboardingStatus.value == 'approved') return '商户进件已完成' |
| | | if (onboardingStatus.value == 'rejected') return '进件审核未通过' |
| | | return '进件信息审核中' |
| | | }) |
| | | const statusIcon = computed((): string => { |
| | | if (onboardingStatus.value == 'approved') return '/static/icons/lucide/badge-check-orange.svg' |
| | | if (onboardingStatus.value == 'rejected') return '/static/icons/lucide/x-orange.svg' |
| | | return '/static/icons/lucide/clock-3.svg' |
| | | }) |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack() |
| | | } |
| | | |
| | | const resubmit = () => { |
| | | uni.redirectTo({ url: '/pages/merchant-onboarding/merchant-onboarding' }) |
| | | } |
| | | |
| | | const loadStatus = () => { |
| | | loadOnboardingStatus((item: SettlementOnboarding) => { |
| | | if (item.status == 'unsubmitted') { |
| | | uni.redirectTo({ url: '/pages/merchant-onboarding/merchant-onboarding' }) |
| | | return |
| | | } |
| | | onboardingStatus.value = item.status |
| | | if (item.status == 'approved') uni.removeStorageSync('merchant-onboarding-draft') |
| | | statusReason.value = item.status == 'rejected' ? item.reason : '' |
| | | }, (error: any) => { |
| | | uni.showToast({ title: error.message != null ? error.message : '进件状态加载失败', icon: 'none' }) |
| | | }) |
| | | } |
| | | |
| | | onShow(() => { |
| | | loadStatus() |
| | | }) |
| | | </script> |
| | | |
| | | <style> |
| | |
| | | .status-icon-wrap { display: flex; width: 176rpx; height: 176rpx; justify-content: center; align-items: center; border-radius: 88rpx; background-image: linear-gradient(145deg, #FFF0E6 0%, #FFF8F3 100%); box-shadow: 0 18rpx 38rpx rgba(255, 107, 44, 0.12); } |
| | | .status-icon { width: 82rpx; height: 82rpx; } |
| | | .result-title { margin-top: 40rpx; color: #293547; font-size: 34rpx; line-height: 48rpx; font-weight: 700; } |
| | | .result-reason { width: 100%; margin-top: 24rpx; color: #7D8796; font-size: 25rpx; line-height: 38rpx; text-align: center; } |
| | | .return-button { display: flex; width: 100%; height: 88rpx; margin-top: 70rpx; justify-content: center; align-items: center; border-radius: 44rpx; color: #FFFFFF; font-size: 29rpx; line-height: 41rpx; font-weight: 700; background-image: linear-gradient(90deg, #FF7625 0%, #FF9630 100%); box-shadow: 0 12rpx 24rpx rgba(255, 107, 36, 0.20); } |
| | | </style> |
| | |
| | | <template> |
| | | <view class="page"> |
| | | <view class="navigation-bar"> |
| | | <view class="back-button" @tap="goBack"><image class="back-icon" src="/static/icons/lucide/back-left.svg" mode="aspectFit"></image></view> |
| | | <text class="page-title">商户进件</text> |
| | | </view> |
| | | |
| | | <scroll-view class="form-scroll" scroll-y="true" :show-scrollbar="false" :enhanced="false"> |
| | | <view class="form-card subject-card"> |
| | | <view class="merchant-type-row" @tap="chooseMerchantType"> |
| | | <view class="type-copy"><text class="field-label inline-label">商户类型</text></view> |
| | | <view class="type-control"><text>{{merchantType == 'enterprise' ? '企业' : '个人'}}</text><image class="down-icon" src="/static/icons/lucide/chevron-down.svg" mode="aspectFit"></image></view> |
| | | </view> |
| | | <view class="navigation-bar"><view class="back-button" @tap="goBack"><image class="back-icon" src="/static/icons/lucide/back-left.svg" mode="aspectFit"></image></view><text class="page-title">商户进件</text></view> |
| | | <view class="type-row"><text class="type-label">商户类型</text><view class="type-select" @tap="toggleMerchantTypeDropdown"><text>{{merchantType == 'enterprise' ? '企业' : '个人'}}</text><image class="down-icon" src="/static/icons/lucide/chevron-down.svg" mode="aspectFit"></image></view></view> |
| | | <scroll-view class="form-scroll" scroll-y="true" :show-scrollbar="false"><view class="form-body"> |
| | | <view v-if="merchantType == 'enterprise'"> |
| | | <view class="field-block"><text class="field-label required">商户名称</text><view class="input-shell" :class="invalidField == 'merchantName' ? 'field-invalid' : ''"><input class="text-input" v-model="merchantName" maxlength="128" placeholder="请输入" placeholder-class="input-placeholder" @input="clearFieldError('merchantName')" /></view><text v-if="invalidField == 'merchantName'" class="field-error">{{errorMessage}}</text></view> |
| | | <view class="field-block"><text class="field-label">商户简称</text><view class="input-shell"><input class="text-input" v-model="merchantShortName" maxlength="128" placeholder="请输入" placeholder-class="input-placeholder" /></view></view> |
| | | <view class="field-block"><text class="field-label required">公司类型</text><picker class="date-picker" mode="selector" :range="companyTypeOptions" :value="companyTypeIndex" @change="onCompanyTypeChange"><view class="select-shell" :class="invalidField == 'companyType' ? 'field-invalid' : ''"><text>{{companyType}}</text><image class="down-icon" src="/static/icons/lucide/chevron-down.svg" mode="aspectFit"></image></view></picker><text v-if="invalidField == 'companyType'" class="field-error">{{errorMessage}}</text></view> |
| | | <view class="field-block"><text class="field-label required">营业执照</text><view class="upload-box" :class="invalidField == 'businessLicenseUrl' ? 'field-invalid' : ''" @tap="chooseDocument('businessLicense')"><image v-if="businessLicenseUrl.length > 0" class="document-image" :src="businessLicenseUrl" mode="aspectFill"></image><text v-else>{{uploadingField == 'businessLicense' ? '上传中...' : '添加图片'}}</text></view><text v-if="invalidField == 'businessLicenseUrl'" class="field-error">{{errorMessage}}</text></view> |
| | | <view class="field-block"><text class="field-label required">统一社会信用代码</text><view class="input-shell" :class="invalidField == 'creditCode' ? 'field-invalid' : ''"><input class="text-input" :value="creditCode" maxlength="18" placeholder="请输入" placeholder-class="input-placeholder" @input="normalizeCreditCode" /></view><text v-if="invalidField == 'creditCode'" class="field-error">{{errorMessage}}</text></view> |
| | | <view class="field-block"><text class="field-label required">营业执照有效期</text><view class="dropdown-wrap"><view class="select-shell" @tap="toggleDropdown('businessValidity')"><text>{{businessValidity}}</text><image class="down-icon" src="/static/icons/lucide/chevron-down.svg" mode="aspectFit"></image></view></view></view> |
| | | <view class="field-block"><text class="field-label required">营业执照有效期起始日期</text><picker class="date-picker" mode="date" :value="businessStartDate" start="1900-01-01" end="2099-12-31" @change="onBusinessStartChange"><view class="select-shell" :class="invalidField == 'businessStartDate' ? 'field-invalid' : ''"><text :class="businessStartDate.length == 0 ? 'select-placeholder' : ''">{{businessStartDate.length > 0 ? businessStartDate : '请选择'}}</text><image class="down-icon" src="/static/icons/lucide/chevron-down.svg" mode="aspectFit"></image></view></picker><text v-if="invalidField == 'businessStartDate'" class="field-error">{{errorMessage}}</text></view> |
| | | <view v-if="businessValidity == '固定期限'" class="field-block"><text class="field-label required">营业执照有效期结束日期</text><picker class="date-picker" mode="date" :value="businessEndDate" start="1900-01-01" end="2099-12-31" @change="onBusinessEndChange"><view class="select-shell" :class="invalidField == 'businessEndDate' ? 'field-invalid' : ''"><text :class="businessEndDate.length == 0 ? 'select-placeholder' : ''">{{businessEndDate.length > 0 ? businessEndDate : '请选择'}}</text><image class="down-icon" src="/static/icons/lucide/chevron-down.svg" mode="aspectFit"></image></view></picker><text v-if="invalidField == 'businessEndDate'" class="field-error">{{errorMessage}}</text></view> |
| | | <view class="field-block"><text class="field-label required">成立时间</text><picker class="date-picker" mode="date" :value="establishedDate" start="1900-01-01" end="2099-12-31" @change="onEstablishedDateChange"><view class="select-shell" :class="invalidField == 'establishedDate' ? 'field-invalid' : ''"><text :class="establishedDate.length == 0 ? 'select-placeholder' : ''">{{establishedDate.length > 0 ? establishedDate : '请选择'}}</text><image class="down-icon" src="/static/icons/lucide/chevron-down.svg" mode="aspectFit"></image></view></picker><text v-if="invalidField == 'establishedDate'" class="field-error">{{errorMessage}}</text></view> |
| | | <view class="field-block"><text class="field-label required">注册地址(省、市、区)</text><picker class="region-picker" mode="region" @change="onRegisteredRegionChange"><view class="select-shell" :class="invalidField == 'registeredRegion' ? 'field-invalid' : ''"><text class="ellipsis-text" :class="registeredRegion.length == 0 ? 'select-placeholder' : ''">{{registeredRegion.length > 0 ? registeredRegion : '请选择'}}</text><image class="down-icon" src="/static/icons/lucide/chevron-down.svg" mode="aspectFit"></image></view></picker><text v-if="invalidField == 'registeredRegion'" class="field-error">{{errorMessage}}</text></view> |
| | | <view class="field-block"><text class="field-label required">详细注册地址</text><view class="input-shell" :class="invalidField == 'registeredAddress' ? 'field-invalid' : ''"><input class="text-input" v-model="registeredAddress" maxlength="255" placeholder="请输入" placeholder-class="input-placeholder" @input="clearFieldError('registeredAddress')" /></view><text v-if="invalidField == 'registeredAddress'" class="field-error">{{errorMessage}}</text></view> |
| | | <view class="field-block"><text class="field-label">注册资本</text><view class="input-shell"><input class="text-input" v-model="registeredCapital" maxlength="64" placeholder="请输入" placeholder-class="input-placeholder" /></view></view> |
| | | </view> |
| | | |
| | | <view v-if="merchantType == 'enterprise'" class="form-card"> |
| | | <text class="section-title">企业资料</text> |
| | | |
| | | <view class="field-block"> |
| | | <text class="field-label">统一社会信用代码</text> |
| | | <view class="input-shell" :class="invalidField == 'creditCode' ? 'field-invalid' : ''"><input class="text-input" :value="creditCode" maxlength="18" placeholder="请输入18位统一社会信用代码" placeholder-class="input-placeholder" @input="normalizeCreditCode" /></view> |
| | | <text v-if="invalidField == 'creditCode'" class="field-error">{{errorMessage}}</text> |
| | | </view> |
| | | |
| | | <view class="field-block"> |
| | | <text class="field-label">营业执照有效期</text> |
| | | <view class="select-shell" :class="invalidField == 'businessValidity' ? 'field-invalid' : ''" @tap="chooseBusinessValidity"><text>{{businessValidity}}</text><image class="down-icon" src="/static/icons/lucide/chevron-down.svg" mode="aspectFit"></image></view> |
| | | <text v-if="invalidField == 'businessValidity'" class="field-error">{{errorMessage}}</text> |
| | | </view> |
| | | |
| | | <view class="field-block"> |
| | | <text class="field-label">证照有效期起始日期</text> |
| | | <picker class="date-picker" mode="date" :value="businessStartDate" start="1990-01-01" end="2099-12-31" @change="onBusinessStartChange"><view class="select-shell" :class="invalidField == 'businessStartDate' ? 'field-invalid' : ''"><text :class="businessStartDate.length == 0 ? 'select-placeholder' : ''">{{businessStartDate.length > 0 ? businessStartDate : '请选择'}}</text><image class="down-icon" src="/static/icons/lucide/chevron-down.svg" mode="aspectFit"></image></view></picker> |
| | | <text v-if="invalidField == 'businessStartDate'" class="field-error">{{errorMessage}}</text> |
| | | </view> |
| | | |
| | | <view v-if="businessValidity == '固定期限'" class="field-block"> |
| | | <text class="field-label">证照有效期结束日期</text> |
| | | <picker class="date-picker" mode="date" :value="businessEndDate" start="1990-01-01" end="2099-12-31" @change="onBusinessEndChange"><view class="select-shell" :class="invalidField == 'businessEndDate' ? 'field-invalid' : ''"><text :class="businessEndDate.length == 0 ? 'select-placeholder' : ''">{{businessEndDate.length > 0 ? businessEndDate : '请选择'}}</text><image class="down-icon" src="/static/icons/lucide/chevron-down.svg" mode="aspectFit"></image></view></picker> |
| | | <text v-if="invalidField == 'businessEndDate'" class="field-error">{{errorMessage}}</text> |
| | | </view> |
| | | |
| | | <view class="field-block"> |
| | | <text class="field-label">注册地址(省、市、区)</text> |
| | | <picker class="region-picker" mode="region" @change="onRegisteredRegionChange"><view class="select-shell" :class="invalidField == 'registeredRegion' ? 'field-invalid' : ''"><text class="ellipsis-text" :class="registeredRegion.length == 0 ? 'select-placeholder' : ''">{{registeredRegion.length > 0 ? registeredRegion : '请选择'}}</text><image class="down-icon" src="/static/icons/lucide/chevron-down.svg" mode="aspectFit"></image></view></picker> |
| | | <text v-if="invalidField == 'registeredRegion'" class="field-error">{{errorMessage}}</text> |
| | | </view> |
| | | |
| | | <view class="field-block last-field"> |
| | | <text class="field-label">详细注册地址</text> |
| | | <view class="input-shell" :class="invalidField == 'registeredAddress' ? 'field-invalid' : ''"><input class="text-input" v-model="registeredAddress" maxlength="80" placeholder="请输入详细注册地址" placeholder-class="input-placeholder" @input="clearFieldError('registeredAddress')" /></view> |
| | | <text v-if="invalidField == 'registeredAddress'" class="field-error">{{errorMessage}}</text> |
| | | </view> |
| | | <view class="field-block"><text class="field-label required">{{merchantType == 'enterprise' ? '法人姓名' : '个人姓名'}}</text><view class="input-shell" :class="invalidField == 'subjectName' ? 'field-invalid' : ''"><input class="text-input" v-model="subjectName" maxlength="64" placeholder="请输入" placeholder-class="input-placeholder" @input="clearFieldError('subjectName')" /></view><text v-if="invalidField == 'subjectName'" class="field-error">{{errorMessage}}</text></view> |
| | | <view v-if="merchantType == 'individual'"> |
| | | <view class="field-block"><text class="field-label required">行业类型</text><picker class="date-picker" mode="selector" :range="industryTypeOptions" :value="industryTypeIndex" @change="onIndustryTypeChange"><view class="select-shell" :class="invalidField == 'industryType' ? 'field-invalid' : ''"><text :class="industryType.length == 0 ? 'select-placeholder' : ''">{{industryType.length > 0 ? industryType : '请选择'}}</text><image class="down-icon" src="/static/icons/lucide/chevron-down.svg" mode="aspectFit"></image></view></picker><text v-if="invalidField == 'industryType'" class="field-error">{{errorMessage}}</text></view> |
| | | <view class="field-block"><text class="field-label required">经营地区(省、市、区)</text><picker class="region-picker" mode="region" @change="onRegisteredRegionChange"><view class="select-shell" :class="invalidField == 'registeredRegion' ? 'field-invalid' : ''"><text class="ellipsis-text" :class="registeredRegion.length == 0 ? 'select-placeholder' : ''">{{registeredRegion.length > 0 ? registeredRegion : '请选择'}}</text><image class="down-icon" src="/static/icons/lucide/chevron-down.svg" mode="aspectFit"></image></view></picker><text v-if="invalidField == 'registeredRegion'" class="field-error">{{errorMessage}}</text></view> |
| | | </view> |
| | | |
| | | <view class="form-card"> |
| | | <text class="section-title">{{merchantType == 'enterprise' ? '法人信息' : '个人信息'}}</text> |
| | | |
| | | <view class="field-block"> |
| | | <text class="field-label">{{merchantType == 'enterprise' ? '法人姓名' : '个人姓名'}}</text> |
| | | <view class="input-shell" :class="invalidField == 'subjectName' ? 'field-invalid' : ''"><input class="text-input" v-model="subjectName" maxlength="30" placeholder="请输入姓名" placeholder-class="input-placeholder" @input="clearFieldError('subjectName')" /></view> |
| | | <text v-if="invalidField == 'subjectName'" class="field-error">{{errorMessage}}</text> |
| | | </view> |
| | | |
| | | <view class="field-block"> |
| | | <text class="field-label">{{merchantType == 'enterprise' ? '法人证件类型' : '个人证件类型'}}</text> |
| | | <view class="select-shell readonly-shell"><text>身份证</text><image class="down-icon" src="/static/icons/lucide/chevron-down.svg" mode="aspectFit"></image></view> |
| | | </view> |
| | | |
| | | <view class="field-block"> |
| | | <text class="field-label">{{merchantType == 'enterprise' ? '法人证件号码' : '个人证件号码'}}</text> |
| | | <view class="input-shell" :class="invalidField == 'subjectIdNumber' ? 'field-invalid' : ''"><input class="text-input" type="idcard" v-model="subjectIdNumber" maxlength="18" placeholder="请输入身份证号码" placeholder-class="input-placeholder" @input="clearFieldError('subjectIdNumber')" /></view> |
| | | <text v-if="invalidField == 'subjectIdNumber'" class="field-error">{{errorMessage}}</text> |
| | | </view> |
| | | |
| | | <view class="field-block"> |
| | | <text class="field-label">{{merchantType == 'enterprise' ? '法人证件有效期类型' : '个人证件有效期类型'}}</text> |
| | | <view class="select-shell" :class="invalidField == 'subjectValidity' ? 'field-invalid' : ''" @tap="chooseSubjectValidity"><text>{{subjectValidity}}</text><image class="down-icon" src="/static/icons/lucide/chevron-down.svg" mode="aspectFit"></image></view> |
| | | <text v-if="invalidField == 'subjectValidity'" class="field-error">{{errorMessage}}</text> |
| | | </view> |
| | | |
| | | <view class="field-block"> |
| | | <text class="field-label">{{merchantType == 'enterprise' ? '法人证件有效期开始日期' : '个人证件有效期开始日期'}}</text> |
| | | <picker class="date-picker" mode="date" :value="subjectStartDate" start="1990-01-01" end="2099-12-31" @change="onSubjectStartChange"><view class="select-shell" :class="invalidField == 'subjectStartDate' ? 'field-invalid' : ''"><text :class="subjectStartDate.length == 0 ? 'select-placeholder' : ''">{{subjectStartDate.length > 0 ? subjectStartDate : '请选择'}}</text><image class="down-icon" src="/static/icons/lucide/chevron-down.svg" mode="aspectFit"></image></view></picker> |
| | | <text v-if="invalidField == 'subjectStartDate'" class="field-error">{{errorMessage}}</text> |
| | | </view> |
| | | |
| | | <view v-if="subjectValidity == '固定期限'" class="field-block"> |
| | | <text class="field-label">{{merchantType == 'enterprise' ? '法人证件有效期截止日期' : '个人证件有效期截止日期'}}</text> |
| | | <picker class="date-picker" mode="date" :value="subjectEndDate" start="1990-01-01" end="2099-12-31" @change="onSubjectEndChange"><view class="select-shell" :class="invalidField == 'subjectEndDate' ? 'field-invalid' : ''"><text :class="subjectEndDate.length == 0 ? 'select-placeholder' : ''">{{subjectEndDate.length > 0 ? subjectEndDate : '请选择'}}</text><image class="down-icon" src="/static/icons/lucide/chevron-down.svg" mode="aspectFit"></image></view></picker> |
| | | <text v-if="invalidField == 'subjectEndDate'" class="field-error">{{errorMessage}}</text> |
| | | </view> |
| | | |
| | | <view v-if="merchantType == 'individual'" class="field-block last-field"> |
| | | <text class="field-label">手机号</text> |
| | | <view class="input-shell" :class="invalidField == 'personalPhone' ? 'field-invalid' : ''"><input class="text-input" type="number" v-model="personalPhone" maxlength="11" placeholder="请输入手机号" placeholder-class="input-placeholder" @input="clearFieldError('personalPhone')" /></view> |
| | | <text v-if="invalidField == 'personalPhone'" class="field-error">{{errorMessage}}</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-if="merchantType == 'enterprise'" class="form-card"> |
| | | <text class="section-title">管理员信息</text> |
| | | <view class="field-block"> |
| | | <text class="field-label">管理员姓名</text> |
| | | <view class="input-shell" :class="invalidField == 'administratorName' ? 'field-invalid' : ''"><input class="text-input" v-model="administratorName" maxlength="30" placeholder="请输入管理员姓名" placeholder-class="input-placeholder" @input="clearFieldError('administratorName')" /></view> |
| | | <text v-if="invalidField == 'administratorName'" class="field-error">{{errorMessage}}</text> |
| | | </view> |
| | | <view class="field-block last-field"> |
| | | <text class="field-label">管理员手机号</text> |
| | | <view class="input-shell" :class="invalidField == 'administratorPhone' ? 'field-invalid' : ''"><input class="text-input" type="number" v-model="administratorPhone" maxlength="11" placeholder="请输入管理员手机号" placeholder-class="input-placeholder" @input="clearFieldError('administratorPhone')" /></view> |
| | | <text v-if="invalidField == 'administratorPhone'" class="field-error">{{errorMessage}}</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="submit-button" @tap="submitOnboarding"><text>提交审核</text></view> |
| | | <view class="safe-space"></view> |
| | | </scroll-view> |
| | | <view class="field-block"><text class="field-label required">{{merchantType == 'enterprise' ? '法人身份证件类型' : '负责人证件类型'}}</text><view class="select-shell readonly-shell"><text>身份证</text><image class="down-icon" src="/static/icons/lucide/chevron-down.svg" mode="aspectFit"></image></view></view> |
| | | <view class="field-block"><text class="field-label required">{{merchantType == 'enterprise' ? '法人身份证件人像面' : '负责人身份证件人像面'}}</text><view class="upload-box" :class="invalidField == 'legalIdFrontUrl' ? 'field-invalid' : ''" @tap="chooseDocument('legalIdFront')"><image v-if="legalIdFrontUrl.length > 0" class="document-image" :src="legalIdFrontUrl" mode="aspectFill"></image><text v-else>{{uploadingField == 'legalIdFront' ? '上传中...' : '添加图片'}}</text></view><text v-if="invalidField == 'legalIdFrontUrl'" class="field-error">{{errorMessage}}</text></view> |
| | | <view class="field-block"><text class="field-label required">{{merchantType == 'enterprise' ? '法人身份证件国徽面' : '负责人身份证件国徽面'}}</text><view class="upload-box" :class="invalidField == 'legalIdBackUrl' ? 'field-invalid' : ''" @tap="chooseDocument('legalIdBack')"><image v-if="legalIdBackUrl.length > 0" class="document-image" :src="legalIdBackUrl" mode="aspectFill"></image><text v-else>{{uploadingField == 'legalIdBack' ? '上传中...' : '添加图片'}}</text></view><text v-if="invalidField == 'legalIdBackUrl'" class="field-error">{{errorMessage}}</text></view> |
| | | <view class="field-block"><text class="field-label required">{{merchantType == 'enterprise' ? '法人证件号码' : '负责人证件号码'}}</text><view class="input-shell" :class="invalidField == 'subjectIdNumber' ? 'field-invalid' : ''"><input class="text-input" :value="subjectIdNumber" maxlength="18" placeholder="请输入" placeholder-class="input-placeholder" @input="normalizeIdNumber" /></view><text v-if="invalidField == 'subjectIdNumber'" class="field-error">{{errorMessage}}</text></view> |
| | | <view class="field-block"><text class="field-label required">{{merchantType == 'enterprise' ? '法人证件有效期类型' : '负责人证件有效期类型'}}</text><view class="dropdown-wrap"><view class="select-shell" @tap="toggleDropdown('subjectValidity')"><text>{{subjectValidity}}</text><image class="down-icon" src="/static/icons/lucide/chevron-down.svg" mode="aspectFit"></image></view></view></view> |
| | | <view class="field-block"><text class="field-label required">{{merchantType == 'enterprise' ? '法人证件有效期开始日期' : '负责人证件有效期开始日期'}}</text><picker class="date-picker" mode="date" :value="subjectStartDate" start="1900-01-01" end="2099-12-31" @change="onSubjectStartChange"><view class="select-shell" :class="invalidField == 'subjectStartDate' ? 'field-invalid' : ''"><text :class="subjectStartDate.length == 0 ? 'select-placeholder' : ''">{{subjectStartDate.length > 0 ? subjectStartDate : '请选择'}}</text><image class="down-icon" src="/static/icons/lucide/chevron-down.svg" mode="aspectFit"></image></view></picker><text v-if="invalidField == 'subjectStartDate'" class="field-error">{{errorMessage}}</text></view> |
| | | <view v-if="subjectValidity != '长期有效'" class="field-block"><text class="field-label required">{{merchantType == 'enterprise' ? '法人证件有效期截止日期' : '负责人证件有效期截止日期'}}</text><picker class="date-picker" mode="date" :value="subjectEndDate" start="1900-01-01" end="2099-12-31" @change="onSubjectEndChange"><view class="select-shell" :class="invalidField == 'subjectEndDate' ? 'field-invalid' : ''"><text :class="subjectEndDate.length == 0 ? 'select-placeholder' : ''">{{subjectEndDate.length > 0 ? subjectEndDate : '请选择'}}</text><image class="down-icon" src="/static/icons/lucide/chevron-down.svg" mode="aspectFit"></image></view></picker><text v-if="invalidField == 'subjectEndDate'" class="field-error">{{errorMessage}}</text></view> |
| | | <view class="field-block"><text class="field-label">{{merchantType == 'enterprise' ? '法人证件地址' : '负责人证件地址'}}</text><view class="input-shell"><input class="text-input" v-model="legalCertificateAddress" maxlength="255" placeholder="请输入" placeholder-class="input-placeholder" /></view></view> |
| | | <view v-if="merchantType == 'enterprise'" class="field-block"><text class="field-label required">管理员姓名</text><view class="input-shell" :class="invalidField == 'contactName' ? 'field-invalid' : ''"><input class="text-input" v-model="contactName" maxlength="64" placeholder="请输入" placeholder-class="input-placeholder" @input="clearFieldError('contactName')" /></view><text v-if="invalidField == 'contactName'" class="field-error">{{errorMessage}}</text></view> |
| | | <view class="field-block"><text class="field-label required">{{merchantType == 'enterprise' ? '管理员手机号' : '手机号'}}</text><view class="input-shell" :class="invalidField == 'contactMobile' ? 'field-invalid' : ''"><input class="text-input" type="number" :value="contactMobile" maxlength="11" placeholder="请输入" placeholder-class="input-placeholder" @input="normalizePhone" /></view><text v-if="invalidField == 'contactMobile'" class="field-error">{{errorMessage}}</text></view> |
| | | <view class="field-block"><text class="field-label required">{{merchantType == 'enterprise' ? '管理员邮箱' : '邮箱'}}</text><view class="input-shell" :class="invalidField == 'administratorEmail' ? 'field-invalid' : ''"><input class="text-input" v-model="administratorEmail" maxlength="128" placeholder="请输入" placeholder-class="input-placeholder" @input="clearFieldError('administratorEmail')" /></view><text v-if="invalidField == 'administratorEmail'" class="field-error">{{errorMessage}}</text></view> |
| | | <view class="settlement-title">结算银行卡信息</view> |
| | | <view class="field-block"><text class="field-label required">银行卡号</text><view class="input-shell" :class="invalidField == 'bankCardNumber' ? 'field-invalid' : ''"><input class="text-input" type="number" maxlength="32" :value="bankCardNumber" placeholder="请输入银行卡号" placeholder-class="input-placeholder" @input="normalizeBankNumber" /></view><text v-if="invalidField == 'bankCardNumber'" class="field-error">{{errorMessage}}</text></view> |
| | | <view class="field-block"><text class="field-label required">开户省份</text><picker class="region-picker" mode="selector" :range="areaProvinceNames" :value="bankProvinceIndex" @change="onBankProvinceChange"><view class="select-shell" :class="invalidField == 'bankProvince' ? 'field-invalid' : ''"><text :class="bankProvince.length == 0 ? 'select-placeholder' : ''">{{bankProvince.length > 0 ? bankProvince : '请选择'}}</text><image class="down-icon" src="/static/icons/lucide/chevron-down.svg" mode="aspectFit"></image></view></picker></view> |
| | | <view class="field-block"><text class="field-label required">开户城市</text><picker class="region-picker" mode="selector" :range="bankCityNames" :value="bankCityIndex" :disabled="bankProvince.length == 0" @change="onBankCityChange"><view class="select-shell" :class="invalidField == 'bankCity' ? 'field-invalid' : ''"><text :class="bankCity.length == 0 ? 'select-placeholder' : ''">{{bankCity.length > 0 ? bankCity : '请选择'}}</text><image class="down-icon" src="/static/icons/lucide/chevron-down.svg" mode="aspectFit"></image></view></picker></view> |
| | | <view class="field-block"><text class="field-label required">开户行</text><view class="select-shell" :class="invalidField == 'bankBranch' ? 'field-invalid' : ''" @tap="chooseBankBranch"><text class="ellipsis-text" :class="bankBranch.length == 0 ? 'select-placeholder' : ''">{{bankBranch.length > 0 ? bankBranch : '请选择'}}</text><image class="down-icon" src="/static/icons/lucide/chevron-down.svg" mode="aspectFit"></image></view><text v-if="invalidField == 'bankBranch'" class="field-error">{{errorMessage}}</text></view> |
| | | <view class="field-block"><text class="field-label required">{{merchantType == 'enterprise' && companyType != '个体工商户' ? '开户许可证' : '银行卡卡号面'}}</text><view class="upload-box" :class="invalidField == 'settlementProofUrl' ? 'field-invalid' : ''" @tap="chooseDocument('settlementProof')"><image v-if="settlementProofUrl.length > 0" class="document-image" :src="settlementProofUrl" mode="aspectFill"></image><text v-else>{{uploadingField == 'settlementProof' ? '上传中...' : '添加图片'}}</text></view><text v-if="invalidField == 'settlementProofUrl'" class="field-error">{{errorMessage}}</text></view> |
| | | <view class="submit-button" :class="submitting || uploadingField.length > 0 ? 'button-disabled' : ''" @tap="submitForm"><text>{{submitting ? '提交中...' : '提交'}}</text></view><view class="safe-space"></view> |
| | | </view></scroll-view> |
| | | <view v-if="activeDropdown.length > 0" class="dropdown-mask" @tap="closeDropdown"></view> |
| | | <view v-if="activeDropdown == 'merchantType'" class="dropdown-menu type-menu"><view class="dropdown-option" :class="merchantType == 'enterprise' ? 'dropdown-option-active' : ''" @tap="selectMerchantType('enterprise')"><text>企业</text></view><view class="dropdown-option" :class="merchantType == 'individual' ? 'dropdown-option-active' : ''" @tap="selectMerchantType('individual')"><text>个人</text></view></view> |
| | | <view v-if="activeDropdown == 'businessValidity'" class="dropdown-menu validity-menu business-validity-menu"><view class="dropdown-option" :class="businessValidity == '固定期限' ? 'dropdown-option-active' : ''" @tap="selectBusinessValidity('固定期限')"><text>固定期限</text></view><view class="dropdown-option" :class="businessValidity == '长期' ? 'dropdown-option-active' : ''" @tap="selectBusinessValidity('长期')"><text>长期</text></view></view> |
| | | <view v-if="activeDropdown == 'subjectValidity'" class="dropdown-menu validity-menu subject-validity-menu"><view class="dropdown-option" :class="subjectValidity == '10年' ? 'dropdown-option-active' : ''" @tap="selectSubjectValidity('10年')"><text>10年</text></view><view class="dropdown-option" :class="subjectValidity == '20年' ? 'dropdown-option-active' : ''" @tap="selectSubjectValidity('20年')"><text>20年</text></view><view class="dropdown-option" :class="subjectValidity == '长期有效' ? 'dropdown-option-active' : ''" @tap="selectSubjectValidity('长期有效')"><text>长期有效</text></view></view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup lang="uts"> |
| | | import { submitOnboarding as submitOnboardingRemote } from '../../services/settlement.uts' |
| | | const submitting = ref(false) |
| | | const merchantType = ref('enterprise') |
| | | const creditCode = ref('') |
| | | const businessValidity = ref('固定期限') |
| | | const businessStartDate = ref('') |
| | | const businessEndDate = ref('') |
| | | const registeredRegion = ref('') |
| | | const registeredProvinceCode = ref('') |
| | | const registeredCityCode = ref('') |
| | | const registeredDistrictCode = ref('') |
| | | const registeredAddress = ref('') |
| | | const subjectName = ref('') |
| | | const subjectIdNumber = ref('') |
| | | const subjectValidity = ref('固定期限') |
| | | const subjectStartDate = ref('') |
| | | const subjectEndDate = ref('') |
| | | const administratorName = ref('') |
| | | const administratorPhone = ref('') |
| | | const personalPhone = ref('') |
| | | const invalidField = ref('') |
| | | const errorMessage = ref('') |
| | | const merchantTypeOptions = ['企业', '个人'] |
| | | const validityOptions = ['固定期限', '长期'] |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack() |
| | | import { loadOnboardingStatus, SettlementOnboarding, submitOnboarding as submitOnboardingRemote } from '../../services/settlement.uts' |
| | | import { uploadMerchantProductImage } from '../../services/merchant-management.uts' |
| | | import { areaProvinceNames, areaProvinceCodes, areaCityNames, areaCityCodes } from '../../config/area-codes.uts' |
| | | const submitting = ref(false); const uploadingField = ref(''); const activeDropdown = ref(''); const merchantType = ref('enterprise'); const merchantName = ref(''); const merchantShortName = ref(''); const companyType = ref('政府机构'); const companyTypeIndex = ref(0); const companyTypeOptions = ['政府机构', '国营企业', '私营企业', '外资企业', '个体工商户', '其它组织', '事业单位', '业主委员会']; const industryType = ref(''); const industryTypeIndex = ref(0); const industryTypeOptions = ['餐饮服务', '酒店服务', '景区服务', '加油服务', '停车服务', '其它']; const businessLicenseUrl = ref(''); const creditCode = ref(''); const businessValidity = ref('固定期限'); const businessStartDate = ref(''); const businessEndDate = ref(''); const establishedDate = ref(''); const registeredRegion = ref(''); const registeredProvinceCode = ref(''); const registeredCityCode = ref(''); const registeredDistrictCode = ref(''); const registeredAddress = ref(''); const registeredCapital = ref(''); const subjectName = ref(''); const legalIdFrontUrl = ref(''); const legalIdBackUrl = ref(''); const subjectIdNumber = ref(''); const subjectValidity = ref('10年'); const subjectStartDate = ref(''); const subjectEndDate = ref(''); const legalCertificateAddress = ref(''); const contactName = ref(''); const contactMobile = ref(''); const administratorEmail = ref(''); const bankCardNumber = ref(''); const bankProvince = ref(''); const bankProvinceCode = ref(''); const bankProvinceIndex = ref(0); const bankCity = ref(''); const bankCityCode = ref(''); const bankCityNames = ref([] as string[]); const bankCityCodes = ref([] as string[]); const bankCityIndex = ref(0); const bankBranch = ref(''); const bankBranchCode = ref(''); const bankName = ref(''); const settlementProofUrl = ref(''); const invalidField = ref(''); const errorMessage = ref('') |
| | | const draftStorageKey = 'merchant-onboarding-draft' |
| | | let draftRestored = false |
| | | const draftPayload = (): any => ({ merchantType: merchantType.value, merchantName: merchantName.value, merchantShortName: merchantShortName.value, companyType: companyType.value, companyTypeIndex: companyTypeIndex.value, industryType: industryType.value, industryTypeIndex: industryTypeIndex.value, businessLicenseUrl: businessLicenseUrl.value, creditCode: creditCode.value, businessValidity: businessValidity.value, businessStartDate: businessStartDate.value, businessEndDate: businessEndDate.value, establishedDate: establishedDate.value, registeredRegion: registeredRegion.value, registeredProvinceCode: registeredProvinceCode.value, registeredCityCode: registeredCityCode.value, registeredDistrictCode: registeredDistrictCode.value, registeredAddress: registeredAddress.value, registeredCapital: registeredCapital.value, subjectName: subjectName.value, legalIdFrontUrl: legalIdFrontUrl.value, legalIdBackUrl: legalIdBackUrl.value, subjectIdNumber: subjectIdNumber.value, subjectValidity: subjectValidity.value, subjectStartDate: subjectStartDate.value, subjectEndDate: subjectEndDate.value, legalCertificateAddress: legalCertificateAddress.value, contactName: contactName.value, contactMobile: contactMobile.value, administratorEmail: administratorEmail.value, bankCardNumber: bankCardNumber.value, bankProvince: bankProvince.value, bankProvinceCode: bankProvinceCode.value, bankProvinceIndex: bankProvinceIndex.value, bankCity: bankCity.value, bankCityCode: bankCityCode.value, bankCityIndex: bankCityIndex.value, bankBranch: bankBranch.value, bankBranchCode: bankBranchCode.value, bankName: bankName.value, settlementProofUrl: settlementProofUrl.value }) |
| | | const saveDraft = () => { uni.setStorageSync(draftStorageKey, JSON.stringify(draftPayload())) } |
| | | const clearDraft = () => { uni.removeStorageSync(draftStorageKey) } |
| | | const restoreDraft = () => { |
| | | const stored = uni.getStorageSync(draftStorageKey) |
| | | if (stored == null || stored == '') return |
| | | try { |
| | | const draft = JSON.parse(stored as string) as any |
| | | if (draft.merchantType != null) merchantType.value = draft.merchantType as string |
| | | if (draft.merchantName != null) merchantName.value = draft.merchantName as string |
| | | if (draft.merchantShortName != null) merchantShortName.value = draft.merchantShortName as string |
| | | if (draft.companyType != null) companyType.value = draft.companyType as string |
| | | if (draft.companyTypeIndex != null) companyTypeIndex.value = Number(draft.companyTypeIndex) |
| | | if (draft.industryType != null) industryType.value = draft.industryType as string |
| | | if (draft.industryTypeIndex != null) industryTypeIndex.value = Number(draft.industryTypeIndex) |
| | | if (draft.businessLicenseUrl != null) businessLicenseUrl.value = draft.businessLicenseUrl as string |
| | | if (draft.creditCode != null) creditCode.value = draft.creditCode as string |
| | | if (draft.businessValidity != null) businessValidity.value = draft.businessValidity as string |
| | | if (draft.businessStartDate != null) businessStartDate.value = draft.businessStartDate as string |
| | | if (draft.businessEndDate != null) businessEndDate.value = draft.businessEndDate as string |
| | | if (draft.establishedDate != null) establishedDate.value = draft.establishedDate as string |
| | | if (draft.registeredRegion != null) registeredRegion.value = draft.registeredRegion as string |
| | | if (draft.registeredProvinceCode != null) registeredProvinceCode.value = draft.registeredProvinceCode as string |
| | | if (draft.registeredCityCode != null) registeredCityCode.value = draft.registeredCityCode as string |
| | | if (draft.registeredDistrictCode != null) registeredDistrictCode.value = draft.registeredDistrictCode as string |
| | | if (draft.registeredAddress != null) registeredAddress.value = draft.registeredAddress as string |
| | | if (draft.registeredCapital != null) registeredCapital.value = draft.registeredCapital as string |
| | | if (draft.subjectName != null) subjectName.value = draft.subjectName as string |
| | | if (draft.legalIdFrontUrl != null) legalIdFrontUrl.value = draft.legalIdFrontUrl as string |
| | | if (draft.legalIdBackUrl != null) legalIdBackUrl.value = draft.legalIdBackUrl as string |
| | | if (draft.subjectIdNumber != null) subjectIdNumber.value = draft.subjectIdNumber as string |
| | | if (draft.subjectValidity != null) subjectValidity.value = draft.subjectValidity as string |
| | | if (draft.subjectStartDate != null) subjectStartDate.value = draft.subjectStartDate as string |
| | | if (draft.subjectEndDate != null) subjectEndDate.value = draft.subjectEndDate as string |
| | | if (draft.legalCertificateAddress != null) legalCertificateAddress.value = draft.legalCertificateAddress as string |
| | | if (draft.contactName != null) contactName.value = draft.contactName as string |
| | | if (draft.contactMobile != null) contactMobile.value = draft.contactMobile as string |
| | | if (draft.administratorEmail != null) administratorEmail.value = draft.administratorEmail as string |
| | | if (draft.bankCardNumber != null) bankCardNumber.value = draft.bankCardNumber as string |
| | | if (draft.bankProvince != null) bankProvince.value = draft.bankProvince as string |
| | | if (draft.bankProvinceCode != null) bankProvinceCode.value = draft.bankProvinceCode as string |
| | | if (draft.bankProvinceIndex != null) bankProvinceIndex.value = Number(draft.bankProvinceIndex) |
| | | if (draft.bankCity != null) bankCity.value = draft.bankCity as string |
| | | if (draft.bankCityCode != null) bankCityCode.value = draft.bankCityCode as string |
| | | if (draft.bankCityIndex != null) bankCityIndex.value = Number(draft.bankCityIndex) |
| | | if (draft.bankBranch != null) bankBranch.value = draft.bankBranch as string |
| | | if (draft.bankBranchCode != null) bankBranchCode.value = draft.bankBranchCode as string |
| | | if (draft.bankName != null) bankName.value = draft.bankName as string |
| | | if (draft.settlementProofUrl != null) settlementProofUrl.value = draft.settlementProofUrl as string |
| | | if (bankProvince.value.length > 0 && bankProvinceIndex.value >= 0 && bankProvinceIndex.value < areaCityNames.length) { bankCityNames.value = areaCityNames[bankProvinceIndex.value]; bankCityCodes.value = areaCityCodes[bankProvinceIndex.value] } |
| | | } catch (_error) { clearDraft() } |
| | | } |
| | | const goBack = () => uni.navigateBack() |
| | | const setError = (field: string, message: string) => { invalidField.value = field; errorMessage.value = message } |
| | | const clearFieldError = (field: string) => { if (invalidField.value == field) { invalidField.value = ''; errorMessage.value = '' } } |
| | | const normalizeCreditCode = (e: any) => { creditCode.value = (e.detail.value as string).toUpperCase().replace(/[^0-9A-Z]/g, ''); clearFieldError('creditCode') } |
| | | const normalizeIdNumber = (e: any) => { subjectIdNumber.value = (e.detail.value as string).toUpperCase().replace(/[^0-9X]/g, ''); clearFieldError('subjectIdNumber') } |
| | | const normalizePhone = (e: any) => { contactMobile.value = (e.detail.value as string).replace(/[^0-9]/g, ''); clearFieldError('contactMobile') } |
| | | const normalizeBankNumber = (e: any) => { bankCardNumber.value = (e.detail.value as string).replace(/[^0-9]/g, ''); clearFieldError('bankCardNumber') } |
| | | const toggleDropdown = (name: string) => { activeDropdown.value = activeDropdown.value == name ? '' : name } |
| | | const closeDropdown = () => { activeDropdown.value = '' } |
| | | const toggleMerchantTypeDropdown = () => { toggleDropdown('merchantType') } |
| | | const selectMerchantType = (value: string) => { merchantType.value = value; activeDropdown.value = '' } |
| | | const onCompanyTypeChange = (event: any) => { const index = parseInt(event.detail.value as string); if (index < 0 || index >= companyTypeOptions.length) return; companyTypeIndex.value = index; companyType.value = companyTypeOptions[index]; clearFieldError('companyType') } |
| | | const onIndustryTypeChange = (event: any) => { const index = parseInt(event.detail.value as string); if (index < 0 || index >= industryTypeOptions.length) return; industryTypeIndex.value = index; industryType.value = industryTypeOptions[index] } |
| | | const selectBusinessValidity = (value: string) => { businessValidity.value = value; activeDropdown.value = ''; if (value == '长期') businessEndDate.value = '' } |
| | | const selectSubjectValidity = (value: string) => { subjectValidity.value = value; activeDropdown.value = ''; if (value == '长期有效') subjectEndDate.value = '' } |
| | | const onBusinessStartChange = (e: any) => { businessStartDate.value = e.detail.value as string; clearFieldError('businessStartDate') }; const onBusinessEndChange = (e: any) => { businessEndDate.value = e.detail.value as string; clearFieldError('businessEndDate') }; const onEstablishedDateChange = (e: any) => { establishedDate.value = e.detail.value as string; clearFieldError('establishedDate') }; const onSubjectStartChange = (e: any) => { subjectStartDate.value = e.detail.value as string; clearFieldError('subjectStartDate') }; const onSubjectEndChange = (e: any) => { subjectEndDate.value = e.detail.value as string; clearFieldError('subjectEndDate') } |
| | | const onRegisteredRegionChange = (e: any) => { const values = e.detail.value as Array<string>; const codes = e.detail.code as Array<string>; if (values.length != 3 || codes.length != 3) return; registeredRegion.value = values[0] + ' ' + values[1] + ' ' + values[2]; registeredProvinceCode.value = codes[0]; registeredCityCode.value = codes[1]; registeredDistrictCode.value = codes[2]; clearFieldError('registeredRegion') } |
| | | const chooseDocument = (kind: string) => { if (uploadingField.value.length > 0) return; uni.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'], success: (r: any) => { const files = r.tempFiles as Array<any>; if (files.length == 0) return; if ((files[0].size as number) > 2097152) { uni.showToast({ title: '图片不能超过2MB', icon: 'none' }); return }; uploadingField.value = kind; uploadMerchantProductImage(files[0].path as string, (url: string) => { uploadingField.value = ''; if (kind == 'businessLicense') { businessLicenseUrl.value = url; clearFieldError('businessLicenseUrl') } else if (kind == 'legalIdFront') { legalIdFrontUrl.value = url; clearFieldError('legalIdFrontUrl') } else if (kind == 'legalIdBack') { legalIdBackUrl.value = url; clearFieldError('legalIdBackUrl') } else { settlementProofUrl.value = url; clearFieldError('settlementProofUrl') } }, (error: any) => { uploadingField.value = ''; uni.showToast({ title: error.message != null ? error.message : '图片上传失败,请重试', icon: 'none' }) }) } }) } |
| | | const onBankProvinceChange = (event: any) => { const index = parseInt(event.detail.value as string); if (index < 0 || index >= areaProvinceNames.length) return; bankProvinceIndex.value = index; bankProvince.value = areaProvinceNames[index]; bankProvinceCode.value = areaProvinceCodes[index]; bankCityNames.value = areaCityNames[index]; bankCityCodes.value = areaCityCodes[index]; bankCityIndex.value = 0; bankCity.value = ''; bankCityCode.value = ''; bankBranch.value = ''; bankBranchCode.value = ''; bankName.value = '' } |
| | | const onBankCityChange = (event: any) => { const index = parseInt(event.detail.value as string); if (index < 0 || index >= bankCityNames.value.length) return; bankCityIndex.value = index; bankCity.value = bankCityNames.value[index]; bankCityCode.value = bankCityCodes.value[index]; bankBranch.value = ''; bankBranchCode.value = ''; bankName.value = '' } |
| | | const chooseBankBranch = () => { if (bankCityCode.value.length == 0) { uni.showToast({ title: '请先选择开户城市', icon: 'none' }); return }; uni.navigateTo({ url: '/pages/bank-branch-select/bank-branch-select?city=' + encodeURIComponent(bankProvince.value + ' ' + bankCity.value) + '&cityCode=' + encodeURIComponent(bankCityCode.value) }) } |
| | | const dateRange = (start: string, end: string, field: string, name: string): boolean => { if (start.length == 0) { setError(field, '请选择' + name + '开始日期'); return false }; if (end.length > 0 && start > end) { setError(field, name + '结束日期不能早于开始日期'); return false }; return true } |
| | | const validate = (): boolean => { if (merchantType.value == 'enterprise') { if (merchantName.value.trim().length == 0) { setError('merchantName', '请输入商户名称'); return false }; if (companyTypeOptions.indexOf(companyType.value) < 0) { setError('companyType', '请选择公司类型'); return false }; if (businessLicenseUrl.value.length == 0) { setError('businessLicenseUrl', '请上传营业执照'); return false }; if (!(/^[0-9A-Z]{18}$/).test(creditCode.value)) { setError('creditCode', '请输入正确的18位统一社会信用代码'); return false }; if (!dateRange(businessStartDate.value, businessEndDate.value, 'businessStartDate', '营业执照有效期')) return false; if (businessValidity.value == '固定期限' && businessEndDate.value.length == 0) { setError('businessEndDate', '请选择营业执照有效期结束日期'); return false }; if (establishedDate.value.length == 0) { setError('establishedDate', '请选择成立时间'); return false }; if (registeredRegion.value.length == 0) { setError('registeredRegion', '请选择注册地址'); return false }; if (registeredAddress.value.trim().length == 0) { setError('registeredAddress', '请输入详细注册地址'); return false } } else { if (industryType.value.length == 0) { setError('industryType', '请选择行业类型'); return false }; if (registeredRegion.value.length == 0) { setError('registeredRegion', '请选择经营地区'); return false }; if (legalCertificateAddress.value.trim().length == 0) { setError('legalCertificateAddress', '请输入负责人证件地址'); return false } }; if (subjectName.value.trim().length == 0) { setError('subjectName', '请输入姓名'); return false }; if (legalIdFrontUrl.value.length == 0) { setError('legalIdFrontUrl', '请上传负责人身份证人像面'); return false }; if (legalIdBackUrl.value.length == 0) { setError('legalIdBackUrl', '请上传负责人身份证国徽面'); return false }; if (!(/^\d{17}[0-9X]$/).test(subjectIdNumber.value)) { setError('subjectIdNumber', '请输入正确的18位身份证号码'); return false }; if (!dateRange(subjectStartDate.value, subjectEndDate.value, 'subjectStartDate', '证件有效期')) return false; if (subjectValidity.value != '长期有效' && subjectEndDate.value.length == 0) { setError('subjectEndDate', '请选择证件有效期截止日期'); return false }; if (merchantType.value == 'enterprise' && contactName.value.trim().length == 0) { setError('contactName', '请输入管理员姓名'); return false }; if (!(/^1[3-9]\d{9}$/).test(contactMobile.value)) { setError('contactMobile', '请输入正确的手机号'); return false }; if (!(/^[^\s@]+@[^\s@]+\.[^\s@]+$/).test(administratorEmail.value)) { setError('administratorEmail', '请输入正确的邮箱'); return false }; if (!(/^\d{16,32}$/).test(bankCardNumber.value)) { setError('bankCardNumber', '请输入正确的银行卡号'); return false }; if (bankProvinceCode.value.length == 0) { setError('bankProvince', '请选择开户省份'); return false }; if (bankCityCode.value.length == 0) { setError('bankCity', '请选择开户城市'); return false }; if (bankBranchCode.value.length == 0) { setError('bankBranch', '请选择开户行'); return false }; if (settlementProofUrl.value.length == 0) { setError('settlementProofUrl', merchantType.value == 'enterprise' && companyType.value != '个体工商户' ? '请上传开户许可证' : '请上传银行卡卡号面'); return false }; return true } |
| | | const submitForm = () => { if (submitting.value || uploadingField.value.length > 0) return; saveDraft(); invalidField.value = ''; errorMessage.value = ''; if (!validate()) { uni.showToast({ title: errorMessage.value, icon: 'none' }); return }; submitting.value = true; submitOnboardingRemote({ subjectType: merchantType.value, subjectName: subjectName.value.trim(), creditCode: creditCode.value, subjectCertificateValidityType: subjectValidity.value == '长期有效' ? 'long_term' : 'fixed', subjectCertificateBeginDate: subjectStartDate.value, subjectCertificateEndDate: subjectEndDate.value, subjectIdNumber: subjectIdNumber.value, licenseValidityType: businessValidity.value == '长期' ? 'long_term' : 'fixed', licenseBeginDate: businessStartDate.value, licenseEndDate: businessEndDate.value, registeredRegion: registeredRegion.value, registeredProvinceCode: registeredProvinceCode.value, registeredCityCode: registeredCityCode.value, registeredDistrictCode: registeredDistrictCode.value, registeredAddress: merchantType.value == 'enterprise' ? registeredAddress.value.trim() : legalCertificateAddress.value.trim(), merchantName: merchantName.value.trim(), merchantShortName: merchantShortName.value.trim(), companyType: merchantType.value == 'enterprise' ? companyType.value : industryType.value, businessLicenseUrl: businessLicenseUrl.value, establishedDate: establishedDate.value, registeredCapital: registeredCapital.value.trim(), legalIdFrontUrl: legalIdFrontUrl.value, legalIdBackUrl: legalIdBackUrl.value, legalCertificateAddress: legalCertificateAddress.value.trim(), administratorEmail: administratorEmail.value.trim(), contactName: merchantType.value == 'enterprise' ? contactName.value.trim() : subjectName.value.trim(), contactMobile: contactMobile.value, bankCardNumber: bankCardNumber.value, bankProvinceCode: bankProvinceCode.value, bankCityCode: bankCityCode.value, bankBranchCode: bankBranchCode.value, settlementCardFrontUrl: merchantType.value == 'enterprise' && companyType.value != '个体工商户' ? '' : settlementProofUrl.value, registrationAccountUrl: merchantType.value == 'enterprise' && companyType.value != '个体工商户' ? settlementProofUrl.value : '' }, 'onboarding-' + Date.now(), (result: SettlementOnboarding) => { submitting.value = false; if (result.status == 'approved') clearDraft(); uni.redirectTo({ url: '/pages/merchant-onboarding-pending/merchant-onboarding-pending' }) }, (error: any) => { submitting.value = false; uni.showToast({ title: error.message != null ? error.message : '提交失败,请重试', icon: 'none' }) }) } |
| | | |
| | | const setFieldError = (field: string, message: string) => { |
| | | invalidField.value = field |
| | | errorMessage.value = message |
| | | } |
| | | |
| | | const clearFieldError = (field: string) => { |
| | | if (invalidField.value == field) { |
| | | invalidField.value = '' |
| | | errorMessage.value = '' |
| | | } |
| | | } |
| | | |
| | | const normalizeCreditCode = (event: any) => { |
| | | const input = event != null && event.detail != null && event.detail.value != null ? '' + event.detail.value : '' |
| | | creditCode.value = input.toUpperCase().replace(/[^0-9A-Z]/g, '') |
| | | clearFieldError('creditCode') |
| | | } |
| | | |
| | | const chooseMerchantType = () => { |
| | | uni.showActionSheet({ |
| | | itemList: merchantTypeOptions, |
| | | success: (result: any) => { |
| | | merchantType.value = result.tapIndex == 0 ? 'enterprise' : 'individual' |
| | | invalidField.value = '' |
| | | errorMessage.value = '' |
| | | onShow(() => { |
| | | if (!draftRestored) { draftRestored = true; restoreDraft() } |
| | | loadOnboardingStatus((item: SettlementOnboarding) => { |
| | | if (item.status == 'pending' || item.status == 'approved') { |
| | | uni.redirectTo({ url: '/pages/merchant-onboarding-pending/merchant-onboarding-pending' }) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | const chooseBusinessValidity = () => { |
| | | uni.showActionSheet({ |
| | | itemList: validityOptions, |
| | | success: (result: any) => { |
| | | businessValidity.value = validityOptions[result.tapIndex] |
| | | if (businessValidity.value == '长期') businessEndDate.value = '' |
| | | clearFieldError('businessValidity') |
| | | } |
| | | }) |
| | | } |
| | | |
| | | const chooseSubjectValidity = () => { |
| | | uni.showActionSheet({ |
| | | itemList: validityOptions, |
| | | success: (result: any) => { |
| | | subjectValidity.value = validityOptions[result.tapIndex] |
| | | if (subjectValidity.value == '长期') subjectEndDate.value = '' |
| | | clearFieldError('subjectValidity') |
| | | } |
| | | }) |
| | | } |
| | | |
| | | |
| | | const onRegisteredRegionChange = (event: any) => { |
| | | const values = event.detail.value as Array<string> |
| | | const codes = event.detail.code as Array<string> |
| | | if (values.length != 3 || codes.length != 3) return |
| | | registeredRegion.value = values[0] + ' ' + values[1] + ' ' + values[2] |
| | | registeredProvinceCode.value = codes[0] |
| | | registeredCityCode.value = codes[1] |
| | | registeredDistrictCode.value = codes[2] |
| | | clearFieldError('registeredRegion') |
| | | } |
| | | |
| | | const onBusinessStartChange = (event: any) => { |
| | | businessStartDate.value = event.detail.value as string |
| | | clearFieldError('businessStartDate') |
| | | } |
| | | |
| | | const onBusinessEndChange = (event: any) => { |
| | | businessEndDate.value = event.detail.value as string |
| | | clearFieldError('businessEndDate') |
| | | } |
| | | |
| | | const onSubjectStartChange = (event: any) => { |
| | | subjectStartDate.value = event.detail.value as string |
| | | clearFieldError('subjectStartDate') |
| | | } |
| | | |
| | | const onSubjectEndChange = (event: any) => { |
| | | subjectEndDate.value = event.detail.value as string |
| | | clearFieldError('subjectEndDate') |
| | | } |
| | | |
| | | const isPhoneValid = (phone: string) => /^1[3-9]\d{9}$/.test(phone) |
| | | |
| | | const validateCommonIdentity = () => { |
| | | if (subjectName.value.trim().length == 0) { |
| | | setFieldError('subjectName', merchantType.value == 'enterprise' ? '请输入法人姓名' : '请输入个人姓名') |
| | | return false |
| | | } |
| | | if (!(/^\d{17}[\dXx]$/).test(subjectIdNumber.value.trim())) { |
| | | setFieldError('subjectIdNumber', '请输入正确的18位身份证号码') |
| | | return false |
| | | } |
| | | if (subjectStartDate.value.length == 0) { |
| | | setFieldError('subjectStartDate', '请选择证件有效期开始日期') |
| | | return false |
| | | } |
| | | if (subjectValidity.value == '固定期限') { |
| | | if (subjectEndDate.value.length == 0) { |
| | | setFieldError('subjectEndDate', '请选择证件有效期截止日期') |
| | | return false |
| | | } |
| | | if (subjectStartDate.value > subjectEndDate.value) { |
| | | setFieldError('subjectEndDate', '证件截止日期不能早于开始日期') |
| | | return false |
| | | } |
| | | } |
| | | return true |
| | | } |
| | | |
| | | const validateEnterprise = () => { |
| | | if (!(/^[0-9A-Z]{18}$/).test(creditCode.value.trim())) { |
| | | setFieldError('creditCode', '请输入正确的18位统一社会信用代码') |
| | | return false |
| | | } |
| | | if (businessStartDate.value.length == 0) { |
| | | setFieldError('businessStartDate', '请选择营业执照有效期开始日期') |
| | | return false |
| | | } |
| | | if (businessValidity.value == '固定期限') { |
| | | if (businessEndDate.value.length == 0) { |
| | | setFieldError('businessEndDate', '请选择营业执照有效期结束日期') |
| | | return false |
| | | } |
| | | if (businessStartDate.value > businessEndDate.value) { |
| | | setFieldError('businessEndDate', '证照结束日期不能早于开始日期') |
| | | return false |
| | | } |
| | | } |
| | | if (registeredRegion.value.length == 0) { |
| | | setFieldError('registeredRegion', '请选择注册地址') |
| | | return false |
| | | } |
| | | if (registeredAddress.value.trim().length < 5) { |
| | | setFieldError('registeredAddress', '请输入完整的详细注册地址') |
| | | return false |
| | | } |
| | | if (!validateCommonIdentity()) return false |
| | | if (administratorName.value.trim().length == 0) { |
| | | setFieldError('administratorName', '请输入管理员姓名') |
| | | return false |
| | | } |
| | | if (!isPhoneValid(administratorPhone.value.trim())) { |
| | | setFieldError('administratorPhone', '请输入正确的管理员手机号') |
| | | return false |
| | | } |
| | | return true |
| | | } |
| | | |
| | | const validateIndividual = () => { |
| | | if (!validateCommonIdentity()) return false |
| | | if (!isPhoneValid(personalPhone.value.trim())) { |
| | | setFieldError('personalPhone', '请输入正确的手机号') |
| | | return false |
| | | } |
| | | return true |
| | | } |
| | | |
| | | const submitOnboarding = () => { |
| | | if (submitting.value) return |
| | | invalidField.value = '' |
| | | errorMessage.value = '' |
| | | const valid = merchantType.value == 'enterprise' ? validateEnterprise() : validateIndividual() |
| | | if (!valid) { |
| | | uni.showToast({ title: errorMessage.value, icon: 'none' }) |
| | | return |
| | | } |
| | | uni.showModal({ |
| | | title: '确认提交审核', |
| | | content: '提交后资料将进入审核,审核期间暂不能发起提现。', |
| | | confirmText: '确认提交', |
| | | confirmColor: '#FF6B2C', |
| | | success: (result: any) => { |
| | | if (!result.confirm) return |
| | | submitting.value = true |
| | | const contactName = merchantType.value == 'enterprise' ? administratorName.value.trim() : subjectName.value.trim() |
| | | const contactMobile = merchantType.value == 'enterprise' ? administratorPhone.value.trim() : personalPhone.value.trim() |
| | | submitOnboardingRemote({ subjectType: merchantType.value, subjectName: subjectName.value.trim(), creditCode: creditCode.value.trim(), subjectIdNumber: subjectIdNumber.value.trim(), subjectCertificateValidityType: subjectValidity.value == '长期' ? 'long_term' : 'fixed', subjectCertificateBeginDate: subjectStartDate.value, subjectCertificateEndDate: subjectEndDate.value, licenseValidityType: businessValidity.value == '长期' ? 'long_term' : 'fixed', licenseBeginDate: businessStartDate.value, licenseEndDate: businessEndDate.value, registeredRegion: registeredRegion.value.trim(), registeredProvinceCode: registeredProvinceCode.value, registeredCityCode: registeredCityCode.value, registeredDistrictCode: registeredDistrictCode.value, registeredAddress: registeredAddress.value.trim(), contactName: contactName, contactMobile: contactMobile }, 'onboarding-' + Date.now(), () => { |
| | | submitting.value = false |
| | | uni.redirectTo({ url: '/pages/merchant-onboarding-pending/merchant-onboarding-pending' }) |
| | | }, (error: any) => { |
| | | submitting.value = false |
| | | uni.showToast({ title: error.message != null ? error.message : '提交失败,请重试', icon: 'none' }) |
| | | }) |
| | | } |
| | | }) |
| | | } |
| | | }, (_error: any) => {}) |
| | | const stored = uni.getStorageSync('merchant-withdrawal-selected-branch') |
| | | if (stored == null || stored == '') return |
| | | try { |
| | | const branch = JSON.parse(stored as string) |
| | | bankBranch.value = branch.branchName as string |
| | | bankName.value = branch.bankName as string |
| | | bankBranchCode.value = branch.branchCode as string |
| | | } catch (_error) {} |
| | | uni.removeStorageSync('merchant-withdrawal-selected-branch') |
| | | }) |
| | | </script> |
| | | |
| | | <style> |
| | | page { background-color: #F4F6F9; } |
| | | .page { display: flex; flex: 1; width: 100%; height: 100vh; flex-direction: column; overflow: hidden; background-color: #F4F6F9; } |
| | | .navigation-bar { display: flex; height: 174rpx; padding: 68rpx 38rpx 0; flex: none; flex-direction: row; align-items: center; position: relative; background-color: #F4F6F9; box-sizing: border-box; } |
| | | .back-button { display: flex; width: 64rpx; height: 64rpx; justify-content: center; align-items: center; position: absolute; left: 38rpx; top: 89rpx; z-index: 2; } |
| | | .back-icon { width: 38rpx; height: 38rpx; } |
| | | .page-title { width: 100%; color: #202B3E; font-size: 34rpx; line-height: 48rpx; text-align: center; font-weight: 700; } |
| | | .form-scroll { flex: 1; height: 0; padding: 0 34rpx; box-sizing: border-box; } |
| | | .form-card { margin-top: 26rpx; padding: 30rpx; border-radius: 30rpx; background-color: #FFFFFF; box-shadow: 0 14rpx 34rpx rgba(49, 55, 66, 0.05); box-sizing: border-box; } |
| | | .subject-card { margin-top: 0; padding-top: 20rpx; padding-bottom: 20rpx; } |
| | | .merchant-type-row { display: flex; min-height: 92rpx; flex-direction: row; align-items: center; } |
| | | .type-copy { display: flex; flex: 1; flex-direction: column; } |
| | | .inline-label { margin-bottom: 0; } |
| | | .type-control { display: flex; width: 190rpx; height: 72rpx; padding: 0 18rpx 0 22rpx; flex-direction: row; align-items: center; justify-content: space-between; border-radius: 18rpx; color: #303B4C; font-size: 27rpx; line-height: 38rpx; background-color: #F2F3F5; box-sizing: border-box; } |
| | | .section-title { display: block; margin-bottom: 28rpx; color: #263244; font-size: 31rpx; line-height: 44rpx; font-weight: 700; } |
| | | .field-block { display: flex; margin-bottom: 26rpx; flex-direction: column; } |
| | | .last-field { margin-bottom: 0; } |
| | | .field-label { display: block; margin-bottom: 13rpx; color: #374253; font-size: 27rpx; line-height: 38rpx; font-weight: 600; } |
| | | .input-shell, .select-shell { display: flex; width: 100%; height: 82rpx; padding: 0 22rpx; flex-direction: row; align-items: center; border-width: 1rpx; border-style: solid; border-color: #DDE3EA; border-radius: 18rpx; background-color: #FFFEFD; box-sizing: border-box; } |
| | | .text-input { flex: 1; height: 80rpx; color: #303B4C; font-size: 27rpx; line-height: 80rpx; } |
| | | .input-placeholder, .select-placeholder { color: #A8B1BE; font-size: 26rpx; } |
| | | .select-shell { justify-content: space-between; color: #303B4C; font-size: 27rpx; line-height: 38rpx; } |
| | | .readonly-shell { color: #5A6472; background-color: #F0F2F5; border-color: #F0F2F5; } |
| | | .date-picker { width: 100%; } |
| | | .down-icon { width: 30rpx; height: 30rpx; margin-left: 14rpx; flex: none; } |
| | | page { background-color: #F5F5F5; } |
| | | .page { display: flex; flex: 1; width: 100%; height: 100vh; flex-direction: column; overflow: hidden; background-color: #F5F5F5; } |
| | | .navigation-bar { display: flex; height: 176rpx; padding: 70rpx 32rpx 0; flex: none; flex-direction: row; align-items: center; position: relative; background-color: #BDBDBD; box-sizing: border-box; } |
| | | .back-button { display: flex; width: 64rpx; height: 64rpx; justify-content: center; align-items: center; position: absolute; left: 30rpx; top: 90rpx; z-index: 2; } |
| | | .back-icon { width: 35rpx; height: 35rpx; } |
| | | .page-title { width: 100%; color: #FFFFFF; font-size: 30rpx; line-height: 42rpx; text-align: center; font-weight: 700; } |
| | | .form-scroll { flex: 1; height: 0; background-color: #FFFFFF; } |
| | | .form-body { width: 518rpx; margin-left: 48rpx; padding-top: 0; background-color: #FFFFFF; box-sizing: border-box; } |
| | | .type-row { display: flex; width: 518rpx; height: 64rpx; margin: 18rpx 0 19rpx 48rpx; flex-direction: row; align-items: center; position: relative; z-index: 100; } |
| | | .type-label { width: 120rpx; color: #202020; font-size: 24rpx; line-height: 34rpx; font-weight: 600; } |
| | | .type-select { display: flex; width: 320rpx; height: 48rpx; padding: 0 14rpx; justify-content: space-between; align-items: center; border: 1rpx solid #CFCFCF; border-radius: 4rpx; color: #202020; font-size: 24rpx; line-height: 34rpx; background-color: #FFFFFF; box-sizing: border-box; } |
| | | .dropdown-wrap { position: relative; z-index: 40; width: 518rpx; } |
| | | .dropdown-menu { position: absolute; top: 62rpx; left: 0; z-index: 80; width: 518rpx; overflow: hidden; border: 1rpx solid #8D8D8D; border-top: 0; background-color: #FFFFFF; box-sizing: border-box; } |
| | | .dropdown-mask { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 900; background-color: rgba(0, 0, 0, 0); } |
| | | .type-menu { position: fixed; top: 250rpx; left: 168rpx; width: 320rpx; z-index: 1000; border-top: 1rpx solid #8D8D8D; } |
| | | .validity-menu { position: fixed; left: 48rpx; width: 518rpx; z-index: 1000; border-top: 1rpx solid #8D8D8D; } |
| | | .business-validity-menu { top: 65vh; } |
| | | .subject-validity-menu { top: 48vh; } |
| | | .dropdown-option { display: flex; height: 54rpx; padding: 0 16rpx; align-items: center; color: #202020; font-size: 24rpx; line-height: 34rpx; background-color: #FFFFFF; box-sizing: border-box; } |
| | | .dropdown-option-active { color: #FFFFFF; background-color: #2768C7; } |
| | | .field-block { display: flex; margin-bottom: 27rpx; flex-direction: column; } |
| | | .settlement-title { margin: 36rpx 0 24rpx; color: #202020; font-size: 27rpx; line-height: 38rpx; font-weight: 700; } |
| | | .field-label { display: block; margin-bottom: 12rpx; color: #202020; font-size: 24rpx; line-height: 34rpx; font-weight: 600; } |
| | | .input-shell, .select-shell { display: flex; width: 518rpx; height: 62rpx; padding: 0 16rpx; align-items: center; border: 1rpx solid #CFCFCF; border-radius: 4rpx; color: #222222; font-size: 24rpx; line-height: 34rpx; background-color: #FFFFFF; box-sizing: border-box; } |
| | | .text-input { flex: 1; height: 60rpx; color: #222222; font-size: 24rpx; line-height: 60rpx; } |
| | | .input-placeholder, .select-placeholder { color: #9C9C9C; font-size: 24rpx; } |
| | | .select-shell { justify-content: space-between; } |
| | | .readonly-shell { background-color: #E2E2E2; } |
| | | .date-picker { width: 518rpx; } |
| | | .down-icon { width: 24rpx; height: 24rpx; flex: none; } |
| | | .ellipsis-text { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
| | | .field-invalid { border-color: #F0746D; background-color: #FFF9F8; } |
| | | .field-error { margin-top: 9rpx; color: #E95B55; font-size: 23rpx; line-height: 33rpx; } |
| | | .submit-button { display: flex; height: 88rpx; margin-top: 34rpx; justify-content: center; align-items: center; border-radius: 44rpx; color: #FFFFFF; font-size: 29rpx; line-height: 41rpx; font-weight: 700; background-image: linear-gradient(90deg, #FF7625 0%, #FF9630 100%); box-shadow: 0 12rpx 24rpx rgba(255, 107, 36, 0.20); } |
| | | .safe-space { height: 70rpx; } |
| | | .upload-box { display: flex; width: 518rpx; height: 132rpx; justify-content: center; align-items: center; overflow: hidden; border: 1rpx solid #CFCFCF; border-radius: 0; color: #333333; font-size: 24rpx; background-color: #FFFFFF; box-sizing: border-box; } |
| | | .document-image { width: 100%; height: 100%; } |
| | | .field-invalid { border-color: #F05252; } |
| | | .field-error { margin-top: 8rpx; color: #E64B4B; font-size: 22rpx; line-height: 30rpx; } |
| | | .submit-button { display: flex; width: 518rpx; height: 66rpx; margin-top: 18rpx; justify-content: center; align-items: center; border-radius: 4rpx; color: #FFFFFF; font-size: 24rpx; line-height: 34rpx; font-weight: 700; background-color: #2F6DF6; } |
| | | .button-disabled { opacity: 0.58; } |
| | | .safe-space { height: 72rpx; } |
| | | </style> |
| | |
| | | <view class="setting-row" @tap="handleMenuAction('营业时间')"><view class="setting-leading"><image class="setting-icon" src="/static/icons/lucide/clock-3.svg" mode="aspectFit"></image><text class="setting-label">营业时间</text></view><image class="setting-chevron" src="/static/icons/lucide/chevron-right.svg" mode="aspectFit"></image></view> |
| | | <view class="setting-row" @tap="handleMenuAction('商户权益')"><view class="setting-leading"><image class="setting-icon" src="/static/icons/lucide/badge-check.svg" mode="aspectFit"></image><text class="setting-label">商户权益</text></view><image class="setting-chevron" src="/static/icons/lucide/chevron-right.svg" mode="aspectFit"></image></view> |
| | | <view class="setting-row" @tap="handleMenuAction('商户协议')"><view class="setting-leading"><image class="setting-icon" src="/static/icons/lucide/file-text.svg" mode="aspectFit"></image><text class="setting-label">商户协议</text></view><image class="setting-chevron" src="/static/icons/lucide/chevron-right.svg" mode="aspectFit"></image></view> |
| | | <view class="setting-row" @tap="handleMenuAction('商户提现')"><view class="setting-leading"><image class="setting-icon" src="/static/icons/lucide/wallet-cards-gray.svg" mode="aspectFit"></image><text class="setting-label">商户提现</text></view><image class="setting-chevron" src="/static/icons/lucide/chevron-right.svg" mode="aspectFit"></image></view> |
| | | <view class="setting-row" @tap="handleMenuAction('商户进件')"><view class="setting-leading"><image class="setting-icon" src="/static/icons/lucide/wallet-cards-gray.svg" mode="aspectFit"></image><text class="setting-label">商户进件</text></view><image class="setting-chevron" src="/static/icons/lucide/chevron-right.svg" mode="aspectFit"></image></view> |
| | | <view class="setting-row" @tap="handleMenuAction('设置')"><view class="setting-leading"><image class="setting-icon" src="/static/icons/lucide/settings.svg" mode="aspectFit"></image><text class="setting-label">设置</text></view><image class="setting-chevron" src="/static/icons/lucide/chevron-right.svg" mode="aspectFit"></image></view> |
| | | </view> |
| | | </view> |
| | |
| | | <script setup lang="uts"> |
| | | import { hasActiveSession } from '../../services/session.uts' |
| | | import { MerchantProfile, loadMerchantProfile, updateMerchantBusinessStatus } from '../../services/merchant-management.uts' |
| | | import { loadOnboardingStatus, SettlementOnboarding } from '../../services/settlement.uts' |
| | | |
| | | const merchant = ref<MerchantProfile>({ merchantId: '', storeId: '', merchantName: '', storeName: '', address: '', coverUrl: '', businessStatus: '', onlineProductCount: 0 }) |
| | | const loadState = ref('loading') |
| | | const traceId = ref('') |
| | | const businessUpdating = ref(false) |
| | | const onboardingLoading = ref(false) |
| | | let cancelProfileRequest: (() => void) | null = null |
| | | const isBusinessOpen = computed(() => merchant.value.businessStatus == 'open') |
| | | |
| | |
| | | uni.navigateTo({ url: '/pages/products/products' }) |
| | | return |
| | | } |
| | | if (name === '商户提现') { |
| | | uni.navigateTo({ url: '/pages/withdrawal/withdrawal' }) |
| | | if (name === '商户进件') { |
| | | if (onboardingLoading.value) return |
| | | onboardingLoading.value = true |
| | | loadOnboardingStatus((item: SettlementOnboarding) => { |
| | | onboardingLoading.value = false |
| | | const target = item.status == 'unsubmitted' |
| | | ? '/pages/merchant-onboarding/merchant-onboarding' |
| | | : '/pages/merchant-onboarding-pending/merchant-onboarding-pending' |
| | | uni.navigateTo({ url: target }) |
| | | }, (error: any) => { |
| | | onboardingLoading.value = false |
| | | uni.showToast({ title: error.message != null ? error.message : '进件状态加载失败', icon: 'none' }) |
| | | }) |
| | | return |
| | | } |
| | | if (name === '营业时间') { |
| | |
| | | return http.get<Merchant, Merchant>(`/api/admin/business/merchants/${id}`) |
| | | } |
| | | |
| | | export function checkMerchantLoginAccount(account: string) { |
| | | return http.get<{ available: boolean }, { available: boolean }>('/api/admin/business/merchants/login-account-available', { params: { account } }) |
| | | export function checkMerchantLoginAccount(account: string, merchantId?: number | string) { |
| | | return http.get<{ available: boolean }, { available: boolean }>('/api/admin/business/merchants/login-account-available', { |
| | | params: { account, merchantId }, |
| | | }) |
| | | } |
| | | |
| | | export function createMerchant(payload: MerchantPayload) { |
| | |
| | | } |
| | | |
| | | .hotel-consume-row { |
| | | display: flex; |
| | | align-items: center; |
| | | display: grid; |
| | | grid-template-columns: 76px 42px max-content; |
| | | align-items: stretch; |
| | | padding-left: 186px; |
| | | padding-top: 20px; |
| | | gap: 12px; |
| | |
| | | } |
| | | |
| | | .hotel-consume-row > strong { |
| | | width: 76px; |
| | | flex-shrink: 0; |
| | | grid-column: 1; |
| | | align-self: center; |
| | | } |
| | | |
| | | .hotel-consume-row > .el-switch { |
| | | margin-top: 0; |
| | | grid-column: 2; |
| | | align-self: center; |
| | | } |
| | | |
| | | .hotel-consume-options { |
| | | grid-column: 3; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 12px; |
| | |
| | | grid-template-columns: 58px 180px 20px 180px 36px 80px; |
| | | } |
| | | |
| | | .movie-ticket-cover-row { |
| | | display: grid; |
| | | grid-template-columns: 58px 160px; |
| | | align-items: start; |
| | | gap: 12px; |
| | | margin-top: 18px; |
| | | color: #111827; |
| | | font-size: 13px; |
| | | } |
| | | |
| | | .movie-ticket-cover-upload { |
| | | display: block; |
| | | width: 160px; |
| | | height: 90px; |
| | | } |
| | | |
| | | .movie-ticket-cover-upload .el-upload { |
| | | display: block; |
| | | width: 160px; |
| | | height: 90px; |
| | | } |
| | | |
| | | .movie-ticket-cover-preview, |
| | | .movie-ticket-cover-placeholder { |
| | | display: flex; |
| | | width: 160px; |
| | | height: 90px; |
| | | overflow: hidden; |
| | | border: 1px dashed #cbd5e1; |
| | | border-radius: 4px; |
| | | background: #f8fafc; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .movie-ticket-cover-preview { |
| | | border-style: solid; |
| | | object-fit: cover; |
| | | } |
| | | |
| | | .movie-ticket-cover-placeholder { |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | color: #94a3b8; |
| | | } |
| | | |
| | | .movie-ticket-cover-placeholder > span { |
| | | font-size: 26px; |
| | | line-height: 28px; |
| | | } |
| | | |
| | | .movie-ticket-cover-placeholder small { |
| | | margin-top: 3px; |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .movie-ticket-merchant-row .el-select, |
| | | .movie-ticket-valid-row .el-input, |
| | | .movie-ticket-valid-row .el-select { |
| | |
| | | <script setup lang="ts"> |
| | | import { onMounted, reactive, ref } from 'vue' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import cityImage from '../assets/login-city.jpg' |
| | | import cityImage from '../assets/login-city-aerial.jpg' |
| | | import { loginAdmin } from '../api/auth' |
| | | import { useAppStore } from '../stores/app' |
| | | |
| | |
| | | const hasValidMerchantId = computed(() => /^\d+$/.test(queryString('merchantId'))) |
| | | const isEditMode = computed(() => route.query.mode === 'edit' && hasValidMerchantId.value) |
| | | const hasExistingMerchant = computed(() => isEditMode.value) |
| | | const merchantDetailLoaded = ref(!isEditMode.value) |
| | | const originalMerchantLoginAccount = ref('') |
| | | const normalizedMerchantType = computed(() => String(form.merchantType || '').trim()) |
| | | const merchantTypeAliases: Record<string, string> = { |
| | | '餐饮服务': 'food', food: 'food', |
| | |
| | | } |
| | | |
| | | async function checkLoginAccountAvailable() { |
| | | if (!requiresMerchantOperator.value) return true |
| | | if (!/^[A-Za-z0-9]{1,32}$/.test(accountForm.loginAccount)) return false |
| | | // Loading an edit record programmatically updates and may blur this field. |
| | | // Do not validate until its current operator account has been identified. |
| | | if (isEditMode.value && !merchantDetailLoaded.value) return true |
| | | const normalizedAccount = accountForm.loginAccount.trim() |
| | | if (isEditMode.value && originalMerchantLoginAccount.value !== '' |
| | | && normalizedAccount === originalMerchantLoginAccount.value) { |
| | | accountTaken.value = false |
| | | return true |
| | | } |
| | | // An existing merchant keeps its owner account; it must not be checked |
| | | // against itself, and any stale validation message should be cleared. |
| | | if (!requiresMerchantOperator.value) { |
| | | accountTaken.value = false |
| | | return true |
| | | } |
| | | if (!/^[A-Za-z0-9]{1,32}$/.test(normalizedAccount)) return false |
| | | accountChecking.value = true |
| | | try { |
| | | const result = await checkMerchantLoginAccount(accountForm.loginAccount) |
| | | const currentMerchantId = isEditMode.value ? queryString('merchantId') : undefined |
| | | const result = await checkMerchantLoginAccount(normalizedAccount, currentMerchantId) |
| | | accountTaken.value = !result.available |
| | | if (accountTaken.value) ElMessage.warning('登录账号已存在,请更换') |
| | | return result.available |
| | |
| | | if (isCarWashMerchant.value) syncCarWashJumpMeta() |
| | | }) |
| | | |
| | | watch(requiresMerchantOperator, (required) => { |
| | | if (!required) accountTaken.value = false |
| | | }) |
| | | |
| | | // The create page is reused when the service type changes, so reset these |
| | | // create-only defaults from the current route instead of retaining old values. |
| | | watch(() => [route.query.mode, route.query.merchantType], applyNewMerchantDefaults, { immediate: true }) |
| | |
| | | form.canLoginMerchantApp = Number(merchant.canLoginMerchantApp ?? 1) |
| | | form.recommend = Number(merchant.isRecommend ?? 0) |
| | | accountForm.loginAccount = merchant.loginAccount || '' |
| | | originalMerchantLoginAccount.value = String(merchant.loginAccount || '').trim() |
| | | merchantHasLoginAccount.value = Boolean(String(merchant.loginAccount || '').trim()) |
| | | accountTaken.value = false |
| | | if (isParkingMerchant.value) { |
| | | parkingAddresses.value = parseJsonValue<ParkingAddress[]>(merchant.usageInstructions, []) |
| | | } else if (isFuelMerchant.value) { |
| | |
| | | resetBusinessHours(merchant.businessHours) |
| | | resetMerchantRights(merchant.merchantRightsConfig) |
| | | resetCarWashJumpFromMerchant(merchant as Record<string, any>) |
| | | }).finally(() => { |
| | | merchantDetailLoaded.value = true |
| | | }) |
| | | } |
| | | if (activeTab.value === 'products') { |
| | |
| | | <el-form-item v-if="supportsMerchantAppLogin" class="merchant-account-login-toggle" label="登录商户端" label-width="100px"> |
| | | <el-switch v-model="form.canLoginMerchantApp" :active-value="1" :inactive-value="0" /> |
| | | </el-form-item> |
| | | <el-form-item class="merchant-account-field" label="登录账号" prop="loginAccount" :required="merchantOperatorEnabled" :error="accountTaken ? '登录账号已存在,请更换' : ''"> |
| | | <el-form-item class="merchant-account-field" label="登录账号" prop="loginAccount" :required="merchantOperatorEnabled" :error="requiresMerchantOperator && accountTaken ? '登录账号已存在,请更换' : ''"> |
| | | <el-input v-model="accountForm.loginAccount" maxlength="32" :disabled="isEditMode && merchantHasLoginAccount" :placeholder="isEditMode && !merchantHasLoginAccount ? '请输入登录账号' : (isEditMode ? '已配置登录账号' : '仅支持英文和数字')" @input="normalizeLoginAccount" @blur="checkLoginAccountAvailable" /> |
| | | </el-form-item> |
| | | <el-form-item class="merchant-account-field" label="登录密码" prop="loginPassword" :required="merchantOperatorEnabled"> |
| | |
| | | <div class="detail-field"><span>商户类型</span><strong>{{ subjectTypeLabel(detail.subjectType) }}</strong></div> |
| | | </div> |
| | | <div v-if="!isIndividual(detail.subjectType)" class="detail-grid"> |
| | | <div class="detail-field"><span>商户名称</span><strong>{{ text(detail.onboardingMerchantName) }}</strong></div> |
| | | <div class="detail-field"><span>商户简称</span><strong>{{ text(detail.merchantShortName) }}</strong></div> |
| | | <div class="detail-field"><span>公司类型</span><strong>{{ text(detail.companyType) }}</strong></div> |
| | | <div class="detail-field"><span>统一社会信用代码</span><strong>{{ text(detail.creditCode) }}</strong></div> |
| | | <div class="detail-field"><span>营业执照</span><strong><a v-if="detail.businessLicenseUrl" :href="detail.businessLicenseUrl" target="_blank">查看图片</a><template v-else>-</template></strong></div> |
| | | <div class="detail-field"><span>营业执照有效期类型</span><strong>{{ validityTypeLabel(detail.licenseValidityType) }}</strong></div> |
| | | <div class="detail-field"><span>证照有效期起始日期</span><strong>{{ dateText(detail.licenseBeginDate) }}</strong></div> |
| | | <div class="detail-field"><span>证照有效期结束日期</span><strong>{{ dateText(detail.licenseEndDate) }}</strong></div> |
| | | <div class="detail-field"><span>成立时间</span><strong>{{ dateText(detail.establishedDate) }}</strong></div> |
| | | <div class="detail-field"><span>注册地址(省、市、区)</span><strong>{{ text(detail.registeredRegion) }}</strong></div> |
| | | <div class="detail-field detail-field--auto-height"><span>详细注册地址</span><strong>{{ text(detail.registeredAddress) }}</strong></div> |
| | | <div class="detail-field"><span>注册资本</span><strong>{{ text(detail.registeredCapital) }}</strong></div> |
| | | <div class="detail-field"><span>法人姓名</span><strong>{{ text(detail.subjectName) }}</strong></div> |
| | | <div class="detail-field"><span>法人证件类型</span><strong>身份证</strong></div> |
| | | <div class="detail-field"><span>法人身份证人像面</span><strong><a v-if="detail.legalIdFrontUrl" :href="detail.legalIdFrontUrl" target="_blank">查看图片</a><template v-else>-</template></strong></div> |
| | | <div class="detail-field"><span>法人身份证国徽面</span><strong><a v-if="detail.legalIdBackUrl" :href="detail.legalIdBackUrl" target="_blank">查看图片</a><template v-else>-</template></strong></div> |
| | | <div class="detail-field"><span>法人证件号码</span><strong>{{ text(detail.subjectIdNo || detail.subjectIdNoMasked) }}</strong></div> |
| | | <div class="detail-field"><span>法人证件有效期类型</span><strong>{{ validityTypeLabel(detail.subjectCertValidityType) }}</strong></div> |
| | | <div class="detail-field"><span>法人证件有效期开始日期</span><strong>{{ dateText(detail.subjectCertBeginDate) }}</strong></div> |
| | | <div class="detail-field"><span>法人证件有效期截止日期</span><strong>{{ dateText(detail.subjectCertEndDate) }}</strong></div> |
| | | <div class="detail-field"><span>管理员姓名</span><strong>{{ text(detail.contactName) }}</strong></div> |
| | | <div class="detail-field"><span>管理员手机号</span><strong>{{ text(detail.contactMobile) }}</strong></div> |
| | | <div class="detail-field detail-field--auto-height"><span>法人证件地址</span><strong>{{ text(detail.legalCertificateAddress) }}</strong></div> |
| | | <div class="detail-field"><span>管理员邮箱</span><strong>{{ text(detail.administratorEmail) }}</strong></div> |
| | | </div> |
| | | <div v-else class="detail-grid"> |
| | | <div class="detail-field"><span>个人姓名</span><strong>{{ text(detail.subjectName) }}</strong></div> |
| | |
| | | import { computed, onMounted, reactive, ref } from 'vue' |
| | | import { ArrowLeft, Download, Plus, Refresh, Search } from '@element-plus/icons-vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import type { UploadFile } from 'element-plus' |
| | | import { |
| | | createTicketPackage, |
| | | generateTicketPackageCodes, |
| | |
| | | supplementTicketPackageClaim, |
| | | updateTicketPackage, |
| | | updateTicketPackageStatus, |
| | | uploadAdminImage, |
| | | voidTicketPackageCode, |
| | | } from '../api/admin' |
| | | import { TALENT_CARD_LEVELS } from '../utils/cardLevels' |
| | |
| | | function openEdit(row: Record<string, any>) { |
| | | selectedPackage.value = row |
| | | setMode('form', row) |
| | | } |
| | | |
| | | async function uploadCover(file: UploadFile) { |
| | | if (!file.raw) return |
| | | const result = await uploadAdminImage(file.raw) |
| | | form.coverUrl = result.url |
| | | ElMessage.success('封面图上传成功') |
| | | } |
| | | |
| | | async function save() { |
| | |
| | | <span>至</span> |
| | | <el-date-picker v-model="form.validEnd" value-format="YYYY-MM-DD" placeholder="结束日期" /> |
| | | </div> |
| | | <div class="movie-ticket-cover-row"> |
| | | <span>封面图</span> |
| | | <el-upload class="movie-ticket-cover-upload" accept="image/*" :auto-upload="false" :show-file-list="false" :on-change="uploadCover"> |
| | | <el-image v-if="form.coverUrl" class="movie-ticket-cover-preview" :src="form.coverUrl" fit="cover" /> |
| | | <div v-else class="movie-ticket-cover-placeholder"><span>+</span><small>上传图片</small></div> |
| | | </el-upload> |
| | | </div> |
| | | </el-form> |
| | | |
| | | <h3>人才权益</h3> |
| | |
| | | } |
| | | |
| | | @GetMapping("/merchants/login-account-available") |
| | | public ApiResponse<Map<String, Object>> merchantLoginAccountAvailable(@RequestParam String account) { |
| | | return ApiResponse.success(adminBusinessService.merchantLoginAccountAvailable(account)); |
| | | public ApiResponse<Map<String, Object>> merchantLoginAccountAvailable( |
| | | @RequestParam String account, |
| | | @RequestParam(required = false) Long merchantId) { |
| | | return ApiResponse.success(adminBusinessService.merchantLoginAccountAvailable(account, merchantId)); |
| | | } |
| | | |
| | | @PostMapping("/merchants") |
| | |
| | | s.external_mini_program_path as car_wash_external_mini_program_path, |
| | | s.external_web_url as car_wash_external_web_url, |
| | | (select o.account from merchant_operator o |
| | | where o.merchant_id = merchant.id and o.deleted = 0 |
| | | order by case when o.role_code = 'owner' then 0 else 1 end, o.id asc limit 1) as login_account |
| | | where o.merchant_id = merchant.id |
| | | order by o.deleted asc, case when o.role_code = 'owner' then 0 else 1 end, o.id asc limit 1) as login_account |
| | | from merchant |
| | | left join transport_car_wash_service s on s.merchant_id = merchant.id and s.deleted = 0 |
| | | where merchant.deleted = 0 and merchant.id = ? |
| | |
| | | |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> merchantLoginAccountAvailable(String account) { |
| | | return merchantLoginAccountAvailable(account, null); |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> merchantLoginAccountAvailable(String account, Long merchantId) { |
| | | String normalizedAccount = account == null ? "" : account.trim(); |
| | | if (!normalizedAccount.matches("^[A-Za-z0-9]{1,32}$")) { |
| | | return Map.of("available", false); |
| | | } |
| | | Long count = jdbcTemplate.queryForObject( |
| | | "select count(*) from merchant_operator where account = ?", Long.class, normalizedAccount); |
| | | Long count; |
| | | if (merchantId == null) { |
| | | count = jdbcTemplate.queryForObject( |
| | | "select count(*) from merchant_operator where account = ?", Long.class, normalizedAccount); |
| | | } else { |
| | | count = jdbcTemplate.queryForObject( |
| | | "select count(*) from merchant_operator where account = ? and merchant_id <> ?", |
| | | Long.class, normalizedAccount, merchantId); |
| | | } |
| | | return Map.of("available", count == null || count == 0); |
| | | } |
| | | |
| | |
| | | } |
| | | int passwordUpdated = jdbcTemplate.update(""" |
| | | update merchant_operator |
| | | set password_hash = ? |
| | | where merchant_id = ? and role_code = 'owner' and deleted = 0 |
| | | set password_hash = ?, deleted = 0, status = 1 |
| | | where merchant_id = ? and role_code = 'owner' |
| | | """, MerchantPasswordHasher.encode(request.loginPassword()), id); |
| | | if (passwordUpdated == 0) { |
| | | createMerchantOperator(id, request); |
| | |
| | | if ((request.loginPassword() == null || request.loginPassword().isBlank()) && supportsMerchantAppLogin(request.merchantType()) |
| | | && (request.canLoginMerchantApp() == null || request.canLoginMerchantApp() == 1) |
| | | && (request.loginAccount() != null && !request.loginAccount().isBlank())) { |
| | | createMerchantOperator(id, request); |
| | | ensureMerchantOperator(id, request); |
| | | } |
| | | return merchant(id); |
| | | } |
| | |
| | | if (request.loginPassword() == null || request.loginPassword().length() < 6 || request.loginPassword().length() > 64) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "商户登录密码长度须为6至64位"); |
| | | } |
| | | Integer occupied = jdbcTemplate.queryForObject("select count(*) from merchant_operator where account = ? and deleted = 0", Integer.class, request.loginAccount().trim()); |
| | | Integer occupied = jdbcTemplate.queryForObject("select count(*) from merchant_operator where account = ?", Integer.class, request.loginAccount().trim()); |
| | | if (occupied != null && occupied > 0) throw new BusinessException(ErrorCode.CONFLICT, "登录账号已存在"); |
| | | jdbcTemplate.update("insert into merchant_operator(merchant_id, account, password_hash, display_name, role_code, status) values (?, ?, ?, ?, 'owner', 1)", |
| | | merchantId, request.loginAccount().trim(), MerchantPasswordHasher.encode(request.loginPassword()), request.merchantName().trim()); |
| | | } |
| | | |
| | | private void ensureMerchantOperator(Long merchantId, MerchantUpdateRequest request) { |
| | | String account = request.loginAccount().trim(); |
| | | Integer sameMerchant = jdbcTemplate.queryForObject( |
| | | "select count(*) from merchant_operator where merchant_id = ? and account = ?", |
| | | Integer.class, merchantId, account); |
| | | if (sameMerchant != null && sameMerchant > 0) { |
| | | jdbcTemplate.update(""" |
| | | update merchant_operator |
| | | set deleted = 0, status = 1, display_name = ? |
| | | where merchant_id = ? and account = ? |
| | | """, request.merchantName().trim(), merchantId, account); |
| | | return; |
| | | } |
| | | createMerchantOperator(merchantId, request); |
| | | } |
| | | |
| | | private void ensureParkingBenefit(Long merchantId, String merchantType) { |
| | | if (!"parking".equals(merchantType)) return; |
| | | jdbcTemplate.update(""" |
| | |
| | | } |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> onboarding(Long id) { |
| | | List<Map<String, Object>> rows = rows("select a.*, m.merchant_name, m.merchant_type, m.contact_mobile, s.store_name from merchant_onboarding_application a join merchant m on m.id = a.merchant_id join merchant_store s on s.id = a.store_id where a.id = ?", id); |
| | | List<Map<String, Object>> rows = rows("select a.*, a.merchant_name as onboarding_merchant_name, m.merchant_name, m.merchant_type, m.contact_mobile, s.store_name from merchant_onboarding_application a join merchant m on m.id = a.merchant_id join merchant_store s on s.id = a.store_id where a.id = ?", id); |
| | | if (rows.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "商户进件不存在"); |
| | | Map<String, Object> result = rows.get(0); |
| | | // 个人主体的历史记录可能使用 individual、personal 或中文类型值; |
| | |
| | | @Pattern(regexp = "|\\d{6}") String registeredCityCode, |
| | | @Pattern(regexp = "|\\d{6}") String registeredDistrictCode, |
| | | @Size(max = 255) String registeredAddress, |
| | | @Size(max = 128) String merchantName, |
| | | @Size(max = 128) String merchantShortName, |
| | | @Size(max = 64) String companyType, |
| | | @Size(max = 512) String businessLicenseUrl, |
| | | @Pattern(regexp = "|\\d{4}-\\d{2}-\\d{2}") String establishedDate, |
| | | @Size(max = 64) String registeredCapital, |
| | | @Size(max = 512) String legalIdFrontUrl, |
| | | @Size(max = 512) String legalIdBackUrl, |
| | | @Size(max = 255) String legalCertificateAddress, |
| | | @Size(max = 32) String administratorEmail, |
| | | @NotBlank @Size(max = 64) String contactName, |
| | | @NotBlank @Pattern(regexp = "1\\d{10}") String contactMobile |
| | | @NotBlank @Pattern(regexp = "1\\d{10}") String contactMobile, |
| | | @NotBlank @Pattern(regexp = "\\d{16,32}") String bankCardNumber, |
| | | @NotBlank @Pattern(regexp = "\\d{6}") String bankProvinceCode, |
| | | @NotBlank @Pattern(regexp = "\\d{6}") String bankCityCode, |
| | | @NotBlank @Pattern(regexp = "\\d{12}") String bankBranchCode, |
| | | @Size(max = 512) String settlementCardFrontUrl, |
| | | @Size(max = 512) String registrationAccountUrl |
| | | ) {} |
| New file |
| | |
| | | package com.talentcard.server.business.merchant; |
| | | |
| | | import java.sql.Connection; |
| | | import java.sql.ResultSet; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.Map; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.boot.ApplicationArguments; |
| | | import org.springframework.boot.ApplicationRunner; |
| | | import org.springframework.jdbc.core.JdbcTemplate; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | /** Keeps merchant onboarding additive columns available when Flyway is disabled in the deployment. */ |
| | | @Slf4j |
| | | @Component |
| | | public class MerchantOnboardingSchemaInitializer implements ApplicationRunner { |
| | | private final JdbcTemplate jdbc; |
| | | |
| | | public MerchantOnboardingSchemaInitializer(JdbcTemplate jdbc) { this.jdbc = jdbc; } |
| | | |
| | | @Override |
| | | public void run(ApplicationArguments args) { |
| | | try { |
| | | Map<String, String> columns = new LinkedHashMap<>(); |
| | | columns.put("merchant_name", "varchar(128) default null"); |
| | | columns.put("merchant_short_name", "varchar(128) default null"); |
| | | columns.put("company_type", "varchar(64) default null"); |
| | | columns.put("business_license_url", "varchar(512) default null"); |
| | | columns.put("established_date", "char(10) default null"); |
| | | columns.put("registered_capital", "varchar(64) default null"); |
| | | columns.put("legal_id_front_url", "varchar(512) default null"); |
| | | columns.put("legal_id_back_url", "varchar(512) default null"); |
| | | columns.put("legal_certificate_address", "varchar(255) default null"); |
| | | columns.put("administrator_email", "varchar(128) default null"); |
| | | columns.put("initial_bank_card_id", "bigint default null"); |
| | | columns.put("provider_huifu_id", "varchar(32) default null"); |
| | | columns.put("provider_apply_no", "varchar(32) default null"); |
| | | columns.put("provider_response_code", "varchar(32) default null"); |
| | | columns.put("provider_response_message", "varchar(512) default null"); |
| | | columns.put("wechat_config_status", "varchar(16) not null default 'not_started'"); |
| | | columns.put("wechat_config_request_no", "varchar(32) default null"); |
| | | columns.put("wechat_config_response_code", "varchar(32) default null"); |
| | | columns.put("wechat_config_response_message", "varchar(512) default null"); |
| | | columns.put("business_open_status", "varchar(16) not null default 'not_started'"); |
| | | columns.put("business_open_request_no", "varchar(32) default null"); |
| | | columns.put("business_open_apply_no", "varchar(32) default null"); |
| | | columns.put("business_open_response_code", "varchar(32) default null"); |
| | | columns.put("business_open_response_message", "varchar(512) default null"); |
| | | for (Map.Entry<String, String> column : columns.entrySet()) { |
| | | if (!hasColumn("merchant_onboarding_application", column.getKey())) jdbc.execute("alter table merchant_onboarding_application add column " + column.getKey() + " " + column.getValue()); |
| | | } |
| | | Map<String, String> bankCardColumns = new LinkedHashMap<>(); |
| | | bankCardColumns.put("provider_token_no", "varchar(32) default null"); |
| | | bankCardColumns.put("provider_apply_no", "varchar(32) default null"); |
| | | bankCardColumns.put("provider_request_no", "varchar(32) default null"); |
| | | bankCardColumns.put("provider_response_code", "varchar(32) default null"); |
| | | bankCardColumns.put("provider_response_message", "varchar(512) default null"); |
| | | bankCardColumns.put("card_no_encrypted", "varchar(1024) default null"); |
| | | bankCardColumns.put("branch_code", "varchar(12) default null"); |
| | | for (Map.Entry<String, String> column : bankCardColumns.entrySet()) { |
| | | if (!hasColumn("merchant_bank_card", column.getKey())) jdbc.execute("alter table merchant_bank_card add column " + column.getKey() + " " + column.getValue()); |
| | | } |
| | | } catch (Exception exception) { |
| | | log.warn("Merchant onboarding schema initialization failed", exception); |
| | | } |
| | | } |
| | | |
| | | private boolean hasColumn(String tableName, String columnName) throws Exception { |
| | | try (Connection connection = jdbc.getDataSource().getConnection(); ResultSet result = connection.getMetaData().getColumns(connection.getCatalog(), null, tableName, columnName)) { |
| | | if (result.next()) return true; |
| | | } |
| | | try (Connection connection = jdbc.getDataSource().getConnection(); ResultSet result = connection.getMetaData().getColumns(connection.getCatalog(), null, tableName.toUpperCase(), columnName.toUpperCase())) { |
| | | return result.next(); |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.security.MessageDigest; |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.time.LocalDateTime; |
| | | import java.util.ArrayList; |
| | | import java.util.LinkedHashMap; |
| | |
| | | import com.talentcard.server.business.payment.huifu.HuifuIndividualOnboardingGateway; |
| | | import com.talentcard.server.business.payment.huifu.HuifuEnterpriseOnboardingCommand; |
| | | import com.talentcard.server.business.payment.huifu.HuifuEnterpriseOnboardingGateway; |
| | | import com.talentcard.server.business.payment.huifu.HuifuMerchantBusinessOpenGateway; |
| | | import com.talentcard.server.business.payment.huifu.HuifuMerchantWechatConfigGateway; |
| | | import com.talentcard.server.business.payment.huifu.HuifuPaymentGatewayException; |
| | | import com.talentcard.server.business.payment.huifu.HuifuPaymentProperties; |
| | | import com.talentcard.server.business.payment.huifu.HuifuPictureUploadGateway; |
| | | import com.talentcard.server.business.payment.huifu.HuifuUserBusinessOpenCommand; |
| | | import com.talentcard.server.business.payment.huifu.HuifuUserBusinessOpenGateway; |
| | | import com.talentcard.server.common.SensitiveDataCrypto; |
| | |
| | | |
| | | @Service |
| | | public class MerchantSettlementService { |
| | | private static final Map<String, String> MCC_BY_MERCHANT_TYPE = Map.ofEntries( |
| | | Map.entry("food", "5812"), Map.entry("scenic", "7991"), Map.entry("hotel", "7011"), Map.entry("apartment", "7011"), |
| | | Map.entry("cinema", "7832"), Map.entry("fuel", "5541"), Map.entry("parking", "7523"), Map.entry("carWash", "7542"), |
| | | Map.entry("medical", "8099"), Map.entry("employment", "7361"), Map.entry("finance", "6012"), Map.entry("approval", "9399"), |
| | | Map.entry("children", "8351"), Map.entry("housing", "6513"), Map.entry("car", "5511"), Map.entry("appliance", "5722"), |
| | | Map.entry("digital", "5732"), Map.entry("culture", "5942"), Map.entry("telecom", "4814"), Map.entry("dental", "8021"), |
| | | Map.entry("shopping", "5311"), Map.entry("other", "7399")); |
| | | private final JdbcTemplate jdbc; |
| | | private final SensitiveDataCrypto sensitiveDataCrypto; |
| | | private final HuifuIndividualOnboardingGateway individualOnboardingGateway; |
| | | private final HuifuEnterpriseOnboardingGateway enterpriseOnboardingGateway; |
| | | private final HuifuMerchantWechatConfigGateway merchantWechatConfigGateway; |
| | | private final HuifuMerchantBusinessOpenGateway merchantBusinessOpenGateway; |
| | | private final HuifuMerchantWithdrawalService withdrawalService; |
| | | private final HuifuUserBusinessOpenGateway userBusinessOpenGateway; |
| | | private final HuifuPaymentProperties huifuPaymentProperties; |
| | | private final HuifuPictureUploadGateway pictureUploadGateway; |
| | | |
| | | public MerchantSettlementService(JdbcTemplate jdbc, SensitiveDataCrypto sensitiveDataCrypto, HuifuIndividualOnboardingGateway individualOnboardingGateway, HuifuEnterpriseOnboardingGateway enterpriseOnboardingGateway, HuifuMerchantWithdrawalService withdrawalService, HuifuUserBusinessOpenGateway userBusinessOpenGateway, HuifuPaymentProperties huifuPaymentProperties) { |
| | | public MerchantSettlementService(JdbcTemplate jdbc, SensitiveDataCrypto sensitiveDataCrypto, HuifuIndividualOnboardingGateway individualOnboardingGateway, HuifuEnterpriseOnboardingGateway enterpriseOnboardingGateway, HuifuMerchantWechatConfigGateway merchantWechatConfigGateway, HuifuMerchantBusinessOpenGateway merchantBusinessOpenGateway, HuifuMerchantWithdrawalService withdrawalService, HuifuUserBusinessOpenGateway userBusinessOpenGateway, HuifuPaymentProperties huifuPaymentProperties, HuifuPictureUploadGateway pictureUploadGateway) { |
| | | this.jdbc = jdbc; |
| | | this.sensitiveDataCrypto = sensitiveDataCrypto; |
| | | this.individualOnboardingGateway = individualOnboardingGateway; |
| | | this.enterpriseOnboardingGateway = enterpriseOnboardingGateway; |
| | | this.merchantWechatConfigGateway = merchantWechatConfigGateway; |
| | | this.merchantBusinessOpenGateway = merchantBusinessOpenGateway; |
| | | this.withdrawalService = withdrawalService; |
| | | this.userBusinessOpenGateway = userBusinessOpenGateway; |
| | | this.huifuPaymentProperties = huifuPaymentProperties; |
| | | this.pictureUploadGateway = pictureUploadGateway; |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> onboardingStatus(MerchantPrincipal principal) { |
| | | store(principal); |
| | | List<Map<String, Object>> records = rows("select * from merchant_onboarding_application where merchant_id = ? and store_id = ? order by id desc limit 1", principal.merchantId(), principal.storeId()); |
| | | List<Map<String, Object>> records = rows("select * from merchant_onboarding_application where merchant_id = ? and store_id = ? and initial_bank_card_id is not null order by id desc limit 1", principal.merchantId(), principal.storeId()); |
| | | return records.isEmpty() ? Map.of("status", "unsubmitted", "reason", "") : onboarding(records.get(0)); |
| | | } |
| | | |
| | |
| | | requireKey(key); store(principal); |
| | | List<Map<String, Object>> repeated = rows("select * from merchant_onboarding_application where merchant_id = ? and idempotency_key = ? limit 1", principal.merchantId(), key.trim()); |
| | | if (!repeated.isEmpty()) return onboarding(repeated.get(0)); |
| | | List<Map<String, Object>> active = rows("select * from merchant_onboarding_application where merchant_id = ? and store_id = ? and status in ('pending','approved') order by id desc limit 1", principal.merchantId(), principal.storeId()); |
| | | List<Map<String, Object>> active = rows("select * from merchant_onboarding_application where merchant_id = ? and store_id = ? and initial_bank_card_id is not null and status in ('pending','approved') order by id desc limit 1", principal.merchantId(), principal.storeId()); |
| | | if (!active.isEmpty()) return onboarding(active.get(0)); |
| | | boolean enterprise = "enterprise".equals(request.subjectType()); |
| | | if (enterprise && (request.creditCode() == null || !request.creditCode().trim().matches("[0-9A-Z]{18}"))) throw new BusinessException(ErrorCode.BAD_REQUEST, "请输入18位统一社会信用代码"); |
| | | if (enterprise && (request.subjectIdNumber() == null || !request.subjectIdNumber().trim().matches("(?:\\d{15}|\\d{17}[0-9X])"))) throw new BusinessException(ErrorCode.BAD_REQUEST, "请输入法人正确的身份证号"); |
| | | if (enterprise && (request.subjectCertificateBeginDate() == null || request.subjectCertificateBeginDate().isBlank())) throw new BusinessException(ErrorCode.BAD_REQUEST, "请输入营业执照成立日期"); |
| | | if (!enterprise && (request.subjectIdNumber() == null || !request.subjectIdNumber().trim().matches("(?:\\d{15}|\\d{17}[0-9X])"))) throw new BusinessException(ErrorCode.BAD_REQUEST, "请输入正确的身份证号"); |
| | | if (enterprise && (request.creditCode() == null || !request.creditCode().trim().matches("[0-9A-Z]{18}"))) throw new BusinessException(ErrorCode.BAD_REQUEST, "请输入正确的18位统一社会信用代码"); |
| | | if (request.subjectIdNumber() == null || !request.subjectIdNumber().trim().matches("(?:\\d{15}|\\d{17}[0-9X])")) throw new BusinessException(ErrorCode.BAD_REQUEST, enterprise ? "请输入正确的法人身份证号" : "请输入正确的负责人身份证号"); |
| | | requireCertificatePeriod(request.subjectCertificateValidityType(), request.subjectCertificateBeginDate(), request.subjectCertificateEndDate(), "Identity document"); |
| | | if (blank(request.legalCertificateAddress())) throw new BusinessException(ErrorCode.BAD_REQUEST, enterprise ? "请输入法人证件地址" : "请输入负责人证件地址"); |
| | | if (blank(request.administratorEmail()) || !request.administratorEmail().trim().matches("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$")) throw new BusinessException(ErrorCode.BAD_REQUEST, "请输入正确的联系人邮箱"); |
| | | if (enterprise) { |
| | | requireCertificatePeriod(request.licenseValidityType(), request.licenseBeginDate(), request.licenseEndDate(), "Business licence"); |
| | | if (blank(request.registeredProvinceCode()) || blank(request.registeredCityCode()) || blank(request.registeredDistrictCode())) throw new BusinessException(ErrorCode.BAD_REQUEST, "Enterprise registration address codes are required"); |
| | | if (blank(request.merchantName()) || blank(request.businessLicenseUrl()) || blank(request.legalIdFrontUrl()) || blank(request.legalIdBackUrl()) || blank(request.establishedDate())) throw new BusinessException(ErrorCode.BAD_REQUEST, "请补全企业进件资料"); |
| | | normalizeRegisteredCapital(request.companyType(), request.registeredCapital()); |
| | | } else { |
| | | if (blank(request.registeredDistrictCode()) || blank(request.registeredAddress()) || blank(request.legalIdFrontUrl()) || blank(request.legalIdBackUrl())) throw new BusinessException(ErrorCode.BAD_REQUEST, "请补全个人商户进件资料"); |
| | | } |
| | | boolean privateSettlement = !enterprise || "个体工商户".equals(clean(request.companyType())); |
| | | if (privateSettlement && blank(request.settlementCardFrontUrl())) throw new BusinessException(ErrorCode.BAD_REQUEST, "请上传结算银行卡卡号面"); |
| | | if (!privateSettlement && blank(request.registrationAccountUrl())) throw new BusinessException(ErrorCode.BAD_REQUEST, "请上传开户许可证"); |
| | | List<Map<String, Object>> branchRows = rows("select clearing_code, branch_name, bank_name, head_bank_code from bank_branch_directory where clearing_code = ? limit 1", request.bankBranchCode().trim()); |
| | | if (branchRows.isEmpty()) throw new BusinessException(ErrorCode.BAD_REQUEST, "请选择真实开户行"); |
| | | Map<String, Object> branch = branchRows.get(0); |
| | | if (clean(branch.get("headBankCode")).isBlank()) throw new BusinessException(ErrorCode.BAD_REQUEST, "所选开户行缺少汇付银行编码"); |
| | | String no = BusinessIdGenerator.next("MO"); |
| | | String certificateValidityType = certificateValidityType(request.subjectCertificateValidityType()); |
| | | String certificateBeginDate = certificateBeginDate(request.subjectCertificateBeginDate()); |
| | | jdbc.update("insert into merchant_onboarding_application(merchant_id, store_id, application_no, subject_type, subject_name, credit_code, license_validity_type, license_begin_date, license_end_date, subject_id_no_masked, subject_id_no_encrypted, subject_cert_validity_type, subject_cert_begin_date, subject_cert_end_date, registered_region, registered_province_code, registered_city_code, registered_district_code, registered_address, contact_name, contact_mobile, idempotency_key) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", principal.merchantId(), principal.storeId(), no, request.subjectType(), request.subjectName().trim(), clean(request.creditCode()), certificateValidityType(request.licenseValidityType()), certificateBeginDate(request.licenseBeginDate()), certificateEndDate(request.licenseEndDate()), maskId(request.subjectIdNumber()), sensitiveDataCrypto.encrypt(clean(request.subjectIdNumber()).toUpperCase()), certificateValidityType, certificateBeginDate, certificateEndDate(request.subjectCertificateEndDate()), clean(request.registeredRegion()), clean(request.registeredProvinceCode()), clean(request.registeredCityCode()), clean(request.registeredDistrictCode()), clean(request.registeredAddress()), request.contactName().trim(), request.contactMobile().trim(), key.trim()); |
| | | if ("individual".equals(request.subjectType())) submitIndividualOnboarding(no, request); |
| | | else submitEnterpriseOnboarding(no, request); |
| | | jdbc.update("insert into merchant_onboarding_application(merchant_id, store_id, application_no, subject_type, subject_name, credit_code, license_validity_type, license_begin_date, license_end_date, subject_id_no_masked, subject_id_no_encrypted, subject_cert_validity_type, subject_cert_begin_date, subject_cert_end_date, registered_region, registered_province_code, registered_city_code, registered_district_code, registered_address, merchant_name, merchant_short_name, company_type, business_license_url, established_date, registered_capital, legal_id_front_url, legal_id_back_url, legal_certificate_address, administrator_email, contact_name, contact_mobile, idempotency_key) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", principal.merchantId(), principal.storeId(), no, request.subjectType(), request.subjectName().trim(), clean(request.creditCode()), certificateValidityType(request.licenseValidityType()), certificateBeginDate(request.licenseBeginDate()), certificateEndDate(request.licenseEndDate()), maskId(request.subjectIdNumber()), sensitiveDataCrypto.encrypt(clean(request.subjectIdNumber()).toUpperCase()), certificateValidityType, certificateBeginDate, certificateEndDate(request.subjectCertificateEndDate()), clean(request.registeredRegion()), clean(request.registeredProvinceCode()), clean(request.registeredCityCode()), clean(request.registeredDistrictCode()), clean(request.registeredAddress()), clean(request.merchantName()), clean(request.merchantShortName()), clean(request.companyType()), clean(request.businessLicenseUrl()), clean(request.establishedDate()), clean(request.registeredCapital()), clean(request.legalIdFrontUrl()), clean(request.legalIdBackUrl()), clean(request.legalCertificateAddress()), clean(request.administratorEmail()), request.contactName().trim(), request.contactMobile().trim(), key.trim()); |
| | | String cardNumber = request.bankCardNumber().trim(); |
| | | String cardKey = key.trim() + ":initial"; |
| | | if (cardKey.length() > 128) cardKey = cardKey.substring(0, 128); |
| | | String cardHash = sha256(cardNumber); |
| | | List<Map<String, Object>> existingCards = rows("select * from merchant_bank_card where merchant_id = ? and card_hash = ? limit 1 for update", principal.merchantId(), cardHash); |
| | | Long bankCardId; |
| | | if (!existingCards.isEmpty()) { |
| | | Map<String, Object> existing = existingCards.get(0); |
| | | String existingStatus = clean(existing.get("status")); |
| | | Long existingCardId = ((Number) existing.get("id")).longValue(); |
| | | List<Map<String, Object>> linkedActiveOnboardings = rows("select id from merchant_onboarding_application where initial_bank_card_id = ? and status in ('pending','approved') limit 1", existingCardId); |
| | | if ("pending".equals(existingStatus) || !linkedActiveOnboardings.isEmpty()) { |
| | | throw new BusinessException(ErrorCode.CONFLICT, "该银行卡已有审核中的进件或已通过的申请,请勿重复提交"); |
| | | } |
| | | bankCardId = existingCardId; |
| | | jdbc.update("update merchant_bank_card set store_id=?, bank_name=?, branch_name=?, branch_code=?, city_code=?, card_tail=?, card_masked=?, card_no_encrypted=?, provider_request_no=?, idempotency_key=?, reject_reason=null, reviewed_at=null, provider_token_no=null, provider_apply_no=null, provider_response_code=null, provider_response_message=null, status='pending' where id=?", principal.storeId(), clean(branch.get("bankName")), clean(branch.get("branchName")), clean(branch.get("clearingCode")), request.bankCityCode().trim(), cardNumber.substring(cardNumber.length() - 4), "**** **** **** " + cardNumber.substring(cardNumber.length() - 4), sensitiveDataCrypto.encrypt(cardNumber), no, cardKey, bankCardId); |
| | | } else { |
| | | jdbc.update("insert into merchant_bank_card(merchant_id, store_id, bank_name, branch_name, branch_code, city_code, card_tail, card_masked, card_hash, card_no_encrypted, provider_request_no, idempotency_key) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", principal.merchantId(), principal.storeId(), clean(branch.get("bankName")), clean(branch.get("branchName")), clean(branch.get("clearingCode")), request.bankCityCode().trim(), cardNumber.substring(cardNumber.length() - 4), "**** **** **** " + cardNumber.substring(cardNumber.length() - 4), cardHash, sensitiveDataCrypto.encrypt(cardNumber), no, cardKey); |
| | | bankCardId = ((Number) rows("select id from merchant_bank_card where merchant_id=? and idempotency_key=?", principal.merchantId(), cardKey).get(0).get("id")).longValue(); |
| | | } |
| | | jdbc.update("update merchant_onboarding_application set initial_bank_card_id=? where application_no=?", bankCardId, no); |
| | | if ("individual".equals(request.subjectType())) submitIndividualOnboarding(principal, no, bankCardId, request, branch); |
| | | else submitEnterpriseOnboarding(principal, no, bankCardId, request, branch, privateSettlement); |
| | | return onboarding(rows("select * from merchant_onboarding_application where application_no = ?", no).get(0)); |
| | | } |
| | | |
| | | private void submitEnterpriseOnboarding(String applicationNo, MerchantOnboardingRequest request) { |
| | | private void submitEnterpriseOnboarding(MerchantPrincipal principal, String applicationNo, Long bankCardId, MerchantOnboardingRequest request, Map<String, Object> branch, boolean privateSettlement) { |
| | | try { |
| | | String licensePictureId = pictureUploadGateway.uploadUrl(request.businessLicenseUrl(), "F07"); |
| | | String legalFrontPictureId = pictureUploadGateway.uploadUrl(request.legalIdFrontUrl(), "F02"); |
| | | String legalBackPictureId = pictureUploadGateway.uploadUrl(request.legalIdBackUrl(), "F03"); |
| | | String registrationAccountPictureId = privateSettlement ? "" : pictureUploadGateway.uploadUrl(request.registrationAccountUrl(), "F08"); |
| | | String settlementCardPictureId = privateSettlement ? pictureUploadGateway.uploadUrl(request.settlementCardFrontUrl(), "F13") : ""; |
| | | String settlementCertFrontPictureId = privateSettlement ? pictureUploadGateway.uploadUrl(request.legalIdFrontUrl(), "F55") : ""; |
| | | String settlementCertBackPictureId = privateSettlement ? pictureUploadGateway.uploadUrl(request.legalIdBackUrl(), "F56") : ""; |
| | | String shortName = blank(request.merchantShortName()) ? request.merchantName().trim() : request.merchantShortName().trim(); |
| | | HuifuEnterpriseOnboardingGateway.Result result = enterpriseOnboardingGateway.create(new HuifuEnterpriseOnboardingCommand( |
| | | applicationNo, request.subjectName().trim(), request.creditCode().trim(), |
| | | applicationNo, clean(request.merchantName()), shortName, request.creditCode().trim(), |
| | | certificateValidityType(request.licenseValidityType()), certificateBeginDate(request.licenseBeginDate()), certificateEndDate(request.licenseEndDate()), |
| | | clean(request.registeredProvinceCode()), clean(request.registeredCityCode()), clean(request.registeredDistrictCode()), clean(request.registeredAddress()), request.subjectName().trim(), |
| | | clean(request.subjectIdNumber()).toUpperCase(), certificateValidityType(request.subjectCertificateValidityType()), |
| | | certificateBeginDate(request.subjectCertificateBeginDate()), certificateEndDate(request.subjectCertificateEndDate()), request.contactName().trim(), request.contactMobile().trim())); |
| | | if (result.accepted() && !result.huifuId().isBlank()) jdbc.update("update merchant_onboarding_application set status='approved', provider_huifu_id=?, reviewed_at=now(), reject_reason=null where application_no=? and status='pending'", result.huifuId(), applicationNo); |
| | | else if (!result.accepted()) jdbc.update("update merchant_onboarding_application set status='rejected', reject_reason=?, reviewed_at=now() where application_no=? and status='pending'", providerReason(result.code(), result.message()), applicationNo); |
| | | else jdbc.update("update merchant_onboarding_application set reject_reason=? where application_no=? and status='pending'", "Huifu accepted the request but did not return huifu_id", applicationNo); |
| | | certificateBeginDate(request.subjectCertificateBeginDate()), certificateEndDate(request.subjectCertificateEndDate()), clean(request.legalCertificateAddress()), request.contactName().trim(), request.contactMobile().trim(), request.administratorEmail().trim(), |
| | | entType(request.companyType()), mcc(principal.merchantId()), licensePictureId, legalFrontPictureId, legalBackPictureId, |
| | | privateSettlement ? "1" : "0", privateSettlement ? request.subjectName().trim() : request.merchantName().trim(), request.bankCardNumber().trim(), request.bankProvinceCode().trim(), request.bankCityCode().trim(), clean(branch.get("headBankCode")), clean(branch.get("clearingCode")), clean(branch.get("branchName")), |
| | | privateSettlement ? request.subjectIdNumber().trim().toUpperCase() : "", privateSettlement ? certificateValidityType(request.subjectCertificateValidityType()) : "", privateSettlement ? certificateBeginDate(request.subjectCertificateBeginDate()) : "", privateSettlement ? certificateEndDate(request.subjectCertificateEndDate()) : "", privateSettlement ? request.contactMobile().trim() : "", |
| | | registrationAccountPictureId, settlementCardPictureId, settlementCertFrontPictureId, settlementCertBackPictureId, |
| | | certificateBeginDate(request.establishedDate()), normalizeRegisteredCapital(request.companyType(), request.registeredCapital()))); |
| | | applyOnboardingResult(applicationNo, bankCardId, result.accepted(), result.huifuId(), result.applyNo(), result.tokenNo(), result.code(), result.message()); |
| | | } catch (HuifuPaymentGatewayException exception) { |
| | | jdbc.update("update merchant_onboarding_application set reject_reason=? where application_no=? and status='pending'", "Huifu submission outcome is pending confirmation", applicationNo); |
| | | } |
| | | } |
| | | |
| | | private void submitIndividualOnboarding(String applicationNo, MerchantOnboardingRequest request) { |
| | | private void submitIndividualOnboarding(MerchantPrincipal principal, String applicationNo, Long bankCardId, MerchantOnboardingRequest request, Map<String, Object> branch) { |
| | | try { |
| | | HuifuIndividualOnboardingGateway.Result result = individualOnboardingGateway.create(new HuifuIndividualOnboardingCommand(applicationNo, request.subjectName().trim(), request.subjectIdNumber().trim().toUpperCase(), certificateValidityType(request.subjectCertificateValidityType()), certificateBeginDate(request.subjectCertificateBeginDate()), certificateEndDate(request.subjectCertificateEndDate()), request.contactMobile().trim(), clean(request.registeredAddress()))); |
| | | if (result.accepted() && !result.huifuId().isBlank()) jdbc.update("update merchant_onboarding_application set status='approved', provider_huifu_id=?, reviewed_at=now(), reject_reason=null where application_no=? and status='pending'", result.huifuId(), applicationNo); |
| | | else if (!result.accepted()) jdbc.update("update merchant_onboarding_application set status='rejected', reject_reason=?, reviewed_at=now() where application_no=? and status='pending'", providerReason(result), applicationNo); |
| | | else jdbc.update("update merchant_onboarding_application set reject_reason=? where application_no=? and status='pending'", "Huifu accepted the request but did not return huifu_id", applicationNo); |
| | | String legalFrontPictureId = pictureUploadGateway.uploadUrl(request.legalIdFrontUrl(), "F40"); |
| | | String legalBackPictureId = pictureUploadGateway.uploadUrl(request.legalIdBackUrl(), "F41"); |
| | | String settlementCardPictureId = pictureUploadGateway.uploadUrl(request.settlementCardFrontUrl(), "F13"); |
| | | HuifuIndividualOnboardingGateway.Result result = individualOnboardingGateway.create(new HuifuIndividualOnboardingCommand( |
| | | applicationNo, request.subjectName().trim(), request.subjectIdNumber().trim().toUpperCase(), certificateValidityType(request.subjectCertificateValidityType()), certificateBeginDate(request.subjectCertificateBeginDate()), certificateEndDate(request.subjectCertificateEndDate()), request.contactMobile().trim(), clean(request.registeredAddress()), request.administratorEmail().trim(), |
| | | mcc(principal.merchantId()), clean(request.registeredProvinceCode()), clean(request.registeredCityCode()), clean(request.registeredDistrictCode()), legalFrontPictureId, legalBackPictureId, |
| | | request.bankCardNumber().trim(), request.bankProvinceCode().trim(), request.bankCityCode().trim(), clean(branch.get("headBankCode")), settlementCardPictureId)); |
| | | applyOnboardingResult(applicationNo, bankCardId, result.accepted(), result.huifuId(), result.applyNo(), result.tokenNo(), result.code(), result.message()); |
| | | } catch (HuifuPaymentGatewayException exception) { |
| | | jdbc.update("update merchant_onboarding_application set reject_reason=? where application_no=? and status='pending'", "Huifu submission outcome is pending confirmation", applicationNo); |
| | | } |
| | | } |
| | | |
| | | private void applyOnboardingResult(String applicationNo, Long bankCardId, boolean accepted, String huifuId, String applyNo, String tokenNo, String code, String message) { |
| | | if (accepted) { |
| | | boolean completed = !blank(huifuId) && !blank(tokenNo); |
| | | jdbc.update("update merchant_onboarding_application set status='pending', provider_huifu_id=nullif(?,''), provider_apply_no=nullif(?,''), provider_response_code=?, provider_response_message=?, reviewed_at=null, reject_reason=null where application_no=?", clean(huifuId), clean(applyNo), clean(code), clean(message), applicationNo); |
| | | jdbc.update("update merchant_bank_card set status=?, provider_token_no=nullif(?,''), provider_apply_no=nullif(?,''), provider_response_code=?, provider_response_message=?, reviewed_at=?, reject_reason=null where id=?", completed ? "approved" : "pending", clean(tokenNo), clean(applyNo), clean(code), clean(message), completed ? LocalDateTime.now() : null, bankCardId); |
| | | if (completed) continuePostOnboardingProvisioning(applicationNo); |
| | | } else { |
| | | String reason = providerReason(code, message); |
| | | jdbc.update("update merchant_onboarding_application set status='rejected', provider_apply_no=nullif(?,''), provider_response_code=?, provider_response_message=?, reject_reason=?, reviewed_at=now() where application_no=?", clean(applyNo), clean(code), clean(message), reason, applicationNo); |
| | | jdbc.update("update merchant_bank_card set status='rejected', provider_apply_no=nullif(?,''), provider_response_code=?, provider_response_message=?, reject_reason=?, reviewed_at=now() where id=?", clean(applyNo), clean(code), clean(message), reason, bankCardId); |
| | | } |
| | | } |
| | | @Transactional(readOnly = true) |
| | |
| | | |
| | | private void requireApproved(MerchantPrincipal principal) { if (!"approved".equals(onboardingStatus(principal).get("status"))) throw new BusinessException(ErrorCode.CONFLICT, "商户进件尚未审核通过"); } |
| | | private void submitBankCardToHuifu(MerchantPrincipal principal, Map<String, Object> card, String cardNumber, Map<String, Object> branch) { |
| | | List<Map<String, Object>> onboarding = rows("select * from merchant_onboarding_application where merchant_id=? and store_id=? and status='approved' and provider_huifu_id is not null order by id desc limit 1", principal.merchantId(), principal.storeId()); |
| | | List<Map<String, Object>> onboarding = rows("select * from merchant_onboarding_application where merchant_id=? and store_id=? and initial_bank_card_id is not null and status='approved' and provider_huifu_id is not null order by id desc limit 1", principal.merchantId(), principal.storeId()); |
| | | if (onboarding.isEmpty()) throw new BusinessException(ErrorCode.CONFLICT, "Huifu merchant onboarding is not approved"); |
| | | Map<String, Object> subject = onboarding.get(0); |
| | | String bankCode = clean(branch.get("headBankCode")); |
| | |
| | | else jdbc.update("update merchant_bank_card set provider_apply_no=coalesce(nullif(?,''),provider_apply_no), provider_response_code=?, provider_response_message=? where id=? and status='pending'", applyNo, code, message, cards.get(0).get("id")); |
| | | return true; |
| | | } |
| | | @Transactional |
| | | @SuppressWarnings("unchecked") |
| | | public boolean handleOnboardingCallback(Map<String, Object> data) { |
| | | Object nested = data.get("data"); |
| | | if (nested instanceof String value && !value.isBlank()) { |
| | | try { data = com.alibaba.fastjson.JSON.parseObject(value, Map.class); } catch (Exception ignored) { } |
| | | } else if (nested instanceof Map<?, ?> value) data = (Map<String, Object>) value; |
| | | String requestNo = clean(data.get("req_seq_id")); |
| | | if (requestNo.isBlank()) return false; |
| | | List<Map<String, Object>> applications = rows("select * from merchant_onboarding_application where application_no=? limit 1 for update", requestNo); |
| | | if (applications.isEmpty()) return false; |
| | | Map<String, Object> application = applications.get(0); |
| | | String code = clean(data.get("sub_resp_code")); if (code.isBlank()) code = clean(data.get("resp_code")); |
| | | String message = clean(data.get("audit_desc")); if (message.isBlank()) message = clean(data.get("sub_resp_desc")); if (message.isBlank()) message = clean(data.get("resp_desc")); |
| | | String auditStatus = clean(data.get("audit_status")); |
| | | String huifuId = clean(data.get("huifu_id")); String applyNo = clean(data.get("apply_no")); String tokenNo = clean(data.get("token_no")); |
| | | Long bankCardId = application.get("initialBankCardId") instanceof Number number ? number.longValue() : null; |
| | | if ("Y".equalsIgnoreCase(auditStatus)) { |
| | | jdbc.update("update merchant_onboarding_application set status='pending', provider_huifu_id=coalesce(nullif(?,''),provider_huifu_id), provider_apply_no=coalesce(nullif(?,''),provider_apply_no), provider_response_code=?, provider_response_message=?, reject_reason=null, reviewed_at=null where application_no=?", huifuId, applyNo, code, message, requestNo); |
| | | if (bankCardId != null) jdbc.update("update merchant_bank_card set status=case when coalesce(nullif(?,''),provider_token_no) is null then status else 'approved' end, provider_token_no=coalesce(nullif(?,''),provider_token_no), provider_apply_no=coalesce(nullif(?,''),provider_apply_no), provider_response_code=?, provider_response_message=?, reject_reason=null where id=?", tokenNo, tokenNo, applyNo, code, message, bankCardId); |
| | | continuePostOnboardingProvisioning(requestNo); |
| | | } else if ("N".equalsIgnoreCase(auditStatus) || "F".equalsIgnoreCase(auditStatus) || !providerSuccess(code)) { |
| | | String reason = providerReason(code, message); |
| | | jdbc.update("update merchant_onboarding_application set status='rejected', provider_huifu_id=coalesce(nullif(?,''),provider_huifu_id), provider_apply_no=coalesce(nullif(?,''),provider_apply_no), provider_response_code=?, provider_response_message=?, reject_reason=?, reviewed_at=now() where application_no=?", huifuId, applyNo, code, message, reason, requestNo); |
| | | if (bankCardId != null) jdbc.update("update merchant_bank_card set status='rejected', provider_apply_no=coalesce(nullif(?,''),provider_apply_no), provider_response_code=?, provider_response_message=?, reject_reason=?, reviewed_at=now() where id=?", applyNo, code, message, reason, bankCardId); |
| | | } else { |
| | | jdbc.update("update merchant_onboarding_application set provider_huifu_id=coalesce(nullif(?,''),provider_huifu_id), provider_apply_no=coalesce(nullif(?,''),provider_apply_no), provider_response_code=?, provider_response_message=? where application_no=?", huifuId, applyNo, code, message, requestNo); |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | @Transactional |
| | | public boolean handleWechatConfigCallback(Map<String, Object> callback) { |
| | | Map<String, Object> data = callbackData(callback); |
| | | String requestNo = clean(data.get("req_seq_id")); |
| | | if (requestNo.isBlank()) return false; |
| | | List<Map<String, Object>> applications = rows("select * from merchant_onboarding_application where wechat_config_request_no=? limit 1 for update", requestNo); |
| | | if (applications.isEmpty()) return false; |
| | | Map<String, Object> application = applications.get(0); |
| | | String code = responseCode(data); |
| | | String message = responseMessage(data); |
| | | String configStatus = wechatAppletConfigStatus(data.get("result_list")); |
| | | if (!providerSuccess(code) || "F".equalsIgnoreCase(configStatus) || "B".equalsIgnoreCase(configStatus)) { |
| | | String reason = providerReason(code, message); |
| | | jdbc.update("update merchant_onboarding_application set status='pending', wechat_config_status='failed', wechat_config_response_code=?, wechat_config_response_message=?, reject_reason=?, reviewed_at=null where id=?", code, message, "微信商户配置失败:" + reason, application.get("id")); |
| | | } else { |
| | | jdbc.update("update merchant_onboarding_application set wechat_config_status='approved', wechat_config_response_code=?, wechat_config_response_message=?, reject_reason=null where id=?", code, message, application.get("id")); |
| | | continuePostOnboardingProvisioning(clean(application.get("applicationNo"))); |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | @Transactional |
| | | public boolean handleBusinessOpenCallback(Map<String, Object> callback) { |
| | | Map<String, Object> data = callbackData(callback); |
| | | String requestNo = clean(data.get("req_seq_id")); |
| | | if (requestNo.isBlank()) return false; |
| | | List<Map<String, Object>> applications = rows("select * from merchant_onboarding_application where business_open_request_no=? limit 1 for update", requestNo); |
| | | if (applications.isEmpty()) return false; |
| | | Map<String, Object> application = applications.get(0); |
| | | String code = responseCode(data); |
| | | String message = responseMessage(data); |
| | | String auditStatus = clean(data.get("audit_status")); |
| | | String businessStatus = wechatBusinessStatus(data); |
| | | String applyNo = clean(data.get("apply_no")); |
| | | boolean rejected = List.of("N", "F", "C").contains(auditStatus.toUpperCase()) || "F".equalsIgnoreCase(businessStatus) || !providerSuccess(code); |
| | | if (rejected) { |
| | | String reason = providerReason(code, message); |
| | | jdbc.update("update merchant_onboarding_application set status='pending', business_open_status='failed', business_open_apply_no=coalesce(nullif(?,''),business_open_apply_no), business_open_response_code=?, business_open_response_message=?, reject_reason=?, reviewed_at=null where id=?", applyNo, code, message, "商户业务开通失败:" + reason, application.get("id")); |
| | | } else if ("S".equalsIgnoreCase(businessStatus)) { |
| | | jdbc.update("update merchant_onboarding_application set business_open_status='approved', business_open_apply_no=coalesce(nullif(?,''),business_open_apply_no), business_open_response_code=?, business_open_response_message=?, reject_reason=null where id=?", applyNo, code, message, application.get("id")); |
| | | completePostOnboardingProvisioning(application.get("id")); |
| | | } else { |
| | | jdbc.update("update merchant_onboarding_application set business_open_status='processing', business_open_apply_no=coalesce(nullif(?,''),business_open_apply_no), business_open_response_code=?, business_open_response_message=? where id=?", applyNo, code, message, application.get("id")); |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | private void continuePostOnboardingProvisioning(String applicationNo) { |
| | | List<Map<String, Object>> applications = rows("select * from merchant_onboarding_application where application_no=? limit 1 for update", applicationNo); |
| | | if (applications.isEmpty()) return; |
| | | Map<String, Object> application = applications.get(0); |
| | | String huifuId = clean(application.get("providerHuifuId")); |
| | | if (huifuId.isBlank() || "rejected".equals(clean(application.get("status")))) return; |
| | | |
| | | String wechatStatus = clean(application.get("wechatConfigStatus")); |
| | | if (wechatStatus.isBlank() || "not_started".equals(wechatStatus) || "failed".equals(wechatStatus)) { |
| | | String requestNo = clean(application.get("wechatConfigRequestNo")); |
| | | if (requestNo.isBlank()) requestNo = provisioningRequestNo(applicationNo, "WC"); |
| | | jdbc.update("update merchant_onboarding_application set status='pending', wechat_config_status='processing', wechat_config_request_no=?, reject_reason=null, reviewed_at=null where id=?", requestNo, application.get("id")); |
| | | try { |
| | | HuifuMerchantWechatConfigGateway.Result result = merchantWechatConfigGateway.configure(requestNo, huifuId); |
| | | if (!result.accepted()) { |
| | | jdbc.update("update merchant_onboarding_application set wechat_config_status='failed', wechat_config_response_code=?, wechat_config_response_message=?, reject_reason=? where id=?", result.code(), result.message(), "微信商户配置失败:" + providerReason(result.code(), result.message()), application.get("id")); |
| | | return; |
| | | } |
| | | jdbc.update("update merchant_onboarding_application set wechat_config_status='approved', wechat_config_response_code=?, wechat_config_response_message=? where id=?", result.code(), result.message(), application.get("id")); |
| | | } catch (HuifuPaymentGatewayException exception) { |
| | | String status = exception.isOutcomeUnknown() ? "processing" : "failed"; |
| | | jdbc.update("update merchant_onboarding_application set wechat_config_status=?, reject_reason=? where id=?", status, exception.isOutcomeUnknown() ? "微信商户配置结果待确认" : "微信商户配置请求失败", application.get("id")); |
| | | return; |
| | | } catch (BusinessException exception) { |
| | | jdbc.update("update merchant_onboarding_application set wechat_config_status='failed', reject_reason=? where id=?", "微信商户配置失败:" + exception.getMessage(), application.get("id")); |
| | | return; |
| | | } |
| | | } else if (!"approved".equals(wechatStatus)) { |
| | | return; |
| | | } |
| | | |
| | | application = rows("select * from merchant_onboarding_application where id=? limit 1", application.get("id")).get(0); |
| | | String businessStatus = clean(application.get("businessOpenStatus")); |
| | | if (businessStatus.isBlank() || "not_started".equals(businessStatus) || "failed".equals(businessStatus)) { |
| | | String requestNo = clean(application.get("businessOpenRequestNo")); |
| | | if (requestNo.isBlank()) requestNo = provisioningRequestNo(applicationNo, "BO"); |
| | | String shortName = clean(application.get("merchantShortName")); |
| | | if (shortName.isBlank()) shortName = clean(application.get("merchantName")); |
| | | if (shortName.isBlank()) shortName = clean(application.get("subjectName")); |
| | | Long merchantId = ((Number) application.get("merchantId")).longValue(); |
| | | jdbc.update("update merchant_onboarding_application set status='pending', business_open_status='processing', business_open_request_no=?, reject_reason=null, reviewed_at=null where id=?", requestNo, application.get("id")); |
| | | try { |
| | | HuifuMerchantBusinessOpenGateway.Result result = merchantBusinessOpenGateway.open(requestNo, huifuId, shortName, mcc(merchantId)); |
| | | if (!result.accepted()) { |
| | | jdbc.update("update merchant_onboarding_application set business_open_status='failed', business_open_apply_no=nullif(?,''), business_open_response_code=?, business_open_response_message=?, reject_reason=? where id=?", clean(result.applyNo()), result.code(), result.message(), "商户业务开通失败:" + providerReason(result.code(), result.message()), application.get("id")); |
| | | return; |
| | | } |
| | | jdbc.update("update merchant_onboarding_application set business_open_status='processing', business_open_apply_no=nullif(?,''), business_open_response_code=?, business_open_response_message=? where id=?", clean(result.applyNo()), result.code(), result.message(), application.get("id")); |
| | | } catch (HuifuPaymentGatewayException exception) { |
| | | String status = exception.isOutcomeUnknown() ? "processing" : "failed"; |
| | | jdbc.update("update merchant_onboarding_application set business_open_status=?, reject_reason=? where id=?", status, exception.isOutcomeUnknown() ? "商户业务开通结果待确认" : "商户业务开通请求失败", application.get("id")); |
| | | } catch (BusinessException exception) { |
| | | jdbc.update("update merchant_onboarding_application set business_open_status='failed', reject_reason=? where id=?", "商户业务开通失败:" + exception.getMessage(), application.get("id")); |
| | | } |
| | | } else if ("approved".equals(businessStatus)) { |
| | | completePostOnboardingProvisioning(application.get("id")); |
| | | } |
| | | } |
| | | |
| | | private void completePostOnboardingProvisioning(Object applicationId) { |
| | | jdbc.update("update merchant_onboarding_application set status='approved', reject_reason=null, reviewed_at=now() where id=? and wechat_config_status='approved' and business_open_status='approved'", applicationId); |
| | | } |
| | | |
| | | @SuppressWarnings("unchecked") |
| | | private Map<String, Object> callbackData(Map<String, Object> callback) { |
| | | Object nested = callback.get("data"); |
| | | if (nested instanceof Map<?, ?> value) return (Map<String, Object>) value; |
| | | if (nested instanceof String value && !value.isBlank()) { |
| | | try { return com.alibaba.fastjson.JSON.parseObject(value, Map.class); } catch (Exception ignored) { } |
| | | } |
| | | return callback; |
| | | } |
| | | |
| | | @SuppressWarnings("unchecked") |
| | | private List<Map<String, Object>> callbackList(Object raw) { |
| | | if (raw instanceof List<?> values) return values.stream().filter(Map.class::isInstance).map(value -> (Map<String, Object>) value).toList(); |
| | | if (raw instanceof String value && !value.isBlank()) { |
| | | try { return (List<Map<String, Object>>) (List<?>) com.alibaba.fastjson.JSON.parseArray(value, Map.class); } catch (Exception ignored) { } |
| | | } |
| | | return List.of(); |
| | | } |
| | | |
| | | private String wechatAppletConfigStatus(Object raw) { |
| | | for (Map<String, Object> result : callbackList(raw)) if ("02".equals(clean(result.get("config_type")))) return clean(result.get("config_stat")); |
| | | return ""; |
| | | } |
| | | |
| | | private String wechatBusinessStatus(Map<String, Object> data) { |
| | | if (!"W".equalsIgnoreCase(clean(data.get("notify_type")))) return ""; |
| | | boolean found = false; |
| | | for (Map<String, Object> result : callbackList(data.get("reg_result_list"))) { |
| | | String status = clean(result.get("business_stat")); |
| | | if ("F".equalsIgnoreCase(status)) return "F"; |
| | | if ("S".equalsIgnoreCase(status)) found = true; |
| | | } |
| | | return found ? "S" : ""; |
| | | } |
| | | |
| | | private String responseCode(Map<String, Object> data) { String code = clean(data.get("sub_resp_code")); return code.isBlank() ? clean(data.get("resp_code")) : code; } |
| | | private String responseMessage(Map<String, Object> data) { String message = clean(data.get("audit_desc")); if (message.isBlank()) message = clean(data.get("sub_resp_desc")); return message.isBlank() ? clean(data.get("resp_desc")) : message; } |
| | | private String provisioningRequestNo(String applicationNo, String suffix) { String value = applicationNo + suffix; return value.length() <= 32 ? value : value.substring(0, 32); } |
| | | private void requireKey(String key) { if (key == null || key.isBlank()) throw new BusinessException(ErrorCode.BAD_REQUEST, "缺少幂等键"); } |
| | | private void store(MerchantPrincipal principal) { if (rows("select id from merchant_store where id = ? and merchant_id = ? and status = 1 and deleted = 0", principal.storeId(), principal.merchantId()).isEmpty()) throw new BusinessException(ErrorCode.FORBIDDEN, "当前门店不可操作"); } |
| | | private Map<String, Object> onboarding(Map<String, Object> row) { return Map.of("id", String.valueOf(row.get("id")), "applicationNo", row.get("applicationNo"), "status", row.get("status"), "subjectType", clean(row.get("subjectType")), "reason", clean(row.get("rejectReason")), "submittedAt", String.valueOf(row.get("createdAt"))); } |
| | |
| | | if (!blank(endDate) && clean(beginDate).compareTo(clean(endDate)) > 0) throw new BusinessException(ErrorCode.BAD_REQUEST, label + " end date must not precede its begin date"); |
| | | } |
| | | private boolean blank(String value) { return value == null || value.isBlank(); } |
| | | private String providerReason(HuifuIndividualOnboardingGateway.Result result) { return providerReason(result.code(), result.message()); } |
| | | private String providerReason(String code, String message) { String value = clean(message); return value.isEmpty() ? "Huifu onboarding rejected: " + clean(code) : value; } |
| | | private boolean providerSuccess(String code) { return "00000000".equals(code) || "10000".equals(code) || "0".equals(code) || "SUCCESS".equalsIgnoreCase(code); } |
| | | private boolean providerSuccess(String code) { return "00000000".equals(code) || "00000100".equals(code) || "10000".equals(code) || "0".equals(code) || "SUCCESS".equalsIgnoreCase(code); } |
| | | private String maskId(String id) { String value = clean(id); return value.length() < 8 ? "" : value.substring(0, 3) + "********" + value.substring(value.length() - 4); } |
| | | private String sha256(String value) { try { byte[] digest = MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8)); StringBuilder result = new StringBuilder(); for (byte valueByte : digest) result.append(String.format("%02x", valueByte)); return result.toString(); } catch (Exception exception) { throw new IllegalStateException("Unable to hash bank card", exception); } } |
| | | private String entType(String companyType) { |
| | | return switch (clean(companyType)) { |
| | | case "政府机构" -> "1"; case "国营企业" -> "2"; case "私营企业" -> "3"; case "外资企业" -> "4"; |
| | | case "个体工商户" -> "5"; case "其它组织" -> "6"; case "事业单位" -> "7"; case "业主委员会" -> "9"; |
| | | default -> throw new BusinessException(ErrorCode.BAD_REQUEST, "公司类型无效"); |
| | | }; |
| | | } |
| | | private String mcc(Long merchantId) { |
| | | List<Map<String, Object>> merchants = rows("select merchant_type_code, merchant_type from merchant where id=? and deleted=0 limit 1", merchantId); |
| | | if (merchants.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "商家不存在"); |
| | | String code = clean(merchants.get(0).get("merchantTypeCode")); |
| | | if (code.isBlank()) code = MerchantTypeCatalog.canonicalCode(clean(merchants.get(0).get("merchantType"))); |
| | | return MCC_BY_MERCHANT_TYPE.getOrDefault(code, "7399"); |
| | | } |
| | | private String normalizeRegisteredCapital(String companyType, String value) { |
| | | String text = clean(value); boolean required = List.of("国营企业", "私营企业", "外资企业", "其它组织", "事业单位").contains(clean(companyType)); |
| | | if (text.isBlank()) { if (required) throw new BusinessException(ErrorCode.BAD_REQUEST, "请输入注册资本"); return ""; } |
| | | try { BigDecimal capital = new BigDecimal(text); if (capital.signum() < 0) throw new NumberFormatException(); return capital.setScale(2, RoundingMode.HALF_UP).toPlainString(); } |
| | | catch (NumberFormatException exception) { throw new BusinessException(ErrorCode.BAD_REQUEST, "注册资本请输入数字,单位为万元"); } |
| | | } |
| | | } |
| | |
| | | if (!settlementService.handleBankCardCallback(data)) throw new BusinessException(ErrorCode.NOT_FOUND, "Unknown Huifu merchant business callback"); |
| | | return ResponseEntity.ok("RECV_ORD_ID_" + no); |
| | | } |
| | | |
| | | @PostMapping(value = "/merchant-onboarding-notify", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.TEXT_PLAIN_VALUE) |
| | | public ResponseEntity<String> merchantOnboardingNotify(@RequestParam Map<String, String> form) { |
| | | String payload = form.get("resp_data"), signature = form.get("sign"); |
| | | if (!signatureService.verifyCallbackPayload(payload, signature)) throw new BusinessException(ErrorCode.FORBIDDEN, "Invalid Huifu merchant onboarding callback signature"); |
| | | @SuppressWarnings("unchecked") Map<String, Object> data = JSON.parseObject(payload, Map.class); |
| | | String no = data.get("req_seq_id") == null ? "" : String.valueOf(data.get("req_seq_id")).trim(); |
| | | if (!settlementService.handleOnboardingCallback(data)) throw new BusinessException(ErrorCode.NOT_FOUND, "Unknown Huifu merchant onboarding callback"); |
| | | return ResponseEntity.ok("RECV_ORD_ID_" + no); |
| | | } |
| | | |
| | | @PostMapping(value = "/merchant-wechat-config-notify", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.TEXT_PLAIN_VALUE) |
| | | public ResponseEntity<String> merchantWechatConfigNotify(@RequestParam Map<String, String> form) { |
| | | Map<String, Object> data = verifiedData(form, "WeChat merchant configuration"); |
| | | if (!settlementService.handleWechatConfigCallback(data)) throw new BusinessException(ErrorCode.NOT_FOUND, "Unknown Huifu WeChat merchant configuration callback"); |
| | | return ResponseEntity.ok("RECV_ORD_ID_" + acknowledgementId(data)); |
| | | } |
| | | |
| | | @PostMapping(value = "/merchant-business-open-notify", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.TEXT_PLAIN_VALUE) |
| | | public ResponseEntity<String> merchantBusinessOpenNotify(@RequestParam Map<String, String> form) { |
| | | Map<String, Object> data = verifiedData(form, "merchant business opening"); |
| | | if (!settlementService.handleBusinessOpenCallback(data)) throw new BusinessException(ErrorCode.NOT_FOUND, "Unknown Huifu merchant business opening callback"); |
| | | return ResponseEntity.ok("RECV_ORD_ID_" + acknowledgementId(data)); |
| | | } |
| | | |
| | | @SuppressWarnings("unchecked") |
| | | private Map<String, Object> verifiedData(Map<String, String> form, String operation) { |
| | | String payload = form.get("resp_data"), signature = form.get("sign"); |
| | | if (!signatureService.verifyCallbackPayload(payload, signature)) throw new BusinessException(ErrorCode.FORBIDDEN, "Invalid Huifu " + operation + " callback signature"); |
| | | Map<String, Object> callback = JSON.parseObject(payload, Map.class); |
| | | Object nested = callback.get("data"); |
| | | if (nested instanceof Map<?, ?> value) return (Map<String, Object>) value; |
| | | if (nested instanceof String value && !value.isBlank()) return JSON.parseObject(value, Map.class); |
| | | return callback; |
| | | } |
| | | |
| | | private String acknowledgementId(Map<String, Object> data) { |
| | | Object value = data.get("ord_id"); |
| | | if (value == null || String.valueOf(value).isBlank()) value = data.get("req_seq_id"); |
| | | return value == null ? "" : String.valueOf(value).trim(); |
| | | } |
| | | } |
| | |
| | | public record HuifuEnterpriseOnboardingCommand( |
| | | String applicationNo, |
| | | String companyName, |
| | | String shortName, |
| | | String creditCode, |
| | | String licenseValidityType, |
| | | String licenseBeginDate, |
| | |
| | | String legalValidityType, |
| | | String legalBeginDate, |
| | | String legalEndDate, |
| | | String legalAddress, |
| | | String contactName, |
| | | String contactMobile |
| | | String contactMobile, |
| | | String contactEmail, |
| | | String entType, |
| | | String mcc, |
| | | String licensePictureId, |
| | | String legalFrontPictureId, |
| | | String legalBackPictureId, |
| | | String cardType, |
| | | String cardName, |
| | | String cardNumber, |
| | | String bankProvinceCode, |
| | | String bankCityCode, |
| | | String bankCode, |
| | | String bankBranchCode, |
| | | String bankBranchName, |
| | | String cardCertificateNumber, |
| | | String cardCertificateValidityType, |
| | | String cardCertificateBeginDate, |
| | | String cardCertificateEndDate, |
| | | String cardMobile, |
| | | String registrationAccountPictureId, |
| | | String settlementCardFrontPictureId, |
| | | String settlementCertificateFrontPictureId, |
| | | String settlementCertificateBackPictureId, |
| | | String foundDate, |
| | | String registeredCapital |
| | | ) {} |
| | |
| | | import com.huifu.bspay.sdk.opps.client.BasePayClient; |
| | | import com.huifu.bspay.sdk.opps.core.BasePay; |
| | | import com.huifu.bspay.sdk.opps.core.config.MerConfig; |
| | | import com.huifu.bspay.sdk.opps.core.request.V2UserBasicdataEntRequest; |
| | | import com.talentcard.server.common.BusinessException; |
| | | import com.talentcard.server.common.ErrorCode; |
| | | import java.time.LocalDate; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | @Service |
| | | public class HuifuEnterpriseOnboardingGateway { |
| | | private static final Object SDK_LOCK = new Object(); |
| | | private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("yyyyMMdd"); |
| | | private static final Logger log = LoggerFactory.getLogger(HuifuEnterpriseOnboardingGateway.class); |
| | | private final HuifuPaymentProperties properties; |
| | | |
| | | public HuifuEnterpriseOnboardingGateway(HuifuPaymentProperties properties) { this.properties = properties; } |
| | | |
| | | public Result create(HuifuEnterpriseOnboardingCommand command) { |
| | | ensureConfigured(); |
| | | V2UserBasicdataEntRequest request = new V2UserBasicdataEntRequest(); |
| | | request.setReqSeqId(command.applicationNo()); |
| | | request.setReqDate(DATE.format(LocalDate.now())); |
| | | request.setRegName(command.companyName()); |
| | | request.setLicenseCode(command.creditCode()); |
| | | request.setLicenseValidityType(command.licenseValidityType()); |
| | | request.setLicenseBeginDate(command.licenseBeginDate()); |
| | | if (!blank(command.licenseEndDate())) request.setLicenseEndDate(command.licenseEndDate()); |
| | | request.setRegProvId(command.registeredProvinceCode()); |
| | | request.setRegAreaId(command.registeredCityCode()); |
| | | request.setRegDistrictId(command.registeredDistrictCode()); |
| | | request.setRegDetail(command.registeredAddress()); |
| | | request.setLegalName(command.legalName()); |
| | | request.setLegalCertType("00"); |
| | | request.setLegalCertNo(command.legalIdNumber()); |
| | | request.setLegalCertValidityType(command.legalValidityType()); |
| | | request.setLegalCertBeginDate(command.legalBeginDate()); |
| | | if (!blank(command.legalEndDate())) request.setLegalCertEndDate(command.legalEndDate()); |
| | | request.setLegalCertNationality("CHN"); |
| | | request.setContactName(command.contactName()); |
| | | request.setContactMobile(command.contactMobile()); |
| | | Map<String, Object> card = new LinkedHashMap<>(); |
| | | card.put("branch_code", command.bankBranchCode()); |
| | | card.put("branch_name", command.bankBranchName()); |
| | | card.put("card_no", command.cardNumber()); |
| | | card.put("card_name", command.cardName()); |
| | | card.put("card_type", command.cardType()); |
| | | card.put("default_cash_flag", "1"); |
| | | putIfPresent(card, "prov_id", command.bankProvinceCode()); |
| | | putIfPresent(card, "area_id", command.bankCityCode()); |
| | | putIfPresent(card, "bank_code", command.bankCode()); |
| | | if ("1".equals(command.cardType()) || "2".equals(command.cardType())) { |
| | | card.put("cert_type", "00"); |
| | | card.put("cert_no", command.cardCertificateNumber()); |
| | | card.put("cert_validity_type", command.cardCertificateValidityType()); |
| | | card.put("cert_begin_date", command.cardCertificateBeginDate()); |
| | | putIfPresent(card, "cert_end_date", command.cardCertificateEndDate()); |
| | | putIfPresent(card, "mp", command.cardMobile()); |
| | | } |
| | | |
| | | Map<String, Object> cash = new LinkedHashMap<>(); |
| | | cash.put("cash_type", "DM"); |
| | | cash.put("fix_amt", "0.00"); |
| | | cash.put("fee_rate", ""); |
| | | |
| | | Map<String, Object> request = new LinkedHashMap<>(); |
| | | request.put("req_seq_id", command.applicationNo()); |
| | | request.put("req_date", LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE)); |
| | | request.put("upper_huifu_id", properties.getOnboardingUpperHuifuId().trim()); |
| | | request.put("reg_name", command.companyName()); |
| | | request.put("short_name", command.shortName()); |
| | | request.put("receipt_name", command.shortName()); |
| | | request.put("ent_type", command.entType()); |
| | | request.put("mcc", command.mcc()); |
| | | request.put("busi_type", "1"); |
| | | request.put("scene_type", "ONLINE"); |
| | | request.put("license_pic", command.licensePictureId()); |
| | | request.put("license_code", command.creditCode()); |
| | | request.put("license_validity_type", command.licenseValidityType()); |
| | | request.put("license_begin_date", command.licenseBeginDate()); |
| | | putIfPresent(request, "license_end_date", command.licenseEndDate()); |
| | | request.put("found_date", command.foundDate()); |
| | | putIfPresent(request, "reg_capital", command.registeredCapital()); |
| | | request.put("reg_prov_id", command.registeredProvinceCode()); |
| | | request.put("reg_area_id", command.registeredCityCode()); |
| | | request.put("reg_district_id", command.registeredDistrictCode()); |
| | | request.put("reg_detail", command.registeredAddress()); |
| | | request.put("prov_id", command.registeredProvinceCode()); |
| | | request.put("area_id", command.registeredCityCode()); |
| | | request.put("district_id", command.registeredDistrictCode()); |
| | | request.put("detail_addr", command.registeredAddress()); |
| | | request.put("legal_name", command.legalName()); |
| | | request.put("legal_cert_type", "00"); |
| | | request.put("legal_cert_no", command.legalIdNumber()); |
| | | request.put("legal_cert_validity_type", command.legalValidityType()); |
| | | request.put("legal_cert_begin_date", command.legalBeginDate()); |
| | | putIfPresent(request, "legal_cert_end_date", command.legalEndDate()); |
| | | request.put("legal_addr", command.legalAddress()); |
| | | request.put("legal_cert_front_pic", command.legalFrontPictureId()); |
| | | request.put("legal_cert_back_pic", command.legalBackPictureId()); |
| | | request.put("contact_name", command.contactName()); |
| | | request.put("contact_mobile_no", command.contactMobile()); |
| | | request.put("contact_email", command.contactEmail()); |
| | | request.put("login_name", loginName(command.applicationNo())); |
| | | request.put("card_info", JSON.toJSONString(card)); |
| | | putIfPresent(request, "reg_acct_pic", command.registrationAccountPictureId()); |
| | | putIfPresent(request, "settle_card_front_pic", command.settlementCardFrontPictureId()); |
| | | putIfPresent(request, "settle_cert_front_pic", command.settlementCertificateFrontPictureId()); |
| | | putIfPresent(request, "settle_cert_back_pic", command.settlementCertificateBackPictureId()); |
| | | request.put("cash_config", JSON.toJSONString(List.of(cash))); |
| | | putIfPresent(request, "async_return_url", properties.getMerchantOnboardingNotifyUrl()); |
| | | try { |
| | | Map<String, Object> response; |
| | | synchronized (SDK_LOCK) { |
| | | initializeSdk(); |
| | | response = BasePayClient.request(request, false); |
| | | Map<String, Object> response = BasePayClient.request(request, "v2.merchant.basicdata.ent", false); |
| | | log.info("Huifu enterprise merchant onboarding response: applicationNo={}, upperHuifuId={}, response={}", command.applicationNo(), properties.getOnboardingUpperHuifuId(), JSON.toJSONString(response)); |
| | | return Result.from(response); |
| | | } |
| | | return Result.from(response); |
| | | } catch (BusinessException exception) { |
| | | throw exception; |
| | | } catch (Exception exception) { |
| | | throw new HuifuPaymentGatewayException("Huifu enterprise onboarding request failed", outcomeUnknown(exception), exception); |
| | | log.error("Huifu enterprise merchant onboarding request error: applicationNo={}, upperHuifuId={}", command.applicationNo(), properties.getOnboardingUpperHuifuId(), exception); |
| | | throw new HuifuPaymentGatewayException("Huifu enterprise merchant onboarding request failed", outcomeUnknown(exception), exception); |
| | | } |
| | | } |
| | | |
| | | private void ensureConfigured() { |
| | | if (!properties.isEnabled()) throw new BusinessException(ErrorCode.CONFLICT, "Huifu payment is disabled"); |
| | | if (blank(properties.getSystemId()) || blank(properties.getProductId()) || blank(properties.getPrivateKey()) || blank(properties.getPlatformPublicKey())) { |
| | | throw new BusinessException(ErrorCode.CONFLICT, "Huifu onboarding configuration is incomplete"); |
| | | } |
| | | if (blank(properties.getSystemId()) || blank(properties.getProductId()) || blank(properties.getOnboardingUpperHuifuId()) || blank(properties.getPrivateKey()) || blank(properties.getPlatformPublicKey())) throw new BusinessException(ErrorCode.CONFLICT, "Huifu merchant onboarding configuration is incomplete"); |
| | | } |
| | | |
| | | private void initializeSdk() throws Exception { |
| | |
| | | BasePay.initWithMerConfig(config); |
| | | } |
| | | |
| | | private boolean blank(String value) { return value == null || value.isBlank(); } |
| | | private static String loginName(String applicationNo) { return ("tc" + applicationNo).replaceAll("[^0-9A-Za-z_]", ""); } |
| | | private static void putIfPresent(Map<String, Object> request, String key, String value) { if (!blank(value)) request.put(key, value.trim()); } |
| | | private static boolean blank(String value) { return value == null || value.isBlank(); } |
| | | private boolean outcomeUnknown(Exception exception) { String message = exception.getMessage() == null ? "" : exception.getMessage().toLowerCase(); return message.contains("timeout") || message.contains("timed out") || message.contains("no response"); } |
| | | |
| | | @SuppressWarnings("unchecked") |
| | | public record Result(boolean accepted, String huifuId, String code, String message) { |
| | | public record Result(boolean accepted, String huifuId, String applyNo, String tokenNo, String code, String message) { |
| | | static Result from(Map<String, Object> response) { |
| | | Object raw = response.get("data"); |
| | | Map<String, Object> data = raw instanceof Map<?, ?> value ? (Map<String, Object>) value : raw instanceof String json && !json.isBlank() ? JSON.parseObject(json, Map.class) : response; |
| | | String code = value(data, "sub_resp_code", "resp_code", "code"); |
| | | String message = value(data, "sub_resp_desc", "resp_desc", "message"); |
| | | String huifuId = value(data, "huifu_id", "huifuId"); |
| | | return new Result("00000000".equals(code) || "10000".equals(code) || "0".equals(code) || "SUCCESS".equalsIgnoreCase(code), huifuId, code, message); |
| | | return new Result(success(code), value(data, "huifu_id", "huifuId"), value(data, "apply_no", "applyNo"), value(data, "token_no", "tokenNo"), code, value(data, "sub_resp_desc", "resp_desc", "message")); |
| | | } |
| | | private static boolean success(String code) { return "00000000".equals(code) || "00000100".equals(code) || "10000".equals(code) || "0".equals(code) || "SUCCESS".equalsIgnoreCase(code); } |
| | | private static String value(Map<String, Object> data, String... keys) { for (String key : keys) if (data.get(key) != null) return String.valueOf(data.get(key)).trim(); return ""; } |
| | | } |
| | | } |
| | |
| | | String certificateBeginDate, |
| | | String certificateEndDate, |
| | | String mobileNo, |
| | | String address |
| | | String address, |
| | | String email, |
| | | String mcc, |
| | | String provinceCode, |
| | | String cityCode, |
| | | String districtCode, |
| | | String legalFrontPictureId, |
| | | String legalBackPictureId, |
| | | String cardNumber, |
| | | String bankProvinceCode, |
| | | String bankCityCode, |
| | | String bankCode, |
| | | String settlementCardFrontPictureId |
| | | ) {} |
| | |
| | | import com.talentcard.server.common.ErrorCode; |
| | | import java.time.LocalDate; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | @Service |
| | | public class HuifuIndividualOnboardingGateway { |
| | | private static final Object SDK_LOCK = new Object(); |
| | | private static final Logger log = LoggerFactory.getLogger(HuifuIndividualOnboardingGateway.class); |
| | | private final HuifuPaymentProperties properties; |
| | | |
| | | public HuifuIndividualOnboardingGateway(HuifuPaymentProperties properties) { this.properties = properties; } |
| | | |
| | | public Result create(HuifuIndividualOnboardingCommand command) { |
| | | ensureConfigured(); |
| | | Map<String, Object> request = new java.util.LinkedHashMap<>(); |
| | | Map<String, Object> card = new LinkedHashMap<>(); |
| | | card.put("card_type", "1"); |
| | | card.put("card_name", command.name()); |
| | | card.put("card_no", command.cardNumber()); |
| | | card.put("prov_id", command.bankProvinceCode()); |
| | | card.put("area_id", command.bankCityCode()); |
| | | card.put("bank_code", command.bankCode()); |
| | | card.put("cert_type", "00"); |
| | | card.put("cert_no", command.idCardNo()); |
| | | card.put("cert_validity_type", command.certificateValidityType()); |
| | | card.put("cert_begin_date", command.certificateBeginDate()); |
| | | if (!blank(command.certificateEndDate())) card.put("cert_end_date", command.certificateEndDate()); |
| | | card.put("mp", command.mobileNo()); |
| | | card.put("is_settle_default", "Y"); |
| | | |
| | | Map<String, Object> cash = new LinkedHashMap<>(); |
| | | cash.put("cash_type", "DM"); |
| | | cash.put("fix_amt", "0.00"); |
| | | cash.put("fee_rate", ""); |
| | | |
| | | Map<String, Object> request = new LinkedHashMap<>(); |
| | | request.put("req_seq_id", command.applicationNo()); |
| | | request.put("req_date", LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE)); |
| | | request.put("name", command.name()); |
| | | request.put("cert_type", "00"); |
| | | request.put("cert_no", command.idCardNo()); |
| | | request.put("cert_validity_type", command.certificateValidityType()); |
| | | request.put("cert_begin_date", command.certificateBeginDate()); |
| | | if (!blank(command.certificateEndDate())) request.put("cert_end_date", command.certificateEndDate()); |
| | | request.put("mobile_no", command.mobileNo()); |
| | | if (!blank(command.address())) request.put("address", command.address()); |
| | | request.put("upper_huifu_id", properties.getOnboardingUpperHuifuId().trim()); |
| | | request.put("reg_name", command.name()); |
| | | request.put("short_name", command.name()); |
| | | request.put("mcc", command.mcc()); |
| | | request.put("scene_type", "ONLINE"); |
| | | request.put("prov_id", command.provinceCode()); |
| | | request.put("area_id", command.cityCode()); |
| | | request.put("district_id", command.districtCode()); |
| | | request.put("detail_addr", command.address()); |
| | | request.put("legal_cert_no", command.idCardNo()); |
| | | request.put("legal_cert_validity_type", command.certificateValidityType()); |
| | | request.put("legal_cert_begin_date", command.certificateBeginDate()); |
| | | putIfPresent(request, "legal_cert_end_date", command.certificateEndDate()); |
| | | request.put("legal_addr", command.address()); |
| | | request.put("legal_cert_front_pic", command.legalFrontPictureId()); |
| | | request.put("legal_cert_back_pic", command.legalBackPictureId()); |
| | | request.put("contact_name", command.name()); |
| | | request.put("contact_mobile_no", command.mobileNo()); |
| | | request.put("contact_email", command.email()); |
| | | request.put("login_name", loginName(command.applicationNo())); |
| | | request.put("card_info", JSON.toJSONString(card)); |
| | | request.put("settle_card_front_pic", command.settlementCardFrontPictureId()); |
| | | request.put("cash_config", JSON.toJSONString(List.of(cash))); |
| | | putIfPresent(request, "async_return_url", properties.getMerchantOnboardingNotifyUrl()); |
| | | try { |
| | | Map<String, Object> response; |
| | | synchronized (SDK_LOCK) { |
| | | initializeSdk(); |
| | | response = BasePayClient.request(request, "v2.user.basicdata.indv", false); |
| | | Map<String, Object> response = BasePayClient.request(request, "v2.merchant.basicdata.indv", false); |
| | | log.info("Huifu individual merchant onboarding response: applicationNo={}, upperHuifuId={}, response={}", command.applicationNo(), properties.getOnboardingUpperHuifuId(), JSON.toJSONString(response)); |
| | | return Result.from(response); |
| | | } |
| | | return Result.from(response); |
| | | } catch (BusinessException exception) { |
| | | throw exception; |
| | | } catch (Exception exception) { |
| | | throw new HuifuPaymentGatewayException("Huifu individual onboarding request failed", outcomeUnknown(exception), exception); |
| | | log.error("Huifu individual merchant onboarding request error: applicationNo={}, upperHuifuId={}", command.applicationNo(), properties.getOnboardingUpperHuifuId(), exception); |
| | | throw new HuifuPaymentGatewayException("Huifu individual merchant onboarding request failed", outcomeUnknown(exception), exception); |
| | | } |
| | | } |
| | | |
| | | private void ensureConfigured() { |
| | | if (!properties.isEnabled()) throw new BusinessException(ErrorCode.CONFLICT, "Huifu payment is disabled"); |
| | | if (blank(properties.getSystemId()) || blank(properties.getProductId()) || blank(properties.getPrivateKey()) || blank(properties.getPlatformPublicKey())) throw new BusinessException(ErrorCode.CONFLICT, "Huifu onboarding configuration is incomplete"); |
| | | if (blank(properties.getSystemId()) || blank(properties.getProductId()) || blank(properties.getOnboardingUpperHuifuId()) || blank(properties.getPrivateKey()) || blank(properties.getPlatformPublicKey())) throw new BusinessException(ErrorCode.CONFLICT, "Huifu merchant onboarding configuration is incomplete"); |
| | | } |
| | | |
| | | private void initializeSdk() throws Exception { |
| | |
| | | BasePay.initWithMerConfig(config); |
| | | } |
| | | |
| | | private boolean blank(String value) { return value == null || value.isBlank(); } |
| | | private static String loginName(String applicationNo) { return ("tc" + applicationNo).replaceAll("[^0-9A-Za-z_]", ""); } |
| | | private static void putIfPresent(Map<String, Object> request, String key, String value) { if (!blank(value)) request.put(key, value.trim()); } |
| | | private static boolean blank(String value) { return value == null || value.isBlank(); } |
| | | private boolean outcomeUnknown(Exception exception) { String message = exception.getMessage() == null ? "" : exception.getMessage().toLowerCase(); return message.contains("timeout") || message.contains("timed out") || message.contains("no response"); } |
| | | |
| | | @SuppressWarnings("unchecked") |
| | | public record Result(boolean accepted, String huifuId, String code, String message) { |
| | | public record Result(boolean accepted, String huifuId, String applyNo, String tokenNo, String code, String message) { |
| | | static Result from(Map<String, Object> response) { |
| | | Object raw = response.get("data"); |
| | | Map<String, Object> data = raw instanceof Map<?, ?> value ? (Map<String, Object>) value : raw instanceof String json && !json.isBlank() ? JSON.parseObject(json, Map.class) : response; |
| | | String code = value(data, "sub_resp_code", "resp_code", "code"); |
| | | String message = value(data, "sub_resp_desc", "resp_desc", "message"); |
| | | String huifuId = value(data, "huifu_id", "huifuId"); |
| | | return new Result("00000000".equals(code) || "10000".equals(code) || "0".equals(code) || "SUCCESS".equalsIgnoreCase(code), huifuId, code, message); |
| | | return new Result(success(code), value(data, "huifu_id", "huifuId"), value(data, "apply_no", "applyNo"), value(data, "token_no", "tokenNo"), code, value(data, "sub_resp_desc", "resp_desc", "message")); |
| | | } |
| | | |
| | | private static boolean success(String code) { return "00000000".equals(code) || "00000100".equals(code) || "10000".equals(code) || "0".equals(code) || "SUCCESS".equalsIgnoreCase(code); } |
| | | private static String value(Map<String, Object> data, String... keys) { for (String key : keys) if (data.get(key) != null) return String.valueOf(data.get(key)).trim(); return ""; } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.talentcard.server.business.payment.huifu; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.huifu.bspay.sdk.opps.client.BasePayClient; |
| | | import com.huifu.bspay.sdk.opps.core.BasePay; |
| | | import com.huifu.bspay.sdk.opps.core.config.MerConfig; |
| | | import com.talentcard.server.common.BusinessException; |
| | | import com.talentcard.server.common.ErrorCode; |
| | | import java.time.LocalDate; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | @Service |
| | | public class HuifuMerchantBusinessOpenGateway { |
| | | private static final Object SDK_LOCK = new Object(); |
| | | private static final Logger log = LoggerFactory.getLogger(HuifuMerchantBusinessOpenGateway.class); |
| | | private final HuifuPaymentProperties properties; |
| | | |
| | | public HuifuMerchantBusinessOpenGateway(HuifuPaymentProperties properties) { this.properties = properties; } |
| | | |
| | | public Result open(String requestNo, String huifuId, String shortName, String mcc) { |
| | | ensureConfigured(); |
| | | Map<String, Object> wechat = new LinkedHashMap<>(); |
| | | wechat.put("pay_scene", "5"); |
| | | wechat.put("mcc", mcc); |
| | | wechat.put("fee_rate", properties.getMerchantWechatFeeRate().trim()); |
| | | wechat.put("is_check_real_name", "N"); |
| | | |
| | | Map<String, Object> media = new LinkedHashMap<>(); |
| | | media.put("media_type", "S3"); |
| | | media.put("media_name", properties.getMerchantOnlineMediaName().trim()); |
| | | |
| | | Map<String, Object> agreement = new LinkedHashMap<>(); |
| | | agreement.put("agreement_type", "3"); |
| | | agreement.put("agreement_url", properties.getMerchantAgreementUrl().trim()); |
| | | |
| | | Map<String, Object> request = new LinkedHashMap<>(); |
| | | request.put("req_seq_id", requestNo); |
| | | request.put("req_date", LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE)); |
| | | request.put("huifu_id", huifuId); |
| | | request.put("upper_huifu_id", properties.getOnboardingUpperHuifuId().trim()); |
| | | request.put("short_name", shortName); |
| | | request.put("wx_conf_list", JSON.toJSONString(List.of(wechat))); |
| | | request.put("online_busi_type", properties.getMerchantOnlineBusinessType().trim()); |
| | | request.put("online_media_info_list", JSON.toJSONString(List.of(media))); |
| | | request.put("agreement_info", JSON.toJSONString(agreement)); |
| | | putIfPresent(request, "async_return_url", properties.getMerchantBusinessOpenNotifyUrl()); |
| | | putIfPresent(request, "busi_async_return_url", properties.getMerchantBusinessOpenNotifyUrl()); |
| | | try { |
| | | synchronized (SDK_LOCK) { |
| | | initializeSdk(); |
| | | Map<String, Object> response = BasePayClient.request(request, "v2.merchant.busi.open", false); |
| | | log.info("Huifu merchant business open response: requestNo={}, huifuId={}, response={}", requestNo, huifuId, JSON.toJSONString(response)); |
| | | return Result.from(response); |
| | | } |
| | | } catch (BusinessException exception) { |
| | | throw exception; |
| | | } catch (Exception exception) { |
| | | log.error("Huifu merchant business open request error: requestNo={}, huifuId={}", requestNo, huifuId, exception); |
| | | throw new HuifuPaymentGatewayException("Huifu merchant business open request failed", outcomeUnknown(exception), exception); |
| | | } |
| | | } |
| | | |
| | | private void ensureConfigured() { |
| | | if (!properties.isEnabled()) throw new BusinessException(ErrorCode.CONFLICT, "Huifu payment is disabled"); |
| | | if (blank(properties.getSystemId()) || blank(properties.getProductId()) || blank(properties.getOnboardingUpperHuifuId()) || blank(properties.getMerchantWechatFeeRate()) || blank(properties.getMerchantOnlineBusinessType()) || blank(properties.getMerchantOnlineMediaName()) || blank(properties.getMerchantAgreementUrl()) || blank(properties.getPrivateKey()) || blank(properties.getPlatformPublicKey())) throw new BusinessException(ErrorCode.CONFLICT, "Huifu merchant business opening configuration is incomplete"); |
| | | } |
| | | |
| | | private void initializeSdk() throws Exception { |
| | | BasePay.debug = false; |
| | | BasePay.prodMode = "prod".equalsIgnoreCase(properties.getEnvironment()) ? BasePay.MODE_PROD : BasePay.MODE_TEST; |
| | | MerConfig config = new MerConfig(); |
| | | config.setSysId(properties.getSystemId().trim()); |
| | | config.setProductId(properties.getProductId().trim()); |
| | | config.setRsaPrivateKey(properties.getPrivateKey().trim()); |
| | | config.setRsaPublicKey(properties.getPlatformPublicKey().trim()); |
| | | BasePay.initWithMerConfig(config); |
| | | } |
| | | |
| | | private static void putIfPresent(Map<String, Object> request, String key, String value) { if (!blank(value)) request.put(key, value.trim()); } |
| | | private static boolean blank(String value) { return value == null || value.isBlank(); } |
| | | private boolean outcomeUnknown(Exception exception) { String message = exception.getMessage() == null ? "" : exception.getMessage().toLowerCase(); return message.contains("timeout") || message.contains("timed out") || message.contains("no response"); } |
| | | |
| | | @SuppressWarnings("unchecked") |
| | | public record Result(boolean accepted, String applyNo, String code, String message) { |
| | | static Result from(Map<String, Object> response) { |
| | | Object raw = response.get("data"); |
| | | Map<String, Object> data = raw instanceof Map<?, ?> value ? (Map<String, Object>) value : raw instanceof String json && !json.isBlank() ? JSON.parseObject(json, Map.class) : response; |
| | | String code = value(data, "sub_resp_code", "resp_code", "code"); |
| | | return new Result(success(code), value(data, "apply_no", "applyNo"), code, value(data, "sub_resp_desc", "resp_desc", "message")); |
| | | } |
| | | private static boolean success(String code) { return "00000000".equals(code) || "00000100".equals(code) || "10000".equals(code) || "0".equals(code) || "SUCCESS".equalsIgnoreCase(code); } |
| | | private static String value(Map<String, Object> data, String... keys) { for (String key : keys) if (data.get(key) != null) return String.valueOf(data.get(key)).trim(); return ""; } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.talentcard.server.business.payment.huifu; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.huifu.bspay.sdk.opps.client.BasePayClient; |
| | | import com.huifu.bspay.sdk.opps.core.BasePay; |
| | | import com.huifu.bspay.sdk.opps.core.config.MerConfig; |
| | | import com.talentcard.server.common.BusinessException; |
| | | import com.talentcard.server.common.ErrorCode; |
| | | import java.time.LocalDate; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.Map; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | @Service |
| | | public class HuifuMerchantWechatConfigGateway { |
| | | private static final Object SDK_LOCK = new Object(); |
| | | private static final Logger log = LoggerFactory.getLogger(HuifuMerchantWechatConfigGateway.class); |
| | | private final HuifuPaymentProperties properties; |
| | | |
| | | public HuifuMerchantWechatConfigGateway(HuifuPaymentProperties properties) { this.properties = properties; } |
| | | |
| | | public Result configure(String requestNo, String huifuId) { |
| | | ensureConfigured(); |
| | | Map<String, Object> request = new LinkedHashMap<>(); |
| | | request.put("req_seq_id", requestNo); |
| | | request.put("req_date", LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE)); |
| | | request.put("huifu_id", huifuId); |
| | | request.put("fee_type", "01"); |
| | | request.put("wx_applet_app_id", properties.getMiniProgramAppId().trim()); |
| | | putIfPresent(request, "async_return_url", properties.getMerchantWechatConfigNotifyUrl()); |
| | | try { |
| | | synchronized (SDK_LOCK) { |
| | | initializeSdk(); |
| | | Map<String, Object> response = BasePayClient.request(request, "v2.merchant.busi.config", false); |
| | | log.info("Huifu merchant WeChat config response: requestNo={}, huifuId={}, response={}", requestNo, huifuId, JSON.toJSONString(response)); |
| | | return Result.from(response); |
| | | } |
| | | } catch (BusinessException exception) { |
| | | throw exception; |
| | | } catch (Exception exception) { |
| | | log.error("Huifu merchant WeChat config request error: requestNo={}, huifuId={}", requestNo, huifuId, exception); |
| | | throw new HuifuPaymentGatewayException("Huifu merchant WeChat config request failed", outcomeUnknown(exception), exception); |
| | | } |
| | | } |
| | | |
| | | private void ensureConfigured() { |
| | | if (!properties.isEnabled()) throw new BusinessException(ErrorCode.CONFLICT, "Huifu payment is disabled"); |
| | | if (blank(properties.getSystemId()) || blank(properties.getProductId()) || blank(properties.getMiniProgramAppId()) || blank(properties.getPrivateKey()) || blank(properties.getPlatformPublicKey())) throw new BusinessException(ErrorCode.CONFLICT, "Huifu merchant WeChat configuration is incomplete"); |
| | | } |
| | | |
| | | private void initializeSdk() throws Exception { |
| | | BasePay.debug = false; |
| | | BasePay.prodMode = "prod".equalsIgnoreCase(properties.getEnvironment()) ? BasePay.MODE_PROD : BasePay.MODE_TEST; |
| | | MerConfig config = new MerConfig(); |
| | | config.setSysId(properties.getSystemId().trim()); |
| | | config.setProductId(properties.getProductId().trim()); |
| | | config.setRsaPrivateKey(properties.getPrivateKey().trim()); |
| | | config.setRsaPublicKey(properties.getPlatformPublicKey().trim()); |
| | | BasePay.initWithMerConfig(config); |
| | | } |
| | | |
| | | private static void putIfPresent(Map<String, Object> request, String key, String value) { if (!blank(value)) request.put(key, value.trim()); } |
| | | private static boolean blank(String value) { return value == null || value.isBlank(); } |
| | | private boolean outcomeUnknown(Exception exception) { String message = exception.getMessage() == null ? "" : exception.getMessage().toLowerCase(); return message.contains("timeout") || message.contains("timed out") || message.contains("no response"); } |
| | | |
| | | @SuppressWarnings("unchecked") |
| | | public record Result(boolean accepted, String code, String message) { |
| | | static Result from(Map<String, Object> response) { |
| | | Object raw = response.get("data"); |
| | | Map<String, Object> data = raw instanceof Map<?, ?> value ? (Map<String, Object>) value : raw instanceof String json && !json.isBlank() ? JSON.parseObject(json, Map.class) : response; |
| | | String code = value(data, "sub_resp_code", "resp_code", "code"); |
| | | return new Result(success(code), code, value(data, "sub_resp_desc", "resp_desc", "message")); |
| | | } |
| | | private static boolean success(String code) { return "00000000".equals(code) || "10000".equals(code) || "0".equals(code) || "SUCCESS".equalsIgnoreCase(code); } |
| | | private static String value(Map<String, Object> data, String... keys) { for (String key : keys) if (data.get(key) != null) return String.valueOf(data.get(key)).trim(); return ""; } |
| | | } |
| | | } |
| | |
| | | private String withdrawalProductId = ""; |
| | | private String withdrawalNotifyUrl = ""; |
| | | private String merchantBusinessNotifyUrl = ""; |
| | | private String merchantOnboardingNotifyUrl = ""; |
| | | private String merchantWechatConfigNotifyUrl = ""; |
| | | private String merchantBusinessOpenNotifyUrl = ""; |
| | | private String onboardingUpperHuifuId = ""; |
| | | private String merchantWechatFeeRate = "0.60"; |
| | | private String merchantOnlineBusinessType = "H5311L"; |
| | | private String merchantOnlineMediaName = "东区人才卡"; |
| | | private String merchantAgreementUrl = "https://cloudpnrcdn.oss-cn-shanghai.aliyuncs.com/opps/api/prod/download_file/PaymentServiceAgreement.htm"; |
| | | private String withdrawalIntoAccountDateType = "DM"; |
| | | private boolean delayAccount; |
| | | |
| | |
| | | return withdrawalNotifyUrl == null ? "" : withdrawalNotifyUrl.replace("/withdrawal-notify", "/merchant-business-notify"); |
| | | } |
| | | public void setMerchantBusinessNotifyUrl(String merchantBusinessNotifyUrl) { this.merchantBusinessNotifyUrl = merchantBusinessNotifyUrl; } |
| | | public String getMerchantOnboardingNotifyUrl() { |
| | | if (merchantOnboardingNotifyUrl != null && !merchantOnboardingNotifyUrl.isBlank()) return merchantOnboardingNotifyUrl; |
| | | return withdrawalNotifyUrl == null ? "" : withdrawalNotifyUrl.replace("/withdrawal-notify", "/merchant-onboarding-notify"); |
| | | } |
| | | public void setMerchantOnboardingNotifyUrl(String merchantOnboardingNotifyUrl) { this.merchantOnboardingNotifyUrl = merchantOnboardingNotifyUrl; } |
| | | public String getMerchantWechatConfigNotifyUrl() { |
| | | if (merchantWechatConfigNotifyUrl != null && !merchantWechatConfigNotifyUrl.isBlank()) return merchantWechatConfigNotifyUrl; |
| | | return callbackUrl("merchant-wechat-config-notify"); |
| | | } |
| | | public void setMerchantWechatConfigNotifyUrl(String merchantWechatConfigNotifyUrl) { this.merchantWechatConfigNotifyUrl = merchantWechatConfigNotifyUrl; } |
| | | public String getMerchantBusinessOpenNotifyUrl() { |
| | | if (merchantBusinessOpenNotifyUrl != null && !merchantBusinessOpenNotifyUrl.isBlank()) return merchantBusinessOpenNotifyUrl; |
| | | return callbackUrl("merchant-business-open-notify"); |
| | | } |
| | | public void setMerchantBusinessOpenNotifyUrl(String merchantBusinessOpenNotifyUrl) { this.merchantBusinessOpenNotifyUrl = merchantBusinessOpenNotifyUrl; } |
| | | public String getOnboardingUpperHuifuId() { |
| | | if (onboardingUpperHuifuId != null && !onboardingUpperHuifuId.isBlank()) return onboardingUpperHuifuId; |
| | | return merchantId; |
| | | } |
| | | public void setOnboardingUpperHuifuId(String onboardingUpperHuifuId) { this.onboardingUpperHuifuId = onboardingUpperHuifuId; } |
| | | public String getMerchantWechatFeeRate() { return merchantWechatFeeRate; } |
| | | public void setMerchantWechatFeeRate(String merchantWechatFeeRate) { this.merchantWechatFeeRate = merchantWechatFeeRate; } |
| | | public String getMerchantOnlineBusinessType() { return merchantOnlineBusinessType; } |
| | | public void setMerchantOnlineBusinessType(String merchantOnlineBusinessType) { this.merchantOnlineBusinessType = merchantOnlineBusinessType; } |
| | | public String getMerchantOnlineMediaName() { return merchantOnlineMediaName; } |
| | | public void setMerchantOnlineMediaName(String merchantOnlineMediaName) { this.merchantOnlineMediaName = merchantOnlineMediaName; } |
| | | public String getMerchantAgreementUrl() { return merchantAgreementUrl; } |
| | | public void setMerchantAgreementUrl(String merchantAgreementUrl) { this.merchantAgreementUrl = merchantAgreementUrl; } |
| | | public String getWithdrawalIntoAccountDateType() { return withdrawalIntoAccountDateType; } |
| | | public void setWithdrawalIntoAccountDateType(String withdrawalIntoAccountDateType) { this.withdrawalIntoAccountDateType = withdrawalIntoAccountDateType; } |
| | | public boolean isDelayAccount() { return delayAccount; } |
| | | public void setDelayAccount(boolean delayAccount) { this.delayAccount = delayAccount; } |
| | | |
| | | private String callbackUrl(String endpoint) { |
| | | if (withdrawalNotifyUrl == null || withdrawalNotifyUrl.isBlank()) return ""; |
| | | int slash = withdrawalNotifyUrl.lastIndexOf('/'); |
| | | return slash < 0 ? "" : withdrawalNotifyUrl.substring(0, slash + 1) + endpoint; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.talentcard.server.business.payment.huifu; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.talentcard.server.business.BusinessIdGenerator; |
| | | import com.talentcard.server.common.BusinessException; |
| | | import com.talentcard.server.common.ErrorCode; |
| | | import java.net.URI; |
| | | import java.nio.file.Files; |
| | | import java.nio.file.Path; |
| | | import java.time.LocalDate; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.Map; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.core.io.FileSystemResource; |
| | | import org.springframework.http.HttpEntity; |
| | | import org.springframework.http.HttpHeaders; |
| | | import org.springframework.http.MediaType; |
| | | import org.springframework.util.LinkedMultiValueMap; |
| | | import org.springframework.util.MultiValueMap; |
| | | import org.springframework.web.client.RestClientException; |
| | | import org.springframework.web.client.RestTemplate; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | @Service |
| | | public class HuifuPictureUploadGateway { |
| | | private final HuifuPaymentProperties properties; |
| | | private final Path uploadDirectory; |
| | | private final RestTemplate restTemplate = new RestTemplate(); |
| | | |
| | | public HuifuPictureUploadGateway( |
| | | HuifuPaymentProperties properties, |
| | | @Value("${app.upload.directory:uploads}") String uploadDirectory |
| | | ) { |
| | | this.properties = properties; |
| | | this.uploadDirectory = Path.of(uploadDirectory).toAbsolutePath().normalize(); |
| | | } |
| | | |
| | | @SuppressWarnings("unchecked") |
| | | public String uploadUrl(String fileUrl, String fileType) { |
| | | ensureConfigured(); |
| | | if (blank(fileType) || !fileType.matches("F\\d{2,3}")) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "汇付图片类型无效"); |
| | | } |
| | | Path file = resolveUploadedFile(fileUrl); |
| | | Map<String, Object> data = Map.of( |
| | | "req_seq_id", BusinessIdGenerator.next("HP"), |
| | | "req_date", LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE), |
| | | "file_type", fileType |
| | | ); |
| | | MultiValueMap<String, Object> form = new LinkedMultiValueMap<>(); |
| | | form.add("sys_id", properties.getSystemId().trim()); |
| | | form.add("product_id", properties.getProductId().trim()); |
| | | form.add("data", JSON.toJSONString(data)); |
| | | form.add("file", new FileSystemResource(file)); |
| | | HttpHeaders headers = new HttpHeaders(); |
| | | headers.setContentType(MediaType.MULTIPART_FORM_DATA); |
| | | try { |
| | | Map<String, Object> response = restTemplate.postForObject(endpoint(), new HttpEntity<>(form, headers), Map.class); |
| | | if (response == null) throw new BusinessException(ErrorCode.INTERNAL_ERROR, "汇付图片上传没有返回结果"); |
| | | Object raw = response.get("data"); |
| | | Map<String, Object> result = raw instanceof Map<?, ?> value |
| | | ? (Map<String, Object>) value |
| | | : raw instanceof String json && !json.isBlank() |
| | | ? JSON.parseObject(json, Map.class) |
| | | : response; |
| | | String code = value(result, "resp_code", "sub_resp_code", "code"); |
| | | String fileId = value(result, "file_id", "fileId"); |
| | | if (!providerSuccess(code) || fileId.isBlank()) { |
| | | String message = value(result, "resp_desc", "sub_resp_desc", "message"); |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, |
| | | message.isBlank() ? "汇付图片上传失败:" + code : message); |
| | | } |
| | | return fileId; |
| | | } catch (BusinessException exception) { |
| | | throw exception; |
| | | } catch (RestClientException exception) { |
| | | throw new HuifuPaymentGatewayException( |
| | | "Huifu onboarding picture upload failed", |
| | | outcomeUnknown(exception), |
| | | exception |
| | | ); |
| | | } |
| | | } |
| | | |
| | | private Path resolveUploadedFile(String fileUrl) { |
| | | if (blank(fileUrl)) throw new BusinessException(ErrorCode.BAD_REQUEST, "进件图片不能为空"); |
| | | try { |
| | | String path = fileUrl.trim(); |
| | | if (path.startsWith("http://") || path.startsWith("https://")) path = URI.create(path).getPath(); |
| | | if (!path.startsWith("/uploads/")) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "进件图片必须是本系统上传的图片"); |
| | | } |
| | | String filename = Path.of(path).getFileName().toString(); |
| | | Path resolved = uploadDirectory.resolve(filename).normalize(); |
| | | if (!resolved.startsWith(uploadDirectory) || !Files.isRegularFile(resolved)) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "进件图片不存在,请重新上传"); |
| | | } |
| | | String extension = extension(filename); |
| | | if (!("jpg".equals(extension) || "jpeg".equals(extension) || "png".equals(extension) || "bmp".equals(extension))) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "汇付仅支持 JPG、BMP、PNG 图片"); |
| | | } |
| | | if (Files.size(resolved) > 2L * 1024 * 1024) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "进件图片不能超过2MB"); |
| | | } |
| | | return resolved; |
| | | } catch (BusinessException exception) { |
| | | throw exception; |
| | | } catch (Exception exception) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "进件图片地址无效"); |
| | | } |
| | | } |
| | | |
| | | private void ensureConfigured() { |
| | | if (!properties.isEnabled()) throw new BusinessException(ErrorCode.CONFLICT, "汇付支付尚未启用"); |
| | | if (blank(properties.getSystemId()) || blank(properties.getProductId())) { |
| | | throw new BusinessException(ErrorCode.CONFLICT, "汇付图片上传配置不完整"); |
| | | } |
| | | } |
| | | |
| | | private String endpoint() { |
| | | return ("prod".equalsIgnoreCase(properties.getEnvironment()) |
| | | ? "https://api.huifu.com" : "https://opps-stbmertest.testpnr.com") |
| | | + "/v2/supplementary/picture"; |
| | | } |
| | | |
| | | private static boolean providerSuccess(String code) { |
| | | return "00000000".equals(code) || "10000".equals(code) || "0".equals(code) |
| | | || "SUCCESS".equalsIgnoreCase(code); |
| | | } |
| | | |
| | | private static String value(Map<String, Object> data, String... keys) { |
| | | for (String key : keys) if (data.get(key) != null) return String.valueOf(data.get(key)).trim(); |
| | | return ""; |
| | | } |
| | | |
| | | private static boolean blank(String value) { return value == null || value.isBlank(); } |
| | | |
| | | private static String extension(String filename) { |
| | | int dot = filename.lastIndexOf('.'); |
| | | return dot < 0 ? "" : filename.substring(dot + 1).toLowerCase(); |
| | | } |
| | | |
| | | private static boolean outcomeUnknown(Exception exception) { |
| | | String message = exception.getMessage() == null ? "" : exception.getMessage().toLowerCase(); |
| | | return message.contains("timeout") || message.contains("timed out") || message.contains("no response"); |
| | | } |
| | | } |
| | |
| | | |
| | | private String rightsDescription(Map<String, Object> right, Map<String, Object> merchant) { |
| | | if (right != null) { |
| | | String detail = text(right.get("detail"), ""); |
| | | String detail = configuredRightText(right); |
| | | if (!detail.isBlank()) return detail; |
| | | String tag = text(right.get("tag"), ""); |
| | | if (!tag.isBlank()) return tag; |
| | | } |
| | | return text(merchant.get("rightsText"), "持人才卡可领取普通厅电影兑换券,到店出示券码后按影城规则兑换。具体适用影片、场次及补差金额以影城现场公示为准。"); |
| | | } |
| | | |
| | | private String configuredRightText(Map<String, Object> right) { |
| | | String detail = text(right.get("detail"), ""); |
| | | if (!detail.isBlank()) return detail; |
| | | detail = text(right.get("benefitDetail"), ""); |
| | | if (!detail.isBlank()) return detail; |
| | | detail = text(right.get("rightsText"), ""); |
| | | if (!detail.isBlank()) return detail; |
| | | return text(right.get("benefit_text"), ""); |
| | | } |
| | | |
| | | private String applicableCards(List<Map<String, Object>> rights, Map<String, Object> right, Map<String, Object> ticketPackage) { |
| | |
| | | if (normalizedLevel.isBlank()) return null; |
| | | for (Map<String, Object> right : rights) { |
| | | if (!booleanValue(right.get("enabled"))) continue; |
| | | if (normalizedLevel.equals(normalizedCardLevel(text(right.get("level"), "")))) return right; |
| | | String configuredLevel = text(right.get("level"), text(right.get("cardLevel"), text(right.get("card_level"), ""))); |
| | | if (normalizedLevel.equals(normalizedCardLevel(configuredLevel))) return right; |
| | | } |
| | | return null; |
| | | } |
| | |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.PathVariable; |
| | | import org.springframework.web.bind.annotation.PostMapping; |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.RequestBody; |
| | | import org.springframework.web.bind.annotation.RequestHeader; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | |
| | | } |
| | | |
| | | @GetMapping("/{merchantId}") |
| | | public ApiResponse<Map<String, Object>> detail(@PathVariable Long merchantId) { |
| | | return ApiResponse.success(service.detail(merchantId)); |
| | | public ApiResponse<Map<String, Object>> detail( |
| | | @PathVariable Long merchantId, |
| | | @RequestParam(required = false) Long talentCardId |
| | | ) { |
| | | return ApiResponse.success(service.detail(merchantId, talentCardId)); |
| | | } |
| | | |
| | | @PostMapping("/{merchantId}/registrations") |
| | |
| | | |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.ArrayList; |
| | | import java.util.Locale; |
| | | import java.util.Map; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.alibaba.fastjson.TypeReference; |
| | | import com.talentcard.server.business.BusinessIdGenerator; |
| | | import com.talentcard.server.business.DatabaseRows; |
| | | import com.talentcard.server.common.BusinessException; |
| | |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> detail(Long merchantId) { |
| | | public Map<String, Object> detail(Long merchantId, Long talentCardId) { |
| | | Map<String, Object> merchant = fuelMerchant(merchantId); |
| | | Map<String, Object> benefit = one(""" |
| | | select id, talent_levels, discount_text, oil_types, single_limit_text, annual_limit_text, |
| | |
| | | where merchant_id = ? and deleted = 0 |
| | | order by id asc limit 1 |
| | | """, merchantId); |
| | | String cardLevel = talentCardId == null ? "" : talentCardLevel(talentCardId); |
| | | String sourceAreaType = talentCardId == null ? "" : talentSourceAreaType(talentCardId); |
| | | Map<String, Object> right = matchingRight(parseRights(merchant.get("merchantRightsConfig")), cardLevel, sourceAreaType); |
| | | boolean available = number(merchant.get("status")) == 1 && number(benefit.get("status")) == 1; |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("merchantId", merchant.get("id")); |
| | |
| | | result.put("available", available); |
| | | result.put("serviceLabel", text(merchant.get("serviceLabel"), "请咨询商户")); |
| | | result.put("discountText", text(benefit.get("discountText"), "请咨询商户")); |
| | | result.put("applicableCards", text(benefit.get("talentLevels"), "请咨询商户")); |
| | | result.put("rightsTag", text(right == null ? null : right.get("tag"), "")); |
| | | result.put("applicableCards", text(right == null ? null : right.get("tag"), text(benefit.get("talentLevels"), "请咨询商户"))); |
| | | result.put("groupBuyingEnabled", false); |
| | | result.put("usageInstructions", text(benefit.get("usageInstructions"), "请咨询商户")); |
| | | result.put("rightsText", text(benefit.get("ruleText"), "请咨询商户")); |
| | | String configuredDetail = text(right == null ? null : right.get("detail"), ""); |
| | | result.put("usageInstructions", text(configuredDetail, text(benefit.get("usageInstructions"), "请咨询商户"))); |
| | | result.put("rightsText", text(configuredDetail, text(benefit.get("ruleText"), "请咨询商户"))); |
| | | result.put("benefit", benefitView(benefit)); |
| | | result.put("heroImages", query(""" |
| | | select id, image_url, sort_no |
| | |
| | | |
| | | private Map<String, Object> fuelMerchant(Long merchantId) { |
| | | return one(""" |
| | | select id, merchant_name, merchant_type_code as merchant_type, contact_mobile, address, cover_url, service_label, status, latitude, longitude |
| | | select id, merchant_name, merchant_type_code as merchant_type, contact_mobile, address, cover_url, service_label, |
| | | merchant_rights_config, status, latitude, longitude |
| | | from merchant |
| | | where id = ? and merchant_type_code = 'fuel' and deleted = 0 |
| | | """, merchantId); |
| | | } |
| | | |
| | | private Map<String, Object> matchingRight(List<Map<String, Object>> rights, String cardLevel, String sourceAreaType) { |
| | | if (rights.isEmpty() || cardLevel.isBlank()) return null; |
| | | for (String candidate : rightCandidates(cardLevel, sourceAreaType)) { |
| | | for (Map<String, Object> right : rights) { |
| | | if (booleanValue(right.get("enabled")) && candidate.equals(text(right.get("level"), ""))) return right; |
| | | } |
| | | } |
| | | String level = normalizedCardLevel(cardLevel); |
| | | for (Map<String, Object> right : rights) { |
| | | if (!booleanValue(right.get("enabled"))) continue; |
| | | if (!level.equals(normalizedCardLevel(text(right.get("level"), "")))) continue; |
| | | if (!"EXCELLENT".equals(level) || !areaSpecificExcellentLevel(text(right.get("level"), ""))) return right; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private List<String> rightCandidates(String cardLevel, String sourceAreaType) { |
| | | String level = normalizedCardLevel(cardLevel); |
| | | if (!"EXCELLENT".equals(level)) return List.of(cardLevel); |
| | | String area = text(sourceAreaType, "").toUpperCase(Locale.ROOT); |
| | | if (area.contains("LOCAL") || area.contains("本地")) return List.of("卓越人才卡(本地)", "卓越人才卡"); |
| | | if (area.contains("OUTSIDE") || area.contains("外地")) return List.of("卓越人才卡(外地)", "卓越人才卡"); |
| | | return List.of("卓越人才卡"); |
| | | } |
| | | |
| | | private boolean areaSpecificExcellentLevel(String level) { |
| | | String value = text(level, "").toUpperCase(Locale.ROOT); |
| | | return value.contains("LOCAL") || value.contains("OUTSIDE") || value.contains("本地") || value.contains("外地"); |
| | | } |
| | | |
| | | private List<Map<String, Object>> parseRights(Object value) { |
| | | if (value instanceof List<?> list) { |
| | | List<Map<String, Object>> result = new ArrayList<>(); |
| | | for (Object item : list) if (item instanceof Map<?, ?> map) result.add(copyMap(map)); |
| | | return result; |
| | | } |
| | | String raw = text(value, ""); |
| | | if (raw.isBlank()) return List.of(); |
| | | try { |
| | | List<Map<String, Object>> parsed = JSON.parseObject(raw, new TypeReference<List<Map<String, Object>>>() {}); |
| | | return parsed == null ? List.of() : parsed.stream().map(this::copyMap).toList(); |
| | | } catch (Exception ignored) { |
| | | return List.of(); |
| | | } |
| | | } |
| | | |
| | | private Map<String, Object> copyMap(Map<?, ?> source) { |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | for (Map.Entry<?, ?> entry : source.entrySet()) result.put(String.valueOf(entry.getKey()), entry.getValue()); |
| | | return result; |
| | | } |
| | | |
| | | private String talentCardLevel(Long talentCardId) { |
| | | List<Map<String, Object>> cards = query("select card_level from talent_card where id = ? and deleted = 0 limit 1", talentCardId); |
| | | return cards.isEmpty() ? "" : text(cards.get(0).get("cardLevel"), ""); |
| | | } |
| | | |
| | | private String talentSourceAreaType(Long talentCardId) { |
| | | List<Map<String, Object>> cards = query(""" |
| | | select u.source_area_type |
| | | from talent_card c join talent_user u on u.id = c.talent_id and u.deleted = 0 |
| | | where c.id = ? and c.deleted = 0 |
| | | limit 1 |
| | | """, talentCardId); |
| | | return cards.isEmpty() ? "" : text(cards.get(0).get("sourceAreaType"), ""); |
| | | } |
| | | |
| | | private String normalizedCardLevel(String value) { |
| | | String raw = text(value, ""); |
| | | String upper = raw.toUpperCase(Locale.ROOT); |
| | | if (upper.contains("EXCELLENT") || raw.contains("卓越")) return "EXCELLENT"; |
| | | if (upper.contains("OUTSTANDING") || raw.contains("杰出")) return "OUTSTANDING"; |
| | | if (upper.contains("ELITE") || raw.contains("菁英") || raw.contains("精英")) return "ELITE"; |
| | | return upper; |
| | | } |
| | | |
| | | private boolean booleanValue(Object value) { |
| | | if (value instanceof Boolean bool) return bool; |
| | | if (value instanceof Number number) return number.intValue() != 0; |
| | | String text = text(value, "").toLowerCase(Locale.ROOT); |
| | | return "true".equals(text) || "1".equals(text) || "yes".equals(text); |
| | | } |
| | | |
| | | private Map<String, Object> benefitView(Map<String, Object> benefit) { |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("benefitId", benefit.get("id")); |
| | |
| | | if (couponAvailable) tags.add("券"); |
| | | return tags; |
| | | } |
| | | // The travel module displays the calculated discount, not the merchant's |
| | | // service label, so search results must use the same source. |
| | | if ("scenic".equals(merchantType)) { |
| | | if (!discountText.isBlank()) tags.add(discountText); |
| | | return tags; |
| | | } |
| | | |
| | | String cardLevel = activeProductCardLevel(talentId); |
| | | String sourceAreaType = activeTalentSourceAreaType(talentId); |
| | |
| | | notify-url: ${HUIFU_NOTIFY_URL:http://221.182.45.100:11451/api/payments/huifu/notify} |
| | | withdrawal-product-id: ${HUIFU_WITHDRAWAL_PRODUCT_ID:KAZX} |
| | | withdrawal-notify-url: ${HUIFU_WITHDRAWAL_NOTIFY_URL:http://221.182.45.100:11451/api/payments/huifu/withdrawal-notify} |
| | | merchant-onboarding-notify-url: ${HUIFU_MERCHANT_ONBOARDING_NOTIFY_URL:http://221.182.45.100:11451/api/payments/huifu/merchant-onboarding-notify} |
| | | merchant-wechat-config-notify-url: ${HUIFU_MERCHANT_WECHAT_CONFIG_NOTIFY_URL:http://221.182.45.100:11451/api/payments/huifu/merchant-wechat-config-notify} |
| | | merchant-business-open-notify-url: ${HUIFU_MERCHANT_BUSINESS_OPEN_NOTIFY_URL:http://221.182.45.100:11451/api/payments/huifu/merchant-business-open-notify} |
| | | onboarding-upper-huifu-id: ${HUIFU_ONBOARDING_UPPER_HUIFU_ID:6666000220591054} |
| | | merchant-wechat-fee-rate: ${HUIFU_MERCHANT_WECHAT_FEE_RATE:0.60} |
| | | merchant-online-business-type: ${HUIFU_MERCHANT_ONLINE_BUSINESS_TYPE:H5311L} |
| | | merchant-online-media-name: ${HUIFU_MERCHANT_ONLINE_MEDIA_NAME:东区人才卡} |
| | | merchant-agreement-url: ${HUIFU_MERCHANT_AGREEMENT_URL:https://cloudpnrcdn.oss-cn-shanghai.aliyuncs.com/opps/api/prod/download_file/PaymentServiceAgreement.htm} |
| | | delay-account: ${HUIFU_DELAY_ACCOUNT:false} |
| | | wechat: |
| | | mini-program: |
| New file |
| | |
| | | DELIMITER $$ |
| | | |
| | | DROP PROCEDURE IF EXISTS add_merchant_onboarding_provider_tracking$$ |
| | | CREATE PROCEDURE add_merchant_onboarding_provider_tracking() |
| | | BEGIN |
| | | DECLARE col_exists INT DEFAULT 0; |
| | | SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'merchant_onboarding_application' AND COLUMN_NAME = 'initial_bank_card_id'; |
| | | IF col_exists = 0 THEN ALTER TABLE merchant_onboarding_application ADD COLUMN initial_bank_card_id BIGINT DEFAULT NULL AFTER provider_huifu_id; END IF; |
| | | SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'merchant_onboarding_application' AND COLUMN_NAME = 'provider_apply_no'; |
| | | IF col_exists = 0 THEN ALTER TABLE merchant_onboarding_application ADD COLUMN provider_apply_no VARCHAR(32) DEFAULT NULL AFTER initial_bank_card_id; END IF; |
| | | SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'merchant_onboarding_application' AND COLUMN_NAME = 'provider_response_code'; |
| | | IF col_exists = 0 THEN ALTER TABLE merchant_onboarding_application ADD COLUMN provider_response_code VARCHAR(32) DEFAULT NULL AFTER provider_apply_no; END IF; |
| | | SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'merchant_onboarding_application' AND COLUMN_NAME = 'provider_response_message'; |
| | | IF col_exists = 0 THEN ALTER TABLE merchant_onboarding_application ADD COLUMN provider_response_message VARCHAR(512) DEFAULT NULL AFTER provider_response_code; END IF; |
| | | END$$ |
| | | |
| | | DELIMITER ; |
| | | CALL add_merchant_onboarding_provider_tracking(); |
| | | DROP PROCEDURE IF EXISTS add_merchant_onboarding_provider_tracking; |
| New file |
| | |
| | | -- Store the extended merchant onboarding profile fields. |
| | | DELIMITER $$ |
| | | DROP PROCEDURE IF EXISTS add_merchant_onboarding_profile_columns$$ |
| | | CREATE PROCEDURE add_merchant_onboarding_profile_columns() |
| | | BEGIN |
| | | DECLARE col_exists INT DEFAULT 0; |
| | | SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'merchant_onboarding_application' AND COLUMN_NAME = 'merchant_name'; |
| | | IF col_exists = 0 THEN ALTER TABLE merchant_onboarding_application ADD COLUMN merchant_name VARCHAR(128) DEFAULT NULL AFTER registered_address; END IF; |
| | | SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'merchant_onboarding_application' AND COLUMN_NAME = 'merchant_short_name'; |
| | | IF col_exists = 0 THEN ALTER TABLE merchant_onboarding_application ADD COLUMN merchant_short_name VARCHAR(128) DEFAULT NULL AFTER merchant_name; END IF; |
| | | SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'merchant_onboarding_application' AND COLUMN_NAME = 'company_type'; |
| | | IF col_exists = 0 THEN ALTER TABLE merchant_onboarding_application ADD COLUMN company_type VARCHAR(64) DEFAULT NULL AFTER merchant_short_name; END IF; |
| | | SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'merchant_onboarding_application' AND COLUMN_NAME = 'business_license_url'; |
| | | IF col_exists = 0 THEN ALTER TABLE merchant_onboarding_application ADD COLUMN business_license_url VARCHAR(512) DEFAULT NULL AFTER company_type; END IF; |
| | | SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'merchant_onboarding_application' AND COLUMN_NAME = 'established_date'; |
| | | IF col_exists = 0 THEN ALTER TABLE merchant_onboarding_application ADD COLUMN established_date CHAR(10) DEFAULT NULL AFTER business_license_url; END IF; |
| | | SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'merchant_onboarding_application' AND COLUMN_NAME = 'registered_capital'; |
| | | IF col_exists = 0 THEN ALTER TABLE merchant_onboarding_application ADD COLUMN registered_capital VARCHAR(64) DEFAULT NULL AFTER established_date; END IF; |
| | | SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'merchant_onboarding_application' AND COLUMN_NAME = 'legal_id_front_url'; |
| | | IF col_exists = 0 THEN ALTER TABLE merchant_onboarding_application ADD COLUMN legal_id_front_url VARCHAR(512) DEFAULT NULL AFTER registered_capital; END IF; |
| | | SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'merchant_onboarding_application' AND COLUMN_NAME = 'legal_id_back_url'; |
| | | IF col_exists = 0 THEN ALTER TABLE merchant_onboarding_application ADD COLUMN legal_id_back_url VARCHAR(512) DEFAULT NULL AFTER legal_id_front_url; END IF; |
| | | SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'merchant_onboarding_application' AND COLUMN_NAME = 'legal_certificate_address'; |
| | | IF col_exists = 0 THEN ALTER TABLE merchant_onboarding_application ADD COLUMN legal_certificate_address VARCHAR(255) DEFAULT NULL AFTER legal_id_back_url; END IF; |
| | | SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'merchant_onboarding_application' AND COLUMN_NAME = 'administrator_email'; |
| | | IF col_exists = 0 THEN ALTER TABLE merchant_onboarding_application ADD COLUMN administrator_email VARCHAR(128) DEFAULT NULL AFTER legal_certificate_address; END IF; |
| | | END$$ |
| | | DELIMITER ; |
| | | CALL add_merchant_onboarding_profile_columns(); |
| | | DROP PROCEDURE IF EXISTS add_merchant_onboarding_profile_columns; |
| New file |
| | |
| | | -- Track WeChat merchant configuration and payment-business opening after KYC approval. |
| | | DELIMITER $$ |
| | | DROP PROCEDURE IF EXISTS add_merchant_onboarding_provisioning_columns$$ |
| | | CREATE PROCEDURE add_merchant_onboarding_provisioning_columns() |
| | | BEGIN |
| | | DECLARE col_exists INT DEFAULT 0; |
| | | SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'merchant_onboarding_application' AND COLUMN_NAME = 'wechat_config_status'; |
| | | IF col_exists = 0 THEN ALTER TABLE merchant_onboarding_application ADD COLUMN wechat_config_status VARCHAR(16) NOT NULL DEFAULT 'not_started' AFTER provider_response_message; END IF; |
| | | SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'merchant_onboarding_application' AND COLUMN_NAME = 'wechat_config_request_no'; |
| | | IF col_exists = 0 THEN ALTER TABLE merchant_onboarding_application ADD COLUMN wechat_config_request_no VARCHAR(32) DEFAULT NULL AFTER wechat_config_status; END IF; |
| | | SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'merchant_onboarding_application' AND COLUMN_NAME = 'wechat_config_response_code'; |
| | | IF col_exists = 0 THEN ALTER TABLE merchant_onboarding_application ADD COLUMN wechat_config_response_code VARCHAR(32) DEFAULT NULL AFTER wechat_config_request_no; END IF; |
| | | SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'merchant_onboarding_application' AND COLUMN_NAME = 'wechat_config_response_message'; |
| | | IF col_exists = 0 THEN ALTER TABLE merchant_onboarding_application ADD COLUMN wechat_config_response_message VARCHAR(512) DEFAULT NULL AFTER wechat_config_response_code; END IF; |
| | | SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'merchant_onboarding_application' AND COLUMN_NAME = 'business_open_status'; |
| | | IF col_exists = 0 THEN ALTER TABLE merchant_onboarding_application ADD COLUMN business_open_status VARCHAR(16) NOT NULL DEFAULT 'not_started' AFTER wechat_config_response_message; END IF; |
| | | SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'merchant_onboarding_application' AND COLUMN_NAME = 'business_open_request_no'; |
| | | IF col_exists = 0 THEN ALTER TABLE merchant_onboarding_application ADD COLUMN business_open_request_no VARCHAR(32) DEFAULT NULL AFTER business_open_status; END IF; |
| | | SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'merchant_onboarding_application' AND COLUMN_NAME = 'business_open_apply_no'; |
| | | IF col_exists = 0 THEN ALTER TABLE merchant_onboarding_application ADD COLUMN business_open_apply_no VARCHAR(32) DEFAULT NULL AFTER business_open_request_no; END IF; |
| | | SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'merchant_onboarding_application' AND COLUMN_NAME = 'business_open_response_code'; |
| | | IF col_exists = 0 THEN ALTER TABLE merchant_onboarding_application ADD COLUMN business_open_response_code VARCHAR(32) DEFAULT NULL AFTER business_open_apply_no; END IF; |
| | | SELECT COUNT(*) INTO col_exists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'merchant_onboarding_application' AND COLUMN_NAME = 'business_open_response_message'; |
| | | IF col_exists = 0 THEN ALTER TABLE merchant_onboarding_application ADD COLUMN business_open_response_message VARCHAR(512) DEFAULT NULL AFTER business_open_response_code; END IF; |
| | | END$$ |
| | | DELIMITER ; |
| | | CALL add_merchant_onboarding_provisioning_columns(); |
| | | DROP PROCEDURE IF EXISTS add_merchant_onboarding_provisioning_columns; |