Browse Source

提交发送消息验证token标识,切换账号遗留问题

master
[yxf] 1 month ago
parent
commit
addaf58e38
  1. 3
      im-platform/src/main/java/com/bx/implatform/dto/LoginDTO.java
  2. 5
      im-platform/src/main/java/com/bx/implatform/entity/User.java
  3. 14
      im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java
  4. 43
      im-platform/src/main/java/com/bx/implatform/service/impl/UserServiceImpl.java
  5. 300
      im-uniapp/pages/login/login.vue
  6. 119
      im-web/src/components/setting/Setting.vue

3
im-platform/src/main/java/com/bx/implatform/dto/LoginDTO.java

@ -31,4 +31,7 @@ public class LoginDTO {
@Schema(description = "来源网址")
private String sourceUrl;
@Schema(description = "token标识")
private String uniqueToken;
}

5
im-platform/src/main/java/com/bx/implatform/entity/User.java

@ -109,5 +109,10 @@ public class User {
*/
private String sourceUrl;
/**
* token标识
*/
private String uniqueToken;
}

14
im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java

@ -13,10 +13,12 @@ import com.bx.imcommon.util.ThreadPoolExecutorFactory;
import com.bx.implatform.contant.Constant;
import com.bx.implatform.dto.PrivateMessageDTO;
import com.bx.implatform.entity.PrivateMessage;
import com.bx.implatform.entity.User;
import com.bx.implatform.enums.MessageStatus;
import com.bx.implatform.enums.MessageType;
import com.bx.implatform.exception.GlobalException;
import com.bx.implatform.mapper.PrivateMessageMapper;
import com.bx.implatform.mapper.UserMapper;
import com.bx.implatform.service.FriendService;
import com.bx.implatform.service.PrivateMessageService;
import com.bx.implatform.session.SessionContext;
@ -46,15 +48,27 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
private final FriendService friendService;
private final IMClient imClient;
private final SensitiveFilterUtil sensitiveFilterUtil;
private final UserMapper userMapper;
private static final ScheduledThreadPoolExecutor EXECUTOR = ThreadPoolExecutorFactory.getThreadPoolExecutor();
@Override
public PrivateMessageVO sendMessage(PrivateMessageDTO dto) {
UserSession session = SessionContext.getSession();
Long sendUserId = session.getUserId();
Long recvUserId = dto.getRecvId();
Boolean isFriends = friendService.isFriend(session.getUserId(), dto.getRecvId());
if (Boolean.FALSE.equals(isFriends)) {
throw new GlobalException("您已不是对方好友,无法发送消息");
}
User sendUser = userMapper.selectById(sendUserId);
User recvUser = userMapper.selectById(recvUserId);
// 打印发送用户和接收用户信息
log.info("发送私聊消息 - 发送用户ID: {}, 接收用户ID: {}",
recvUser, sendUser);
if(!recvUser.getUniqueToken().equals(sendUser.getUniqueToken())){
throw new GlobalException("非法客服,发送失败");
}
// 保存消息
PrivateMessage msg = BeanUtils.copyProperties(dto, PrivateMessage.class);
msg.setSendId(session.getUserId());

43
im-platform/src/main/java/com/bx/implatform/service/impl/UserServiceImpl.java

@ -93,7 +93,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
@Override
public LoginVO login(LoginDTO dto) {
log.info("【测试】前端传的IP:{}", dto.getSourceUrl());
log.info("【测试】前端传的uniqueToken:{}", dto.getUniqueToken());
// 生成游客唯一标识UUID
String guestUuid = UUID.randomUUID().toString();
@ -106,18 +106,13 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
guestUser.setNickName(guestUserName);
guestUser.setPassword("");
guestUser.setUuid(guestUuid);
// ========== 先设置IP ==========
guestUser.setLastLoginIp(dto.getIp());
guestUser.setSourceUrl(dto.getSourceUrl());
guestUser.setSourceUrl(dto.getSourceUrl());
guestUser.setUniqueToken(dto.getUniqueToken());
// 保存到数据库
this.save(guestUser);
// ========== 正确更新 IP 和地址 ==========
// if(StrUtil.isNotBlank(dto.getIp())){
// this.updateIpAndAddress(guestUser);
// }
Long customerServiceId = this.getRandomCustomerServiceId();
Long customerServiceId = this.getCustomerServiceIdByUniqueToken(dto.getUniqueToken());
UserSession guestSession = new UserSession();
guestSession.setUserId(guestUser.getId());
@ -148,23 +143,33 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
vo.setCustomerServiceId(customerServiceId == null ? -1 : customerServiceId);
vo.setUser(guestUser);
log.info("游客登录成功,userId:{}, uuid:{}, ip:{}", guestUser.getId(), guestUuid, dto.getIp());
log.info("游客登录成功,userId:{}, uniqueToken:{},分配客服ID:{}",
guestUser.getId(), dto.getUniqueToken(), customerServiceId);
return vo;
}
public Long getRandomCustomerServiceId() {
/**
* 根据 uniqueToken 查询客服
* 客服条件is_customer = 2 unique_token = 传入的token
*/
public Long getCustomerServiceIdByUniqueToken(String uniqueToken) {
// 1. 构建查询条件
LambdaQueryWrapper<User> queryWrapper = Wrappers.lambdaQuery();
// 条件:is_customer = 2 表示客服
queryWrapper.eq(User::getIsCustomer, 2);
// 只查id字段,提升效率
queryWrapper.eq(User::getIsCustomer, 2); // 只查客服
// 2. 有 token 才按 token 匹配
if (StrUtil.isNotBlank(uniqueToken)) {
queryWrapper.eq(User::getUniqueToken, uniqueToken);
}
// 3. 只查 ID
queryWrapper.select(User::getId);
// 随机排序(mysql用rand())
queryWrapper.last("ORDER BY RAND() LIMIT 1");
User customerService = this.getOne(queryWrapper, false); // false=无结果不抛异常
// 4. 查一条(false=查不到不抛异常)
User customer = this.getOne(queryWrapper, false);
return customerService == null ? null : customerService.getId();
// 5. 有客服返回ID,没有返回null
return customer == null ? null : customer.getId();
}

300
im-uniapp/pages/login/login.vue

@ -19,7 +19,8 @@ export default {
userName: '',
password: '',
ip: '',
sourceUrl: ''
sourceUrl: '',
uniqueToken: '' // token
}
}
},
@ -53,35 +54,79 @@ export default {
console.log("来源网址:", this.dataForm.sourceUrl);
},
// URLtoken
getTokenFromUrl() {
// #ifdef H5
// 1options
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
const options = currentPage.options || {};
if (options.token) {
this.dataForm.uniqueToken = options.token;
console.log("从options获取到token:", this.dataForm.uniqueToken);
return;
}
// 2URL
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
if (token) {
this.dataForm.uniqueToken = token;
console.log("从URL解析获取到token:", this.dataForm.uniqueToken);
}
// #endif
// #ifdef APP-PLUS
// App
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
const options = currentPage.options || {};
if (options.token) {
this.dataForm.uniqueToken = options.token;
console.log("App端获取到token:", this.dataForm.uniqueToken);
}
// #endif
// #ifdef MP-WEIXIN
//
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
const options = currentPage.options || {};
if (options.token) {
this.dataForm.uniqueToken = options.token;
console.log("小程序端获取到token:", this.dataForm.uniqueToken);
}
// #endif
},
async autoLogin() {
if (GLOBAL_AUTO_LOGIN_LOCK) return;
GLOBAL_AUTO_LOGIN_LOCK = true;
// //
// const loginInfo = uni.getStorageSync("loginInfo");
// //
// if (loginInfo) {
// try {
// // IM
// getApp().$vm.init();
// getApp().$vm.unloadStore();
// uni.reLaunch({
// url: "/pages/chat/chat-box?chatIdx=0"
// });
// } catch (err) {
// console.log("", err);
// }
// return;
// }
// token使URLtoken
this.getTokenFromUrl();
//
this.getSourceUrl();
await this.getIp();
//
const loginData = {
terminal: this.dataForm.terminal,
userName: this.dataForm.userName,
password: this.dataForm.password,
ip: this.dataForm.ip,
sourceUrl: this.dataForm.sourceUrl,
uniqueToken: this.dataForm.uniqueToken,
};
console.log("登录参数:", loginData);
this.$http({
url: '/login',
data: this.dataForm,
data: loginData,
method: 'POST'
}).then(loginInfo => {
uni.setStorageSync("isAgree", this.isAgree);
@ -89,6 +134,7 @@ export default {
getApp().$vm.init()
getApp().$vm.unloadStore();
console.log(loginInfo.customerServiceId);
this.$http({
url: "/friend/add?friendId=" + loginInfo.customerServiceId,
method: "POST"
@ -102,17 +148,25 @@ export default {
}
this.friendStore.addFriend(friend);
// ID
uni.reLaunch({
url: `/pages/chat/chat-box?targetId=${loginInfo.customerServiceId}&type=PRIVATE`
});
// URL token
// #ifdef H5
// 使 window.location
const cleanUrl = window.location.origin + window.location.pathname + '#/pages/chat/chat-box?targetId=' + loginInfo.customerServiceId + '&type=PRIVATE';
window.location.href = cleanUrl;
// #endif
// #ifndef H5
uni.reLaunch({
url: `/pages/chat/chat-box?targetId=${loginInfo.customerServiceId}&type=PRIVATE`
});
// #endif
})
}).catch(err => {
console.log("自动登录失败", err);
});
},
getIp() {
// ip
return new Promise((resolve, reject) => {
@ -120,17 +174,12 @@ export default {
url: 'https://api.ipify.org?format=json',
method: 'GET',
success: (res) => {
// console.log("IP", res.data);
this.dataForm.ip = res.data.ip;
// console.log("", this.dataForm);
resolve(res.data.ip); // Promise
resolve(res.data.ip);
},
fail: (err) => {
// console.log("IP", err);
// IPip
this.dataForm.ip = '';
// console.log("", this.dataForm);
resolve(''); // Promise
resolve('');
}
});
});
@ -141,7 +190,12 @@ export default {
}
},
onLoad() {
onLoad(options) {
// onLoadoptionstoken
if (options.token) {
this.dataForm.uniqueToken = options.token;
console.log("onLoad获取到token:", this.dataForm.uniqueToken);
}
//
setTimeout(() => {
@ -153,182 +207,4 @@ export default {
}
}
</script>
<!-- <style lang="scss" scoped>
.login {
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
//
.content {
position: relative;
display: flex;
flex-direction: column;
margin-top: 120rpx;
// #ifdef APP-PLUS
margin-top: calc(120rpx + var(--status-bar-height));
// #endif
padding: 0 60rpx;
}
//
.header {
text-align: center;
padding: 80rpx 0;
.title {
color: $im-color-primary;
font-size: 48rpx;
font-weight: 700;
margin-bottom: 20rpx;
letter-spacing: 2rpx;
}
.subtitle {
color: $im-text-color-light;
font-size: 28rpx;
opacity: 0.8;
}
}
//
.form-container {
display: flex;
flex-direction: column;
}
//
.form {
margin-bottom: 20rpx;
.form-item {
position: relative;
display: flex;
align-items: center;
padding: 0 30rpx;
height: 100rpx;
margin: 24rpx 0;
border-radius: 25rpx;
background: rgba(255, 255, 255, 0.9);
border: 2rpx solid transparent;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
&.focused {
border-color: $im-color-primary;
box-shadow: 0 8rpx 32rpx rgba($im-color-primary, 0.15);
transform: translateY(-2rpx);
}
.icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 60rpx;
height: 60rpx;
margin-right: 30rpx;
border-radius: 50%;
background: $im-bg-active;
transition: all 0.3s ease;
.icon {
font-size: 32rpx;
color: $im-color-primary;
font-weight: bold;
}
}
&.focused .icon-wrapper {
transform: scale(1.1);
}
.input {
flex: 1;
font-size: 32rpx;
color: #333;
background: transparent;
border: none;
outline: none;
&::placeholder {
color: $im-text-color-light;
font-size: 30rpx;
}
}
.icon-suffix {
font-size: 36rpx;
padding: 10rpx;
}
}
}
//
.submit-btn {
height: 100rpx;
border-radius: 50rpx;
border: none;
transition: all 0.3s ease;
overflow: hidden;
position: relative;
width: 100%;
&:active {
transform: translateY(2rpx);
&::before {
left: 100%;
}
}
.btn-content {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: white;
font-size: $im-font-size-large;
font-weight: 600;
.btn-text {
margin-right: 10rpx;
}
}
&:active .btn-icon {
transform: translateX(4rpx);
}
}
//
.nav-tool-bar {
padding: 40rpx 0 60rpx;
display: flex;
align-items: center;
justify-content: space-between;
.nav-register {
.register-link {
display: flex;
align-items: center;
text-decoration: none;
.register-text {
color: $im-text-color-light;
font-size: $im-font-size-small;
margin-right: 8rpx;
}
.register-highlight {
color: $im-color-primary;
font-size: $im-font-size-small;
font-weight: 600;
}
}
}
}
}
</style>-->
</script>

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

@ -1,3 +1,5 @@
<template>
<div>
<!-- 设置弹窗 -->
@ -27,14 +29,12 @@
<el-input type="textarea" v-model="userInfo.signature" :rows="3" maxlength="64"></el-input>
</el-form-item>
</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
@ -130,6 +130,9 @@ export default {
onClose() {
this.$emit("close");
},
setCookie(name, value) {
document.cookie = name + "=" + escape(value);
},
onSubmit() {
this.$refs['settingForm'].validate((valid) => {
@ -179,21 +182,12 @@ export default {
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 = {
@ -206,14 +200,9 @@ export default {
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;
@ -242,66 +231,60 @@ export default {
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'
});
if (targetUser.id === this.userStore.userInfo.id) {
this.$message.warning('已是当前账号');
return;
}
const loading = this.$loading({
lock: true,
text: '正在切换账号...',
spinner: 'el-icon-loading'
});
try {
await this.$confirm(`确定要切换到客服账号【${targetUser.nickName}】吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
//
const res = await this.$http({
url: '/user/switchAccount',
method: 'post',
data: {
targetUserId: targetUser.id,
terminal: this.getTerminalType()
}
});
const loading = this.$loading({
lock: true,
text: '正在切换账号...',
spinner: 'el-icon-loading'
});
const loginData = res;
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) {
this.setCookie('username', loginData.user.userName);
// this.setCookie('password', loginData.user.password);
sessionStorage.setItem("accessToken", loginData.accessToken);
sessionStorage.setItem("refreshToken", loginData.refreshToken);
localStorage.setItem('userInfo', JSON.stringify(loginData.user));
this.userStore.setUserInfo(loginData.user);
}
//
if (loginData.user) {
localStorage.setItem('userInfo', JSON.stringify(loginData.user));
}
loading.close();
this.$message.success(`已切换到客服账号:${targetUser.nickName}`);
loading.close();
this.$message.success(`已切换到客服账号:${targetUser.nickName}`);
//
setTimeout(() => {
window.location.reload();
}, 10);
setTimeout(() => {
window.location.reload();
}, 300);
} catch (error) {
if (error !== 'cancel') {
console.error('切换账号失败:', error);
this.$message.error('切换账号失败');
}
} catch (error) {
if (error !== 'cancel') {
console.error('切换账号失败:', error);
this.$message.error('切换账号失败');
}
}
},
//
getTerminalType() {

Loading…
Cancel
Save