Эх сурвалжийг харах

登录页-版本更新&时间差异&登录接口限流

Michael Wang 3 жил өмнө
parent
commit
9dfcfcdd48

+ 3 - 1
src/api/login.ts

@@ -28,6 +28,8 @@ export async function loginApi(
 export async function getStudentInfoBySessionApi() {
   return httpApp.get<any>("/api/ecs_core/student/getStudentInfoBySession", {
     setGlobalMask: true,
+    noErrorMessage: true,
+    "axios-retry": { retries: 3 },
   });
 }
 
@@ -35,6 +37,6 @@ export async function getStudentInfoBySessionApi() {
 export async function getStudentSpecialtyNameListApi() {
   return httpApp.get<string[]>(
     "/api/ecs_exam_work/exam_student/specialtyNameList/",
-    { setGlobalMask: true }
+    { setGlobalMask: true, noErrorMessage: true, "axios-retry": { retries: 3 } }
   );
 }

+ 103 - 36
src/features/UserLogin/UserLogin.vue

@@ -1,20 +1,22 @@
 <script lang="ts" setup>
 import {
-  loginApi,
   getStudentInfoBySessionApi,
   getStudentSpecialtyNameListApi,
+  loginApi,
 } from "@/api/login";
+import { DOMAIN, VITE_GIT_REPO_VERSION } from "@/constants/constants";
+import { useTimers } from "@/setups/useTimers";
 import { store } from "@/store/store";
 import { createUserDetailLog } from "@/utils/logger";
-import { useRouter } from "vue-router";
-import { getElectronConfig } from "./useElectronConfig";
-import { getGeeTestConfig } from "./useGeeTestConfig";
-import { DOMAIN, VITE_GIT_REPO_VERSION } from "@/constants/constants";
+import { CloseCircleOutline, LockClosed, Person } from "@vicons/ionicons5";
+import { FormItemInst, FormRules } from "naive-ui";
 import { onMounted, onUnmounted, watch } from "vue";
-import { Person, LockClosed, CloseCircleOutline } from "@vicons/ionicons5";
-import { FormRules, FormItemInst } from "naive-ui";
+import { useRouter } from "vue-router";
 import GeeTest from "./GeeTest.vue";
 import GlobalNotice from "./GlobalNotice.vue";
+import { getElectronConfig } from "./useElectronConfig";
+import { getGeeTestConfig } from "./useGeeTestConfig";
+import { limitLogin } from "./useLimitLogin";
 
 logger({
   cnl: ["console", "local", "server"],
@@ -22,6 +24,8 @@ logger({
   act: "首次渲染",
 });
 
+const { addTimeout } = useTimers();
+
 //#region service worker通知是否有版本更新
 let newVersionAvailable = $ref(false);
 const checkNewVersionListener = () => {
@@ -63,7 +67,7 @@ const productName = $computed(
 const allowLoginType = $computed(() => QECSConfig.LOGIN_TYPE ?? []);
 
 let disableLoginBtn = $computed(
-  () => false
+  () => disableLoginBtnBecauseNotTimeout
   // (isElectron() &&
   //   (this.disableLoginBtnBecauseRemoteApp ||
   //     this.disableLoginBtnBecauseVCam)) ||
@@ -72,8 +76,6 @@ let disableLoginBtn = $computed(
   // this.disableLoginBtnBecauseNotAllowedNative
 );
 
-let loginBtnLoading = $ref(false);
-
 //#region form校验
 const domain = DOMAIN;
 type FormModel = {
@@ -132,19 +134,74 @@ watch(
 );
 //#endregion
 
+/** 有版本更新返回true,函数内部处理;没版本更新则返回false */
+async function checkNewVersion(): Promise<boolean> {
+  let myHeaders = new Headers();
+  myHeaders.append("Content-Type", "application/javascript");
+  myHeaders.append("Cache-Control", "no-cache");
+  const response = await fetch(
+    [...document.scripts].at(-1)?.src + "?x" + Date.now(),
+    {
+      method: import.meta.env.DEV ? "GET" : "HEAD",
+      headers: myHeaders,
+    }
+  );
+  if (!response.ok || newVersionAvailable) {
+    if (
+      response.ok &&
+      newVersionAvailable &&
+      localStorage.getItem("__swReload")
+    ) {
+      logger({
+        cnl: ["local", "server"],
+        pgu: "AUTO",
+        dtl: "service worker刷新失败",
+      });
+      $message.info("请重新打开程序。", { duration: 2 * 24 * 60 * 60 });
+      return true;
+    }
+    logger({
+      cnl: ["local", "server"],
+      pgu: "AUTO",
+      dtl: "新版本发布后,客户端自动刷新",
+    });
+    $message.info("正在获取新版本...");
+    localStorage.setItem("__swReload", "anything");
+    // disableLoginBtnBecauseRefreshServiceWorker = true;
+    await new Promise((resolve) => setTimeout(resolve, 1000));
+    location.reload();
+
+    return true;
+  }
+  return false;
+}
+
 const router = useRouter();
 
+let loginBtnLoading = $ref(false);
+let disableLoginBtnBecauseNotTimeout = $ref(false);
 async function loginForuser() {
+  if (await checkNewVersion()) return;
+
   if (await formRef.validate().catch(() => true)) return;
 
   if (isGeeTestEnabled) {
     if (!captchaObj?.getValidate()) {
       $message.error("请完成验证");
-      loginBtnLoading = false;
       return;
     }
   }
 
+  loginBtnLoading = true;
+  // 登录接口调一次必然间隔10秒以上
+  disableLoginBtnBecauseNotTimeout = true;
+  addTimeout(() => (disableLoginBtnBecauseNotTimeout = false), 10 * 1000);
+
+  if (await limitLogin()) {
+    loginBtnLoading = false;
+    return;
+  }
+
   logger({
     pgn: "登录页面",
     cnl: ["local", "server"],
@@ -165,39 +222,49 @@ async function loginForuser() {
       seccode: geeRes.geetest_seccode, // geeForm[2].value,
     };
   }
-  const res = await loginApi(
-    formValue.accountType,
-    formValue.accountValue,
-    formValue.password,
-    domain,
-    QECSConfig.ROOT_ORG_ID,
-    geeParams
-  );
 
-  if (res.data.code === "200") {
-    errorInfo = "";
-    // 准备下面的登录token
-    store.user = res.data.content;
-  } else {
-    errorInfo = res.data.desc;
-    captchaObj.destroy();
-    resetGeeTime = Date.now();
-    logger({
-      pgu: "AUTO",
-      act: "点击登录-res-error",
-      stk: res.data.code + res.data.desc,
-      cnl: ["console", "server"],
-    });
-    return;
+  try {
+    const res = await loginApi(
+      formValue.accountType,
+      formValue.accountValue,
+      formValue.password,
+      domain,
+      QECSConfig.ROOT_ORG_ID,
+      geeParams
+    );
+
+    if (res.data.code === "200") {
+      errorInfo = "";
+      // 准备下面的登录token
+      store.user = res.data.content;
+    } else {
+      errorInfo = res.data.desc;
+      captchaObj?.destroy();
+      resetGeeTime = Date.now();
+      logger({
+        pgu: "AUTO",
+        act: "点击登录-res-error",
+        stk: res.data.code + res.data.desc,
+        cnl: ["console", "server"],
+      });
+      return;
+    }
+
+    await afterLogin(res);
+  } finally {
+    loginBtnLoading = false;
   }
+}
 
+/** 登录成功后,取学生信息和跳转 */
+async function afterLogin(loginRes: any) {
   try {
     const [{ data: student }, { data: specialty }] = await Promise.all([
       getStudentInfoBySessionApi(),
       getStudentSpecialtyNameListApi(),
     ]);
     const user = {
-      ...res.data.content,
+      ...loginRes.data.content,
       ...student,
       specialty: specialty.join(),
       schoolDomain: domain,
@@ -211,7 +278,7 @@ async function loginForuser() {
     logger({
       cnl: ["local", "server"],
       act: "登录失败",
-      dtl: "getStudentInfoBySession失败",
+      dtl: "getStudentInfoBySession/getStudentSpecialtyNameListApi失败",
     });
     $message.error("获取学生信息失败,请重试!", {
       duration: 15,

+ 36 - 0
src/features/UserLogin/useLimitLogin.ts

@@ -0,0 +1,36 @@
+import { tryLimit } from "@/utils/tryLimit";
+
+/** 本函数确保无需catch
+ *  @returns true: 限流。 false: 不限 */
+export async function limitLogin(): Promise<boolean> {
+  const { limitResult, serverOk } = await tryLimit({
+    action: "login",
+    limit: 500,
+  });
+  logger({
+    cnl: ["server"],
+    pgn: "登录页面",
+    act: "login clicked",
+    key: "登录限流API call",
+  });
+  if (!limitResult) {
+    logger({
+      cnl: ["server"],
+      pgn: "登录页面",
+      act: "登录被限流",
+      key: "登录限流API call",
+      ext: { serverOk },
+    });
+    $message.warning("网络繁忙,请稍后再试。");
+    await new Promise((resolve) => setTimeout(resolve, 3000));
+    return true;
+  } else {
+    logger({
+      cnl: ["server"],
+      pgn: "登录页面",
+      key: "登录限流API call",
+      act: "登录未限流",
+    });
+    return false;
+  }
+}

+ 4 - 0
src/plugins/axiosApp.ts

@@ -3,6 +3,7 @@ import { loadProgressBar } from "axios-progress-bar";
 import { notifyInvalidTokenThrottled } from "./axiosNotice";
 import axiosRetry from "axios-retry";
 import { store } from "@/store/store";
+import moment from "moment";
 
 const config = {
   // baseURL: process.env.baseURL || process.env.apiUrl || ""
@@ -61,6 +62,9 @@ _axiosApp.interceptors.response.use(
     if (response.config.setGlobalMask) {
       store.decreaseGlobalMaskCount("axios");
     }
+    const serverDate = response.headers.date;
+    // console.log(response.headers, serverDate);
+    store.updateTimeDifference(moment(serverDate).diff(moment()));
     return response;
   },
   (error) => {

+ 1 - 2
src/plugins/axiosNotice.ts

@@ -1,6 +1,5 @@
 import { throttle } from "lodash-es";
-import { useRouter } from "vue-router";
-const router = useRouter();
+import router from "@/router";
 
 export const notifyInvalidTokenThrottled = throttle(
   () => {

+ 3 - 0
src/store/store.ts

@@ -64,6 +64,9 @@ export const useStore = defineStore("ecs", {
         store.globalMaskCount = 0;
       }
     },
+    updateTimeDifference(timeDiff: number) {
+      store.sysTime.difference = timeDiff;
+    },
     updateSiteMessagesTimeStamp() {
       store.siteMessagesTimeStamp = Date.now();
     },

+ 1 - 1
tsconfig.json

@@ -10,7 +10,7 @@
     "isolatedModules": true,
     "resolveJsonModule": true,
     "esModuleInterop": true,
-    "lib": ["esnext", "dom"],
+    "lib": ["esnext", "dom", "DOM.Iterable"],
     "types": [
       "node",
       "./node_modules/vite/client",