feat:用户6位编号

This commit is contained in:
dzc committed 2026-07-30 11:43:04 +08:00
1 parent cf96d48a38
commit 3184608d99
9 files changed
+204 -139

No files matched your search

+25 -23
View File
@@ -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>
+159
View File
@@ -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
View File
@@ -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;
+6 -23
View File
@@ -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>
+2 -2
View File
@@ -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">
+1 -1
View File
@@ -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" />
+3 -3
View File
@@ -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 -1
View File
@@ -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;
+2
View File
@@ -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();