瀏覽代碼

移植日志上报代码

shmily1213 1 月之前
父節點
當前提交
8dc6d9bee0

+ 3 - 1
src/api/flyio.ts

@@ -62,6 +62,7 @@ fly.interceptors.response.use(
       const { code, msg } = result;
       if (code === 401) {
         logout();
+        return Promise.reject({ code: 401, msg: '未授权,请重新登录', result });
       } else {
         if (code !== 200) {
           uni.$ie.showToast(msg);
@@ -71,7 +72,8 @@ fly.interceptors.response.use(
         }
       }
     } catch (err) {
-      return Promise.reject(result);
+      // 响应数据格式异常,reject 错误对象而不是 result
+      return Promise.reject(err);
     }
   },
   function (err: Error, promise: Promise<any>) {

+ 277 - 273
src/common/enum.ts

@@ -2,170 +2,174 @@
  * 应用配置项的 key
  */
 export enum EnumAppConfigKey {
-    /**
-     * 短信验证码是否开启图形验证
-     */
-    SMS_CAPTCHA_ENABLE = 'sys.sms.captchaEnabled'
+  /**
+   * 短信验证码是否开启图形验证
+   */
+  SMS_CAPTCHA_ENABLE = 'sys.sms.captchaEnabled',
+  /**
+   * app 是否开启日志上报
+   */
+  APP_LOG_REPORT_ENABLE = 'app.log.report.enable'
 }
 
 /**
  * 短信发送类型
  */
 export enum EnumSmsApiType {
-    /**
-     * 无需校验 无需token 发送短信-适用于登录
-     */
-    NO_VALIDATION_NO_TOKEN = 'NoValidationNoToken',
-    /**
-     * 需校验 需要 token 发送短信-适用于注册
-     */
-    NO_TOKEN = 'NoToken',
-    /**
-     * 普通发送短信
-     */
-    NORMAL = 'normal'
+  /**
+   * 无需校验 无需token 发送短信-适用于登录
+   */
+  NO_VALIDATION_NO_TOKEN = 'NoValidationNoToken',
+  /**
+   * 需校验 需要 token 发送短信-适用于注册
+   */
+  NO_TOKEN = 'NoToken',
+  /**
+   * 普通发送短信
+   */
+  NORMAL = 'normal'
 }
 
 /**
  * 短信类型
  */
 export enum EnumSmsType {
-    /**
-     * code
-     */
-    CODE = 'CODE',
-    /**
-     * ecard
-     */
-    ECARD = 'ECARD',
-    /**
-     * password
-     */
-    PASSWORD = 'PASSWORD'
+  /**
+   * code
+   */
+  CODE = 'CODE',
+  /**
+   * ecard
+   */
+  ECARD = 'ECARD',
+  /**
+   * password
+   */
+  PASSWORD = 'PASSWORD'
 }
 
 export enum EnumDictName {
-    /**
-     * 考生类别
-     */
-    EXAM_TYPE = 'exam_type'
+  /**
+   * 考生类别
+   */
+  EXAM_TYPE = 'exam_type'
 }
 
 export enum STATIC_PAGE_PATH {
-    /**
-     * 登录
-     */
-    LOGIN = '/pagesSystem/pages/login/login',
-    /**
-     * 注册
-     */
-    REGISTER = '/pagesSystem/pages/register/register',
-    /**
-     * 找回密码
-     */
-    FIND_PASSWORD = '/pagesSystem/pages/find-password/find-password',
-    /**
-     * 修改密码
-     */
-    CHANGE_PASSWORD = '/pagesSystem/pages/change-password/change-password',
-    /**
-     * 绑定手机号
-     */
-    BIND_PHONE = '/pagesSystem/pages/bind-phone/bind-phone'
+  /**
+   * 登录
+   */
+  LOGIN = '/pagesSystem/pages/login/login',
+  /**
+   * 注册
+   */
+  REGISTER = '/pagesSystem/pages/register/register',
+  /**
+   * 找回密码
+   */
+  FIND_PASSWORD = '/pagesSystem/pages/find-password/find-password',
+  /**
+   * 修改密码
+   */
+  CHANGE_PASSWORD = '/pagesSystem/pages/change-password/change-password',
+  /**
+   * 绑定手机号
+   */
+  BIND_PHONE = '/pagesSystem/pages/bind-phone/bind-phone'
 }
 
 export enum EnumExamMode {
-    /**
-     * 练习
-     */
-    PRACTICE = 1,
-    /**
-     * 考试
-     */
-    EXAM = 2
+  /**
+   * 练习
+   */
+  PRACTICE = 1,
+  /**
+   * 考试
+   */
+  EXAM = 2
 }
 
 /**
  * 题目类型
  */
 export enum EnumQuestionType {
-    /**
-     * 单选
-     */
-    SINGLE_CHOICE = 1,
-    /**
-     * 多选
-     */
-    MULTIPLE_CHOICE = 2,
-    /**
-     * 判断
-     */
-    JUDGMENT = 3,
-    /**
-     * 填空
-     */
-    FILL_IN_THE_BLANK = 4,
-    /**
-     * 主观题
-     */
-    SUBJECTIVE = 5,
-    /**
-     * 简答
-     */
-    SHORT_ANSWER = 6,
-    /**
-     * 问答题
-     */
-    ESSAY = 7,
-    /**
-     * 分析题
-     */
-    ANALYSIS = 8,
+  /**
+   * 单选
+   */
+  SINGLE_CHOICE = 1,
+  /**
+   * 多选
+   */
+  MULTIPLE_CHOICE = 2,
+  /**
+   * 判断
+   */
+  JUDGMENT = 3,
+  /**
+   * 填空
+   */
+  FILL_IN_THE_BLANK = 4,
+  /**
+   * 主观题
+   */
+  SUBJECTIVE = 5,
+  /**
+   * 简答
+   */
+  SHORT_ANSWER = 6,
+  /**
+   * 问答题
+   */
+  ESSAY = 7,
+  /**
+   * 分析题
+   */
+  ANALYSIS = 8,
 
-    /**
-     * 阅读题
-     */
-    OTHER = 99
+  /**
+   * 阅读题
+   */
+  OTHER = 99
 }
 
 /**
  * 用户类型
  */
 export enum EnumUserType {
-    /**
-     * 系统用户
-     */
-    SYSTEM = '00',
-    /**
-     * 学生
-     */
-    STUDENT = '01',
-    /**
-     * 教师
-     */
-    TEACHER = '11',
-    /**
-     * 代理商
-     */
-    AGENT = '10',
-    /**
-     * 机构
-     */
-    AGENCY = '12'
+  /**
+   * 系统用户
+   */
+  SYSTEM = '00',
+  /**
+   * 学生
+   */
+  STUDENT = '01',
+  /**
+   * 教师
+   */
+  TEACHER = '11',
+  /**
+   * 代理商
+   */
+  AGENT = '10',
+  /**
+   * 机构
+   */
+  AGENCY = '12'
 }
 
 /**
  * 考试类型
  */
 export enum EnumExamRecordType {
-    /**
-     * 模拟考试
-     */
-    SIMULATED = 'simulated',
-    /**
-     * 组卷作业
-     */
-    HOMEWORK = 'homework'
+  /**
+   * 模拟考试
+   */
+  SIMULATED = 'simulated',
+  /**
+   * 组卷作业
+   */
+  HOMEWORK = 'homework'
 }
 
 
@@ -173,192 +177,192 @@ export enum EnumExamRecordType {
  * 绑定场景
  */
 export enum EnumBindScene {
-    /**
-     * 注册
-     */
-    REGISTER = 'register',
-    /**
-     * 注册绑定
-     */
-    REGISTER_BIND = 'register_bind',
-    /**
-     * 登录绑定
-     */
-    LOGIN_BIND = 'login_bind',
-    /**
-     * 老师或者代理商完善信息
-     */
-    IMPROVE = 'improve'
+  /**
+   * 注册
+   */
+  REGISTER = 'register',
+  /**
+   * 注册绑定
+   */
+  REGISTER_BIND = 'register_bind',
+  /**
+   * 登录绑定
+   */
+  LOGIN_BIND = 'login_bind',
+  /**
+   * 老师或者代理商完善信息
+   */
+  IMPROVE = 'improve'
 }
 
 export enum EnumExamType {
-    /**
-     * 职高对口升学
-     */
-    VHS = 'VHS',
-    /**
-     * 单招(应届普高)
-     */
-    OHS = 'OHS',
-    /**
-     * 单招(中职)
-     */
-    SVS = 'SVS'
+  /**
+   * 职高对口升学
+   */
+  VHS = 'VHS',
+  /**
+   * 单招(应届普高)
+   */
+  OHS = 'OHS',
+  /**
+   * 单招(中职)
+   */
+  SVS = 'SVS'
 }
 
 export enum EnumSimulatedRecordStatus {
-    /**
-     * 空卷
-     */
-    INIT = 1,
-    /**
-     * 签到
-     */
-    SIGN = 2,
-    /**
-     * 考试
-     */
-    EXAM = 3,
-    /**
-     * 交卷
-     */
-    SUBMIT = 4,
-    /**
-     * 阅卷
-     */
-    REVIEW = 5,
-    /**
-     * 发布
-     */
-    PUBLISH = 6,
-    /**
-     * 关闭
-     */
-    CLOSE = 7,
+  /**
+   * 空卷
+   */
+  INIT = 1,
+  /**
+   * 签到
+   */
+  SIGN = 2,
+  /**
+   * 考试
+   */
+  EXAM = 3,
+  /**
+   * 交卷
+   */
+  SUBMIT = 4,
+  /**
+   * 阅卷
+   */
+  REVIEW = 5,
+  /**
+   * 发布
+   */
+  PUBLISH = 6,
+  /**
+   * 关闭
+   */
+  CLOSE = 7,
 }
 
 export enum EnumPaperType {
-    /**
-     * 练习
-     */
-    PRACTICE = 'Practice',
-    /**
-     * 考试
-     */
-    SIMULATED = 'Simulated',
-    /**
-     * 教材同步练习
-     */
-    COURSE = 'Course',
-    /**
-     * 测试卷
-     */
-    TEST = 'Test'
+  /**
+   * 练习
+   */
+  PRACTICE = 'Practice',
+  /**
+   * 考试
+   */
+  SIMULATED = 'Simulated',
+  /**
+   * 教材同步练习
+   */
+  COURSE = 'Course',
+  /**
+   * 测试卷
+   */
+  TEST = 'Test'
 }
 
 export enum EnumReviewMode {
-    /**
-     * 交卷后评卷
-     */
-    AFTER_SUBMIT = 1,
-    /**
-     * 答完一题就评卷
-     */
-    DURING_ANSWER = 2
+  /**
+   * 交卷后评卷
+   */
+  AFTER_SUBMIT = 1,
+  /**
+   * 答完一题就评卷
+   */
+  DURING_ANSWER = 2
 }
 
 export enum EnumUserRole {
-    /**
-     * 普通用户
-     */
-    NORMAL = 'normal',
-    /**
-     * 游客
-     */
-    GUEST = 'guest',
-    /**
-     * 会员
-     */
-    VIP = 'vip',
-    /**
-     * 代理商
-     */
-    AGENT = 'agent',
-    /**
-     * 教师
-     */
-    TEACHER = 'teacher'
+  /**
+   * 普通用户
+   */
+  NORMAL = 'normal',
+  /**
+   * 游客
+   */
+  GUEST = 'guest',
+  /**
+   * 会员
+   */
+  VIP = 'vip',
+  /**
+   * 代理商
+   */
+  AGENT = 'agent',
+  /**
+   * 教师
+   */
+  TEACHER = 'teacher'
 }
 
 export enum EnumEvent {
-    /**
-     * 打开VIP弹窗
-     */
-    OPEN_VIP_POPUP = 'OPEN_VIP_POPUP'
+  /**
+   * 打开VIP弹窗
+   */
+  OPEN_VIP_POPUP = 'OPEN_VIP_POPUP'
 }
 
 /**
  * 卡类型
  */
 export enum CardType {
-    VIP = 1,
-    DEPT = 2,
-    PLATFORM = 6,
-    /**
-     * 体验卡
-     */
-    EXPERIENCE = 9
+  VIP = 1,
+  DEPT = 2,
+  PLATFORM = 6,
+  /**
+   * 体验卡
+   */
+  EXPERIENCE = 9
 }
 
 export enum EnumPaperWorkState {
-    /**
-     * 未完成
-     */
-    NOT_COMPLETED = 2,
-    /**
-     * 已完成
-     */
-    COMPLETED = 4
+  /**
+   * 未完成
+   */
+  NOT_COMPLETED = 2,
+  /**
+   * 已完成
+   */
+  COMPLETED = 4
 }
 
 export enum EnumPaperBuildType {
-    /**
-     * 定向智能
-     */
-    EXACT_INTELLIGENT = 'ExactIntelligent',
-    /**
-     * 全量智能
-     */
-    FULL_INTELLIGENT = 'FullIntelligent',
-    /**
-     * 定向手动
-     */
-    EXACT_HAND = 'ExactHand',
-    /**
-     * 全量手动
-     */
-    FULL_HAND = 'FullHand'
+  /**
+   * 定向智能
+   */
+  EXACT_INTELLIGENT = 'ExactIntelligent',
+  /**
+   * 全量智能
+   */
+  FULL_INTELLIGENT = 'FullIntelligent',
+  /**
+   * 定向手动
+   */
+  EXACT_HAND = 'ExactHand',
+  /**
+   * 全量手动
+   */
+  FULL_HAND = 'FullHand'
 }
 
 export enum EnumBrochureType {
-    INTRODUCTION = 1, // 学校概况
-    ENROLL_RULE = 2, // 招生简章
-    EXAM_TIME = 3, // 考试大纲
-    WORK = 4, // 就业
-    OTHER_RULE = 5 // 其它
+  INTRODUCTION = 1, // 学校概况
+  ENROLL_RULE = 2, // 招生简章
+  EXAM_TIME = 3, // 考试大纲
+  WORK = 4, // 就业
+  OTHER_RULE = 5 // 其它
 }
 
 export enum EnumSimulatePickType {
-    DANGER = 0,
-    NORMAL = 1,
-    SAFETY = 2
+  DANGER = 0,
+  NORMAL = 1,
+  SAFETY = 2
 }
 
 export enum EnumScoreLock {
-    // 待锁定状态
-    locking = -1,
-    // 正常状态
-    unlock = 0,
-    // 已锁定状态
-    locked = 1
+  // 待锁定状态
+  locking = -1,
+  // 正常状态
+  unlock = 0,
+  // 已锁定状态
+  locked = 1
 }

+ 1 - 0
src/global.d.ts

@@ -14,6 +14,7 @@ declare global {
    */
   interface Wx {
     exitMiniProgram: (options?: { success?: () => void; fail?: (err: any) => void; complete?: () => void }) => void;
+    restartMiniProgram: (options?: { path?: string; success?: () => void; fail?: (err: any) => void; complete?: () => void }) => void;
   }
   
   /**

+ 9 - 1
src/hooks/useAppConfig.ts

@@ -9,7 +9,15 @@ export const useAppConfig = () => {
   const isSmsCaptchaEnable = computed(() => {
     return appStore.getAppConfig(EnumAppConfigKey.SMS_CAPTCHA_ENABLE) === 'true';
   });
+  /**
+  * app 是否开启日志上报
+  */
+  const isAppLogReportEnable = computed(() => {
+    return appStore.getAppConfig(EnumAppConfigKey.APP_LOG_REPORT_ENABLE) === 'true';
+  });
+
   return {
-    isSmsCaptchaEnable
+    isSmsCaptchaEnable,
+    isAppLogReportEnable
   }
 }

+ 63 - 0
src/hooks/useReport.ts

@@ -0,0 +1,63 @@
+import { useUserStore } from "@/store/userStore";
+import { useEnv } from "./useEnv";
+import { useTransferPage } from "./useTransferPage";
+import { useAppConfig } from "./useAppConfig";
+
+export const events = {
+  ExamStartEnter: 'exam-start-enter',
+
+  ExamStartLoadSuccess: 'exam-start-load-success',
+  ExamStartLoadSlow: 'exam-start-load-slow',
+  ExamStartLoadError: 'exam-start-load-error',
+
+  ExamStartCombineData: 'exam-start-combine-data',
+  ExamStartCombineDataSuccess: 'exam-start-combine-data-success',
+  ExamStartCombineDataError: 'exam-start-combine-data-error',
+
+  ExamStartGetPaperSuccess: 'exam-start-get-paper-success',
+  ExamStartGetPaperSlow: 'exam-start-get-paper-slow',
+  ExamStartGetPaperError: 'exam-start-get-paper-error',
+
+  ExamStartBeginExamineeSlow: 'exam-start-begin-examinee-slow',
+  ExamStartBeginExamineeSuccess: 'exam-start-begin-examinee-success',
+  ExamStartBeginExamineeError: 'exam-start-begin-examinee-error',
+
+  ExamStartInitError: 'exam-start-init-error',
+  ExamStartSubmitSuccess: 'exam-start-submit-success',
+  ExamStartSubmitError: 'exam-start-submit-error',
+  ExamStartExit: 'exam-start-exit',
+  PracticeDetailEnter: 'practice-detail-enter',
+} as const;
+
+type EventType = typeof events[keyof typeof events];
+
+const { isAppLogReportEnable } = useAppConfig();
+const userStore = useUserStore();
+const { platform } = useEnv();
+
+export const useReport = () => {
+  const { prevData } = useTransferPage();
+  const report = (event: EventType, extraData: any = {}) => {
+    if (isAppLogReportEnable.value) {
+      uni.report(event, getReportData(extraData));
+    }
+  }
+  const getReportData = (extraData: any = {}) => {
+    return {
+      pageOptions: prevData.value,
+      userInfo: {
+        userName: userStore.userInfo.userName,
+        phonenumber: userStore.userInfo.phonenumber,
+      },
+      platform: platform.value,
+      extraData,
+      createTime: Date.now()
+    }
+  }
+
+  return {
+    report,
+    getReportData,
+    events
+  }
+}

+ 6 - 0
src/pages.json

@@ -320,6 +320,12 @@
           "style": {
             "navigationBarTitleText": ""
           }
+        },
+        {
+          "path": "pages/debug/debug",
+          "style": {
+            "navigationBarTitleText": ""
+          }
         }
       ]
     },

