From a49affbcf23ee5f0e7cbe27da42d348c87d21d15 Mon Sep 17 00:00:00 2001
From: liangzai <2440983361@qq.com>
Date: Sun, 26 Jul 2026 23:14:54 +0800
Subject: [PATCH] f
---
src/api/index.js | 16 +-
.../admin/certification-reviews/index.vue | 59 +++-
src/pages/admin/users/index.vue | 109 ++++++++
src/pages/certification/IframeCallback.vue | 39 ++-
.../components/ContractPreview.vue | 260 ++++++++++--------
.../certification/components/ContractSign.vue | 87 +++++-
.../components/PlatformSelect.vue | 155 +++++++++++
src/pages/certification/index.vue | 194 ++++++++++++-
src/router/index.js | 5 +
vite.config.js | 12 +-
10 files changed, 770 insertions(+), 166 deletions(-)
create mode 100644 src/pages/certification/components/PlatformSelect.vue
diff --git a/src/api/index.js b/src/api/index.js
index 1faa175..a89cbc6 100644
--- a/src/api/index.js
+++ b/src/api/index.js
@@ -199,8 +199,11 @@ export const certificationApi = {
// 申请合同签署
applyContract: () => request.post('/certifications/apply-contract'),
- // 获取认证详情
- getCertificationDetails: () => request.get('/certifications/details'),
+ // 刷新签署 iframe 长链(法大大 EmbedURL 约 10 分钟/单次)
+ refreshContractSignUrl: () => request.post('/certifications/refresh-contract-sign-url'),
+
+ // 刷新合同预览链接(法大大 get-preview-url)
+ refreshContractPreviewUrl: () => request.post('/certifications/refresh-contract-preview-url'),
// 确认签署状态
confirmSign: (data) => request.post('/certifications/confirm-sign', data),
@@ -208,6 +211,12 @@ export const certificationApi = {
// 确认认证状态
confirmAuth: (data) => request.post('/certifications/confirm-auth', data),
+ // 可选签署平台列表
+ getSignPlatforms: () => request.get('/certifications/sign-platforms'),
+
+ // 选择签署平台并生成企业认证链接
+ selectSignPlatform: (data) => request.post('/certifications/select-sign-platform', data),
+
// OCR营业执照识别
recognizeBusinessLicense: (formData) => request.post('/certifications/ocr/business-license', formData, {
headers: {
@@ -229,6 +238,9 @@ export const certificationApi = {
// 管理员代用户完成认证(暂不关联合同)
adminCompleteWithoutContract: (data) => request.post('/certifications/admin/complete-without-contract', data),
+ // 管理员触发重新认证(回退到提交企业信息,保留钱包/API Key/历史记录)
+ adminResetForResignContract: (data) => request.post('/certifications/admin/reset-for-resign-contract', data),
+
// 管理端企业审核:列表(按状态机 certification_status 筛选)、详情、通过、拒绝、按用户变更状态
adminListSubmitRecords: (params) => request.get('/certifications/admin/submit-records', { params }),
adminGetSubmitRecord: (id) => request.get(`/certifications/admin/submit-records/${id}`),
diff --git a/src/pages/admin/certification-reviews/index.vue b/src/pages/admin/certification-reviews/index.vue
index 14f341a..09c3691 100644
--- a/src/pages/admin/certification-reviews/index.vue
+++ b/src/pages/admin/certification-reviews/index.vue
@@ -2,7 +2,7 @@
+
+
+
+
+
+
+
+ 用户手机:
+ {{ formatPhone(selectedUser.phone) }}
+
+
+ 企业名称:
+ {{ selectedUser.enterprise_info?.company_name || '-' }}
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+
+
+ 确认重新认证
+
+
+
+
@@ -745,6 +807,20 @@ const adminCertForm = reactive({
reason: ''
})
+// 管理员重新认证
+const resignContractDialogVisible = ref(false)
+const resignContractLoading = ref(false)
+const resignContractFormRef = ref(null)
+const resignContractForm = reactive({
+ reason: ''
+})
+const resignContractRules = {
+ reason: [
+ { required: true, message: '请填写操作原因', trigger: 'blur' },
+ { min: 2, max: 500, message: '原因长度需在 2-500 字之间', trigger: 'blur' }
+ ]
+}
+
const adminCertRules = {
company_name: [{ required: true, message: '请输入企业名称', trigger: 'blur' }],
unified_social_code: [{ required: true, message: '请输入统一社会信用代码', trigger: 'blur' }],
@@ -999,6 +1075,39 @@ const handleSubmitAdminCert = async () => {
}
}
+// 打开重新认证弹窗
+const openResignContractDialog = () => {
+ if (!selectedUser.value?.is_certified) {
+ ElMessage.warning('仅已认证用户可重新认证')
+ return
+ }
+ resignContractForm.reason = ''
+ resignContractDialogVisible.value = true
+}
+
+// 提交重新认证
+const handleSubmitResignContract = async () => {
+ if (!resignContractFormRef.value || !selectedUser.value) return
+ try {
+ await resignContractFormRef.value.validate()
+ resignContractLoading.value = true
+ await certificationApi.adminResetForResignContract({
+ user_id: selectedUser.value.id,
+ reason: resignContractForm.reason
+ })
+ ElMessage.success('已重置为重新认证,请通知用户前往企业入驻重新填写企业信息')
+ resignContractDialogVisible.value = false
+ userDialogVisible.value = false
+ await loadUsers()
+ await loadStats()
+ } catch (error) {
+ console.error('重新认证失败:', error)
+ ElMessage.error(error?.response?.data?.message || '重新认证失败')
+ } finally {
+ resignContractLoading.value = false
+ }
+}
+
// 查看合同
const handleViewContract = (contract) => {
if (contract.contract_file_url) {
diff --git a/src/pages/certification/IframeCallback.vue b/src/pages/certification/IframeCallback.vue
index 43d0d2f..5e1d533 100644
--- a/src/pages/certification/IframeCallback.vue
+++ b/src/pages/certification/IframeCallback.vue
@@ -15,13 +15,13 @@
diff --git a/src/pages/certification/components/ContractPreview.vue b/src/pages/certification/components/ContractPreview.vue
index ad6ec8b..38bd095 100644
--- a/src/pages/certification/components/ContractPreview.vue
+++ b/src/pages/certification/components/ContractPreview.vue
@@ -14,10 +14,31 @@
-
+
+
+
+
+
+
{{ refreshing ? '正在获取预览...' : '正在加载预览...' }}
+
+
+
{{ previewError }}
+
重新获取预览
+
+
+
+
+
全屏预览
-
+
+
+
+
开始签署
-
+
diff --git a/src/pages/certification/components/ContractSign.vue b/src/pages/certification/components/ContractSign.vue
index 934b311..9b943cd 100644
--- a/src/pages/certification/components/ContractSign.vue
+++ b/src/pages/certification/components/ContractSign.vue
@@ -15,19 +15,24 @@
-
+
-
+
-
正在加载合同签署页面...
+
{{ refreshing ? '正在获取签署页面...' : '正在加载合同签署页面...' }}
+
+
+
{{ refreshError }}
+
重新获取链接
@@ -36,29 +41,68 @@
diff --git a/src/pages/certification/index.vue b/src/pages/certification/index.vue
index 514aea5..7ef4bc8 100644
--- a/src/pages/certification/index.vue
+++ b/src/pages/certification/index.vue
@@ -75,7 +75,12 @@
:certification-data="certificationData"
:submit-time="manualReviewSubmitTime"
:company-name="enterpriseForm.companyName"
- @refresh="getCertificationDetails"
+ @refresh="() => getCertificationDetails({ silent: true })"
+ />
+
+
@@ -145,7 +150,9 @@ import ContractSign from './components/ContractSign.vue'
import EnterpriseInfo from './components/EnterpriseInfo.vue'
import EnterpriseVerify from './components/EnterpriseVerify.vue'
import ManualReviewPending from './components/ManualReviewPending.vue'
+import PlatformSelect from './components/PlatformSelect.vue'
const router = useRouter()
+const route = useRoute()
const userStore = useUserStore()
const certificationSteps = [
{
@@ -160,6 +167,12 @@ const certificationSteps = [
description: '等待管理员审核企业信息',
icon: ClockIcon,
},
+ {
+ key: 'platform_select',
+ title: '选择平台',
+ description: '选择签署平台(法大大)',
+ icon: DocumentTextIcon,
+ },
{
key: 'enterprise_verify',
title: '企业认证',
@@ -224,6 +237,86 @@ const devCurrentStep = ref('enterprise_info')
// 合同签署加载状态
const contractSignLoading = ref(false)
+// 合同签署状态轮询:进入 contract_applied 后定时拉详情,触发后端 checkAndUpdateSignStatus
+let signPollTimer = null
+const SIGN_POLL_INTERVAL = 5000
+const signPolling = ref(false)
+const autoSelectingPlatform = ref(false)
+
+const stopSignStatusPolling = () => {
+ if (signPollTimer) {
+ clearTimeout(signPollTimer)
+ signPollTimer = null
+ }
+ signPolling.value = false
+}
+
+const startSignStatusPolling = () => {
+ if (signPollTimer) return
+ signPolling.value = true
+
+ const tick = async () => {
+ const status = certificationData.value?.status
+ if (status !== 'contract_applied') {
+ stopSignStatusPolling()
+ return
+ }
+ try {
+ const res = await certificationApi.getCertificationDetails()
+ if (res?.data) {
+ certificationData.value = res.data
+ stepMeta.value = res.data?.metadata || {}
+ const newStatus = res.data?.status
+ if (
+ newStatus === 'contract_signed' ||
+ newStatus === 'completed' ||
+ newStatus === 'contract_rejected' ||
+ newStatus === 'contract_expired'
+ ) {
+ stopSignStatusPolling()
+ await setCurrentStepByStatus()
+ return
+ }
+ }
+ } catch (e) {
+ console.warn('合同签署状态轮询失败:', e)
+ }
+ if (signPolling.value) {
+ signPollTimer = setTimeout(tick, SIGN_POLL_INTERVAL)
+ }
+ }
+ signPollTimer = setTimeout(tick, SIGN_POLL_INTERVAL)
+}
+
+// 仅当可见平台为 1 个时可静默 select,否则展示 PlatformSelect
+async function maybeGoPlatformSelectOrAuto() {
+ const meta = certificationData.value?.metadata || {}
+ if (!meta.need_select_platform) return false
+
+ const platforms = meta.available_platforms || []
+ if (platforms.length <= 1 && meta.auto_select_platform && !autoSelectingPlatform.value) {
+ autoSelectingPlatform.value = true
+ try {
+ const res = await certificationApi.selectSignPlatform({
+ sign_platform: meta.auto_select_platform,
+ })
+ if (res?.data) {
+ certificationData.value = res.data
+ stepMeta.value = res.data.metadata || {}
+ }
+ await getCertificationDetails({ silent: true })
+ return true
+ } catch (e) {
+ ElMessage.error(e?.message || '选择签署平台失败')
+ } finally {
+ autoSelectingPlatform.value = false
+ }
+ }
+
+ currentStep.value = 'platform_select'
+ return true
+}
+
// 只补 enterprise_verify 所需 auth_url,不改动原有页面展示数据结构
// 轮询策略:最多 5 秒,每秒 1 次(最多 5 次)
const pollAuthUrlOnly = async (maxTries = 5) => {
@@ -282,6 +375,19 @@ const handleEnterpriseSubmit = async (payload) => {
}
}
+const handlePlatformSelected = async (data) => {
+ if (data) {
+ certificationData.value = data
+ stepMeta.value = data.metadata || {}
+ }
+ await getCertificationDetails()
+ if (stepMeta.value?.auth_url) {
+ currentStep.value = 'enterprise_verify'
+ } else if (certificationData.value) {
+ await setCurrentStepByStatus()
+ }
+}
+
const handleEnterpriseVerifySubmit = async () => {
try {
ElMessage.success('企业认证成功')
@@ -294,10 +400,23 @@ const handleEnterpriseVerifySubmit = async () => {
const handleStartContractSign = async () => {
try {
contractSignLoading.value = true
- // 调用后端接口,发起合同签署
- await certificationApi.applyContract()
+ const res = await certificationApi.applyContract()
+ const embedUrl = res?.data?.contract_sign_url
+ if (embedUrl) {
+ stepMeta.value = {
+ ...(stepMeta.value || {}),
+ contract_sign_url: embedUrl,
+ contract_sign_short_url: res?.data?.contract_sign_short_url || '',
+ }
+ }
ElMessage.success('合同签署流程已启动')
await getCertificationDetails()
+ if (embedUrl) {
+ stepMeta.value = {
+ ...(stepMeta.value || {}),
+ contract_sign_url: embedUrl,
+ }
+ }
} catch (error) {
ElMessage.error(error?.message || '发起签署失败,请稍后重试')
} finally {
@@ -357,10 +476,11 @@ const goToProfile = () => {
router.push('/profile')
}
-// 获取认证详情
-const getCertificationDetails = async () => {
+// 获取认证详情(silent=true 时不展示全页 loading,用于人工审核/平台轮询)
+const getCertificationDetails = async (opts = {}) => {
+ const silent = opts?.silent === true
try {
- loading.value = true
+ if (!silent) loading.value = true
const res = await certificationApi.getCertificationDetails()
certificationData.value = res.data
console.log('s', res)
@@ -393,9 +513,14 @@ const getCertificationDetails = async () => {
// 步骤特定元数据
stepMeta.value = res.data?.metadata || {}
await setCurrentStepByStatus()
+ return true
} catch (error) {
console.error('获取认证详情失败:', error)
ElMessage.error('获取认证详情失败')
+ if (certificationData.value) {
+ await setCurrentStepByStatus()
+ }
+ return false
} finally {
loading.value = false
}
@@ -416,20 +541,46 @@ const setCurrentStepByStatus = async () => {
currentStep.value = 'enterprise_info'
break
case 'info_pending_review':
- currentStep.value = 'manual_review'
+ if (certificationData.value?.metadata?.need_select_platform) {
+ if (await maybeGoPlatformSelectOrAuto()) return
+ } else {
+ currentStep.value = 'manual_review'
+ }
break
case 'info_submitted':
- currentStep.value = 'enterprise_verify'
+ if (
+ !certificationData.value?.metadata?.auth_url &&
+ certificationData.value?.metadata?.need_select_platform
+ ) {
+ if (await maybeGoPlatformSelectOrAuto()) return
+ } else {
+ currentStep.value = 'enterprise_verify'
+ }
break
case 'enterprise_verified':
+ if (
+ !certificationData.value?.metadata?.esign_flow_id &&
+ !certificationData.value?.metadata?.sign_task_ready
+ ) {
+ if (await maybeGoPlatformSelectOrAuto()) return
+ currentStep.value = 'platform_select'
+ } else {
+ currentStep.value = 'contract_sign'
+ }
+ break
case 'contract_applied':
+ currentStep.value = 'contract_sign'
+ startSignStatusPolling()
+ break
case 'contract_rejected':
case 'contract_expired':
currentStep.value = 'contract_sign'
+ stopSignStatusPolling()
break
case 'contract_signed':
case 'completed':
currentStep.value = 'completed'
+ stopSignStatusPolling()
// 认证完成时重新获取用户信息
if (previousStatus !== 'completed') {
try {
@@ -530,12 +681,35 @@ async function handleIframeMessage(event) {
// 初始化
onMounted(() => {
+ // 若法大大等第三方在 iframe 内回跳到本页,立即顶层打开入驻页
+ if (window.top && window.top !== window.self) {
+ const target = `${window.location.origin}/profile/certification${window.location.search || ''}`
+ window.top.location.href = target
+ return
+ }
+
window.addEventListener('message', handleIframeMessage)
- getCertificationDetails()
+ getCertificationDetails().then(async () => {
+ if (route.query?.signCallback) {
+ try {
+ await certificationApi.confirmSign()
+ await getCertificationDetails()
+ } catch (e) {
+ console.warn('签署回调确认失败,将轮询状态', e)
+ pollCertificationDetails(8)
+ }
+ }
+ })
+
+ const q = route.query || {}
+ if (q.authResult || q.openCorpId || q.clientCorpId) {
+ pollCertificationDetails(8)
+ }
})
onBeforeUnmount(() => {
window.removeEventListener('message', handleIframeMessage)
+ stopSignStatusPolling()
})
diff --git a/src/router/index.js b/src/router/index.js
index 7accde7..114ec63 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -365,6 +365,11 @@ const routes = [
component: () => import('@/pages/certification/IframeCallback.vue'),
meta: { requiresAuth: false },
},
+ {
+ path: '/certification/callback/:platform/:scene',
+ component: () => import('@/pages/certification/IframeCallback.vue'),
+ meta: { requiresAuth: false },
+ },
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
diff --git a/vite.config.js b/vite.config.js
index b35c619..4e210d9 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -125,12 +125,12 @@ export default defineConfig({
server: {
proxy: {
// // 本地开发时将 /api/v1 的请求代理到 8080 端口
- '/api/v1': {
- target: 'http://localhost:8080',
- changeOrigin: true,
- // 保持路径不变
- rewrite: path => path,
- },
+ // '/api/v1': {
+ // target: 'http://localhost:8080',
+ // changeOrigin: true,
+ // // 保持路径不变
+ // rewrite: path => path,
+ // },
// '/api/v1': {
// target: 'http://localhost:8080',
// changeOrigin: true,