From 64eb1c2ebfc25f11f5757a0eef04de230fa8fa15 Mon Sep 17 00:00:00 2001
From: zhengyiming <540361168@qq.com>
Date: 星期四, 04 十二月 2025 17:52:54 +0800
Subject: [PATCH] fix: 账号密码双因子登录

---
 src/services/api/Account.ts                             |   67 ++++++++++-
 src/views/Login/Login.vue                               |   57 +++++++-
 src/services/api/typings.d.ts                           |   70 +++++++++++
 src/store/modules/user.ts                               |   31 +++-
 src/style/element/element-plus.scss                     |    7 +
 src/views/Login/components/SendVerificationCodeView.vue |   97 ++++++++++++++++
 6 files changed, 303 insertions(+), 26 deletions(-)

diff --git a/src/services/api/Account.ts b/src/services/api/Account.ts
index 3dc39aa..67f2e96 100644
--- a/src/services/api/Account.ts
+++ b/src/services/api/Account.ts
@@ -219,9 +219,17 @@
   });
 }
 
+/** 鏌ヨ绯荤粺淇℃伅 GET /api/Account/GetSystemInfo */
+export async function getSystemInfo(options?: API.RequestConfig) {
+  return request<API.GetSystemInfoOutput>('/api/Account/GetSystemInfo', {
+    method: 'GET',
+    ...(options || {}),
+  });
+}
+
 /** 鐢靛瓙绛剧櫥褰� POST /api/Account/GetTokenForUserSign */
 export async function getTokenForUserSign(body: API.AccessRequestDto, options?: API.RequestConfig) {
-  return request<API.IdentityModelTokenCacheItem>('/api/Account/GetTokenForUserSign', {
+  return request<API.IdentityModelToken>('/api/Account/GetTokenForUserSign', {
     method: 'POST',
     headers: {
       'Content-Type': 'application/json',
@@ -233,7 +241,7 @@
 
 /** 姝ゅ鍚庣娌℃湁鎻愪緵娉ㄩ噴 POST /api/Account/GetTokenForWeb */
 export async function getTokenForWeb(body: API.AccessRequestDto, options?: API.RequestConfig) {
-  return request<API.IdentityModelTokenCacheItem>('/api/Account/GetTokenForWeb', {
+  return request<API.IdentityModelToken>('/api/Account/GetTokenForWeb', {
     method: 'POST',
     headers: {
       'Content-Type': 'application/json',
@@ -313,7 +321,7 @@
 
 /** 瀵嗙爜鐧诲綍 POST /api/Account/PasswordLogin */
 export async function passwordLogin(body: API.PasswordLoginInput, options?: API.RequestConfig) {
-  return request<API.IdentityModelTokenCacheItem>('/api/Account/PasswordLogin', {
+  return request<API.IdentityModelToken>('/api/Account/PasswordLogin', {
     method: 'POST',
     headers: {
       'Content-Type': 'application/json',
@@ -328,7 +336,7 @@
   body: API.PhoneMesssageCodeLoginInput,
   options?: API.RequestConfig
 ) {
-  return request<API.IdentityModelTokenCacheItem>('/api/Account/PhoneMesssageCodeLogin', {
+  return request<API.IdentityModelToken>('/api/Account/PhoneMesssageCodeLogin', {
     method: 'POST',
     headers: {
       'Content-Type': 'application/json',
@@ -395,6 +403,51 @@
   });
 }
 
+/** 鍙屽洜瀛愮櫥褰�-绗竴姝ヨ处鍙峰瘑鐮佺櫥褰� POST /api/Account/TwoFactorLoginPassword */
+export async function twoFactorLoginPassword(
+  body: API.TwoFactorLoginPasswordInput,
+  options?: API.RequestConfig
+) {
+  return request<API.TwoFactorLoginPasswordOutput>('/api/Account/TwoFactorLoginPassword', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    data: body,
+    ...(options || {}),
+  });
+}
+
+/** 鍙屽洜瀛愮浜屾-鍙戦�侀獙璇佺爜 POST /api/Account/TwoFactorLoginSendVerificationCode */
+export async function twoFactorLoginSendVerificationCode(
+  body: API.TwoFactorLoginSendVerificationCodeInput,
+  options?: API.RequestConfig
+) {
+  return request<number>('/api/Account/TwoFactorLoginSendVerificationCode', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    data: body,
+    ...(options || {}),
+  });
+}
+
+/** 鍙屽洜瀛愮涓夋-楠岃瘉鐮佺櫥褰� POST /api/Account/TwoFactorLoginSms */
+export async function twoFactorLoginSms(
+  body: API.TwoFactorLoginSmsInput,
+  options?: API.RequestConfig
+) {
+  return request<API.IdentityModelToken>('/api/Account/TwoFactorLoginSms', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    data: body,
+    ...(options || {}),
+  });
+}
+
 /** 瑙g粦鐢ㄦ埛閭 POST /api/Account/UnbindingUserEmail */
 export async function unbindingUserEmail(
   body: API.UnbindingUserEmailInput,
@@ -445,7 +498,7 @@
   body: API.WxMiniAppPhoneLoginInput,
   options?: API.RequestConfig
 ) {
-  return request<API.IdentityModelTokenCacheItem>('/api/Account/WxMiniAppPhoneAuthLogin', {
+  return request<API.IdentityModelToken>('/api/Account/WxMiniAppPhoneAuthLogin', {
     method: 'POST',
     headers: {
       'Content-Type': 'application/json',
@@ -460,7 +513,7 @@
   body: API.WxMiniAppPhoneAuthLoginFromScanInput,
   options?: API.RequestConfig
 ) {
-  return request<API.IdentityModelTokenCacheItem>('/api/Account/WxMiniAppPhoneAuthLoginFromScan', {
+  return request<API.IdentityModelToken>('/api/Account/WxMiniAppPhoneAuthLoginFromScan', {
     method: 'POST',
     headers: {
       'Content-Type': 'application/json',
@@ -490,7 +543,7 @@
   body: API.WxMiniAppUserLoginFromScanInput,
   options?: API.RequestConfig
 ) {
-  return request<API.IdentityModelTokenCacheItem>('/api/Account/WxMiniAppUserLoginFromScan', {
+  return request<API.IdentityModelToken>('/api/Account/WxMiniAppUserLoginFromScan', {
     method: 'POST',
     headers: {
       'Content-Type': 'application/json',
diff --git a/src/services/api/typings.d.ts b/src/services/api/typings.d.ts
index e6dfa4b..573ce44 100644
--- a/src/services/api/typings.d.ts
+++ b/src/services/api/typings.d.ts
@@ -6488,6 +6488,8 @@
 
   type EnumWalletSignStatus = 1 | 10 | 100 | 999;
 
+  type EnumWalletTransactionStatus = 1 | 10 | 20 | 30 | 40 | 50;
+
   interface ExportBountyApplyData {
     /** 浼佷笟鍚嶇О */
     enterpriseName: string;
@@ -8004,6 +8006,13 @@
     amount?: number;
     remainAmount?: number;
     invoiceUrl?: string;
+    transactionStatus?: EnumWalletTransactionStatus;
+    /** 璁㈠崟鏀粯鏃堕棿 */
+    transactionDate?: string;
+    /** 鏌ヨ鍒扮殑璁㈠崟鐘舵�佷负FAIL澶辫触鎴朢EFUND閫�绁ㄦ椂锛岃繑鍥為敊璇唬鐮� */
+    transactionErrorCode?: string;
+    /** 鏌ヨ鍒扮殑璁㈠崟鐘舵�佷负FAIL澶辫触鎴朢EFUND閫�绁ㄦ椂锛岃繑鍥炲叿浣撶殑鍘熷洜銆� */
+    transactionFailReason?: string;
     checkStatus?: EnterpriseRechargeStatusEnum;
     checkTime?: string;
     checkUserId?: string;
@@ -8034,7 +8043,15 @@
     parkType?: string;
     amount?: number;
     remainAmount?: number;
+    transactionStatus?: EnumWalletTransactionStatus;
+    /** 璁㈠崟鏀粯鏃堕棿 */
+    transactionDate?: string;
+    /** 鏌ヨ鍒扮殑璁㈠崟鐘舵�佷负FAIL澶辫触鎴朢EFUND閫�绁ㄦ椂锛岃繑鍥為敊璇唬鐮� */
+    transactionErrorCode?: string;
+    /** 鏌ヨ鍒扮殑璁㈠崟鐘舵�佷负FAIL澶辫触鎴朢EFUND閫�绁ㄦ椂锛岃繑鍥炲叿浣撶殑鍘熷洜銆� */
+    transactionFailReason?: string;
     checkStatus?: EnterpriseRechargeStatusEnum;
+    status?: GetEnterpriseDrawWithListOutputStatus;
     checkTime?: string;
     checkRemark?: string;
     checkFileUrl?: string;
@@ -8047,6 +8064,8 @@
     objectData?: any;
     data?: GetEnterpriseDrawWithListOutput[];
   }
+
+  type GetEnterpriseDrawWithListOutputStatus = 10 | 20 | 21 | 22 | 30;
 
   interface GetEnterpriseMonthApplyFileOutput {
     id?: string;
@@ -9681,6 +9700,13 @@
     auditRemark?: string;
     /** 瀹℃牳鏃堕棿 */
     auditTime?: string;
+    transactionStatus?: EnumWalletTransactionStatus;
+    /** 璁㈠崟鏀粯鏃堕棿 */
+    transactionDate?: string;
+    /** 鏌ヨ鍒扮殑璁㈠崟鐘舵�佷负FAIL澶辫触鎴朢EFUND閫�绁ㄦ椂锛岃繑鍥為敊璇唬鐮� */
+    transactionErrorCode?: string;
+    /** 鏌ヨ鍒扮殑璁㈠崟鐘舵�佷负FAIL澶辫触鎴朢EFUND閫�绁ㄦ椂锛岃繑鍥炲叿浣撶殑鍘熷洜銆� */
+    transactionFailReason?: string;
     financeAuditStatus?: EnumParkBountyTradeDetailAuditStatus;
     /** 璐㈠姟瀹℃牳澶囨敞 */
     financeAuditRemark?: string;
@@ -10617,6 +10643,10 @@
     signName?: string;
   }
 
+  interface GetSystemInfoOutput {
+    openTwoFactorLogin?: boolean;
+  }
+
   interface GetTagsInput {
     /** 绫诲瀷锛�0浜у搧鏍囩锛�1璧勮鏍囩锛�3蹇嵎璇勮鏍囩 */
     type?: number;
@@ -11549,6 +11579,15 @@
 
   interface IanaTimeZone {
     timeZoneName?: string;
+  }
+
+  interface IdentityModelToken {
+    accessToken?: string;
+    expiresIn?: number;
+    creationTime?: string;
+    refreshToken?: string;
+    /** 鐢ㄦ埛Id */
+    userId?: string;
   }
 
   interface IdentityModelTokenCacheItem {
@@ -22785,6 +22824,33 @@
 
   type TransferToStatusEnum = 1 | 2;
 
+  interface TwoFactorLoginPasswordInput {
+    clientId?: string;
+    /** 鐧诲綍鍚� */
+    loginName: string;
+    /** 鐧诲綍瀵嗙爜 */
+    password: string;
+  }
+
+  interface TwoFactorLoginPasswordOutput {
+    /** 鐧诲綍瀵嗛挜 */
+    loginKey?: string;
+    /** 鎵嬫満鍙� */
+    phoneNumber?: string;
+  }
+
+  interface TwoFactorLoginSendVerificationCodeInput {
+    /** 鐧诲綍瀵嗛挜 */
+    loginKey?: string;
+  }
+
+  interface TwoFactorLoginSmsInput {
+    /** 鐧诲綍瀵嗛挜 */
+    loginKey?: string;
+    /** 楠岃瘉鐮� */
+    code?: string;
+  }
+
   interface TypeApiDescriptionModel {
     baseType?: string;
     isEnum?: boolean;
@@ -25929,6 +25995,8 @@
     iv: string;
     /** 鑾峰彇浼氳瘽瀵嗛挜 */
     sessionKey: string;
+    /** 閴存潈 */
+    sign?: string;
     /** 灏忕▼搴廜penId */
     openId: string;
     wxMiniApp?: WxMiniAppEnum;
@@ -25941,6 +26009,8 @@
     iv: string;
     /** 鑾峰彇浼氳瘽瀵嗛挜 */
     sessionKey: string;
+    /** 閴存潈 */
+    sign?: string;
     /** 灏忕▼搴廜penId */
     openId: string;
     wxMiniApp?: WxMiniAppEnum;
diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts
index 49bd21d..c226b2e 100644
--- a/src/store/modules/user.ts
+++ b/src/store/modules/user.ts
@@ -68,15 +68,7 @@
           .then((res) => {
             if (res) {
               console.log('res: ', res);
-              this.setToken(res.accessToken);
-
-              const accountInfo = getAccountInfoFromAccessToken(res.accessToken);
-
-              this.setName(accountInfo.name);
-              this.setAccountInfo(accountInfo);
-
-              // 鑾峰彇鐢ㄦ埛淇℃伅
-              this.setUserInfo(res);
+              this.loginSuccess(res);
 
               resolve();
             }
@@ -87,6 +79,27 @@
       });
     },
 
+    async twoFactorLoginSms(params: API.TwoFactorLoginSmsInput) {
+      try {
+        let res = await accountServices.twoFactorLoginSms(params, { showLoading: false });
+        if (res) {
+          this.loginSuccess(res);
+        }
+      } catch (error) {}
+    },
+
+    loginSuccess(res: API.IdentityModelToken) {
+      this.setToken(res.accessToken);
+
+      const accountInfo = getAccountInfoFromAccessToken(res.accessToken);
+
+      this.setName(accountInfo.name);
+      this.setAccountInfo(accountInfo);
+
+      // 鑾峰彇鐢ㄦ埛淇℃伅
+      this.setUserInfo(res);
+    },
+
     // 鐧诲嚭 娓呯┖缂撳瓨
     logout(redirectPath = '/') {
       return new Promise(async (resolve) => {
diff --git a/src/style/element/element-plus.scss b/src/style/element/element-plus.scss
index ab5713f..4262378 100644
--- a/src/style/element/element-plus.scss
+++ b/src/style/element/element-plus.scss
@@ -129,3 +129,10 @@
     }
   }
 }
+
+.send-code-message-box {
+  .el-message-box__container,
+  .el-message-box__message {
+    width: 100%;
+  }
+}
diff --git a/src/views/Login/Login.vue b/src/views/Login/Login.vue
index 774987f..3d476ee 100644
--- a/src/views/Login/Login.vue
+++ b/src/views/Login/Login.vue
@@ -137,6 +137,8 @@
 
 import closeEye from '@/assets/svgIcons/close_eye.svg?component';
 import openEye from '@/assets/svgIcons/close_eye.svg?component';
+import * as accountServices from '@/services/api/Account';
+import SendVerificationCodeView from './components/SendVerificationCodeView.vue';
 // import { useSettingStoreHook } from '@/store/modules/settings';
 import Config from '@config/config';
 
@@ -175,16 +177,44 @@
       return;
     }
     loading.value = true;
-    await userStore.loginByUsername({
-      userName: unref(user),
-      userPassword: unref(pwd),
-      clientId: 'goverend-admin-app-client',
-    });
-    loading.value = false;
-    router.push({
-      path: redirect.value || '/',
-      query: otherQuery.value,
-    });
+    let systemInfo = await getSystemInfo();
+    if (systemInfo.openTwoFactorLogin) {
+      let twoFactorLoginPasswordRes = await accountServices.twoFactorLoginPassword({
+        loginName: unref(user),
+        password: unref(pwd),
+        clientId: 'goverend-admin-app-client',
+      });
+      loading.value = false;
+      ElMessageBox({
+        title: `鍙戦�侀獙璇佺爜鍒�${twoFactorLoginPasswordRes.phoneNumber}`,
+        customClass: 'send-code-message-box',
+        //@ts-ignore
+        modalClass: 'send-code-message-box-model',
+        showConfirmButton: false,
+        message: h(SendVerificationCodeView, {
+          phoneNumber: twoFactorLoginPasswordRes.phoneNumber,
+          loginKey: twoFactorLoginPasswordRes.loginKey,
+          onSuccess: () => {
+            router.push({
+              path: redirect.value || '/',
+              query: otherQuery.value,
+            });
+            document.querySelector('.send-code-message-box-model').remove();
+          },
+        }),
+      });
+    } else {
+      await userStore.loginByUsername({
+        userName: unref(user),
+        userPassword: unref(pwd),
+        clientId: 'goverend-admin-app-client',
+      });
+      loading.value = false;
+      router.push({
+        path: redirect.value || '/',
+        query: otherQuery.value,
+      });
+    }
   } catch (error) {
     console.log(error);
     // ElMessage({
@@ -195,6 +225,13 @@
 };
 const beforeLog = useDebounceFn(onLogin, 1000);
 
+async function getSystemInfo() {
+  let res = await accountServices.getSystemInfo({
+    showLoading: false,
+  });
+  return res;
+}
+
 function onUserFocus() {
   addClass(document.querySelector('.user'), 'focus');
 }
diff --git a/src/views/Login/components/SendVerificationCodeView.vue b/src/views/Login/components/SendVerificationCodeView.vue
new file mode 100644
index 0000000..6af7c12
--- /dev/null
+++ b/src/views/Login/components/SendVerificationCodeView.vue
@@ -0,0 +1,97 @@
+<template>
+  <ProForm :model="form" ref="formRef" class="el-message-box__input">
+    <ProFormItemV2
+      prop="code"
+      :check-rules="[{ message: '璇疯緭鍏ラ獙璇佺爜' }]"
+      label-width="0px"
+      class="pro-form-item-label-hidden"
+    >
+      <ProFormText v-model.trim="form.code" placeholder="璇疯緭鍏ラ獙璇佺爜">
+        <template #suffix>
+          <ProFormCaptcha
+            :onGetCaptcha="onGetCaptcha"
+            phonePropName="loginKey"
+            class="code-btn"
+            link
+          ></ProFormCaptcha>
+        </template>
+      </ProFormText>
+    </ProFormItemV2>
+    <div class="el-message-box__btns" style="padding-top: 0">
+      <el-button type="primary" @click="handleConfirm" :loading="loading">纭畾</el-button>
+    </div>
+  </ProForm>
+</template>
+
+<script setup lang="ts">
+import { ProForm, ProFormText, ProFormCaptcha, ProFormItemV2 } from '@bole-core/components';
+import { FormInstance } from 'element-plus';
+import * as accountServices from '@/services/api/Account';
+import { useUserStore } from '@/store/modules/user';
+
+defineOptions({
+  name: 'SendVerificationCodeView',
+});
+
+type Props = {
+  /** 鐧诲綍瀵嗛挜 */
+  loginKey?: string;
+  /** 鎵嬫満鍙� */
+  phoneNumber?: string;
+};
+
+const props = withDefaults(defineProps<Props>(), {});
+
+const emit = defineEmits<{
+  (e: 'success'): void;
+}>();
+
+const form = reactive({
+  loginKey: props.loginKey,
+  code: '',
+});
+
+const loading = ref(false);
+
+async function onGetCaptcha() {
+  await accountServices.twoFactorLoginSendVerificationCode(
+    {
+      loginKey: props.loginKey,
+    },
+    { showLoading: false }
+  );
+}
+
+const formRef = ref<FormInstance>();
+
+const handleConfirm = () => {
+  if (!formRef.value) return;
+  formRef.value.validate((valid) => {
+    if (valid) {
+      twoFactorLoginSms();
+    } else {
+      return;
+    }
+  });
+};
+
+const userStore = useUserStore();
+
+async function twoFactorLoginSms() {
+  try {
+    loading.value = true;
+    await userStore.twoFactorLoginSms({
+      loginKey: props.loginKey,
+      code: form.code,
+    });
+    emit('success');
+  } catch (error) {
+  } finally {
+    loading.value = false;
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@use '@/style/common.scss' as *;
+</style>

--
Gitblit v1.9.1