需求背景
上周,运营的同事提了一个非正式需求,说希望在我们的一个业务板块里,增加一个计时板的功能。更具体的功能也没有细说,当然也不是非常要紧的需求,也就没有限制上线时间,自由发挥。
那这周开始,有时间我就准备做一下这个小模块,结果不做不知道,一做就做了 2 天。。。
那在这里,就来聊聊,为啥一个计时模块做了 2 天才做完。
明确需求
运营虽然没有提具体需求,但肯定是不能影响原来的操作流程,属于锦上添花的功能,想开就开,相关就关。
基于这个大方向,我这就大体琢磨了一下。
首先,这个功能要加到的是在线问辩这个业务板块上的,那既然是问辩,所以要保证所有人的计时板是统一启动或者暂停的,而且,一旦发生突发情况,重新计入问辩房间后,倒计时应该是继续上一次的计时而不能重新计,也就是这个计时的模式,类似于裁判员吹哨,大家收到的时间应该是一致的,而不是各计各。还有一点就是,计时的模式,应该分为倒计时和正计时两种。
这样梳理过后,需求就稍微明朗一些了
计时模块应该是和问辩房间绑定,由管理人员控制房间是否需要开启计时功能
计时分为两种形式,正计时和倒计时
倒计时要限制计时时长,计时结束后问辩环节也就随之结束;而正计时不限制时长,最终问辩结束的时长将作为评分维度之一
房间内的不同人员的计时应该由管理人员统一控制启动和暂停
计时时长应该定期同步到服务端,保证问辩过程出现特殊情况后,再次进入房间后计时仍然是统一的。
计时模块的呈现风格应该简约方便
基本就是这些了,那接下来就准备开干吧
整活
计时模型
首先,要确定一个计时模型来方便数据的同步和传输,模型中包含,房间号,是否开启计时,计时方式(正,倒计时),倒计时的时长,倒计时剩余的时长,计时持续时长等几个属性,长这样👇
{
"roomMode": 2,
"timingMode": 1,
"countdownTime": 6,
"leftTime": 1.5,
"lastTime": 0.5,
"roomId": 6666,
"status": 0
}
复制代码
客户端即使消息模块
这里我引入了 signalr 模块,来实现实时消息,方便问辩房间内各用户接受来自控制中心发送的计时指令和一些额外的文字消息。
用 signalr 的好处就是不用自己控制传输协议,框架会自动选择,默认是 websocket,如果不支持的话会降级成 sse,在不支持就降成长轮询,具体的使用方式,可以参加微软的文档:👉:https://learn.microsoft.com/zh-cn/aspnet/signalr/overview/getting-started/introduction-to-signalr
这里即时消息用来完成的任务就是,发送指令和文字消息。
前端的基础代码差不多长这样👇
//创建连接对象connection
const signalr_connection = new signalR.HubConnectionBuilder()
.withUrl("/MyHub")
.withAutomaticReconnect()
.configureLogging(signalR.LogLevel.Information)
.build();
//监听文字消息
signalr_connection.on("ReceiveRoomMessage", function (msg, roomId, userId) {
if (roomId && roomId != getQueryString("roomId"))
return;
if (userId && userId != getQueryString("userId"))
return;
if (msg && roomId && userId) {
TT.notify("收到了来自房间【" + roomId + "】的消息:" + msg);
}
if (msg && roomId) {
TT.notify("收到了来自房间【" + roomId + "】的广播消息:" + msg);
} else if (msg) {
TT.notify("收到了广播消息:" + msg);
}
});
//监听计时指令
signalr_connection.on("ReceiveTimingCmd", function (cmd, roomId, callback) {
if (!cmd || !roomId) {
TT.error("参数缺失");
return;
}
if (roomId && roomId != getQueryString("roomId")) {
//TT.error("房间号参数错误")
return;
}
if (cmd == "start") {
start();
}
else if (cmd == "pause") {
pause();
}
if (typeof (callback) == "function") {
callback();
}
});
//发送指令
function sendMsg(msg,roomId, method, feedback=true) {
$.post("@Url.Action("ForwardMsg")", { "msg": msg, "roomId": roomId, "method": method }, function (json) {
if(!feedback){
return;
}
if(json.code==1)
TT.info("指令发送成功");
else
TT.error("指令发送失败");
})
}
复制代码
服务端
对应的服务端要有注册即时消息中间件,还要有一个转发消息的服务端接口,差不多就长这样👇
//中间件
public class MyHub : Hub
{
public override Task OnConnectedAsync()
{
Console.WriteLine("Connected->User:" + this.Context.User);
Console.WriteLine("Connected->UserIdentifier:" + this.Context.UserIdentifier);
Console.WriteLine("Connected->ConnectionId:" + this.Context.ConnectionId);
int user_cnt = RedisHelper.HKeys("chathash").Length;
base.Clients.All.SendAsync("OnlineCount", user_cnt.ToString());//推送全局,也可以推送给指定用户
if (!string.IsNullOrEmpty(this.Context.UserIdentifier))
base.Clients.All.SendAsync("OnlineMsg", this.Context.UserIdentifier + "|" + this.Context.User.Identity.Name);
return base.OnConnectedAsync();
}
public override Task OnDisconnectedAsync(Exception exception)
{
Console.WriteLine("Disconnected->ConnectionId:" + this.Context.UserIdentifier);
var path = this.Context.GetHttpContext().Request;
if (!string.IsNullOrEmpty(this.Context.UserIdentifier))
{
base.Clients.All.SendAsync("Offline", this.Context.UserIdentifier + "|" + this.Context.User.Identity.Name);//推送全局,也可以推送给指定用户 /
int user_cnt = RedisHelper.HKeys("chathash").Length;
base.Clients.All.SendAsync("OnlineCount", user_cnt.ToString());//推送全局,也可以推送给指定用户
}
return base.OnDisconnectedAsync(exception);
}
//发送消息--发送给所有连接的客户端
public Task SendMessage(string msg)
{
return Clients.All.SendAsync("ReceiveMessage", msg);
}
//发送消息--发送给所有连接的客户端
public async Task SendMessage2(string userId, string msg)
{
Console.WriteLine($"user:{userId},msg:{msg},{DateTime.Now}");
await Clients.All.SendAsync("ReceiveMessage", userId, msg);
}
//发送消息--发送给指定用户
//默认情况下,使用 SignalRClaimTypes.NameIdentifier从ClaimsPrincipal与作为用户标识符连接相关联
//所以使用自带授权Authorize登陆时,可以把用户id保存在NameIdentifier中
public Task SendPrivateMessage(string userId, string message)
{
Console.WriteLine($"user:{userId},msg:{message},{DateTime.Now}");
return Clients.User(userId).SendAsync("ReceiveMessage", message);
}
}
//转发接口
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ForwardMsg(ChatConfig config)
{
try
{
if (string.IsNullOrEmpty(config.msg))
{
return Json(resp.ret(0, "无效消息"));
}
//跨房间指定人发消息
else if (string.IsNullOrEmpty(config.roomId) && !string.IsNullOrEmpty(config.userId))
await _myHub.Clients.All.SendAsync(config.method, config.msg, config.userId);
//房间内指定人发消息
else if (!string.IsNullOrEmpty(config.roomId) && !string.IsNullOrEmpty(config.userId))
await _myHub.Clients.All.SendAsync(config.method, config.msg, config.roomId, config.userId);
//房间内广播
else if (!string.IsNullOrEmpty(config.roomId))
await _myHub.Clients.All.SendAsync(config.method, config.msg, config.roomId);
//跨房间群发
else
await _myHub.Clients.All.SendAsync(config.method, config.msg);
return Json(resp.success("发送成功"));
}
catch(Exception ex)
{
return Json(resp.ret(-1, "发送失败", ex.Message));
}
}
复制代码
除此之外,还要有一个同步时间的接口,用来统一客户端的计时时长,这样即使问辩过程发生一些不可预知的问题,再次回到问辩房价,还是可以最大限度保留上次的计时时长的,这里说最大限度是因为我这里不是实时同步的,而是每隔 30 秒同步一次,所以如果发生突发情况,还是需要管理人员介入,好让大家统一同步一下时间。
//同步时间
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SyncRoomConfig(string roomId, double minutes, long timestamp, int isPause=0)
{
if (isPause == 1)
{
minutes = Math.Round((DateTime.Now - Utils.TimeStampToDateTime(timestamp)).TotalMinutes, 1);
}
if (await RedisHelper.HExistsAsync("lastSyncTime", roomId))
{
var lastSyncTime = Convert.ToInt64(await RedisHelper.HGetAsync("lastSyncTime", roomId));
TimeSpan ts = Utils.TimeStampToDateTime(timestamp) - Utils.TimeStampToDateTime(lastSyncTime);
if (ts.TotalMinutes < minutes && isPause==0)
{
return Json(resp.ret(0, "未到同步间隔"));
}
}
string configStr = await RedisHelper.HGetAsync("chatrooms", roomId);
var config = JsonHelper.JsonDeserialize<TimingModel>(configStr);
if (config.timingMode == 1 && config.leftTime>0)
{
config.leftTime -= minutes;//倒计时
}
config.lastTime += minutes;//持续时长不论正计时还是倒计时都要累加记录
await RedisHelper.HSetAsync("chatrooms", roomId, JsonHelper.JsonSerialize(config));
await RedisHelper.HSetAsync("lastSyncTime", roomId, timestamp);
return Json(resp.success(config));
}
复制代码
稍加美化
这部分,其实不具备典型性,主要的目的就是使组件看起来更顺眼,而且只要方便,简约不影响主要业务的运行,只要别太丑,基本都说得过去
我这边基本就不贴代码了,贴几个截图得了
到此,这个符合当前问辩需求场景的计时模块,基本是完成了。当然还有很多细节的点我没提到,只是把通用的地方提取了一下,仅供参考。
当然,通过这个小模块的开发,还是想说,需求本身是没有大小的,可能我们理解开发一个常见的功能非常简单,但实际想要开发的这个功能要怎么融入到现有的业务系统中,是有侵入的融入还是无侵入的融入,要不要打乱原来的操作流程,新增的功能是怎么定位的,以及会不会给用户造成困扰,这些都是要考虑的。像这个计时的模块,它本身是一个比较重要的功能点,但却不能触及核心业务,不能打断问辩,而是要恰到好处的提醒问辩人员,把握好时间。同样,如果去掉这个功能,它也不应该影响到原来的业务,这就是这个计时板的定位。
好了,差不多就聊这些~
ps.同步发表于 csdn👉:https://blog.csdn.net/juanhuge/article/details/128630865
评论