一、问题

核心点:如何找到要发送的人?

要完成一个功能我觉得首先要分析该功能的逻辑及技术难点,而不是盲目的直接就撸代码,这样非常浪费时间。个人觉得web版聊天功能没什么实际应用场景,以前看过中国移动好像有过这种东西,所以就简单实现了下

解决:使用缓存存储当前聊天状态

    public class SignalRMessageGroups
{
public string ConnectionId { get; set; }
public long UserId { get; set; }
public string GroupName { get; set; }
public static List<SignalRMessageGroups> UserGroups = new List<SignalRMessageGroups>();
}

将当前聊天信息存储在内存中,当然你也可以持久化到其它地方,思路是一样的

二、具体实现代码

使用 SignalR 进行通讯,具体逻辑不描述(注释都有),因为是在自己的项目实现的,所以只显示部分代码,非常简单的东西,可能js和css写起来麻烦些

Hub代码:

[Authorize]
public class ChatHub: Hub
{
private readonly IOaChatService _chatService; public ChatHub(IOaChatService chatService)
{
this._chatService = chatService;
} public override async Task OnConnectedAsync()
{
await base.OnConnectedAsync();
} public override async Task OnDisconnectedAsync(Exception ex)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, "ChatHubGroup");
var curUser = SignalRMessageGroups.UserGroups.FirstOrDefault(m => m.ConnectionId == Context.ConnectionId && m.GroupName == "ChatHubGroup");
if (curUser != null)
{
SignalRMessageGroups.UserGroups.Remove(curUser);
}
await base.OnDisconnectedAsync(ex);
} /// <summary>
/// 信息发送
/// </summary>
/// <param name="receiver">接收人</param>
/// <param name="sender">发送人</param>
/// <param name="message"></param>
/// <returns></returns>
public async Task SendMessage(long receiver,long sender, string message)
{
//判断接收的人是否在线
var receiveUser = SignalRMessageGroups.UserGroups.FirstOrDefault(m => m.UserId == receiver && m.GroupName == "ChatHubGroup");
if (receiveUser != null)
{
await Clients.Client(receiveUser.ConnectionId).SendAsync("ReceiveChater", new
{
sender,
message,
time = DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss")
});
await _chatService.InsertAsync(new Model.OaChat
{
Receiver = receiver,
Sender = sender,
Message = message
});
}
else
{
//发送邮件/短信提醒
}
} public async Task InitMessage(long userid)
{
await Groups.AddToGroupAsync(Context.ConnectionId, "ChatHubGroup");
var curUser = SignalRMessageGroups.UserGroups.FirstOrDefault(m => m.UserId == userid && m.GroupName == "ChatHubGroup");
if (curUser != null)
{
SignalRMessageGroups.UserGroups.Remove(curUser);
}
SignalRMessageGroups.UserGroups.Add(new SignalRMessageGroups
{
ConnectionId = Context.ConnectionId,
GroupName = "ChatHubGroup",
UserId = userid
});
//刷新在线用户列表 await Clients.All.SendAsync("RefreshOnliner", userid);
}
}

接口(获取在线人员)

