diff --git a/.env b/.env index 09cea1c..180a94d 100644 --- a/.env +++ b/.env @@ -19,4 +19,8 @@ VITE_CHAT_AES_IV=345GDFED433223DF VITE_SHARE_TITLE=真爱查官网_婚姻状态核验_婚前背景互信平台 VITE_SHARE_DESC=提供个人信用评估、入职背调、信贷风控、企业风险监测等服务 VITE_SHARE_IMG=https://www.zhenaicha.com/logo.png -VITE_TOKEN_VERSION=1.0 \ No newline at end of file +VITE_TOKEN_VERSION=1.0 + +# 阿里云验证码配置(当前使用非加密模式) +VITE_ALIYUN_CAPTCHA_ENCRYPTED=false +VITE_ALIYUN_CAPTCHA_SCENE_ID=wynt39to \ No newline at end of file diff --git a/index.html b/index.html index dd41306..e0983aa 100644 --- a/index.html +++ b/index.html @@ -109,6 +109,15 @@ delete window.wx; + + + + @@ -207,6 +216,8 @@
加载中
+ +
diff --git a/src/App.vue b/src/App.vue index 72042b4..f8072ef 100644 --- a/src/App.vue +++ b/src/App.vue @@ -87,9 +87,7 @@ const handleWeixinAuthCallback = async (code) => { const tokenVersion = import.meta.env.VITE_TOKEN_VERSION || "1.1"; localStorage.setItem("tokenVersion", tokenVersion); - console.log("✅ Token saved successfully, token:", token.substring(0, 20) + "..."); - console.log("✅ Token saved to localStorage, userId:", data.value.data.userId || "unknown"); - console.log(`✅ TokenVersion set to ${tokenVersion}`); + // 验证 token 是否真的保存成功 const savedToken = localStorage.getItem("token"); diff --git a/src/auto-imports.d.ts b/src/auto-imports.d.ts index a4ca6de..7faddae 100644 --- a/src/auto-imports.d.ts +++ b/src/auto-imports.d.ts @@ -117,6 +117,7 @@ declare global { const useActiveElement: typeof import('@vueuse/core')['useActiveElement'] const useAgent: typeof import('./composables/useAgent.js')['useAgent'] const useAgentStore: typeof import('./stores/agentStore.js')['useAgentStore'] + const useAliyunCaptcha: typeof import('./composables/useAliyunCaptcha.js')['default'] const useAnimate: typeof import('@vueuse/core')['useAnimate'] const useApiFetch: typeof import('./composables/useApiFetch.js')['default'] const useAppStore: typeof import('./stores/appStore.js')['useAppStore'] diff --git a/src/components/InquireForm.vue b/src/components/InquireForm.vue index 3e4ddf1..7e12802 100644 --- a/src/components/InquireForm.vue +++ b/src/components/InquireForm.vue @@ -35,7 +35,8 @@ -
+ +
@@ -133,6 +134,7 @@ import { useUserStore } from "@/stores/userStore"; import { useDialogStore } from "@/stores/dialogStore"; import { useEnv } from "@/composables/useEnv"; import { showConfirmDialog } from "vant"; +import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha"; import Payment from "@/components/Payment.vue"; import BindPhoneOnlyDialog from "@/components/BindPhoneOnlyDialog.vue"; @@ -200,6 +202,7 @@ const dialogStore = useDialogStore(); const userStore = useUserStore(); const { isWeChat } = useEnv(); const appStore = useAppStore(); +const { runWithCaptcha } = useAliyunCaptcha(); // 响应式数据 const showPayment = ref(false); @@ -210,6 +213,14 @@ const trapezoidBgImage = ref(''); const isCountingDown = ref(false); const countdown = ref(60); +// 不需要短信验证码的产品列表(使用拼图验证) +const noSmsCodeRequiredProducts = ['marriage']; + +// 判断当前产品是否需要短信验证码 +const needSmsCode = computed(() => { + return !noSmsCodeRequiredProducts.includes(props.feature); +}); + // 使用传入的featureData或创建响应式引用 const featureData = computed(() => props.featureData || {}); @@ -349,34 +360,75 @@ function handleSubmit() { formData.idCard, (v) => isIdCardValid.value, "请输入有效的身份证号码" - ) || - !validateField( - "verificationCode", - formData.verificationCode, - (v) => v, - "请输入验证码" ) ) { return; } + // 需要短信验证码的产品,校验验证码 + if (needSmsCode.value && !formData.verificationCode) { + showToast({ message: "请输入验证码" }); + return; + } + // 不需要登录也能查询,直接提交请求 // 如果是已登录用户在微信环境且未绑定手机号,提示绑定(但不强制) if (isLoggedIn.value && !userStore.mobile && props.type !== 'promotion' && isWeChat.value) { pendingPayment.value = true; dialogStore.openBindPhone(); } else { - submitRequest(); + // 不需要短信验证码的产品,使用拼图验证 + if (!needSmsCode.value) { + runWithCaptcha( + (captchaVerifyParam) => submitRequest(captchaVerifyParam), + (res) => { + if (res.code === 200) { + handleQuerySuccess(res); + } + } + ); + } else { + // 需要短信验证码的产品,直接提交 + submitRequest().then(({ data, error }) => { + if (data?.code === 200) { + handleQuerySuccess(data); + } + }); + } } } -async function submitRequest() { +// 处理查询成功 +function handleQuerySuccess(res) { + queryId.value = res.data.id; + if (props.type === 'promotion') { + localStorage.setItem("token", res.data.accessToken); + localStorage.setItem("refreshAfter", res.data.refreshAfter); + localStorage.setItem("accessExpire", res.data.accessExpire); + const tokenVersion = import.meta.env.VITE_TOKEN_VERSION || "1.1"; + localStorage.setItem("tokenVersion", tokenVersion); + } + showPayment.value = true; + emit('submit-success', res.data); +} + +async function submitRequest(captchaVerifyParam = null) { const req = { name: formData.name, id_card: formData.idCard, - mobile: formData.mobile, - code: formData.verificationCode + mobile: formData.mobile }; + + // 需要短信验证码的产品,添加 code 字段 + if (needSmsCode.value) { + req.code = formData.verificationCode; + } + + // 不需要短信验证码的产品,添加拼图验证参数 + if (captchaVerifyParam) { + req.captchaVerifyParam = captchaVerifyParam; + } + const reqStr = JSON.stringify(req); const encodeData = aesEncrypt( reqStr, @@ -397,22 +449,7 @@ async function submitRequest() { .post(requestData) .json(); - if (data.value.code === 200) { - queryId.value = data.value.data.id; - - // 推广查询需要保存token - if (props.type === 'promotion') { - localStorage.setItem("token", data.value.data.accessToken); - localStorage.setItem("refreshAfter", data.value.data.refreshAfter); - localStorage.setItem("accessExpire", data.value.data.accessExpire); - // ⚠️ 重要:保存 token 后立即设置 tokenVersion,防止被 checkTokenVersion 清除 - const tokenVersion = import.meta.env.VITE_TOKEN_VERSION || "1.1"; - localStorage.setItem("tokenVersion", tokenVersion); - } - - showPayment.value = true; - emit('submit-success', data.value.data); - } + return { data: data.value, error }; } async function sendVerificationCode() { @@ -422,22 +459,27 @@ async function sendVerificationCode() { return; } - const { data, error } = await useApiFetch("/auth/sendSms") - .post({ mobile: formData.mobile, actionType: "query" }) - .json(); - - if (!error.value && data.value.code === 200) { - showToast({ message: "验证码发送成功", type: "success" }); - startCountdown(); - nextTick(() => { - const verificationCodeInput = document.getElementById('verificationCode'); - if (verificationCodeInput) { - verificationCodeInput.focus(); + // 使用阿里云滑块验证码 + runWithCaptcha( + (captchaVerifyParam) => + useApiFetch("/auth/sendSms") + .post({ mobile: formData.mobile, actionType: "query", captchaVerifyParam }) + .json(), + (res) => { + if (res.code === 200) { + showToast({ message: "验证码发送成功", type: "success" }); + startCountdown(); + nextTick(() => { + const verificationCodeInput = document.getElementById('verificationCode'); + if (verificationCodeInput) { + verificationCodeInput.focus(); + } + }); + } else { + showToast({ message: res.msg || "验证码发送失败,请重试" }); } - }); - } else { - showToast({ message: "验证码发送失败,请重试" }); - } + } + ); } let timer = null; diff --git a/src/composables/useAliyunCaptcha.js b/src/composables/useAliyunCaptcha.js new file mode 100644 index 0000000..cf1f154 --- /dev/null +++ b/src/composables/useAliyunCaptcha.js @@ -0,0 +1,173 @@ +import { showToast, showLoadingToast, closeToast } from "vant"; +import useApiFetch from "@/composables/useApiFetch"; + +// 阿里云验证码场景 ID +const ALIYUN_CAPTCHA_SCENE_ID = "wynt39to"; +// 是否启用加密模式(通过环境变量控制,非加密模式时前端不调用后端获取 EncryptedSceneId) +const ENABLE_ENCRYPTED =import.meta.env.VITE_ALIYUN_CAPTCHA_ENCRYPTED === "false"; + +let captchaInitialised = false; +/** 首次初始化后,SDK 会异步调用 getInstance,用此 Promise 在实例就绪后再 show */ +let captchaReadyPromise = null; +let captchaReadyResolve = null; + +async function ensureCaptchaInit() { + if (captchaInitialised || typeof window === "undefined") return; + if (typeof window.initAliyunCaptcha !== "function") return; + + captchaInitialised = true; + window.captcha = null; + window.__lastBizResponse = null; + window.__onCaptchaBizSuccess = null; + captchaReadyPromise = new Promise((resolve) => { + captchaReadyResolve = resolve; + }); + + // 非加密模式:仅传 SceneId,不调用后端接口 + if (ENABLE_ENCRYPTED) { + console.log("非加密", ALIYUN_CAPTCHA_SCENE_ID); + window.initAliyunCaptcha({ + SceneId: ALIYUN_CAPTCHA_SCENE_ID, + mode: "popup", + element: "#captcha-element", + getInstance(instance) { + window.captcha = instance; + if (typeof captchaReadyResolve === "function") { + captchaReadyResolve(); + captchaReadyResolve = null; + } + }, + captchaVerifyCallback(param) { + return typeof window.__captchaVerifyCallback === "function" + ? window.__captchaVerifyCallback(param) + : Promise.resolve({ + captchaResult: false, + bizResult: false, + }); + }, + onBizResultCallback(bizResult) { + if (typeof window.__onBizResultCallback === "function") { + window.__onBizResultCallback(bizResult); + } + window.__lastBizResponse = null; + window.__onCaptchaBizSuccess = null; + }, + slideStyle: { width: 360, height: 40 }, + language: "cn", + }); + return; + } + + // 加密模式:先从后端获取 EncryptedSceneId,再初始化 + const { data, error } = await useApiFetch("/captcha/encryptedSceneId") + .post() + .json(); + const resp = data?.value; + console.log("加密", resp); + const encryptedSceneId = resp?.data?.encryptedSceneId; + if (error?.value || !encryptedSceneId) { + showToast({ message: "获取验证码参数失败,请稍后重试" }); + captchaInitialised = false; + captchaReadyPromise = null; + captchaReadyResolve = null; + return; + } + window.initAliyunCaptcha({ + SceneId: ALIYUN_CAPTCHA_SCENE_ID, + EncryptedSceneId: encryptedSceneId, + mode: "popup", + element: "#captcha-element", + getInstance(instance) { + window.captcha = instance; + if (typeof captchaReadyResolve === "function") { + captchaReadyResolve(); + captchaReadyResolve = null; + } + }, + captchaVerifyCallback(param) { + return typeof window.__captchaVerifyCallback === "function" + ? window.__captchaVerifyCallback(param) + : Promise.resolve({ captchaResult: false, bizResult: false }); + }, + onBizResultCallback(bizResult) { + if (typeof window.__onBizResultCallback === "function") { + window.__onBizResultCallback(bizResult); + } + window.__lastBizResponse = null; + window.__onCaptchaBizSuccess = null; + }, + slideStyle: { width: 360, height: 40 }, + language: "cn", + }); +} + +/** + * 阿里云滑块验证码通用封装。 + * 依赖 index.html 中已加载的 AliyunCaptcha.js;初始化在首次调起时执行。 + * + * @param { (captchaVerifyParam: string) => Promise<{ data: Ref, error: Ref }> } bizVerify - 业务请求函数,接收滑块参数,返回 useApiFetch 的 { data, error } + * @param { (res: any) => void } [onSuccess] - 业务成功回调(code===200 时调用,传入接口返回的 data.value) + */ +export function useAliyunCaptcha() { + /** + * 先弹出滑块,通过后执行 bizVerify(captchaVerifyParam),再根据结果调用 onSuccess。 + */ + async function runWithCaptcha(bizVerify, onSuccess) { + if (typeof window === "undefined") { + showToast({ message: "验证码仅支持浏览器环境" }); + return; + } + + const loading = showLoadingToast({ + message: "安全验证加载中...", + forbidClick: true, + duration: 0, + loadingType: "spinner", + }); + + try { + window.__captchaVerifyCallback = async (captchaVerifyParam) => { + window.__lastBizResponse = null; + const { data, error } = await bizVerify(captchaVerifyParam); + const result = data?.value ?? data; + if (error?.value || !result) { + return { captchaResult: false, bizResult: false }; + } + window.__lastBizResponse = result; + const captchaOk = result.captchaVerifyResult !== false; + const bizOk = result.code === 200; + return { captchaResult: captchaOk, bizResult: bizOk }; + }; + + window.__onBizResultCallback = (bizResult) => { + if ( + bizResult === true && + window.__lastBizResponse && + typeof window.__onCaptchaBizSuccess === "function" + ) { + window.__onCaptchaBizSuccess(window.__lastBizResponse); + } + }; + + await ensureCaptchaInit(); + + // 首次初始化时 SDK 会异步调用 getInstance,需等待实例就绪后再 show + if (captchaReadyPromise) { + await captchaReadyPromise; + captchaReadyPromise = null; + } + if (!window.captcha) { + showToast({ message: "验证码未加载,请刷新页面重试" }); + return; + } + window.__onCaptchaBizSuccess = onSuccess; + window.captcha.show(); + } finally { + closeToast(); + } + } + + return { runWithCaptcha }; +} + +export default useAliyunCaptcha; diff --git a/src/views/Inquire.vue b/src/views/Inquire.vue index d9f279d..ebe3423 100644 --- a/src/views/Inquire.vue +++ b/src/views/Inquire.vue @@ -40,9 +40,9 @@ onMounted(async () => { } // 添加婚姻查询的特殊处理 - if (feature.value === 'marriage') { - showMarriageUpgradeNotice(); - } + // if (feature.value === 'marriage') { + // showMarriageUpgradeNotice(); + // } // 直接加载产品信息,不需要登录 await getProduct(); diff --git a/src/views/Login.vue b/src/views/Login.vue index 9c35d81..c697906 100644 --- a/src/views/Login.vue +++ b/src/views/Login.vue @@ -6,11 +6,13 @@ import { useUserStore } from '@/stores/userStore' import { useRouter, useRoute } from 'vue-router' import { mobileCodeLogin } from '@/api/user' import useApiFetch from '@/composables/useApiFetch' +import { useAliyunCaptcha } from '@/composables/useAliyunCaptcha' const router = useRouter() const route = useRoute() const agentStore = useAgentStore() const userStore = useUserStore() +const { runWithCaptcha } = useAliyunCaptcha() const phoneNumber = ref('') const verificationCode = ref('') @@ -35,25 +37,32 @@ async function sendVerificationCode() { return } - const { data, error } = await useApiFetch('auth/sendSms') - .post({ mobile: phoneNumber.value, actionType: 'login' }) - .json() - - if (data.value && !error.value) { - if (data.value.code === 200) { - showToast({ message: "获取成功" }); - startCountdown() - // 聚焦到验证码输入框 - nextTick(() => { - const verificationCodeInput = document.getElementById('verificationCode'); - if (verificationCodeInput) { - verificationCodeInput.focus(); - } - }); - } else { - showToast(data.value.msg) + // 使用阿里云滑块验证码 + runWithCaptcha( + (captchaVerifyParam) => + useApiFetch('auth/sendSms') + .post({ + mobile: phoneNumber.value, + actionType: 'login', + captchaVerifyParam + }) + .json(), + (res) => { + if (res.code === 200) { + showToast({ message: "获取成功" }); + startCountdown() + // 聚焦到验证码输入框 + nextTick(() => { + const verificationCodeInput = document.getElementById('verificationCode'); + if (verificationCodeInput) { + verificationCodeInput.focus(); + } + }); + } else { + showToast(res.msg) + } } - } + ) } function startCountdown() {