From 7e5d20a64a22073da99be34afe1d228d5b906773 Mon Sep 17 00:00:00 2001 From: youngq Date: Fri, 20 Sep 2024 15:37:12 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E8=80=83=E5=8B=A4=E8=A1=A8Ex?= =?UTF-8?q?cel=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AgoraCallbackComsuerService.cs | 90 +++++++++++++ .../BackgroudServices/AgoraDataService.cs | 56 -------- .../Frontend/AgoraCallbackController.cs | 104 ++++++++++++++- .../Controllers/Frontend/HomeController.cs | 125 +++++++++++++++++- WGShare.API/Helpers/AgoraHelper.cs | 54 ++++++++ WGShare.API/Helpers/DateTimeUtils.cs | 41 ++++++ WGShare.API/Helpers/RedisHelper.cs | 70 +--------- WGShare.API/Program.cs | 2 +- WGShare.API/WGShare.API.csproj | 3 + WGShare.API/WGShare.API.xml | 52 ++++++++ WGShare.API/appsettings.Development.json | 12 +- WGShare.API/appsettings.json | 6 +- WGShare.API/meetingRecordTemplate.xlsx | Bin 0 -> 10143 bytes WGShare.Domain/Constant/RedisKeyConstant.cs | 7 + .../DTOs/AgoraCallback/EventBody.cs | 54 +++++++- .../DTOs/Room/RoomMettingRecordExportDTO.cs | 30 +++++ WGShare.Domain/Entities/MeetingRecord.cs | 101 ++++++++++++++ WGShare.Domain/Enums/LeaveReasonEnum.cs | 59 +++++++++ WGShare.Domain/Enums/PlatformType.cs | 22 +++ WGShare.Domain/WGShare.Domain.csproj | 2 +- 20 files changed, 748 insertions(+), 142 deletions(-) create mode 100644 WGShare.API/BackgroudServices/AgoraCallbackComsuerService.cs delete mode 100644 WGShare.API/BackgroudServices/AgoraDataService.cs create mode 100644 WGShare.API/Helpers/DateTimeUtils.cs create mode 100644 WGShare.API/meetingRecordTemplate.xlsx create mode 100644 WGShare.Domain/DTOs/Room/RoomMettingRecordExportDTO.cs create mode 100644 WGShare.Domain/Entities/MeetingRecord.cs create mode 100644 WGShare.Domain/Enums/LeaveReasonEnum.cs create mode 100644 WGShare.Domain/Enums/PlatformType.cs diff --git a/WGShare.API/BackgroudServices/AgoraCallbackComsuerService.cs b/WGShare.API/BackgroudServices/AgoraCallbackComsuerService.cs new file mode 100644 index 0000000..9415a1c --- /dev/null +++ b/WGShare.API/BackgroudServices/AgoraCallbackComsuerService.cs @@ -0,0 +1,90 @@ +using Hangfire; +using Hangfire.Server; +using Mapster; +using Masuit.Tools; +using SqlSugar; +using WGShare.API.Helpers; +using WGShare.Domain.Constant; +using WGShare.Domain.DTOs.AgoraCallback; +using WGShare.Domain.Entities; + +namespace WGShare.API.BackgroudServices +{ + public class AgoraCallbackComsuerService : BackgroundService + { + private readonly ILogger _logger; + private readonly ISqlSugarClient _sugarClient; + private readonly SemaphoreSlim _semaphore; + + public AgoraCallbackComsuerService(ILogger logger, ISqlSugarClient sugarClient) + { + _logger = logger; + this._sugarClient = sugarClient; + _semaphore = new SemaphoreSlim(1);// 允许最多1个线程同时访问 + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("参会记录监听服务已启动"); + + stoppingToken.Register(() => _logger.LogInformation("RedisSubscriberService is stopping.")); + + // 离开频道消息订阅 + using (RedisHelper.Instance.SubscribeList(RedisKeyConstant.PubSub.MeetingRecord, async (message) => + { + _logger.LogDebug($"接受消息: {message}"); + if (message == null) return; + + + var body = message.ToString().FromJson(); + if (body == null) + { + _logger.LogError("消息体为空"); + return; + } + + if (body.payload.uid.ToString().Length == 9) + { + _logger.LogInformation("共享屏幕不记录参会记录"); + return; + } + + await MeetingRecord(body); + + })) + + while (!stoppingToken.IsCancellationRequested) + { + await Task.Delay(10000, stoppingToken); // 等待一段时间,然后继续循环 + } + + _logger.LogInformation("参会记录监听服务已停止"); + } + + public async Task MeetingRecord(EventBody body) + { + await _semaphore.WaitAsync(); + + try + { + + var entity = body.payload.Adapt(); + entity.EventType = body.eventType; + + await _sugarClient.Storageable(entity) + .WhereColumns(x => new { x.uid, x.clientSeq }) + .ToStorage() + .AsInsertable + .ExecuteCommandAsync(); + } + catch (Exception e) + { + throw; + } + finally + { + _semaphore.Release(); + } + } + } +} diff --git a/WGShare.API/BackgroudServices/AgoraDataService.cs b/WGShare.API/BackgroudServices/AgoraDataService.cs deleted file mode 100644 index 8af0e34..0000000 --- a/WGShare.API/BackgroudServices/AgoraDataService.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Hangfire; -using Hangfire.Server; -using Masuit.Tools; -using SqlSugar; -using WGShare.API.Helpers; -using WGShare.Domain.Entities; - -namespace WGShare.API.BackgroudServices -{ - public class OssCleanWorker : BackgroundService - { - private readonly ILogger _logger; - private readonly ISqlSugarClient _sugarClient; - private readonly OssHelper _ossHelper; - - public OssCleanWorker(ILogger logger, ISqlSugarClient sugarClient, OssHelper ossHelper) - { - _logger = logger; - this._sugarClient = sugarClient; - this._ossHelper = ossHelper; - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - RecurringJob.AddOrUpdate("DeleteOssFile", () => DeleteOssFile(), Cron.Daily()); - } - - public async Task DeleteOssFile() - { - await Console.Out.WriteLineAsync($@"开始清除过期文件,当前时间:{DateTime.Now}"); - - // 查找已删除,未文件清理,并过期180天的文件 - var deleteFiles = await _sugarClient.Queryable() - .Where(x => x.IsDelete == true && x.IsFileClean == false && SqlFunc.DateDiff(DateType.Day, x.ModifyTime, SqlFunc.GetDate()) > 180) - .ToListAsync(); - if (deleteFiles.IsNullOrEmpty()) - { - await Console.Out.WriteLineAsync($@"当前无可清除文件,退出本次操作,当前时间:{DateTime.Now}"); - return; - } - - await Console.Out.WriteLineAsync($@"本次清除文件数量:{deleteFiles.Count}"); - await Console.Out.WriteLineAsync($@"本次清除文件路径:{string.Join(',', deleteFiles.Select(x => x.FileUrl))}"); - - if (_ossHelper.DeleteObjects(deleteFiles.Select(x => x.FileUrl).ToArray())) - { - await _sugarClient.Updateable() - .SetColumns(x => x.IsFileClean == true) - .Where(x => deleteFiles.Select(a => a.Id).Contains(x.Id)) - .ExecuteCommandAsync(); - } - - await Console.Out.WriteLineAsync($@"本次清除操作结束,当前时间:{DateTime.Now}"); - } - } -} diff --git a/WGShare.API/Controllers/Frontend/AgoraCallbackController.cs b/WGShare.API/Controllers/Frontend/AgoraCallbackController.cs index 0ceb99a..8b4e689 100644 --- a/WGShare.API/Controllers/Frontend/AgoraCallbackController.cs +++ b/WGShare.API/Controllers/Frontend/AgoraCallbackController.cs @@ -2,13 +2,18 @@ using AgoraIO.Rtm; using Mapster; using Masuit.Tools; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using SharpCompress; using SqlSugar; +using System.Diagnostics.CodeAnalysis; using System.Net.Http; using WGShare.API.Controllers.Basic; using WGShare.API.Helpers; using WGShare.Domain.AgoraApiResult; using WGShare.Domain.Constant; +using WGShare.Domain.DTOs.AgoraCallback; using WGShare.Domain.DTOs.Room; using WGShare.Domain.Entities; using WGShare.Domain.FriendlyException; @@ -21,21 +26,112 @@ namespace WGShare.API.Controllers.Frontend /// Agora接口 /// [ApiExplorerSettings(GroupName = "frontend")] - [Route("agora-cb")] + [Route("agora-cb"), AllowAnonymous] public class AgoraCallbackController : BasicController { private readonly ILogger _logger; + private readonly AgoraHelper _agoraHelper; + private readonly IConfiguration _configuration; public AgoraCallbackController( - ILogger logger) + ILogger logger, + AgoraHelper agoraHelper, + IConfiguration configuration) { this._logger = logger; + this._agoraHelper = agoraHelper; + this._configuration = configuration; } + [HttpPost("event")] - public async Task Event() + public async Task Event() { - return string.Empty; + using var reader = new StreamReader(Request.Body); + var bodyString = await reader.ReadToEndAsync(); + + //_logger.LogInformation($"Agora回调内容:{bodyString}"); + + // 校验请求体是否为空 + if (string.IsNullOrWhiteSpace(bodyString)) + { + await ExceptionNotice.SendAsync(new ArgumentNullException("Agora-Signature-V2 声网请求体为空"), "声网回调异常"); + return; + } + + var body = bodyString.FromJson(); + if (body == null) + { + await ExceptionNotice.SendAsync(new InvalidDataException("Agora-Signature-V2 声网请求体反序列化失败"), "声网回调异常"); + return; + } + + // 声网健康检查回调,直接返回 + if (body.payload.channelName == "test_webhook") + { + _logger.LogDebug($"测试回调,测试信息:{bodyString}"); + return; + } + + // 验证签名 + var sig = Request.Headers["Agora-Signature-V2"].ToString(); + if (!await _agoraHelper.CheckSignatureAsync(sig, bodyString)) + { + _logger.LogWarning($"Agora回调签名验证失败,声网签名:{sig}"); + return; + } + + switch (body.eventType) + { + case Domain.Enums.EventType.channel_create: + break; + case Domain.Enums.EventType.channel_destroy: + break; + case Domain.Enums.EventType.broadcaster_join_channel: + case Domain.Enums.EventType.audience_join_channel: + JoinChannelEvent(bodyString); + break; + case Domain.Enums.EventType.broadcaster_leave_channel: + case Domain.Enums.EventType.audience_leave_channel: + LeaveChannelEvent(bodyString); + break; + case Domain.Enums.EventType.client_role_change_to_broadcaster: + break; + case Domain.Enums.EventType.client_role_change__to_audience: + break; + default: + await ExceptionNotice.SendAsync(new Exception("声网事件未知类型"), "声网回调异常"); + break; + } + + } + + /// + /// 加入频道 + /// + [NonAction] + private void JoinChannelEvent(string bodyString) + { + _logger.LogDebug($"Agora回调内容 加入频道:{bodyString}"); + + RedisHelper.Instance.LPush(RedisKeyConstant.PubSub.MeetingRecord, bodyString); + } + + /// + /// 离开频道 + /// + [NonAction] + private void LeaveChannelEvent(string bodyString) + { + _logger.LogDebug($"Agora回调内容 离开频道:{bodyString}"); + + // 离会记录 + RedisHelper.Instance.LPush(RedisKeyConstant.PubSub.MeetingRecord, bodyString); + } + + } + + } diff --git a/WGShare.API/Controllers/Frontend/HomeController.cs b/WGShare.API/Controllers/Frontend/HomeController.cs index 82c10c0..fae5b6b 100644 --- a/WGShare.API/Controllers/Frontend/HomeController.cs +++ b/WGShare.API/Controllers/Frontend/HomeController.cs @@ -3,17 +3,20 @@ using AgoraIO.Rtm; using Mapster; using Masuit.Tools; using Microsoft.AspNetCore.Mvc; +using MiniExcelLibs; using SqlSugar; -using System.Net.Http; +using System.IO; +using System.Text.RegularExpressions; using WGShare.API.Controllers.Basic; using WGShare.API.Helpers; -using WGShare.Domain.AgoraApiResult; using WGShare.Domain.Constant; using WGShare.Domain.DTOs.Room; using WGShare.Domain.Entities; +using WGShare.Domain.Enums; using WGShare.Domain.FriendlyException; using WGShare.Domain.GeneralModel; using Yitter.IdGenerator; +using IConfiguration = Microsoft.Extensions.Configuration.IConfiguration; namespace WGShare.API.Controllers.Frontend { @@ -29,19 +32,22 @@ namespace WGShare.API.Controllers.Frontend private readonly IHttpClientFactory _httpClientFactory; private readonly AgoraHelper _agoraHelper; private readonly ILogger _logger; + private readonly OssHelper _ossHelper; public HomeController( ISqlSugarClient sqlSugar, IConfiguration configuration, IHttpClientFactory httpClientFactory, AgoraHelper agoraHelper, - ILogger logger) + ILogger logger, + OssHelper ossHelper) { _sqlSugar = sqlSugar; this._configuration = configuration; this._httpClientFactory = httpClientFactory; this._agoraHelper = agoraHelper; this._logger = logger; + this._ossHelper = ossHelper; } @@ -101,6 +107,20 @@ namespace WGShare.API.Controllers.Frontend return await _sqlSugar.Insertable(entity).ExecuteCommandAsync() > 0; } + + /// + /// 删除会议室 + /// + /// + /// + [HttpDelete("room")] + public async Task DeleteRoom([FromQuery] string roomId) + { + return await _sqlSugar.Updateable() + .SetColumns(x => x.IsDelete == true) + .Where(x => x.Id == roomId).ExecuteCommandAsync() > 0; + } + /// /// 获取 rtm token /// @@ -124,5 +144,104 @@ namespace WGShare.API.Controllers.Frontend { return _configuration["Agora:appId"].ToString(); } + + /// + /// 获取会议记录 + /// + /// + [HttpGet("record")] + public async Task GetMeetingRecord([FromQuery] long beginTimestamp, [FromQuery] long endTimestamp, [FromQuery] string roomNum) + { + var room = await _sqlSugar.Queryable().Where(x => x.RoomNum == roomNum && x.IsDelete == false).FirstAsync(); + if (room == null) + { + throw Oops.Oh("该会议室不存在!"); + } + + var recordList = await _sqlSugar.Queryable() + .InnerJoin((mr, u) => mr.uid == u.Id) + .InnerJoin((mr, u, r) => u.RoleId == r.Id) + .Where((mr, u, r) => mr.channelName == roomNum && mr.ts >= beginTimestamp && mr.ts <= endTimestamp) + .Select((mr, u, r) => new MeetingRecord + { + Id = mr.Id, + uid = u.Id, + ts = mr.ts, + account = mr.account, + channelName = mr.channelName, + clientSeq = mr.clientSeq, + CreateTime = mr.CreateTime, + duration = mr.duration, + EventType = mr.EventType, + platform = mr.platform, + reason = mr.reason, + userName = u.UserName, + UserAccount = u.Account, + RoleName = r.RoleName, + }).ToListAsync(); + + + if (recordList.IsNullOrEmpty()) + { + throw Oops.Oh("该时间段内没有记录!"); + } + + var groupByUser = recordList.GroupBy(x => x.uid).ToList(); + + var value = new RoomMettingRecordExportDTO + { + RoomName = room.RoomName, + RoomNum = room.RoomNum, + BeginTime = DateTimeUtils.FromJavaScriptTimestampToLocal(beginTimestamp).ToString("yyyy-MM-dd HH:mm:ss"), + EndTime = DateTimeUtils.FromJavaScriptTimestampToLocal(endTimestamp).ToString("yyyy-MM-dd HH:mm:ss"), + Users = new List() + }; + + foreach (var userRecord in groupByUser) + { + if (userRecord.IsNullOrEmpty()) + { + continue; + } + // 按时间排序 + var orderedRecord = userRecord.OrderBy(x => x.ts).ToList(); + + // 获取第一次进入房间的事件记录 + var firstJoinTime = orderedRecord + .FirstOrDefault(x => x.EventType == EventType.broadcaster_join_channel + || x.EventType == EventType.audience_join_channel); + + // 获取最后一次离开房间的事件记录 + var lastLeaveTime = orderedRecord + .LastOrDefault(x => x.EventType == EventType.broadcaster_leave_channel + || x.EventType == EventType.audience_leave_channel); + + // 计算入会次数 + var joinCount = orderedRecord.Count(x => x.EventType == EventType.broadcaster_join_channel + || x.EventType == EventType.audience_join_channel); + + // 累计参会时长 + var sumTime = orderedRecord.Sum(x => x.duration); + + value.Users.Add(new UserBehavior + { + Account = userRecord.FirstOrDefault().UserAccount, + FirstJoinTime = DateTimeUtils.FromJavaScriptTimestampToLocal(firstJoinTime.ts).ToString("yyyy-MM-dd HH:mm:ss"), + LastExitTime = DateTimeUtils.FromJavaScriptTimestampToLocal(lastLeaveTime.ts).ToString("yyyy-MM-dd HH:mm:ss"), + JoinCount = joinCount, + Role = userRecord.FirstOrDefault().RoleName, + UserName = userRecord.FirstOrDefault().userName, + SumTime = sumTime, + }); + } + + using var stream = new MemoryStream(); + + await MiniExcel.SaveAsByTemplateAsync(stream, $@"{AppDomain.CurrentDomain.BaseDirectory}meetingRecordTemplate.xlsx", value); + stream.Seek(0, SeekOrigin.Begin); + var fileName = $@"excel/{room.RoomName}-参会记录.xlsx"; + _ossHelper.UploadWithExpireTime(fileName, stream, 10); + return _ossHelper.GetAccessFileUrl(fileName, 5); + } } } diff --git a/WGShare.API/Helpers/AgoraHelper.cs b/WGShare.API/Helpers/AgoraHelper.cs index 778acc1..5f73984 100644 --- a/WGShare.API/Helpers/AgoraHelper.cs +++ b/WGShare.API/Helpers/AgoraHelper.cs @@ -1,6 +1,7 @@ using Masuit.Tools; using System.Drawing.Printing; using System.Net.Http; +using System.Security.Cryptography; using System.Text; using WGShare.Domain.AgoraApiResult; using WGShare.Domain.FriendlyException; @@ -127,6 +128,59 @@ namespace WGShare.API.Helpers } + // 将加密后的字节数组转换成字符串 + private string BytesToHex(byte[] bytes) + { + StringBuilder sb = new StringBuilder(); + foreach (byte b in bytes) + { + string hex = b.ToString("x2"); + sb.Append(hex); + } + return sb.ToString(); + } + // HMAC/SHA256 加密,返回加密后的字符串 + private string HmacSha256(string message, string secret) + { + try + { + byte[] keyBytes = Encoding.UTF8.GetBytes(secret); + byte[] messageBytes = Encoding.UTF8.GetBytes(message); + + using (HMACSHA256 hmacsha256 = new HMACSHA256(keyBytes)) + { + byte[] hashBytes = hmacsha256.ComputeHash(messageBytes); + return BytesToHex(hashBytes); + } + } + catch (Exception e) + { + throw new Exception("Error in HmacSha256", e); + } + } + + /// + /// 验证声网回调签名 + /// + /// + /// + /// + public async Task CheckSignatureAsync(string agoraSig, string bodyString) + { + if (string.IsNullOrWhiteSpace(agoraSig)) + { + await ExceptionNotice.SendAsync(new ArgumentNullException("Agora-Signature-V2 声网密钥为空"), "声网回调异常"); + return false; + } + + if (HmacSha256(bodyString, _configuration["Agora:eventSecret"].ToString()) != agoraSig) + { + await ExceptionNotice.SendAsync(new ArgumentNullException("Agora-Signature-V2 声网密钥验证失败"), "声网回调异常"); + return false; + } + + return true; + } } } diff --git a/WGShare.API/Helpers/DateTimeUtils.cs b/WGShare.API/Helpers/DateTimeUtils.cs new file mode 100644 index 0000000..b5e7d13 --- /dev/null +++ b/WGShare.API/Helpers/DateTimeUtils.cs @@ -0,0 +1,41 @@ +namespace WGShare.API.Helpers +{ + public class DateTimeUtils + { + + // Unix 纪元时间 + private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + /// + /// 将 JavaScript 时间戳(以秒为单位)转换为 DateTime 对象(UTC 时间)。 + /// + /// JavaScript 时间戳(以秒为单位)。 + /// 对应的 DateTime 对象(UTC 时间)。 + public static DateTime FromJavaScriptTimestamp(long jsTimestamp) + { + return Epoch.AddSeconds(jsTimestamp); + } + + /// + /// 将 DateTime 对象(UTC 时间)转换为 JavaScript 时间戳(以秒为单位)。 + /// + /// DateTime 对象(UTC 时间)。 + /// 对应的 JavaScript 时间戳(以秒为单位)。 + public static long ToJavaScriptTimestamp(DateTime dateTime) + { + return (long)(dateTime.ToUniversalTime() - Epoch).Seconds; + } + + /// + /// 将 JavaScript 时间戳(以秒为单位)转换为本地时间的 DateTime 对象。 + /// + /// JavaScript 时间戳(以秒为单位)。 + /// 对应的本地时间的 DateTime 对象。 + public static DateTime FromJavaScriptTimestampToLocal(long jsTimestamp) + { + DateTime utcDateTime = FromJavaScriptTimestamp(jsTimestamp); + return utcDateTime.ToLocalTime(); + + } + } +} diff --git a/WGShare.API/Helpers/RedisHelper.cs b/WGShare.API/Helpers/RedisHelper.cs index bcf82c7..835b7ba 100644 --- a/WGShare.API/Helpers/RedisHelper.cs +++ b/WGShare.API/Helpers/RedisHelper.cs @@ -45,74 +45,6 @@ namespace WGShare.API.Helpers /// 最大秒数 /// public static int RandomExpired(int minTimeoutSeconds, int maxTimeoutSeconds) => rnd.Value.Next(minTimeoutSeconds, maxTimeoutSeconds); - - //public static List HVals(string key) where T : class - //{ - // var valueStrings = Instance.HVals(key); - // if (valueStrings.IsNullOrEmpty()) - // { - // return null; - // } - - // return valueStrings.ToList().ConvertAll(x => JsonConvert.DeserializeObject(x)); - //} - - //public static T HGet(string key, string field) where T : class - //{ - // var valueString = Instance.HGet(key, field); - // if (valueString.IsNullOrEmpty()) - // { - // return null; - // } - // return JsonConvert.DeserializeObject(valueString); - //} - - //public static List HMGet(string key, params string[] fields) where T : class - //{ - // var valueStrings = Instance.HMGet(key, fields); - // if (valueStrings.IsNullOrEmpty()) - // { - // return null; - // } - // return valueStrings.ToList().ConvertAll(x => JsonConvert.DeserializeObject(x)); - //} - - //public static Dictionary HGetAll(string key) where T : class - //{ - // var dic = Instance.HGetAll(key); - // if (dic == null || dic.Count == 0) - // { - // return null; - // } - // Dictionary result = new Dictionary(); - - // foreach (var kv in dic) - // { - // if (string.IsNullOrWhiteSpace(kv.Key) || string.IsNullOrWhiteSpace(kv.Value)) - // { - // continue; - // } - // result.Add(kv.Key, JsonConvert.DeserializeObject(kv.Value)); - // } - - // return result; - //} - - - //public static long HSet(string key, string fields, T value) where T : class - //{ - // return Instance.HSet(key, fields, value.ToJsonString()); - //} - - - //public static void HMSet(string key, Dictionary keyValues) where T : class - //{ - // var dic = new Dictionary(); - // foreach (var kv in keyValues) - // { - // dic.Add(kv.Key,kv.Value.ToJsonString()); - // } - // Instance.HMSet(key, dic); - //} + } } diff --git a/WGShare.API/Program.cs b/WGShare.API/Program.cs index 2f3088d..b7ff144 100644 --- a/WGShare.API/Program.cs +++ b/WGShare.API/Program.cs @@ -66,7 +66,7 @@ namespace WGShare.API builder.Services.AddSingleton(new JwtHelper(configuration)); builder.Services.AddSingleton(new AgoraHelper(configuration)); builder.Services.AddSingleton(new OssHelper(configuration)); - builder.Services.AddHostedService(); + builder.Services.AddHostedService(); builder.Services.AddAuth(configuration["Jwt:Issuer"], configuration["Jwt:Audience"], configuration["Jwt:SecretKey"]); diff --git a/WGShare.API/WGShare.API.csproj b/WGShare.API/WGShare.API.csproj index cbe3796..b2a151f 100644 --- a/WGShare.API/WGShare.API.csproj +++ b/WGShare.API/WGShare.API.csproj @@ -29,6 +29,9 @@ + + PreserveNewest + Never diff --git a/WGShare.API/WGShare.API.xml b/WGShare.API/WGShare.API.xml index 16316fb..b86f7e0 100644 --- a/WGShare.API/WGShare.API.xml +++ b/WGShare.API/WGShare.API.xml @@ -66,6 +66,16 @@ Agora接口 + + + 加入频道 + + + + + 离开频道 + + 首页接口 @@ -85,6 +95,13 @@ + + + 删除会议室 + + + + 获取 rtm token @@ -97,6 +114,12 @@ + + + 获取会议记录 + + + 会议室接口 @@ -335,6 +358,35 @@ + + + 验证声网回调签名 + + + + + + + + 将 JavaScript 时间戳(以秒为单位)转换为 DateTime 对象(UTC 时间)。 + + JavaScript 时间戳(以秒为单位)。 + 对应的 DateTime 对象(UTC 时间)。 + + + + 将 DateTime 对象(UTC 时间)转换为 JavaScript 时间戳(以秒为单位)。 + + DateTime 对象(UTC 时间)。 + 对应的 JavaScript 时间戳(以秒为单位)。 + + + + 将 JavaScript 时间戳(以秒为单位)转换为本地时间的 DateTime 对象。 + + JavaScript 时间戳(以秒为单位)。 + 对应的本地时间的 DateTime 对象。 + 获取上传url diff --git a/WGShare.API/appsettings.Development.json b/WGShare.API/appsettings.Development.json index fa7b4e5..697dbbe 100644 --- a/WGShare.API/appsettings.Development.json +++ b/WGShare.API/appsettings.Development.json @@ -1,13 +1,14 @@ { "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Default": "Debug", + "Microsoft.AspNetCore": "Information" }, "Console": { "LogLevel": { - "Default": "Information", - "Microsoft": "Warning" + "Default": "Debug", + "Microsoft": "Warning", + "Hangfire": "Information" } //"FormatterName": "CustomTimePrefixingFormatter", //"FormatterOptions": { @@ -43,6 +44,7 @@ "tokenExpireTimeInSecond": 7200, "apiPrefix": "https://api.sd-rtn.com/", "clientId": "80cdc24f7dfa4497a37d98da95a3c4a4", - "clientSecret": "8323581d4d464114b1f324b26cc62e09" + "clientSecret": "8323581d4d464114b1f324b26cc62e09", + "eventSecret": "gPPaFQS1U" } } diff --git a/WGShare.API/appsettings.json b/WGShare.API/appsettings.json index e4d688f..28d75ef 100644 --- a/WGShare.API/appsettings.json +++ b/WGShare.API/appsettings.json @@ -3,6 +3,9 @@ "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" + }, + "Console": { + "TimestampFormat": "[yyyy-MM-dd HH:mm:ss]" } }, "AllowedHosts": "*", @@ -33,6 +36,7 @@ "AccessKeySecret": "FKFNYRdS53FwA5ME2wM1585qX5eVEd", "Endpoint": "oss-cn-chengdu.aliyuncs.com", "Host": "https://wgshare.oss-cn-chengdu.aliyuncs.com/", - "BucketName": "wgshare" + "BucketName": "wgshare", + "eventSecret": "" } } diff --git a/WGShare.API/meetingRecordTemplate.xlsx b/WGShare.API/meetingRecordTemplate.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..824af456cd9e85c533a62df7ac9b363a393464e3 GIT binary patch literal 10143 zcmaJ{bwE|i)~6e#rMpYIyIZ=u`@o^QyF7^ow3xdQEDnp?HU@ATJlcd|?4?~xk z>vQ-#&ewXPs7u#!RlF{gSKqw8VE9a)SLS(kH8DQK*`<|oDItAmnir!uO>tx%#YGH> ziNNhJV&vCUD~=ze8_K7Srpp2&>bY`}>Q8`~MM<3Cs$C_IKn8^)vLMw3A^mz`;-o7-9|1wYX_lx2Yr$m63OCBX zuw$C{>Uq|;^~(+4LCcO0SN<>D?sY9UYLVJ-pi1ja`qMkwMYGWD^Q4@m2OHMV?0`lU zk;+45!bc1%IoroguJDDf`j;|VP4f*4CyT{Bk!mKWijE6Cv_=MoNaliPErWvoOD=1P z`Mp&RXQ@$J8*|>SNv|l!&NIC|Q(xu2Nd4FqKE_$o=4V^DO4NN1^*i>`?}pf)u$Kg3 zkNQvSO#nc%C-7qudSv>Tu!lh4gKU@7=+rvtn)faVvM2Hj19`4at-LJSk?T3f0^|*L z&Op>lGLtx?c^8)g*tqq_v6qj>vTRwHE$2`&7RAvC$EchBo5jPM%PWP_k?Kw4l~wcyEUo3lndiL`pl5O@ija?G)tF4;tOMx= zi58$7grtaG7^WG9uE$JP@sL@+4s;vcSK5jZazNt7AdVUA+}Qxk`_o?JzQ{0*qi12Q zK9sXt_GwyMswuSKIp}VE+reXNz3^S2lfXciy|6X?^I;^U49ERQI)_Qy=si;Z9-mfM zXSF_7F5pAO7Vx1q`~*Ob)=nec0%0!1b*0tH&?0N!TX*gpoahh5pNgdG7oR9_6p|d@ z26tR~6w> zyC?XI6ZB;Jm@q<*p&vzueKOb{#w~g8Y6INYyIXW<>{>B1irDs?!ylKUxd#vd7iSLo zggMt)rnj^(4x33|U(4kmpujcTsxFQBAmldMV2I|)Gc0*3CYGG^v+J5SagLZPljX{N zV6TBIQ*mc9*EOungnZH9dZ@i9UqY#b$E)B~+4>gXUYw!jV8uj1-akBSkAq)w|vE>y-a@_oGU~nxBBL!3SqPyLCSwO^7Sx~%VkhA(0Dm)`4wQaBnf*A6JZA$0G# zo051p%vL||8vdmg=7G=8k78d5exrt`29}xR?l&{X^sd^T+-j#9m=irg`|qv?5(KTg z?GLB(?*z|&61V|?HcpmiX3l>)C@)$HLSv8=L68%}{x8}eEX+R~mAVdKkqN^$xBk)a zwxdP3MK!oQUhePjFL^u1-{!K!K2xQo zJi`vt_eY&TPk85zw_zZt@d7aeSP$WSN8^i3anEm7eZx zr;IBb*(f^ngpnDUsRY}%(W+_$X=&@%EMFysna)^yfrs?EBTWk^naHxc%H;Ra+lx2} z$>_+Z6$Esuc1JWmNgb+*XuzrsZg|H%bzHCYnimFxXf(+SmM>A$k3|kFxGzhp`09!* zcI9eWNQtfops^W=TVMy%eH-`Y!IgaKRZ@yIutsIiXd{y#LO>=N(6>rph@f;As6+Bj z{qCK@Qd*EtDBT)yD(`-2Zo;9E1`laR!Y6vGJ)K&yRfSQ0PVSId-*N+(RvDH&ydB-ebwC3q9SE@+yVQ zUg%+qKAi6d<&LP+S;VlZO`vXEbLcmeEsN1cUSvnfl^ZlNxYu=AzQwSkM(l%1IxrF> z$mnvUT<)Q$X5+S~(JW`CJJTKn68b|n#ua+dnCcQ)Jk7QHOd9)B{k-tp8O9fpcX`TT zE$T$>eLdu9`V#iuYCf7}eY$1OLyzGI_sxUCZvEQhXkF55jsx6G=`y-|>009Gp8h6r ztnbYF4CNkC&pe)|V^9Rvfrqm_pa@s+l*oq(abor3TdmK*WpFU>;`!sjOXL*_d8Wa# zK7u`@tA;NLklK=A`H0O*(BFp(<2inmL|O`0BXayi>@C$<26nO5MaN`M3wELaEfJ%q zQ@A8x{8@tjE?gR~))@{vljqe^c$)y}p<*o$%Sa_6|yYPc)0K?x8MQ4t7dq1-T>b$OS8&V(``kSQW>Fq+LVt|` zB8HJX$ilzl!9*9XjKT=_2I4l#AG(uXGa44$7U;fd3DcxT242T|05tO<>HWmfW#Op- z4R}jmH2z>7>`;gs(2|`|^5NV6Si?{I3(z8WwluRd`(uA0N02hP1Pum8jPujY{!R9S z_{YjU(vk<@aiVm**!9yq>_{*@AY&ngZ4~%I>53*M6Nz7@XPt`b)dHX#3+xxO)eg51&yyfhN&Fd2t7 zvKp$^E59RDfW%5<+rM#>42omC&xxl-Mm3}PdgDa@t)h*{*8%!y7@KMV@?P~YFP2C) z{YaIT)Zd7lu@KkSKJGCkfI(X8u<6>&?rY1^sG($hP^~P5{ zZnPn|aMVP!CGG@x3N{EN+Xb>MZqb%QVK^eLEWOW*kCn*D`F3yA9(FWbt5qizjKsEJ zbo%V`9ueAliL>nYxINeysj5EWU2f~|89ew>;kA`ZDc`U@)}9Y~2jvcIpG{Tn@1GgMi*n z$gAaFFZUThRLm56&d7RpY)x%Zb1Cugg|G_lZAwJ-jR-rk7^)yOpvqQP z8+^OD8(=-Yj4COK%gGhE+qyX2n2{9R>7?(2OqO`FUUA%YbHDVeqbtYfdVfU2zMbi` z>+#|fpv!j;rdL3l#q;)ZMleUv=XPSp@B2wV`09qGZLcJGPj6eqt9D-cfQG&9h4It0 zuJi$FiT-y&%r~yhU&j_8R4zX>mbk`o zgINpB7z;nKOp*9Hay#C?EWEqG?)IWs-TJ-`sXnP$*VV*!s$E`*Rw-D8p8qmhp4qkX zs{o0*Zk$wix*_`BGS!Lz>HGNT;RV{F;wf-x;+IY@gryoVGYh>|1Pb4(U7ys(1S1EO z27QB5p^N@FDOb?H1q?>BhWUmvvLFuR+6jm_K!i_^Wirzj>=$XawRhC4PPo7$Ci+@W zH05DT);*yZ4ziZstPV0EP^PM^n0UPh47u=&ai?qzvbqd%_;| z)(=>LZFyOg#byDeRIFczx7f!k?+D)SK^axC5Fe)KQ<3wlw4dXwjJXc2ZuGZ!JX7!Y zME@#CJ;l!^3ou#0JYd-G5b4seeDtfH%)B~V`nXk$5hPR5XXyK7e&^V91&TySHwmxPk{af&`J9Z(G!ZEu6cZAqF51RfU z)Mwnwnwh5F87*Lc@3(oaiZVQNMnZ^@E%yec*uEIy?Ok&lk0jb-7nb^)y;+Jp7~CqG+bVXAmD3@Q2G&XM4(}3D8e@DZ z;F;U?uxz(_yEK-peW6Mnsfo>|o%$FvAnt_hTCpgkqC#W2pY8l@+Z9rVQb=nkxBG}d zfD&gmR*_0ajLiKOQz{2=6GbgM7;;F0x&!{PAgMxFgnXNjLV8GJ?B$G*N)<_ljvXS@ zdN##J7%h*F!@|=?ONqaY<-!*SVCD=aI_igvBRXco3%8% zL>jw%Rkv9$Lgy1%)=bY@2#?@Gl{y2GJl2j3fz?A_$Sooyo*>xiZcp21oY$l;;E)#| zV1M6ogU`Gp$^dP-*+JX6rxPgXTi|4A1T-^MaRyr1TR1&!<9re)p3a+3M;??bdlmk0 zDKw?2pm110LgIRxw#ntHPchW_C}KUNzKUfcYDKU|;uTU;L8u0U@{1}4J@O{}l~t)B z*|!gmyu7S~xj3NN2JI3X5R-+_4@F?iE2mI{ znBPh>7*P&8>1WNmq>PB#)?qM+kg{gZ_Hd+XP?gjbKpxSLi4Hpo=Sr1Ox!96H)FZjT zbM^##&r?{Ag`9VRj~NcLG;euSB!9ug>rf8Ft5kzo%bo9}-lb7Nytg|=5U=5?eH>kea)pONc zW<9Mr52Wi%9``LcxuS7j+bgOoeONLC1}LAVM_XYtXYNhh0|f7}D(ac?mL3jCf!GEG zeWBAsDRzS|Tv3QV-w9yeU!>T%v)eGL^E4OR{GuiMJqIo`P)o{MWYJ7yz*O zadK4=2Fx&FbdsGC(XZ-IS=DiR^z_IT^z%LTdAZSNmlD_{CN-LlIPn%7+=wKOTBq$~fMpkn zBp#s-t|SZfMJE{=q35gIXhcTF6$fANdaxu$x#UqvxDh52oD{tr^f#}J8CfcwY3HT3 z1J|S|gG=efQ;hKQkW^r7LJ}+sdp6QSO-K9WCDO{pp`>f4x5k3E(L6WO-k7SnK{@2) zev$jwAx|Pzt6CRMi`G(8qM{!U+Z`%o3-l9J@!7vXLL@!z9AV8*dbfm05#rrFhJleR z-p*B$Afs!EoYk39UTYEj7QJ}L`Vz*qEbRMaDNKGtIh>~2*4O*3<&Uof{dAA*?Y|1> zAKP~w+IKy)-c|Bb54l##vbtoGl?(PenyP2SD2C6R$oaFViVd;Rmzc&^@g$T-8j_VF zuHIiwa(c$j?ZKK)?q$YKojwHl4&}h`dl@RknY*6kGn?zs4#s^Rv7~O^tlbag6K!CQ zNWP8T6(WIw zy)gs>L;h>UJA2rg{aEVDdOmS@k~oh<5yv$^um(5_gNJt?+YsG4b;^{3NzqWy#HSYZ zSF1F=COorO8p@LsLxq&$75#`ppDrog8m}{w ztdBi&u2=ca1@yX{C$Ed}6qtYyJrd#_rK;o>lc#N84FQE`K{o0QS_u;Q{JV=ef({(C zVedIz%C1IZiICjw0}yND_FjADHmpj+~X&9bX(xiTmv$t)=D=T^ykh2Jw5a zA>p_2iir8Vn#U++ytZ>wB*CA*%O;h(b3K_ktyw(17v*(cdb<-EmZl-T?>4W$igy#R zd?hp&)E&*;_Yr5_rwRRrtm}n7l={2XgBAh{3DE{d*!RdUJccT5-##DosUV6iiWMXb z#=|7@phz&#KMs73cNr;GX@9FXxp7Cgn!@P4(+j;-P@wB~$$oJ&NOu~=IUEIeussZ` zRvQ^AmTg{etM1ZHFiFqym(T{wzlXV%l9qi)LCd~-&4)DySwq5>+@x&!1s{^cfo~%@ zW5Fv;azAXCho0pU-2#`+Ahw{jW#t@}tyvqnAZo?-ZQM$t{Q_aIdJlN*{WFgh1l=Xk zYMy&nroaq;*>w??eDX&-(S+cz_vdN&4|kuEyjbN_mxYcI>a_A`>#oQfOWPU5Ut`yO z@8;v_+<{fD`M6T5nF8@hA7zn!Oqu0WkawXs4941-N%jt&G_!0sN1gnb!Z@FImk0Kl zX~g!Y45~y-kEEM(+ku8pV8$xp9wbG+!|pGkPH$_Sw5;kJtTcB40Wk9@$KtrdE$F-e zt7&XEMoM??;aP^|z(KSuUMb~$D>{N5nZf2#7UG0E(pna}=|$^9YKtuGZB46nKZ}Ef zEH1{GUbv!#35&1{{EfV%@*I~NV1GJ;WyJeth0Vx{k10la7>%rNVMzPl;}SQ+as=k& z9e79&8GKY)MFAnyM@;S<7y$iT`Ql-59R;@sJUUeoGg-QYyZ9R7UNMBx!YDA|xJ9pM zKtJk;H$P+5nuOzMH>_w(iHcuFV!dG0Y}C3z&Pnk$e4?Z9Mwh>)`Z+u0*Ddk^F&==4lc7R%w3|>MOqhn}=4%{Y#vnS=wnS zMrebb;BN1JE=HAQzBuEVwbQbbCrLCJq#J<9C-gf?4DAC`@YcJD%eOYiw~$Lg)j&B^ zKT;V@_F(AV7@6khh~B3cR&0n(Rr!LmV8f-B2fWk*3~fjtTIR1!f(IWc8FWX#Q|ic&coJ%C zkJ#$amGIiWV`B}X^Szggh^owzWwqJSMGGi)vWi^fDUpfX z(!{EKT&FRUGJC1=GK4*(;`rQdJ94KCwOB`Lg<$CXZ2{YTp?+cya&qr;&v9Syh3SBl z{HYbF3rtGd!fJB8B-j%?NsJ@oG)c-&efe&d;9lcdd;w5*^)YzZckNp(UO}CGXu9y< zCijBfVG(jj6s!W9ctP8ecDj5h&r;;}(*lf)=a?i+>W!py8OF+)%M%j@wZ#h}#Hu!}$G0L{ z($`c4#ML&vZ!y%0&x@H4!+6|iAw@#;VX!ymE2LaC>49pB#fP-}IJCX@@7$SX#k*~d zFb$f|f#u22h|progh!SKoS%HMAH11tI#c5bcD%Qn0#<=1R&n_{j+jw4? zH3a3^#wug6iPZz@4TH_tz4JV!RcF&epA|@pX6DK@8aN+1m0#pD%j1bP70Ix~;PpRU z>|p)tieyCIMzz70lDR&shw@KRm8D!Oy?1$5zsUnvEF((K*fQ8suMMnnDXTYi)LID* zn|s&J@H}eljDx>2Wy{VhExfQ(>GqsrAOAIo^^3O05~2FoStCC^qwq$eL>7qJ?m6Bm z=*AD~h49p-Bcld-z*3)>?n_W_t+mn^7SY^!)+lNIv2J9aqpl_<*2D?159R*$U~yXb zReSe@BI`M&72m+DDQRS-lc*yf!@ziCSzcy#<>S$Pz&#ed3p1Vn%TCJS@uXx)8W_ec zrN}a?F%1M0EX7gV!L=r1^0bsm$AOs9&C`vIu;ZQq3{=MAtA$HL6_dbeoWQoG9R3#G z+pU)h0ylbcp=bycWK^LW9mEM)8;^Whe7>BNLN1wTYmjtS+#en_izn*yTeh$B&gO>TAcng{90_Xce3zCy}=SX3%u zr9d&hqX9S!2?kOKHgPdN300#7Qc`MM)YB8i5XY$SJWH|uji4;<49ABVucRe-9#uj@ zCWR1r1=2*`Tq!ddCMQ%z4BrcJk+BUC zbeKpX-%zaQZC34UM%g*v58hvFKN>r_BaO$yF|&J|kkfpyK$eG#7_pF)_8>&gKtZPB z^!t8zS{NKW6iC_MyBYN%?$HC0Nx;MTadYE&oZlr)2m^k)Sw#wEWq-z*d>QT5yY*U# z2N_|Em|I)-Z@VJjW_Rk=nW6bv12`a@5{B5897c%FZ`2SwIT-}(1irZ+9C4k zWSBj87}Jnqq>?92OZ2aY=Ex-Ge?OeKkm-f_+#R}zVlEk=K}=cH0nX`w6r473fne5W zGLK@+@k6q%X!z$R$!kP!U~oh>5!xY8qS_%UHDN@?&7Vfbtb_g+<>w5PmUR7mTldX* zCg7eXM(fowWNR1h9oNmk=X;{*R7&#uwxl{7?vi0d%oK4A?%}+{79|vz5eGJ^R$b&K zr5nbmVNd)IyD)6?mEtN1S|(q^mG(aip%GfDo3%Fxazm==Dn;Mj1ad9_lAKbdC{u)d zy_*wftO!|Z5P7xU#}dSQ#6~5tAH%)SaKCDXb88K!qvK*t(Bl$d7N#;ddu~SFpy&{< z@J23e#?IE%fJHZAGDP)(%eEi0gh}wqj9D=kt=9l2*HoPfi)(;(C6P>1_gtifXOLue z)frAp6HTRg%5;Y>kXSjF$fR76b@^o`VV*Nu-P4f!n&$=kzRez znJYj~6l83qt<+7~;&S>t!`;8=O;7E4V8?aK&)Ar}h}G`YV|8mhkgrm!!g-^_?&{`R zq2p4lrIwu1qEm-4HhbY_NG}}o`Eu=QR+u{E=6f^ewam1p-34UOpD ztA4CBu|t(w1ENIpB@5H;;aO^}J1~VcTV5LipXUjzg2J}7pmO+C2;(Z=jhcYxy`N)p z2vZ=1d=3t@3Rk9KnQnOFT$x(fY#t=k>ujq#Wy*(KsSZfHG_TOA(wSG7v)sGfSFb2m zpIBy?TU5%N(6*s_SyLAfm~L&kT-wTqVoqAT4Z|^(B)*XeEY83)r$5|p=C3fZua{I9 zB61FKJ|~M+WU(iE*-LSWLBqAJB|r`_knD~|7Uf`h%$;)JzC7l-bSe?S#plA3x*OKW z8H#wmfg4m}XDSl{6DN%&DjRMo1e5z0N;2hz zzOuq^jt_Ee!rOlTwpcB5)V0ofSE0&!zR``CqM^~vm0o2kd7jtb&N<+5F~GY0GsSHr zP39)4pJu*BOhW>!#vZKFO6=LO+MRu2FQ2YNQ&fC_Kq+$QfW&a&rXN$yt>6Ej*Mb3^ zW(q7*0u7{3Yz4uI3%Y_rMnB1(@`e8iHvEOI zymcQFO2~D{itu=6N_L8}%%-gj$K}Klag(!=~54myEB)b~rTG_4^^a^ikvZ`BdQ|D< zXmd0!yf(-Kh1L1&u^KBgB2c1JNE1{#o%j8f?cp)iw%HeU{W(p8Kp$t%bev z!p1w}FAzyhsjSa&Q?$0_Zblt!#h@;8NVBC5<%Up%scpd$+2?YuzLw%gbzCOQ!+c*B z!r~WJa4z%T0dzTmX>CKDj1AqET%W(M>8q!?97Q;KzqZ35vglL3w{qE;(AY)ae%^C# z!to78Q5GBm<40)oXK~G+mH`=IY)bEr0tDSxd`1eHllgIhlp6YZ|h<^E>-<5j( z;enobekyhQo9D-h`CrPOmgLX&r0l8vqkiajeScOEJrV!ZNA*v(zv_wpgy_#2m|xt5 zpjY~DrT?jj`IG(6Dvn?5#Gt_BFZRD{I{w$BK^%Xzr%8K3{Z+Z}G~ItU$?quT692`1;{8>Mq9_afW3i%ws=8o7bb(|~eE$apkC}P^ literal 0 HcmV?d00001 diff --git a/WGShare.Domain/Constant/RedisKeyConstant.cs b/WGShare.Domain/Constant/RedisKeyConstant.cs index 7b33549..04c0f77 100644 --- a/WGShare.Domain/Constant/RedisKeyConstant.cs +++ b/WGShare.Domain/Constant/RedisKeyConstant.cs @@ -79,5 +79,12 @@ namespace WGShare.Domain.Constant } + /// + /// 发布订阅 + /// + public class PubSub + { + public static string MeetingRecord => "meeting_record"; + } } } diff --git a/WGShare.Domain/DTOs/AgoraCallback/EventBody.cs b/WGShare.Domain/DTOs/AgoraCallback/EventBody.cs index d844ae5..224ad09 100644 --- a/WGShare.Domain/DTOs/AgoraCallback/EventBody.cs +++ b/WGShare.Domain/DTOs/AgoraCallback/EventBody.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -27,7 +28,7 @@ namespace WGShare.Domain.DTOs.AgoraCallback /// /// 声网消息服务器向你的服务器发送事件通知的 Unix 时间戳 (ms)。通知重试时该值会更新。 /// - public int notifyMs { get; set; } + public long notifyMs { get; set; } /// /// 会话 ID。 @@ -37,7 +38,7 @@ namespace WGShare.Domain.DTOs.AgoraCallback /// /// 通知事件的具体内容。payload 因 eventType 而异,详见频道事件类型。 /// - public string payload { get; set; } + public AgoraCallbackPayload payload { get; set; } } public class AgoraCallbackPayload @@ -46,5 +47,54 @@ namespace WGShare.Domain.DTOs.AgoraCallback /// 频道名称 /// public string channelName { get; set; } + + /// + /// 该事件在声网业务服务器上发生的 Unix 时间戳 (s)。 + /// + public long ts { get; set; } + + /// + /// 最后一个离开频道的用户 ID。(则声网消息通知可能返回不同的 lastUid,此时任选其一即可。) + /// + public long lastUid { get; set; } + + /// + /// 主播设备所属平台 + /// + public PlatformType platform { get; set; } + + /// + /// 序列号,标识该事件在 App 客户端上发生的顺序,可用于对同一用户的事件进行排序。详见维护用户在线状态。 + /// + public long clientSeq { get; set; } + + /// + /// String 类型的用户 ID。 + /// + public string account { get; set; } + + /// + /// 主播在频道内的时长 (s)。 + /// + public int duration { get; set; } + + /// + /// 主播离开频道的原因: + /// 1:主播正常离开频道。 + /// 2:客户端与声网业务服务器连接超时。判断标准为声网 SD-RTN 超过 10 秒未收到该主播的任何数据包,或连接单台服务器 4 秒超时并在 1 秒内没有完成重连。 + /// 3:权限问题。如被运营人员通过踢人 RESTful API 踢出频道。 + /// 4:声网业务服务器内部原因。如声网业务服务器在调整负载,和客户端短暂断开连接,之后会重新连接。 + /// 5:主播切换新设备,迫使旧设备下线。 + /// 9:由于客户端有多个 IP 地址,SDK 主动与声网业务服务器断开连接并重连。此过程用户无感知。请检查用户是否存在多个公网 IP 或使用了 VPN。 + /// 10:由于网络连接问题,例如 SDK 超过 4 秒未收到来自声网业务服务器的任何数据包或 socket 连接错误,SDK 主动与声网业务服务器断开连接并重连。此过程用户无感知。请检查网络连接状态。 + /// 999:异常用户。例如,用户短时间内频繁登录登出频道会被判定为异常用户。信息:你的 App 服务端需要在收到 reason 为 999 的 104 事件 60 秒后调用踢人 API 将该用户踢出频道。否则,该用户再次加入频道后,可能无法收到相关事件通知。 + /// 0:其他原因。 + /// + public LeaveReasonEnum reason { get; set; } + + /// + /// 主播在频道内的用户 ID。 + /// + public long uid { get; set; } } } diff --git a/WGShare.Domain/DTOs/Room/RoomMettingRecordExportDTO.cs b/WGShare.Domain/DTOs/Room/RoomMettingRecordExportDTO.cs new file mode 100644 index 0000000..e655c4e --- /dev/null +++ b/WGShare.Domain/DTOs/Room/RoomMettingRecordExportDTO.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WGShare.Domain.DTOs.Room +{ + public class RoomMettingRecordExportDTO + { + public string RoomName { get; set; } + public string RoomNum { get; set; } + + public string BeginTime { get; set; } + public string EndTime { get; set; } + + public List Users { get; set; } + } + + public class UserBehavior + { + public string UserName { get; set; } + public string Account { get; set; } + public string Role { get; set; } + public string FirstJoinTime { get; set; } + public string LastExitTime { get; set; } + public int JoinCount { get; set; } + public int SumTime { get; set; } + } +} diff --git a/WGShare.Domain/Entities/MeetingRecord.cs b/WGShare.Domain/Entities/MeetingRecord.cs new file mode 100644 index 0000000..2d86ae1 --- /dev/null +++ b/WGShare.Domain/Entities/MeetingRecord.cs @@ -0,0 +1,101 @@ +using SqlSugar; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using WGShare.Domain.Enums; +using Yitter.IdGenerator; + +namespace WGShare.Domain.Entities +{ + /// + /// 会议室在会记录 + /// + [SugarTable("meeting_record")] + public class MeetingRecord + { + /// + /// + /// + [SugarColumn(ColumnName = "id", IsPrimaryKey = true, IsIdentity = true)] + public long Id { get; set; } + + /// + /// 事件类型 + /// + [SugarColumn(ColumnName = "event_type")] + public EventType EventType { get; set; } + + /// + /// 客户端序列号 + /// + [SugarColumn(ColumnName = "client_seq")] + public long clientSeq { get; set; } + + + /// + /// 发生Unix时间戳 + /// + [SugarColumn(ColumnName = "ts")] + public long ts { get; set; } + /// + /// 用户ID + /// + [SugarColumn(ColumnName = "uid")] + public string uid { get; set; } + /// + /// 创建时间 + /// + [SugarColumn(ColumnName = "create_time", IsOnlyIgnoreInsert = true, IsOnlyIgnoreUpdate = true)] + public DateTime CreateTime { get; set; } + + /// + /// 频道号 + /// + [SugarColumn(ColumnName = "channel_name")] + public string channelName { get; set; } + + /// + /// 客户端类型 + /// + [SugarColumn(ColumnName = "platform")] + public PlatformType platform { get; set; } + + /// + /// 离开原因 + /// + [SugarColumn(ColumnName = "leave_reason")] + public LeaveReasonEnum reason { get; set; } + + /// + /// 在会时长 + /// + [SugarColumn(ColumnName = "duration")] + public int duration { get; set; } + + /// + /// 字符串类型 用户id(声网返回) + /// + [SugarColumn(ColumnName = "account")] + public string account { get; set; } + + + /// + /// 用户名称 + /// + [SugarColumn(IsIgnore = true)] + public string userName { get; set; } + /// + /// 用户账号 + /// + [SugarColumn(IsIgnore = true)] + public string UserAccount { get; set; } + + /// + /// 用户角色名称 + /// + [SugarColumn(IsIgnore = true)] + public string RoleName { get; set; } + } +} diff --git a/WGShare.Domain/Enums/LeaveReasonEnum.cs b/WGShare.Domain/Enums/LeaveReasonEnum.cs new file mode 100644 index 0000000..de29ce1 --- /dev/null +++ b/WGShare.Domain/Enums/LeaveReasonEnum.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WGShare.Domain.Enums +{ + /// + /// 声网回调接口:主播离开频道的原因 + /// + public enum LeaveReasonEnum + { + /// + /// 主播正常离开频道。 + /// + normal = 1, + + /// + /// 客户端与声网业务服务器连接超时。判断标准为声网 SD-RTN 超过 10 秒未收到该主播的任何数据包,或连接单台服务器 4 秒超时并在 1 秒内没有完成重连。 + /// + timeout = 2, + + /// + /// 权限问题。如被运营人员通过踢人 RESTful API 踢出频道。 + /// + permission = 3, + + /// + /// 声网业务服务器内部原因。如声网业务服务器在调整负载,和客户端短暂断开连接,之后会重新连接。 + /// + agora_internal_error = 4, + + /// + /// 主播切换新设备,迫使旧设备下线。。 + /// + force_logout = 5, + + /// + /// 由于客户端有多个 IP 地址,SDK 主动与声网业务服务器断开连接并重连。此过程用户无感知。请检查用户是否存在多个公网 IP 或使用了 VPN。 + /// + multiple_ip = 9, + + /// + /// 由于网络连接问题,例如 SDK 超过 4 秒未收到来自声网业务服务器的任何数据包或 socket 连接错误,SDK 主动与声网业务服务器断开连接并重连。此过程用户无感知。请检查网络连接状态。 + /// + network_error = 10, + + /// + /// 异常用户。例如,用户短时间内频繁登录登出频道会被判定为异常用户。 + /// + abnormal_user = 999, + + /// + /// 其他原因 + /// + other = 0 + } +} diff --git a/WGShare.Domain/Enums/PlatformType.cs b/WGShare.Domain/Enums/PlatformType.cs new file mode 100644 index 0000000..f848de4 --- /dev/null +++ b/WGShare.Domain/Enums/PlatformType.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WGShare.Domain.Enums +{ + /// + /// 声网平台类型 + /// + public enum PlatformType + { + Android = 1, + iOS = 2, + Windows = 5, + Linux = 6, + Web = 7, + MacOS = 8, + Other = 0 + } +} diff --git a/WGShare.Domain/WGShare.Domain.csproj b/WGShare.Domain/WGShare.Domain.csproj index 2d6f2bc..bae8b90 100644 --- a/WGShare.Domain/WGShare.Domain.csproj +++ b/WGShare.Domain/WGShare.Domain.csproj @@ -13,7 +13,7 @@ - +