瀏覽代碼

feat: 24h登录态保持 + 修复冷启动enterpriseId为空 + 隐藏转账功能

- Auth: 增加24小时登录窗口校验,登录时间戳存储
- APIs: 修复401检测死代码,增加静默token刷新(并发锁)
- App: 冷启动恢复本地用户信息,防止enterpriseId/accountId为空
- Index: 首页兜底恢复用户信息,隐藏转账/转账记录入口
- 移除可选链(?.)语法兼容支付宝构建工具
alphah 1 周之前
父節點
當前提交
4830cc6e6a
共有 6 個文件被更改,包括 485 次插入324 次删除
  1. 120 112
      package-lock.json
  2. 4 1
      src/app.ts
  3. 55 45
      src/pages/index/index.tsx
  4. 34 29
      src/schemas/account.ts
  5. 168 92
      src/services/apis.ts
  6. 104 45
      src/utils/auth.ts

文件差異過大導致無法顯示
+ 120 - 112
package-lock.json


+ 4 - 1
src/app.ts

@@ -5,10 +5,13 @@ import '@nutui/nutui-react-taro/dist/style.css';
 import '@nutui/nutui-react-taro/dist/styles/themes/jmapp.css';
 import './app.less';
 import { Auth } from './utils/auth';
+import { loadUserFromStorage } from './stores/user';
 
 function App({ children }: PropsWithChildren<any>) {
-
   useLaunch(() => {
+    // 从本地恢复用户信息
+    loadUserFromStorage();
+    // 检查登录态(token + 24h 窗口)
     Auth.requireAuth();
   });
 

+ 55 - 45
src/pages/index/index.tsx

@@ -1,9 +1,7 @@
+import { ReactNode, useEffect, useState } from 'react';
 import { View, Text } from '@tarojs/components';
 import Taro, { useLoad } from '@tarojs/taro';
-import './index.less';
 import {
-  Button,
-  SearchBar,
   Picker,
   PickerOnChangeCallbackParameter,
   Row,
@@ -15,17 +13,18 @@ import {
   PickerValue,
   Toast,
 } from '@nutui/nutui-react-taro';
-import { ReactNode, useState } from 'react';
-import { ArrowTransfer, QrCode, Received, Scan, Filter } from '@nutui/icons-react-taro';
+import { QrCode, Received, Scan } from '@nutui/icons-react-taro';
 import { ArrowRightIcon } from '@/components/Icon';
 import { scanWithAlipay, codeWithAlipay } from '@/utils/alipay';
 import { useUserStore } from '@/stores/user';
+import { Auth } from '@/utils/auth';
+import './index.less';
 
 // 模拟数据
 const statistics = {
-  shouldReport: 0.00,
-  reported: 0.00,
-  owed: 0.00,
+  shouldReport: 0.0,
+  reported: 0.0,
+  owed: 0.0,
   groupCount: 6,
 };
 
@@ -39,10 +38,15 @@ const options = [
 ];
 
 export default function Index() {
-
   const [statusBarHeight, setStatusBarHeight] = useState<number | undefined>(0);
   const [pickerVisible, setPickerVisible] = useState<boolean>(false);
-  const [toast, setToast] = useState<{ visible: boolean, content?: string, duration?: number, icon: string | ReactNode, title?: string }>({
+  const [toast, setToast] = useState<{
+    visible: boolean;
+    content?: string;
+    duration?: number;
+    icon: string | ReactNode;
+    title?: string;
+  }>({
     visible: false,
     content: '',
     duration: 2000,
@@ -53,15 +57,30 @@ export default function Index() {
   const { enterpriseId, accountId } = useUserStore((state) => state);
 
   useLoad(() => {
-    const { statusBarHeight } = Taro.getSystemInfoSync();
-    setStatusBarHeight(statusBarHeight);
+    const { statusBarHeight: sHeight } = Taro.getSystemInfoSync();
+    setStatusBarHeight(sHeight);
   });
 
+  /** 兜底恢复:token 有效但 enterpriseId 为空时,自动从 API 拉取用户信息 */
+  useEffect(() => {
+    if (Auth.checkLogin() && !enterpriseId) {
+      console.log('[Index] token 有效但 enterpriseId 为空,尝试从 API 恢复用户信息');
+      useUserStore
+        .getState()
+        .getUserInfo()
+        .catch((err) => {
+          console.error('[Index] 恢复用户信息失败:', err);
+        });
+    }
+    // 仅页面挂载时执行一次
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
   const showToast = (
     icon: string | React.ReactNode,
     title?: string,
     duration?: number,
-    content?: string
+    content?: string,
   ) => {
     setToast({ visible: true, icon, title, duration, content });
   };
@@ -95,12 +114,12 @@ export default function Index() {
       icon: <QrCode color="var(--nutui-color-primary)" />,
       fun: () => qrCode(),
     },
-    {
-      order: 3,
-      name: '转账',
-      icon: <ArrowTransfer color="var(--nutui-color-primary)" />,
-      fun: () => Taro.navigateTo({ url: '/pages/transfer/index' }),
-    },
+    // {
+    //   order: 3,
+    //   name: '转账',
+    //   icon: null,
+    //   fun: () => Taro.navigateTo({ url: '/pages/transfer/index' }),
+    // },
     {
       order: 4,
       name: '收款',
@@ -113,11 +132,8 @@ export default function Index() {
     console.log('changePicker', value, index, selectedOptions);
   };
 
-  const confirmPicker = (selectedOptions: PickerOptions, selectedValue: PickerValue[]) => {
-    let description = '';
-    selectedOptions.forEach((option: any) => {
-      description += ` ${option.label}`;
-    });
+  const confirmPicker = (_selectedOptions: PickerOptions, _selectedValue: PickerValue[]) => {
+    // 占位,暂无逻辑
   };
 
   return (
@@ -159,7 +175,7 @@ export default function Index() {
           />
         </View>
 
-        <View className="statistics-card" style={{marginTop: '30px'}}>
+        <View className="statistics-card" style={{ marginTop: '30px' }}>
           <Row gutter="10">
             <Col span="8">
               <View className="statistics-item">
@@ -216,11 +232,13 @@ export default function Index() {
 
       <View style={{ margin: '20px 0px' }}>
         <NoticeBar
-          content={"暂无通知"}
-          style={{
-            '--nutui-noticebar-background': '#EDF4FF',
-            '--nutui-noticebar-color': '#3768FA',
-          } as any}
+          content="暂无通知"
+          style={
+            {
+              '--nutui-noticebar-background': '#EDF4FF',
+              '--nutui-noticebar-color': '#3768FA',
+            } as any
+          }
         />
       </View>
 
@@ -228,15 +246,11 @@ export default function Index() {
         <View style={{ padding: '10px', color: '#959292' }}>常用功能</View>
         <Cell
           className="custom-cell"
-          title={
-            <Text>报账记录</Text>
-          }
+          title={<Text>报账记录</Text>}
           description="我是描述"
-          extra={
-            <ArrowRightIcon />
-          }
+          extra={<ArrowRightIcon />}
         />
-        <Cell
+        {/* <Cell
           className="custom-cell"
           title={
             <Text>转账记录</Text>
@@ -246,16 +260,12 @@ export default function Index() {
             <ArrowRightIcon />
           }
           onClick={() => Taro.navigateTo({ url: '/pages/transfer/record/index' })}
-        />
+        /> */}
         <Cell
           className="custom-cell"
-          title={
-            <Text>发票管理</Text>
-          }
+          title={<Text>发票管理</Text>}
           description="我是描述"
-          extra={
-            <ArrowRightIcon />
-          }
+          extra={<ArrowRightIcon />}
         />
         {/* <Cell
           className="custom-cell"
@@ -277,7 +287,7 @@ export default function Index() {
           title={toast.title}
           visible={toast.visible}
           onClose={() => {
-            setToast({ visible: false, content: '', duration: 2000, icon: null, title: '' })
+            setToast({ visible: false, content: '', duration: 2000, icon: null, title: '' });
           }}
         />
       </View>

+ 34 - 29
src/schemas/account.ts

@@ -1,4 +1,3 @@
-
 export interface BankcardExtInfoSchema {
   account_type: string; // 收款账户类型: 1/2。对公: 1,对私: 2
   inst_name?: string; // 机构名称
@@ -8,7 +7,6 @@ export interface BankcardExtInfoSchema {
   bank_code?: string; // 银行支行联行号
 }
 
-
 // 收款方信息schema
 export interface PayeeInfoSchema {
   identityType: string; // 收款方类型: ALIPAY_ACCOUNT/BANK_CARD/BOOK
@@ -17,61 +15,63 @@ export interface PayeeInfoSchema {
   bankcardExtInfo?: BankcardExtInfoSchema; // 银行卡信息
 }
 
-
 // 资金专户转账请求schema
 export interface AccountTransferSchema {
   /** 企业ID */
-  enterpriseId: string; 
-  
+  enterpriseId: string;
+
   /** 付款方资金专户号 */
   accountBookId?: string;
-  
+
   /** 商家侧订单号 */
   outBizNo?: string;
-  
+
   /** 转账金额,单位为元,精确到小数点后两位 */
   amount: string;
-  
+
   /** 转账标题 */
   orderTitle?: string;
-  
+
   /** 转账备注 */
   remark?: string;
-  
+
   /** 收款方信息 */
   payeeInfo: PayeeInfoSchema;
-  
+
   /** 员工ID */
   employeeId?: string;
-  
+
   /** 优先级: HIGH/NORMAL/LOW */
   priority?: string;
-  
+
   /** 扩展信息 */
   extInfo?: Record<string, any>;
 }
 
 // 验证转账请求
-export const validateTransferRequest = (data: AccountTransferSchema): Partial<Record<keyof AccountTransferSchema, string>> => {
+export const validateTransferRequest = (
+  data: AccountTransferSchema,
+): Partial<Record<keyof AccountTransferSchema, string>> => {
   const errors: Partial<Record<keyof AccountTransferSchema, string>> = {};
-  
+
   // 验证必填字段
   if (!data.enterpriseId) {
     errors.enterpriseId = '企业ID不能为空';
   }
-  
-  if (data.amount && (data.amount.toString().split('.')[1]?.length || 0) > 2) {
+
+  const parts = data.amount.toString().split('.');
+  if (data.amount && parts[1] && parts[1].length > 2) {
     errors.amount = '转账金额最多支持两位小数';
   }
-  
+
   if (!data.orderTitle) {
     errors.orderTitle = '转账标题不能为空';
   }
-  
+
   if (data.orderTitle && data.orderTitle.length >= 4 && data.orderTitle.length <= 64) {
     errors.orderTitle = '转账标题长度必须在4个字符到64个字符之间';
   }
-  
+
   if (!data.payeeInfo) {
     errors.payeeInfo = '收款方信息不能为空';
   } else {
@@ -83,7 +83,7 @@ export const validateTransferRequest = (data: AccountTransferSchema): Partial<Re
     if (!data.payeeInfo.identityType) {
       errors.payeeInfo = '收款方类型不能为空';
     }
-    
+
     if (!data.payeeInfo.identity) {
       errors.payeeInfo = '收款方唯一标识不能为空';
     }
@@ -91,19 +91,24 @@ export const validateTransferRequest = (data: AccountTransferSchema): Partial<Re
     if (data.payeeInfo.identityType === 'BANK_CARD' && !data.payeeInfo.bankcardExtInfo) {
       errors.payeeInfo = '收款方银行卡信息不能为空';
       // @ts-ignore
-    } else if (data.payeeInfo.identityType === 'BANK_CARD' && !data.payeeInfo.bankcardExtInfo.account_type) {
+    } else if (
+      data.payeeInfo.identityType === 'BANK_CARD' &&
+      !data.payeeInfo.bankcardExtInfo.account_type
+    ) {
       errors.payeeInfo = '收款方银行卡账户类型不能为空';
-       // @ts-ignore
-    } else if (data.payeeInfo.identityType === 'BANK_CARD' && data.payeeInfo.bankcardExtInfo.account_type === '1' && !data.payeeInfo.bankcardExtInfo.inst_name) {
+      // @ts-ignore
+    } else if (
+      data.payeeInfo.identityType === 'BANK_CARD' &&
+      data.payeeInfo.bankcardExtInfo.account_type === '1' &&
+      !data.payeeInfo.bankcardExtInfo.inst_name
+    ) {
       errors.payeeInfo = '收款方银行卡机构名称不能为空';
     }
-
-
   }
-  
+
   if (data.priority && !['HIGH', 'NORMAL', 'LOW'].includes(data.priority)) {
     errors.priority = '优先级必须是 HIGH、NORMAL 或 LOW';
   }
-  
+
   return errors;
-};
+};

+ 168 - 92
src/services/apis.ts

@@ -5,122 +5,198 @@ import humps from 'humps';
 import { Auth } from '@/utils/auth';
 import { AccountTransferSchema } from '@/schemas/account';
 
+const API_BASE_URL = process.env.TARO_APP_API_BASE_URL;
 
-const API_BASE_URL = process.env.TARO_APP_API_BASE_URL
+// ---- Token 刷新 ----
 
-const handleExpiredLogin = () => {
-    Auth.clearToken();
-    Taro.redirectTo({ url: '/pages/login/index' });
-    throw new Error('登录已过期,请重新登录');
-}
+/** 防止并发刷新:Promise 锁 */
+let refreshPromise: Promise<boolean> | null = null;
 
+/** 静默刷新 token,成功返回 true */
+const tryRefreshToken = async (): Promise<boolean> => {
+  const refreshToken = Auth.getRefreshToken();
+  if (!refreshToken) return false;
 
-export const requestApi = async (options: Taro.request.Option, defaultErrorMsg: string = '') => {
+  // 已有进行中的刷新则复用
+  if (refreshPromise) return refreshPromise;
 
-    const accessToken = Auth.getAccessToken();
-    if (accessToken) {
-        options.header = options.header || {};
-        options.header['Authorization'] = `Bearer ${accessToken}`;
+  refreshPromise = (async () => {
+    try {
+      const response = await Taro.request({
+        url: `${API_BASE_URL}/system/auth/refresh`,
+        method: 'POST',
+        header: { 'Content-Type': 'application/json' },
+        data: humps.decamelizeKeys({ refreshToken }),
+        timeout: 10000,
+      });
+      if (response.statusCode === 200 && response.data && response.data.code === 0) {
+        const newToken = humps.camelizeKeys(response.data.data);
+        Auth.setToken(newToken);
+        console.log('[Token刷新] 成功');
+        return true;
+      }
+      console.log('[Token刷新] 失败:', response.data && response.data.msg);
+      return false;
+    } catch (e) {
+      console.error('[Token刷新] 异常:', e);
+      return false;
+    } finally {
+      refreshPromise = null;
     }
+  })();
 
-    options.data = humps.decamelizeKeys(options.data);
-    console.log('[API请求]', options.url, JSON.stringify(options.data));
+  return refreshPromise;
+};
 
-    try {
-        const response = await Taro.request({
-            ...options,
-            url: `${API_BASE_URL}${options.url}`,
-            timeout: 10000,
-        });
+/** 登录已过期:清除凭据,跳转登录页 */
+const handleExpiredLogin = () => {
+  Auth.clearToken();
+  Taro.reLaunch({ url: '/pages/login/index' });
+  throw new Error('登录已过期,请重新登录');
+};
 
-        if (response.statusCode === 200) {
-            if (response.data.code === 0) {
-                return humps.camelizeKeys(response.data.data);
-            } else {
-                throw new Error(response.data.msg || defaultErrorMsg);
-            }
-        } else {
-            throw new Error(`${defaultErrorMsg} (${response.statusCode})`);
+export const requestApi = async (options: Taro.request.Option, defaultErrorMsg: string = '') => {
+  const accessToken = Auth.getAccessToken();
+  if (accessToken) {
+    options.header = options.header || {};
+    options.header['Authorization'] = `Bearer ${accessToken}`;
+  }
+
+  options.data = humps.decamelizeKeys(options.data);
+  console.log('[API请求]', options.url, JSON.stringify(options.data));
+
+  try {
+    const response = await Taro.request({
+      ...options,
+      url: `${API_BASE_URL}${options.url}`,
+      timeout: 10000,
+    });
+
+    // 401:尝试静默刷新
+    if (response.statusCode === 401) {
+      const refreshed = await tryRefreshToken();
+      if (refreshed) {
+        // 刷新成功,用新 token 重试一次
+        const newToken = Auth.getAccessToken();
+        options.header = options.header || {};
+        options.header['Authorization'] = `Bearer ${newToken}`;
+        const retryResponse = await Taro.request({
+          ...options,
+          url: `${API_BASE_URL}${options.url}`,
+          timeout: 10000,
+        });
+        if (retryResponse.statusCode === 200) {
+          if (retryResponse.data.code === 0) {
+            return humps.camelizeKeys(retryResponse.data.data);
+          }
+          throw new Error(retryResponse.data.msg || defaultErrorMsg);
         }
-
-    } catch (error: any) {
-        if (error && error.status === 401) {
-            handleExpiredLogin();
+        if (retryResponse.statusCode === 401) {
+          handleExpiredLogin();
         }
-        throw new Error((error && error.data && error.data.msg) || defaultErrorMsg);
+        throw new Error(`${defaultErrorMsg} (${retryResponse.statusCode})`);
+      }
+      // 刷新失败,跳转登录
+      handleExpiredLogin();
+    }
+
+    if (response.statusCode === 200) {
+      if (response.data.code === 0) {
+        return humps.camelizeKeys(response.data.data);
+      } else {
+        throw new Error(response.data.msg || defaultErrorMsg);
+      }
+    } else {
+      throw new Error(`${defaultErrorMsg} (${response.statusCode})`);
     }
+  } catch (error: any) {
+    // 网络错误、超时等
+    throw new Error((error && error.message) || defaultErrorMsg);
+  }
 };
 
 // 发送短信验证码
 export const sendSmsCodeApi = async (mobile: string) => {
-    return requestApi(
-      {
-        url: '/system/auth/sms-code',
-        method: 'POST',
-        header: {
-          'Content-Type': 'application/json',
-        },
-        data: {
-          mobile,
-          template_name: 'verify',
-        },
+  return requestApi(
+    {
+      url: '/system/auth/sms-code',
+      method: 'POST',
+      header: {
+        'Content-Type': 'application/json',
       },
-      '发送验证码失败',
-    );
+      data: {
+        mobile,
+        template_name: 'verify',
+      },
+    },
+    '发送验证码失败',
+  );
 };
 
 // 短信验证码登录
 export const loginApi = async (data: LoginFormData) => {
-    return requestApi({
-        url: '/system/auth/login/sms',
-        method: 'POST',
-        header: {
-            "Content-Type": "application/json",
-        },
-        data: {
-            mobile: data.mobile,
-            code: data.code,
-            template_name: 'verify',
-        },
-    }, '登录失败');
+  return requestApi(
+    {
+      url: '/system/auth/login/sms',
+      method: 'POST',
+      header: {
+        'Content-Type': 'application/json',
+      },
+      data: {
+        mobile: data.mobile,
+        code: data.code,
+        template_name: 'verify',
+      },
+    },
+    '登录失败',
+  );
 };
 
-
 export const getUserInfoApi = async () => {
-    return requestApi({
-        url: '/payment/employee/info',
-        method: 'GET',
-        dataType: 'json',
-        header: {
-            "Content-Type": "application/json",
-        },
-    }, '获取用户信息失败');
+  return requestApi(
+    {
+      url: '/payment/employee/info',
+      method: 'GET',
+      dataType: 'json',
+      header: {
+        'Content-Type': 'application/json',
+      },
+    },
+    '获取用户信息失败',
+  );
 };
 
-
 export const transferApi = async (data: AccountTransferSchema) => {
-    return requestApi({
-        url: '/payment/account/transfer',
-        method: 'POST',
-        dataType: 'json',
-        header: {
-            "Content-Type": "application/json",
-        },
-        data,
-    }, '转账失败');
-}
-
-
-export const listTransferApi = async (
-    data: { pageNo: number, pageSize: number, status?: string, orderNo?: string }
-) => {
-    return requestApi({
-        url: '/payment/account/transfer',
-        method: 'GET',
-        dataType: 'json',
-        header: {
-            "Content-Type": "application/json",
-        },
-        data,
-    }, '获取转账记录失败');
-}
+  return requestApi(
+    {
+      url: '/payment/account/transfer',
+      method: 'POST',
+      dataType: 'json',
+      header: {
+        'Content-Type': 'application/json',
+      },
+      data,
+    },
+    '转账失败',
+  );
+};
+
+export const listTransferApi = async (data: {
+  pageNo: number;
+  pageSize: number;
+  status?: string;
+  orderNo?: string;
+}) => {
+  return requestApi(
+    {
+      url: '/payment/account/transfer',
+      method: 'GET',
+      dataType: 'json',
+      header: {
+        'Content-Type': 'application/json',
+      },
+      data,
+    },
+    '获取转账记录失败',
+  );
+};

+ 104 - 45
src/utils/auth.ts

@@ -1,61 +1,120 @@
 // src/utils/auth.ts
 import Taro from '@tarojs/taro';
 
-
 export interface Token {
-    accessToken: string;
-    refreshToken?: string;
-    expiresIn?: number;
-    tokenType?: string;
+  accessToken: string;
+  refreshToken?: string;
+  expiresIn?: number;
+  tokenType?: string;
 }
 
+/** 登录态有效期(小时),在此期间内重新进入小程序无需重新登录 */
+const TOKEN_VALID_HOURS = 24;
+
 export class Auth {
-    static readonly ACCESS_TOKEN_KEY = "access_token";
-    static readonly REFRESH_TOKEN_KEY = "refresh_token";
-
-    static getAccessToken() {
-        try {
-            return Taro.getStorageSync(Auth.ACCESS_TOKEN_KEY);
-        } catch (error) {
-            return null;
-        }
-    }
+  static readonly ACCESS_TOKEN_KEY = 'access_token';
+  static readonly REFRESH_TOKEN_KEY = 'refresh_token';
+  static readonly LOGIN_TIME_KEY = 'login_time';
 
-    static checkLogin() {
-        return !!Auth.getAccessToken();
-    }
+  // ---- Token 读写 ----
 
-    static clearStorage() {
-        Taro.clearStorageSync();
+  static getAccessToken() {
+    try {
+      return Taro.getStorageSync(Auth.ACCESS_TOKEN_KEY);
+    } catch (error) {
+      return null;
     }
+  }
 
-    static setToken(token: Token) {
-        try {
-            Taro.setStorageSync(Auth.ACCESS_TOKEN_KEY, token.accessToken);
-            Taro.setStorageSync(Auth.REFRESH_TOKEN_KEY, token.refreshToken || '');
-        } catch (e) { }
+  static getRefreshToken() {
+    try {
+      return Taro.getStorageSync(Auth.REFRESH_TOKEN_KEY);
+    } catch (error) {
+      return null;
     }
+  }
 
-    static clearToken() {
-        try {
-            Taro.removeStorageSync(Auth.ACCESS_TOKEN_KEY);
-            Taro.removeStorageSync(Auth.REFRESH_TOKEN_KEY);
-        } catch (e) { }
+  // ---- 登录时间戳 ----
+
+  /** 记录登录时间(Unix 毫秒时间戳) */
+  static setLoginTime() {
+    try {
+      Taro.setStorageSync(Auth.LOGIN_TIME_KEY, Date.now());
+    } catch (e) {}
+  }
+
+  /** 读取登录时间戳,未记录时返回 0 */
+  static getLoginTime(): number {
+    try {
+      const ts = Taro.getStorageSync(Auth.LOGIN_TIME_KEY);
+      return ts ? Number(ts) : 0;
+    } catch (error) {
+      return 0;
     }
+  }
+
+  /** 登录时间是否在 {TOKEN_VALID_HOURS} 小时内 */
+  static isLoginTimeValid(): boolean {
+    const loginTime = Auth.getLoginTime();
+    if (!loginTime) return false;
+    const diffMs = Date.now() - loginTime;
+    return diffMs >= 0 && diffMs < TOKEN_VALID_HOURS * 3600 * 1000;
+  }
+
+  // ---- 复合校验 ----
 
-    static requireAuth(): boolean {
-        if (!Auth.checkLogin()) {
-            Taro.reLaunch({
-                url: '/pages/login/index',
-                success: () => {
-                    Taro.showToast({
-                        title: '请先登录',
-                        icon: 'none'
-                    });
-                }
-            });
-            return false;
-        }
-        return true;
+  /**
+   * 检查登录态是否有效:
+   * 1. token 存在
+   * 2. 登录时间在 24 小时内
+   */
+  static checkLogin(): boolean {
+    const hasToken = !!Auth.getAccessToken();
+    if (!hasToken) return false;
+    // 没有登录时间戳的旧 token 仍按有效处理(向后兼容)
+    const loginTime = Auth.getLoginTime();
+    if (!loginTime) return true;
+    return Auth.isLoginTimeValid();
+  }
+
+  // ---- 存储操作 ----
+
+  static clearStorage() {
+    Taro.clearStorageSync();
+  }
+
+  static setToken(token: Token) {
+    try {
+      Taro.setStorageSync(Auth.ACCESS_TOKEN_KEY, token.accessToken);
+      Taro.setStorageSync(Auth.REFRESH_TOKEN_KEY, token.refreshToken || '');
+      Auth.setLoginTime();
+    } catch (e) {}
+  }
+
+  static clearToken() {
+    try {
+      Taro.removeStorageSync(Auth.ACCESS_TOKEN_KEY);
+      Taro.removeStorageSync(Auth.REFRESH_TOKEN_KEY);
+      Taro.removeStorageSync(Auth.LOGIN_TIME_KEY);
+      Taro.removeStorageSync('userInfo');
+    } catch (e) {}
+  }
+
+  // ---- 登录守卫 ----
+
+  static requireAuth(): boolean {
+    if (!Auth.checkLogin()) {
+      Taro.reLaunch({
+        url: '/pages/login/index',
+        success: () => {
+          Taro.showToast({
+            title: '请先登录',
+            icon: 'none',
+          });
+        },
+      });
+      return false;
     }
-}
+    return true;
+  }
+}

部分文件因文件數量過多而無法顯示