ソースを参照

完成银行卡绑定

yuanmingze 2 ヶ月 前
コミット
254852af99

+ 583 - 0
src/pages-auth/bank/index.vue

@@ -0,0 +1,583 @@
+<template>
+  <view class="bank-auth">
+    <scroll-view scroll-y class="bank-auth__scroll">
+      <view class="bank-auth__hero">
+        <view class="bank-auth__hero-main">
+          <text class="bank-auth__hero-title">银行卡认证</text>
+          <text class="bank-auth__hero-desc">
+            请绑定本人 I 类银行账户借记卡,用于佣金结算发放
+          </text>
+        </view>
+
+        <view class="bank-auth__hero-badge">
+          {{ certified ? '已认证' : '待认证' }}
+        </view>
+      </view>
+
+      <view class="notice-card">
+        <view class="notice-card__icon">!</view>
+        <text class="notice-card__text"> 需绑定本人 I 类银行账户借记卡,否则无法发放佣金。 </text>
+      </view>
+
+      <view class="form-card">
+        <view class="form-header">
+          <text class="form-title">银行卡信息</text>
+          <text class="form-subtitle">请确认银行卡开户人与身份认证信息一致</text>
+        </view>
+
+        <view class="form-row">
+          <text class="form-label">银行卡号</text>
+          <input
+            v-model="bankInfo.cardNumber"
+            class="form-input"
+            type="number"
+            maxlength="19"
+            placeholder="请输入银行卡号"
+            placeholder-class="form-placeholder"
+            :disabled="isFormDisabled"
+          />
+        </view>
+
+        <view class="form-row">
+          <text class="form-label">开户行</text>
+          <input
+            v-model="bankInfo.bankName"
+            class="form-input"
+            placeholder="请输入开户行"
+            placeholder-class="form-placeholder"
+            :disabled="isFormDisabled"
+          />
+        </view>
+
+        <view class="form-row">
+          <text class="form-label">预留手机号</text>
+          <input
+            v-model="bankInfo.phone"
+            class="form-input"
+            type="number"
+            maxlength="11"
+            placeholder="请输入银行预留手机号"
+            placeholder-class="form-placeholder"
+            :disabled="isFormDisabled"
+          />
+        </view>
+      </view>
+    </scroll-view>
+
+    <view class="bottom-bar">
+      <button class="bottom-btn bottom-btn--secondary" :disabled="loading" @click="handleBack">
+        返回
+      </button>
+
+      <button
+        class="bottom-btn bottom-btn--primary"
+        :disabled="loading || certified"
+        @click="handleSubmit"
+      >
+        {{ submitButtonText }}
+      </button>
+    </view>
+
+    <BaseAuthDialog
+      v-model="bankPhoneAuthFailVisible"
+      title="手机三要素认证未通过"
+      confirm-text="我知道啦"
+    >
+      <view class="dialog-text"> 抱歉,您的预留手机号三要素认证未通过! </view>
+
+      <view class="dialog-text"> 请确保您的手机号持卡人身份证号与您提供的身份证号一致: </view>
+
+      <view class="dialog-text">
+        您可以登录银行卡 APP 或前往网点
+        <text class="dialog-highlight">更换预留手机号</text>
+        。
+      </view>
+    </BaseAuthDialog>
+
+    <BaseAuthDialog
+      v-model="reSignVisible"
+      title="重新签约提醒"
+      confirm-text="去签约"
+      @confirm="handleGoSign"
+    >
+      <view class="dialog-title-text"> 您更新了银行卡信息,需要与结算渠道重新签约。 </view>
+
+      <view class="dialog-text">
+        请点击“去签约”完成结算渠道签约,在完成签约前将无法完成结算。
+      </view>
+    </BaseAuthDialog>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { computed, reactive, ref } from 'vue'
+
+import { onLoad, onUnload } from '@dcloudio/uni-app'
+
+import {
+  bindBankInfoApi,
+  gigCertPersonVerifyMobile3Api,
+  personVerifyBank4Api,
+} from '@/services/modules/auth'
+
+import { useUserStore } from '@/stores/modules/user'
+
+import BaseAuthDialog from '@/components/BaseAuthDialog/index.vue'
+
+defineOptions({
+  name: 'BankAuthPage',
+})
+
+const API_SUCCESS_CODE = 0
+const SIGN_URL = '/pages-sub-packages/mine/auth/index'
+const BACK_DELAY = 500
+
+interface CurrentUserForBankAuth {
+  phone?: string
+  realname?: string
+  idCardNumber?: string
+  bankCardNumber?: string
+  bankPhone?: string
+}
+
+const userStore = useUserStore()
+
+const loading = ref(false)
+const certified = ref(false)
+const bankPhoneAuthFailVisible = ref(false)
+const reSignVisible = ref(false)
+
+let backTimer: ReturnType<typeof setTimeout> | null = null
+
+const bankInfo = reactive({
+  cardNumber: '',
+  bankName: '',
+  phone: '',
+})
+
+const currentUserInfo = computed(() => {
+  return (userStore.currentUserInfo ?? {}) as CurrentUserForBankAuth
+})
+
+const loginMobile = computed(() => currentUserInfo.value.phone ?? '')
+const realName = computed(() => currentUserInfo.value.realname ?? '')
+const idCardNumber = computed(() => currentUserInfo.value.idCardNumber ?? '')
+
+const hasExistingBankInfo = computed(() => {
+  return Boolean(currentUserInfo.value.bankCardNumber || currentUserInfo.value.bankPhone)
+})
+
+const isFormDisabled = computed(() => loading.value || certified.value)
+
+const submitButtonText = computed(() => {
+  if (certified.value) return '已认证'
+  if (loading.value) return '提交中...'
+  return '去认证'
+})
+
+const resetForm = () => {
+  certified.value = false
+  bankPhoneAuthFailVisible.value = false
+  reSignVisible.value = false
+
+  bankInfo.cardNumber = ''
+  bankInfo.bankName = ''
+  bankInfo.phone = ''
+}
+
+const normalizeForm = () => {
+  bankInfo.cardNumber = bankInfo.cardNumber.replace(/\s+/g, '')
+  bankInfo.bankName = bankInfo.bankName.trim()
+  bankInfo.phone = bankInfo.phone.replace(/\s+/g, '')
+}
+
+const validateUserIdentity = () => {
+  if (!realName.value || !idCardNumber.value) {
+    uni.showToast({
+      title: '请先完成身份信息认证',
+      icon: 'none',
+    })
+    return false
+  }
+
+  return true
+}
+
+const validateForm = () => {
+  normalizeForm()
+
+  if (!/^\d{16,19}$/.test(bankInfo.cardNumber)) {
+    uni.showToast({
+      title: '银行卡号格式错误',
+      icon: 'none',
+    })
+    return false
+  }
+
+  if (bankInfo.bankName.length < 2) {
+    uni.showToast({
+      title: '开户行名称过短',
+      icon: 'none',
+    })
+    return false
+  }
+
+  if (!/^1[3-9]\d{9}$/.test(bankInfo.phone)) {
+    uni.showToast({
+      title: '手机号格式错误',
+      icon: 'none',
+    })
+    return false
+  }
+
+  return true
+}
+
+const verifyBankPhoneIfNeeded = async () => {
+  if (loginMobile.value === bankInfo.phone) return true
+
+  const res = await gigCertPersonVerifyMobile3Api({
+    mobile: bankInfo.phone,
+    realName: realName.value,
+    cardId: idCardNumber.value,
+  })
+
+  if (res.code !== API_SUCCESS_CODE) {
+    bankPhoneAuthFailVisible.value = true
+    return false
+  }
+
+  return true
+}
+
+const verifyBankCard = async () => {
+  const res = await personVerifyBank4Api({
+    mobile: bankInfo.phone,
+    realName: realName.value,
+    cardId: idCardNumber.value,
+    bankCard: bankInfo.cardNumber,
+    bankPhone: bankInfo.phone,
+    bankName: bankInfo.bankName,
+  })
+
+  if (res.code !== API_SUCCESS_CODE) {
+    uni.showToast({
+      title: res.msg || '认证失败',
+      icon: 'none',
+    })
+    return false
+  }
+
+  return true
+}
+
+const bindBankInfo = async () => {
+  const res = await bindBankInfoApi({
+    bankCardNumber: bankInfo.cardNumber,
+    bankPhone: bankInfo.phone,
+  })
+
+  if (res.code !== API_SUCCESS_CODE) {
+    uni.showToast({
+      title: res.msg || '银行卡绑定失败',
+      icon: 'none',
+    })
+    return false
+  }
+
+  return true
+}
+
+const handleSubmit = async () => {
+  if (loading.value || certified.value) return
+  if (!validateUserIdentity() || !validateForm()) return
+
+  try {
+    loading.value = true
+
+    const bankPhonePassed = await verifyBankPhoneIfNeeded()
+    if (!bankPhonePassed) return
+
+    const bankCardPassed = await verifyBankCard()
+    if (!bankCardPassed) return
+
+    const bindPassed = await bindBankInfo()
+    if (!bindPassed) return
+
+    certified.value = true
+
+    uni.showToast({
+      title: '认证成功',
+      icon: 'none',
+    })
+
+    if (hasExistingBankInfo.value) {
+      reSignVisible.value = true
+      return
+    }
+
+    await userStore.getUserInfoByCode()
+
+    backTimer = setTimeout(() => {
+      uni.navigateBack()
+    }, BACK_DELAY)
+  } catch {
+    uni.showToast({
+      title: '请求失败,请检查网络',
+      icon: 'none',
+    })
+  } finally {
+    loading.value = false
+  }
+}
+
+const handleGoSign = () => {
+  uni.navigateTo({
+    url: SIGN_URL,
+  })
+}
+
+const handleBack = () => {
+  if (loading.value) return
+  uni.navigateBack()
+}
+
+onLoad(() => {
+  resetForm()
+})
+
+onUnload(() => {
+  if (backTimer) {
+    clearTimeout(backTimer)
+    backTimer = null
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+.bank-auth {
+  min-height: 100vh;
+  background: linear-gradient(180deg, #eef6ff 0%, #f7f8fa 420rpx), #f7f8fa;
+}
+
+.bank-auth__scroll {
+  height: 100vh;
+  padding: 28rpx 28rpx 180rpx;
+  box-sizing: border-box;
+}
+
+.bank-auth__hero {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  margin-bottom: 24rpx;
+  padding: 34rpx 32rpx;
+  border-radius: 28rpx;
+  background: linear-gradient(135deg, #1677ff 0%, #69b1ff 100%);
+  box-shadow: 0 18rpx 40rpx rgba(22, 119, 255, 0.18);
+  box-sizing: border-box;
+}
+
+.bank-auth__hero-main {
+  flex: 1;
+  padding-right: 24rpx;
+}
+
+.bank-auth__hero-title {
+  display: block;
+  color: #ffffff;
+  font-size: 40rpx;
+  font-weight: 700;
+  line-height: 56rpx;
+}
+
+.bank-auth__hero-desc {
+  display: block;
+  margin-top: 12rpx;
+  color: rgba(255, 255, 255, 0.88);
+  font-size: 26rpx;
+  line-height: 38rpx;
+}
+
+.bank-auth__hero-badge {
+  flex-shrink: 0;
+  height: 48rpx;
+  padding: 0 18rpx;
+  border-radius: 999rpx;
+  background: rgba(255, 255, 255, 0.18);
+  color: #ffffff;
+  font-size: 24rpx;
+  line-height: 48rpx;
+}
+
+.notice-card {
+  display: flex;
+  align-items: flex-start;
+  margin-bottom: 24rpx;
+  padding: 24rpx 26rpx;
+  border: 1rpx solid #ffe3b8;
+  border-radius: 22rpx;
+  background: #fff8ec;
+  box-sizing: border-box;
+}
+
+.notice-card__icon {
+  flex-shrink: 0;
+  width: 34rpx;
+  height: 34rpx;
+  margin-top: 2rpx;
+  margin-right: 14rpx;
+  border-radius: 50%;
+  background: #fa8c16;
+  color: #ffffff;
+  font-size: 24rpx;
+  font-weight: 700;
+  line-height: 34rpx;
+  text-align: center;
+}
+
+.notice-card__text {
+  flex: 1;
+  color: #b45309;
+  font-size: 26rpx;
+  line-height: 40rpx;
+}
+
+.form-card {
+  padding: 32rpx;
+  border-radius: 28rpx;
+  background: #ffffff;
+  box-shadow: 0 10rpx 32rpx rgba(15, 23, 42, 0.06);
+  box-sizing: border-box;
+}
+
+.form-header {
+  margin-bottom: 12rpx;
+}
+
+.form-title {
+  display: block;
+  color: #111827;
+  font-size: 34rpx;
+  font-weight: 700;
+  line-height: 48rpx;
+}
+
+.form-subtitle {
+  display: block;
+  margin-top: 8rpx;
+  color: #8a94a6;
+  font-size: 24rpx;
+  line-height: 34rpx;
+}
+
+.form-row {
+  display: flex;
+  align-items: center;
+  min-height: 104rpx;
+  border-bottom: 1rpx solid #eef0f3;
+}
+
+.form-row:last-child {
+  border-bottom: none;
+}
+
+.form-label {
+  width: 170rpx;
+  flex-shrink: 0;
+  color: #4b5563;
+  font-size: 28rpx;
+  line-height: 40rpx;
+}
+
+.form-input {
+  flex: 1;
+  height: 72rpx;
+  color: #111827;
+  font-size: 29rpx;
+  line-height: 72rpx;
+  text-align: right;
+}
+
+.form-placeholder {
+  color: #b6beca;
+}
+
+.bottom-bar {
+  position: fixed;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 20;
+  display: flex;
+  gap: 20rpx;
+  padding: 20rpx 28rpx calc(20rpx + env(safe-area-inset-bottom));
+  border-top: 1rpx solid rgba(229, 231, 235, 0.8);
+  background: rgba(255, 255, 255, 0.96);
+  box-sizing: border-box;
+}
+
+.bottom-btn {
+  flex: 1;
+  height: 88rpx;
+  margin: 0;
+  border-radius: 999rpx;
+  font-size: 32rpx;
+  font-weight: 600;
+  line-height: 88rpx;
+}
+
+.bottom-btn::after {
+  border: none;
+}
+
+.bottom-btn--secondary {
+  background: #f3f4f6;
+  color: #4b5563;
+}
+
+.bottom-btn--primary {
+  background: linear-gradient(135deg, #1677ff 0%, #4096ff 100%);
+  color: #ffffff;
+  box-shadow: 0 12rpx 28rpx rgba(22, 119, 255, 0.22);
+}
+
+.bottom-btn[disabled] {
+  opacity: 1;
+}
+
+.bottom-btn--secondary[disabled] {
+  background: #f3f4f6 !important;
+  color: #a1a1aa !important;
+}
+
+.bottom-btn--primary[disabled] {
+  background: #d1d5db !important;
+  color: #ffffff !important;
+  box-shadow: none;
+}
+
+.dialog-title-text {
+  margin-bottom: 32rpx;
+  color: #111827;
+  font-size: 32rpx;
+  font-weight: 700;
+  line-height: 48rpx;
+}
+
+.dialog-text {
+  margin-bottom: 24rpx;
+  color: #4b5563;
+  font-size: 29rpx;
+  line-height: 46rpx;
+}
+
+.dialog-text:last-child {
+  margin-bottom: 0;
+}
+
+.dialog-highlight {
+  color: #1677ff;
+  font-weight: 700;
+}
+</style>

+ 1 - 1
src/pages-auth/certification-service/index.vue

@@ -78,7 +78,7 @@ import { useUserStore } from '@/stores/modules/user'
 const AUTH_SUCCESS_STATUS = 1
 
 const IDENTITY_AUTH_URL = '/pages-auth/identity/index'
-const BANK_AUTH_URL = '/pages-sub-verify/auth/bank'
+const BANK_AUTH_URL = '/pages-auth/bank/index'
 
 const userStore = useUserStore()
 

+ 3 - 1
src/pages-task/task-package/list.vue

@@ -345,7 +345,9 @@ const submitReceiveAfterTestPassed = async (item: TaskPackageItem) => {
       confirmText: '去认证',
       success: (modalRes) => {
         if (modalRes.confirm) {
-          console.log('认证页面')
+          uni.navigateTo({
+            url: `/pages-auth/certification-service/index`,
+          })
         }
       },
     })

+ 7 - 0
src/pages.json

@@ -226,6 +226,13 @@
             "navigationBarTitleText": "个人人人脸活体认证",
             "navigationStyle": "default"
           }
+        },
+        {
+          "path": "bank/index",
+          "style": {
+            "navigationBarTitleText": "银行卡认证",
+            "navigationStyle": "default"
+          }
         }
       ]
     }

+ 9 - 0
src/services/modules/auth/index.ts

@@ -8,6 +8,7 @@ import type {
   GigCertPersonVerifyMobile3Request,
   GigCertPersonVerifyMobile3Response,
   PersonCheckFaceRequest,
+  PersonVerifyBank4Request,
 } from './type'
 
 export const getAuthInfoApi = () => {
@@ -33,3 +34,11 @@ export const personCheckFaceApi = (data: PersonCheckFaceRequest) => {
 export const bindBaseInfoApi = (data: GigCertPersonVerifyFaceRequest) => {
   return http.post(`/admin/user-sign-cert/bind-base-info`, data)
 }
+
+export const personVerifyBank4Api = (data: PersonVerifyBank4Request) => {
+  return http.post(`/gig/cert/person-verify-bank4`, data)
+}
+
+export const bindBankInfoApi = (data: PersonVerifyBank4Request) => {
+  return http.post(`/admin/user-sign-cert/bind-bank-info`, data)
+}

+ 13 - 0
src/services/modules/auth/type.d.ts

@@ -99,3 +99,16 @@ export interface PersonCheckFaceRequest {
 export interface BindBaseInfoRequest {
   idCardNumber: string
 }
+
+export interface PersonVerifyBank4Request {
+  mobile: string
+  realName: string
+  cardId: string
+  bankCard: string
+  bankPhone: string
+  bankName: string
+}
+export interface BindBankInfoRequest {
+  bankCardNumber: string
+  bankPhone: string
+}