+ 1 - 1
src/pagesMain/pages/index/components/index-popup.vue

@@ -1,5 +1,5 @@
 <template>
-  <ie-popup title="" mode="bottom" ref="popupRef" :showToolbar="false">
+  <ie-popup title="" mode="bottom" ref="popupRef" :close-on-click-overlay="false" :showToolbar="false">
     <view class="popup-content relative">
       <ie-image :is-oss="true" src="/study-bg15.png" custom-class="absolute top-0 left-0 w-full h-392 z-0"
         mode="aspectFill" />

+ 229 - 23
src/pagesStudy/pages/exam-start/exam-start.vue

@@ -32,7 +32,13 @@ import {
   EXAM_PAGE_OPTIONS,
   EXAM_DATA
 } from '@/types/injectionSymbols';
+import { useAppStore } from '@/store/appStore';
+import { useEnv } from '@/hooks/useEnv';
+import { useReport } from '@/hooks/useReport';
 
+const { report, events } = useReport();
+const appStore = useAppStore();
+const { platform } = useEnv();
 const userStore = useUserStore();
 // import { Examinee, ExamPaper, ExamPaperSubmit } from '@/types/study';
 const { prevData, transferBack, transferTo } = useTransferPage<Transfer.ExamAnalysisPageOptions, {}>();
