feat:昵称重复、显示分销人分销tree
This commit is contained in:
1 parent
4f3b7ba3b4
commit
7571180b55
4 files changed
+166
-5
No files matched your search
+13
-3
@@ -79,9 +79,9 @@ export default {
|
||||
},
|
||||
// 初始化聊天
|
||||
async init() {
|
||||
this.isExit = false;
|
||||
this.startChatTokenCheck();
|
||||
this.reconnecting = false;
|
||||
this.isExit = false;
|
||||
try {
|
||||
// 加载数据
|
||||
await this.loadStore()
|
||||
@@ -112,9 +112,10 @@ export default {
|
||||
});
|
||||
wsApi.onMessage((cmd, msgInfo) => {
|
||||
if (cmd == 2) {
|
||||
// 异地登录,强制下线
|
||||
const reason = typeof msgInfo === 'string' ? msgInfo : msgInfo?.reason || '聊天登录状态已失效';
|
||||
chatTokenManager.stopForAccount(reason);
|
||||
uni.showModal({
|
||||
content: '您已在其他地方登录,将被强制下线',
|
||||
content: reason,
|
||||
showCancel: false,
|
||||
});
|
||||
this.exit();
|
||||
@@ -465,6 +466,9 @@ export default {
|
||||
// this.audioTip.play();
|
||||
},
|
||||
startChatTokenCheck() {
|
||||
if (this.isExit) {
|
||||
return;
|
||||
}
|
||||
// IM 双 token 都过期时,使用仍有效的主站登录态重新签发。
|
||||
chatTokenManager.start({
|
||||
reissueToken: reissueChatToken,
|
||||
@@ -535,6 +539,12 @@ export default {
|
||||
// #endif
|
||||
|
||||
async onLaunch() {
|
||||
uni.$on('chat-account-unavailable', (message) => {
|
||||
if (!this.isExit) {
|
||||
this.exit();
|
||||
uni.showToast({ title: message || '聊天账号已停用', icon: 'none' });
|
||||
}
|
||||
});
|
||||
// 加载Shopro底层依赖
|
||||
await ShoproInit();
|
||||
// #ifdef H5
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import vm from 'node:vm';
|
||||
|
||||
async function fixture() {
|
||||
const storage = new Map([
|
||||
['token', 'business-a'],
|
||||
['loginInfo', { accessToken: 'im-a', refreshToken: 'refresh-a', accessTokenExpiresAt: Date.now() + 3600000 }],
|
||||
]);
|
||||
const events = [];
|
||||
const requests = [];
|
||||
let intervals = 0;
|
||||
const source = (await readFile(new URL('../chat-token-manager.js', import.meta.url), 'utf8'))
|
||||
.replace("import CHAT_ENV from '@/chat-env.js';", 'const CHAT_ENV = { BASE_URL: "http://localhost" };')
|
||||
.replace(/export \{[^}]+\};/g, '')
|
||||
.replace('export default new ChatTokenManager();', 'globalThis.manager = new ChatTokenManager();');
|
||||
const context = vm.createContext({
|
||||
console,
|
||||
setInterval: () => ++intervals,
|
||||
clearInterval: () => { intervals--; },
|
||||
uni: {
|
||||
getStorageSync: key => storage.get(key),
|
||||
removeStorageSync: key => storage.delete(key),
|
||||
setStorageSync: (key, value) => storage.set(key, value),
|
||||
$emit: (name, value) => events.push([name, value]),
|
||||
request: request => requests.push(request),
|
||||
},
|
||||
});
|
||||
vm.runInContext(source, context);
|
||||
return { manager: context.manager, storage, events, requests, intervals: () => intervals };
|
||||
}
|
||||
|
||||
test('unavailable account stops polling and blocks refresh until a new business login', async () => {
|
||||
const context = await fixture();
|
||||
const { manager, storage, events } = context;
|
||||
manager.start();
|
||||
assert.equal(context.intervals(), 1);
|
||||
assert.equal(manager.handleAccountUnavailable({ code: 10005, message: '账号已被禁用' }, 'im-a', 'business-a'), true);
|
||||
assert.equal(context.intervals(), 0);
|
||||
manager.start();
|
||||
assert.equal(context.intervals(), 0);
|
||||
await assert.rejects(manager.refreshAccessToken(), error => error.code === 10005);
|
||||
assert.equal(context.requests.length, 0);
|
||||
assert.equal(events.length, 1);
|
||||
storage.set('token', 'business-b');
|
||||
storage.set('loginInfo', { accessToken: 'im-b', refreshToken: 'refresh-b', accessTokenExpiresAt: Date.now() + 3600000 });
|
||||
manager.start();
|
||||
assert.equal(context.intervals(), 1);
|
||||
manager.stop();
|
||||
});
|
||||
|
||||
test('late rejection from an old business login preserves the new session', async () => {
|
||||
const { manager, storage, requests, events } = await fixture();
|
||||
const refresh = manager.refreshAccessToken();
|
||||
storage.set('token', 'business-b');
|
||||
storage.set('loginInfo', { accessToken: 'im-b', refreshToken: 'refresh-b' });
|
||||
requests[0].success({ data: { code: 10005, message: '旧账号停用' } });
|
||||
await assert.rejects(refresh);
|
||||
assert.equal(storage.get('loginInfo').accessToken, 'im-b');
|
||||
assert.equal(events.length, 0);
|
||||
assert.equal(manager.accountUnavailableError(), null);
|
||||
});
|
||||
|
||||
test('a server forced logout invalidates the in-flight refresh result', async () => {
|
||||
const { manager, storage, requests } = await fixture();
|
||||
const refresh = manager.refreshAccessToken();
|
||||
manager.stopForAccount('账号已注销');
|
||||
requests[0].success({ data: { code: 200, data: { accessToken: 'late', refreshToken: 'late-refresh' } } });
|
||||
await assert.rejects(refresh, /聊天登录状态已停止/);
|
||||
assert.equal(storage.has('loginInfo'), false);
|
||||
await assert.rejects(manager.refreshAccessToken(), error => error.code === 10005);
|
||||
});
|
||||
|
||||
test('business reissue reports an IM ban and stops further credential retries', async () => {
|
||||
const { manager, storage, events } = await fixture();
|
||||
storage.delete('loginInfo');
|
||||
let attempts = 0;
|
||||
manager.setReissueToken(async () => {
|
||||
attempts++;
|
||||
throw { code: 1004018005, msg: '聊天账号已封禁,请联系客服' };
|
||||
});
|
||||
await assert.rejects(manager.refreshAccessToken());
|
||||
await assert.rejects(manager.refreshAccessToken(), error => error.code === 10005);
|
||||
assert.equal(attempts, 1);
|
||||
assert.equal(events[0][1], '聊天账号已封禁,请联系客服');
|
||||
});
|
||||
|
||||
test('late initial reissue cannot replace credentials from a new business login', async () => {
|
||||
const { manager, storage } = await fixture();
|
||||
storage.delete('loginInfo');
|
||||
let complete;
|
||||
manager.setReissueToken(() => new Promise(resolve => { complete = resolve; }));
|
||||
const reissue = manager.refreshAccessToken();
|
||||
await Promise.resolve();
|
||||
storage.set('token', 'business-b');
|
||||
storage.set('loginInfo', { accessToken: 'im-b', refreshToken: 'refresh-b' });
|
||||
complete({ accessToken: 'old-im-a', refreshToken: 'old-refresh-a' });
|
||||
assert.equal((await reissue).accessToken, 'im-b');
|
||||
assert.equal(storage.get('loginInfo').accessToken, 'im-b');
|
||||
});
|
||||
@@ -14,10 +14,15 @@ class ChatTokenManager {
|
||||
this.reissueToken = null;
|
||||
// 用于忽略定时器停止后才返回的旧检查结果。
|
||||
this.monitorVersion = 0;
|
||||
this.blockedBusinessToken = null;
|
||||
this.accountError = null;
|
||||
}
|
||||
|
||||
// 启动时立即检查一次,之后每 30 秒检查一次;重复调用不会创建多个定时器。
|
||||
start(options = {}) {
|
||||
if (this.accountUnavailableError()) {
|
||||
return;
|
||||
}
|
||||
this.onRefreshError = options.onRefreshError || null;
|
||||
if (options.reissueToken) {
|
||||
this.reissueToken = options.reissueToken;
|
||||
@@ -50,6 +55,39 @@ class ChatTokenManager {
|
||||
this.onRefreshError = null;
|
||||
}
|
||||
|
||||
accountUnavailableError() {
|
||||
const token = uni.getStorageSync('token') || '';
|
||||
if (this.blockedBusinessToken !== null && this.blockedBusinessToken === token) {
|
||||
return this.accountError;
|
||||
}
|
||||
this.blockedBusinessToken = null;
|
||||
this.accountError = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
stopForAccount(message) {
|
||||
this.blockedBusinessToken = uni.getStorageSync('token') || '';
|
||||
this.accountError = Object.assign(new Error(message || '聊天账号已停用'), { code: 10005 });
|
||||
this.stop();
|
||||
uni.removeStorageSync('loginInfo');
|
||||
}
|
||||
|
||||
handleAccountUnavailable(error, accessToken, businessToken = uni.getStorageSync('token')) {
|
||||
const code = error?.code ?? error?.data?.code;
|
||||
const currentAccessToken = uni.getStorageSync('loginInfo')?.accessToken;
|
||||
if (![10005, 1004018005].includes(Number(code)) || businessToken !== uni.getStorageSync('token')
|
||||
|| (accessToken && currentAccessToken && accessToken !== currentAccessToken)) {
|
||||
return false;
|
||||
}
|
||||
const alreadyBlocked = Boolean(this.accountUnavailableError());
|
||||
const message = error?.message || error?.msg || error?.data?.message || error?.data?.msg;
|
||||
this.stopForAccount(message);
|
||||
if (!alreadyBlocked) {
|
||||
uni.$emit('chat-account-unavailable', message);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
setReissueToken(reissueToken) {
|
||||
this.reissueToken = reissueToken;
|
||||
}
|
||||
@@ -127,11 +165,16 @@ class ChatTokenManager {
|
||||
}
|
||||
|
||||
refreshAccessToken(loginInfo = uni.getStorageSync('loginInfo')) {
|
||||
const unavailable = this.accountUnavailableError();
|
||||
if (unavailable) {
|
||||
return Promise.reject(unavailable);
|
||||
}
|
||||
if (this.refreshPromise) {
|
||||
return this.refreshPromise;
|
||||
}
|
||||
|
||||
const refreshVersion = this.monitorVersion;
|
||||
const businessToken = uni.getStorageSync('token');
|
||||
const accessToken = loginInfo?.accessToken;
|
||||
const refreshToken = loginInfo?.refreshToken;
|
||||
const refreshRequest = refreshToken
|
||||
@@ -160,9 +203,9 @@ class ChatTokenManager {
|
||||
|
||||
// 刷新期间用户可能退出或重新登录,旧请求结果不能覆盖新的登录状态。
|
||||
const currentLoginInfo = uni.getStorageSync('loginInfo');
|
||||
const loginChanged = refreshToken
|
||||
const loginChanged = businessToken !== uni.getStorageSync('token') || (refreshToken
|
||||
? currentLoginInfo?.refreshToken !== refreshToken
|
||||
: accessToken && currentLoginInfo?.accessToken !== accessToken;
|
||||
: accessToken && currentLoginInfo?.accessToken !== accessToken);
|
||||
if (loginChanged) {
|
||||
if (currentLoginInfo?.accessToken) {
|
||||
return currentLoginInfo;
|
||||
@@ -172,6 +215,10 @@ class ChatTokenManager {
|
||||
|
||||
return this.saveTokenInfo(newLoginInfo);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.handleAccountUnavailable(error, accessToken, businessToken);
|
||||
throw error;
|
||||
})
|
||||
.finally(() => {
|
||||
this.refreshPromise = null;
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ const MAX_AUTH_RETRY_COUNT = 2;
|
||||
const request = (options, authRetryCount = 0) => {
|
||||
const header = options.header || {};
|
||||
const loginInfo = uni.getStorageSync('loginInfo');
|
||||
const businessToken = uni.getStorageSync('token');
|
||||
if (loginInfo) {
|
||||
header.accessToken = loginInfo.accessToken;
|
||||
}
|
||||
@@ -29,6 +30,8 @@ const request = (options, authRetryCount = 0) => {
|
||||
}
|
||||
}
|
||||
return resolve(res.data.data);
|
||||
} else if (chatTokenManager.handleAccountUnavailable(res.data, header.accessToken, businessToken)) {
|
||||
return reject(res.data);
|
||||
} else if (res.data.code == 400 || res.data.code == 401) {
|
||||
console.log('token失效,尝试重新获取');
|
||||
const currentLoginInfo = uni.getStorageSync('loginInfo');
|
||||
|
||||
Reference in new issue
Block a user