[{"data":1,"prerenderedAt":5218},["ShallowReactive",2],{"page-/post/nuxt/blog/auth-system-docs":3,"surrounding-page":5209},{"id":4,"title":5,"author":6,"body":7,"date":5198,"description":86,"extension":5199,"group":6,"lastmod":5198,"meta":5200,"navigation":209,"path":5201,"rawbody":5202,"seo":5203,"showTitle":6,"stem":5204,"tags":5205,"versions":6,"__hash__":5208},"content/post/nuxt/blog/auth-system-docs.md","博客鉴权机制详细文档",null,{"type":8,"value":9,"toc":5160},"minimark",[10,14,18,21,26,69,73,80,160,164,429,432,436,873,877,880,1093,1096,1281,1285,1639,1643,2018,2021,2025,2641,2645,3194,3198,3554,3557,3561,3801,3805,3891,3895,4185,4188,4192,4389,4393,4691,4694,4698,4717,4721,4810,4814,5031,5034,5038,5044,5049,5064,5068,5073,5077,5091,5095,5100,5104,5118,5121,5124,5156],[11,12,13],"h2",{"id":13},"概述",[15,16,17],"p",{},"本项目采用双 Token 鉴权机制，结合了 JWT 的无状态特性和 Redis 的有状态管理，提供了安全性和用户体验的最佳平衡。",[11,19,20],{"id":20},"鉴权架构",[22,23,25],"h3",{"id":24},"_1-双-token-机制","1. 双 Token 机制",[27,28,29,50],"ul",{},[30,31,32,36],"li",{},[33,34,35],"strong",{},"Access Token (JWT)",[27,37,38,41,44,47],{},[30,39,40],{},"类型：无状态 JWT",[30,42,43],{},"过期时间：15 分钟",[30,45,46],{},"用途：API 访问凭证",[30,48,49],{},"存储：客户端内存/localStorage",[30,51,52,55],{},[33,53,54],{},"Refresh Token",[27,56,57,60,63,66],{},[30,58,59],{},"类型：随机字符串",[30,61,62],{},"过期时间：7 天",[30,64,65],{},"用途：刷新 Access Token",[30,67,68],{},"存储：Redis（服务端）+ 客户端",[22,70,72],{"id":71},"_2-错误代码系统","2. 错误代码系统",[15,74,75,76,79],{},"所有 API 统一返回 200 HTTP 状态码，通过 ",[77,78,77],"code",{}," 字段（数值）标识业务状态：",[81,82,87],"pre",{"className":83,"code":84,"language":85,"meta":86,"style":86},"language-typescript shiki shiki-themes github-light","// 响应格式\n{\n  code: number,      // 错误代码（0表示成功）\n  message: string,   // 错误描述\n  data: any,         // 响应数据\n  timestamp: number  // 时间戳\n}\n","typescript","",[77,88,89,98,105,118,130,142,154],{"__ignoreMap":86},[90,91,94],"span",{"class":92,"line":93},"line",1,[90,95,97],{"class":96},"sAwPA","// 响应格式\n",[90,99,101],{"class":92,"line":100},2,[90,102,104],{"class":103},"sgsFI","{\n",[90,106,108,112,115],{"class":92,"line":107},3,[90,109,111],{"class":110},"s7eDp","  code",[90,113,114],{"class":103},": number,      ",[90,116,117],{"class":96},"// 错误代码（0表示成功）\n",[90,119,121,124,127],{"class":92,"line":120},4,[90,122,123],{"class":110},"  message",[90,125,126],{"class":103},": string,   ",[90,128,129],{"class":96},"// 错误描述\n",[90,131,133,136,139],{"class":92,"line":132},5,[90,134,135],{"class":110},"  data",[90,137,138],{"class":103},": any,         ",[90,140,141],{"class":96},"// 响应数据\n",[90,143,145,148,151],{"class":92,"line":144},6,[90,146,147],{"class":110},"  timestamp",[90,149,150],{"class":103},": number  ",[90,152,153],{"class":96},"// 时间戳\n",[90,155,157],{"class":92,"line":156},7,[90,158,159],{"class":103},"}\n",[161,162,163],"h4",{"id":163},"错误代码定义",[81,165,168],{"className":83,"code":166,"filename":167,"language":85,"meta":86,"style":86},"export const API_CODES = {\n  // 成功\n  SUCCESS: 0,\n\n  // 认证相关错误 (1000-1999)\n  NO_TOKEN: 1001,           // 未提供token\n  TOKEN_EXPIRED: 1002,      // token过期（可刷新）\n  TOKEN_INVALID: 1003,      // token无效（不可刷新）\n  AUTH_FAILED: 1004,        // 认证失败\n  REFRESH_TOKEN_EXPIRED: 1005, // refresh token过期\n\n  // 权限相关错误 (2000-2999)\n  PERMISSION_DENIED: 2001,  // 无权限\n  FORBIDDEN: 2002,          // 禁止访问\n\n  // 业务相关错误 (3000-3999)\n  VALIDATION_ERROR: 3001,   // 参数验证错误\n  RESOURCE_NOT_FOUND: 3002, // 资源不存在\n  DUPLICATE_ERROR: 3003,    // 重复错误\n\n  // 系统错误 (9000-9999)\n  INTERNAL_ERROR: 9001,     // 内部错误\n  NETWORK_ERROR: 9002,      // 网络错误\n}\n","/shared/utils/apiCodes.ts",[77,169,170,189,194,205,211,216,230,244,258,273,288,293,299,314,329,334,340,355,369,384,389,395,410,424],{"__ignoreMap":86},[90,171,172,176,179,183,186],{"class":92,"line":93},[90,173,175],{"class":174},"sD7c4","export",[90,177,178],{"class":174}," const",[90,180,182],{"class":181},"sYu0t"," API_CODES",[90,184,185],{"class":174}," =",[90,187,188],{"class":103}," {\n",[90,190,191],{"class":92,"line":100},[90,192,193],{"class":96},"  // 成功\n",[90,195,196,199,202],{"class":92,"line":107},[90,197,198],{"class":103},"  SUCCESS: ",[90,200,201],{"class":181},"0",[90,203,204],{"class":103},",\n",[90,206,207],{"class":92,"line":120},[90,208,210],{"emptyLinePlaceholder":209},true,"\n",[90,212,213],{"class":92,"line":132},[90,214,215],{"class":96},"  // 认证相关错误 (1000-1999)\n",[90,217,218,221,224,227],{"class":92,"line":144},[90,219,220],{"class":103},"  NO_TOKEN: ",[90,222,223],{"class":181},"1001",[90,225,226],{"class":103},",           ",[90,228,229],{"class":96},"// 未提供token\n",[90,231,232,235,238,241],{"class":92,"line":156},[90,233,234],{"class":103},"  TOKEN_EXPIRED: ",[90,236,237],{"class":181},"1002",[90,239,240],{"class":103},",      ",[90,242,243],{"class":96},"// token过期（可刷新）\n",[90,245,247,250,253,255],{"class":92,"line":246},8,[90,248,249],{"class":103},"  TOKEN_INVALID: ",[90,251,252],{"class":181},"1003",[90,254,240],{"class":103},[90,256,257],{"class":96},"// token无效（不可刷新）\n",[90,259,261,264,267,270],{"class":92,"line":260},9,[90,262,263],{"class":103},"  AUTH_FAILED: ",[90,265,266],{"class":181},"1004",[90,268,269],{"class":103},",        ",[90,271,272],{"class":96},"// 认证失败\n",[90,274,276,279,282,285],{"class":92,"line":275},10,[90,277,278],{"class":103},"  REFRESH_TOKEN_EXPIRED: ",[90,280,281],{"class":181},"1005",[90,283,284],{"class":103},", ",[90,286,287],{"class":96},"// refresh token过期\n",[90,289,291],{"class":92,"line":290},11,[90,292,210],{"emptyLinePlaceholder":209},[90,294,296],{"class":92,"line":295},12,[90,297,298],{"class":96},"  // 权限相关错误 (2000-2999)\n",[90,300,302,305,308,311],{"class":92,"line":301},13,[90,303,304],{"class":103},"  PERMISSION_DENIED: ",[90,306,307],{"class":181},"2001",[90,309,310],{"class":103},",  ",[90,312,313],{"class":96},"// 无权限\n",[90,315,317,320,323,326],{"class":92,"line":316},14,[90,318,319],{"class":103},"  FORBIDDEN: ",[90,321,322],{"class":181},"2002",[90,324,325],{"class":103},",          ",[90,327,328],{"class":96},"// 禁止访问\n",[90,330,332],{"class":92,"line":331},15,[90,333,210],{"emptyLinePlaceholder":209},[90,335,337],{"class":92,"line":336},16,[90,338,339],{"class":96},"  // 业务相关错误 (3000-3999)\n",[90,341,343,346,349,352],{"class":92,"line":342},17,[90,344,345],{"class":103},"  VALIDATION_ERROR: ",[90,347,348],{"class":181},"3001",[90,350,351],{"class":103},",   ",[90,353,354],{"class":96},"// 参数验证错误\n",[90,356,358,361,364,366],{"class":92,"line":357},18,[90,359,360],{"class":103},"  RESOURCE_NOT_FOUND: ",[90,362,363],{"class":181},"3002",[90,365,284],{"class":103},[90,367,368],{"class":96},"// 资源不存在\n",[90,370,372,375,378,381],{"class":92,"line":371},19,[90,373,374],{"class":103},"  DUPLICATE_ERROR: ",[90,376,377],{"class":181},"3003",[90,379,380],{"class":103},",    ",[90,382,383],{"class":96},"// 重复错误\n",[90,385,387],{"class":92,"line":386},20,[90,388,210],{"emptyLinePlaceholder":209},[90,390,392],{"class":92,"line":391},21,[90,393,394],{"class":96},"  // 系统错误 (9000-9999)\n",[90,396,398,401,404,407],{"class":92,"line":397},22,[90,399,400],{"class":103},"  INTERNAL_ERROR: ",[90,402,403],{"class":181},"9001",[90,405,406],{"class":103},",     ",[90,408,409],{"class":96},"// 内部错误\n",[90,411,413,416,419,421],{"class":92,"line":412},23,[90,414,415],{"class":103},"  NETWORK_ERROR: ",[90,417,418],{"class":181},"9002",[90,420,240],{"class":103},[90,422,423],{"class":96},"// 网络错误\n",[90,425,427],{"class":92,"line":426},24,[90,428,159],{"class":103},[11,430,431],{"id":431},"后端实现",[22,433,435],{"id":434},"_1-api-响应处理器","1. API 响应处理器",[81,437,440],{"className":83,"code":438,"filename":439,"language":85,"meta":86,"style":86},"export const defineStandardResponseHandler = \u003CT extends EventHandlerRequest, D> (\n  handler: EventHandler\u003CT, D>,\n): EventHandler\u003CT, D> =>\n  defineEventHandler\u003CT>(async (event) => {\n    try {\n      const response = await handler(event)\n      // 成功响应\n      return {\n        code: API_CODES.SUCCESS,\n        message: 'ok',\n        data: response,\n        timestamp: Date.now(),\n      }\n    }\n    catch (error: any) {\n      // 强制设置 HTTP 状态码为 200\n      setResponseStatus(event, 200)\n      \n      if (error.statusCode) {\n        const customCode = error.data?.code\n        const customMessage = error.data?.message || error.message\n        \n        return {\n          code: customCode || API_CODES.INTERNAL_ERROR,\n          message: customMessage || '出错啦，请稍后再试～',\n          data: error.data?.data || null,\n          timestamp: Date.now(),\n        }\n      }\n      \n      // 未知错误\n      return {\n        code: API_CODES.INTERNAL_ERROR,\n        message: '出错啦，请稍后再试～',\n        data: null,\n        timestamp: Date.now(),\n      }\n    }\n  })\n","/server/utils/handler.ts",[77,441,442,473,497,520,549,556,575,580,587,603,614,619,630,635,640,658,663,677,682,690,703,721,726,733,749,762,775,785,791,796,801,807,814,827,837,848,857,862,867],{"__ignoreMap":86},[90,443,444,446,448,451,453,456,459,462,465,467,470],{"class":92,"line":93},[90,445,175],{"class":174},[90,447,178],{"class":174},[90,449,450],{"class":110}," defineStandardResponseHandler",[90,452,185],{"class":174},[90,454,455],{"class":103}," \u003C",[90,457,458],{"class":110},"T",[90,460,461],{"class":174}," extends",[90,463,464],{"class":110}," EventHandlerRequest",[90,466,284],{"class":103},[90,468,469],{"class":110},"D",[90,471,472],{"class":103},"> (\n",[90,474,475,479,482,485,488,490,492,494],{"class":92,"line":100},[90,476,478],{"class":477},"sqxcx","  handler",[90,480,481],{"class":174},":",[90,483,484],{"class":110}," EventHandler",[90,486,487],{"class":103},"\u003C",[90,489,458],{"class":110},[90,491,284],{"class":103},[90,493,469],{"class":110},[90,495,496],{"class":103},">,\n",[90,498,499,502,504,506,508,510,512,514,517],{"class":92,"line":107},[90,500,501],{"class":103},")",[90,503,481],{"class":174},[90,505,484],{"class":110},[90,507,487],{"class":103},[90,509,458],{"class":110},[90,511,284],{"class":103},[90,513,469],{"class":110},[90,515,516],{"class":103},"> ",[90,518,519],{"class":174},"=>\n",[90,521,522,525,527,529,532,535,538,541,544,547],{"class":92,"line":120},[90,523,524],{"class":110},"  defineEventHandler",[90,526,487],{"class":103},[90,528,458],{"class":110},[90,530,531],{"class":103},">(",[90,533,534],{"class":174},"async",[90,536,537],{"class":103}," (",[90,539,540],{"class":477},"event",[90,542,543],{"class":103},") ",[90,545,546],{"class":174},"=>",[90,548,188],{"class":103},[90,550,551,554],{"class":92,"line":132},[90,552,553],{"class":174},"    try",[90,555,188],{"class":103},[90,557,558,561,564,566,569,572],{"class":92,"line":144},[90,559,560],{"class":174},"      const",[90,562,563],{"class":181}," response",[90,565,185],{"class":174},[90,567,568],{"class":174}," await",[90,570,571],{"class":110}," handler",[90,573,574],{"class":103},"(event)\n",[90,576,577],{"class":92,"line":156},[90,578,579],{"class":96},"      // 成功响应\n",[90,581,582,585],{"class":92,"line":246},[90,583,584],{"class":174},"      return",[90,586,188],{"class":103},[90,588,589,592,595,598,601],{"class":92,"line":260},[90,590,591],{"class":103},"        code: ",[90,593,594],{"class":181},"API_CODES",[90,596,597],{"class":103},".",[90,599,600],{"class":181},"SUCCESS",[90,602,204],{"class":103},[90,604,605,608,612],{"class":92,"line":275},[90,606,607],{"class":103},"        message: ",[90,609,611],{"class":610},"sYBdl","'ok'",[90,613,204],{"class":103},[90,615,616],{"class":92,"line":290},[90,617,618],{"class":103},"        data: response,\n",[90,620,621,624,627],{"class":92,"line":295},[90,622,623],{"class":103},"        timestamp: Date.",[90,625,626],{"class":110},"now",[90,628,629],{"class":103},"(),\n",[90,631,632],{"class":92,"line":301},[90,633,634],{"class":103},"      }\n",[90,636,637],{"class":92,"line":316},[90,638,639],{"class":103},"    }\n",[90,641,642,645,647,650,652,655],{"class":92,"line":331},[90,643,644],{"class":174},"    catch",[90,646,537],{"class":103},[90,648,649],{"class":477},"error",[90,651,481],{"class":174},[90,653,654],{"class":181}," any",[90,656,657],{"class":103},") {\n",[90,659,660],{"class":92,"line":336},[90,661,662],{"class":96},"      // 强制设置 HTTP 状态码为 200\n",[90,664,665,668,671,674],{"class":92,"line":342},[90,666,667],{"class":110},"      setResponseStatus",[90,669,670],{"class":103},"(event, ",[90,672,673],{"class":181},"200",[90,675,676],{"class":103},")\n",[90,678,679],{"class":92,"line":357},[90,680,681],{"class":103},"      \n",[90,683,684,687],{"class":92,"line":371},[90,685,686],{"class":174},"      if",[90,688,689],{"class":103}," (error.statusCode) {\n",[90,691,692,695,698,700],{"class":92,"line":386},[90,693,694],{"class":174},"        const",[90,696,697],{"class":181}," customCode",[90,699,185],{"class":174},[90,701,702],{"class":103}," error.data?.code\n",[90,704,705,707,710,712,715,718],{"class":92,"line":391},[90,706,694],{"class":174},[90,708,709],{"class":181}," customMessage",[90,711,185],{"class":174},[90,713,714],{"class":103}," error.data?.message ",[90,716,717],{"class":174},"||",[90,719,720],{"class":103}," error.message\n",[90,722,723],{"class":92,"line":397},[90,724,725],{"class":103},"        \n",[90,727,728,731],{"class":92,"line":412},[90,729,730],{"class":174},"        return",[90,732,188],{"class":103},[90,734,735,738,740,742,744,747],{"class":92,"line":426},[90,736,737],{"class":103},"          code: customCode ",[90,739,717],{"class":174},[90,741,182],{"class":181},[90,743,597],{"class":103},[90,745,746],{"class":181},"INTERNAL_ERROR",[90,748,204],{"class":103},[90,750,752,755,757,760],{"class":92,"line":751},25,[90,753,754],{"class":103},"          message: customMessage ",[90,756,717],{"class":174},[90,758,759],{"class":610}," '出错啦，请稍后再试～'",[90,761,204],{"class":103},[90,763,765,768,770,773],{"class":92,"line":764},26,[90,766,767],{"class":103},"          data: error.data?.data ",[90,769,717],{"class":174},[90,771,772],{"class":181}," null",[90,774,204],{"class":103},[90,776,778,781,783],{"class":92,"line":777},27,[90,779,780],{"class":103},"          timestamp: Date.",[90,782,626],{"class":110},[90,784,629],{"class":103},[90,786,788],{"class":92,"line":787},28,[90,789,790],{"class":103},"        }\n",[90,792,794],{"class":92,"line":793},29,[90,795,634],{"class":103},[90,797,799],{"class":92,"line":798},30,[90,800,681],{"class":103},[90,802,804],{"class":92,"line":803},31,[90,805,806],{"class":96},"      // 未知错误\n",[90,808,810,812],{"class":92,"line":809},32,[90,811,584],{"class":174},[90,813,188],{"class":103},[90,815,817,819,821,823,825],{"class":92,"line":816},33,[90,818,591],{"class":103},[90,820,594],{"class":181},[90,822,597],{"class":103},[90,824,746],{"class":181},[90,826,204],{"class":103},[90,828,830,832,835],{"class":92,"line":829},34,[90,831,607],{"class":103},[90,833,834],{"class":610},"'出错啦，请稍后再试～'",[90,836,204],{"class":103},[90,838,840,843,846],{"class":92,"line":839},35,[90,841,842],{"class":103},"        data: ",[90,844,845],{"class":181},"null",[90,847,204],{"class":103},[90,849,851,853,855],{"class":92,"line":850},36,[90,852,623],{"class":103},[90,854,626],{"class":110},[90,856,629],{"class":103},[90,858,860],{"class":92,"line":859},37,[90,861,634],{"class":103},[90,863,865],{"class":92,"line":864},38,[90,866,639],{"class":103},[90,868,870],{"class":92,"line":869},39,[90,871,872],{"class":103},"  })\n",[22,874,876],{"id":875},"_2-api-开发方式","2. API 开发方式",[161,878,879],{"id":879},"成功案例",[81,881,883],{"className":83,"code":882,"language":85,"meta":86,"style":86},"// /server/api/v1/user/login.post.ts\nexport default defineStandardResponseHandler(async (event) => {\n  const body = await useSafeValidatedBody(event, schema)\n\n  if (!body.success) {\n    throw createError({\n      statusCode: 400,\n      data: {\n        code: API_CODES.VALIDATION_ERROR,\n        message: '参数验证失败',\n        data: body.error,\n      },\n    })\n  }\n\n  // 业务逻辑处理...\n  const tokenPair = await generateTokenPair(user.id)\n\n  // 直接返回数据，由 handler 包装成标准格式\n  return {\n    accessToken: tokenPair.accessToken,\n    refreshToken: tokenPair.refreshToken,\n    accessExpiresAt: tokenPair.accessExpiresAt,\n    refreshExpiresAt: tokenPair.refreshExpiresAt,\n    user,\n  }\n})\n",[77,884,885,890,914,932,936,949,960,970,975,988,997,1002,1007,1012,1017,1021,1026,1043,1047,1052,1059,1064,1069,1074,1079,1084,1088],{"__ignoreMap":86},[90,886,887],{"class":92,"line":93},[90,888,889],{"class":96},"// /server/api/v1/user/login.post.ts\n",[90,891,892,894,897,899,902,904,906,908,910,912],{"class":92,"line":100},[90,893,175],{"class":174},[90,895,896],{"class":174}," default",[90,898,450],{"class":110},[90,900,901],{"class":103},"(",[90,903,534],{"class":174},[90,905,537],{"class":103},[90,907,540],{"class":477},[90,909,543],{"class":103},[90,911,546],{"class":174},[90,913,188],{"class":103},[90,915,916,919,922,924,926,929],{"class":92,"line":107},[90,917,918],{"class":174},"  const",[90,920,921],{"class":181}," body",[90,923,185],{"class":174},[90,925,568],{"class":174},[90,927,928],{"class":110}," useSafeValidatedBody",[90,930,931],{"class":103},"(event, schema)\n",[90,933,934],{"class":92,"line":120},[90,935,210],{"emptyLinePlaceholder":209},[90,937,938,941,943,946],{"class":92,"line":132},[90,939,940],{"class":174},"  if",[90,942,537],{"class":103},[90,944,945],{"class":174},"!",[90,947,948],{"class":103},"body.success) {\n",[90,950,951,954,957],{"class":92,"line":144},[90,952,953],{"class":174},"    throw",[90,955,956],{"class":110}," createError",[90,958,959],{"class":103},"({\n",[90,961,962,965,968],{"class":92,"line":156},[90,963,964],{"class":103},"      statusCode: ",[90,966,967],{"class":181},"400",[90,969,204],{"class":103},[90,971,972],{"class":92,"line":246},[90,973,974],{"class":103},"      data: {\n",[90,976,977,979,981,983,986],{"class":92,"line":260},[90,978,591],{"class":103},[90,980,594],{"class":181},[90,982,597],{"class":103},[90,984,985],{"class":181},"VALIDATION_ERROR",[90,987,204],{"class":103},[90,989,990,992,995],{"class":92,"line":275},[90,991,607],{"class":103},[90,993,994],{"class":610},"'参数验证失败'",[90,996,204],{"class":103},[90,998,999],{"class":92,"line":290},[90,1000,1001],{"class":103},"        data: body.error,\n",[90,1003,1004],{"class":92,"line":295},[90,1005,1006],{"class":103},"      },\n",[90,1008,1009],{"class":92,"line":301},[90,1010,1011],{"class":103},"    })\n",[90,1013,1014],{"class":92,"line":316},[90,1015,1016],{"class":103},"  }\n",[90,1018,1019],{"class":92,"line":331},[90,1020,210],{"emptyLinePlaceholder":209},[90,1022,1023],{"class":92,"line":336},[90,1024,1025],{"class":96},"  // 业务逻辑处理...\n",[90,1027,1028,1030,1033,1035,1037,1040],{"class":92,"line":342},[90,1029,918],{"class":174},[90,1031,1032],{"class":181}," tokenPair",[90,1034,185],{"class":174},[90,1036,568],{"class":174},[90,1038,1039],{"class":110}," generateTokenPair",[90,1041,1042],{"class":103},"(user.id)\n",[90,1044,1045],{"class":92,"line":357},[90,1046,210],{"emptyLinePlaceholder":209},[90,1048,1049],{"class":92,"line":371},[90,1050,1051],{"class":96},"  // 直接返回数据，由 handler 包装成标准格式\n",[90,1053,1054,1057],{"class":92,"line":386},[90,1055,1056],{"class":174},"  return",[90,1058,188],{"class":103},[90,1060,1061],{"class":92,"line":391},[90,1062,1063],{"class":103},"    accessToken: tokenPair.accessToken,\n",[90,1065,1066],{"class":92,"line":397},[90,1067,1068],{"class":103},"    refreshToken: tokenPair.refreshToken,\n",[90,1070,1071],{"class":92,"line":412},[90,1072,1073],{"class":103},"    accessExpiresAt: tokenPair.accessExpiresAt,\n",[90,1075,1076],{"class":92,"line":426},[90,1077,1078],{"class":103},"    refreshExpiresAt: tokenPair.refreshExpiresAt,\n",[90,1080,1081],{"class":92,"line":751},[90,1082,1083],{"class":103},"    user,\n",[90,1085,1086],{"class":92,"line":764},[90,1087,1016],{"class":103},[90,1089,1090],{"class":92,"line":777},[90,1091,1092],{"class":103},"})\n",[161,1094,1095],{"id":1095},"错误处理",[81,1097,1099],{"className":83,"code":1098,"language":85,"meta":86,"style":86},"// 参数验证错误\nthrow createError({\n  statusCode: 400,\n  data: {\n    code: API_CODES.VALIDATION_ERROR,\n    message: '参数验证失败',\n    data: validationErrors,\n  },\n})\n\n// 认证失败\nthrow createError({\n  statusCode: 401,\n  data: {\n    code: API_CODES.AUTH_FAILED,\n    message: '账号或密码错误',\n  },\n})\n\n// 内部错误\nthrow createError({\n  statusCode: 500,\n  data: {\n    code: API_CODES.INTERNAL_ERROR,\n    message: '系统繁忙，请稍后重试',\n  },\n})\n",[77,1100,1101,1105,1114,1123,1128,1141,1150,1155,1160,1164,1168,1172,1180,1189,1193,1206,1215,1219,1223,1227,1231,1239,1248,1252,1264,1273,1277],{"__ignoreMap":86},[90,1102,1103],{"class":92,"line":93},[90,1104,354],{"class":96},[90,1106,1107,1110,1112],{"class":92,"line":100},[90,1108,1109],{"class":174},"throw",[90,1111,956],{"class":110},[90,1113,959],{"class":103},[90,1115,1116,1119,1121],{"class":92,"line":107},[90,1117,1118],{"class":103},"  statusCode: ",[90,1120,967],{"class":181},[90,1122,204],{"class":103},[90,1124,1125],{"class":92,"line":120},[90,1126,1127],{"class":103},"  data: {\n",[90,1129,1130,1133,1135,1137,1139],{"class":92,"line":132},[90,1131,1132],{"class":103},"    code: ",[90,1134,594],{"class":181},[90,1136,597],{"class":103},[90,1138,985],{"class":181},[90,1140,204],{"class":103},[90,1142,1143,1146,1148],{"class":92,"line":144},[90,1144,1145],{"class":103},"    message: ",[90,1147,994],{"class":610},[90,1149,204],{"class":103},[90,1151,1152],{"class":92,"line":156},[90,1153,1154],{"class":103},"    data: validationErrors,\n",[90,1156,1157],{"class":92,"line":246},[90,1158,1159],{"class":103},"  },\n",[90,1161,1162],{"class":92,"line":260},[90,1163,1092],{"class":103},[90,1165,1166],{"class":92,"line":275},[90,1167,210],{"emptyLinePlaceholder":209},[90,1169,1170],{"class":92,"line":290},[90,1171,272],{"class":96},[90,1173,1174,1176,1178],{"class":92,"line":295},[90,1175,1109],{"class":174},[90,1177,956],{"class":110},[90,1179,959],{"class":103},[90,1181,1182,1184,1187],{"class":92,"line":301},[90,1183,1118],{"class":103},[90,1185,1186],{"class":181},"401",[90,1188,204],{"class":103},[90,1190,1191],{"class":92,"line":316},[90,1192,1127],{"class":103},[90,1194,1195,1197,1199,1201,1204],{"class":92,"line":331},[90,1196,1132],{"class":103},[90,1198,594],{"class":181},[90,1200,597],{"class":103},[90,1202,1203],{"class":181},"AUTH_FAILED",[90,1205,204],{"class":103},[90,1207,1208,1210,1213],{"class":92,"line":336},[90,1209,1145],{"class":103},[90,1211,1212],{"class":610},"'账号或密码错误'",[90,1214,204],{"class":103},[90,1216,1217],{"class":92,"line":342},[90,1218,1159],{"class":103},[90,1220,1221],{"class":92,"line":357},[90,1222,1092],{"class":103},[90,1224,1225],{"class":92,"line":371},[90,1226,210],{"emptyLinePlaceholder":209},[90,1228,1229],{"class":92,"line":386},[90,1230,409],{"class":96},[90,1232,1233,1235,1237],{"class":92,"line":391},[90,1234,1109],{"class":174},[90,1236,956],{"class":110},[90,1238,959],{"class":103},[90,1240,1241,1243,1246],{"class":92,"line":397},[90,1242,1118],{"class":103},[90,1244,1245],{"class":181},"500",[90,1247,204],{"class":103},[90,1249,1250],{"class":92,"line":412},[90,1251,1127],{"class":103},[90,1253,1254,1256,1258,1260,1262],{"class":92,"line":426},[90,1255,1132],{"class":103},[90,1257,594],{"class":181},[90,1259,597],{"class":103},[90,1261,746],{"class":181},[90,1263,204],{"class":103},[90,1265,1266,1268,1271],{"class":92,"line":751},[90,1267,1145],{"class":103},[90,1269,1270],{"class":610},"'系统繁忙，请稍后重试'",[90,1272,204],{"class":103},[90,1274,1275],{"class":92,"line":764},[90,1276,1159],{"class":103},[90,1278,1279],{"class":92,"line":777},[90,1280,1092],{"class":103},[22,1282,1284],{"id":1283},"_3-鉴权中间件","3. 鉴权中间件",[81,1286,1289],{"className":83,"code":1287,"filename":1288,"language":85,"meta":86,"style":86},"export default defineEventHandler(async (event) => {\n  // 需要鉴权的路径判断\n  if (needsAuth(event)) {\n    if (!event.context.token) {\n      // 强制设置状态码为 200，返回错误代码\n      setResponseStatus(event, 200)\n      return {\n        code: API_CODES.NO_TOKEN,\n        message: API_ERROR_MESSAGES[API_CODES.NO_TOKEN],\n        data: null,\n        timestamp: Date.now(),\n      }\n    }\n\n    const { isAuth, userId, error } = verifyJWTAccessToken(event.context.token)\n\n    if (!isAuth) {\n      let errorCode = API_CODES.AUTH_FAILED\n      if (error?.includes('expired')) {\n        errorCode = API_CODES.TOKEN_EXPIRED\n      } else if (error?.includes('invalid')) {\n        errorCode = API_CODES.TOKEN_INVALID\n      }\n\n      setResponseStatus(event, 200)\n      return {\n        code: errorCode,\n        message: API_ERROR_MESSAGES[errorCode],\n        data: null,\n        timestamp: Date.now(),\n      }\n    }\n\n    event.context.userId = userId\n  }\n})\n","/server/middleware/2.auth0.ts",[77,1290,1291,1314,1319,1331,1343,1348,1358,1364,1377,1396,1404,1412,1416,1420,1424,1456,1460,1471,1488,1506,1520,1542,1555,1559,1563,1573,1579,1584,1593,1601,1609,1613,1617,1621,1631,1635],{"__ignoreMap":86},[90,1292,1293,1295,1297,1300,1302,1304,1306,1308,1310,1312],{"class":92,"line":93},[90,1294,175],{"class":174},[90,1296,896],{"class":174},[90,1298,1299],{"class":110}," defineEventHandler",[90,1301,901],{"class":103},[90,1303,534],{"class":174},[90,1305,537],{"class":103},[90,1307,540],{"class":477},[90,1309,543],{"class":103},[90,1311,546],{"class":174},[90,1313,188],{"class":103},[90,1315,1316],{"class":92,"line":100},[90,1317,1318],{"class":96},"  // 需要鉴权的路径判断\n",[90,1320,1321,1323,1325,1328],{"class":92,"line":107},[90,1322,940],{"class":174},[90,1324,537],{"class":103},[90,1326,1327],{"class":110},"needsAuth",[90,1329,1330],{"class":103},"(event)) {\n",[90,1332,1333,1336,1338,1340],{"class":92,"line":120},[90,1334,1335],{"class":174},"    if",[90,1337,537],{"class":103},[90,1339,945],{"class":174},[90,1341,1342],{"class":103},"event.context.token) {\n",[90,1344,1345],{"class":92,"line":132},[90,1346,1347],{"class":96},"      // 强制设置状态码为 200，返回错误代码\n",[90,1349,1350,1352,1354,1356],{"class":92,"line":144},[90,1351,667],{"class":110},[90,1353,670],{"class":103},[90,1355,673],{"class":181},[90,1357,676],{"class":103},[90,1359,1360,1362],{"class":92,"line":156},[90,1361,584],{"class":174},[90,1363,188],{"class":103},[90,1365,1366,1368,1370,1372,1375],{"class":92,"line":246},[90,1367,591],{"class":103},[90,1369,594],{"class":181},[90,1371,597],{"class":103},[90,1373,1374],{"class":181},"NO_TOKEN",[90,1376,204],{"class":103},[90,1378,1379,1381,1384,1387,1389,1391,1393],{"class":92,"line":260},[90,1380,607],{"class":103},[90,1382,1383],{"class":181},"API_ERROR_MESSAGES",[90,1385,1386],{"class":103},"[",[90,1388,594],{"class":181},[90,1390,597],{"class":103},[90,1392,1374],{"class":181},[90,1394,1395],{"class":103},"],\n",[90,1397,1398,1400,1402],{"class":92,"line":275},[90,1399,842],{"class":103},[90,1401,845],{"class":181},[90,1403,204],{"class":103},[90,1405,1406,1408,1410],{"class":92,"line":290},[90,1407,623],{"class":103},[90,1409,626],{"class":110},[90,1411,629],{"class":103},[90,1413,1414],{"class":92,"line":295},[90,1415,634],{"class":103},[90,1417,1418],{"class":92,"line":301},[90,1419,639],{"class":103},[90,1421,1422],{"class":92,"line":316},[90,1423,210],{"emptyLinePlaceholder":209},[90,1425,1426,1429,1432,1435,1437,1440,1442,1444,1447,1450,1453],{"class":92,"line":331},[90,1427,1428],{"class":174},"    const",[90,1430,1431],{"class":103}," { ",[90,1433,1434],{"class":181},"isAuth",[90,1436,284],{"class":103},[90,1438,1439],{"class":181},"userId",[90,1441,284],{"class":103},[90,1443,649],{"class":181},[90,1445,1446],{"class":103}," } ",[90,1448,1449],{"class":174},"=",[90,1451,1452],{"class":110}," verifyJWTAccessToken",[90,1454,1455],{"class":103},"(event.context.token)\n",[90,1457,1458],{"class":92,"line":336},[90,1459,210],{"emptyLinePlaceholder":209},[90,1461,1462,1464,1466,1468],{"class":92,"line":342},[90,1463,1335],{"class":174},[90,1465,537],{"class":103},[90,1467,945],{"class":174},[90,1469,1470],{"class":103},"isAuth) {\n",[90,1472,1473,1476,1479,1481,1483,1485],{"class":92,"line":357},[90,1474,1475],{"class":174},"      let",[90,1477,1478],{"class":103}," errorCode ",[90,1480,1449],{"class":174},[90,1482,182],{"class":181},[90,1484,597],{"class":103},[90,1486,1487],{"class":181},"AUTH_FAILED\n",[90,1489,1490,1492,1495,1498,1500,1503],{"class":92,"line":371},[90,1491,686],{"class":174},[90,1493,1494],{"class":103}," (error?.",[90,1496,1497],{"class":110},"includes",[90,1499,901],{"class":103},[90,1501,1502],{"class":610},"'expired'",[90,1504,1505],{"class":103},")) {\n",[90,1507,1508,1511,1513,1515,1517],{"class":92,"line":386},[90,1509,1510],{"class":103},"        errorCode ",[90,1512,1449],{"class":174},[90,1514,182],{"class":181},[90,1516,597],{"class":103},[90,1518,1519],{"class":181},"TOKEN_EXPIRED\n",[90,1521,1522,1525,1528,1531,1533,1535,1537,1540],{"class":92,"line":391},[90,1523,1524],{"class":103},"      } ",[90,1526,1527],{"class":174},"else",[90,1529,1530],{"class":174}," if",[90,1532,1494],{"class":103},[90,1534,1497],{"class":110},[90,1536,901],{"class":103},[90,1538,1539],{"class":610},"'invalid'",[90,1541,1505],{"class":103},[90,1543,1544,1546,1548,1550,1552],{"class":92,"line":397},[90,1545,1510],{"class":103},[90,1547,1449],{"class":174},[90,1549,182],{"class":181},[90,1551,597],{"class":103},[90,1553,1554],{"class":181},"TOKEN_INVALID\n",[90,1556,1557],{"class":92,"line":412},[90,1558,634],{"class":103},[90,1560,1561],{"class":92,"line":426},[90,1562,210],{"emptyLinePlaceholder":209},[90,1564,1565,1567,1569,1571],{"class":92,"line":751},[90,1566,667],{"class":110},[90,1568,670],{"class":103},[90,1570,673],{"class":181},[90,1572,676],{"class":103},[90,1574,1575,1577],{"class":92,"line":764},[90,1576,584],{"class":174},[90,1578,188],{"class":103},[90,1580,1581],{"class":92,"line":777},[90,1582,1583],{"class":103},"        code: errorCode,\n",[90,1585,1586,1588,1590],{"class":92,"line":787},[90,1587,607],{"class":103},[90,1589,1383],{"class":181},[90,1591,1592],{"class":103},"[errorCode],\n",[90,1594,1595,1597,1599],{"class":92,"line":793},[90,1596,842],{"class":103},[90,1598,845],{"class":181},[90,1600,204],{"class":103},[90,1602,1603,1605,1607],{"class":92,"line":798},[90,1604,623],{"class":103},[90,1606,626],{"class":110},[90,1608,629],{"class":103},[90,1610,1611],{"class":92,"line":803},[90,1612,634],{"class":103},[90,1614,1615],{"class":92,"line":809},[90,1616,639],{"class":103},[90,1618,1619],{"class":92,"line":816},[90,1620,210],{"emptyLinePlaceholder":209},[90,1622,1623,1626,1628],{"class":92,"line":829},[90,1624,1625],{"class":103},"    event.context.userId ",[90,1627,1449],{"class":174},[90,1629,1630],{"class":103}," userId\n",[90,1632,1633],{"class":92,"line":839},[90,1634,1016],{"class":103},[90,1636,1637],{"class":92,"line":850},[90,1638,1092],{"class":103},[22,1640,1642],{"id":1641},"_4-token-管理","4. Token 管理",[81,1644,1647],{"className":83,"code":1645,"filename":1646,"language":85,"meta":86,"style":86},"// 生成双 Token\nexport async function generateTokenPair(userId: number) {\n  const accessToken = generateJWT(userId, '15m')\n  const refreshToken = generateRandomToken()\n  \n  // 存储 refresh token 到 Redis\n  await redis.setex(`refresh_token:${userId}`, 7 * 24 * 60 * 60, refreshToken)\n  \n  return {\n    accessToken,\n    refreshToken,\n    accessExpiresAt: Date.now() + 15 * 60 * 1000,\n    refreshExpiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000,\n  }\n}\n\n// 刷新访问令牌\nexport async function refreshAccessToken(refreshToken: string) {\n  // 从 Redis 验证 refresh token\n  const userId = await redis.get(`refresh_token_user:${refreshToken}`)\n  \n  if (!userId) {\n    return null // refresh token 无效或过期\n  }\n\n  // 生成新的 access token\n  const newAccessToken = generateJWT(userId, '15m')\n  \n  return {\n    accessToken: newAccessToken,\n    expiresAt: Date.now() + 15 * 60 * 1000,\n  }\n}\n","/server/utils/token.ts",[77,1648,1649,1654,1677,1697,1712,1717,1722,1766,1770,1776,1781,1786,1813,1845,1849,1853,1857,1862,1885,1890,1917,1921,1932,1942,1946,1950,1955,1972,1976,1982,1987,2010,2014],{"__ignoreMap":86},[90,1650,1651],{"class":92,"line":93},[90,1652,1653],{"class":96},"// 生成双 Token\n",[90,1655,1656,1658,1661,1664,1666,1668,1670,1672,1675],{"class":92,"line":100},[90,1657,175],{"class":174},[90,1659,1660],{"class":174}," async",[90,1662,1663],{"class":174}," function",[90,1665,1039],{"class":110},[90,1667,901],{"class":103},[90,1669,1439],{"class":477},[90,1671,481],{"class":174},[90,1673,1674],{"class":181}," number",[90,1676,657],{"class":103},[90,1678,1679,1681,1684,1686,1689,1692,1695],{"class":92,"line":107},[90,1680,918],{"class":174},[90,1682,1683],{"class":181}," accessToken",[90,1685,185],{"class":174},[90,1687,1688],{"class":110}," generateJWT",[90,1690,1691],{"class":103},"(userId, ",[90,1693,1694],{"class":610},"'15m'",[90,1696,676],{"class":103},[90,1698,1699,1701,1704,1706,1709],{"class":92,"line":120},[90,1700,918],{"class":174},[90,1702,1703],{"class":181}," refreshToken",[90,1705,185],{"class":174},[90,1707,1708],{"class":110}," generateRandomToken",[90,1710,1711],{"class":103},"()\n",[90,1713,1714],{"class":92,"line":132},[90,1715,1716],{"class":103},"  \n",[90,1718,1719],{"class":92,"line":144},[90,1720,1721],{"class":96},"  // 存储 refresh token 到 Redis\n",[90,1723,1724,1727,1730,1733,1735,1738,1740,1743,1745,1748,1751,1754,1756,1759,1761,1763],{"class":92,"line":156},[90,1725,1726],{"class":174},"  await",[90,1728,1729],{"class":103}," redis.",[90,1731,1732],{"class":110},"setex",[90,1734,901],{"class":103},[90,1736,1737],{"class":610},"`refresh_token:${",[90,1739,1439],{"class":103},[90,1741,1742],{"class":610},"}`",[90,1744,284],{"class":103},[90,1746,1747],{"class":181},"7",[90,1749,1750],{"class":174}," *",[90,1752,1753],{"class":181}," 24",[90,1755,1750],{"class":174},[90,1757,1758],{"class":181}," 60",[90,1760,1750],{"class":174},[90,1762,1758],{"class":181},[90,1764,1765],{"class":103},", refreshToken)\n",[90,1767,1768],{"class":92,"line":246},[90,1769,1716],{"class":103},[90,1771,1772,1774],{"class":92,"line":260},[90,1773,1056],{"class":174},[90,1775,188],{"class":103},[90,1777,1778],{"class":92,"line":275},[90,1779,1780],{"class":103},"    accessToken,\n",[90,1782,1783],{"class":92,"line":290},[90,1784,1785],{"class":103},"    refreshToken,\n",[90,1787,1788,1791,1793,1796,1799,1802,1804,1806,1808,1811],{"class":92,"line":295},[90,1789,1790],{"class":103},"    accessExpiresAt: Date.",[90,1792,626],{"class":110},[90,1794,1795],{"class":103},"() ",[90,1797,1798],{"class":174},"+",[90,1800,1801],{"class":181}," 15",[90,1803,1750],{"class":174},[90,1805,1758],{"class":181},[90,1807,1750],{"class":174},[90,1809,1810],{"class":181}," 1000",[90,1812,204],{"class":103},[90,1814,1815,1818,1820,1822,1824,1827,1829,1831,1833,1835,1837,1839,1841,1843],{"class":92,"line":301},[90,1816,1817],{"class":103},"    refreshExpiresAt: Date.",[90,1819,626],{"class":110},[90,1821,1795],{"class":103},[90,1823,1798],{"class":174},[90,1825,1826],{"class":181}," 7",[90,1828,1750],{"class":174},[90,1830,1753],{"class":181},[90,1832,1750],{"class":174},[90,1834,1758],{"class":181},[90,1836,1750],{"class":174},[90,1838,1758],{"class":181},[90,1840,1750],{"class":174},[90,1842,1810],{"class":181},[90,1844,204],{"class":103},[90,1846,1847],{"class":92,"line":316},[90,1848,1016],{"class":103},[90,1850,1851],{"class":92,"line":331},[90,1852,159],{"class":103},[90,1854,1855],{"class":92,"line":336},[90,1856,210],{"emptyLinePlaceholder":209},[90,1858,1859],{"class":92,"line":342},[90,1860,1861],{"class":96},"// 刷新访问令牌\n",[90,1863,1864,1866,1868,1870,1873,1875,1878,1880,1883],{"class":92,"line":357},[90,1865,175],{"class":174},[90,1867,1660],{"class":174},[90,1869,1663],{"class":174},[90,1871,1872],{"class":110}," refreshAccessToken",[90,1874,901],{"class":103},[90,1876,1877],{"class":477},"refreshToken",[90,1879,481],{"class":174},[90,1881,1882],{"class":181}," string",[90,1884,657],{"class":103},[90,1886,1887],{"class":92,"line":371},[90,1888,1889],{"class":96},"  // 从 Redis 验证 refresh token\n",[90,1891,1892,1894,1897,1899,1901,1903,1906,1908,1911,1913,1915],{"class":92,"line":386},[90,1893,918],{"class":174},[90,1895,1896],{"class":181}," userId",[90,1898,185],{"class":174},[90,1900,568],{"class":174},[90,1902,1729],{"class":103},[90,1904,1905],{"class":110},"get",[90,1907,901],{"class":103},[90,1909,1910],{"class":610},"`refresh_token_user:${",[90,1912,1877],{"class":103},[90,1914,1742],{"class":610},[90,1916,676],{"class":103},[90,1918,1919],{"class":92,"line":391},[90,1920,1716],{"class":103},[90,1922,1923,1925,1927,1929],{"class":92,"line":397},[90,1924,940],{"class":174},[90,1926,537],{"class":103},[90,1928,945],{"class":174},[90,1930,1931],{"class":103},"userId) {\n",[90,1933,1934,1937,1939],{"class":92,"line":412},[90,1935,1936],{"class":174},"    return",[90,1938,772],{"class":181},[90,1940,1941],{"class":96}," // refresh token 无效或过期\n",[90,1943,1944],{"class":92,"line":426},[90,1945,1016],{"class":103},[90,1947,1948],{"class":92,"line":751},[90,1949,210],{"emptyLinePlaceholder":209},[90,1951,1952],{"class":92,"line":764},[90,1953,1954],{"class":96},"  // 生成新的 access token\n",[90,1956,1957,1959,1962,1964,1966,1968,1970],{"class":92,"line":777},[90,1958,918],{"class":174},[90,1960,1961],{"class":181}," newAccessToken",[90,1963,185],{"class":174},[90,1965,1688],{"class":110},[90,1967,1691],{"class":103},[90,1969,1694],{"class":610},[90,1971,676],{"class":103},[90,1973,1974],{"class":92,"line":787},[90,1975,1716],{"class":103},[90,1977,1978,1980],{"class":92,"line":793},[90,1979,1056],{"class":174},[90,1981,188],{"class":103},[90,1983,1984],{"class":92,"line":798},[90,1985,1986],{"class":103},"    accessToken: newAccessToken,\n",[90,1988,1989,1992,1994,1996,1998,2000,2002,2004,2006,2008],{"class":92,"line":803},[90,1990,1991],{"class":103},"    expiresAt: Date.",[90,1993,626],{"class":110},[90,1995,1795],{"class":103},[90,1997,1798],{"class":174},[90,1999,1801],{"class":181},[90,2001,1750],{"class":174},[90,2003,1758],{"class":181},[90,2005,1750],{"class":174},[90,2007,1810],{"class":181},[90,2009,204],{"class":103},[90,2011,2012],{"class":92,"line":809},[90,2013,1016],{"class":103},[90,2015,2016],{"class":92,"line":816},[90,2017,159],{"class":103},[11,2019,2020],{"id":2020},"前端实现",[22,2022,2024],{"id":2023},"_1-请求拦截器","1. 请求拦截器",[81,2026,2029],{"className":83,"code":2027,"filename":2028,"language":85,"meta":86,"style":86},"const $api = $fetch.create({\n  onRequest: async ({ options }) => {\n    const userStore = useUser()\n    \n    // 自动添加 Authorization 头\n    if (userStore.tokenInfo.value.accessToken) {\n      options.headers.set('Authorization', `Bearer ${userStore.tokenInfo.value.accessToken}`)\n    }\n  },\n\n  onResponse: async ({ request, response }) => {\n    const userStore = useUser()\n    const globalToast = useGlobalToast()\n\n    const apiResponse = response._data\n    \n    // 处理业务层面的错误\n    if (apiResponse?.code && apiResponse.code !== API_CODES.SUCCESS) {\n      const { code, message } = apiResponse\n\n      // 不可刷新的认证错误\n      if ([API_CODES.NO_TOKEN, API_CODES.TOKEN_INVALID, API_CODES.AUTH_FAILED].includes(code)) {\n        userStore.logout()\n        globalToast.add({ message: message || '认证失败，请重新登录', type: 'error' })\n        return\n      }\n\n      // Token 过期处理\n      if (code === API_CODES.TOKEN_EXPIRED) {\n        // 自动刷新 token 逻辑\n        if (userStore.tokenInfo.value.refreshToken && !userStore.isRefreshTokenExpired.value) {\n          const { refreshToken } = useAuth()\n          const success = await refreshToken()\n          \n          if (!success) {\n            userStore.logout()\n            globalToast.add({ message: '登录已过期，请重新登录', type: 'error' })\n          }\n        }\n        return\n      }\n\n      // 其他错误处理\n      globalToast.add({ message: message || '操作失败', type: 'error' })\n    }\n  },\n\n  onResponseError: async ({ response }) => {\n    // 只处理真正的网络错误\n    const globalToast = useGlobalToast()\n    const errorMessage = response?._data?.message || '网络请求失败'\n    globalToast.add({ message: errorMessage, type: 'error' })\n  },\n})\n","/app/plugins/fetch.ts",[77,2030,2031,2049,2072,2086,2091,2096,2103,2143,2147,2151,2155,2180,2192,2206,2210,2222,2226,2231,2255,2275,2279,2284,2322,2332,2357,2362,2366,2370,2375,2394,2399,2415,2433,2448,2453,2465,2474,2493,2498,2502,2507,2512,2517,2523,2544,2549,2554,2559,2579,2585,2598,2616,2631,2636],{"__ignoreMap":86},[90,2032,2033,2036,2039,2041,2044,2047],{"class":92,"line":93},[90,2034,2035],{"class":174},"const",[90,2037,2038],{"class":181}," $api",[90,2040,185],{"class":174},[90,2042,2043],{"class":103}," $fetch.",[90,2045,2046],{"class":110},"create",[90,2048,959],{"class":103},[90,2050,2051,2054,2057,2059,2062,2065,2068,2070],{"class":92,"line":100},[90,2052,2053],{"class":110},"  onRequest",[90,2055,2056],{"class":103},": ",[90,2058,534],{"class":174},[90,2060,2061],{"class":103}," ({ ",[90,2063,2064],{"class":477},"options",[90,2066,2067],{"class":103}," }) ",[90,2069,546],{"class":174},[90,2071,188],{"class":103},[90,2073,2074,2076,2079,2081,2084],{"class":92,"line":107},[90,2075,1428],{"class":174},[90,2077,2078],{"class":181}," userStore",[90,2080,185],{"class":174},[90,2082,2083],{"class":110}," useUser",[90,2085,1711],{"class":103},[90,2087,2088],{"class":92,"line":120},[90,2089,2090],{"class":103},"    \n",[90,2092,2093],{"class":92,"line":132},[90,2094,2095],{"class":96},"    // 自动添加 Authorization 头\n",[90,2097,2098,2100],{"class":92,"line":144},[90,2099,1335],{"class":174},[90,2101,2102],{"class":103}," (userStore.tokenInfo.value.accessToken) {\n",[90,2104,2105,2108,2111,2113,2116,2118,2121,2124,2126,2129,2131,2134,2136,2139,2141],{"class":92,"line":156},[90,2106,2107],{"class":103},"      options.headers.",[90,2109,2110],{"class":110},"set",[90,2112,901],{"class":103},[90,2114,2115],{"class":610},"'Authorization'",[90,2117,284],{"class":103},[90,2119,2120],{"class":610},"`Bearer ${",[90,2122,2123],{"class":103},"userStore",[90,2125,597],{"class":610},[90,2127,2128],{"class":103},"tokenInfo",[90,2130,597],{"class":610},[90,2132,2133],{"class":103},"value",[90,2135,597],{"class":610},[90,2137,2138],{"class":103},"accessToken",[90,2140,1742],{"class":610},[90,2142,676],{"class":103},[90,2144,2145],{"class":92,"line":246},[90,2146,639],{"class":103},[90,2148,2149],{"class":92,"line":260},[90,2150,1159],{"class":103},[90,2152,2153],{"class":92,"line":275},[90,2154,210],{"emptyLinePlaceholder":209},[90,2156,2157,2160,2162,2164,2166,2169,2171,2174,2176,2178],{"class":92,"line":290},[90,2158,2159],{"class":110},"  onResponse",[90,2161,2056],{"class":103},[90,2163,534],{"class":174},[90,2165,2061],{"class":103},[90,2167,2168],{"class":477},"request",[90,2170,284],{"class":103},[90,2172,2173],{"class":477},"response",[90,2175,2067],{"class":103},[90,2177,546],{"class":174},[90,2179,188],{"class":103},[90,2181,2182,2184,2186,2188,2190],{"class":92,"line":295},[90,2183,1428],{"class":174},[90,2185,2078],{"class":181},[90,2187,185],{"class":174},[90,2189,2083],{"class":110},[90,2191,1711],{"class":103},[90,2193,2194,2196,2199,2201,2204],{"class":92,"line":301},[90,2195,1428],{"class":174},[90,2197,2198],{"class":181}," globalToast",[90,2200,185],{"class":174},[90,2202,2203],{"class":110}," useGlobalToast",[90,2205,1711],{"class":103},[90,2207,2208],{"class":92,"line":316},[90,2209,210],{"emptyLinePlaceholder":209},[90,2211,2212,2214,2217,2219],{"class":92,"line":331},[90,2213,1428],{"class":174},[90,2215,2216],{"class":181}," apiResponse",[90,2218,185],{"class":174},[90,2220,2221],{"class":103}," response._data\n",[90,2223,2224],{"class":92,"line":336},[90,2225,2090],{"class":103},[90,2227,2228],{"class":92,"line":342},[90,2229,2230],{"class":96},"    // 处理业务层面的错误\n",[90,2232,2233,2235,2238,2241,2244,2247,2249,2251,2253],{"class":92,"line":357},[90,2234,1335],{"class":174},[90,2236,2237],{"class":103}," (apiResponse?.code ",[90,2239,2240],{"class":174},"&&",[90,2242,2243],{"class":103}," apiResponse.code ",[90,2245,2246],{"class":174},"!==",[90,2248,182],{"class":181},[90,2250,597],{"class":103},[90,2252,600],{"class":181},[90,2254,657],{"class":103},[90,2256,2257,2259,2261,2263,2265,2268,2270,2272],{"class":92,"line":371},[90,2258,560],{"class":174},[90,2260,1431],{"class":103},[90,2262,77],{"class":181},[90,2264,284],{"class":103},[90,2266,2267],{"class":181},"message",[90,2269,1446],{"class":103},[90,2271,1449],{"class":174},[90,2273,2274],{"class":103}," apiResponse\n",[90,2276,2277],{"class":92,"line":386},[90,2278,210],{"emptyLinePlaceholder":209},[90,2280,2281],{"class":92,"line":391},[90,2282,2283],{"class":96},"      // 不可刷新的认证错误\n",[90,2285,2286,2288,2291,2293,2295,2297,2299,2301,2303,2306,2308,2310,2312,2314,2317,2319],{"class":92,"line":397},[90,2287,686],{"class":174},[90,2289,2290],{"class":103}," ([",[90,2292,594],{"class":181},[90,2294,597],{"class":103},[90,2296,1374],{"class":181},[90,2298,284],{"class":103},[90,2300,594],{"class":181},[90,2302,597],{"class":103},[90,2304,2305],{"class":181},"TOKEN_INVALID",[90,2307,284],{"class":103},[90,2309,594],{"class":181},[90,2311,597],{"class":103},[90,2313,1203],{"class":181},[90,2315,2316],{"class":103},"].",[90,2318,1497],{"class":110},[90,2320,2321],{"class":103},"(code)) {\n",[90,2323,2324,2327,2330],{"class":92,"line":412},[90,2325,2326],{"class":103},"        userStore.",[90,2328,2329],{"class":110},"logout",[90,2331,1711],{"class":103},[90,2333,2334,2337,2340,2343,2345,2348,2351,2354],{"class":92,"line":426},[90,2335,2336],{"class":103},"        globalToast.",[90,2338,2339],{"class":110},"add",[90,2341,2342],{"class":103},"({ message: message ",[90,2344,717],{"class":174},[90,2346,2347],{"class":610}," '认证失败，请重新登录'",[90,2349,2350],{"class":103},", type: ",[90,2352,2353],{"class":610},"'error'",[90,2355,2356],{"class":103}," })\n",[90,2358,2359],{"class":92,"line":751},[90,2360,2361],{"class":174},"        return\n",[90,2363,2364],{"class":92,"line":764},[90,2365,634],{"class":103},[90,2367,2368],{"class":92,"line":777},[90,2369,210],{"emptyLinePlaceholder":209},[90,2371,2372],{"class":92,"line":787},[90,2373,2374],{"class":96},"      // Token 过期处理\n",[90,2376,2377,2379,2382,2385,2387,2389,2392],{"class":92,"line":793},[90,2378,686],{"class":174},[90,2380,2381],{"class":103}," (code ",[90,2383,2384],{"class":174},"===",[90,2386,182],{"class":181},[90,2388,597],{"class":103},[90,2390,2391],{"class":181},"TOKEN_EXPIRED",[90,2393,657],{"class":103},[90,2395,2396],{"class":92,"line":798},[90,2397,2398],{"class":96},"        // 自动刷新 token 逻辑\n",[90,2400,2401,2404,2407,2409,2412],{"class":92,"line":803},[90,2402,2403],{"class":174},"        if",[90,2405,2406],{"class":103}," (userStore.tokenInfo.value.refreshToken ",[90,2408,2240],{"class":174},[90,2410,2411],{"class":174}," !",[90,2413,2414],{"class":103},"userStore.isRefreshTokenExpired.value) {\n",[90,2416,2417,2420,2422,2424,2426,2428,2431],{"class":92,"line":809},[90,2418,2419],{"class":174},"          const",[90,2421,1431],{"class":103},[90,2423,1877],{"class":181},[90,2425,1446],{"class":103},[90,2427,1449],{"class":174},[90,2429,2430],{"class":110}," useAuth",[90,2432,1711],{"class":103},[90,2434,2435,2437,2440,2442,2444,2446],{"class":92,"line":816},[90,2436,2419],{"class":174},[90,2438,2439],{"class":181}," success",[90,2441,185],{"class":174},[90,2443,568],{"class":174},[90,2445,1703],{"class":110},[90,2447,1711],{"class":103},[90,2449,2450],{"class":92,"line":829},[90,2451,2452],{"class":103},"          \n",[90,2454,2455,2458,2460,2462],{"class":92,"line":839},[90,2456,2457],{"class":174},"          if",[90,2459,537],{"class":103},[90,2461,945],{"class":174},[90,2463,2464],{"class":103},"success) {\n",[90,2466,2467,2470,2472],{"class":92,"line":850},[90,2468,2469],{"class":103},"            userStore.",[90,2471,2329],{"class":110},[90,2473,1711],{"class":103},[90,2475,2476,2479,2481,2484,2487,2489,2491],{"class":92,"line":859},[90,2477,2478],{"class":103},"            globalToast.",[90,2480,2339],{"class":110},[90,2482,2483],{"class":103},"({ message: ",[90,2485,2486],{"class":610},"'登录已过期，请重新登录'",[90,2488,2350],{"class":103},[90,2490,2353],{"class":610},[90,2492,2356],{"class":103},[90,2494,2495],{"class":92,"line":864},[90,2496,2497],{"class":103},"          }\n",[90,2499,2500],{"class":92,"line":869},[90,2501,790],{"class":103},[90,2503,2505],{"class":92,"line":2504},40,[90,2506,2361],{"class":174},[90,2508,2510],{"class":92,"line":2509},41,[90,2511,634],{"class":103},[90,2513,2515],{"class":92,"line":2514},42,[90,2516,210],{"emptyLinePlaceholder":209},[90,2518,2520],{"class":92,"line":2519},43,[90,2521,2522],{"class":96},"      // 其他错误处理\n",[90,2524,2526,2529,2531,2533,2535,2538,2540,2542],{"class":92,"line":2525},44,[90,2527,2528],{"class":103},"      globalToast.",[90,2530,2339],{"class":110},[90,2532,2342],{"class":103},[90,2534,717],{"class":174},[90,2536,2537],{"class":610}," '操作失败'",[90,2539,2350],{"class":103},[90,2541,2353],{"class":610},[90,2543,2356],{"class":103},[90,2545,2547],{"class":92,"line":2546},45,[90,2548,639],{"class":103},[90,2550,2552],{"class":92,"line":2551},46,[90,2553,1159],{"class":103},[90,2555,2557],{"class":92,"line":2556},47,[90,2558,210],{"emptyLinePlaceholder":209},[90,2560,2562,2565,2567,2569,2571,2573,2575,2577],{"class":92,"line":2561},48,[90,2563,2564],{"class":110},"  onResponseError",[90,2566,2056],{"class":103},[90,2568,534],{"class":174},[90,2570,2061],{"class":103},[90,2572,2173],{"class":477},[90,2574,2067],{"class":103},[90,2576,546],{"class":174},[90,2578,188],{"class":103},[90,2580,2582],{"class":92,"line":2581},49,[90,2583,2584],{"class":96},"    // 只处理真正的网络错误\n",[90,2586,2588,2590,2592,2594,2596],{"class":92,"line":2587},50,[90,2589,1428],{"class":174},[90,2591,2198],{"class":181},[90,2593,185],{"class":174},[90,2595,2203],{"class":110},[90,2597,1711],{"class":103},[90,2599,2601,2603,2606,2608,2611,2613],{"class":92,"line":2600},51,[90,2602,1428],{"class":174},[90,2604,2605],{"class":181}," errorMessage",[90,2607,185],{"class":174},[90,2609,2610],{"class":103}," response?._data?.message ",[90,2612,717],{"class":174},[90,2614,2615],{"class":610}," '网络请求失败'\n",[90,2617,2619,2622,2624,2627,2629],{"class":92,"line":2618},52,[90,2620,2621],{"class":103},"    globalToast.",[90,2623,2339],{"class":110},[90,2625,2626],{"class":103},"({ message: errorMessage, type: ",[90,2628,2353],{"class":610},[90,2630,2356],{"class":103},[90,2632,2634],{"class":92,"line":2633},53,[90,2635,1159],{"class":103},[90,2637,2639],{"class":92,"line":2638},54,[90,2640,1092],{"class":103},[22,2642,2644],{"id":2643},"_2-用户状态管理","2. 用户状态管理",[81,2646,2649],{"className":83,"code":2647,"filename":2648,"language":85,"meta":86,"style":86},"export const useUser = () => {\n  const tokenInfo = ref({\n    accessToken: '',\n    refreshToken: '',\n    accessExpiresAt: 0,\n    refreshExpiresAt: 0,\n  })\n\n  // 检查 access token 是否过期\n  const isAccessTokenExpired = computed(() => {\n    return Date.now() >= tokenInfo.value.accessExpiresAt\n  })\n\n  // 检查 refresh token 是否过期\n  const isRefreshTokenExpired = computed(() => {\n    return Date.now() >= tokenInfo.value.refreshExpiresAt\n  })\n\n  // 登录\n  const login = async (credentials: LoginData) => {\n    try {\n      const response = await $fetch.post('/api/v1/user/login', credentials)\n      \n      if (response.code === API_CODES.SUCCESS) {\n        tokenInfo.value = {\n          accessToken: response.data.accessToken,\n          refreshToken: response.data.refreshToken,\n          accessExpiresAt: response.data.accessExpiresAt,\n          refreshExpiresAt: response.data.refreshExpiresAt,\n        }\n        \n        // 保存到 localStorage\n        localStorage.setItem('tokenInfo', JSON.stringify(tokenInfo.value))\n        return true\n      }\n      \n      return false\n    } catch (error) {\n      console.error('登录失败:', error)\n      return false\n    }\n  }\n\n  // 登出\n  const logout = () => {\n    tokenInfo.value = {\n      accessToken: '',\n      refreshToken: '',\n      accessExpiresAt: 0,\n      refreshExpiresAt: 0,\n    }\n    localStorage.removeItem('tokenInfo')\n    navigateTo('/login')\n  }\n\n  return {\n    tokenInfo: readonly(tokenInfo),\n    isAccessTokenExpired,\n    isRefreshTokenExpired,\n    login,\n    logout,\n  }\n}\n","/composables/useUser.ts",[77,2650,2651,2668,2682,2692,2701,2710,2719,2723,2727,2732,2751,2768,2772,2776,2781,2798,2813,2817,2821,2826,2853,2859,2882,2886,2903,2912,2917,2922,2927,2932,2936,2940,2945,2971,2978,2982,2986,2993,3004,3019,3025,3029,3033,3037,3042,3057,3066,3075,3084,3093,3102,3106,3120,3132,3136,3141,3148,3160,3166,3172,3178,3184,3189],{"__ignoreMap":86},[90,2652,2653,2655,2657,2659,2661,2664,2666],{"class":92,"line":93},[90,2654,175],{"class":174},[90,2656,178],{"class":174},[90,2658,2083],{"class":110},[90,2660,185],{"class":174},[90,2662,2663],{"class":103}," () ",[90,2665,546],{"class":174},[90,2667,188],{"class":103},[90,2669,2670,2672,2675,2677,2680],{"class":92,"line":100},[90,2671,918],{"class":174},[90,2673,2674],{"class":181}," tokenInfo",[90,2676,185],{"class":174},[90,2678,2679],{"class":110}," ref",[90,2681,959],{"class":103},[90,2683,2684,2687,2690],{"class":92,"line":107},[90,2685,2686],{"class":103},"    accessToken: ",[90,2688,2689],{"class":610},"''",[90,2691,204],{"class":103},[90,2693,2694,2697,2699],{"class":92,"line":120},[90,2695,2696],{"class":103},"    refreshToken: ",[90,2698,2689],{"class":610},[90,2700,204],{"class":103},[90,2702,2703,2706,2708],{"class":92,"line":132},[90,2704,2705],{"class":103},"    accessExpiresAt: ",[90,2707,201],{"class":181},[90,2709,204],{"class":103},[90,2711,2712,2715,2717],{"class":92,"line":144},[90,2713,2714],{"class":103},"    refreshExpiresAt: ",[90,2716,201],{"class":181},[90,2718,204],{"class":103},[90,2720,2721],{"class":92,"line":156},[90,2722,872],{"class":103},[90,2724,2725],{"class":92,"line":246},[90,2726,210],{"emptyLinePlaceholder":209},[90,2728,2729],{"class":92,"line":260},[90,2730,2731],{"class":96},"  // 检查 access token 是否过期\n",[90,2733,2734,2736,2739,2741,2744,2747,2749],{"class":92,"line":275},[90,2735,918],{"class":174},[90,2737,2738],{"class":181}," isAccessTokenExpired",[90,2740,185],{"class":174},[90,2742,2743],{"class":110}," computed",[90,2745,2746],{"class":103},"(() ",[90,2748,546],{"class":174},[90,2750,188],{"class":103},[90,2752,2753,2755,2758,2760,2762,2765],{"class":92,"line":290},[90,2754,1936],{"class":174},[90,2756,2757],{"class":103}," Date.",[90,2759,626],{"class":110},[90,2761,1795],{"class":103},[90,2763,2764],{"class":174},">=",[90,2766,2767],{"class":103}," tokenInfo.value.accessExpiresAt\n",[90,2769,2770],{"class":92,"line":295},[90,2771,872],{"class":103},[90,2773,2774],{"class":92,"line":301},[90,2775,210],{"emptyLinePlaceholder":209},[90,2777,2778],{"class":92,"line":316},[90,2779,2780],{"class":96},"  // 检查 refresh token 是否过期\n",[90,2782,2783,2785,2788,2790,2792,2794,2796],{"class":92,"line":331},[90,2784,918],{"class":174},[90,2786,2787],{"class":181}," isRefreshTokenExpired",[90,2789,185],{"class":174},[90,2791,2743],{"class":110},[90,2793,2746],{"class":103},[90,2795,546],{"class":174},[90,2797,188],{"class":103},[90,2799,2800,2802,2804,2806,2808,2810],{"class":92,"line":336},[90,2801,1936],{"class":174},[90,2803,2757],{"class":103},[90,2805,626],{"class":110},[90,2807,1795],{"class":103},[90,2809,2764],{"class":174},[90,2811,2812],{"class":103}," tokenInfo.value.refreshExpiresAt\n",[90,2814,2815],{"class":92,"line":342},[90,2816,872],{"class":103},[90,2818,2819],{"class":92,"line":357},[90,2820,210],{"emptyLinePlaceholder":209},[90,2822,2823],{"class":92,"line":371},[90,2824,2825],{"class":96},"  // 登录\n",[90,2827,2828,2830,2833,2835,2837,2839,2842,2844,2847,2849,2851],{"class":92,"line":386},[90,2829,918],{"class":174},[90,2831,2832],{"class":110}," login",[90,2834,185],{"class":174},[90,2836,1660],{"class":174},[90,2838,537],{"class":103},[90,2840,2841],{"class":477},"credentials",[90,2843,481],{"class":174},[90,2845,2846],{"class":110}," LoginData",[90,2848,543],{"class":103},[90,2850,546],{"class":174},[90,2852,188],{"class":103},[90,2854,2855,2857],{"class":92,"line":391},[90,2856,553],{"class":174},[90,2858,188],{"class":103},[90,2860,2861,2863,2865,2867,2869,2871,2874,2876,2879],{"class":92,"line":397},[90,2862,560],{"class":174},[90,2864,563],{"class":181},[90,2866,185],{"class":174},[90,2868,568],{"class":174},[90,2870,2043],{"class":103},[90,2872,2873],{"class":110},"post",[90,2875,901],{"class":103},[90,2877,2878],{"class":610},"'/api/v1/user/login'",[90,2880,2881],{"class":103},", credentials)\n",[90,2883,2884],{"class":92,"line":412},[90,2885,681],{"class":103},[90,2887,2888,2890,2893,2895,2897,2899,2901],{"class":92,"line":426},[90,2889,686],{"class":174},[90,2891,2892],{"class":103}," (response.code ",[90,2894,2384],{"class":174},[90,2896,182],{"class":181},[90,2898,597],{"class":103},[90,2900,600],{"class":181},[90,2902,657],{"class":103},[90,2904,2905,2908,2910],{"class":92,"line":751},[90,2906,2907],{"class":103},"        tokenInfo.value ",[90,2909,1449],{"class":174},[90,2911,188],{"class":103},[90,2913,2914],{"class":92,"line":764},[90,2915,2916],{"class":103},"          accessToken: response.data.accessToken,\n",[90,2918,2919],{"class":92,"line":777},[90,2920,2921],{"class":103},"          refreshToken: response.data.refreshToken,\n",[90,2923,2924],{"class":92,"line":787},[90,2925,2926],{"class":103},"          accessExpiresAt: response.data.accessExpiresAt,\n",[90,2928,2929],{"class":92,"line":793},[90,2930,2931],{"class":103},"          refreshExpiresAt: response.data.refreshExpiresAt,\n",[90,2933,2934],{"class":92,"line":798},[90,2935,790],{"class":103},[90,2937,2938],{"class":92,"line":803},[90,2939,725],{"class":103},[90,2941,2942],{"class":92,"line":809},[90,2943,2944],{"class":96},"        // 保存到 localStorage\n",[90,2946,2947,2950,2953,2955,2958,2960,2963,2965,2968],{"class":92,"line":816},[90,2948,2949],{"class":103},"        localStorage.",[90,2951,2952],{"class":110},"setItem",[90,2954,901],{"class":103},[90,2956,2957],{"class":610},"'tokenInfo'",[90,2959,284],{"class":103},[90,2961,2962],{"class":181},"JSON",[90,2964,597],{"class":103},[90,2966,2967],{"class":110},"stringify",[90,2969,2970],{"class":103},"(tokenInfo.value))\n",[90,2972,2973,2975],{"class":92,"line":829},[90,2974,730],{"class":174},[90,2976,2977],{"class":181}," true\n",[90,2979,2980],{"class":92,"line":839},[90,2981,634],{"class":103},[90,2983,2984],{"class":92,"line":850},[90,2985,681],{"class":103},[90,2987,2988,2990],{"class":92,"line":859},[90,2989,584],{"class":174},[90,2991,2992],{"class":181}," false\n",[90,2994,2995,2998,3001],{"class":92,"line":864},[90,2996,2997],{"class":103},"    } ",[90,2999,3000],{"class":174},"catch",[90,3002,3003],{"class":103}," (error) {\n",[90,3005,3006,3009,3011,3013,3016],{"class":92,"line":869},[90,3007,3008],{"class":103},"      console.",[90,3010,649],{"class":110},[90,3012,901],{"class":103},[90,3014,3015],{"class":610},"'登录失败:'",[90,3017,3018],{"class":103},", error)\n",[90,3020,3021,3023],{"class":92,"line":2504},[90,3022,584],{"class":174},[90,3024,2992],{"class":181},[90,3026,3027],{"class":92,"line":2509},[90,3028,639],{"class":103},[90,3030,3031],{"class":92,"line":2514},[90,3032,1016],{"class":103},[90,3034,3035],{"class":92,"line":2519},[90,3036,210],{"emptyLinePlaceholder":209},[90,3038,3039],{"class":92,"line":2525},[90,3040,3041],{"class":96},"  // 登出\n",[90,3043,3044,3046,3049,3051,3053,3055],{"class":92,"line":2546},[90,3045,918],{"class":174},[90,3047,3048],{"class":110}," logout",[90,3050,185],{"class":174},[90,3052,2663],{"class":103},[90,3054,546],{"class":174},[90,3056,188],{"class":103},[90,3058,3059,3062,3064],{"class":92,"line":2551},[90,3060,3061],{"class":103},"    tokenInfo.value ",[90,3063,1449],{"class":174},[90,3065,188],{"class":103},[90,3067,3068,3071,3073],{"class":92,"line":2556},[90,3069,3070],{"class":103},"      accessToken: ",[90,3072,2689],{"class":610},[90,3074,204],{"class":103},[90,3076,3077,3080,3082],{"class":92,"line":2561},[90,3078,3079],{"class":103},"      refreshToken: ",[90,3081,2689],{"class":610},[90,3083,204],{"class":103},[90,3085,3086,3089,3091],{"class":92,"line":2581},[90,3087,3088],{"class":103},"      accessExpiresAt: ",[90,3090,201],{"class":181},[90,3092,204],{"class":103},[90,3094,3095,3098,3100],{"class":92,"line":2587},[90,3096,3097],{"class":103},"      refreshExpiresAt: ",[90,3099,201],{"class":181},[90,3101,204],{"class":103},[90,3103,3104],{"class":92,"line":2600},[90,3105,639],{"class":103},[90,3107,3108,3111,3114,3116,3118],{"class":92,"line":2618},[90,3109,3110],{"class":103},"    localStorage.",[90,3112,3113],{"class":110},"removeItem",[90,3115,901],{"class":103},[90,3117,2957],{"class":610},[90,3119,676],{"class":103},[90,3121,3122,3125,3127,3130],{"class":92,"line":2633},[90,3123,3124],{"class":110},"    navigateTo",[90,3126,901],{"class":103},[90,3128,3129],{"class":610},"'/login'",[90,3131,676],{"class":103},[90,3133,3134],{"class":92,"line":2638},[90,3135,1016],{"class":103},[90,3137,3139],{"class":92,"line":3138},55,[90,3140,210],{"emptyLinePlaceholder":209},[90,3142,3144,3146],{"class":92,"line":3143},56,[90,3145,1056],{"class":174},[90,3147,188],{"class":103},[90,3149,3151,3154,3157],{"class":92,"line":3150},57,[90,3152,3153],{"class":103},"    tokenInfo: ",[90,3155,3156],{"class":110},"readonly",[90,3158,3159],{"class":103},"(tokenInfo),\n",[90,3161,3163],{"class":92,"line":3162},58,[90,3164,3165],{"class":103},"    isAccessTokenExpired,\n",[90,3167,3169],{"class":92,"line":3168},59,[90,3170,3171],{"class":103},"    isRefreshTokenExpired,\n",[90,3173,3175],{"class":92,"line":3174},60,[90,3176,3177],{"class":103},"    login,\n",[90,3179,3181],{"class":92,"line":3180},61,[90,3182,3183],{"class":103},"    logout,\n",[90,3185,3187],{"class":92,"line":3186},62,[90,3188,1016],{"class":103},[90,3190,3192],{"class":92,"line":3191},63,[90,3193,159],{"class":103},[22,3195,3197],{"id":3196},"_3-鉴权组合式函数","3. 鉴权组合式函数",[81,3199,3202],{"className":83,"code":3200,"filename":3201,"language":85,"meta":86,"style":86},"export const useAuth = () => {\n  const userStore = useUser()\n\n  // 刷新 token\n  const refreshToken = async (): Promise\u003Cboolean> => {\n    try {\n      const response = await $fetch.post('/api/v1/auth/refresh', {\n        refreshToken: userStore.tokenInfo.value.refreshToken,\n      })\n\n      if (response.code === API_CODES.SUCCESS) {\n        // 更新 access token\n        userStore.tokenInfo.value.accessToken = response.data.accessToken\n        userStore.tokenInfo.value.accessExpiresAt = response.data.expiresAt\n        \n        // 更新 localStorage\n        localStorage.setItem('tokenInfo', JSON.stringify(userStore.tokenInfo.value))\n        return true\n      }\n\n      return false\n    } catch (error) {\n      console.error('Token 刷新失败:', error)\n      return false\n    }\n  }\n\n  // 检查登录状态\n  const checkAuth = () => {\n    if (!userStore.tokenInfo.value.accessToken) {\n      return false\n    }\n\n    if (userStore.isRefreshTokenExpired.value) {\n      userStore.logout()\n      return false\n    }\n\n    return true\n  }\n\n  return {\n    refreshToken,\n    checkAuth,\n  }\n}\n","/composables/useAuth.ts",[77,3203,3204,3220,3232,3236,3241,3270,3276,3298,3303,3308,3312,3328,3333,3343,3353,3357,3362,3383,3389,3393,3397,3403,3411,3424,3430,3434,3438,3442,3447,3462,3473,3479,3483,3487,3494,3503,3509,3513,3517,3523,3527,3531,3537,3541,3546,3550],{"__ignoreMap":86},[90,3205,3206,3208,3210,3212,3214,3216,3218],{"class":92,"line":93},[90,3207,175],{"class":174},[90,3209,178],{"class":174},[90,3211,2430],{"class":110},[90,3213,185],{"class":174},[90,3215,2663],{"class":103},[90,3217,546],{"class":174},[90,3219,188],{"class":103},[90,3221,3222,3224,3226,3228,3230],{"class":92,"line":100},[90,3223,918],{"class":174},[90,3225,2078],{"class":181},[90,3227,185],{"class":174},[90,3229,2083],{"class":110},[90,3231,1711],{"class":103},[90,3233,3234],{"class":92,"line":107},[90,3235,210],{"emptyLinePlaceholder":209},[90,3237,3238],{"class":92,"line":120},[90,3239,3240],{"class":96},"  // 刷新 token\n",[90,3242,3243,3245,3247,3249,3251,3254,3256,3259,3261,3264,3266,3268],{"class":92,"line":132},[90,3244,918],{"class":174},[90,3246,1703],{"class":110},[90,3248,185],{"class":174},[90,3250,1660],{"class":174},[90,3252,3253],{"class":103}," ()",[90,3255,481],{"class":174},[90,3257,3258],{"class":110}," Promise",[90,3260,487],{"class":103},[90,3262,3263],{"class":181},"boolean",[90,3265,516],{"class":103},[90,3267,546],{"class":174},[90,3269,188],{"class":103},[90,3271,3272,3274],{"class":92,"line":144},[90,3273,553],{"class":174},[90,3275,188],{"class":103},[90,3277,3278,3280,3282,3284,3286,3288,3290,3292,3295],{"class":92,"line":156},[90,3279,560],{"class":174},[90,3281,563],{"class":181},[90,3283,185],{"class":174},[90,3285,568],{"class":174},[90,3287,2043],{"class":103},[90,3289,2873],{"class":110},[90,3291,901],{"class":103},[90,3293,3294],{"class":610},"'/api/v1/auth/refresh'",[90,3296,3297],{"class":103},", {\n",[90,3299,3300],{"class":92,"line":246},[90,3301,3302],{"class":103},"        refreshToken: userStore.tokenInfo.value.refreshToken,\n",[90,3304,3305],{"class":92,"line":260},[90,3306,3307],{"class":103},"      })\n",[90,3309,3310],{"class":92,"line":275},[90,3311,210],{"emptyLinePlaceholder":209},[90,3313,3314,3316,3318,3320,3322,3324,3326],{"class":92,"line":290},[90,3315,686],{"class":174},[90,3317,2892],{"class":103},[90,3319,2384],{"class":174},[90,3321,182],{"class":181},[90,3323,597],{"class":103},[90,3325,600],{"class":181},[90,3327,657],{"class":103},[90,3329,3330],{"class":92,"line":295},[90,3331,3332],{"class":96},"        // 更新 access token\n",[90,3334,3335,3338,3340],{"class":92,"line":301},[90,3336,3337],{"class":103},"        userStore.tokenInfo.value.accessToken ",[90,3339,1449],{"class":174},[90,3341,3342],{"class":103}," response.data.accessToken\n",[90,3344,3345,3348,3350],{"class":92,"line":316},[90,3346,3347],{"class":103},"        userStore.tokenInfo.value.accessExpiresAt ",[90,3349,1449],{"class":174},[90,3351,3352],{"class":103}," response.data.expiresAt\n",[90,3354,3355],{"class":92,"line":331},[90,3356,725],{"class":103},[90,3358,3359],{"class":92,"line":336},[90,3360,3361],{"class":96},"        // 更新 localStorage\n",[90,3363,3364,3366,3368,3370,3372,3374,3376,3378,3380],{"class":92,"line":342},[90,3365,2949],{"class":103},[90,3367,2952],{"class":110},[90,3369,901],{"class":103},[90,3371,2957],{"class":610},[90,3373,284],{"class":103},[90,3375,2962],{"class":181},[90,3377,597],{"class":103},[90,3379,2967],{"class":110},[90,3381,3382],{"class":103},"(userStore.tokenInfo.value))\n",[90,3384,3385,3387],{"class":92,"line":357},[90,3386,730],{"class":174},[90,3388,2977],{"class":181},[90,3390,3391],{"class":92,"line":371},[90,3392,634],{"class":103},[90,3394,3395],{"class":92,"line":386},[90,3396,210],{"emptyLinePlaceholder":209},[90,3398,3399,3401],{"class":92,"line":391},[90,3400,584],{"class":174},[90,3402,2992],{"class":181},[90,3404,3405,3407,3409],{"class":92,"line":397},[90,3406,2997],{"class":103},[90,3408,3000],{"class":174},[90,3410,3003],{"class":103},[90,3412,3413,3415,3417,3419,3422],{"class":92,"line":412},[90,3414,3008],{"class":103},[90,3416,649],{"class":110},[90,3418,901],{"class":103},[90,3420,3421],{"class":610},"'Token 刷新失败:'",[90,3423,3018],{"class":103},[90,3425,3426,3428],{"class":92,"line":426},[90,3427,584],{"class":174},[90,3429,2992],{"class":181},[90,3431,3432],{"class":92,"line":751},[90,3433,639],{"class":103},[90,3435,3436],{"class":92,"line":764},[90,3437,1016],{"class":103},[90,3439,3440],{"class":92,"line":777},[90,3441,210],{"emptyLinePlaceholder":209},[90,3443,3444],{"class":92,"line":787},[90,3445,3446],{"class":96},"  // 检查登录状态\n",[90,3448,3449,3451,3454,3456,3458,3460],{"class":92,"line":793},[90,3450,918],{"class":174},[90,3452,3453],{"class":110}," checkAuth",[90,3455,185],{"class":174},[90,3457,2663],{"class":103},[90,3459,546],{"class":174},[90,3461,188],{"class":103},[90,3463,3464,3466,3468,3470],{"class":92,"line":798},[90,3465,1335],{"class":174},[90,3467,537],{"class":103},[90,3469,945],{"class":174},[90,3471,3472],{"class":103},"userStore.tokenInfo.value.accessToken) {\n",[90,3474,3475,3477],{"class":92,"line":803},[90,3476,584],{"class":174},[90,3478,2992],{"class":181},[90,3480,3481],{"class":92,"line":809},[90,3482,639],{"class":103},[90,3484,3485],{"class":92,"line":816},[90,3486,210],{"emptyLinePlaceholder":209},[90,3488,3489,3491],{"class":92,"line":829},[90,3490,1335],{"class":174},[90,3492,3493],{"class":103}," (userStore.isRefreshTokenExpired.value) {\n",[90,3495,3496,3499,3501],{"class":92,"line":839},[90,3497,3498],{"class":103},"      userStore.",[90,3500,2329],{"class":110},[90,3502,1711],{"class":103},[90,3504,3505,3507],{"class":92,"line":850},[90,3506,584],{"class":174},[90,3508,2992],{"class":181},[90,3510,3511],{"class":92,"line":859},[90,3512,639],{"class":103},[90,3514,3515],{"class":92,"line":864},[90,3516,210],{"emptyLinePlaceholder":209},[90,3518,3519,3521],{"class":92,"line":869},[90,3520,1936],{"class":174},[90,3522,2977],{"class":181},[90,3524,3525],{"class":92,"line":2504},[90,3526,1016],{"class":103},[90,3528,3529],{"class":92,"line":2509},[90,3530,210],{"emptyLinePlaceholder":209},[90,3532,3533,3535],{"class":92,"line":2514},[90,3534,1056],{"class":174},[90,3536,188],{"class":103},[90,3538,3539],{"class":92,"line":2519},[90,3540,1785],{"class":103},[90,3542,3543],{"class":92,"line":2525},[90,3544,3545],{"class":103},"    checkAuth,\n",[90,3547,3548],{"class":92,"line":2546},[90,3549,1016],{"class":103},[90,3551,3552],{"class":92,"line":2551},[90,3553,159],{"class":103},[11,3555,3556],{"id":3556},"前端使用方式",[22,3558,3560],{"id":3559},"_1-api-调用","1. API 调用",[81,3562,3564],{"className":83,"code":3563,"language":85,"meta":86,"style":86},"// 在组件中使用\nexport default defineNuxtPlugin({\n  async setup() {\n    const { $api } = useNuxtApp()\n\n    // GET 请求\n    const userData = await $api.get('/api/v1/user/profile')\n    if (userData.code === API_CODES.SUCCESS) {\n      console.log('用户信息:', userData.data)\n    }\n\n    // POST 请求\n    const result = await $api.post('/api/v1/posts', {\n      title: '新文章',\n      content: '文章内容...'\n    })\n\n    if (result.code === API_CODES.SUCCESS) {\n      console.log('创建成功:', result.data)\n    } else {\n      console.error('创建失败:', result.message)\n    }\n  }\n})\n",[77,3565,3566,3571,3582,3593,3611,3615,3620,3643,3660,3675,3679,3683,3688,3710,3720,3728,3732,3736,3753,3767,3775,3789,3793,3797],{"__ignoreMap":86},[90,3567,3568],{"class":92,"line":93},[90,3569,3570],{"class":96},"// 在组件中使用\n",[90,3572,3573,3575,3577,3580],{"class":92,"line":100},[90,3574,175],{"class":174},[90,3576,896],{"class":174},[90,3578,3579],{"class":110}," defineNuxtPlugin",[90,3581,959],{"class":103},[90,3583,3584,3587,3590],{"class":92,"line":107},[90,3585,3586],{"class":174},"  async",[90,3588,3589],{"class":110}," setup",[90,3591,3592],{"class":103},"() {\n",[90,3594,3595,3597,3599,3602,3604,3606,3609],{"class":92,"line":120},[90,3596,1428],{"class":174},[90,3598,1431],{"class":103},[90,3600,3601],{"class":181},"$api",[90,3603,1446],{"class":103},[90,3605,1449],{"class":174},[90,3607,3608],{"class":110}," useNuxtApp",[90,3610,1711],{"class":103},[90,3612,3613],{"class":92,"line":132},[90,3614,210],{"emptyLinePlaceholder":209},[90,3616,3617],{"class":92,"line":144},[90,3618,3619],{"class":96},"    // GET 请求\n",[90,3621,3622,3624,3627,3629,3631,3634,3636,3638,3641],{"class":92,"line":156},[90,3623,1428],{"class":174},[90,3625,3626],{"class":181}," userData",[90,3628,185],{"class":174},[90,3630,568],{"class":174},[90,3632,3633],{"class":103}," $api.",[90,3635,1905],{"class":110},[90,3637,901],{"class":103},[90,3639,3640],{"class":610},"'/api/v1/user/profile'",[90,3642,676],{"class":103},[90,3644,3645,3647,3650,3652,3654,3656,3658],{"class":92,"line":246},[90,3646,1335],{"class":174},[90,3648,3649],{"class":103}," (userData.code ",[90,3651,2384],{"class":174},[90,3653,182],{"class":181},[90,3655,597],{"class":103},[90,3657,600],{"class":181},[90,3659,657],{"class":103},[90,3661,3662,3664,3667,3669,3672],{"class":92,"line":260},[90,3663,3008],{"class":103},[90,3665,3666],{"class":110},"log",[90,3668,901],{"class":103},[90,3670,3671],{"class":610},"'用户信息:'",[90,3673,3674],{"class":103},", userData.data)\n",[90,3676,3677],{"class":92,"line":275},[90,3678,639],{"class":103},[90,3680,3681],{"class":92,"line":290},[90,3682,210],{"emptyLinePlaceholder":209},[90,3684,3685],{"class":92,"line":295},[90,3686,3687],{"class":96},"    // POST 请求\n",[90,3689,3690,3692,3695,3697,3699,3701,3703,3705,3708],{"class":92,"line":301},[90,3691,1428],{"class":174},[90,3693,3694],{"class":181}," result",[90,3696,185],{"class":174},[90,3698,568],{"class":174},[90,3700,3633],{"class":103},[90,3702,2873],{"class":110},[90,3704,901],{"class":103},[90,3706,3707],{"class":610},"'/api/v1/posts'",[90,3709,3297],{"class":103},[90,3711,3712,3715,3718],{"class":92,"line":316},[90,3713,3714],{"class":103},"      title: ",[90,3716,3717],{"class":610},"'新文章'",[90,3719,204],{"class":103},[90,3721,3722,3725],{"class":92,"line":331},[90,3723,3724],{"class":103},"      content: ",[90,3726,3727],{"class":610},"'文章内容...'\n",[90,3729,3730],{"class":92,"line":336},[90,3731,1011],{"class":103},[90,3733,3734],{"class":92,"line":342},[90,3735,210],{"emptyLinePlaceholder":209},[90,3737,3738,3740,3743,3745,3747,3749,3751],{"class":92,"line":357},[90,3739,1335],{"class":174},[90,3741,3742],{"class":103}," (result.code ",[90,3744,2384],{"class":174},[90,3746,182],{"class":181},[90,3748,597],{"class":103},[90,3750,600],{"class":181},[90,3752,657],{"class":103},[90,3754,3755,3757,3759,3761,3764],{"class":92,"line":371},[90,3756,3008],{"class":103},[90,3758,3666],{"class":110},[90,3760,901],{"class":103},[90,3762,3763],{"class":610},"'创建成功:'",[90,3765,3766],{"class":103},", result.data)\n",[90,3768,3769,3771,3773],{"class":92,"line":386},[90,3770,2997],{"class":103},[90,3772,1527],{"class":174},[90,3774,188],{"class":103},[90,3776,3777,3779,3781,3783,3786],{"class":92,"line":391},[90,3778,3008],{"class":103},[90,3780,649],{"class":110},[90,3782,901],{"class":103},[90,3784,3785],{"class":610},"'创建失败:'",[90,3787,3788],{"class":103},", result.message)\n",[90,3790,3791],{"class":92,"line":397},[90,3792,639],{"class":103},[90,3794,3795],{"class":92,"line":412},[90,3796,1016],{"class":103},[90,3798,3799],{"class":92,"line":426},[90,3800,1092],{"class":103},[22,3802,3804],{"id":3803},"_2-路由守卫","2. 路由守卫",[81,3806,3808],{"className":83,"code":3807,"language":85,"meta":86,"style":86},"// /middleware/auth.ts\nexport default defineNuxtRouteMiddleware((to) => {\n  const { checkAuth } = useAuth()\n  \n  if (!checkAuth()) {\n    return navigateTo('/login')\n  }\n})\n",[77,3809,3810,3815,3836,3853,3857,3870,3883,3887],{"__ignoreMap":86},[90,3811,3812],{"class":92,"line":93},[90,3813,3814],{"class":96},"// /middleware/auth.ts\n",[90,3816,3817,3819,3821,3824,3827,3830,3832,3834],{"class":92,"line":100},[90,3818,175],{"class":174},[90,3820,896],{"class":174},[90,3822,3823],{"class":110}," defineNuxtRouteMiddleware",[90,3825,3826],{"class":103},"((",[90,3828,3829],{"class":477},"to",[90,3831,543],{"class":103},[90,3833,546],{"class":174},[90,3835,188],{"class":103},[90,3837,3838,3840,3842,3845,3847,3849,3851],{"class":92,"line":107},[90,3839,918],{"class":174},[90,3841,1431],{"class":103},[90,3843,3844],{"class":181},"checkAuth",[90,3846,1446],{"class":103},[90,3848,1449],{"class":174},[90,3850,2430],{"class":110},[90,3852,1711],{"class":103},[90,3854,3855],{"class":92,"line":120},[90,3856,1716],{"class":103},[90,3858,3859,3861,3863,3865,3867],{"class":92,"line":132},[90,3860,940],{"class":174},[90,3862,537],{"class":103},[90,3864,945],{"class":174},[90,3866,3844],{"class":110},[90,3868,3869],{"class":103},"()) {\n",[90,3871,3872,3874,3877,3879,3881],{"class":92,"line":144},[90,3873,1936],{"class":174},[90,3875,3876],{"class":110}," navigateTo",[90,3878,901],{"class":103},[90,3880,3129],{"class":610},[90,3882,676],{"class":103},[90,3884,3885],{"class":92,"line":156},[90,3886,1016],{"class":103},[90,3888,3889],{"class":92,"line":246},[90,3890,1092],{"class":103},[22,3892,3894],{"id":3893},"_3-页面使用","3. 页面使用",[81,3896,3900],{"className":3897,"code":3898,"language":3899,"meta":86,"style":86},"language-vue shiki shiki-themes github-light","\u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch1>受保护的页面\u003C/h1>\n    \u003Cbutton @click=\"handleApiCall\">调用API\u003C/button>\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup>\n// 页面级别的鉴权\ndefinePageMeta({\n  middleware: 'auth'\n})\n\nconst { $api } = useNuxtApp()\n\nconst handleApiCall = async () => {\n  try {\n    const response = await $api.post('/api/v1/some-protected-endpoint', {\n      data: 'some data'\n    })\n    \n    // 成功处理\n    if (response.code === API_CODES.SUCCESS) {\n      console.log('操作成功:', response.data)\n    }\n  } catch (error) {\n    // 错误会被拦截器自动处理，显示对应的 toast 消息\n    console.error('请求失败:', error)\n  }\n}\n\u003C/script>\n","vue",[77,3901,3902,3913,3923,3938,3960,3969,3978,3982,3993,3998,4005,4013,4017,4021,4037,4041,4058,4065,4086,4094,4098,4102,4107,4123,4137,4141,4150,4155,4169,4173,4177],{"__ignoreMap":86},[90,3903,3904,3906,3910],{"class":92,"line":93},[90,3905,487],{"class":103},[90,3907,3909],{"class":3908},"shJU0","template",[90,3911,3912],{"class":103},">\n",[90,3914,3915,3918,3921],{"class":92,"line":100},[90,3916,3917],{"class":103},"  \u003C",[90,3919,3920],{"class":3908},"div",[90,3922,3912],{"class":103},[90,3924,3925,3928,3931,3934,3936],{"class":92,"line":107},[90,3926,3927],{"class":103},"    \u003C",[90,3929,3930],{"class":3908},"h1",[90,3932,3933],{"class":103},">受保护的页面\u003C/",[90,3935,3930],{"class":3908},[90,3937,3912],{"class":103},[90,3939,3940,3942,3945,3948,3950,3953,3956,3958],{"class":92,"line":120},[90,3941,3927],{"class":103},[90,3943,3944],{"class":3908},"button",[90,3946,3947],{"class":110}," @click",[90,3949,1449],{"class":103},[90,3951,3952],{"class":610},"\"handleApiCall\"",[90,3954,3955],{"class":103},">调用API\u003C/",[90,3957,3944],{"class":3908},[90,3959,3912],{"class":103},[90,3961,3962,3965,3967],{"class":92,"line":132},[90,3963,3964],{"class":103},"  \u003C/",[90,3966,3920],{"class":3908},[90,3968,3912],{"class":103},[90,3970,3971,3974,3976],{"class":92,"line":144},[90,3972,3973],{"class":103},"\u003C/",[90,3975,3909],{"class":3908},[90,3977,3912],{"class":103},[90,3979,3980],{"class":92,"line":156},[90,3981,210],{"emptyLinePlaceholder":209},[90,3983,3984,3986,3989,3991],{"class":92,"line":246},[90,3985,487],{"class":103},[90,3987,3988],{"class":3908},"script",[90,3990,3589],{"class":110},[90,3992,3912],{"class":103},[90,3994,3995],{"class":92,"line":260},[90,3996,3997],{"class":96},"// 页面级别的鉴权\n",[90,3999,4000,4003],{"class":92,"line":275},[90,4001,4002],{"class":110},"definePageMeta",[90,4004,959],{"class":103},[90,4006,4007,4010],{"class":92,"line":290},[90,4008,4009],{"class":103},"  middleware: ",[90,4011,4012],{"class":610},"'auth'\n",[90,4014,4015],{"class":92,"line":295},[90,4016,1092],{"class":103},[90,4018,4019],{"class":92,"line":301},[90,4020,210],{"emptyLinePlaceholder":209},[90,4022,4023,4025,4027,4029,4031,4033,4035],{"class":92,"line":316},[90,4024,2035],{"class":174},[90,4026,1431],{"class":103},[90,4028,3601],{"class":181},[90,4030,1446],{"class":103},[90,4032,1449],{"class":174},[90,4034,3608],{"class":110},[90,4036,1711],{"class":103},[90,4038,4039],{"class":92,"line":331},[90,4040,210],{"emptyLinePlaceholder":209},[90,4042,4043,4045,4048,4050,4052,4054,4056],{"class":92,"line":336},[90,4044,2035],{"class":174},[90,4046,4047],{"class":110}," handleApiCall",[90,4049,185],{"class":174},[90,4051,1660],{"class":174},[90,4053,2663],{"class":103},[90,4055,546],{"class":174},[90,4057,188],{"class":103},[90,4059,4060,4063],{"class":92,"line":342},[90,4061,4062],{"class":174},"  try",[90,4064,188],{"class":103},[90,4066,4067,4069,4071,4073,4075,4077,4079,4081,4084],{"class":92,"line":357},[90,4068,1428],{"class":174},[90,4070,563],{"class":181},[90,4072,185],{"class":174},[90,4074,568],{"class":174},[90,4076,3633],{"class":103},[90,4078,2873],{"class":110},[90,4080,901],{"class":103},[90,4082,4083],{"class":610},"'/api/v1/some-protected-endpoint'",[90,4085,3297],{"class":103},[90,4087,4088,4091],{"class":92,"line":371},[90,4089,4090],{"class":103},"      data: ",[90,4092,4093],{"class":610},"'some data'\n",[90,4095,4096],{"class":92,"line":386},[90,4097,1011],{"class":103},[90,4099,4100],{"class":92,"line":391},[90,4101,2090],{"class":103},[90,4103,4104],{"class":92,"line":397},[90,4105,4106],{"class":96},"    // 成功处理\n",[90,4108,4109,4111,4113,4115,4117,4119,4121],{"class":92,"line":412},[90,4110,1335],{"class":174},[90,4112,2892],{"class":103},[90,4114,2384],{"class":174},[90,4116,182],{"class":181},[90,4118,597],{"class":103},[90,4120,600],{"class":181},[90,4122,657],{"class":103},[90,4124,4125,4127,4129,4131,4134],{"class":92,"line":426},[90,4126,3008],{"class":103},[90,4128,3666],{"class":110},[90,4130,901],{"class":103},[90,4132,4133],{"class":610},"'操作成功:'",[90,4135,4136],{"class":103},", response.data)\n",[90,4138,4139],{"class":92,"line":751},[90,4140,639],{"class":103},[90,4142,4143,4146,4148],{"class":92,"line":764},[90,4144,4145],{"class":103},"  } ",[90,4147,3000],{"class":174},[90,4149,3003],{"class":103},[90,4151,4152],{"class":92,"line":777},[90,4153,4154],{"class":96},"    // 错误会被拦截器自动处理，显示对应的 toast 消息\n",[90,4156,4157,4160,4162,4164,4167],{"class":92,"line":787},[90,4158,4159],{"class":103},"    console.",[90,4161,649],{"class":110},[90,4163,901],{"class":103},[90,4165,4166],{"class":610},"'请求失败:'",[90,4168,3018],{"class":103},[90,4170,4171],{"class":92,"line":793},[90,4172,1016],{"class":103},[90,4174,4175],{"class":92,"line":798},[90,4176,159],{"class":103},[90,4178,4179,4181,4183],{"class":92,"line":803},[90,4180,3973],{"class":103},[90,4182,3988],{"class":3908},[90,4184,3912],{"class":103},[11,4186,4187],{"id":4187},"错误处理最佳实践",[22,4189,4191],{"id":4190},"_1-前端错误分类处理","1. 前端错误分类处理",[81,4193,4195],{"className":83,"code":4194,"language":85,"meta":86,"style":86},"const handleApiResponse = (response: ApiResponse) => {\n  switch (response.code) {\n    case API_CODES.SUCCESS:\n      // 成功处理\n      break\n      \n    case API_CODES.VALIDATION_ERROR:\n      // 参数验证错误，显示具体字段错误\n      showValidationErrors(response.data)\n      break\n      \n    case API_CODES.PERMISSION_DENIED:\n      // 权限不足，可能需要升级账户\n      showPermissionDialog()\n      break\n      \n    case API_CODES.RESOURCE_NOT_FOUND:\n      // 资源不存在，可能需要刷新页面\n      navigateTo('/404')\n      break\n      \n    default:\n      // 其他错误，显示通用错误消息\n      showErrorToast(response.message)\n  }\n}\n",[77,4196,4197,4221,4229,4243,4248,4253,4257,4269,4274,4282,4286,4290,4303,4308,4315,4319,4323,4336,4341,4353,4357,4361,4368,4373,4381,4385],{"__ignoreMap":86},[90,4198,4199,4201,4204,4206,4208,4210,4212,4215,4217,4219],{"class":92,"line":93},[90,4200,2035],{"class":174},[90,4202,4203],{"class":110}," handleApiResponse",[90,4205,185],{"class":174},[90,4207,537],{"class":103},[90,4209,2173],{"class":477},[90,4211,481],{"class":174},[90,4213,4214],{"class":110}," ApiResponse",[90,4216,543],{"class":103},[90,4218,546],{"class":174},[90,4220,188],{"class":103},[90,4222,4223,4226],{"class":92,"line":100},[90,4224,4225],{"class":174},"  switch",[90,4227,4228],{"class":103}," (response.code) {\n",[90,4230,4231,4234,4236,4238,4240],{"class":92,"line":107},[90,4232,4233],{"class":174},"    case",[90,4235,182],{"class":181},[90,4237,597],{"class":103},[90,4239,600],{"class":181},[90,4241,4242],{"class":103},":\n",[90,4244,4245],{"class":92,"line":120},[90,4246,4247],{"class":96},"      // 成功处理\n",[90,4249,4250],{"class":92,"line":132},[90,4251,4252],{"class":174},"      break\n",[90,4254,4255],{"class":92,"line":144},[90,4256,681],{"class":103},[90,4258,4259,4261,4263,4265,4267],{"class":92,"line":156},[90,4260,4233],{"class":174},[90,4262,182],{"class":181},[90,4264,597],{"class":103},[90,4266,985],{"class":181},[90,4268,4242],{"class":103},[90,4270,4271],{"class":92,"line":246},[90,4272,4273],{"class":96},"      // 参数验证错误，显示具体字段错误\n",[90,4275,4276,4279],{"class":92,"line":260},[90,4277,4278],{"class":110},"      showValidationErrors",[90,4280,4281],{"class":103},"(response.data)\n",[90,4283,4284],{"class":92,"line":275},[90,4285,4252],{"class":174},[90,4287,4288],{"class":92,"line":290},[90,4289,681],{"class":103},[90,4291,4292,4294,4296,4298,4301],{"class":92,"line":295},[90,4293,4233],{"class":174},[90,4295,182],{"class":181},[90,4297,597],{"class":103},[90,4299,4300],{"class":181},"PERMISSION_DENIED",[90,4302,4242],{"class":103},[90,4304,4305],{"class":92,"line":301},[90,4306,4307],{"class":96},"      // 权限不足，可能需要升级账户\n",[90,4309,4310,4313],{"class":92,"line":316},[90,4311,4312],{"class":110},"      showPermissionDialog",[90,4314,1711],{"class":103},[90,4316,4317],{"class":92,"line":331},[90,4318,4252],{"class":174},[90,4320,4321],{"class":92,"line":336},[90,4322,681],{"class":103},[90,4324,4325,4327,4329,4331,4334],{"class":92,"line":342},[90,4326,4233],{"class":174},[90,4328,182],{"class":181},[90,4330,597],{"class":103},[90,4332,4333],{"class":181},"RESOURCE_NOT_FOUND",[90,4335,4242],{"class":103},[90,4337,4338],{"class":92,"line":357},[90,4339,4340],{"class":96},"      // 资源不存在，可能需要刷新页面\n",[90,4342,4343,4346,4348,4351],{"class":92,"line":371},[90,4344,4345],{"class":110},"      navigateTo",[90,4347,901],{"class":103},[90,4349,4350],{"class":610},"'/404'",[90,4352,676],{"class":103},[90,4354,4355],{"class":92,"line":386},[90,4356,4252],{"class":174},[90,4358,4359],{"class":92,"line":391},[90,4360,681],{"class":103},[90,4362,4363,4366],{"class":92,"line":397},[90,4364,4365],{"class":174},"    default",[90,4367,4242],{"class":103},[90,4369,4370],{"class":92,"line":412},[90,4371,4372],{"class":96},"      // 其他错误，显示通用错误消息\n",[90,4374,4375,4378],{"class":92,"line":426},[90,4376,4377],{"class":110},"      showErrorToast",[90,4379,4380],{"class":103},"(response.message)\n",[90,4382,4383],{"class":92,"line":751},[90,4384,1016],{"class":103},[90,4386,4387],{"class":92,"line":764},[90,4388,159],{"class":103},[22,4390,4392],{"id":4391},"_2-自动重试机制","2. 自动重试机制",[81,4394,4396],{"className":83,"code":4395,"language":85,"meta":86,"style":86},"const apiWithRetry = async (url: string, options: any, maxRetries = 3) => {\n  for (let i = 0; i \u003C maxRetries; i++) {\n    try {\n      const response = await $api.request(url, options)\n      \n      if (response.code === API_CODES.SUCCESS) {\n        return response\n      }\n      \n      // 如果是 token 过期，等待刷新后重试\n      if (response.code === API_CODES.TOKEN_EXPIRED && i \u003C maxRetries - 1) {\n        await new Promise(resolve => setTimeout(resolve, 1000))\n        continue\n      }\n      \n      return response\n    } catch (error) {\n      if (i === maxRetries - 1) throw error\n      await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)))\n    }\n  }\n}\n",[77,4397,4398,4442,4473,4479,4496,4500,4516,4523,4527,4531,4536,4568,4598,4603,4607,4611,4617,4625,4647,4679,4683,4687],{"__ignoreMap":86},[90,4399,4400,4402,4405,4407,4409,4411,4414,4416,4418,4420,4422,4424,4426,4428,4431,4433,4436,4438,4440],{"class":92,"line":93},[90,4401,2035],{"class":174},[90,4403,4404],{"class":110}," apiWithRetry",[90,4406,185],{"class":174},[90,4408,1660],{"class":174},[90,4410,537],{"class":103},[90,4412,4413],{"class":477},"url",[90,4415,481],{"class":174},[90,4417,1882],{"class":181},[90,4419,284],{"class":103},[90,4421,2064],{"class":477},[90,4423,481],{"class":174},[90,4425,654],{"class":181},[90,4427,284],{"class":103},[90,4429,4430],{"class":477},"maxRetries",[90,4432,185],{"class":174},[90,4434,4435],{"class":181}," 3",[90,4437,543],{"class":103},[90,4439,546],{"class":174},[90,4441,188],{"class":103},[90,4443,4444,4447,4449,4452,4455,4457,4460,4463,4465,4468,4471],{"class":92,"line":100},[90,4445,4446],{"class":174},"  for",[90,4448,537],{"class":103},[90,4450,4451],{"class":174},"let",[90,4453,4454],{"class":103}," i ",[90,4456,1449],{"class":174},[90,4458,4459],{"class":181}," 0",[90,4461,4462],{"class":103},"; i ",[90,4464,487],{"class":174},[90,4466,4467],{"class":103}," maxRetries; i",[90,4469,4470],{"class":174},"++",[90,4472,657],{"class":103},[90,4474,4475,4477],{"class":92,"line":107},[90,4476,553],{"class":174},[90,4478,188],{"class":103},[90,4480,4481,4483,4485,4487,4489,4491,4493],{"class":92,"line":120},[90,4482,560],{"class":174},[90,4484,563],{"class":181},[90,4486,185],{"class":174},[90,4488,568],{"class":174},[90,4490,3633],{"class":103},[90,4492,2168],{"class":110},[90,4494,4495],{"class":103},"(url, options)\n",[90,4497,4498],{"class":92,"line":132},[90,4499,681],{"class":103},[90,4501,4502,4504,4506,4508,4510,4512,4514],{"class":92,"line":144},[90,4503,686],{"class":174},[90,4505,2892],{"class":103},[90,4507,2384],{"class":174},[90,4509,182],{"class":181},[90,4511,597],{"class":103},[90,4513,600],{"class":181},[90,4515,657],{"class":103},[90,4517,4518,4520],{"class":92,"line":156},[90,4519,730],{"class":174},[90,4521,4522],{"class":103}," response\n",[90,4524,4525],{"class":92,"line":246},[90,4526,634],{"class":103},[90,4528,4529],{"class":92,"line":260},[90,4530,681],{"class":103},[90,4532,4533],{"class":92,"line":275},[90,4534,4535],{"class":96},"      // 如果是 token 过期，等待刷新后重试\n",[90,4537,4538,4540,4542,4544,4546,4548,4550,4553,4555,4557,4560,4563,4566],{"class":92,"line":290},[90,4539,686],{"class":174},[90,4541,2892],{"class":103},[90,4543,2384],{"class":174},[90,4545,182],{"class":181},[90,4547,597],{"class":103},[90,4549,2391],{"class":181},[90,4551,4552],{"class":174}," &&",[90,4554,4454],{"class":103},[90,4556,487],{"class":174},[90,4558,4559],{"class":103}," maxRetries ",[90,4561,4562],{"class":174},"-",[90,4564,4565],{"class":181}," 1",[90,4567,657],{"class":103},[90,4569,4570,4573,4576,4578,4580,4583,4586,4589,4592,4595],{"class":92,"line":295},[90,4571,4572],{"class":174},"        await",[90,4574,4575],{"class":174}," new",[90,4577,3258],{"class":181},[90,4579,901],{"class":103},[90,4581,4582],{"class":477},"resolve",[90,4584,4585],{"class":174}," =>",[90,4587,4588],{"class":110}," setTimeout",[90,4590,4591],{"class":103},"(resolve, ",[90,4593,4594],{"class":181},"1000",[90,4596,4597],{"class":103},"))\n",[90,4599,4600],{"class":92,"line":301},[90,4601,4602],{"class":174},"        continue\n",[90,4604,4605],{"class":92,"line":316},[90,4606,634],{"class":103},[90,4608,4609],{"class":92,"line":331},[90,4610,681],{"class":103},[90,4612,4613,4615],{"class":92,"line":336},[90,4614,584],{"class":174},[90,4616,4522],{"class":103},[90,4618,4619,4621,4623],{"class":92,"line":342},[90,4620,2997],{"class":103},[90,4622,3000],{"class":174},[90,4624,3003],{"class":103},[90,4626,4627,4629,4632,4634,4636,4638,4640,4642,4644],{"class":92,"line":357},[90,4628,686],{"class":174},[90,4630,4631],{"class":103}," (i ",[90,4633,2384],{"class":174},[90,4635,4559],{"class":103},[90,4637,4562],{"class":174},[90,4639,4565],{"class":181},[90,4641,543],{"class":103},[90,4643,1109],{"class":174},[90,4645,4646],{"class":103}," error\n",[90,4648,4649,4652,4654,4656,4658,4660,4662,4664,4666,4668,4670,4672,4674,4676],{"class":92,"line":371},[90,4650,4651],{"class":174},"      await",[90,4653,4575],{"class":174},[90,4655,3258],{"class":181},[90,4657,901],{"class":103},[90,4659,4582],{"class":477},[90,4661,4585],{"class":174},[90,4663,4588],{"class":110},[90,4665,4591],{"class":103},[90,4667,4594],{"class":181},[90,4669,1750],{"class":174},[90,4671,4631],{"class":103},[90,4673,1798],{"class":174},[90,4675,4565],{"class":181},[90,4677,4678],{"class":103},")))\n",[90,4680,4681],{"class":92,"line":386},[90,4682,639],{"class":103},[90,4684,4685],{"class":92,"line":391},[90,4686,1016],{"class":103},[90,4688,4689],{"class":92,"line":397},[90,4690,159],{"class":103},[11,4692,4693],{"id":4693},"安全考虑",[22,4695,4697],{"id":4696},"_1-token-存储安全","1. Token 存储安全",[27,4699,4700,4706,4711],{},[30,4701,4702,4705],{},[33,4703,4704],{},"Access Token",": 存储在内存中，避免 XSS 攻击",[30,4707,4708,4710],{},[33,4709,54],{},": 存储在 httpOnly cookie 中（推荐）或 localStorage",[30,4712,4713,4716],{},[33,4714,4715],{},"敏感信息",": 永远不要在 JWT 中存储敏感信息",[22,4718,4720],{"id":4719},"_2-csrf-防护","2. CSRF 防护",[81,4722,4724],{"className":83,"code":4723,"language":85,"meta":86,"style":86},"// 添加 CSRF token 到请求头\nonRequest: ({ options }) => {\n  const csrfToken = document.querySelector('meta[name=\"csrf-token\"]')?.getAttribute('content')\n  if (csrfToken) {\n    options.headers.set('X-CSRF-Token', csrfToken)\n  }\n}\n",[77,4725,4726,4731,4747,4780,4787,4802,4806],{"__ignoreMap":86},[90,4727,4728],{"class":92,"line":93},[90,4729,4730],{"class":96},"// 添加 CSRF token 到请求头\n",[90,4732,4733,4736,4739,4741,4743,4745],{"class":92,"line":100},[90,4734,4735],{"class":110},"onRequest",[90,4737,4738],{"class":103},": ({ ",[90,4740,2064],{"class":477},[90,4742,2067],{"class":103},[90,4744,546],{"class":174},[90,4746,188],{"class":103},[90,4748,4749,4751,4754,4756,4759,4762,4764,4767,4770,4773,4775,4778],{"class":92,"line":107},[90,4750,918],{"class":174},[90,4752,4753],{"class":181}," csrfToken",[90,4755,185],{"class":174},[90,4757,4758],{"class":103}," document.",[90,4760,4761],{"class":110},"querySelector",[90,4763,901],{"class":103},[90,4765,4766],{"class":610},"'meta[name=\"csrf-token\"]'",[90,4768,4769],{"class":103},")?.",[90,4771,4772],{"class":110},"getAttribute",[90,4774,901],{"class":103},[90,4776,4777],{"class":610},"'content'",[90,4779,676],{"class":103},[90,4781,4782,4784],{"class":92,"line":120},[90,4783,940],{"class":174},[90,4785,4786],{"class":103}," (csrfToken) {\n",[90,4788,4789,4792,4794,4796,4799],{"class":92,"line":132},[90,4790,4791],{"class":103},"    options.headers.",[90,4793,2110],{"class":110},[90,4795,901],{"class":103},[90,4797,4798],{"class":610},"'X-CSRF-Token'",[90,4800,4801],{"class":103},", csrfToken)\n",[90,4803,4804],{"class":92,"line":144},[90,4805,1016],{"class":103},[90,4807,4808],{"class":92,"line":156},[90,4809,159],{"class":103},[22,4811,4813],{"id":4812},"_3-请求频率限制","3. 请求频率限制",[81,4815,4817],{"className":83,"code":4816,"language":85,"meta":86,"style":86},"// 在中间件中添加频率限制\nexport default defineEventHandler(async (event) => {\n  const ip = getClientIP(event)\n  const key = `rate_limit:${ip}`\n  \n  const current = await redis.incr(key)\n  if (current === 1) {\n    await redis.expire(key, 60) // 1分钟窗口\n  }\n  \n  if (current > 100) { // 每分钟最多100个请求\n    setResponseStatus(event, 200)\n    return {\n      code: API_CODES.FORBIDDEN,\n      message: '请求过于频繁，请稍后再试',\n      data: null,\n      timestamp: Date.now(),\n    }\n  }\n})\n",[77,4818,4819,4824,4846,4860,4878,4882,4901,4914,4935,4939,4943,4961,4972,4978,4992,5002,5010,5019,5023,5027],{"__ignoreMap":86},[90,4820,4821],{"class":92,"line":93},[90,4822,4823],{"class":96},"// 在中间件中添加频率限制\n",[90,4825,4826,4828,4830,4832,4834,4836,4838,4840,4842,4844],{"class":92,"line":100},[90,4827,175],{"class":174},[90,4829,896],{"class":174},[90,4831,1299],{"class":110},[90,4833,901],{"class":103},[90,4835,534],{"class":174},[90,4837,537],{"class":103},[90,4839,540],{"class":477},[90,4841,543],{"class":103},[90,4843,546],{"class":174},[90,4845,188],{"class":103},[90,4847,4848,4850,4853,4855,4858],{"class":92,"line":107},[90,4849,918],{"class":174},[90,4851,4852],{"class":181}," ip",[90,4854,185],{"class":174},[90,4856,4857],{"class":110}," getClientIP",[90,4859,574],{"class":103},[90,4861,4862,4864,4867,4869,4872,4875],{"class":92,"line":120},[90,4863,918],{"class":174},[90,4865,4866],{"class":181}," key",[90,4868,185],{"class":174},[90,4870,4871],{"class":610}," `rate_limit:${",[90,4873,4874],{"class":103},"ip",[90,4876,4877],{"class":610},"}`\n",[90,4879,4880],{"class":92,"line":132},[90,4881,1716],{"class":103},[90,4883,4884,4886,4889,4891,4893,4895,4898],{"class":92,"line":144},[90,4885,918],{"class":174},[90,4887,4888],{"class":181}," current",[90,4890,185],{"class":174},[90,4892,568],{"class":174},[90,4894,1729],{"class":103},[90,4896,4897],{"class":110},"incr",[90,4899,4900],{"class":103},"(key)\n",[90,4902,4903,4905,4908,4910,4912],{"class":92,"line":156},[90,4904,940],{"class":174},[90,4906,4907],{"class":103}," (current ",[90,4909,2384],{"class":174},[90,4911,4565],{"class":181},[90,4913,657],{"class":103},[90,4915,4916,4919,4921,4924,4927,4930,4932],{"class":92,"line":246},[90,4917,4918],{"class":174},"    await",[90,4920,1729],{"class":103},[90,4922,4923],{"class":110},"expire",[90,4925,4926],{"class":103},"(key, ",[90,4928,4929],{"class":181},"60",[90,4931,543],{"class":103},[90,4933,4934],{"class":96},"// 1分钟窗口\n",[90,4936,4937],{"class":92,"line":260},[90,4938,1016],{"class":103},[90,4940,4941],{"class":92,"line":275},[90,4942,1716],{"class":103},[90,4944,4945,4947,4949,4952,4955,4958],{"class":92,"line":290},[90,4946,940],{"class":174},[90,4948,4907],{"class":103},[90,4950,4951],{"class":174},">",[90,4953,4954],{"class":181}," 100",[90,4956,4957],{"class":103},") { ",[90,4959,4960],{"class":96},"// 每分钟最多100个请求\n",[90,4962,4963,4966,4968,4970],{"class":92,"line":295},[90,4964,4965],{"class":110},"    setResponseStatus",[90,4967,670],{"class":103},[90,4969,673],{"class":181},[90,4971,676],{"class":103},[90,4973,4974,4976],{"class":92,"line":301},[90,4975,1936],{"class":174},[90,4977,188],{"class":103},[90,4979,4980,4983,4985,4987,4990],{"class":92,"line":316},[90,4981,4982],{"class":103},"      code: ",[90,4984,594],{"class":181},[90,4986,597],{"class":103},[90,4988,4989],{"class":181},"FORBIDDEN",[90,4991,204],{"class":103},[90,4993,4994,4997,5000],{"class":92,"line":331},[90,4995,4996],{"class":103},"      message: ",[90,4998,4999],{"class":610},"'请求过于频繁，请稍后再试'",[90,5001,204],{"class":103},[90,5003,5004,5006,5008],{"class":92,"line":336},[90,5005,4090],{"class":103},[90,5007,845],{"class":181},[90,5009,204],{"class":103},[90,5011,5012,5015,5017],{"class":92,"line":342},[90,5013,5014],{"class":103},"      timestamp: Date.",[90,5016,626],{"class":110},[90,5018,629],{"class":103},[90,5020,5021],{"class":92,"line":357},[90,5022,639],{"class":103},[90,5024,5025],{"class":92,"line":371},[90,5026,1016],{"class":103},[90,5028,5029],{"class":92,"line":386},[90,5030,1092],{"class":103},[11,5032,5033],{"id":5033},"常见问题排查",[22,5035,5037],{"id":5036},"_1-token-刷新失败","1. Token 刷新失败",[15,5039,5040,5043],{},[33,5041,5042],{},"现象",": 用户频繁被要求重新登录",[15,5045,5046,481],{},[33,5047,5048],{},"排查步骤",[5050,5051,5052,5055,5058,5061],"ol",{},[30,5053,5054],{},"检查 Redis 中的 refresh token 是否存在",[30,5056,5057],{},"确认 refresh token 的过期时间设置",[30,5059,5060],{},"检查网络请求是否正常到达服务器",[30,5062,5063],{},"确认前端 token 刷新逻辑是否正确触发",[22,5065,5067],{"id":5066},"_2-鉴权中间件不生效","2. 鉴权中间件不生效",[15,5069,5070,5072],{},[33,5071,5042],{},": 未登录用户可以访问受保护的 API",[15,5074,5075,481],{},[33,5076,5048],{},[5050,5078,5079,5082,5085,5088],{},[30,5080,5081],{},"检查路由是否在白名单中",[30,5083,5084],{},"确认中间件的执行顺序",[30,5086,5087],{},"检查 Authorization 头是否正确传递",[30,5089,5090],{},"验证 JWT 解析逻辑",[22,5092,5094],{"id":5093},"_3-前端错误处理不正确","3. 前端错误处理不正确",[15,5096,5097,5099],{},[33,5098,5042],{},": 错误信息显示不准确或不显示",[15,5101,5102,481],{},[33,5103,5048],{},[5050,5105,5106,5109,5112,5115],{},[30,5107,5108],{},"检查 onResponse 和 onResponseError 的处理逻辑",[30,5110,5111],{},"确认错误代码的匹配是否正确",[30,5113,5114],{},"验证 toast 组件是否正常工作",[30,5116,5117],{},"检查控制台是否有 JavaScript 错误",[11,5119,5120],{"id":5120},"总结",[15,5122,5123],{},"本鉴权系统提供了：",[5050,5125,5126,5132,5138,5144,5150],{},[30,5127,5128,5131],{},[33,5129,5130],{},"安全性",": 双 Token 机制，JWT + Redis 存储",[30,5133,5134,5137],{},[33,5135,5136],{},"用户体验",": 自动 token 刷新，无感知续期",[30,5139,5140,5143],{},[33,5141,5142],{},"开发友好",": 统一的错误处理，清晰的错误代码",[30,5145,5146,5149],{},[33,5147,5148],{},"可维护性",": 模块化设计，易于扩展和修改",[30,5151,5152,5155],{},[33,5153,5154],{},"类型安全",": 完整的 TypeScript 支持",[5157,5158,5159],"style",{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sqxcx, html code.shiki .sqxcx{--shiki-default:#E36209}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html pre.shiki code .shJU0, html code.shiki .shJU0{--shiki-default:#22863A}",{"title":86,"searchDepth":100,"depth":100,"links":5161},[5162,5163,5167,5173,5178,5183,5187,5192,5197],{"id":13,"depth":100,"text":13},{"id":20,"depth":100,"text":20,"children":5164},[5165,5166],{"id":24,"depth":107,"text":25},{"id":71,"depth":107,"text":72},{"id":431,"depth":100,"text":431,"children":5168},[5169,5170,5171,5172],{"id":434,"depth":107,"text":435},{"id":875,"depth":107,"text":876},{"id":1283,"depth":107,"text":1284},{"id":1641,"depth":107,"text":1642},{"id":2020,"depth":100,"text":2020,"children":5174},[5175,5176,5177],{"id":2023,"depth":107,"text":2024},{"id":2643,"depth":107,"text":2644},{"id":3196,"depth":107,"text":3197},{"id":3556,"depth":100,"text":3556,"children":5179},[5180,5181,5182],{"id":3559,"depth":107,"text":3560},{"id":3803,"depth":107,"text":3804},{"id":3893,"depth":107,"text":3894},{"id":4187,"depth":100,"text":4187,"children":5184},[5185,5186],{"id":4190,"depth":107,"text":4191},{"id":4391,"depth":107,"text":4392},{"id":4693,"depth":100,"text":4693,"children":5188},[5189,5190,5191],{"id":4696,"depth":107,"text":4697},{"id":4719,"depth":107,"text":4720},{"id":4812,"depth":107,"text":4813},{"id":5033,"depth":100,"text":5033,"children":5193},[5194,5195,5196],{"id":5036,"depth":107,"text":5037},{"id":5066,"depth":107,"text":5067},{"id":5093,"depth":107,"text":5094},{"id":5120,"depth":100,"text":5120},"2025-08-21T00:00:00.000Z","md",{},"/post/nuxt/blog/auth-system-docs","---\ntitle: 博客鉴权机制详细文档\ndate: 2025-08-21\nlastmod: 2025-08-21\ntags: [\"博客\", \"Nuxt\"]\n\n---\n## 概述\n\n本项目采用双 Token 鉴权机制，结合了 JWT 的无状态特性和 Redis 的有状态管理，提供了安全性和用户体验的最佳平衡。\n\n## 鉴权架构\n\n### 1. 双 Token 机制\n\n- **Access Token (JWT)**\n  - 类型：无状态 JWT\n  - 过期时间：15 分钟\n  - 用途：API 访问凭证\n  - 存储：客户端内存/localStorage\n\n- **Refresh Token**\n  - 类型：随机字符串\n  - 过期时间：7 天\n  - 用途：刷新 Access Token\n  - 存储：Redis（服务端）+ 客户端\n\n### 2. 错误代码系统\n\n所有 API 统一返回 200 HTTP 状态码，通过 `code` 字段（数值）标识业务状态：\n\n```typescript\n// 响应格式\n{\n  code: number,      // 错误代码（0表示成功）\n  message: string,   // 错误描述\n  data: any,         // 响应数据\n  timestamp: number  // 时间戳\n}\n```\n\n#### 错误代码定义\n\n```typescript [/shared/utils/apiCodes.ts]\nexport const API_CODES = {\n  // 成功\n  SUCCESS: 0,\n\n  // 认证相关错误 (1000-1999)\n  NO_TOKEN: 1001,           // 未提供token\n  TOKEN_EXPIRED: 1002,      // token过期（可刷新）\n  TOKEN_INVALID: 1003,      // token无效（不可刷新）\n  AUTH_FAILED: 1004,        // 认证失败\n  REFRESH_TOKEN_EXPIRED: 1005, // refresh token过期\n\n  // 权限相关错误 (2000-2999)\n  PERMISSION_DENIED: 2001,  // 无权限\n  FORBIDDEN: 2002,          // 禁止访问\n\n  // 业务相关错误 (3000-3999)\n  VALIDATION_ERROR: 3001,   // 参数验证错误\n  RESOURCE_NOT_FOUND: 3002, // 资源不存在\n  DUPLICATE_ERROR: 3003,    // 重复错误\n\n  // 系统错误 (9000-9999)\n  INTERNAL_ERROR: 9001,     // 内部错误\n  NETWORK_ERROR: 9002,      // 网络错误\n}\n```\n\n## 后端实现\n\n### 1. API 响应处理器\n\n```typescript [/server/utils/handler.ts]\nexport const defineStandardResponseHandler = \u003CT extends EventHandlerRequest, D> (\n  handler: EventHandler\u003CT, D>,\n): EventHandler\u003CT, D> =>\n  defineEventHandler\u003CT>(async (event) => {\n    try {\n      const response = await handler(event)\n      // 成功响应\n      return {\n        code: API_CODES.SUCCESS,\n        message: 'ok',\n        data: response,\n        timestamp: Date.now(),\n      }\n    }\n    catch (error: any) {\n      // 强制设置 HTTP 状态码为 200\n      setResponseStatus(event, 200)\n      \n      if (error.statusCode) {\n        const customCode = error.data?.code\n        const customMessage = error.data?.message || error.message\n        \n        return {\n          code: customCode || API_CODES.INTERNAL_ERROR,\n          message: customMessage || '出错啦，请稍后再试～',\n          data: error.data?.data || null,\n          timestamp: Date.now(),\n        }\n      }\n      \n      // 未知错误\n      return {\n        code: API_CODES.INTERNAL_ERROR,\n        message: '出错啦，请稍后再试～',\n        data: null,\n        timestamp: Date.now(),\n      }\n    }\n  })\n```\n\n### 2. API 开发方式\n\n#### 成功案例\n\n```typescript\n// /server/api/v1/user/login.post.ts\nexport default defineStandardResponseHandler(async (event) => {\n  const body = await useSafeValidatedBody(event, schema)\n\n  if (!body.success) {\n    throw createError({\n      statusCode: 400,\n      data: {\n        code: API_CODES.VALIDATION_ERROR,\n        message: '参数验证失败',\n        data: body.error,\n      },\n    })\n  }\n\n  // 业务逻辑处理...\n  const tokenPair = await generateTokenPair(user.id)\n\n  // 直接返回数据，由 handler 包装成标准格式\n  return {\n    accessToken: tokenPair.accessToken,\n    refreshToken: tokenPair.refreshToken,\n    accessExpiresAt: tokenPair.accessExpiresAt,\n    refreshExpiresAt: tokenPair.refreshExpiresAt,\n    user,\n  }\n})\n```\n\n#### 错误处理\n\n```typescript\n// 参数验证错误\nthrow createError({\n  statusCode: 400,\n  data: {\n    code: API_CODES.VALIDATION_ERROR,\n    message: '参数验证失败',\n    data: validationErrors,\n  },\n})\n\n// 认证失败\nthrow createError({\n  statusCode: 401,\n  data: {\n    code: API_CODES.AUTH_FAILED,\n    message: '账号或密码错误',\n  },\n})\n\n// 内部错误\nthrow createError({\n  statusCode: 500,\n  data: {\n    code: API_CODES.INTERNAL_ERROR,\n    message: '系统繁忙，请稍后重试',\n  },\n})\n```\n\n### 3. 鉴权中间件\n\n```typescript [/server/middleware/2.auth0.ts]\nexport default defineEventHandler(async (event) => {\n  // 需要鉴权的路径判断\n  if (needsAuth(event)) {\n    if (!event.context.token) {\n      // 强制设置状态码为 200，返回错误代码\n      setResponseStatus(event, 200)\n      return {\n        code: API_CODES.NO_TOKEN,\n        message: API_ERROR_MESSAGES[API_CODES.NO_TOKEN],\n        data: null,\n        timestamp: Date.now(),\n      }\n    }\n\n    const { isAuth, userId, error } = verifyJWTAccessToken(event.context.token)\n\n    if (!isAuth) {\n      let errorCode = API_CODES.AUTH_FAILED\n      if (error?.includes('expired')) {\n        errorCode = API_CODES.TOKEN_EXPIRED\n      } else if (error?.includes('invalid')) {\n        errorCode = API_CODES.TOKEN_INVALID\n      }\n\n      setResponseStatus(event, 200)\n      return {\n        code: errorCode,\n        message: API_ERROR_MESSAGES[errorCode],\n        data: null,\n        timestamp: Date.now(),\n      }\n    }\n\n    event.context.userId = userId\n  }\n})\n```\n\n### 4. Token 管理\n\n```typescript [/server/utils/token.ts]\n// 生成双 Token\nexport async function generateTokenPair(userId: number) {\n  const accessToken = generateJWT(userId, '15m')\n  const refreshToken = generateRandomToken()\n  \n  // 存储 refresh token 到 Redis\n  await redis.setex(`refresh_token:${userId}`, 7 * 24 * 60 * 60, refreshToken)\n  \n  return {\n    accessToken,\n    refreshToken,\n    accessExpiresAt: Date.now() + 15 * 60 * 1000,\n    refreshExpiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000,\n  }\n}\n\n// 刷新访问令牌\nexport async function refreshAccessToken(refreshToken: string) {\n  // 从 Redis 验证 refresh token\n  const userId = await redis.get(`refresh_token_user:${refreshToken}`)\n  \n  if (!userId) {\n    return null // refresh token 无效或过期\n  }\n\n  // 生成新的 access token\n  const newAccessToken = generateJWT(userId, '15m')\n  \n  return {\n    accessToken: newAccessToken,\n    expiresAt: Date.now() + 15 * 60 * 1000,\n  }\n}\n```\n\n## 前端实现\n\n### 1. 请求拦截器\n\n```typescript [/app/plugins/fetch.ts]\nconst $api = $fetch.create({\n  onRequest: async ({ options }) => {\n    const userStore = useUser()\n    \n    // 自动添加 Authorization 头\n    if (userStore.tokenInfo.value.accessToken) {\n      options.headers.set('Authorization', `Bearer ${userStore.tokenInfo.value.accessToken}`)\n    }\n  },\n\n  onResponse: async ({ request, response }) => {\n    const userStore = useUser()\n    const globalToast = useGlobalToast()\n\n    const apiResponse = response._data\n    \n    // 处理业务层面的错误\n    if (apiResponse?.code && apiResponse.code !== API_CODES.SUCCESS) {\n      const { code, message } = apiResponse\n\n      // 不可刷新的认证错误\n      if ([API_CODES.NO_TOKEN, API_CODES.TOKEN_INVALID, API_CODES.AUTH_FAILED].includes(code)) {\n        userStore.logout()\n        globalToast.add({ message: message || '认证失败，请重新登录', type: 'error' })\n        return\n      }\n\n      // Token 过期处理\n      if (code === API_CODES.TOKEN_EXPIRED) {\n        // 自动刷新 token 逻辑\n        if (userStore.tokenInfo.value.refreshToken && !userStore.isRefreshTokenExpired.value) {\n          const { refreshToken } = useAuth()\n          const success = await refreshToken()\n          \n          if (!success) {\n            userStore.logout()\n            globalToast.add({ message: '登录已过期，请重新登录', type: 'error' })\n          }\n        }\n        return\n      }\n\n      // 其他错误处理\n      globalToast.add({ message: message || '操作失败', type: 'error' })\n    }\n  },\n\n  onResponseError: async ({ response }) => {\n    // 只处理真正的网络错误\n    const globalToast = useGlobalToast()\n    const errorMessage = response?._data?.message || '网络请求失败'\n    globalToast.add({ message: errorMessage, type: 'error' })\n  },\n})\n```\n\n### 2. 用户状态管理\n\n```typescript [/composables/useUser.ts]\nexport const useUser = () => {\n  const tokenInfo = ref({\n    accessToken: '',\n    refreshToken: '',\n    accessExpiresAt: 0,\n    refreshExpiresAt: 0,\n  })\n\n  // 检查 access token 是否过期\n  const isAccessTokenExpired = computed(() => {\n    return Date.now() >= tokenInfo.value.accessExpiresAt\n  })\n\n  // 检查 refresh token 是否过期\n  const isRefreshTokenExpired = computed(() => {\n    return Date.now() >= tokenInfo.value.refreshExpiresAt\n  })\n\n  // 登录\n  const login = async (credentials: LoginData) => {\n    try {\n      const response = await $fetch.post('/api/v1/user/login', credentials)\n      \n      if (response.code === API_CODES.SUCCESS) {\n        tokenInfo.value = {\n          accessToken: response.data.accessToken,\n          refreshToken: response.data.refreshToken,\n          accessExpiresAt: response.data.accessExpiresAt,\n          refreshExpiresAt: response.data.refreshExpiresAt,\n        }\n        \n        // 保存到 localStorage\n        localStorage.setItem('tokenInfo', JSON.stringify(tokenInfo.value))\n        return true\n      }\n      \n      return false\n    } catch (error) {\n      console.error('登录失败:', error)\n      return false\n    }\n  }\n\n  // 登出\n  const logout = () => {\n    tokenInfo.value = {\n      accessToken: '',\n      refreshToken: '',\n      accessExpiresAt: 0,\n      refreshExpiresAt: 0,\n    }\n    localStorage.removeItem('tokenInfo')\n    navigateTo('/login')\n  }\n\n  return {\n    tokenInfo: readonly(tokenInfo),\n    isAccessTokenExpired,\n    isRefreshTokenExpired,\n    login,\n    logout,\n  }\n}\n```\n\n### 3. 鉴权组合式函数\n\n```typescript [/composables/useAuth.ts]\nexport const useAuth = () => {\n  const userStore = useUser()\n\n  // 刷新 token\n  const refreshToken = async (): Promise\u003Cboolean> => {\n    try {\n      const response = await $fetch.post('/api/v1/auth/refresh', {\n        refreshToken: userStore.tokenInfo.value.refreshToken,\n      })\n\n      if (response.code === API_CODES.SUCCESS) {\n        // 更新 access token\n        userStore.tokenInfo.value.accessToken = response.data.accessToken\n        userStore.tokenInfo.value.accessExpiresAt = response.data.expiresAt\n        \n        // 更新 localStorage\n        localStorage.setItem('tokenInfo', JSON.stringify(userStore.tokenInfo.value))\n        return true\n      }\n\n      return false\n    } catch (error) {\n      console.error('Token 刷新失败:', error)\n      return false\n    }\n  }\n\n  // 检查登录状态\n  const checkAuth = () => {\n    if (!userStore.tokenInfo.value.accessToken) {\n      return false\n    }\n\n    if (userStore.isRefreshTokenExpired.value) {\n      userStore.logout()\n      return false\n    }\n\n    return true\n  }\n\n  return {\n    refreshToken,\n    checkAuth,\n  }\n}\n```\n\n## 前端使用方式\n\n### 1. API 调用\n\n```typescript\n// 在组件中使用\nexport default defineNuxtPlugin({\n  async setup() {\n    const { $api } = useNuxtApp()\n\n    // GET 请求\n    const userData = await $api.get('/api/v1/user/profile')\n    if (userData.code === API_CODES.SUCCESS) {\n      console.log('用户信息:', userData.data)\n    }\n\n    // POST 请求\n    const result = await $api.post('/api/v1/posts', {\n      title: '新文章',\n      content: '文章内容...'\n    })\n\n    if (result.code === API_CODES.SUCCESS) {\n      console.log('创建成功:', result.data)\n    } else {\n      console.error('创建失败:', result.message)\n    }\n  }\n})\n```\n\n### 2. 路由守卫\n\n```typescript\n// /middleware/auth.ts\nexport default defineNuxtRouteMiddleware((to) => {\n  const { checkAuth } = useAuth()\n  \n  if (!checkAuth()) {\n    return navigateTo('/login')\n  }\n})\n```\n\n### 3. 页面使用\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch1>受保护的页面\u003C/h1>\n    \u003Cbutton @click=\"handleApiCall\">调用API\u003C/button>\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup>\n// 页面级别的鉴权\ndefinePageMeta({\n  middleware: 'auth'\n})\n\nconst { $api } = useNuxtApp()\n\nconst handleApiCall = async () => {\n  try {\n    const response = await $api.post('/api/v1/some-protected-endpoint', {\n      data: 'some data'\n    })\n    \n    // 成功处理\n    if (response.code === API_CODES.SUCCESS) {\n      console.log('操作成功:', response.data)\n    }\n  } catch (error) {\n    // 错误会被拦截器自动处理，显示对应的 toast 消息\n    console.error('请求失败:', error)\n  }\n}\n\u003C/script>\n```\n\n## 错误处理最佳实践\n\n### 1. 前端错误分类处理\n\n```typescript\nconst handleApiResponse = (response: ApiResponse) => {\n  switch (response.code) {\n    case API_CODES.SUCCESS:\n      // 成功处理\n      break\n      \n    case API_CODES.VALIDATION_ERROR:\n      // 参数验证错误，显示具体字段错误\n      showValidationErrors(response.data)\n      break\n      \n    case API_CODES.PERMISSION_DENIED:\n      // 权限不足，可能需要升级账户\n      showPermissionDialog()\n      break\n      \n    case API_CODES.RESOURCE_NOT_FOUND:\n      // 资源不存在，可能需要刷新页面\n      navigateTo('/404')\n      break\n      \n    default:\n      // 其他错误，显示通用错误消息\n      showErrorToast(response.message)\n  }\n}\n```\n\n### 2. 自动重试机制\n\n```typescript\nconst apiWithRetry = async (url: string, options: any, maxRetries = 3) => {\n  for (let i = 0; i \u003C maxRetries; i++) {\n    try {\n      const response = await $api.request(url, options)\n      \n      if (response.code === API_CODES.SUCCESS) {\n        return response\n      }\n      \n      // 如果是 token 过期，等待刷新后重试\n      if (response.code === API_CODES.TOKEN_EXPIRED && i \u003C maxRetries - 1) {\n        await new Promise(resolve => setTimeout(resolve, 1000))\n        continue\n      }\n      \n      return response\n    } catch (error) {\n      if (i === maxRetries - 1) throw error\n      await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)))\n    }\n  }\n}\n```\n\n## 安全考虑\n\n### 1. Token 存储安全\n\n- **Access Token**: 存储在内存中，避免 XSS 攻击\n- **Refresh Token**: 存储在 httpOnly cookie 中（推荐）或 localStorage\n- **敏感信息**: 永远不要在 JWT 中存储敏感信息\n\n### 2. CSRF 防护\n\n```typescript\n// 添加 CSRF token 到请求头\nonRequest: ({ options }) => {\n  const csrfToken = document.querySelector('meta[name=\"csrf-token\"]')?.getAttribute('content')\n  if (csrfToken) {\n    options.headers.set('X-CSRF-Token', csrfToken)\n  }\n}\n```\n\n### 3. 请求频率限制\n\n```typescript\n// 在中间件中添加频率限制\nexport default defineEventHandler(async (event) => {\n  const ip = getClientIP(event)\n  const key = `rate_limit:${ip}`\n  \n  const current = await redis.incr(key)\n  if (current === 1) {\n    await redis.expire(key, 60) // 1分钟窗口\n  }\n  \n  if (current > 100) { // 每分钟最多100个请求\n    setResponseStatus(event, 200)\n    return {\n      code: API_CODES.FORBIDDEN,\n      message: '请求过于频繁，请稍后再试',\n      data: null,\n      timestamp: Date.now(),\n    }\n  }\n})\n```\n\n## 常见问题排查\n\n### 1. Token 刷新失败\n\n**现象**: 用户频繁被要求重新登录\n\n**排查步骤**:\n1. 检查 Redis 中的 refresh token 是否存在\n2. 确认 refresh token 的过期时间设置\n3. 检查网络请求是否正常到达服务器\n4. 确认前端 token 刷新逻辑是否正确触发\n\n### 2. 鉴权中间件不生效\n\n**现象**: 未登录用户可以访问受保护的 API\n\n**排查步骤**:\n1. 检查路由是否在白名单中\n2. 确认中间件的执行顺序\n3. 检查 Authorization 头是否正确传递\n4. 验证 JWT 解析逻辑\n\n### 3. 前端错误处理不正确\n\n**现象**: 错误信息显示不准确或不显示\n\n**排查步骤**:\n1. 检查 onResponse 和 onResponseError 的处理逻辑\n2. 确认错误代码的匹配是否正确\n3. 验证 toast 组件是否正常工作\n4. 检查控制台是否有 JavaScript 错误\n\n## 总结\n\n本鉴权系统提供了：\n\n1. **安全性**: 双 Token 机制，JWT + Redis 存储\n2. **用户体验**: 自动 token 刷新，无感知续期\n3. **开发友好**: 统一的错误处理，清晰的错误代码\n4. **可维护性**: 模块化设计，易于扩展和修改\n5. **类型安全**: 完整的 TypeScript 支持\n\n",{"title":5,"description":86},"post/nuxt/blog/auth-system-docs",[5206,5207],"博客","Nuxt","vabFoCSZeH71pIoULYiVbReZFmIWFA0Mrm2W3MKTHJQ",[5210,5214],{"title":5211,"path":5212,"stem":5213},"OpenClaw 安装入门（Windows）","/post/zzao/openclaw/openclaw-install-windows","post/zzao/openclaw/openclaw-install-windows",{"title":5215,"path":5216,"stem":5217},"假设你是AI，你的Skill应该是什么样的","/post/zzao/ai-skill-structure","post/zzao/ai-skill-structure",1779005085373]