1
0

8 Commitit 9f5e64aa98 ... ab480c592a

Tekijä SHA1 Viesti Päivämäärä
  yuanmingze ab480c592a 优化提交逻辑 1 kuukausi sitten
  yuanmingze 2dc4e0a804 修改认证登录状态保存 1 kuukausi sitten
  yuanmingze d751e767ca 修改图片展示大小 1 kuukausi sitten
  yuanmingze e7ed10933e 优化开票流程状态判断与按钮禁用逻辑 1 kuukausi sitten
  yuanmingze 6a5e8ec79f 修改图片展示效果,将长图切割 1 kuukausi sitten
  yuanmingze 73824629a9 优化人脸识别相关接口类型定义,修复响应类型错误,并增强用户反馈提示 1 kuukausi sitten
  yuanmingze 3617d45da8 新增 电子税务局提升账号安全等级操作指引 功能 1 kuukausi sitten
  yuanmingze 87359ee522 优化拒绝并退回逻辑,增加错误处理和用户反馈 1 kuukausi sitten

BIN
src/assets/images/securityLevelGuide/1.webp


BIN
src/assets/images/securityLevelGuide/2.webp


BIN
src/assets/images/securityLevelGuide/3.webp


BIN
src/assets/images/securityLevelGuide/4.webp


BIN
src/assets/images/securityLevelGuide/5.webp


BIN
src/assets/images/securityLevelGuide/6.webp


+ 125 - 74
src/hooks/useInvoice.ts

@@ -1,38 +1,60 @@
-import { ref, reactive } from 'vue'
-import { useRouter, useRoute } from 'vue-router'
+import { ref, reactive, computed } from 'vue'
+import { useRouter } from 'vue-router'
 import { showToast, showSuccessToast } from 'vant'
 import { getStatusApi, submitInvoiceApplyApi } from '@/services/modules/invoiceInformation'
-import type { PushRecordIdRequest } from '@/services/modules/invoiceInformation/type.d.ts'
-import { useUserStore } from '@/stores/modules/user'
 import { getFaceAuthResultApi } from '@/services/modules/faceRecognition'
+import type { PushRecordIdRequest } from '@/services/modules/invoiceInformation/type'
+import { useUserStore } from '@/stores/modules/user'
+
+/* ========================== 类型定义 ========================== */
 
 interface ToStatus {
-  /** 待处理事件 */
   eventStatus?: string
-  /** 开票状态 */
   invoiceStatus?: string
-  /** 是否上传身份证图片 */
   isIdImgReady?: boolean
-  [property: string]: any
+  [key: string]: any
 }
 