[Authorize]
[Route("api/Chat/[action]")]
[Produces("application/json")]
[ApiController]
public class ChatController : ControllerBase
{
private ISystemService _systemService;
private readonly IOaChatService _chatService;
private IHubContext<ChatHub> _hubContext; public ChatController(IServiceProvider serviceProvider, ISystemService systemService,IOaChatService chatService)
{
_hubContext = serviceProvider.GetService<IHubContext<ChatHub>>();
this._systemService = systemService;
this._chatService = chatService;
} /// <summary>
/// 获取全部聊天用户
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<List<ChatUserViewModel>> GetChatUserAsync([FromBody]List<long> chattinguserids)
{
//获取用户
var allUsers = await _systemService.GetAllUserAsync();
List<ChatUserViewModel> chatUsers = allUsers.Select(m => new ChatUserViewModel
{
UserId = m.UserId,
UserName = m.UserName,
HeadImg = m.HeadImg,
CreateTime = m.CreateTime,
IsChatting = 0,
IsOnline = 0
}).ToList();
var userids = SignalRMessageGroups.UserGroups.Where(m => m.GroupName == "ChatHubGroup").Select(m => m.UserId); foreach (var item in chatUsers)
{
if (userids.Contains(item.UserId))
{
item.IsOnline = 1;
}
if (chattinguserids.HasItems() && chattinguserids.Contains(item.UserId))
{
item.IsChatting = 1;
}
}
return chatUsers.OrderByDescending(m => m.IsChatting).ThenByDescending(m => m.IsOnline).ThenBy(m => m.CreateTime).ToList(); ;
} [HttpGet]
public async Task<List<ChatUserListDto>> GetChatListAsync([FromQuery]ChatUserListSearchDto model)
{
return await _chatService.GetChatListAsync(model);
}
}

页面代码(css、js代码较多)

