feat:用户6位编号
This commit is contained in:
1 parent
cf96d48a38
commit
3184608d99
9 files changed
+204
-139
No files matched your search
+25
-23
@@ -2,10 +2,10 @@
|
||||
import { mapState } from 'pinia';
|
||||
import { onLaunch, onShow, onError } from '@dcloudio/uni-app';
|
||||
import { ShoproInit } from './sheep';
|
||||
import http from './common/request';
|
||||
import * as msgType from './common/messageType';
|
||||
import * as enums from './common/enums';
|
||||
import * as wsApi from './common/wssocket';
|
||||
import chatTokenManager from './common/chat-token-manager';
|
||||
import CHAT_ENV from '@/chat-env.js';
|
||||
|
||||
import useChatStore from '@/chat-store/chatStore.js';
|
||||
@@ -64,6 +64,7 @@ export default {
|
||||
},
|
||||
// 初始化聊天
|
||||
async init() {
|
||||
this.startChatTokenCheck();
|
||||
this.reconnecting = false;
|
||||
this.isExit = false;
|
||||
try {
|
||||
@@ -431,6 +432,7 @@ export default {
|
||||
exit() {
|
||||
console.log('exit');
|
||||
this.isExit = true;
|
||||
chatTokenManager.stop();
|
||||
wsApi.close(3099);
|
||||
uni.removeStorageSync('loginInfo');
|
||||
uni.reLaunch({
|
||||
@@ -444,27 +446,15 @@ export default {
|
||||
// this.audioTip.src = "/static/audio/tip.wav";
|
||||
// this.audioTip.play();
|
||||
},
|
||||
// 刷新token
|
||||
refreshToken(loginInfo) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!loginInfo || !loginInfo.refreshToken) {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
http({
|
||||
url: '/refreshToken',
|
||||
method: 'PUT',
|
||||
header: {
|
||||
refreshToken: loginInfo.refreshToken,
|
||||
},
|
||||
})
|
||||
.then((newLoginInfo) => {
|
||||
uni.setStorageSync('loginInfo', newLoginInfo);
|
||||
resolve();
|
||||
})
|
||||
.catch((e) => {
|
||||
reject(e);
|
||||
});
|
||||
startChatTokenCheck() {
|
||||
// 轮询只处理聊天 token;仅凭证明确失效时退出,网络错误等待下次检查。
|
||||
chatTokenManager.start({
|
||||
onRefreshError: (error) => {
|
||||
const code = error?.code ?? error?.data?.code;
|
||||
if (code == 400 || code == 401) {
|
||||
this.exit();
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
reconnectWs() {
|
||||
@@ -542,7 +532,9 @@ export default {
|
||||
this.closeSplashscreen(0);
|
||||
return;
|
||||
}
|
||||
this.refreshToken(loginInfo)
|
||||
this.startChatTokenCheck();
|
||||
chatTokenManager
|
||||
.refreshAccessToken(loginInfo)
|
||||
.then(() => {
|
||||
// 聊天初始化
|
||||
this.init();
|
||||
@@ -559,6 +551,10 @@ export default {
|
||||
// console.log('onError', error);
|
||||
// },
|
||||
onShow() {
|
||||
// #ifndef MP-WEIXIN
|
||||
// H5 页面重新可见或 App 回到前台时,立即检查并重新建立轮询。
|
||||
this.startChatTokenCheck();
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
// 获取urlSchemes参数
|
||||
const args = plus.runtime.arguments;
|
||||
@@ -570,6 +566,12 @@ export default {
|
||||
});
|
||||
// #endif
|
||||
},
|
||||
onHide() {
|
||||
// #ifndef MP-WEIXIN
|
||||
// 进入后台后停止轮询,避免无效请求和重复定时器。
|
||||
chatTokenManager.stop();
|
||||
// #endif
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import CHAT_ENV from '@/chat-env.js';
|
||||
|
||||
const TOKEN_REFRESH_THRESHOLD_SECONDS = 5 * 60;
|
||||
const TOKEN_CHECK_INTERVAL_MS = 30 * 1000;
|
||||
|
||||
// 统一管理聊天 token 的定时检查和刷新,供请求、上传及 App 生命周期共同复用。
|
||||
class ChatTokenManager {
|
||||
constructor() {
|
||||
this.timer = null;
|
||||
// 所有调用共享同一个刷新 Promise,避免并发刷新后 token 相互覆盖。
|
||||
this.refreshPromise = null;
|
||||
this.onRefreshError = null;
|
||||
// 用于忽略定时器停止后才返回的旧检查结果。
|
||||
this.monitorVersion = 0;
|
||||
}
|
||||
|
||||
// 启动时立即检查一次,之后每 30 秒检查一次;重复调用不会创建多个定时器。
|
||||
start(options = {}) {
|
||||
this.onRefreshError = options.onRefreshError || null;
|
||||
|
||||
const loginInfo = uni.getStorageSync('loginInfo');
|
||||
if (!loginInfo?.refreshToken) {
|
||||
this.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.timer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const monitorVersion = ++this.monitorVersion;
|
||||
this.runCheck(monitorVersion);
|
||||
this.timer = setInterval(() => {
|
||||
this.runCheck(monitorVersion);
|
||||
}, TOKEN_CHECK_INTERVAL_MS);
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.monitorVersion += 1;
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
this.onRefreshError = null;
|
||||
}
|
||||
|
||||
runCheck(monitorVersion = this.monitorVersion) {
|
||||
this.checkAccessToken().catch((error) => {
|
||||
if (monitorVersion !== this.monitorVersion) {
|
||||
return;
|
||||
}
|
||||
console.warn('定时检查聊天 token 失败', error);
|
||||
this.onRefreshError?.(error);
|
||||
});
|
||||
}
|
||||
|
||||
checkAccessToken() {
|
||||
const loginInfo = uni.getStorageSync('loginInfo');
|
||||
if (!loginInfo?.refreshToken) {
|
||||
this.stop();
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
// accessToken 缺失或即将过期时才请求刷新,正常状态不产生网络请求。
|
||||
if (!loginInfo.accessToken || this.isAccessTokenExpiringSoon(loginInfo.accessToken)) {
|
||||
return this.refreshAccessToken(loginInfo);
|
||||
}
|
||||
|
||||
return Promise.resolve(loginInfo);
|
||||
}
|
||||
|
||||
isAccessTokenExpiringSoon(accessToken) {
|
||||
if (!accessToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// JWT 第二段是 payload,H5 和 App 均通过 uni API 完成 Base64 解码。
|
||||
const payload = accessToken.split('.')[1];
|
||||
if (!payload) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const paddedBase64 = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=');
|
||||
const bytes = new Uint8Array(uni.base64ToArrayBuffer(paddedBase64));
|
||||
let decodedPayload = '';
|
||||
bytes.forEach((byte) => {
|
||||
decodedPayload += String.fromCharCode(byte);
|
||||
});
|
||||
|
||||
const expMatch = decodedPayload.match(/"exp"\s*:\s*(\d+)/);
|
||||
if (!expMatch) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const remainingSeconds = Number(expMatch[1]) - Math.floor(Date.now() / 1000);
|
||||
return remainingSeconds <= TOKEN_REFRESH_THRESHOLD_SECONDS;
|
||||
} catch (error) {
|
||||
console.warn('解析聊天 accessToken 过期时间失败', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
refreshAccessToken(loginInfo = uni.getStorageSync('loginInfo')) {
|
||||
if (!loginInfo?.refreshToken) {
|
||||
return Promise.reject(new Error('缺少聊天 refreshToken'));
|
||||
}
|
||||
|
||||
if (this.refreshPromise) {
|
||||
return this.refreshPromise;
|
||||
}
|
||||
|
||||
const refreshToken = loginInfo.refreshToken;
|
||||
this.refreshPromise = this.requestRefreshToken(refreshToken)
|
||||
.then((res) => {
|
||||
if (!res || res.data?.code != 200) {
|
||||
throw res?.data || new Error('刷新聊天 token 失败');
|
||||
}
|
||||
|
||||
// 刷新期间用户可能退出或重新登录,旧请求结果不能覆盖新的登录状态。
|
||||
const currentLoginInfo = uni.getStorageSync('loginInfo');
|
||||
if (currentLoginInfo?.refreshToken !== refreshToken) {
|
||||
if (currentLoginInfo?.accessToken) {
|
||||
return currentLoginInfo;
|
||||
}
|
||||
throw new Error('聊天登录状态已变化,忽略本次 token 刷新结果');
|
||||
}
|
||||
|
||||
const newLoginInfo = res.data.data;
|
||||
uni.setStorageSync('loginInfo', newLoginInfo);
|
||||
return newLoginInfo;
|
||||
})
|
||||
.finally(() => {
|
||||
this.refreshPromise = null;
|
||||
});
|
||||
|
||||
return this.refreshPromise;
|
||||
}
|
||||
|
||||
requestRefreshToken(refreshToken) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
method: 'PUT',
|
||||
url: CHAT_ENV.BASE_URL + '/refreshToken',
|
||||
header: {
|
||||
refreshToken,
|
||||
},
|
||||
timeout: 10000,
|
||||
success: resolve,
|
||||
fail: reject,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { ChatTokenManager, TOKEN_CHECK_INTERVAL_MS, TOKEN_REFRESH_THRESHOLD_SECONDS };
|
||||
|
||||
export default new ChatTokenManager();
|
||||
+5
-86
@@ -1,11 +1,5 @@
|
||||
import CHAT_ENV from '@/chat-env.js';
|
||||
|
||||
const TOKEN_REFRESH_THRESHOLD_SECONDS = 5 * 60;
|
||||
|
||||
// 等待同一次 token 刷新结果的请求队列
|
||||
let refreshWaitList = [];
|
||||
// 是否正在刷新中
|
||||
let isRefreshToken = false;
|
||||
import chatTokenManager from './chat-token-manager';
|
||||
|
||||
const request = (options) => {
|
||||
const header = options.header || {};
|
||||
@@ -23,9 +17,10 @@ const request = (options) => {
|
||||
async success(res) {
|
||||
if (res.data.code == 200) {
|
||||
const currentLoginInfo = uni.getStorageSync('loginInfo');
|
||||
if (isAccessTokenExpiringSoon(currentLoginInfo?.accessToken)) {
|
||||
// 与全局轮询共享刷新锁,避免请求成功检查和定时检查同时刷新。
|
||||
if (chatTokenManager.isAccessTokenExpiringSoon(currentLoginInfo?.accessToken)) {
|
||||
try {
|
||||
await refreshAccessToken(currentLoginInfo);
|
||||
await chatTokenManager.refreshAccessToken(currentLoginInfo);
|
||||
} catch (error) {
|
||||
console.log('刷新token失败', error);
|
||||
getApp().$vm.exit();
|
||||
@@ -49,7 +44,7 @@ const request = (options) => {
|
||||
}
|
||||
|
||||
try {
|
||||
await refreshAccessToken(currentLoginInfo);
|
||||
await chatTokenManager.refreshAccessToken(currentLoginInfo);
|
||||
// 重新发送刚才的请求
|
||||
return resolve(request(options));
|
||||
} catch (error) {
|
||||
@@ -78,80 +73,4 @@ const request = (options) => {
|
||||
});
|
||||
};
|
||||
|
||||
const isAccessTokenExpiringSoon = (accessToken) => {
|
||||
if (!accessToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = accessToken.split('.')[1];
|
||||
const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const paddedBase64 = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=');
|
||||
const bytes = new Uint8Array(uni.base64ToArrayBuffer(paddedBase64));
|
||||
let decodedPayload = '';
|
||||
bytes.forEach((byte) => {
|
||||
decodedPayload += String.fromCharCode(byte);
|
||||
});
|
||||
const expMatch = decodedPayload.match(/"exp"\s*:\s*(\d+)/);
|
||||
if (!expMatch) {
|
||||
return false;
|
||||
}
|
||||
const remainingSeconds = Number(expMatch[1]) - Math.floor(Date.now() / 1000);
|
||||
console.log("remainingSeconds:", remainingSeconds);
|
||||
|
||||
return remainingSeconds <= TOKEN_REFRESH_THRESHOLD_SECONDS;
|
||||
} catch (error) {
|
||||
console.warn('解析 accessToken 过期时间失败', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const refreshAccessToken = (loginInfo) => {
|
||||
if (!loginInfo?.refreshToken) {
|
||||
return Promise.reject(new Error('缺少 refreshToken'));
|
||||
}
|
||||
if (isRefreshToken) {
|
||||
return new Promise((resolve, reject) => {
|
||||
refreshWaitList.push({ resolve, reject });
|
||||
});
|
||||
}
|
||||
isRefreshToken = true;
|
||||
return reqRefreshToken(loginInfo)
|
||||
.then((res) => {
|
||||
if (!res || res.data.code != 200) {
|
||||
throw res?.data || new Error('刷新token失败');
|
||||
}
|
||||
const newInfo = res.data.data;
|
||||
uni.setStorageSync('loginInfo', newInfo);
|
||||
refreshWaitList.forEach(({ resolve }) => resolve(newInfo));
|
||||
return newInfo;
|
||||
})
|
||||
.catch((error) => {
|
||||
refreshWaitList.forEach(({ reject }) => reject(error));
|
||||
throw error;
|
||||
})
|
||||
.finally(() => {
|
||||
refreshWaitList = [];
|
||||
isRefreshToken = false;
|
||||
});
|
||||
};
|
||||
|
||||
const reqRefreshToken = (loginInfo) => {
|
||||
return new Promise(function (resolve, reject) {
|
||||
uni.request({
|
||||
method: 'PUT',
|
||||
url: CHAT_ENV.BASE_URL + '/refreshToken',
|
||||
header: {
|
||||
refreshToken: loginInfo.refreshToken,
|
||||
},
|
||||
success: (res) => {
|
||||
resolve(res);
|
||||
},
|
||||
fail: (res) => {
|
||||
reject(res);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default request;
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
<script>
|
||||
import CHAT_ENV from '@/chat-env.js';
|
||||
import chatTokenManager from '@/common/chat-token-manager';
|
||||
|
||||
export default {
|
||||
name: 'file-upload',
|
||||
@@ -67,11 +68,11 @@
|
||||
// 上传成功
|
||||
this.onOk(file, res);
|
||||
} else if (res.code == 401) {
|
||||
// token已过期,重新获取token
|
||||
this.refreshToken()
|
||||
.then((res) => {
|
||||
let newToken = res.data.accessToken;
|
||||
this.option.header.accessToken = newToken;
|
||||
// 上传绕过了 request.js,因此也复用统一刷新锁后重试。
|
||||
chatTokenManager
|
||||
.refreshAccessToken()
|
||||
.then((newLoginInfo) => {
|
||||
this.option.header.accessToken = newLoginInfo.accessToken;
|
||||
this.$refs.lsjUpload.setData(this.option);
|
||||
// 重新上传
|
||||
this.$refs.lsjUpload.upload(file.name);
|
||||
@@ -105,24 +106,6 @@
|
||||
this.$refs.lsjUpload.clear(file.name);
|
||||
this.onError && this.onError(file, res);
|
||||
},
|
||||
refreshToken() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let loginInfo = uni.getStorageSync('loginInfo');
|
||||
uni.request({
|
||||
method: 'PUT',
|
||||
url: CHAT_ENV.BASE_URL + '/refreshToken',
|
||||
header: {
|
||||
refreshToken: loginInfo.refreshToken,
|
||||
},
|
||||
success: (res) => {
|
||||
resolve(res.data);
|
||||
},
|
||||
fail: (res) => {
|
||||
reject(res);
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -7,10 +7,10 @@
|
||||
</navigator>
|
||||
</template>
|
||||
</wd-navbar>
|
||||
|
||||
<view class="text-base mx-4 my-2">问题描述</view>
|
||||
<view class="mx-4 rounded-3">
|
||||
<wd-textarea v-model="params.content" :maxlength="1000" clearable show-word-limit />
|
||||
<wd-textarea v-model="params.content" placeholder="我们想听听你的心声,如果愿意,你也可以留下联系方式,我们期待与你的真诚沟通" :maxlength="1000"
|
||||
clearable show-word-limit />
|
||||
</view>
|
||||
<view class="text-base mx-4 my-2">图片</view>
|
||||
<view class="px-4">
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<image class="size-26rpx ml-2" :src="userGenderImageSrc" mode="scaleToFill" />
|
||||
</view>
|
||||
<view class="mb-2 text-xs text-#ffffff80 flex items-center">
|
||||
<text>ID: {{ userInfo.id }}</text>
|
||||
<text>ID: {{ userInfo.userCode }}</text>
|
||||
<view v-if="userInfo.memberVip?.vip"
|
||||
class="text-20rpx ml-2 flex-row-center bg-color-theme-dark px-1 rounded-1">
|
||||
<uni-icons type="vip-filled" size="14" />
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
</text>
|
||||
<text class="py-4rpx px-2 rounded-full text-22rpx bg-color-theme-dark whitespace-nowrap">
|
||||
{{
|
||||
member.appRefreshDate
|
||||
? formatRelativeTime(member.appRefreshDate) + ' 在线'
|
||||
: '离线'
|
||||
data.onLine
|
||||
? ' 在线'
|
||||
: formatRelativeTime(member.appRefreshDate)
|
||||
}}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const H5_STATUS_BAR_QUERY_KEYS = ['statusBarHeight', 'safeTop', 'safeAreaTop', 'appStatusBarHeight'];
|
||||
const H5_MIN_STATUS_BAR_HEIGHT = 22;
|
||||
const H5_MIN_STATUS_BAR_HEIGHT = 54;
|
||||
const H5_MAX_STATUS_BAR_HEIGHT = 200;
|
||||
let currentStatusBarHeight = H5_MIN_STATUS_BAR_HEIGHT;
|
||||
let statusBarStyleObserver = null;
|
||||
|
||||
@@ -8,6 +8,7 @@ import UserApi from '@/sheep/api/member/user';
|
||||
import PayWalletApi from '@/sheep/api/pay/wallet';
|
||||
import OrderApi from '@/sheep/api/trade/order';
|
||||
import CouponApi from '@/sheep/api/promotion/coupon';
|
||||
import chatTokenManager from '@/common/chat-token-manager';
|
||||
|
||||
// 默认用户信息
|
||||
const defaultUserInfo = {
|
||||
@@ -128,6 +129,7 @@ const user = defineStore({
|
||||
this.userWallet = clone(defaultUserWallet);
|
||||
this.numData = cloneDeep(defaultNumData);
|
||||
// 清空聊天相关
|
||||
chatTokenManager.stop();
|
||||
uni.removeStorageSync('loginInfo');
|
||||
// 清空购物车的缓存
|
||||
cart().emptyList();
|
||||
|
||||
Reference in new issue
Block a user