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. 296
      im-uniapp/pages/login/login.vue
  6. 115
      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 = "来源网址") @Schema(description = "来源网址")
private String sourceUrl; 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; 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.contant.Constant;
import com.bx.implatform.dto.PrivateMessageDTO; import com.bx.implatform.dto.PrivateMessageDTO;
import com.bx.implatform.entity.PrivateMessage; import com.bx.implatform.entity.PrivateMessage;
import com.bx.implatform.entity.User;
import com.bx.implatform.enums.MessageStatus; import com.bx.implatform.enums.MessageStatus;
import com.bx.implatform.enums.MessageType; import com.bx.implatform.enums.MessageType;
import com.bx.implatform.exception.GlobalException; import com.bx.implatform.exception.GlobalException;
import com.bx.implatform.mapper.PrivateMessageMapper; import com.bx.implatform.mapper.PrivateMessageMapper;
import com.bx.implatform.mapper.UserMapper;
import com.bx.implatform.service.FriendService; import com.bx.implatform.service.FriendService;
import com.bx.implatform.service.PrivateMessageService; import com.bx.implatform.service.PrivateMessageService;
import com.bx.implatform.session.SessionContext; import com.bx.implatform.session.SessionContext;
@ -46,15 +48,27 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
private final FriendService friendService; private final FriendService friendService;
private final IMClient imClient; private final IMClient imClient;
private final SensitiveFilterUtil sensitiveFilterUtil; private final SensitiveFilterUtil sensitiveFilterUtil;
private final UserMapper userMapper;
private static final ScheduledThreadPoolExecutor EXECUTOR = ThreadPoolExecutorFactory.getThreadPoolExecutor(); private static final ScheduledThreadPoolExecutor EXECUTOR = ThreadPoolExecutorFactory.getThreadPoolExecutor();
@Override @Override
public PrivateMessageVO sendMessage(PrivateMessageDTO dto) { public PrivateMessageVO sendMessage(PrivateMessageDTO dto) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
Long sendUserId = session.getUserId();
Long recvUserId = dto.getRecvId();
Boolean isFriends = friendService.isFriend(session.getUserId(), dto.getRecvId()); Boolean isFriends = friendService.isFriend(session.getUserId(), dto.getRecvId());
if (Boolean.FALSE.equals(isFriends)) { if (Boolean.FALSE.equals(isFriends)) {
throw new GlobalException("您已不是对方好友,无法发送消息"); 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); PrivateMessage msg = BeanUtils.copyProperties(dto, PrivateMessage.class);
msg.setSendId(session.getUserId()); 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 @Override
public LoginVO login(LoginDTO dto) { public LoginVO login(LoginDTO dto) {
log.info("【测试】前端传的IP:{}", dto.getSourceUrl()); log.info("【测试】前端传的uniqueToken:{}", dto.getUniqueToken());
// 生成游客唯一标识UUID // 生成游客唯一标识UUID
String guestUuid = UUID.randomUUID().toString(); String guestUuid = UUID.randomUUID().toString();
@ -106,18 +106,13 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
guestUser.setNickName(guestUserName); guestUser.setNickName(guestUserName);
guestUser.setPassword(""); guestUser.setPassword("");
guestUser.setUuid(guestUuid); guestUser.setUuid(guestUuid);
// ========== 先设置IP ==========
guestUser.setLastLoginIp(dto.getIp()); guestUser.setLastLoginIp(dto.getIp());
guestUser.setSourceUrl(dto.getSourceUrl()); guestUser.setSourceUrl(dto.getSourceUrl());
guestUser.setUniqueToken(dto.getUniqueToken());
// 保存到数据库 // 保存到数据库
this.save(guestUser); this.save(guestUser);
// ========== 正确更新 IP 和地址 ========== Long customerServiceId = this.getCustomerServiceIdByUniqueToken(dto.getUniqueToken());
// if(StrUtil.isNotBlank(dto.getIp())){
// this.updateIpAndAddress(guestUser);
// }
Long customerServiceId = this.getRandomCustomerServiceId();
UserSession guestSession = new UserSession(); UserSession guestSession = new UserSession();
guestSession.setUserId(guestUser.getId()); guestSession.setUserId(guestUser.getId());
@ -148,23 +143,33 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
vo.setCustomerServiceId(customerServiceId == null ? -1 : customerServiceId); vo.setCustomerServiceId(customerServiceId == null ? -1 : customerServiceId);
vo.setUser(guestUser); 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; return vo;
} }
/**
public Long getRandomCustomerServiceId() { * 根据 uniqueToken 查询客服
* 客服条件is_customer = 2 unique_token = 传入的token
*/
public Long getCustomerServiceIdByUniqueToken(String uniqueToken) {
// 1. 构建查询条件
LambdaQueryWrapper<User> queryWrapper = Wrappers.lambdaQuery(); LambdaQueryWrapper<User> queryWrapper = Wrappers.lambdaQuery();
// 条件:is_customer = 2 表示客服 queryWrapper.eq(User::getIsCustomer, 2); // 只查客服
queryWrapper.eq(User::getIsCustomer, 2);
// 只查id字段,提升效率 // 2. 有 token 才按 token 匹配
if (StrUtil.isNotBlank(uniqueToken)) {
queryWrapper.eq(User::getUniqueToken, uniqueToken);
}
// 3. 只查 ID
queryWrapper.select(User::getId); 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();
} }

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

@ -19,7 +19,8 @@ export default {
userName: '', userName: '',
password: '', password: '',
ip: '', ip: '',
sourceUrl: '' sourceUrl: '',
uniqueToken: '' // token
} }
} }
}, },
@ -53,35 +54,79 @@ export default {
console.log("来源网址:", this.dataForm.sourceUrl); 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() { async autoLogin() {
if (GLOBAL_AUTO_LOGIN_LOCK) return; if (GLOBAL_AUTO_LOGIN_LOCK) return;
GLOBAL_AUTO_LOGIN_LOCK = true; GLOBAL_AUTO_LOGIN_LOCK = true;
// // // token使URLtoken
// const loginInfo = uni.getStorageSync("loginInfo"); this.getTokenFromUrl();
// //
// 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;
// }
// //
this.getSourceUrl(); this.getSourceUrl();
await this.getIp(); 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({ this.$http({
url: '/login', url: '/login',
data: this.dataForm, data: loginData,
method: 'POST' method: 'POST'
}).then(loginInfo => { }).then(loginInfo => {
uni.setStorageSync("isAgree", this.isAgree); uni.setStorageSync("isAgree", this.isAgree);
@ -89,6 +134,7 @@ export default {
getApp().$vm.init() getApp().$vm.init()
getApp().$vm.unloadStore(); getApp().$vm.unloadStore();
console.log(loginInfo.customerServiceId);
this.$http({ this.$http({
url: "/friend/add?friendId=" + loginInfo.customerServiceId, url: "/friend/add?friendId=" + loginInfo.customerServiceId,
method: "POST" method: "POST"
@ -102,17 +148,25 @@ export default {
} }
this.friendStore.addFriend(friend); this.friendStore.addFriend(friend);
// ID // URL token
uni.reLaunch({ // #ifdef H5
url: `/pages/chat/chat-box?targetId=${loginInfo.customerServiceId}&type=PRIVATE` // 使 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 => { }).catch(err => {
console.log("自动登录失败", err); console.log("自动登录失败", err);
}); });
}, },
getIp() { getIp() {
// ip // ip
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -120,17 +174,12 @@ export default {
url: 'https://api.ipify.org?format=json', url: 'https://api.ipify.org?format=json',
method: 'GET', method: 'GET',
success: (res) => { success: (res) => {
// console.log("IP", res.data);
this.dataForm.ip = res.data.ip; this.dataForm.ip = res.data.ip;
// console.log("", this.dataForm); resolve(res.data.ip);
resolve(res.data.ip); // Promise
}, },
fail: (err) => { fail: (err) => {
// console.log("IP", err);
// IPip
this.dataForm.ip = ''; this.dataForm.ip = '';
// console.log("", this.dataForm); resolve('');
resolve(''); // Promise
} }
}); });
}); });
@ -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(() => { setTimeout(() => {
@ -154,181 +208,3 @@ export default {
} }
} }
</script> </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>-->

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

@ -1,3 +1,5 @@
<template> <template>
<div> <div>
<!-- 设置弹窗 --> <!-- 设置弹窗 -->
@ -27,14 +29,12 @@
<el-input type="textarea" v-model="userInfo.signature" :rows="3" maxlength="64"></el-input> <el-input type="textarea" v-model="userInfo.signature" :rows="3" maxlength="64"></el-input>
</el-form-item> </el-form-item>
</el-form> </el-form>
<span slot="footer" class="dialog-footer"> <span slot="footer" class="dialog-footer">
<el-button @click="onSwitchAccount" style="float: left;">切换账号</el-button> <el-button @click="onSwitchAccount" style="float: left;">切换账号</el-button>
<el-button @click="onClose()"> </el-button> <el-button @click="onClose()"> </el-button>
<el-button type="primary" @click="onSubmit()"> </el-button> <el-button type="primary" @click="onSubmit()"> </el-button>
</span> </span>
</el-dialog> </el-dialog>
<!-- 切换账号弹窗 --> <!-- 切换账号弹窗 -->
<el-dialog title="切换账号" :visible.sync="switchDialogVisible" width="500px" append-to-body :close-on-click-modal="false"> <el-dialog title="切换账号" :visible.sync="switchDialogVisible" width="500px" append-to-body :close-on-click-modal="false">
<el-input <el-input
@ -130,6 +130,9 @@ export default {
onClose() { onClose() {
this.$emit("close"); this.$emit("close");
}, },
setCookie(name, value) {
document.cookie = name + "=" + escape(value);
},
onSubmit() { onSubmit() {
this.$refs['settingForm'].validate((valid) => { this.$refs['settingForm'].validate((valid) => {
@ -179,21 +182,12 @@ export default {
let apiCustomers = res.data || res || []; let apiCustomers = res.data || res || [];
const currentUser = this.userStore.userInfo; const currentUser = this.userStore.userInfo;
// ==============================================
// 1.
// ==============================================
let cachedCustomers = JSON.parse(localStorage.getItem('allCustomerAccounts') || '[]'); let cachedCustomers = JSON.parse(localStorage.getItem('allCustomerAccounts') || '[]');
// ==============================================
// 2. +
// ==============================================
let allCustomers = [...apiCustomers, ...cachedCustomers]; let allCustomers = [...apiCustomers, ...cachedCustomers];
// id // id
allCustomers = Array.from(new Map(allCustomers.map(item => [item.id, item])).values()); allCustomers = Array.from(new Map(allCustomers.map(item => [item.id, item])).values());
// ==============================================
// 3.
// ==============================================
const currentExists = allCustomers.some(item => item.id === currentUser.id); const currentExists = allCustomers.some(item => item.id === currentUser.id);
if (!currentExists) { if (!currentExists) {
const fullCurrentUser = { const fullCurrentUser = {
@ -206,14 +200,9 @@ export default {
allCustomers.unshift(fullCurrentUser); allCustomers.unshift(fullCurrentUser);
} }
// ==============================================
// 4.
// ==============================================
localStorage.setItem('allCustomerAccounts', JSON.stringify(allCustomers)); localStorage.setItem('allCustomerAccounts', JSON.stringify(allCustomers));
// ==============================================
// 5.
// ==============================================
this.allCustomerList = allCustomers.sort((a, b) => { this.allCustomerList = allCustomers.sort((a, b) => {
if (a.id === currentUser.id) return -1; if (a.id === currentUser.id) return -1;
if (b.id === currentUser.id) return 1; if (b.id === currentUser.id) return 1;
@ -242,66 +231,60 @@ export default {
this.searchKeyword = ''; this.searchKeyword = '';
}, },
// //
async switchToAccount(targetUser) { async switchToAccount(targetUser) {
if (targetUser.id === this.userStore.userInfo.id) { if (targetUser.id === this.userStore.userInfo.id) {
this.$message.warning('已是当前账号'); this.$message.warning('已是当前账号');
return; return;
} }
try {
await this.$confirm(`确定要切换到客服账号【${targetUser.nickName}】吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
const loading = this.$loading({ try {
lock: true, await this.$confirm(`确定要切换到客服账号【${targetUser.nickName}】吗?`, '提示', {
text: '正在切换账号...', confirmButtonText: '确定',
spinner: 'el-icon-loading' cancelButtonText: '取消',
}); type: 'warning'
});
// const loading = this.$loading({
const res = await this.$http({ lock: true,
url: '/user/switchAccount', text: '正在切换账号...',
method: 'post', spinner: 'el-icon-loading'
data: { });
targetUserId: targetUser.id,
terminal: this.getTerminalType()
}
});
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.user) {
if (loginData.accessToken) { this.setCookie('username', loginData.user.userName);
localStorage.setItem('accessToken', loginData.accessToken); // this.setCookie('password', loginData.user.password);
}
if (loginData.refreshToken) {
localStorage.setItem('refreshToken', loginData.refreshToken);
}
// sessionStorage.setItem("accessToken", loginData.accessToken);
if (loginData.user) { sessionStorage.setItem("refreshToken", loginData.refreshToken);
localStorage.setItem('userInfo', JSON.stringify(loginData.user));
}
loading.close(); localStorage.setItem('userInfo', JSON.stringify(loginData.user));
this.userStore.setUserInfo(loginData.user);
}
this.$message.success(`已切换到客服账号:${targetUser.nickName}`); loading.close();
this.$message.success(`已切换到客服账号:${targetUser.nickName}`);
// setTimeout(() => {
setTimeout(() => { window.location.reload();
window.location.reload(); }, 300);
}, 10);
} catch (error) { } catch (error) {
if (error !== 'cancel') { if (error !== 'cancel') {
console.error('切换账号失败:', error); console.error('切换账号失败:', error);
this.$message.error('切换账号失败'); this.$message.error('切换账号失败');
}
} }
}
}, },
// //
getTerminalType() { getTerminalType() {

Loading…
Cancel
Save