@@ -207,10 +213,14 @@ const handleSubmit = (tempSave: boolean = false) => {
       duration: practiceDuration.value
     };
     console.log('提交试卷参数', params)
+    const start = Date.now();
     await commitExamineePaper(params);
+    const costTime = Date.now() - start;
+    report(events.ExamStartSubmitSuccess, { time: costTime });
     if (isSimulationExam.value || isTestExam.value) {
       if (!tempSave) {
         setTimeout(async () => {
+          report(events.ExamStartExit);
           uni.$ie.hideLoading();
           await nextTick();
           confirmQuit.value = true;
@@ -234,6 +244,7 @@ const handleSubmit = (tempSave: boolean = false) => {
     } else if (isPracticeExam.value) {
       if (!tempSave) {
         setTimeout(async () => {
+          report(events.ExamStartExit);
           uni.$ie.hideLoading();
           await nextTick();
           confirmQuit.value = true;
@@ -250,6 +261,7 @@ const handleSubmit = (tempSave: boolean = false) => {
           });
         }, 2500);
       } else {
+        report(events.ExamStartExit);
         uni.$ie.hideLoading();
         confirmQuit.value = true;
         confirmShowing.value = false;
@@ -291,6 +303,135 @@ const restoreQuestion = (savedQuestion: Study.ExamineeQuestion[], fullQuestion:
   }
   return fullQuestion;
 }
+/**
+ * 带超时检测的 getOpenExaminee 请求
+ * 在 3s, 5s, 10s, 15s, 20s 这 5 个时间点检测超时并上报
+ * 每个时间点如果请求还未返回,都会上报一次
+ */
+const getOpenExamineeWithTimeoutCheck = async (params: Study.OpenExamineeRequestDTO) => {
+  // 超时时间档次(单位:毫秒)
+  const timeoutLevels = [3000, 5000, 10000, 15000, 20000];
+  // 存储所有定时器ID,用于清理
+  const timers: number[] = [];
+  // 请求是否已完成
+  let isCompleted = false;
+
+  // 创建超时检测定时器
+  timeoutLevels.forEach((timeout) => {
+    const timer = setTimeout(() => {
+      // 如果请求已完成,不进行上报
+      if (isCompleted) {
+        return;
+      }
+      // 上报超时数据,超时时间单位为秒
+      // 每个时间点如果请求还未返回,都会上报一次
+      report(events.ExamStartLoadSlow, { time: timeout });
+    }, timeout) as unknown as number;
+    timers.push(timer);
+  });
+
+  try {
+    // 执行请求
+    const result = await getOpenExaminee(params);
+    // 标记请求已完成
+    isCompleted = true;
+    // 清除所有定时器
+    timers.forEach((timer) => clearTimeout(timer));
+    return result;
+  } catch (error) {
+    // 请求失败时也要清除定时器
+    isCompleted = true;
+    timers.forEach((timer) => clearTimeout(timer));
+    throw error;
+  }
+};
+
+/**
+ * 带超时检测的 getPaper 请求
+ * 在 3s, 5s, 10s, 15s, 20s 这 5 个时间点检测超时并上报
+ * 每个时间点如果请求还未返回,都会上报一次
+ */
+const getPaperWithTimeoutCheck = async (params: Study.GetExamPaperRequestDTO) => {
+  // 超时时间档次(单位:毫秒)
+  const timeoutLevels = [3000, 5000, 10000, 15000, 20000];
+  // 存储所有定时器ID,用于清理
+  const timers: number[] = [];
+  // 请求是否已完成
+  let isCompleted = false;
+
+  // 创建超时检测定时器
+  timeoutLevels.forEach((timeout) => {
+    const timer = setTimeout(() => {
+      // 如果请求已完成,不进行上报
+      if (isCompleted) {
+        return;
+      }
+      // 上报超时数据,超时时间单位为秒
+      // 每个时间点如果请求还未返回,都会上报一次
+      report(events.ExamStartGetPaperSlow, { time: timeout });
+    }, timeout) as unknown as number;
+    timers.push(timer);
+  });
+
+  try {
+    // 执行请求
+    const result = await getPaper(params);
+    // 标记请求已完成
+    isCompleted = true;
+    // 清除所有定时器
+    timers.forEach((timer) => clearTimeout(timer));
+    return result;
+  } catch (error) {
+    // 请求失败时也要清除定时器
+    isCompleted = true;
+    timers.forEach((timer) => clearTimeout(timer));
+    throw error;
+  }
+};
+
+/**
+ * 带超时检测的 beginExaminee 请求
+ * 在 3s, 5s, 10s, 15s, 20s 这 5 个时间点检测超时并上报
+ * 每个时间点如果请求还未返回,都会上报一次
+ */
+const beginExamineeWithTimeoutCheck = async (examineeId: number) => {
+  // 超时时间档次(单位:毫秒)
+  const timeoutLevels = [3000, 5000, 10000, 15000, 20000];
+  // 存储所有定时器ID,用于清理
+  const timers: number[] = [];
+  // 请求是否已完成
+  let isCompleted = false;
+
+  // 创建超时检测定时器
+  timeoutLevels.forEach((timeout) => {
+    const timer = setTimeout(() => {
+      // 如果请求已完成,不进行上报
+      if (isCompleted) {
+        return;
+      }
+      // 上报超时数据,超时时间单位为秒
+      // 每个时间点如果请求还未返回,都会上报一次
+      report(events.ExamStartBeginExamineeSlow, { time: timeout });
+    }, timeout) as unknown as number;
+    timers.push(timer);
+  });
+
+  try {
+    // 执行请求
+    const result = await beginExaminee(examineeId);
+    // 标记请求已完成
+    isCompleted = true;
+    // 清除所有定时器
+    timers.forEach((timer) => clearTimeout(timer));
+    return result;
+  } catch (error) {
+    // 请求失败时也要清除定时器
+    isCompleted = true;
+    timers.forEach((timer) => clearTimeout(timer));
+    throw error;
+  }
+};
+
 // 1、加载知识点练习数据
 const loadPracticeData = async () => {
   const { paperType, readonly, practiceInfo } = prevData.value;
@@ -301,17 +442,25 @@ const loadPracticeData = async () => {
       data = res.data;
     }
   } else {
-    const params = {
-      paperType: paperType,
-      relateId: practiceInfo?.relateId,
-    } as Study.OpenExamineeRequestDTO;
-    if (userStore.isVHS) {
-      params.questionType = practiceInfo?.questionType;
-    } else {
-      params.directed = practiceInfo?.directed || false;
+    try {
+      const params = {
+        paperType: paperType,
+        relateId: practiceInfo?.relateId,
+      } as Study.OpenExamineeRequestDTO;
+      if (userStore.isVHS) {
+        params.questionType = practiceInfo?.questionType;
+      } else {
+        params.directed = practiceInfo?.directed || false;
+      }
+      const start = Date.now();
+      const res = await getOpenExamineeWithTimeoutCheck(params);
+      data = res.data || {};
+      const costTime = Date.now() - start;
+      console.log('开卷用时:', costTime);
+      report(events.ExamStartLoadSuccess, { time: costTime, request: params, response: res.data || {} });
+    } catch (error) {
+      report(events.ExamStartLoadError, { error: error });
     }
-    const res = await getOpenExaminee(params);
-    data = res.data || {};
   }
 
   if (!data) {
@@ -332,8 +481,16 @@ const loadExamData = async () => {
       const res = await getExamineeResult(simulationInfo.examineeId);
       data = res.data;
     } else {
-      const res = await beginExaminee(simulationInfo.examineeId);
-      data = res.data || {};
+      try {
+        const start = Date.now();
+        const res = await beginExamineeWithTimeoutCheck(simulationInfo.examineeId);
+        data = res.data || {};
+        const costTime = Date.now() - start;
+        report(events.ExamStartBeginExamineeSuccess, { time: costTime, request: { examineeId: simulationInfo.examineeId }, response: res.data || {} });
+      } catch (error) {
+        report(events.ExamStartBeginExamineeError, { error: error });
+        throw error;
+      }
     }
     if (!data) {
       uni.$ie.hideLoading();
@@ -371,16 +528,49 @@ const loadExamData = async () => {
 // }
 const combinePaperData = async (examinee: Study.Examinee, paperType: EnumPaperType) => {
   examineeId.value = examinee.examineeId;
-  if (examinee.paperId) {
-    const res = await getPaper({
+  report(events.ExamStartCombineData, {
+    envUser: localStorage.getItem('ie-user'),
+    envApp: localStorage.getItem('ie-app'),
+  });
+
+  if (!examinee.paperId) {
+    report(events.ExamStartGetPaperError, {
+      error: '获取试卷数据失败,没有paperId',
+      examineeId: examinee.examineeId
+    });
+    return;
+  }
+
+  // 第一步:请求试卷数据(单独 try-catch)
+  let res;
+  try {
+    const start = Date.now();
+    res = await getPaperWithTimeoutCheck({
       type: paperType,
       id: examinee.paperId
     });
+    const costTime = Date.now() - start;
+    report(events.ExamStartGetPaperSuccess, {
+      time: costTime,
+      paperId: examinee.paperId,
+      paperType: paperType
+    });
+  } catch (error: any) {
+    // 请求失败的错误上报
+    report(events.ExamStartGetPaperError, {
+      error: error
+    });
+    throw error; // 继续抛出,让外层处理
+  }
+
+  // 第二步:处理和组装数据(单独 try-catch)
+  try {
     paperData.value = res.data;
     paperData.value.questions = restoreQuestion(examinee.questions, res.data.questions);
     console.log('初始化数据', paperData.value.questions)
     setQuestionList(paperData.value.questions);
     setDuration(examinee.duration || 0);
+
     await nextTick();
     const targetQuestion = flatQuestionList.value.find(item => item.id === prevData.value.questionId);
     if (targetQuestion) {
@@ -412,6 +602,16 @@ const combinePaperData = async (examinee: Study.Examinee, paperType: EnumPaperTy
         startTime();
       }
     }
+    report(events.ExamStartCombineDataSuccess, {
+      paperId: examinee.paperId,
+      paperType: paperType
+    });
+  } catch (error: any) {
+    // 数据处理失败的错误上报
+    report(events.ExamStartCombineDataError, {
+      error: error
+    });
+    throw error; // 继续抛出,让外层处理
   }
 }
 const handleSwiperTipNext = () => {
@@ -427,19 +627,25 @@ const handleGuideClose = () => {
 const loadData = async () => {
   uni.$ie.showLoading();
   const { paperType } = prevData.value;
-  if (paperType === EnumPaperType.PRACTICE || paperType === EnumPaperType.COURSE) {
-    loadPracticeData();
-  } else if (paperType === EnumPaperType.SIMULATED || paperType === EnumPaperType.TEST) {
-    // if (paperType === EnumPaperType.SIMULATED && userStore.isVHS) {
-    //   loadVHSPaperData();
-    // } else {
-    //   loadExamData();
-    // }
-    loadExamData();
+  try {
+    if (paperType === EnumPaperType.PRACTICE || paperType === EnumPaperType.COURSE) {
+      loadPracticeData();
+    } else if (paperType === EnumPaperType.SIMULATED || paperType === EnumPaperType.TEST) {
+      loadExamData();
+    }
+  } catch (error) {
+    report(events.ExamStartInitError, { error });
+    uni.$ie.showToast('加载失败,请稍后重试');
+    setTimeout(() => {
+      uni.$ie.hideLoading();
+      transferBack();
+    }, 1000);
   }
 };
+
 onLoad(() => {
   console.log(prevData.value)
+  report(events.ExamStartEnter);
   loadData();
   uni.addInterceptor('navigateBack', {
     invoke: (e) => {

+ 38 - 26
src/pagesStudy/pages/knowledge-practice-detail/knowledge-practice-detail.vue

@@ -27,10 +27,12 @@
     </view>
     <ie-safe-toolbar :height="84" :shadow="false" bg-color="#FFFFFF">
       <view class="h-[84px] px-46 bg-white flex items-center justify-between gap-x-40">
-        <view class="text-30 text-primary bg-back flex-1 py-22 rounded-full text-center h-fit" @click="handleStartPractice">
+        <view class="text-30 text-primary bg-back flex-1 py-22 rounded-full text-center h-fit"
+          @click="handleStartPractice">
           继续刷题
         </view>
-        <view class="text-30 text-white bg-primary flex-1 py-22 rounded-full text-center h-fit" @click="handleViewAnalysis">
+        <view class="text-30 text-white bg-primary flex-1 py-22 rounded-full text-center h-fit"
+          @click="handleViewAnalysis">
           查看解析
         </view>
       </view>
@@ -41,11 +43,13 @@
 import RateChart from '@/pagesStudy/pages/simulation-analysis/components/rate-chart.vue';
 import ExamStat from '@/pagesStudy/pages/simulation-analysis/components/exam-stat.vue';
 import { useTransferPage } from '@/hooks/useTransferPage';
-import { Study, Transfer } from '@/types';
+import type { Study, Transfer } from '@/types';
 import { getExamineeResult } from '@/api/modules/study';
 import { EnumPaperType } from '@/common/enum';
-const { prevData, transferTo } = useTransferPage<Transfer.PracticeResultPageOptions, Transfer.ExamAnalysisPageOptions>();
+import { useReport } from '@/hooks/useReport';
 
+const { prevData, transferTo } = useTransferPage<Transfer.PracticeResultPageOptions, Transfer.ExamAnalysisPageOptions>();
+const { report, events } = useReport();
 
 const rightRate = computed(() => {
   const { totalCount = 0, wrongCount = 0 } = examineeData.value || {};
@@ -80,7 +84,7 @@ const handleQuestionDetail = (item: Study.Question) => {
   }
   transferTo('/pagesStudy/pages/exam-start/exam-start', {
     data: {
-      name: paperName.value,
+      // name: paperName.value,
       paperType: prevData.value.paperType || EnumPaperType.PRACTICE,
       readonly: true,
       questionId: item.id,
@@ -88,7 +92,9 @@ const handleQuestionDetail = (item: Study.Question) => {
         name: prevData.value.name,
         relateId: examineeData.value.knowledgeId,
         directed: prevData.value.directed,
-        examineeId: examineeData.value.examineeId
+        examineeId: examineeData.value.examineeId,
+        // 对口升学
+        questionType: prevData.value.questionType
       },
     }
   });
@@ -99,34 +105,38 @@ const handleStartPractice = () => {
     console.error('knowledgeId is null');
     return;
   }
+  const pageOptions: Transfer.ExamAnalysisPageOptions = {
+    paperType: prevData.value.paperType || EnumPaperType.PRACTICE,
+    readonly: false,
+    practiceInfo: {
+      name: prevData.value.name,
+      relateId: knowledgeId,
+      directed: prevData.value.directed,
+      questionType: prevData.value.questionType
+    },
+  }
   transferTo('/pagesStudy/pages/exam-start/exam-start', {
-    data: {
-      name: paperName.value,
-      paperType: prevData.value.paperType || EnumPaperType.PRACTICE,
-      practiceInfo: {
-        name: prevData.value.name,
-        relateId: knowledgeId,
-        directed: prevData.value.directed
-      },
-    }
+    data: pageOptions
   });
 }
 const handleViewAnalysis = () => {
   if (!examineeData.value) {
     return;
   }
+  const pageOptions: Transfer.ExamAnalysisPageOptions = {
+    paperType: prevData.value.paperType || EnumPaperType.PRACTICE,
+    readonly: true,
+    practiceInfo: {
+      name: prevData.value.name,
+      relateId: examineeData.value.knowledgeId,
+      directed: prevData.value.directed,
+      examineeId: examineeData.value.examineeId,
+      // 对口升学
+      questionType: prevData.value.questionType
+    },
+  }
   transferTo('/pagesStudy/pages/exam-start/exam-start', {
-    data: {
-      name: paperName.value,
-      paperType: EnumPaperType.PRACTICE,
-      readonly: true,
-      practiceInfo: {
-        name: prevData.value.name,
-        relateId: examineeData.value.knowledgeId,
-        directed: prevData.value.directed,
-        examineeId: examineeData.value.examineeId
-      },
-    }
+    data: pageOptions
   });
 }
 const loadData = async () => {
@@ -139,6 +149,8 @@ const loadData = async () => {
   }
 }
 onLoad(() => {
+  report(events.PracticeDetailEnter)
+  console.log(prevData.value)
   loadData();
 });
 </script>

+ 3 - 3
src/pagesStudy/pages/textbooks-practice/textbooks-practice.vue

@@ -30,7 +30,7 @@ import { useAuth } from '@/hooks/useAuth';
 
 const { prevData, transferTo } = useTransferPage();
 const userStore = useUserStore();
-const pagingRef = ref();
+const pagingRef = ref<ZPagingInstance>();
 const { checkVipPermission } = useAuth();
 
 const pageTitle = computed(() => {
@@ -50,10 +50,10 @@ const loadKnowledgeList = async (page: number, size: number) => {
     uni.$ie.showLoading();
     const { data } = await getTextbooksKnowledgeList();
     treeData.value = data as Study.KnowledgeNode[];
-    pagingRef.value.complete(data);
+    pagingRef.value?.complete(data);
   } catch (error) {
     console.log(error);
-    pagingRef.value.complete(false);
+    pagingRef.value?.complete(false);
   } finally {
     uni.$ie.hideLoading();
   }

+ 67 - 0
src/pagesSystem/pages/debug/debug.vue

@@ -0,0 +1,67 @@
+<template>
+  <ie-page bg-color="#F6F8FA">
+    <ie-navbar title="调试" />
+    <view class="p-20">
+      <view class="my-20">用户数据:</view>
+      <view class="bg-white">
+        <textarea class="p-20 h-400 break-all" v-model="userJson" maxlength="-1"></textarea>
+      </view>
+    </view>
+    <view class="m-20">
+      <uv-button type="primary" @click="handleLoad">加载</uv-button>
+    </view>
+    <view class="p-20">
+      <view class="my-20">app数据:</view>
+      <view class="bg-white">
+        <textarea class="p-20 h-400 break-all" v-model="appJson" maxlength="-1"></textarea>
+      </view>
+    </view>
+    <view class="m-20">
+      <uv-button type="primary" @click="handleLoad">加载</uv-button>
+    </view>
+    <view class="m-20">
+      <uv-button type="success" @click="handleToHome">进入首页</uv-button>
+    </view>
+  </ie-page>
+</template>
+
+<script lang="ts" setup>
+import { useUserStore } from '@/store/userStore';
+import { useAppStore } from '@/store/appStore';
+
+const userStore = useUserStore();
+const appStore = useAppStore();
+
+const userJson = ref('');
+const appJson = ref('');
+
+const handleLoad = () => {
+  if (userJson.value) {
+    try {
+      userStore.loadFromLocal(userJson.value);
+      uni.$ie.showToast('加载成功');
+    } catch (error) {
+      uni.$ie.showToast('加载失败');
+    }
+  }
+}
+const handleLoadApp = () => {
+  if (appJson.value) {
+    try {
+      appStore.loadFromLocal(appJson.value);
+      uni.$ie.showToast('加载成功');
+    } catch (error) {
+      uni.$ie.showToast('加载失败');
+    }
+  }
+}
+
+const handleToHome = () => {
+  uni.reLaunch({
+    url: '/pagesMain/pages/index/index',
+    type: 'reLaunch'
+  });
+}
+</script>
+
+<style lang="scss"></style>

+ 29 - 0
src/pagesSystem/pages/setting/setting.vue

@@ -38,6 +38,35 @@ const settings = computed(() => [
       transferTo(routes.pagePrivacyPolicy);
     }
   },
+  {
+    title: '清除数据',
+    icon: 'empty-page',
+    isLink: true,
+    value: '',
+    handler: () => {
+      // 清除所有本地数据,重启 app
+      uni.$ie.showModal({
+        title: '提示',
+        content: '清除数据后需要重新登录\n是否继续?',
+      }).then(confirm => {
+        if (confirm) {
+          uni.clearStorageSync();
+          setTimeout(() => {
+            // 再次尝试清除,防止有补偿机制
+            uni.clearStorageSync();
+            // #ifdef H5
+            window.location.href = '/h5/';
+            // #endif
+            // #ifdef MP-WEIXIN
+            wx.restartMiniProgram({
+              path: '/pagesMain/pages/splash/splash'
+            });
+            // #endif
+          }, 100);
+        }
+      })
+    }
+  },
   {
     title: '当前版本',
     icon: 'tags',

+ 13 - 0
src/store/appStore.ts

@@ -118,6 +118,19 @@ export const useAppStore = defineStore('ie-app', {
         return Promise.resolve(false);
       }
     },
+    loadFromLocal(state: string) {
+      const value = JSON.parse(state);
+      if (value) {
+        const { appConfig, activeTabbar, statusBarHeight, sysInfo, isInitialized } = value;
+        this.appConfig = appConfig || [];
+        this.activeTabbar = activeTabbar || 0;
+        this.statusBarHeight = statusBarHeight || 0;
+        this.systemInfo = sysInfo || null;
+        this.isInitialized = isInitialized || false;
+        return true;
+      }
+      return false;
+    },
     setActiveTabbar(index: number) {
       this.activeTabbar = index;
     },

+ 27 - 2
src/store/userStore.ts

@@ -6,7 +6,7 @@ import type { Study, UserStoreState } from '@/types';
 import type { UserInfo, VipCardInfo } from '@/types/user';
 import tools from '@/utils/uni-tool';
 import defaultAvatar from '@/static/personal/avatar_default.png'
-import {CardType, EnumBindScene, EnumExamType, EnumReviewMode, EnumScoreLock, EnumUserType} from '@/common/enum';
+import { CardType, EnumBindScene, EnumExamType, EnumReviewMode, EnumScoreLock, EnumUserType } from '@/common/enum';
 import { getDirectedSchool, saveDirectedSchool } from '@/api/modules/study';
 import { usePay } from '@/hooks/usePay';
 import { getUserBindCard } from '@/api/modules/user';
@@ -357,7 +357,32 @@ export const useUserStore = defineStore('ie-user', {
           });
         }
       });
-    }
+    },
+    loadFromLocal(state: string) {
+      const value = JSON.parse(state);
+      if (value) {
+        const { accessToken, user, card, isDefaultModifyPwd, isPasswordExpired,
+          org, rememberPwd, cardNo, cardPassword, directedSchoolList, practiceSettings,
+          isExamGuideShow,
+          tempInfo,
+        } = value;
+        this.accessToken = accessToken;
+        this.user = user;
+        this.card = card;
+        this.isDefaultModifyPwd = isDefaultModifyPwd;
+        this.isPasswordExpired = isPasswordExpired;
+        this.org = org;
+        this.rememberPwd = rememberPwd;
+        this.cardNo = cardNo;
+        this.cardPassword = cardPassword;
+        this.directedSchoolList = directedSchoolList;
+        this.practiceSettings = practiceSettings;
+        this.isExamGuideShow = isExamGuideShow;
+        this.tempInfo = tempInfo;
+        return true;
+      }
+      return false;
+    },
   },
   persist: {
     storage: {