Browse Source

新增切换账号功能

master
[yxf] 1 month ago
parent
commit
c81f109640
  1. 50
      im-platform/src/main/java/com/bx/implatform/controller/UserController.java
  2. 2
      im-platform/src/main/java/com/bx/implatform/service/UserService.java
  3. 331
      im-web/src/components/setting/Setting.vue
  4. 1
      im-web/src/main.js
  5. 31
      im-web/src/store/userStore.js

50
im-platform/src/main/java/com/bx/implatform/controller/UserController.java

@ -18,6 +18,11 @@ import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import com.bx.implatform.vo.LoginVO;
import com.bx.implatform.enums.ResultCode;
import com.alibaba.fastjson.JSON;
import com.bx.imcommon.util.JwtUtil;
import com.bx.implatform.config.props.JwtProperties;
import java.util.HashMap;
import java.util.List;
@ -32,6 +37,7 @@ import static com.bx.implatform.enums.ResultCode.XSS_PARAM_ERROR;
public class UserController {
private final UserService userService;
private final JwtProperties jwtProperties;
@GetMapping("/terminal/online")
@Operation(summary = "判断用户哪个终端在线", description = "返回在线的用户id的终端集合")
@ -115,5 +121,49 @@ public class UserController {
return ResultUtils.success();
}
@PostMapping("/switchAccount")
@Operation(summary = "切换账号", description = "管理员或客服免密切换到其他账号")
public Result<LoginVO> switchAccount(@RequestBody JSONObject jsonObject) {
Long targetUserId = jsonObject.getLong("targetUserId");
Integer terminal = jsonObject.getInt("terminal");
// 获取当前登录用户
UserSession currentSession = SessionContext.getSession();
User currentUser = userService.getById(currentSession.getUserId());
// 权限校验:只有客服才能切换账号
if (currentUser.getIsCustomer() != 2) {
return ResultUtils.error(XSS_PARAM_ERROR, "无权限切换账号");
}
// 获取目标用户信息
User targetUser = userService.getById(targetUserId);
if (ObjectUtil.isNull(targetUser)) {
return ResultUtils.error(ResultCode.XSS_PARAM_ERROR, "目标用户不存在");
}
// 生成新的token
UserSession newSession = BeanUtils.copyProperties(targetUser, UserSession.class);
newSession.setUserId(targetUser.getId());
newSession.setTerminal(terminal);
String strJson = JSON.toJSONString(newSession);
String accessToken = JwtUtil.sign(targetUser.getId(), strJson,
jwtProperties.getAccessTokenExpireIn(), jwtProperties.getAccessTokenSecret());
String refreshToken = JwtUtil.sign(targetUser.getId(), strJson,
jwtProperties.getRefreshTokenExpireIn(), jwtProperties.getRefreshTokenSecret());
LoginVO vo = new LoginVO();
vo.setAccessToken(accessToken);
vo.setAccessTokenExpiresIn(jwtProperties.getAccessTokenExpireIn());
vo.setRefreshToken(refreshToken);
vo.setRefreshTokenExpiresIn(jwtProperties.getRefreshTokenExpireIn());
vo.setUser(targetUser);
// log.info("账号切换:从用户 {} 切换到用户 {}", currentSession.getUserId(), targetUserId);
return ResultUtils.success(vo);
}
}

2
im-platform/src/main/java/com/bx/implatform/service/UserService.java

@ -115,4 +115,6 @@ public interface UserService extends IService<User> {
* @return 客服列表
*/
List<User> getEnableChangeCustomerList(Long userId);
}

331
im-web/src/components/setting/Setting.vue

