一、文生文功能
(1)前端部分
使用 Pinia 狀態管理庫創建的聊天機器人消息存儲模塊,它實現了文生文(文本生成文本)的核心邏輯。
1.Pinia狀態管理
這個模塊管理兩個主要狀態:
messages
:存儲所有聊天歷史記錄,包括用戶消息和 AI 回復receiveText
:臨時存儲 AI 流式返回的文本內容
主要包含兩個核心方法:
startSending
:處理用戶消息發送邏輯handleText
:處理 AI 模型返回的流式響應
messages
?數組中的每個元素都是一個對象,具有以下結構:
2.消息流轉過程
-
用戶發送消息
- 用戶輸入文本后,
startSending
?方法會創建一個用戶消息對象并添加到?messages
?數組 - 同時預添加一個空的 AI 回復對象,初始狀態為?
"start"
- 用戶輸入文本后,
? ? ? ? ? ? ? ? ??
-
AI 流式響應
- 每次接收到新的文本片段時,
handleText
?方法會更新 AI 回復對象 finish_reason
?會從?"start"
?變為?"respond"
,表示正在生成回復content
?字段會逐步追加新的文本片段
- 每次接收到新的文本片段時,
? ? ? ? ? ? ? ? ?
?這里receiveText一點一點追加大模型返回的文本片段,然后賦值給大模型消息對象的content字段
-
回復完成
- 當 AI 完成回復時,
finish_reason
?會被設置為最終狀態(如?"stop"
) - 如果有網絡搜索結果,會添加到?
web_search
?字段 - 最后將最新的兩條消息(用戶 + AI)保存到服務器
- 當 AI 完成回復時,
? ? ? ? ? ? ? ??
- 在流式響應過程中,每個數據塊(token)會依次返回,此時?
finish_reason
?字段通常是?null
?或?undefined
- 當回復完成時,最后一個數據塊會包含?
finish_reason
?字段,指示回復是如何結束的
? ? ?
以下是完整的message數組示例:
[{"role": "user","content": "推薦幾部科幻電影"},{"role": "assistant","content": "以下是幾部值得一看的科幻電影:1.《星際穿越》2.《盜夢空間》3.《2001太空漫游》","finish_reason": "stop","web_search": [{"title": "豆瓣科幻電影Top10","url": "https://movie.douban.com/chart","snippet": "豆瓣評分最高的科幻電影排行榜..."}]},{"role": "user","content": "《星際穿越》的導演是誰?"},{"role": "assistant","content": "《星際穿越》是由克里斯托弗·諾蘭執導的。","finish_reason": "stop","web_search": []}
]
?3.HTTP 請求封裝中的流式數據處理
- 使用
onChunkReceived
監聽流式數據 - 將二進制數據轉換為字符串并處理編碼
- 實現緩沖區機制,按行解析 SSE (Server-Sent Events) 格式數據
- 過濾有效數據塊并傳遞給
chatbotMessage
存儲模塊處理
requestTask.onChunkReceived(response=>{// 將ArrayBuffer轉換為字符串let arrayBuffer = response.dataconst arrayBufferss = new Uint8Array(arrayBuffer)let string = ''for(let i = 0; i < arrayBufferss.length; i++){string += String.fromCharCode(arrayBufferss[i])}// 處理編碼并追加到緩沖區buffer += decodeURIComponent(escape(string))// 按行解析數據while(buffer.includes('\n')){const index = buffer.indexOf('\n')const chunk = buffer.slice(0,index)buffer = buffer.slice(index + 1)// 處理SSE格式數據if(chunk.startsWith('data: ') && !chunk.includes('[DONE]')){const jsonData = JSON.parse(chunk.replace('data: ',''))chatbotMessage().handleText(jsonData)}}
})
大模型返回的SSE數據格式
4.實時UI渲染
<view class="zhipu-message" v-if="item.role === 'assistant'"><towxml :nodes="appContext.$towxml(item.content,'markdown')"></towxml><!-- 加載動畫 --><loadingVue v-if="item.finish_reason == 'start'"></loadingVue><!-- 網絡搜索結果 -->
</view>
item.content
?是當前 AI 回復的內容towxml
?組件將 Markdown 格式的文本渲染為富文本- 每當
item.content
更新時,towxml
會重新渲染,顯示最新內容
(2)后端部分
async createCompletions(ctx) {const { messages } = ctx.request.body;await Validate.isarrayCheck(messages, "缺少對話信息", "messages");
- 從請求體中獲取對話歷史
messages
- 使用
Validate.isarrayCheck
驗證messages
是否為數組 - 如果驗證失敗,會拋出錯誤并返回相應的錯誤信息
const data = await ai.createCompletions({model: "glm-4-0520",messages,stream: true,tools: [{type: "web_search",web_search: {enable: true,search_result: true,},},],
});
- 調用 AI 模型的
createCompletions
方法生成回復 - 指定使用
glm-4-0520
模型 - 傳遞完整的對話歷史
messages
- 設置
stream: true
啟用流式響應 - 啟用網絡搜索工具,允許模型在生成回答時參考實時網絡信息
ctx.status = 200;
for await (const chunk of data) {console.log(chunk.toString());ctx.res.write(chunk);
}
- 設置 HTTP 狀態碼為 200(成功)
- 使用
for await...of
遍歷異步可迭代對象data
- 每次迭代獲取模型生成的一個數據塊(可能是一個單詞、一個句子片段等)
- 通過
ctx.res.write(chunk)
將數據塊實時寫入 HTTP 響應- 這些數據會立即傳輸到前端,而不需要等待整個回復完成
與前端的配合
前端代碼(之前分析過的)會這樣處理這個流式響應:
- 接收二進制數據塊并轉換為文本
- 按行解析 SSE 格式的數據
- 提取 JSON 對象并更新聊天界面
- 隨著新數據的到來,文本會逐字顯示在界面上
(3)SSE通信
? ? ? ? ?不同類型的大模型應用,對網絡通信的需求不盡相同,但幾乎都離不開以下需求。
? ? ? ? ?具體就是:
- 1)實時對話:用戶與模型進行連續交互,模型需要即時響應。例如通義千問,HIgress 官網的答疑機器人,都是需要依據客戶問題,即時做出響應;
- 2)流式輸出:大模型生成內容時,逐字或逐句返回結果,而不是一次性返回。但是釘釘、微信等應用,兩個人相互對話時,采用的就不是流式輸出了,文字等內容都是一次性返回的;
- 3)長時任務處理:大模型可能需要較長時間處理復雜任務,同時需要向客戶端反饋進度,尤其是處理長文本、以及圖片、視頻等多模態內容;這是因為依賴大模型計算的響應,要比依賴人為寫入的業務邏輯的響應,消耗的資源多的多,這也是為什么大模型的計算要依靠 GPU,而非 CPU,CPU 在并行計算和大規模矩陣計算上遠不如 GPU;
- 4)多輪交互:用戶與模型之間需要多次往返交互,保持上下文。這是大模型應用保障用戶體驗的必備能力。
? ? ? ?這些場景對實時性和雙向通信有較高要求,沿用 Web 類應用的主流通信協議 - HTTPS,將會? ?存在很多問題。
? ? ?以下是主要的問題:
- 1)僅支持單向通信,即請求-響應模型,必須是客戶端發起時,服務端才能做出響應,無法進行雙向通信,導致無法支持流式輸出,無法處理長時任務;
- 2)客戶端每次發出請求都需要重新建立連接,延遲增加,導致無法支持實時對話;
- 3)HTTPS 是一種無狀態的通信協議,每次請求都是獨立的,服務端不會保存客戶端的狀態,即便客戶端可以在每次請求時重復發送上下文信息,但會帶來額外的網絡開銷,導致無法高效的支持多輪交互場景。
? ? ? 雖然 HTTPS 已經發展到 HTTPS/2 和 HTTPS/3,在性能上了有了提升,但是面對大模型應用這類對實時性要求較高的場景,依舊不夠原生,并未成為這類場景下的主流通信協議。
二、實時語音功能
(1)前端部分
1. 初始化與準備工作
-
引入必要的模塊和變量:在input-box.vue文件中,引入了阿里云相關的請求接口
aliToken
、aliyunUrl
、appKey
,以及自定義的語音識別類SpeechTranscription
。
import {aliToken,aliyunUrl,appKey} from '@/api/request.js'
import {SpeechTranscription} from '@/voice/st.js'
-
獲取 Token:在主頁面加載時,調用
aliToken
接口獲取阿里云語音識別所需的 Token。 -
實例化語音識別對象:使用獲取到的 Token、URL 和 AppKey 實例化
SpeechTranscription
對象,并存儲在launckVoice
變量中。
onLoad(async()=>{const token = await aliToken()const st = new SpeechTranscription({url:aliyunUrl,token:token.data,appkey:appKey})launckVoice.value = st
})
2. 開始語音錄制與識別
-
長按開始說話:用戶長按 “按住 說話” 按鈕,觸發
longpress
方法。 -
檢查當前是否有正在進行的對話,如果有則返回。
-
顯示語音錄制區域。
-
調用
launckVoice.value.start
方法開始語音識別,并傳入默認的開始參數。 -
調用
recorderManager.start
方法開始錄音。
const longpress = async()=>{if(inProgress().queryValue())return falseshowAudio.value = trueawait launckVoice.value.start(launckVoice.value.defaultStartParams())recorderManager.start({duration:100000,sampleRate:16000,numberOfChannels:1,format:'PCM',frameSize:4})
}
-
實時輸出錄音:使用
recorderManager.onFrameRecorded
監聽錄音的每一幀數據,并將其發送給阿里云語音識別服務。
recorderManager.onFrameRecorded(res=>{launckVoice.value.sendAudio(res.frameBuffer)
})
3. 處理語音識別結果
-
監聽語音識別事件:在
SpeechTranscription(實例化的阿里云語音對象)
對象上監聽多個事件,包括開始、中間結果、句子結束、關閉和錯誤。
// 實時語音識別開始。
st.on("started",()=>{console.log('實時語音識別開始');
})
// 實時語音識別中間結果。
st.on("changed",msg=>{console.log('實時語音識別中間結果');console.log(msg);const res = JSON.parse(msg)const queryIndex = storageArr.value.findIndex(item=>item.index === res.payload.index)if(queryIndex >= 0){storageArr.value[queryIndex].result = res.payload.result}else{storageArr.value.push(res.payload)}
})
// 提示句子結束。
st.on("end",msg=>{console.log('提示句子結束');console.log(msg);const res = JSON.parse(msg)const queryIndex = storageArr.value.findIndex(item=>item.index === res.payload.index)if(queryIndex >= 0){storageArr.value[queryIndex].result = res.payload.result}else{storageArr.value.push(res.payload)}
})
// 連接關閉。
st.on("closed",()=>{console.log('連接關閉');
})
// 錯誤。
st.on("failed",(err)=>{console.log('阿里云語音識別錯誤');console.log(err);uni.showToast({icon:"none",title:'錄音出現錯誤'})
})
4. 結束語音錄制與識別
-
手指放開停止錄音:用戶放開手指,觸發
touchend
方法。
const touchend = ()=>{showAudio.value = falserecorderManager.stop()
}
-
隱藏語音錄制區域。
-
調用
recorderManager.stop
方法停止錄音。 -
處理錄音結束事件:使用
recorderManager.onStop
監聽錄音結束事件,強制關閉阿里云語音識別監聽,并將識別結果拼接成字符串存儲在inputContent
中。
recorderManager.onStop(res=>{console.log('錄音結束了');console.log(res);showAudio.value = false// 強制關閉阿里云語音識別監聽launckVoice.value.shutdown()// 錄制結束取出文字發送大模型if(storageArr.value.length > 0){storageArr.value.forEach(item=>{inputContent.value += item.result})}
})
5. 實際數據流轉示例
1.?changed
?事件(中間結果)
阿里云返回的json數據格式
{"payload": {"index": 1, // 當前句子的唯一索引(同一輪錄音中的句子編號)"result": "你好,", // 中間識別結果(可能后續會補充)"status": "partial" // 標識為中間結果}
}
st.on("changed", msg => {const res = JSON.parse(msg); // 解析JSON數據const { index, result } = res.payload; // 提取索引和文本// 查找是否已存在相同index的結果塊const queryIndex = storageArr.value.findIndex(item => item.index === index);if (queryIndex >= 0) {// **存在已記錄的塊**:更新該塊的文本(中間結果可能逐次補充)storageArr.value[queryIndex].result = result;} else {// **不存在記錄**:新增一個結果塊(處理可能的亂序返回)storageArr.value.push({ index, result });}
});
changed: { index: 1, result: "今天" }
changed: { index: 1, result: "今天天氣" }
changed: { index: 1, result: "今天天氣怎么樣" }
end: { index: 1, result: "今天天氣怎么樣" } // 第一句結束changed: { index: 2, result: "明天" }
changed: { index: 2, result: "明天有什么" }
changed: { index: 2, result: "明天有什么安排" }
end: { index: 2, result: "明天有什么安排" } // 第二句結束
這段數組邏輯是這樣的:先查找數組中有沒有存在的和阿里云返回結果的index相同的index,有的話說明是同一個片段句子,覆蓋就行,沒有的話說明是新句子,重新push一個對象進數組
(例如返回來的index為1,數組已經存在index為1的對象,則覆蓋;如果返回來的index是2,數組不存在index為2的對象,則新增一個index=2的對象去存儲)
關鍵點:
- 增量更新:每次返回的中間結果會覆蓋前一次的結果。
- 按索引管理:通過?
index
?區分不同的句子(若用戶連續說多句話)。 - 實時展示:可用于實現 “邊說邊顯示” 的效果(如語音輸入法的逐字顯示)。
2.end
?事件(最終結果)
當檢測到語音停頓(用戶停止說話),阿里云返回完整的最終識別結果。
st.on("end", msg => {const res = JSON.parse(msg);const queryIndex = storageArr.value.findIndex(item => item.index === res.payload.index);if (queryIndex >= 0) {// 更新已有結果塊(將中間結果替換為最終結果)storageArr.value[queryIndex].result = res.payload.result;} else {// 添加新結果塊(理論上不會觸發,因為end事件前必有changed事件)storageArr.value.push(res.payload);}
});
關鍵點:
- 最終確認:
end
?事件的結果比?changed
?事件更準確(經過模型后處理優化)。 - 句子邊界:一個?
end
?事件表示一個完整句子的結束。 - 結果固化:最終結果不會再被覆蓋,可直接用于后續處理。
總的來說,on事件用于實時更新識別出來的文本數據,end事件用來處理最后識別結果,糾正一些on事件中的諧音錯誤
用戶按下按鈕
↓
recorderManager.start() 開始錄音
↓
每采集一幀音頻數據↓recorderManager.onFrameRecorded() 觸發↓launckVoice.value.sendAudio(res.frameBuffer) 發送數據到阿里云↓阿里云處理數據并返回識別結果↓st.on("changed") 或 st.on("end") 觸發↓將識別結果存入 storageArr
↓
用戶松開按鈕
↓
recorderManager.onStop() 觸發↓launckVoice.value.shutdown() 關閉連接↓拼接 storageArr 中的所有結果↓sendIng() 將文本發送給大模型
(2)后端部分
class VoiceController {async aliToken(ctx) {// 檢查Redis緩存中是否已有Tokenconst alitoken = await ctx.redis.get("aliToken")if (alitoken) {ctx.send(alitoken)return false}// 調用阿里云API生成新Tokenconst result = await client.request('CreateToken')console.log(result)// 處理返回結果并緩存if (result.Token && result.Token.Id) {// 計算Token過期時間const expires_in = result.Token.ExpireTime - dayjs().unix()// 緩存到Redis并設置過期時間await ctx.redis.set('aliToken', result.Token.Id, 'EX', expires_in)ctx.send(result.Token.Id)} else {ctx.send(null, 500, "獲取阿里云token失敗", result)}}
}
- 緩存優先策略:首先檢查 Redis 中是否有緩存的 Token,如果有則直接返回
- Token 生成:調用
CreateToken
接口生成新 Token - 緩存處理:將 Token 存入 Redis 并設置與阿里云一致的過期時間,避免頻繁調用 API
三、用戶登錄
(1)前端部分
1. 獲取登陸碼
uni.login({success:async(res)=>{await userData().isNotLoggedIn(userInfo.nickname,fileurl,res.code)loading.value = false}})
? ?使用?uni.login
?方法獲取用戶的登錄碼?code
關于code的一些說明:
當用戶退出登錄后再次登錄,后端服務器使用新的?code
?向微信服務器換取?openid
?時,獲取到的仍然是之前的?openid
- 避免敏感信息泄露:
code
?是臨時且一次性有效的,它不是用戶的敏感信息(如?openid
?、用戶的真實身份信息等)。在小程序前端向微信服務器請求登錄時,微信服務器會返回一個?code
?,小程序前端再把這個?code
?發送給開發者的后端服務器。后端服務器拿著這個?code
?去微信服務器換取用戶的?openid
?等敏感信息。如此一來,用戶的敏感信息就不會直接在前端暴露,減少了信息泄露的風險。 - 防止惡意攻擊:由于?
code
?有有效期,而且只能使用一次,這就增加了攻擊者利用?code
?進行惡意操作的難度。就算攻擊者截獲了?code
?,在其過期之后也就無法再使用了。
2. 調用后端wxLogin接口
// 未登錄獲取用戶信息async isNotLoggedIn(nickName, avatar, code){// 請求接口const result = await wxLogin({nickName, avatar, code})// console.log(result);// 存儲本地緩存uni.setStorageSync('userInfo',result.data)this.userInfo = result.data// 請求聊天列表const chatListData = await userChatList()this.chatList = chatListData.datathis.isLogin = true}
調用后端接口,將用戶昵稱、頭像、獲取到的code傳給后端,后端返回結果,將其保存在本地緩存,其中后端返回的結果如下:
{"data": {"token": "生成的 JWT 令牌","nickName": "用戶昵稱","avatar": "用戶頭像地址"},"msg": "SUCCESS","error": null,"serviceCode": 200
}
(2)后端部分
class UserController {//用戶登錄async wxLogin(ctx) {const { nickName, avatar, code } = ctx.request.bodyawait Validate.nullCheck(nickName, '請輸入昵稱', 'name')await Validate.nullCheck(avatar, '請上傳頭像', 'avatar')await Validate.nullCheck(code, '缺少code', 'avatar')//獲取openidconst openid = await new UserService().getOpenid(code)//查詢數據庫是否已存在用戶信息// console.log(nickName, avatar, openid)const userInfo = await User.findOne({ where: { openid } })if (!userInfo) {await User.create({ nickName, avatar, openid })}ctx.send({ token: generateToken(openid), nickName, avatar })}
}
將code換為openid的service部分:
const { appid, secret, code2session } = require("@/config/default").weixin
const qs = require("querystring")
const axios = require("axios")
class UserService {// 獲取openidasync getOpenid(code) {const query = qs.stringify({appid,secret,js_code: code,grant_type: "authorization_code"})const res = await axios.get(`${code2session}?${query}`)console.log(res)if (res.data.errcode) {throw { msg: "獲取code出錯", code: 400, error: res.data }} else {return res.data.openid}}
}
- 用途:實現微信登錄流程中的「code 換取 openid」環節,這是微信小程序 / 公眾號登錄的核心步驟。
- 技術棧:
- 使用?
axios
?發起 HTTP 請求,調用微信官方接口。 - 通過?
querystring
?處理 URL 查詢參數。 - 配置信息(如?
appid
、secret
)從項目配置文件中讀取。
- 使用?
?微信登錄流程關聯
這段代碼是微信登錄流程中的關鍵環節,整體流程如下:
- 前端獲取?
code
:
前端調用微信登錄接口(如小程序的?wx.login
),獲取臨時登錄憑證?code
,并傳遞給后端。 - 后端換取?
openid
:
后端通過?code
、appid
、secret
?向微信服務器發起請求,驗證?code
?的有效性并獲取?openid
(用戶在微信體系內的唯一標識)。 - 業務邏輯處理:
后端使用?openid
?進行用戶注冊 / 登錄(如查詢數據庫是否存在該用戶),并生成自定義令牌(如 JWT)返回給前端。
四、用戶鑒權
生成 JWT Token
在wxLogin
方法中,用戶登錄時會獲取openid
,然后后端調用generateToken
方法生成 JWT Token,并將其返回給前端。
存儲 Token
前端收到 Token 后,將其存儲到本地緩存中。在后續的請求中,前端需要在請求頭中攜帶這個 Token。
驗證 JWT Token
在每個需要鑒權的接口中,會對請求頭中的 Token 進行驗證。
const basicAuth = require("basic-auth");
var jwt = require("jsonwebtoken");
const { secretkey } = require("./default").userToken;const authority = async (ctx, next) => {const token = basicAuth(ctx.req);if (!token || !token.name) {throw { msg: "沒有登陸,沒有訪問權限", code: 401 };}try {var authcode = jwt.verify(token.name, secretkey); //解密token為openid} catch (error) {if (error.name == "TokenExpiredError") {throw { msg: "登錄過期,重新登陸", code: 401 };}throw { msg: "沒有訪問權限", code: 401 };}ctx.auth = {uid: authcode.uid,};await next();
};module.exports = authority;
在authority
中間件中,首先從請求頭中獲取 Token,然后使用jwt.verify
方法對 Token 進行驗證。如果驗證通過,將openid
存儲在ctx.auth
中,并繼續執行后續的中間件或路由處理函數;如果驗證失敗,拋出相應的錯誤信息。
應用鑒權中間件
在路由中,為需要鑒權的接口應用authority
中間件。
?jwt.verify
?是如何驗證 Token 的有效性的?
1. 生成 Token
首先,假設我們要為一個用戶生成一個 Token,該用戶的?openid
?為?"user123"
。在?AIGC - backend/config/jwt.js
?中,生成 Token 的代碼如下:
const jwt = require('jsonwebtoken');
const { secretkey, expiresIn } = require('./default').userToken;// 生成token
function generateToken(uid) {return jwt.sign({ uid }, secretkey, { expiresIn }); //uid是openid,密鑰是自己定義的
}// 示例:生成一個openid為user123的token
const openid = "user123";
const token = generateToken(openid);
console.log("生成的Token:", token);
假設?secretkey
?為?"mysecretkey"
,expiresIn
?為?"1h"
(表示 Token 在 1 小時后過期),生成的 Token 可能如下(實際生成的 Token 會有所不同):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiJ1c2Vy123IiwiaWF0IjoxNjk4NjM2MDAwLCJleHAiOjE2OTg2Mzk2MDB9.abcdef1234567890
這個 Token 由三部分組成,用點號?.
?分隔:
- 頭部(Header):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
,它通常包含了令牌的類型(JWT)和使用的簽名算法(這里是 HMAC SHA256,即?HS256
)。 - 負載(Payload):
eyJ1aWQiOiJ1c2Vy123IiwiaWF0IjoxNjk4NjM2MDAwLCJleHAiOjE2OTg2Mzk2MDB9
,包含了我們存儲的用戶?openid
(uid
)、簽發時間(iat
)和過期時間(exp
)。 - 簽名(Signature):
abcdef1234567890
,用于驗證消息在傳輸過程中沒有被更改。
2. 驗證 Token
當用戶發起請求時,服務器會從請求頭中獲取 Token,并使用?jwt.verify
?方法進行驗證。在?AIGC - backend/config/auth.js
?中,驗證 Token 的代碼如下:
const basicAuth = require("basic-auth");
var jwt = require("jsonwebtoken");
const { secretkey } = require("./default").userToken;const authority = async (ctx, next) => {const token = basicAuth(ctx.req);if (!token || !token.name) {throw { msg: "沒有登陸,沒有訪問權限", code: 401 };}try {var authcode = jwt.verify(token.name, secretkey); //解密token為openid} catch (error) {if (error.name == "TokenExpiredError") {throw { msg: "登錄過期,重新登陸", code: 401 };}throw { msg: "沒有訪問權限", code: 401 };}ctx.auth = {uid: authcode.uid,};await next();
};module.exports = authority;
- 獲取 Token:假設客戶端在請求頭中攜帶了上面生成的 Token,服務器通過?
basicAuth(ctx.req)
?獲取到該 Token。 - 簽名驗證:
jwt.verify
?方法會根據 Token 的頭部指定的簽名算法(HS256
),使用相同的密鑰(mysecretkey
)重新計算簽名。具體步驟如下:- 將頭部和負載部分用點號?
.
?連接起來,得到一個字符串。 - 使用?
HS256
?算法和?mysecretkey
?對這個字符串進行簽名。 - 將計算得到的簽名與 Token 中的簽名部分進行比對。如果兩者一致,說明 Token 沒有被篡改。
- 將頭部和負載部分用點號?
- 過期時間驗證:
jwt.verify
?方法會檢查當前時間是否超過了 Token 的過期時間。假設當前時間是?1698638000
,而 Token 的過期時間是?1698639600
,當前時間小于過期時間,說明 Token 未過期。 - 返回結果:如果簽名驗證和過期時間驗證都通過,
jwt.verify
?方法會返回 Token 中包含的信息,即:
{"uid": "user123","iat": 1698636000,"exp": 1698639600
}
服務器把?openid
?存儲在?ctx.auth
?中,在后續存儲和獲取聊天對應用戶聊天記錄起到關鍵作用