-/**
- * 发票状态 Hook
- * 用于复用:状态查询 + 提交开票申请
- */
+/* ========================== 主 Hook ========================== */
+
 export function useInvoice() {
   const userStore = useUserStore()
   const router = useRouter()
 
-  const route = useRoute()
+  /* ------------------ 基础状态 ------------------ */
 
-  // state
-  const btnDisabled = ref(false)
-  const statusMap = ref<ToStatus>({})
   const isLoading = ref(false)
+  const statusMap = ref<ToStatus>({})
+
+  // 状态层面是否允许继续流程
+  const statusReady = ref(false)
+
+  // 人脸认证接口是否已经返回成功
+  const faceAuthReady = ref(false)
+
+  // 人脸是否“通过或无需认证”
+  const faceAuthResult = ref(false)
+
+  /* ------------------ UI 控制层 ------------------ */
+
+  /**
+   * 仅用于按钮禁用控制
+   * 不参与真实业务逻辑
+   */
+  const btnDisabled = computed(() => {
+    return !(statusReady.value && faceAuthReady.value)
+  })
+
+  /* ------------------ 请求参数 ------------------ */
+
+  const params = reactive<PushRecordIdRequest>({
+    pushRecordId: userStore.pushRecordId,
+  })
+
+  /* ------------------ 常量映射 ------------------ */
 
-  // 已提交状态映射
   const isSubmitStatusMap = ['PENDING', 'FINISHED', 'RED_INK_ENTRY', 'NOT_APPROVED', 'DISCARD']
+
   const isSubmitEventStatusMap = [
     'SUBMITTED',
     'PENDING_PAYMENT',
@@ -43,46 +65,28 @@ export function useInvoice() {
     'INVOICE_DOWNLOADABLE',
   ]
 
-  // 请求参数
-  const params = reactive<PushRecordIdRequest>({
-    pushRecordId: userStore.pushRecordId,
-  })
-
-  const faceAuthResult = ref(false)
-
-  // 获取认证结果
-  const getFaceAuthResult = async () => {
-    try {
-      const res = await getFaceAuthResultApi(params)
-      if (res.code === 0 && res.data?.success) {
-        const rzzt = res.data.rzzt
-        // 已认证或无需认证提交跳转
-        if (rzzt === 'NO_REQUIRED_AUTHENTICATION') {
-          faceAuthResult.value = true
-        }
-      }
-    } catch (err: any) {
-      console.error('获取认证结果失败', err)
-      showToast(err!.message || '获取认证结果失败,请稍后重试')
-      btnDisabled.value = true
-    }
-  }
+  /* ============================================================
+   * 状态获取层
+   * ============================================================ */
 
-  /**
-   * 获取开票状态
-   */
   const getStatus = async () => {
+    if (!params.pushRecordId) return
+
     isLoading.value = true
     try {
       const res = await getStatusApi(params)
-      if (res.code === 0) {
-        statusMap.value = res.data as ToStatus
-        //  判断当前是否已提交
-        const { invoiceStatus, eventStatus } = res.data
-        const isInvoiceSubmitted = invoiceStatus && isSubmitStatusMap.includes(invoiceStatus)
-        const isEventSubmitted = eventStatus && isSubmitEventStatusMap.includes(eventStatus)
-        btnDisabled.value = isInvoiceSubmitted && isEventSubmitted
-      }
+      if (res.code !== 0) return
+
+      statusMap.value = res.data as ToStatus
+
+      const { invoiceStatus, eventStatus } = res.data
+
+      const isInvoiceSubmitted = !!invoiceStatus && isSubmitStatusMap.includes(invoiceStatus)
+
+      const isEventSubmitted = !!eventStatus && isSubmitEventStatusMap.includes(eventStatus)
+
+      // 只有未提交状态才允许继续
+      statusReady.value = !(isInvoiceSubmitted && isEventSubmitted)
     } catch (err) {
       console.error('获取状态失败', err)
       showToast('获取开票状态失败,请稍后重试')
@@ -91,48 +95,95 @@ export function useInvoice() {
     }
   }
 
-  /**
-   * 提交开票申请
-   */
-  const submitInvoiceApply = async () => {
+  /* ============================================================
+   * 人脸认证状态层
+   * ============================================================ */
+
+  const getFaceAuthResult = async () => {
+    if (!params.pushRecordId) return
+
+    try {
+      const res = await getFaceAuthResultApi(params)
+
+      if (res.code !== 0 || !res.data?.success) return
+
+      faceAuthReady.value = true
+
+      if (res.data.rzzt === 'NO_REQUIRED_AUTHENTICATION') {
+        faceAuthResult.value = true
+      }
+    } catch (err: any) {
+      console.error('获取认证结果失败', err)
+      showToast(err?.message || '获取认证结果失败,请稍后重试')
+    }
+  }
+
+  /* ============================================================
+   * 核心提交逻辑(纯业务)
+   * ⚠ 不依赖任何 UI 计算属性
+   * ============================================================ */
+
+  const doSubmitInvoice = async (): Promise<boolean> => {
+    if (!params.pushRecordId) {
+      showToast('参数异常')
+      return false
+    }
+
     try {
-      // 1. 提交流程
       const res = await submitInvoiceApplyApi(params)
-      // 2. 成功逻辑
+
       if (res.code === 0 && res.data) {
         showSuccessToast('提交成功')
+
         setTimeout(() => {
           userStore.LogOut()
           router.replace({ path: '/success' })
         }, 800)
-        return
+
+        return true
       }
-      // 3. 失败逻辑(接口返回的错误)
-      showToast(res.msg)
+
+      showToast(res.msg || '提交失败')
+      return false
     } catch (err: any) {
-      // 4. 异常逻辑
       console.error('提交失败', err)
-      showToast(err.message)
+      showToast(err?.message || '提交失败')
+      return false
     }
+  }
 
-    // 5. 失败后统一处理 —— 判断当前页面是否需要跳转
-    if (route.path !== '/h5/invoice-information/index') {
-      setTimeout(() => {
-        router.replace({
-          path: '/invoice-information',
-        })
-      }, 1800)
-    }
+  /* ============================================================
+   * UI 场景提交(按钮点击)
+   * ============================================================ */
+
+  const submitInvoiceWithGuard = async () => {
+    if (btnDisabled.value) return false
+    return await doSubmitInvoice()
   }
 
+  /* ============================================================
+   * 流程场景提交(loading页/自动流程)
+   * ============================================================ */
+
+  const submitInvoiceForce = async () => {
+    return await doSubmitInvoice()
+  }
+
+  /* ========================== 暴露 API ========================== */
+
   return {
-    params,
+    /* 状态 */
     statusMap,
-    btnDisabled,
     isLoading,
     faceAuthResult,
+    btnDisabled,
+
+    /* 状态获取 */
     getStatus,
-    submitInvoiceApply,
     getFaceAuthResult,
+
+    /* 提交 */
+    submitInvoiceWithGuard, // 按钮用
+    submitInvoiceForce, // 流程自动用
   }
 }

+ 7 - 3
src/services/modules/faceRecognition/index.ts

@@ -1,16 +1,20 @@
 import http from '../../index'
 
 import type { PushRecordIdRequest } from '../invoiceInformation/type.d'
-import type { GetFaceAuthInfoRequest } from './type.d'
+import type {
+  GetFaceAuthInfoRequest,
+  GetFaceAuthInfoResponse,
+  GetFaceAuthResultReqsponse,
+} from './type.d'
 export const getFaceAuthInfoApi = (data: GetFaceAuthInfoRequest) => {
-  return http.post({
+  return http.post<GetFaceAuthInfoResponse>({
     url: '/admin/invoice-order/get-face-auth-info',
     data,
   })
 }
 
 export const getFaceAuthResultApi = (data: PushRecordIdRequest) => {
-  return http.post({
+  return http.post<GetFaceAuthResultReqsponse>({
     url: '/admin/invoice-order/get-face-auth-result',
     data,
   })

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

@@ -2,3 +2,16 @@ export interface GetFaceAuthInfoRequest {
   pushRecordId: string
   callbackUrl: string
 }
+
+export interface GetFaceAuthInfoResponse {
+  faceAuthUrl: string
+  msg: string
+  success: boolean
+}
+
+export interface GetFaceAuthResultReqsponse {
+  lowCertLevel: boolean
+  msg: string
+  rzzt: string
+  success: boolean
+}

+ 2 - 3
src/views/face-recognition/index.vue

@@ -28,6 +28,7 @@ import StepProgress from '@/components/StepProgress.vue'
 import { getFaceAuthInfoApi } from '@/services/modules/faceRecognition'
 import { onBeforeMount } from 'vue'
 import { useUserStore } from '@/stores/modules/user'
+import { showToast } from 'vant'
 
 const userStore = useUserStore()
 
@@ -37,14 +38,12 @@ const getConfirmInvoiceInfo = async () => {
     pushRecordId: userStore.pushRecordId,
     callbackUrl,
   }
-
   const res = await getFaceAuthInfoApi(params)
   if (res.code === 0 && res.data?.faceAuthUrl) {
-    console.log('faceAuthUrl:', res.data.faceAuthUrl)
     // 直接跳转进行人脸识别
     window.location.href = res.data.faceAuthUrl
   } else {
-    console.warn('获取认证链接失败', res)
+    showToast(res.data?.msg || '获取认证信息失败,请稍后重试')
   }
 }
 

+ 2 - 2
src/views/identity-upload/index.vue

@@ -81,7 +81,7 @@ import { useInvoice } from '@/hooks/useInvoice'
 import type { UpdateH5IdcardInfoRequest } from '@/services/modules/identityUpload/type.d'
 
 // --- 初始化 Hooks ---
-const { getStatus, submitInvoiceApply, getFaceAuthResult, faceAuthResult } = useInvoice()
+const { getStatus, submitInvoiceForce, getFaceAuthResult, faceAuthResult } = useInvoice()
 const userStore = useUserStore()
 /** 上传项类型 */
 interface UploadFileItem {
@@ -200,7 +200,7 @@ const handleNext = async () => {
         return router.push({ path: '/face-recognition' })
       }
       // 其他情况,直接提交
-      await submitInvoiceApply()
+      await submitInvoiceForce()
     }
   } catch (err: any) {
     const { message } = err

+ 38 - 20
src/views/invoice-information/index.vue

@@ -152,8 +152,14 @@ import ModernDialog from '@/components/ModernDialog.vue'
 // ✅ 使用封装好的 Hook
 import { useInvoice } from '@/hooks/useInvoice'
 // --- 初始化 Hooks ---
-const { getStatus, submitInvoiceApply, getFaceAuthResult, btnDisabled, statusMap, faceAuthResult } =
-  useInvoice()
+const {
+  getStatus,
+  submitInvoiceWithGuard,
+  getFaceAuthResult,
+  btnDisabled,
+  statusMap,
+  faceAuthResult,
+} = useInvoice()
 
 // --- 其余本页面独有逻辑 ---
 const userStore = useUserStore()
@@ -188,6 +194,7 @@ const getConfirmInvoiceInfo = async () => {
  * 使用 hooks 提供的状态进行判断
  */
 const handleNext = async () => {
+  if (btnDisabled.value) return
   // 未上传身份证,跳转上传页
   if (!statusMap.value.isIdImgReady) {
     return router.push({ path: '/identity-upload' })
@@ -199,7 +206,7 @@ const handleNext = async () => {
   }
 
   // 其他情况,直接提交
-  await submitInvoiceApply()
+  await submitInvoiceWithGuard()
 }
 
 const rejectAndReturnDialog = ref(false)
@@ -210,25 +217,36 @@ const rejectAndReturn = () => {
 
 const rejectAndReturnDialogConfirm = async () => {
   rejectAndReturnDialog.value = false
-  const res = await invoiceRecordInvalidateApi(params)
-  if (res.code === 0 && res.data) {
-    await showSuccessToast({
-      message: '拒绝成功',
-      duration: 800,
-      forbidClick: true,
-    })
-
-    setTimeout(() => {
-      router.replace({
-        path: '/login',
-        query: {
-          from: 'reject',
-          pushRecordId: userStore.pushRecordId,
-        },
+
+  try {
+    const res = await invoiceRecordInvalidateApi(params)
+    if (res.code === 0 && res.data) {
+      await showSuccessToast({
+        message: '拒绝成功',
+        duration: 800,
+        forbidClick: true,
       })
 
-      userStore.LogOut()
-    }, 1000)
+      setTimeout(() => {
+        router.replace({
+          path: '/login',
+          query: {
+            from: 'reject',
+            pushRecordId: userStore.pushRecordId,
+          },
+        })
+
+        userStore.LogOut()
+      }, 1000)
+    } else {
+      showToast(res.msg || '操作失败,请重试')
+    }
+  } catch (err: any) {
+    const { code, message } = err
+    if (code === 1) {
+      showToast(message)
+    }
+    return
   }
 }
 

+ 3 - 0
src/views/login/index.vue

@@ -392,6 +392,9 @@ onActivated(() => {
 })
 
 onBeforeMount(() => {
+  const userStore = useUserStore()
+  userStore.$reset()
+  localStorage.removeItem('user-store')
   registerUrlParams.value.returnUrl = `${location.origin}/h5${route.fullPath}`
   const pushRecordId = route.query.pushRecordId
   if (!pushRecordId) {

+ 87 - 3
src/views/pedding-face-recognition/index.vue

@@ -11,13 +11,48 @@
         </van-loading>
       </div>
     </div>
+
+    <!-- 低证等级弹窗 -->
+    <van-dialog
+      v-model:show="lowCertLevelDialog"
+      title="税局账号安全等级升级提醒"
+      confirmButtonText="我知道啦"
+      @confirm="lowCertLevelConfirm"
+    >
+      <div class="dialog-content">
+        <div class="dialog-text">
+          抱歉,您在电子税务局的账号安全等级较低,无法确认开票申请,请登录至电子税务局提升账号安全等级。
+        </div>
+        <div class="dialog-btn" @click="handleSecurityLevelGuideClick">
+          点击了解提升安全等级操作指引
+        </div>
+      </div>
+    </van-dialog>
+
+    <!-- 安全等级操作指引 -->
+    <van-dialog
+      v-model:show="securityLevelGuideDialog"
+      width="90%"
+      :closeOnClickOverlay="true"
+      className="security-level-guide-dialog"
+      @confirm="securityLevelGuideDialog = false"
+    >
+      <div class="security-level-guide-body">
+        <img src="@/assets/images/securityLevelGuide/1.webp" class="guide-image" />
+        <img src="@/assets/images/securityLevelGuide/2.webp" class="guide-image" />
+        <img src="@/assets/images/securityLevelGuide/3.webp" class="guide-image" />
+        <img src="@/assets/images/securityLevelGuide/4.webp" class="guide-image" />
+        <img src="@/assets/images/securityLevelGuide/5.webp" class="guide-image" />
+        <img src="@/assets/images/securityLevelGuide/6.webp" class="guide-image" />
+      </div>
+    </van-dialog>
   </div>
 </template>
 
 <script setup lang="ts">
 import { getFaceAuthResultApi } from '@/services/modules/faceRecognition'
 import type { PushRecordIdRequest } from '@/services/modules/invoiceInformation/type.d.ts'
-import { reactive, onMounted } from 'vue'
+import { ref, reactive, onMounted } from 'vue'
 import { useUserStore } from '@/stores/modules/user'
 import { showToast } from 'vant'
 import { useInvoice } from '@/hooks/useInvoice'
@@ -27,7 +62,7 @@ const userStore = useUserStore()
 const router = useRouter()
 
 // --- 初始化 Hooks ---
-const { submitInvoiceApply } = useInvoice()
+const { submitInvoiceForce } = useInvoice()
 
 const loadingSize = '10vw'
 const loadingColor = '#1677ff'
@@ -36,15 +71,22 @@ const params = reactive<PushRecordIdRequest>({
   pushRecordId: userStore.pushRecordId,
 })
 
+const lowCertLevelDialog = ref(false)
 // 获取认证结果
 const getFaceAuthResult = async () => {
   try {
     const res = await getFaceAuthResultApi(params)
+
+    if (res.code === 0 && res.data.lowCertLevel) {
+      lowCertLevelDialog.value = true
+      return
+    }
+
     if (res.code === 0 && res.data?.success) {
       const rzzt = res.data.rzzt
       // 已认证或无需认证提交跳转
       if (rzzt === 'NO_REQUIRED_AUTHENTICATION') {
-        submitInvoiceApply()
+        submitInvoiceForce()
         return
       }
     } else {
@@ -62,6 +104,15 @@ const getFaceAuthResult = async () => {
   }, 1800)
 }
 
+const lowCertLevelConfirm = () => {
+  router.replace('/invoice-information')
+}
+
+const securityLevelGuideDialog = ref(false)
+const handleSecurityLevelGuideClick = () => {
+  securityLevelGuideDialog.value = true
+}
+
 onMounted(() => {
   getFaceAuthResult()
 })
@@ -97,4 +148,37 @@ html {
   justify-content: center;
   padding-top: 14vw; /* 预留固定导航空间 */
 }
+.dialog-content {
+  padding: 12px;
+  .dialog-text {
+    font-size: 14px;
+    line-height: 22px;
+    color: #333;
+
+    font-weight: 400;
+  }
+  .dialog-btn {
+    text-align: center;
+    font-weight: 600;
+    margin: 18px 0;
+    color: #0072f8;
+  }
+}
+:deep(.security-level-guide-dialog) {
+  top: 50%;
+  height: 90vh;
+
+  .security-level-guide-body {
+    height: 80vh;
+    overflow-y: auto;
+    -webkit-overflow-scrolling: touch;
+    padding: 12px;
+
+    .guide-image {
+      width: 100%;
+      height: auto;
+      display: block;
+    }
+  }
+}
 </style>