@ -1,4 +1,6 @@
<template>
<div>
<!-- 设置弹窗 -->
<el-dialog v-dialogDrag class="setting" title="设置" :visible.sync="visible" width="420px" :before-close="onClose">
<el-form :model="userInfo" label-width="80px" :rules="rules" ref="settingForm" size="small">
<el-form-item label="头像" style="margin-bottom: 0 !important;">
@ -27,10 +29,56 @@
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="onSwitchAccount" style="float: left;">切换账号</el-button>
<el-button @click="onClose()"> </el-button>
<el-button type="primary" @click="onSubmit()"> </el-button>
</span>
</el-dialog>
<!-- 切换账号弹窗 -->
<el-dialog title="切换账号" :visible.sync="switchDialogVisible" width="500px" append-to-body :close-on-click-modal="false">
<el-input
v-model="searchKeyword"
placeholder="请输入客服昵称搜索"
@keyup.enter.native="filterCustomerList"
@clear="clearSearch"
clearable
style="margin-bottom: 20px">
<el-button slot="append" icon="el-icon-search" @click="filterCustomerList">搜索</el-button>
</el-input>
<div class="user-list" v-loading="userListLoading">
<div v-for="user in filteredUserList" :key="user.id" class="user-item" @click="switchToAccount(user)">
<img :src="user.headImageThumb || defaultAvatar" class="user-avatar" @error="handleAvatarError">
<div class="user-info">
<div class="user-name">
<span>{{ user.nickName }}</span>
<el-tag type="success" size="mini" style="margin-left: 8px">客服</el-tag>
<el-tag v-if="user.id === userStore.userInfo.id" type="warning" size="mini" style="margin-left: 8px">当前</el-tag>
</div>
<div class="user-username">用户名: {{ user.userName }}</div>
<div class="user-id">ID: {{ user.id }}</div>
</div>
<el-button
:type="user.id === userStore.userInfo.id ? 'info' : 'primary'"
size="small"
:disabled="user.id === userStore.userInfo.id"
@click.stop="switchToAccount(user)">
{{ user.id === userStore.userInfo.id ? '当前账号' : '切换' }}
</el-button>
</div>
<div v-if="filteredUserList.length === 0 && !userListLoading" class="empty-text">
<i class="el-icon-info"></i>
<p>{{ searchKeyword ? '未找到相关客服' : '暂无其他客服账号' }}</p>
</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="switchDialogVisible = false"> </el-button>
</span>
</el-dialog>
</div>
</template>
<script>
@ -52,13 +100,37 @@ export default {
message: '请输入昵称',
trigger: 'blur'
}]
},
//
switchDialogVisible: false,
searchKeyword: '',
allCustomerList: [], //
userListLoading: false,
defaultAvatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
}
},
computed: {
imageAction() {
return `/image/upload?thumbSize=20`;
},
//
filteredUserList() {
if (!this.searchKeyword.trim()) {
return this.allCustomerList;
}
const keyword = this.searchKeyword.toLowerCase();
return this.allCustomerList.filter(user =>
user.nickName?.toLowerCase().includes(keyword) ||
user.userName?.toLowerCase().includes(keyword) ||
user.id?.toString().includes(keyword)
);
}
},
methods: {
onClose() {
this.$emit("close");
},
onSubmit() {
this.$refs['settingForm'].validate((valid) => {
if (!valid) {
@ -75,6 +147,179 @@ export default {
})
});
},
//
onSwitchAccount() {
// isCustomer == 2
// const currentUser = this.userStore.userInfo;
// console.log(currentthis.userStoreUser);
// if (currentUser.isCustomer !== 2) {
// this.$message.warning('');
// return;
// }
this.showSwitchAccountDialog();
},
//
showSwitchAccountDialog() {
this.switchDialogVisible = true;
this.searchKeyword = '';
this.loadCustomerList();
},
//
async loadCustomerList() {
this.userListLoading = true;
try {
const res = await this.$http({
url: '/user/getEnableChangeCustomer',
method: 'post'
});
let apiCustomers = res.data || res || [];
const currentUser = this.userStore.userInfo;
// ==============================================
// 1.
// ==============================================
let cachedCustomers = JSON.parse(localStorage.getItem('allCustomerAccounts') || '[]');
// ==============================================
// 2. +
// ==============================================
let allCustomers = [...apiCustomers, ...cachedCustomers];
// id
allCustomers = Array.from(new Map(allCustomers.map(item => [item.id, item])).values());
// ==============================================
// 3.
// ==============================================
const currentExists = allCustomers.some(item => item.id === currentUser.id);
if (!currentExists) {
const fullCurrentUser = {
id: currentUser.id,
nickName: currentUser.nickName,
userName: currentUser.userName,
headImageThumb: currentUser.headImageThumb,
headImage: currentUser.headImage
};
allCustomers.unshift(fullCurrentUser);
}
// ==============================================
// 4.
// ==============================================
localStorage.setItem('allCustomerAccounts', JSON.stringify(allCustomers));
// ==============================================
// 5.
// ==============================================
this.allCustomerList = allCustomers.sort((a, b) => {
if (a.id === currentUser.id) return -1;
if (b.id === currentUser.id) return 1;
return 0;
});
} catch (error) {
console.error('加载客服列表失败:', error);
this.$message.error('加载客服列表失败');
} finally {
this.userListLoading = false;
}
},
//
filterCustomerList() {
//
// computed
if (this.searchKeyword.trim() && this.filteredUserList.length === 0) {
this.$message.info('未找到相关客服');
}
},
//
clearSearch() {
this.searchKeyword = '';
},
//
async switchToAccount(targetUser) {
if (targetUser.id === this.userStore.userInfo.id) {
this.$message.warning('已是当前账号');
return;
}
try {
await this.$confirm(`确定要切换到客服账号【${targetUser.nickName}】吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
const loading = this.$loading({
lock: true,
text: '正在切换账号...',
spinner: 'el-icon-loading'
});
//
const res = await this.$http({
url: '/user/switchAccount',
method: 'post',
data: {
targetUserId: targetUser.id,
terminal: this.getTerminalType()
}
});
const loginData = res;
// token
if (loginData.accessToken) {
localStorage.setItem('accessToken', loginData.accessToken);
}
if (loginData.refreshToken) {
localStorage.setItem('refreshToken', loginData.refreshToken);
}
//
if (loginData.user) {
localStorage.setItem('userInfo', JSON.stringify(loginData.user));
}
loading.close();
this.$message.success(`已切换到客服账号:${targetUser.nickName}`);
//
setTimeout(() => {
window.location.reload();
}, 10);
} catch (error) {
if (error !== 'cancel') {
console.error('切换账号失败:', error);
this.$message.error('切换账号失败');
}
}
},
//
getTerminalType() {
const userAgent = navigator.userAgent;
if (/mobile/i.test(userAgent)) {
return 2; // APP
}
if (/tablet/i.test(userAgent)) {
return 3; //
}
return 1; // WEB
},
//
handleAvatarError(e) {
e.target.src = this.defaultAvatar;
},
onUploadSuccess(data, file) {
this.userInfo.headImage = data.originUrl;
this.userInfo.headImageThumb = data.thumbUrl;
@ -85,16 +330,12 @@ export default {
type: Boolean
}
},
computed: {
imageAction() {
return `/image/upload?thumbSize=20`;
}
},
watch: {
visible: function () {
//
let mine = this.userStore.userInfo;
this.userInfo = JSON.parse(JSON.stringify(mine));
visible: function (newVal) {
if (newVal) {
//
this.userInfo = JSON.parse(JSON.stringify(this.userStore.userInfo));
}
}
}
}
@ -137,4 +378,76 @@ export default {
}
}
}
.user-list {
max-height: 400px;
overflow-y: auto;
.user-item {
display: flex;
align-items: center;
padding: 12px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background-color 0.3s;
&:hover {
background-color: #f5f7fa;
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 12px;
object-fit: cover;
flex-shrink: 0;
}
.user-info {
flex: 1;
min-width: 0;
.user-name {
font-weight: 500;
margin-bottom: 4px;
display: flex;
align-items: center;
flex-wrap: wrap;
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.user-username {
font-size: 12px;
color: #909399;
margin-bottom: 2px;
}
.user-id {
font-size: 11px;
color: #c0c4cc;
}
}
}
.empty-text {
text-align: center;
padding: 40px;
color: #909399;
i {
font-size: 48px;
margin-bottom: 10px;
}
p {
margin: 0;
}
}
}
</style>

1
im-web/src/main.js

@ -36,6 +36,7 @@ Vue.prototype.$str = str; // 字符串相关
Vue.prototype.$elm = element; // 元素操作
Vue.prototype.$enums = enums; // 枚举
Vue.prototype.$eventBus = new Vue(); // 全局事件
Vue.config.productionTip = false;
new Vue({

31
im-web/src/store/userStore.js

@ -5,17 +5,22 @@ import { RTC_STATE } from "../api/enums.js"
export default defineStore('userStore', {
state: () => {
return {
userInfo: {},
// 初始化永远从本地存储取
userInfo: localStorage.getItem('userInfo')
? JSON.parse(localStorage.getItem('userInfo'))
: {},
rtcInfo: {
friend: {}, // 好友信息
mode: "video", // 模式 video:视频 voice:语音
state: RTC_STATE.FREE // FREE:空闲 WAIT_CALL:呼叫方等待 WAIT_ACCEPT: 被呼叫方等待接听 CHATING:聊天中
friend: {},
mode: "video",
state: RTC_STATE.FREE
}
}
},
actions: {
setUserInfo(userInfo) {
this.userInfo = userInfo
this.userInfo = userInfo;
// 同步本地存储
localStorage.setItem('userInfo', JSON.stringify(userInfo));
},
setRtcInfo(rtcInfo) {
this.rtcInfo = rtcInfo;
@ -25,14 +30,30 @@ export default defineStore('userStore', {
},
clear() {
this.userInfo = {};
localStorage.removeItem('userInfo');
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
this.rtcInfo = {
friend: {},
mode: "video",
state: RTC_STATE.FREE
};
},
// ==============================================
// 【关键修复】:如果本地已有用户信息,不再重复拉取覆盖!
// ==============================================
loadUser() {
return new Promise((resolve, reject) => {
// 如果本地已经有账号 → 直接用本地的,不请求接口!
const localUser = localStorage.getItem('userInfo');
if (localUser && localUser !== '{}') {
this.userInfo = JSON.parse(localUser);
resolve();
return;
}
// 只有本地没有时,才去后端获取
http({
url: '/user/self',
method: 'GET'

Loading…
Cancel
Save