集成RocketChat至现有的.Net项目中,为ChatGPT铺路
文章目录
- 前言
- 项目搭建
- 后端
- 前端
- 代理账号
- 鉴权方式介绍
- 登录校验模块
- 前端鉴权方式
- 后端鉴权方式
- 登录委托
- 使用登录委托
- 处理聊天消息
- 前端鉴权方式
- 后端校验方式
- 项目地址
前言
今天我们来聊一聊一个Paas的方案,如何集成到一个既有的项目中。
以其中一个需求为例子:在产品项目中,加入IM(即时通信)功能,开始徒手撸代码,会发现工作量很大,去github找开源项目,结果也可能事与愿违:功能不够强大,或者用不同的语言编写的,编译出来程序集无法集成到项目中。
可能当下最好的方案是利用独立的聊天功能组件,作为项目的中间件(Paas方案)。
- 组件是独立部署,独立运行的,功能的稳定性,搭建速度快,
- 作为基础设施服务,可以用在其他项目中,并且项目中的对接作为抽象层,可随时替换现有组件。
这个聊天组件就是RocketChat。
RocketChat 是一款免费,开源的聊天软件平台。
其主要功能是:群组聊天、相互通信、私密聊群、桌面通知、文件上传、语音/视频、截图等,实现了用户之间的实时消息转换。
https://github.com/RocketChat/Rocket.Chat
它本身是使用Meteor全栈框架以JavaScript开发的Web聊天服务器。本身带有一个精美的web端,甚至有开源的App端。
集成到一个既有的项目中我们是需要做减法的,然而在实际对接中,我们仍然需要解决一些问题:
首先是Rocket.Chat自己有一套独立的用户系统,其中登录鉴权逻辑,这一部分是我们不需要的。
第二是Rocket.Chat聊天功能依赖这个用户系统,需要简化流程同步用户信息,只保留用户,不需要权限,角色。
准备工作:搭建Rocket.Chat服务
Rocket.Chat有两套Api,一个是基于https的REST Api,和一个基于wss的Realtime API, https://developer.rocket.chat/reference/api/realtime-api
这两个Api都需要鉴权。
解决这个有两套方案,一个是通过完全的后端接管,两个Api都经过后端项目进行转发,另一个是后端只接管REST Api, Realtime API和Rocket.Chat服务直接通信
项目搭建
后端
新建一个.Net 6 Abp项目后,添加AbpBoilerplate.RocketChat库,AbpBoilerplate.RocketChat的具体介绍请参考https://blog.csdn.net/jevonsflash/article/details/128342430
dotnet add package AbpBoilerplate.RocketChat
在Domain层中创建IM项目,创建Authorization目录存放与IM鉴权相关的代码,ImWebSocket目录用于存放处理Realtime API相关的代码.
在搭建Rocket.Chat环节,还记得有一个设置管理员的步骤吗?在AdminUserName和AdminPassword配置中,指定这个管理员的密码,
管理员用于在用户未登录时,提供操作的权限主体,
"Im": {"Provider": "RocketChat","Address": "http://localhost:3000/","WebSocketAddress": "ws://localhost:3000/","AdminUserName": "super","AdminPassword": "123qwe","DefaultPassword": "123qwe"}
前端
用vue2来搭建一个简单的前端界面,需要用到以下库
- element-UI库
- axios
- vuex
- signalr
新建一个vue项目,在package.json中的 "dependencies"添加如下:
"axios": "^0.26.1",
"element-ui": "^2.15.6",
"@microsoft/signalr": "^5.0.6"
"vuex": "^3.6.2"
代理账号
代理账号是一个管理员账号
在程序的启动时,要登录这个管理员账号,并保存Token,程序停止时退出登录这个账号。
我们需要一个cache存储管理员账号的登录信息(用户ID和Token)
在Threads目录下创建ImAdminAgentAuthBackgroundWorker,
并在ImModule中注册这个后台任务
private async Task LoginAdminAgent()
{var userName = rocketChatConfiguration.AdminUserName;var password = rocketChatConfiguration.AdminPassword;var loginResult = await imManager.Authenticate(userName, password);if (loginResult.Success && loginResult.Content != null){var cache = imAdminAgentCache.GetCache("ImAdminAgent");await cache.SetAsync("UserId", loginResult.Content.Data.UserId);await cache.SetAsync("AuthToken", loginResult.Content.Data.AuthToken);await cache.SetAsync("UserName", userName);}else{throw new UserFriendlyException("无法登录IM服务Admin代理账号");}
}public override async void Stop()
{base.Stop();var cache = imAdminAgentCache.GetCache("ImAdminAgent");var token = (string)cache.Get("AuthToken", (i) => { return string.Empty; });var userId = (string)cache.Get("UserId", (i) => { return string.Empty; });if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(userId)){return;}using (_iocManager.IocContainer.BeginScope()) //extension method{_iocManager.Resolve<SessionContextDto>().Token = token;_iocManager.Resolve<SessionContextDto>().UserId = userId;_iocManager.Resolve<SessionContextDto>().IsAuthorized = true;try{await imManager.Logout();}catch (Exception ex){throw;}}}
SessionContextDto是一个会话上下文对象,在.net项目中,登录校验成功后写入,在请求Rocket.Chat的时候读取,并写入到请求头中。
在ImModule的PostInitialize方法中注册ImAdminAgentAuthBackgroundWorker
public override void PostInitialize()
{var workerManager = IocManager.Resolve<IBackgroundWorkerManager>();workerManager.Add(IocManager.Resolve<ImAdminAgentAuthBackgroundWorker>());
}
用户登录时,需要传用户名密码,用户名是跟.net项目中相同的,密码可以独立设置,也可以设定约定一个默认密码,那么新建用户和登录的时候,可以不用传密码,直接使用默认密码即可,用户成功登录后,将用户ID和Token回传给前端。
定义传输对象类AuthenticateResultDto
public class AuthenticateResultDto
{public string AccessToken { get; set; }public string UserId { get; set; }
}
在应用层中创建类ImAppService,创建应用层服务Authenticate,用于用户登录。
private async Task<AuthenticateResultDto> Authenticate(MatoAppSample.Authorization.Users.User user, string password = null)
{var loginResult = await _imManager.Authenticate(user.UserName, password);if (loginResult.Success){var userId = loginResult.Content.Data.UserId;var token = loginResult.Content.Data.AuthToken;this.imAuthTokenCache.Set(user.UserName, new ImAuthTokenCacheItem(userId, token), new TimeSpan(1, 0, 0));}else{this.imAuthTokenCache.Remove(user.UserName);throw new UserFriendlyException($"登录失败, {loginResult.Error}");}return new AuthenticateResultDto{AccessToken = loginResult.Content.Data.AuthToken,UserId = loginResult.Content.Data.UserId};
}
鉴权方式介绍
由于Rocket.Chat的Realtime API基于REST API基础上进行鉴权,在调用完成/api/v1/login
接口后,需要在已经建立的Websocket连接中发送
{"msg": "method","method": "login","id": "42","params":[{ "resume": "auth-token" }]
}
详见官方文档
在集成RocketChat时,对于Realtime API方案有二:
-
前端鉴权,前端通过Abp登录后,调用
/api/v1/login
接口,返回token之后存入前端Token缓存中,之后前端将与Rocketchat直接建立websocket联系,订阅的聊天消息和房间消息将被直接推送至前端。优点是消息订阅推送直接,效率较高,但前端需要同时顾及Abp的鉴权和RocketChat Realtime API鉴权,前端的代码逻辑复杂,代理账号逻辑复杂,后期扩展性差。小型项目适合此方式
-
后端鉴权,前端通过Abp登录后,调用
/api/v1/login
接口,返回token之后存入后端Token缓存中,由后端发起websocket连接,订阅的聊天消息和房间消息将被转发成signalR消息发送给前端,由后端缓存过期机制统一管理各连接的生命周期。优点是统一了前端的消息推送机制,架构更趋于合理,对于多用户端的大型项目,能够减少前端不必要的代码逻辑。但是后端的代码会复杂一些。适合中大型项目。
Realtime API 的前端鉴权
Realtime API 的后端鉴权
登录校验模块
前端鉴权方式
由于是从小程序,或者web端共用的所以要分别从Header和Cookie中获取登录信息,IHttpContextAccessor类型的参数用于从http请求上下文对象中访问Header或Cookie,
整个流程如下:
创建AuthorizedFrontendWrapper.cs,新建AuthorizationVerification方法,此方法是登录校验逻辑
private static void AuthorizationVerification(IHttpContextAccessor _httpContextAccessor, bool useAdminIfNotAuthorized, out StringValues? token, out StringValues? userId)
{var isCommonUserLoginPassed = true;token = _httpContextAccessor.HttpContext?.Request.Headers["X-Auth-Token"];userId = _httpContextAccessor.HttpContext?.Request.Headers["X-User-Id"];if (!ValidateToken(token, userId)){token = _httpContextAccessor.HttpContext?.Request.Cookies["chat_token"];userId = _httpContextAccessor.HttpContext?.Request.Cookies["chat_uid"];if (!ValidateToken(token, userId)){isCommonUserLoginPassed = false;}}var cache = Manager.GetCache("ImAdminAgent");if (!isCommonUserLoginPassed){if (useAdminIfNotAuthorized){//若不存在则取admin作为主体token = (string)cache.Get("AuthToken", (i) => { return string.Empty; });userId = (string)cache.Get("UserId", (i) => { return string.Empty; });if (!ValidateToken(token, userId)){throw new UserFriendlyException("操作未取得IM服务授权, 当前用户未登录,且初始代理用户未登录");}}else{throw new UserFriendlyException("操作未取得IM服务授权, 当前用户未登录");}}else{if ((string)cache.Get("UserId", (i) => { return string.Empty; }) == userId.Value){token = (string)cache.Get("AuthToken", (i) => { return string.Empty; });if (!ValidateToken(token, userId)){throw new UserFriendlyException("操作未取得IM服务授权, 初始代理用户未登录");}}}
}
后端鉴权方式
整个流程如下:
创建AuthorizedBackendWrapper.cs,新建AuthorizationVerification方法,登录校验代码如下
public void AuthorizationVerification(out string token, out string userId)
{User user = null;try{user = userManager.FindByIdAsync(abpSession.GetUserId().ToString()).Result;}catch (Exception){}var userName = user != null ? user.UserName : rocketChatConfiguration.AdminUserName;var password = user != null ? ImUserDefaultPassword : rocketChatConfiguration.AdminPassword;var userIdAndToken = imAuthTokenCache.Get(userName, (i) => { return default; });if (userIdAndToken == default){var loginResult = imManager.Authenticate(userName, password).Result;if (loginResult.Success && loginResult.Content != null){userId = loginResult.Content.Data.UserId;token = loginResult.Content.Data.AuthToken;var imAuthTokenCacheItem = new ImAuthTokenCacheItem(userId, token);imAuthTokenCache.Set(userName, imAuthTokenCacheItem, new TimeSpan(1, 0, 0));var userIdentifier = abpSession.ToUserIdentifier();if (userIdentifier != null){Task.Run(async () =>{await Login(imAuthTokenCacheItem, userIdentifier, userName);});}}else{var adminUserName = rocketChatConfiguration.AdminUserName;var adminPassword = rocketChatConfiguration.AdminPassword;var adminLoginResult = imManager.Authenticate(adminUserName, adminPassword).Result;if (adminLoginResult.Success && adminLoginResult.Content != null){userId = adminLoginResult.Content.Data.UserId;token = adminLoginResult.Content.Data.AuthToken;if (!ValidateToken(token, userId)){throw new UserFriendlyException("操作未取得IM服务授权, 无法登录账号" + userName);}}else{throw new UserFriendlyException("账号登录失败:" + adminLoginResult.Error);}}}else{userId = userIdAndToken.UserId;token = userIdAndToken.Token;}if (!ValidateToken(token, userId)){throw new UserFriendlyException("操作未取得IM服务授权, 登录失败");}
}
登录委托
在AuthorizedFrontendWrapper(或AuthorizedBackendWrapper)中
写一个登录委托AuthorizedChatAction,用于包装一个需要登录之后才能使用的操作
public static async Task AuthorizedChatAction(Func<Task> func, IocManager _iocManager)
{if (_iocManager.IsRegistered<SessionContextDto>()){string token, userId;AuthorizationVerification(out token, out userId);using (_iocManager.IocContainer.Begin()) //extension method{_iocManager.Resolve<SessionContextDto>().Token = token;_iocManager.Resolve<SessionContextDto>().UserId = userId;_iocManager.Resolve<SessionContextDto>().IsAuthorized = true;try{await func();}catch (Exception ex){throw;}}}else{throw new UserFriendlyException("没有注册即时通信会话上下文对象");}
}
使用登录委托
我们在创建IM相关方法的时候,需要用AuthorizedFrontendWrapper(或AuthorizedBackendWrapper),来包装登录校验的逻辑。
public async Task<bool> DeleteUser(long userId)
{var user = await _userManager.GetUserByIdAsync(userId);var result = await AuthorizedBackendWrapper.AuthorizedChatAction(() =>{return _imManager.DeleteUser(user.UserName);}, _iocManager);if (!result.Success || !result.Content){throw new UserFriendlyException($"删除失败, {result.Error}");}return result.Content;
}
处理聊天消息
前端鉴权方式
新建messageHandler_frontend_auth.ts
处理程序
客户端支持WebSocket的浏览器中,在创建socket后,可以通过onopen、onmessage、onclose和onerror四个事件对socket进行响应。
我已经封装好了一个WebSocket 通信模块\web\src\utils\socket.ts
,Socket对象是一个WebSocket抽象,后期将扩展到uniapp小程序项目上使用的WebSocket。通过这个对象可以方便的进行操作。
创建一个Socket对象wsConnection
,用于接收和发送基于wss的Realtime API消息
const wsRequestUrl: string = "ws://localhost:3000/websocket";const socketOpt: ISocketOption = {server: wsRequestUrl,reconnect: true,reconnectDelay: 2000,
};const wsConnection: Socket = new Socket(socketOpt);
WebSocket的所有操作都是采用事件的方式触发的,这样不会阻塞UI,是的UI有更快的响应时间,有更好的用户体验。
连接建立后,客户端和服务器就可以通过TCP连接直接交换数据。我们订阅onmessage事件触发newMsgHandler处理信息
wsConnection.$on("message", newMsgHandler);
当链接打开后,立即发送{"msg":"connect","version":"1","support":["1","pre2","pre1"]}
报文
wsConnection.$on("open", (newMsg) => {console.info("WebSocket Connected");wsConnection.send({msg: "connect",version: "1",support: ["1"],});});
建立链接后,会从Rocket.Chat收到connected消息,此时需要发送登录请求的消息到Rocket.Chat
接收到报文
"{"msg":"connected","session":"cMvzWpCNSCR24bwCf"}"
发送报文
{"msg":"method","method":"login","params":[{"resume":"wY67O8rJFyf2FrqD5vxpQjIUs5tdThmyfW_VaA7MrsG"}],"id":"1"}
接下来,在newMsgHandler方法中,根据msg类型,处理一系列的消息
const newMsgHandler: Function = (newMsgRaw) => {if (!getIsNull(newMsgRaw)) {if (newMsgRaw.msg == "ping") {wsConnection.send({msg: "pong",});} else if (newMsgRaw.msg == "connected") {let newMsg: ConnectedWsDto = newMsgRawlet session = newMsg.session;if (wsConnection.isConnected) {wsConnection.send({msg: "method",method: "login",params: [{resume: UserModule.chatToken,},],id: "1",});}} else if (newMsgRaw.msg == "added") {subEvent("stream-notify-user", "message");subEvent("stream-notify-user", "subscriptions-changed");subEvent("stream-notify-user", "rooms-changed");} else if (newMsgRaw.msg == "changed") {let newMsg: SubChangedWsDto = newMsgRawif (newMsg.collection == "stream-notify-user") {let fields = newMsg.fields;if (fields.eventName.indexOf("/") != -1) {let id = fields.eventName.split('/')[0];let eventName = fields.eventName.split('/')[1];if (eventName == "subscriptions-changed") {let args = fields.args;let msg: ISubscription = null;let method: string;args.forEach((arg) => {if (typeof arg == "string") {if (arg == "remove" || arg == "insert") {method = arg;}}else if (typeof arg == "object") {msg = arg}});$EventBus.$emit("getRoomSubscriptionChangedNotification", { msg, method });}else if (eventName == "rooms-changed") {let args = fields.args;let msg: RoomMessageNotificationDto = null;args.forEach((arg) => {if (typeof arg == "object") {msg = arg}});$EventBus.$emit("getRoomMessageNotification", msg.lastMessage);}}else {let id = fields.eventName}}else if (newMsg.collection == "stream-room-messages") {let fields = newMsg.fields;let id = fields.eventNamelet msg: MessageItemDto = fields.args;$EventBus.$emit("getRoomMessageNotification", msg);}}}
}
store/chat.ts文件中,定义了ChatState用于存储聊天信息,当有消息收到,或者房间信息变更时,更新这些存储对象
export interface IChatState {currentChannel: ChannelDto;channelList: Array<ChannelDto>;currentMessage: MessageDto;
}
后端校验方式
Login时将生成webSocket对象,并发送connect消息
public async Task Login(ImAuthTokenCacheItem imAuthTokenCacheItem, UserIdentifier userIdentifier, string userName)
{using (var webSocket = new ClientWebSocket()){webSocket.Options.RemoteCertificateValidationCallback = delegate { return true; };var url = Flurl.Url.Combine(rocketChatConfiguration.WebSocketHost, "websocket");await webSocket.ConnectAsync(new Uri(url), CancellationToken.None);if (webSocket.State == WebSocketState.Open){var model = new ImWebSocketConnectRequest(){Msg = "connect",Version = "1",Support = new string[] { "1" }};var jsonStr = JsonConvert.SerializeObject(model);var sendStr = Encoding.UTF8.GetBytes(jsonStr);await webSocket.SendAsync(sendStr, WebSocketMessageType.Text, true, CancellationToken.None);await Echo(webSocket, imAuthTokenCacheItem, userIdentifier, userName);}}
}
每次接收指令时,将判断缓存中的Token值是否合法,若不存在,或过期(session变化),将主动断开websocket连接
在接收Realtime API消息后,解析方式同前端鉴权逻辑
在拿到数据后,做signalR转发。
private async Task Echo(WebSocket webSocket, ImAuthTokenCacheItem imAuthTokenCacheItem, UserIdentifier userIdentifier, string userName)
{JsonSerializerSettings serializerSettings = new JsonSerializerSettings(){NullValueHandling = NullValueHandling.Ignore};var buffer = new byte[1024 * 4];var receiveResult = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);string session=string.Empty;ImAuthTokenCacheItem im;while (!receiveResult.CloseStatus.HasValue){im = imAuthTokenCache.GetOrDefault(userName);if (im == null){await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure,"缓存超时自动退出",CancellationToken.None);Console.WriteLine(userName + "超时主动断开IM连接");break;}else{if (!string.IsNullOrEmpty(session) && im.Session!=session){await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure,"缓存更新自动退出",CancellationToken.None);Console.WriteLine(userName + "缓存更新主动断开IM连接");break;}}var text = Encoding.UTF8.GetString(buffer.AsSpan(0, receiveResult.Count));if (!string.IsNullOrEmpty(text)){dynamic response = JsonConvert.DeserializeObject<dynamic>(text);if (response.msg == "ping"){var model = new ImWebSocketCommandRequest(){Msg = "pong",};var jsonStr = JsonConvert.SerializeObject(model, serializerSettings);var sendStr = Encoding.UTF8.GetBytes(jsonStr);await webSocket.SendAsync(sendStr, WebSocketMessageType.Text, true, CancellationToken.None);}if (response.msg == "connected"){session = response.session;var model = new ImWebSocketCommandRequest(){Msg = "method",Method = "login",Params = new object[]{new {resume = imAuthTokenCacheItem.Token,}},Id = "1"};imAuthTokenCacheItem.Session = session;imAuthTokenCache.Set(userName, imAuthTokenCacheItem, new TimeSpan(1, 0, 0));var jsonStr = JsonConvert.SerializeObject(model, serializerSettings);var sendStr = Encoding.UTF8.GetBytes(jsonStr);await webSocket.SendAsync(sendStr, WebSocketMessageType.Text, true, CancellationToken.None);}else if (response.msg == "added"){await SubEvent(webSocket, imAuthTokenCacheItem, "stream-notify-user", "message");await SubEvent(webSocket, imAuthTokenCacheItem, "stream-notify-user", "subscriptions-changed");await SubEvent(webSocket, imAuthTokenCacheItem, "stream-notify-user", "rooms-changed");}else if (response.msg == "changed"){var newMsg = response;if (newMsg.collection == "stream-notify-user"){var fields = newMsg.fields;var fullEventName = fields.eventName.ToString();if (fullEventName.IndexOf("/") != -1){var id = fullEventName.Split('/')[0];var eventName = fullEventName.Split('/')[1];if (eventName == "subscriptions-changed"){var args = fields.args;dynamic msg = null;var method = string.Empty;foreach (var arg in args as IEnumerable<dynamic>){if (arg.ToString() == "remove" || arg.ToString() == "insert"){method = arg.ToString();}else{msg = arg;}}await signalREventPublisher.PublishAsync(userIdentifier, "getRoomSubscriptionChangedNotification", new { msg, method });}else if (eventName == "rooms-changed"){var args = fields.args;dynamic msg = null;var method = string.Empty;foreach (var arg in args as IEnumerable<dynamic>){if (arg.ToString() == "updated"){method = arg.ToString();}else{msg = arg;}};var jobject = msg.lastMessage as JObject;await signalREventPublisher.PublishAsync(userIdentifier, "getRoomMessageNotification", jobject);}}else{var id = fields.eventName;}}}else if (response.collection == "stream-room-messages"){var fields = response.fields;var id = fields.eventName;var msg = fields.args;var jobject = msg as JObject;await signalREventPublisher.PublishAsync(userIdentifier, "getRoomMessageNotification", jobject);}}try{receiveResult = await webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer), CancellationToken.None);}catch (Exception ex){Console.WriteLine(userName + "异常断开IM连接");break;}}try{await webSocket.CloseAsync(
receiveResult.CloseStatus.Value,
receiveResult.CloseStatusDescription,
CancellationToken.None);}catch (Exception ex){}imAuthTokenCache.Remove(userName);}private async Task SubEvent(WebSocket webSocket, ImAuthTokenCacheItem imAuthTokenCacheItem, string name, string type)
{var eventstr = $"{imAuthTokenCacheItem.UserId}/${type}";var id = RandomHelper.GetRandom(100000).ToString().PadRight(5, '0');var model = new ImWebSocketCommandRequest(){Msg = "sub",Params = new object[]{eventstr,new {useCollection= false,args = new string[]{ }}},Id = id,Name = name,};var jsonStr = JsonConvert.SerializeObject(model);var sendStr = Encoding.UTF8.GetBytes(jsonStr);await webSocket.SendAsync(sendStr, WebSocketMessageType.Text, true, CancellationToken.None);
}
SignalREventPublisher.cs 中的PublishAsync,将消息转发给对应的用户。
public async Task PublishAsync(IUserIdentifier userIdentifier, string method, object message)
{try{var onlineClients = _onlineClientManager.GetAllByUserId(userIdentifier);foreach (var onlineClient in onlineClients){var signalRClient = _hubContext.Clients.Client(onlineClient.ConnectionId);if (signalRClient == null){Logger.Debug("Can not get user " + userIdentifier.ToUserIdentifier() + " with connectionId " + onlineClient.ConnectionId + " from SignalR hub!");continue;}await signalRClient.SendAsync(method, message);}}catch (Exception ex){Logger.Warn("Could not send notification to user: " + userIdentifier.ToUserIdentifier());Logger.Warn(ex.ToString(), ex);}}
前端代码则要简单得多
新建messageHandler_backend_auth.ts
处理程序
import * as signalR from "@microsoft/signalr";
创建一个HubConnection对象hubConnection
,用于接收SignalR消息
const baseURL = "http://localhost:44311/"; // url = base url + request url
const requestUrl = "signalr";
let header = {};
if (UserModule.token) {header = {"X-XSRF-TOKEN": UserModule.token,Authorization: "Bearer " + UserModule.token,};
}//signalR config
const hubConnection: signalR.HubConnection = new signalR.HubConnectionBuilder().withUrl(baseURL + requestUrl, {headers: header,accessTokenFactory: () => getAccessToken(),transport: signalR.HttpTransportType.WebSockets,logMessageContent: true,logger: signalR.LogLevel.Trace,}).withAutomaticReconnect().withHubProtocol(new signalR.JsonHubProtocol()).build();
我们只需要响应后端程序中定义好的signalR消息的methodName就可以了
hubConnection.on("getRoomMessageNotification", (n: MessageItemDto) => {console.info(n.msg)if (ChatModule.currentChannel._id != n.rid) {ChatModule.increaseChannelUnread(n.rid);} else {if (n.t == null) {n.from =n.u.username == UserModule.userName? constant.MSG_FROM_SELF: constant.MSG_FROM_OPPOSITE;} else {n.from = constant.MSG_FROM_SYSTEM;}ChatModule.appendMessage(n);}
});hubConnection.on("getRoomSubscriptionChangedNotification", (n) => {console.info(n.method, n.msg)if (n.method == "insert") {console.info(n.msg + "has been inserted!");ChatModule.insertChannel(n.msg);}else if (n.method == "update") {}
});
至此,完成了所有的集成工作。
此文目的是介绍一种思路,使用缓存生命周期管理的相关机制,规避第三方用户系统对现有项目的用户系统的影响。举一反三,可以用到其他Paas的方案集成中。最近ChatGPT很火,可惜没时间研究怎么接入,有闲工夫的同学们可以尝试着写一个ChatGPT聊天机器人,欢迎大家评论留言!
最终效果如图
项目地址
Github:matoapp-samples
相关文章:
集成RocketChat至现有的.Net项目中,为ChatGPT铺路
文章目录前言项目搭建后端前端代理账号鉴权方式介绍登录校验模块前端鉴权方式后端鉴权方式登录委托使用登录委托处理聊天消息前端鉴权方式后端校验方式项目地址前言 今天我们来聊一聊一个Paas的方案,如何集成到一个既有的项目中。 以其中一个需求为例子:…...
王道操作系统课代表 - 考研计算机 第三章 内存管理 究极精华总结笔记
本篇博客是考研期间学习王道课程 传送门 的笔记,以及一整年里对 操作系统 知识点的理解的总结。希望对新一届的计算机考研人提供帮助!!! 关于对 “内存管理” 章节知识点总结的十分全面,涵括了《操作系统》课程里的全部…...
Cypher中的聚合
深解Cypher中的聚合 值或计数的聚合要么从查询返回,要么用作多步查询下一部分的输入。查看数据模型 CALL db.schema.visualization() 查看图中节点的属性类型 CALL db.schema.notetypeproperties() 查看图中关系的属性类型 CALL db.schema.reltypeproperties() C…...
图注意网络GAT理解及Pytorch代码实现【PyGAT代码详细注释】
文章目录GAT代码实现【PyGAT】GraphAttentionLayer【一个图注意力层实现】用上面实现的单层网络测试加入Multi-head机制的GAT对数据集Cora的处理csr_matrix()处理稀疏矩阵encode_onehot()对label编号build graph邻接矩阵构造GAT的推广GAT 题:Graph Attention Netwo…...
项目成本管理中的常见误区及解决方案
做过项目的人都明白,项目实施时间一般很长,在实施期间总有很多项目结果不尽人意的问题。要使一个项目取得成功,就要结合很多因素一起才能作用,其中做好项目成本的管理就是最重要的步骤之一,下面列出了常见的项目成本管…...
墨天轮2022年度数据库获奖名单
2022年,国家相继从高位部署、省级试点布局、地市重点深入三个维度,颁布了多项中国数据库行业发展的利好政策。但是我们也能清晰地看到,中国数据库行业发展之路道阻且长,而道路上的“拦路虎”之一则是生态。中国数据库的发展需要多…...
仓储调度|库存管理系统
技术:Java、JSP等摘要:随着电子商务技术和网络技术的快速发展,现代物流技术也在不断进步。物流技术是指与物流要素活动有关的所有专业技术的总称,包括各种操作方法、管理技能等,物流业采用某些现代信息技术方面的成功经…...
Canvas入门-01
导读: 读完全文需要2min。通过这篇文章,你可以了解到以下内容: Canvas标签基本属性如何使用Canvas画矩形、圆形、线条、曲线、笑脸😊 如果你曾经了解过Canvas,可以对照目录回忆一下能否回答上来 毕竟带着问题学习最有效…...
运算符优先级
醋坛酸味罐,位落跳福豆 醋:初等运算符: () [] -> . 坛:单目运算符: - ~ – * & ! sizeof 右结合 酸:算术运算符: - * / % 味:位移运算符:>> << …...
微信小程序使用scss编译wxss文件的配置步骤
文章目录1、在 vscode 中搜索 easysass 插件并安装2、在微信开发工具中导入安装的easysass插件3、修改 spook.easysass-0.0.6/package.json 文件中的配置4、重启开发者工具,就可用使用了微信小程序开发者工具集成了 vscode 编辑器,可以使用 vscode 中众多…...
一步一步教你如何使用 Visual Studio Code 编译一段 C# 代码
以下是一步一步教你如何使用 Visual Studio Code 编写使用 C# 语言输出当前日期和时间的代码: 1、下载并安装 .NET SDK。您可以从 Microsoft 官网下载并安装它。 2、打开 Visual Studio Code,并安装 C# 扩展。您可以在 Visual Studio Code 中通过扩展菜…...
vue-cli中的环境变量注意点
在客户端侧代码中使用环境变量只有以 VUE_APP_ 开头的变量会被 webpack.DefinePlugin 静态嵌入到客户端侧的包中。你可以在应用的代码中这样访问它们:console.log(process.env.VUE_APP_SECRET)在构建过程中,process.env.VUE_APP_SECRET 将会被相应的值所…...
2.3数据类型
文章目录1. 命名规则2.字符3.数字4.日期5.图片1. 命名规则 字段名必须以字母开头,尽量不要使用拼音长度不能超过30个字符(不同数据库,不同版本会有不同)不能使用SQL的保留字,如where,order,group只能使用如下字符a-z、…...
Kafka基本概念
什么是Kafka Kafka是一个消息系统。它可以集中收集生产者的消息,并由消费者按需获取。在Kafka中,也将消息称为日志(log)。 一个系统,若仅有一类或者少量的消息,可直接进行发送和接收。 随着业务量日益复杂,消息的种类…...
使用QueryBuilders、NativeSearchQuery实现复杂查询
使用QueryBuilders、NativeSearchQuery实现复杂查询 本文继续前面文章《ElasticSearch系列(二)springboot中集成使用ElasticSearch的Demo》,在前文中,我们介绍了使用springdata做一些简单查询,但是要实现一些高级的组…...
taobao.open.account.update( Open Account数据更新 )
¥开放平台免费API不需用户授权 Open Account数据更新 公共参数 请求地址: HTTP地址 http://gw.api.taobao.com/router/rest 公共请求参数: 公共响应参数: 响应参数 点击获取key和secret 请求示例 TaobaoClient client new DefaultTaobaoClient(url, appkey, sec…...
PT100铂电阻温度传感器
PT100温度传感器又叫做铂热电阻。 热电阻是中低温区﹡常用的一种温度检测器。它的主要特点是测量精度高,性能稳定。其中铂热电阻的测量精确度是﹡高的,它不仅广泛应用于工业测温,而且被制成标准的基准仪。金属热…...
蓝桥杯-本质上升序列
没有白走的路,每一步都算数🎈🎈🎈 题目描述: 小蓝特别喜欢单调递增的事物 在一个字符串中如果取出若干个字符,按照在原来字符串中的顺序排列在一起,组成的新的字符串如果是单调递增的…...
synchronized锁重入验证
文章目录synchronized锁重入验证1. 可重入锁2. synchronized锁重入2.1 本类同步方法内部调用本类其它同步方法2.2 子类同步方法内部调用父类的同步方法2.3 A类的同步方法内部调用B类的同步方法3. synchronized修饰方法写法synchronized锁重入验证 1. 可重入锁 可重入锁&#…...
超简单的计数排序!!
假设给定混乱数据为:3,0,1,3,6,5,4,2,1,9。 下面我们将通过使用计数排序的思想来完成对上面数据的排序。(先不谈负数) 计数排序 该排序的思路和它的名字一样…...
发现新大陆——原来软件开发根本不需要会编码(看我10分钟应用上线)
目录 一、前言 二、官网基础功能及搭建 三、体验过程 01、连接数据源 02、设计表单 03、流程设计 04、图表呈现 05、组织架构设置 五、效率评价 六、小结 一、前言 众所周知,每家公司在发展过程中都需要构建大量的内部系统, 如运营使用的用户…...
【Leedcode】栈和队列必备的面试题(第二期)
【Leedcode】栈和队列必备的面试题(第二期) 文章目录【Leedcode】栈和队列必备的面试题(第二期)一、题目(用两个队列实现栈)二、思路图解1.定义两个队列2.初始化两个队列3.往两个队列中放入数据4.两个队列出…...
Elasticsearch实战之(商品搜索API实现)
Elasticsearch实战之(商品搜索API实现) 1、案例介绍 某医药电商H5商城基于Elasticsearch实现商品搜索 2、案例分析 2.1、数据来源 商品库 - 平台运营维护商品库 - 供应商维护 2.2、数据同步 2.2.1、同步双写 写入 MySQL,直接也同步往…...
剑指 Offer 14-剪绳子
摘要 剑指 Offer 14- I. 剪绳子 剑指 Offer 14- II. 剪绳子 II 343. 整数拆分 一、动态规划解析 这道题给定一个大于1的正整数n,要求将n 拆分成至少两个正整数的和,并使这些正整数的乘积最大化,返回最大乘积。令x是拆分出的第…...
泰克示波器|MSO64示波器的应用
泰克新一代示波器MSO64为实例来讲解时频域信号分析技术。MSO64采用全新TEK049平台,不仅实现了4通道同时打开时25GS/s的高采样率,而且实现了12-bit高垂直分辨率。同时,由于采用了新型低噪声前端放大ASIC—TEK061,大大降低了噪声水平…...
1.4 黑群晖安装:SataPortMap和DiskIdxMap两种获取方式
tinycore及安装工具下载:工具:链接:https://pan.baidu.com/s/1CMLl6waOuW-Ys2gKZx7Jgg?pwdchct提取码:chcttinycore:链接:https://pan.baidu.com/s/19lchzLj-WDXPQu2cEcskBg?pwddcw2 提取码:d…...
JVM虚拟机概述(2)
3.JVM 运行时数据区 3.1.1 程序计数器(Program Counter Register) 是一块很小的内存空间,用来记录每个线程运行的指令位置,是线程私有的,每个线程都拥有一个程序计数器,生命周期与线程一致,是运行时数据区中唯一一个不…...
Intel CSME 简述
SME 算是 Intel X86 PC 上最神秘的部分了,本文根据 us-19-Hasarfaty-Behind-The-Scenes-Of-Intel-Security-And-Manageability-Engine 一文写成。讲述内容无法证伪,各位随便听听即可,了解这些能够帮助BIOS 工程师更好的理解一些操作的实现。文章基于 Intel 第八代第九代CPU(…...
复位理论基础
先收集资料,了解当前常用的基础理论和实现方式 复位 初始化微控制器内部电路 将所有寄存器恢复成默认值确认MCU的工作模式禁止全局中断关闭外设将IO设置为高阻输入状态等待时钟趋于稳定从固定地址取得复位向量并开始执行 造成复位的原因 有多种引起复位的因素&…...
Python基础知识——列表
列表 列表是可以存放任何数据,包括整型,浮点型,字符串,布尔型等等,是常用的数据类型之一。 1.列表的创建 列表也是一个可迭代对象 1. 普通形式l [1,2,3,4,5] ---整型列表l ["a","b","c&…...
wordpress插件分享显示/中国新闻
首先声明本文旨在介绍centos环境下安装Docker-CE(社区版),社区版是免费提供给个人开发者和小团队,Docker-EE (企业版)有额外费用,想了解其他系统下搭建,请传送《docker官网》 准备工作 1、docker要求Linux内…...
怎么样做网站/seo优化工具
最近因为工作需求原因一直使用VUE框架,作为时下最热门的渐进式框架,开发起来确实非常给力~ 当然一个好的工具也不可能完全对你百依百顺,最近在工作中就遇到了一个问题,经过一下午的奋战终于搞定了,秉承着本熊一贯的无私…...
如何查找网站建设时间/韩国vs加纳分析比分
双链表是一种重要的线性存储结构,对于双链表中的每个节点,不仅仅存储自己的信息,还要保存前驱和后继节点的地址。PHP SPL中的SplDoublyLinkedList类提供了对双链表的操作。SplDoublyLinkedList类摘要如下:SplDoublyLinkedList imp…...
网站模板 简洁/制作网站的全过程
SIP有多种定义和解释,其中一说是多芯片堆叠的3D封装内系统集成,在芯片的正方向堆叠2片以上互连的裸芯片的封装。SIP是强调封装内包含了某种系统的功能封装,3D封装仅强调在芯片方向上的多芯片堆叠,如今3D封装已从芯片堆叠发展到封装堆叠,扩大了3D封装的内涵。 …...
药检局信息化网站系统建设方案/新闻最新消息
本节课演示如何以动画的方式,显示或隐藏指定的位图,该功能在日常的开发工作里还是挺常见的。 首先添加一个布尔属性,标识是否显示或隐藏指定的视图。 添加另一个字符串属性,作为和密码输入框进行绑定的状态属性。 添加一个VStack视图,作为子视图的容器。 然后添加一个…...
wordpress上传媒体/seo搜索引擎专员
原标题:计算器知识详解计算器是现代人发明的可以进行数字运算的电子机器。现代的电子计算器能进行数学运算的手持电子机器,拥有集成电路芯片,但结构比电脑简单得多,可以说是第一代的电子计算机(电脑),且功能也较弱&…...