Przeglądaj źródła

fix(auth): 统一 params/header token 解析与注入;问题答复列表与调试弹窗;详情解答按状态展示

Made-with: Cursor
haifeng.zhang 1 dzień temu
rodzic
commit
f050d8182e

+ 1 - 1
src/subPages/pages/reportProblems/detail.vue

@@ -108,7 +108,7 @@ onLoad(async (option) => {
     </view>
     <van-divider></van-divider>
     <!-- 问题解答 -->
-    <view class="form-item">
+    <view class="form-item" v-if="questDetail?.status === 1">
       <view class="label">解答:</view>
       <view class="value" v-if="questDetail?.questionAnswer">{{ questDetail?.questionAnswer }}</view>
       <view class="value text-[#e3e3e3]" v-else>暂无</view>

+ 45 - 7
src/subPages/pages/reportProblems/index.vue

@@ -2,6 +2,7 @@
 import { addQuestionReportData,REPORTDATA, getQuestionTypeList, pageQuestionReportData,getPerson } from '@/api/questionReqort'
 import { AddQuestionReportDataReq, PageQuestionReportDataReq, QuestionListRes, QuestionType, QuestionTypeListRes } from '@/api/questionReqort/types'
 import type { ApiResponse, PageResp } from '@/api/types'
+import type { AuthTokenChannelSnapshot } from '@/utils/request/requestHandler'
 import { getCachedWecomVisitorProfile, getWecomInternalUser } from '@/api/wecom'
 import { computed, onMounted, ref, watch } from 'vue'
 import GridAddress from '@/components/address/gridAddress/index.vue'
@@ -9,6 +10,7 @@ import { showNotify } from 'vant'
 import dayjs from 'dayjs';
 import { onLoad } from '@dcloudio/uni-app'
 import useUserState from '@/store/userState'
+import { getAuthTokenChannelSnapshot, resolveUserAuthToken } from '@/utils/request/requestHandler'
 
 const active = ref(0)
 const userState = useUserState()
@@ -222,6 +224,12 @@ const finished = ref(false)
 const listInitialized = ref(false)
 const listRequesting = ref(false)
 
+/** 列表重置代数:reset 后递增,用于丢弃 reset 之前发出的请求的响应,避免乱序写回 */
+const listLoadVersion = ref(0)
+
+/** 合并并发 init,避免 watch 与重复进入导致多次 reset/请求交错 */
+let listInitPromise: Promise<void> | null = null
+
 /** 列表接口调试:最近一次请求参数与完整响应 */
 const showListRawJsonPopup = ref(false)
 const listLastReqParams = ref<PageQuestionReportDataReq | null>(null)
@@ -237,28 +245,42 @@ const listApiRawJson = computed(() => {
   return JSON.stringify(listLastApiResponse.value, null, 2)
 })
 
+/** 最近一次列表请求时的 token 各渠道快照(脱敏,用于弹窗诊断) */
+const listLastTokenDiag = ref<AuthTokenChannelSnapshot | null>(null)
+
+const listTokenDiagJson = computed(() => {
+  if (!listLastTokenDiag.value) return '{}'
+  return JSON.stringify(listLastTokenDiag.value, null, 2)
+})
+
 const resetQuestionList = () => {
   list.value = []
   pageInfo.value.pageNumber = 1
   finished.value = false
   loading.value = false
   listRequesting.value = false
+  listLoadVersion.value += 1
 }
 
 const questList = async () => {
   if (listRequesting.value || finished.value) return
 
+  const requestVersion = listLoadVersion.value
   listRequesting.value = true
   loading.value = true
   try {
     const currentPage = pageInfo.value.pageNumber
+    listLastTokenDiag.value = getAuthTokenChannelSnapshot()
+    const effectiveToken = resolveUserAuthToken()
     const reqParams: PageQuestionReportDataReq = {
       pageNumber: currentPage,
       pageSize: pageInfo.value.pageSize,
-      token: userState.getUserTokenInfo || '',
+      token: effectiveToken,
     }
-    listLastReqParams.value = reqParams
     const res = await pageQuestionReportData(reqParams)
+    if (requestVersion !== listLoadVersion.value) return
+
+    listLastReqParams.value = reqParams
     listLastApiResponse.value = res
     const records = res.data.records || []
 
@@ -271,10 +293,16 @@ const questList = async () => {
   }
 }
 
-const initQuestionList = async () => {
-  resetQuestionList()
-  await questList()
-  listInitialized.value = true
+const initQuestionList = (): Promise<void> => {
+  if (listInitPromise) return listInitPromise
+  listInitPromise = (async () => {
+    resetQuestionList()
+    await questList()
+    listInitialized.value = true
+  })().finally(() => {
+    listInitPromise = null
+  })
+  return listInitPromise
 }
 
 /** 跳转问题详情 */
@@ -385,7 +413,13 @@ watch(active, async (value) => {
         <view class="mb-[8px]">
           <van-button size="small" plain type="primary" @click="showListRawJsonPopup = true">查看接口原始JSON</van-button>
         </view>
-        <van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="questList">
+        <van-list
+          v-model:loading="loading"
+          :finished="finished"
+          :immediate-check="false"
+          finished-text="没有更多了"
+          @load="questList"
+        >
           <view v-for="item in list" :key="item.uuid" class="list-item-style">
             <view class="top">
               <view class="title">{{ item.questionContent }}</view>
@@ -416,6 +450,10 @@ watch(active, async (value) => {
             <scroll-view scroll-y class="json-scroll json-scroll--small">
               <text class="json-text">{{ listReqParamsJson }}</text>
             </scroll-view>
+            <view class="json-title">token 渠道诊断(明文)</view>
+            <scroll-view scroll-y class="json-scroll json-scroll--small">
+              <text class="json-text">{{ listTokenDiagJson }}</text>
+            </scroll-view>
             <view class="json-title">列表接口完整JSON</view>
             <scroll-view scroll-y class="json-scroll">
               <text class="json-text">{{ listApiRawJson }}</text>

+ 74 - 12
src/utils/request/requestHandler.ts

@@ -1,20 +1,82 @@
 import useUserState from '@/store/userState'
-import  { AxiosRequestConfig, AxiosHeaders } from 'axios'
+import { AxiosRequestConfig, AxiosHeaders } from 'axios'
 import { HeaderKey } from './header'
+import StorageUtil from '@/utils/storage/storage'
+
+/** 弹窗/调试:各渠道 token 明文(仅用于本地调试,勿对外暴露页面) */
+export interface AuthTokenChannelSnapshot {
+  /** Pinia store(getUserTokenInfo) */
+  store: { hasValue: boolean; token: string }
+  /** 命名空间 localStorage(StorageUtil userTokenInfo) */
+  storage: { hasValue: boolean; token: string }
+  /** 最终请求实际采用的渠道 */
+  usedChannel: 'store' | 'storage' | 'none'
+  /** 与 resolveUserAuthToken() 一致,实际参与请求的明文 token */
+  effectiveToken: string
+}
+
+function readTokenFromStore(): string {
+  const userState = useUserState()
+  const raw = userState.getUserTokenInfo as string | null | undefined
+  if (raw == null) return ''
+  const s = String(raw).trim()
+  return s
+}
+
+function readTokenFromStorage(): string {
+  const raw = StorageUtil.getItem('userTokenInfo')
+  if (raw == null) return ''
+  const s = String(raw).trim()
+  return s
+}
+
+/** 登录态 token:优先 store,其次本地存储(解决首包时序导致 store 未就绪) */
+export function resolveUserAuthToken(): string {
+  return readTokenFromStore() || readTokenFromStorage() || ''
+}
+
+export function getAuthTokenChannelSnapshot(): AuthTokenChannelSnapshot {
+  const fromStore = readTokenFromStore()
+  const fromStorage = readTokenFromStorage()
+  const resolved = fromStore || fromStorage || ''
+  return {
+    store: { hasValue: !!fromStore, token: fromStore },
+    storage: { hasValue: !!fromStorage, token: fromStorage },
+    usedChannel: fromStore ? 'store' : fromStorage ? 'storage' : 'none',
+    effectiveToken: resolved,
+  }
+}
+
+function isPlainParamsObject(params: unknown): params is Record<string, unknown> {
+  return (
+    !!params &&
+    typeof params === 'object' &&
+    !Array.isArray(params) &&
+    !(params instanceof URLSearchParams)
+  )
+}
 
 /**
- * 处理token问题
+ * 处理 token:请求头 + query params.token(后端从参数取 token 时统一补齐)
  */
-export async function handleToken(requestConfig: AxiosRequestConfig) {
-  const userState = useUserState()
-  if (requestConfig.isAuth && requestConfig.headers && requestConfig.headers instanceof AxiosHeaders) {
-    if (!requestConfig.headers.get('authorization')) {
-      if (userState.getUserTokenInfo) {
-        requestConfig.headers.set(
-          HeaderKey.AUTHORIZATION,
-          userState.getUserTokenInfo
-        )
-      }
+export function handleToken(requestConfig: AxiosRequestConfig) {
+  if (!requestConfig.isAuth) return
+
+  const resolved = resolveUserAuthToken()
+
+  if (requestConfig.headers && requestConfig.headers instanceof AxiosHeaders) {
+    if (!requestConfig.headers.get('authorization') && resolved) {
+      requestConfig.headers.set(HeaderKey.AUTHORIZATION, resolved)
     }
   }
+
+  const prev = requestConfig.params
+  const base: Record<string, unknown> = isPlainParamsObject(prev) ? { ...prev } : {}
+  const existing = String(base.token ?? '').trim()
+  if (!existing && resolved) {
+    base.token = resolved
+  }
+  if (Object.keys(base).length > 0) {
+    requestConfig.params = base
+  }
 }