@{
ViewData["Title"] = "聊天";
Layout = "~/Views/Shared/_LayoutJQ.cshtml";
UserIdentity user = ViewBag.User;
}
@inject MsSystem.Web.Infrastructure.TokenClient tokenClient
@inject Microsoft.Extensions.Configuration.IConfiguration configuration
@section css{
<style>
body {
background:#fff !important;
}
.ibox-content{
border:none !important;
}
.chat-main {
display: inline-block;
background: #eee;
border-radius: 3px;
}
.chat-main-left {
width: 200px;
float: left;
overflow-y: auto;
display: none;
border: 1px solid #ddd;
box-shadow: 0px 0px 10px #ddd;
border-right: none;
}
.chat-main-right {
width: 600px;
float: left;
height: 550px;
border: 1px solid #ddd;
box-shadow: 0px 0px 10px #ddd;
border-radius: 3px;
} .chat-users{
margin-left:0px;
height:550px;
}
.chat-user {
cursor: pointer;
margin: 10px;
border-radius: 3px;
}
.chat-user.active {
background: #fff;
border-radius: 4px;
}
.currentUser {
background: #eee;
font-size: 12px;
text-align: left;
line-height: 60px;
height: 60px;
color: #333;
font-weight: bold;
opacity: .8;
}
.currentUser img{
width:45px;
height:45px;
margin-left:10px;
}
.message-count {
background: red;
color: #fff;
width: 15px;
height: 15px;
text-align: center;
border-radius: 50%;
display: none;
margin: 2px;
line-height: 15px;
}
.chatim {
position: absolute;
right: 0px;
background: #fff;
width: 260px;
border-radius: 3px;
bottom: 0px;
}
.chatim.active {
box-shadow: 0px 0px 10px #eee;
border: 1px solid #eee;
}
.chatim-title {
height: 55px;
line-height: 55px;
background: #eee;
padding-left: 10px;
}
.chatim-body {
height: 450px;
overflow-y: auto;
}
.chatim-userbox {
margin: 10px;
}
.chatim-userbox-category{
cursor:pointer;
}
.chatim-userbox-online a{
color:#333;
}
.chatim-userbox-notonline a{
color:#ddd;
}
.chatim ul {
list-style: none;
margin: 0px;
padding: 0px;
}
.chatim ul li{
cursor:pointer;
margin:10px;
}
.chat-user:not(.active):hover {
background: #fff;
}
.chat-user a{
margin-left:10px;
}
.chat-user-name {
text-align: left;
}
.chat-user-name .chat-close {
width: 16px;
height: 16px;
line-height: 16px;
border-radius: 50%;
background: #ddd;
color: #fff;
font-size: 12px;
margin-left: 30px;
position: relative;
text-align: center;
float: right;
display:none;
}
.chat-user-name:hover .chat-close {
display: inline-block;
background: #FF0000;
}
.chatim-user img {
width: 35px;
height: 35px;
}
.message-input{
border-right:none;
border-left:none;
}
#sendMessage {
position: relative;
top: -35px;
float: right;
border-radius: 0px;
width: 70px;
}
.chatim-title-username{
font-size:16px;
font-weight:bold;
margin:0px 10px;
}
.chatim-title img{
width:40px;
height:40px;
margin-top:-3px;
}
.chatim-user span{
margin:0px 10px;
}
.chatim-userbox-category span {
width: 12px;
display: inline-block;
font-size: 14px;
}
.chatim-footer{
text-align:center;
padding-bottom:10px;
}
.chatim-footer a {
font-size: 18px;
color: #fff;
cursor: pointer;
width: 38px;
height: 38px;
display: inline-block;
line-height: 38px;
border-radius: 50%;
background: #0e9aef;
box-shadow: 0px 0px 5px #0e9aef;
text-align: center;
}
.more-msg {
font-size: 12px;
color: #4ea9e9;
}
.more-msg:hover{
text-decoration:underline;
color: #4ea9e9;
}
</style>
}
@section scripts{
<script src="/lib/signalr/dist/browser/signalr.min.js"></script>
<script>
$(function () {
var chattinguserids = [];
var currentUserid = "@user.UserId";
const connection = new signalR.HubConnectionBuilder()
.withUrl("http://localhost:5000/hub/oa/chatHub", {
transport: signalR.HttpTransportType.LongPolling,
accessTokenFactory: () => {
return "Authorization", getToken();
}
}).build();
connection.start().then(function () {
connection.invoke('InitMessage', currentUserid);
}).catch(function (err) {
return console.error(err.toString());
});
//获取聊天信息
connection.on("ReceiveChater", function (data) {
//判断是否已打开
var _target = $('.users-list .chat-user[data-userid=' + data.sender + ']');
var userlength = $('.users-list .chat-user').length;
if (_target[0] == undefined) {
var issactive = userlength == 0 ? true : false;
var username = $('.chatim-body a[data-userid=' + data.sender + ']').text();
var img = $('.chatim-body a[data-userid=' + data.sender + ']').parent().prev().attr('src');
addLeftUser(data.sender, username, img, issactive);
resetDiscussionTitle(data.sender, username, img);
}
var chatuser = $('.users-list .chat-user[data-userid=' + data.sender + ']');
var msgcount = chatuser.find('.message-count').text();
chatuser.find('.message-count').show();
if (msgcount == '') {
chatuser.find('.message-count').text(1);
} else {
chatuser.find('.message-count').text(parseInt(msgcount) + 1);
}
var headimg = chatuser.find('img').attr('src');
var username = chatuser.find('.chat-user-name a').text();
var html = '';
html += '<div class="chat-message chat-left">';
html += '<img class="message-avatar" src="' + headimg +'" alt="">';
html += '<div class="message">';
html += '<div><a class="message-author">' + username +'</a>';
html += '<span class="message-date">' + data.time+'</span></div>';
html += '<span class="message-content">';
html += data.message;
html += '</span>';
html += '</div>';
html += '</div>';
addOrShowDiscussion(data.sender, false);
var relBox = $('.chat-discussion[data-touser=' + data.sender + ']');
relBox.append(html);
srcollBottom(relBox);
});
//获取在线人列表
connection.on("RefreshOnliner", data => {
axios.post('/OA/Chat/GetChatUserAsync', chattinguserids).then(function (response) {
var data = response.data;
$('.chatim-userbox ul').empty();
$.each(data, function (index, item) {
var html = '<li>';
html += '<div class="chatim-user">';
html += '<img src="' + item.HeadImg+'" />';
html += '<span><a data-userid="' + item.UserId + '">' + item.UserName + '</a></span>';
html += '</div>';
html += '</li>';
if (item.IsOnline == 1) {
$('.chatim-userbox-online ul').append(html);
} else {
$('.chatim-userbox-notonline ul').append(html);
}
});
});
});
function getToken() {
return '@tokenClient.GetToken().Result';
}
document.getElementById('sendMessage').addEventListener('click', function (e) {
e.preventDefault();
sendMessage();
});
$('textarea[name=message]').on('keydown', function (e) {
if (e.keyCode != 13) {
return;
} else {
event.preventDefault();
e.returnValue = false;
sendMessage();
}
});
$('.users-list').on('click', '.chat-user', function () {
$('.users-list .chat-user').removeClass('active');
$(this).addClass('active');
var currentDom = $(this).find('.chat-user-name a');
var userid = currentDom.attr('data-userid');
var imgsrc = $(this).find('img').attr('src');
resetDiscussionTitle(userid, currentDom.text(), imgsrc);
addOrShowDiscussion(userid, true);
resetUserMessageCount(userid);
addOnlines(userid);
$('.chat-message-form').show();
});
function sendMessage() {
var userid = $('#ToUserId').val();
var message = $('textarea[name=message]').val();
if (message == '' || message.trim() == '') {
return;
}
connection.invoke('SendMessage', userid, currentUserid, message);
$('textarea[name=message]').val('');
var time = $.getCurrentTime();
var html = '';
html += '<div class="chat-message chat-right">';
html += '<img class="message-avatar" src="@user.HeadImg" alt="">';
html += '<div class="message">';
html += '<div><a class="message-author">@user.UserName</a>';
html += '<span class="message-date">' + time + '</span></div>';
html += '<span class="message-content">';
html += message;
html += '</span>';
html += '</div>';
html += '</div>';
var chatdis = $('.chat-discussion[data-touser=' + userid + ']');
chatdis.append(html);
resetUserMessageCount(userid);
srcollBottom(chatdis);
}
function srcollBottom(container) {
var scrollToContainer = container.find('.chat-message :last');
container.animate({
scrollTop: scrollToContainer.offset().top - container.offset().top + container.scrollTop()
}, 800);
}
function resetUserMessageCount(userid) {
var chatuser = $('.users-list .chat-user[data-userid=' + userid + ']');
chatuser.find('.message-count').text('').hide();
}
function addOnlines(userid) {
var flag = false;
for (var i = 0; i < chattinguserids.length; i++) {
if (chattinguserids[i] == userid) {
flag = true;
}
}
if (!flag) {
chattinguserids.push(userid);
}
}
function showChatBox(userid) {
var length = $('.users-list .chat-user').length;
if (length <= 1) {
$('.chat-main-left').show();
}
addOrShowDiscussion(userid, true);
}
function addOrShowDiscussion(userid, toTop) {
var disc = $('.chat-discussion[data-touser=' + userid + ']');
if (disc.length == 1) {
$('.chat-discussion').hide();
disc.show();
} else {
var html = '';
if ($('.chat-discussion').length == 1 || toTop == true) {
$('.chat-discussion').hide();
html = '<div class="chat-discussion" data-pageindex="0" data-touser="' + userid + '"><a class="more-msg"><i class="fa fa-clock-o"></i>查看更多消息</a></div>';
} else {
html = '<div class="chat-discussion" style="display:none" data-pageindex="0" data-touser="' + userid + '"><a class="more-msg"><i class="fa fa-clock-o"></i>查看更多消息</a></div>';
}
$('.chat-discussion:last').after(html)
}
$('.chat-message-form').show();
}
function resetDiscussionTitle(userid, username,imgsrc) {
$('#ToUserId').val(userid);
$('.currentUser span').text(username);
$('.currentUser img').attr('src', imgsrc);
}
function addLeftUser(userid, username, img,isactive) {
var html = '';
if (isactive) {
$('.chat-user').removeClass('active');
html += '<div class="chat-user active" data-userid="' + userid + '">';
} else {
html += '<div class="chat-user" data-userid="' + userid + '">';
}
html += '<span class="pull-right message-count"></span>';
html += '<img class="chat-avatar" src="' + img + '" alt="">';
html += '<div class="chat-user-name"><a data-userid="' + userid + '">' + username + '</a><span class="chat-close"><i class="fa fa-close"></i></span>';
html += '</div>';
html += '</div>';
$('.users-list').append(html);
$('.chat-main-left').show();
}
$('.chatim-userbox-category').on('click', function () {
var type = $(this).attr('data-type');
if (type == 1) {
$(this).attr('data-type', 0);
$(this).find('span i').attr('class', 'fa fa-angle-right');
$(this).next().hide();
} else {
$(this).attr('data-type', 1);
$(this).find('span i').attr('class', 'fa fa-angle-down');
$(this).next().show();
}
});
$('.chatim-userbox-online ul').on('dblclick', 'li', function () {
var userid = $(this).find('a').attr('data-userid');
var username = $(this).find('a').text();
var _target = $('.users-list .chat-user[data-userid=' + userid + ']');
if (_target[0] == undefined) {
var img = $(this).find('img').attr('src');
addLeftUser(userid, username, img, true);
showChatBox(userid);
} else {
$('.users-list .chat-user').removeClass('active');
_target.addClass('active');
addOrShowDiscussion(userid, true);
}
var imgsrc = $(this).find('img').attr('src');
resetDiscussionTitle(userid, username, imgsrc);
});
$('.users-list').on('click', '.chat-close', function (e) {
e.stopPropagation();
var currentUser = $(this).parent().parent();
//当前会话只有一个情况
var uleg = $('.users-list .chat-user').length;
if (uleg == 1) {
currentUser.remove();
$('.chat-discussion').hide();
$('.chat-discussion:first').show();
resetDiscussionTitle('', '','/uploadfile/342bd59b-edf4-48cf-aa27-d13e5a0b70df.jpeg');
$('.chat-main-left,.chat-message-form').hide();
} else {
var prevUser = currentUser.prev();
if (prevUser.length == 1) {
var userid = prevUser.find('a').attr('data-userid');
var username = prevUser.find('a').text();
if (currentUser.hasClass('active')) {
prevUser.addClass('active');
}
addOrShowDiscussion(userid);
var imgsrc = prevUser.find('img').attr('src');
resetDiscussionTitle(userid, username, imgsrc)
} else {
var nextUser = currentUser.next();
if (nextUser.length == 1) {
var nextUserId = nextUser.find('a').attr('data-userid');
var nextUsername = nextUser.find('a').text();
if (currentUser.hasClass('active')) {
nextUser.addClass('active');
}
addOrShowDiscussion(nextUserId);
var imgsrc = nextUser.find('img').attr('src');
resetDiscussionTitle(nextUserId, nextUsername, imgsrc)
}
}
currentUser.remove();
}
}); $('.chatim-footer').on('click', 'a', function () {
var type = $(this).attr('data-type');
if (type == 1) {
$(this).attr('data-type', 0);
$('.chatim-title,.chatim-body').fadeOut();
$(this).find('i').attr('class', 'fa fa-comments');
$('.chatim').removeClass('active');
} else {
$(this).attr('data-type', 1);
$('.chatim-title,.chatim-body').fadeIn();
$(this).find('i').attr('class', 'fa fa-close');
$('.chatim').addClass('active');
}
});
$('.chat-main-right').on('click', 'a.more-msg', function () {
var _this = $(this);
var pageindex = _this.parent().attr('data-pageindex');
var receiver = _this.parent().attr('data-touser');
var url = '/OA/Chat/GetChatListAsync?PageIndex=' + pageindex + '&Receiver=' + receiver + '';
axios.get(url).then(function (response) {
var data = response.data;
if (data.length > 0) {
var ToUserId = $('#ToUserId').val();
var currentDis = $('.chat-discussion[data-touser=' + ToUserId + ']');
currentDis.find('a.more-msg').hide();
var html = '<a class="more-msg"><i class="fa fa-clock-o"></i>查看更多消息</a>';
$.each(data, function (index, item) {
var headimg = '';
var username = '';
if (item.Sender == '@user.UserId') {
headimg = '@user.HeadImg';
username = '@user.UserName';
html += '<div class="chat-message chat-right">';
} else {
headimg = $('.currentUser img').attr('src');
username = $('.currentUser span').text();
html += '<div class="chat-message chat-left">';
}
var time = $.unixToDate(item.CreateTime, true);
html += '<img class="message-avatar" src="' + headimg + '" alt="">';
html += '<div class="message">';
html += '<div><a class="message-author">' + username + '</a>';
html += '<span class="message-date">' + time + '</span></div>';
html += '<span class="message-content">';
html += item.Message;
html += '</span>';
html += '</div>';
html += '</div>';
});
var addpageindex = pageindex == 0 ? 1 : pageindex;
if (addpageindex == 1) {
currentDis.empty();
}
currentDis.prepend(html);
currentDis.attr('data-pageindex', parseInt(addpageindex) + 1);
} else {
_this.hide();
}
});
});
});
</script>
}
<div class="wrapper-content">
<div class="row">
<div class="ibox">
<div class="ibox-content">
<div class="row" style="margin:0px auto;text-align: center;">
<div class="chat-main">
<div class="chat-main-left">
<div class="chat-users">
<div class="users-list"></div>
</div>
</div>
<div class="chat-main-right">
<div class="currentUser">
<img src="/uploadfile/342bd59b-edf4-48cf-aa27-d13e5a0b70df.jpeg" />
<span></span>
<input type="hidden" id="ToUserId" value="" />
</div>
<div class="chat-discussion" data-touser="">
<h3>
WEB 在线聊天系统
</h3>
</div>
<div class="chat-message-form" style="display:none">
<div class="form-group">
<textarea class="form-control message-input" name="message"></textarea>
</div>
<button class="btn btn-primary" id="sendMessage">发送</button>
</div>
</div>
<div class="clear"></div>
</div>
</div> <div class="chatim active">
<div class="chatim-title">
<img src="@user.HeadImg" />
<span class="chatim-title-username">
@user.UserName
</span>
<span class="green">在线</span>
</div>
<div class="chatim-body">
<div class="chatim-userbox chatim-userbox-online">
<div class="chatim-userbox-category" data-type="1">
<span><i class="fa fa-angle-down"></i></span>在线用户
</div>
<ul></ul>
</div>
<div class="chatim-userbox chatim-userbox-notonline">
<div class="chatim-userbox-category" data-type="0">
<span><i class="fa fa-angle-right"></i></span>未在线用户
</div>
<ul style="display:none"></ul>
</div>
</div> <div class="chatim-footer">
<a data-type="1"><i class="fa fa-close"></i></a>
</div>
</div>
</div>
</div>
</div>
</div>

数据库设计,就一张表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0; -- ----------------------------
-- Table structure for oa_chat
-- ----------------------------
DROP TABLE IF EXISTS `oa_chat`;
CREATE TABLE `oa_chat` (
`Id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`Sender` bigint(20) NOT NULL COMMENT '发送方',
`Message` text CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '消息',
`Receiver` bigint(20) NOT NULL COMMENT '接收方',
`CreateTime` bigint(20) NOT NULL COMMENT '创建时间',
PRIMARY KEY (`Id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 156 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1;

效果如下:

双击在线用户发送并发送信息

用户接收到消息

部分代码地址:

https://github.com/wangmaosheng/MsSystem-BPM-ServiceAndWebApps/blob/master/src/Web/MVC/MsSystem.Web/Areas/OA/Views/Chat/Index.cshtml

也可以clone 下项目运行查看效果,docker功能已完成,可直接运行