完成考勤表Excel导出

This commit is contained in:
youngq 2024-09-20 15:37:12 +08:00
parent aad7f3df40
commit 7e5d20a64a
20 changed files with 748 additions and 142 deletions

View File

@ -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<AgoraCallbackComsuerService> _logger;
private readonly ISqlSugarClient _sugarClient;
private readonly SemaphoreSlim _semaphore;
public AgoraCallbackComsuerService(ILogger<AgoraCallbackComsuerService> 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<EventBody>();
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<MeetingRecord>();
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();
}
}
}
}

View File

@ -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<OssCleanWorker> _logger;
private readonly ISqlSugarClient _sugarClient;
private readonly OssHelper _ossHelper;
public OssCleanWorker(ILogger<OssCleanWorker> 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<ShareFile>()
.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<ShareFile>()
.SetColumns(x => x.IsFileClean == true)
.Where(x => deleteFiles.Select(a => a.Id).Contains(x.Id))
.ExecuteCommandAsync();
}
await Console.Out.WriteLineAsync($@"本次清除操作结束,当前时间:{DateTime.Now}");
}
}
}

View File

@ -2,13 +2,18 @@
using AgoraIO.Rtm; using AgoraIO.Rtm;
using Mapster; using Mapster;
using Masuit.Tools; using Masuit.Tools;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using SharpCompress;
using SqlSugar; using SqlSugar;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http; using System.Net.Http;
using WGShare.API.Controllers.Basic; using WGShare.API.Controllers.Basic;
using WGShare.API.Helpers; using WGShare.API.Helpers;
using WGShare.Domain.AgoraApiResult; using WGShare.Domain.AgoraApiResult;
using WGShare.Domain.Constant; using WGShare.Domain.Constant;
using WGShare.Domain.DTOs.AgoraCallback;
using WGShare.Domain.DTOs.Room; using WGShare.Domain.DTOs.Room;
using WGShare.Domain.Entities; using WGShare.Domain.Entities;
using WGShare.Domain.FriendlyException; using WGShare.Domain.FriendlyException;
@ -21,21 +26,112 @@ namespace WGShare.API.Controllers.Frontend
/// Agora接口 /// Agora接口
/// </summary> /// </summary>
[ApiExplorerSettings(GroupName = "frontend")] [ApiExplorerSettings(GroupName = "frontend")]
[Route("agora-cb")] [Route("agora-cb"), AllowAnonymous]
public class AgoraCallbackController : BasicController public class AgoraCallbackController : BasicController
{ {
private readonly ILogger<AgoraCallbackController> _logger; private readonly ILogger<AgoraCallbackController> _logger;
private readonly AgoraHelper _agoraHelper;
private readonly IConfiguration _configuration;
public AgoraCallbackController( public AgoraCallbackController(
ILogger<AgoraCallbackController> logger) ILogger<AgoraCallbackController> logger,
AgoraHelper agoraHelper,
IConfiguration configuration)
{ {
this._logger = logger; this._logger = logger;
this._agoraHelper = agoraHelper;
this._configuration = configuration;
} }
[HttpPost("event")] [HttpPost("event")]
public async Task<string> 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<EventBody>();
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;
}
} }
/// <summary>
/// 加入频道
/// </summary>
[NonAction]
private void JoinChannelEvent(string bodyString)
{
_logger.LogDebug($"Agora回调内容 加入频道:{bodyString}");
RedisHelper.Instance.LPush(RedisKeyConstant.PubSub.MeetingRecord, bodyString);
}
/// <summary>
/// 离开频道
/// </summary>
[NonAction]
private void LeaveChannelEvent(string bodyString)
{
_logger.LogDebug($"Agora回调内容 离开频道:{bodyString}");
// 离会记录
RedisHelper.Instance.LPush(RedisKeyConstant.PubSub.MeetingRecord, bodyString);
}
} }
} }

View File

@ -3,17 +3,20 @@ using AgoraIO.Rtm;
using Mapster; using Mapster;
using Masuit.Tools; using Masuit.Tools;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using MiniExcelLibs;
using SqlSugar; using SqlSugar;
using System.Net.Http; using System.IO;
using System.Text.RegularExpressions;
using WGShare.API.Controllers.Basic; using WGShare.API.Controllers.Basic;
using WGShare.API.Helpers; using WGShare.API.Helpers;
using WGShare.Domain.AgoraApiResult;
using WGShare.Domain.Constant; using WGShare.Domain.Constant;
using WGShare.Domain.DTOs.Room; using WGShare.Domain.DTOs.Room;
using WGShare.Domain.Entities; using WGShare.Domain.Entities;
using WGShare.Domain.Enums;
using WGShare.Domain.FriendlyException; using WGShare.Domain.FriendlyException;
using WGShare.Domain.GeneralModel; using WGShare.Domain.GeneralModel;
using Yitter.IdGenerator; using Yitter.IdGenerator;
using IConfiguration = Microsoft.Extensions.Configuration.IConfiguration;
namespace WGShare.API.Controllers.Frontend namespace WGShare.API.Controllers.Frontend
{ {
@ -29,19 +32,22 @@ namespace WGShare.API.Controllers.Frontend
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
private readonly AgoraHelper _agoraHelper; private readonly AgoraHelper _agoraHelper;
private readonly ILogger<HomeController> _logger; private readonly ILogger<HomeController> _logger;
private readonly OssHelper _ossHelper;
public HomeController( public HomeController(
ISqlSugarClient sqlSugar, ISqlSugarClient sqlSugar,
IConfiguration configuration, IConfiguration configuration,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
AgoraHelper agoraHelper, AgoraHelper agoraHelper,
ILogger<HomeController> logger) ILogger<HomeController> logger,
OssHelper ossHelper)
{ {
_sqlSugar = sqlSugar; _sqlSugar = sqlSugar;
this._configuration = configuration; this._configuration = configuration;
this._httpClientFactory = httpClientFactory; this._httpClientFactory = httpClientFactory;
this._agoraHelper = agoraHelper; this._agoraHelper = agoraHelper;
this._logger = logger; this._logger = logger;
this._ossHelper = ossHelper;
} }
@ -101,6 +107,20 @@ namespace WGShare.API.Controllers.Frontend
return await _sqlSugar.Insertable(entity).ExecuteCommandAsync() > 0; return await _sqlSugar.Insertable(entity).ExecuteCommandAsync() > 0;
} }
/// <summary>
/// 删除会议室
/// </summary>
/// <param name="inputDTO"></param>
/// <returns></returns>
[HttpDelete("room")]
public async Task<bool> DeleteRoom([FromQuery] string roomId)
{
return await _sqlSugar.Updateable<Room>()
.SetColumns(x => x.IsDelete == true)
.Where(x => x.Id == roomId).ExecuteCommandAsync() > 0;
}
/// <summary> /// <summary>
/// 获取 rtm token /// 获取 rtm token
/// </summary> /// </summary>
@ -124,5 +144,104 @@ namespace WGShare.API.Controllers.Frontend
{ {
return _configuration["Agora:appId"].ToString(); return _configuration["Agora:appId"].ToString();
} }
/// <summary>
/// 获取会议记录
/// </summary>
/// <returns></returns>
[HttpGet("record")]
public async Task<string> GetMeetingRecord([FromQuery] long beginTimestamp, [FromQuery] long endTimestamp, [FromQuery] string roomNum)
{
var room = await _sqlSugar.Queryable<Room>().Where(x => x.RoomNum == roomNum && x.IsDelete == false).FirstAsync();
if (room == null)
{
throw Oops.Oh("该会议室不存在!");
}
var recordList = await _sqlSugar.Queryable<MeetingRecord>()
.InnerJoin<User>((mr, u) => mr.uid == u.Id)
.InnerJoin<Role>((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<UserBehavior>()
};
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);
}
} }
} }

View File

@ -1,6 +1,7 @@
using Masuit.Tools; using Masuit.Tools;
using System.Drawing.Printing; using System.Drawing.Printing;
using System.Net.Http; using System.Net.Http;
using System.Security.Cryptography;
using System.Text; using System.Text;
using WGShare.Domain.AgoraApiResult; using WGShare.Domain.AgoraApiResult;
using WGShare.Domain.FriendlyException; 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);
}
}
/// <summary>
/// 验证声网回调签名
/// </summary>
/// <param name="agoraSig"></param>
/// <param name="bodyString"></param>
/// <returns></returns>
public async Task<bool> 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;
}
} }
} }

View File

@ -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);
/// <summary>
/// 将 JavaScript 时间戳(以秒为单位)转换为 DateTime 对象UTC 时间)。
/// </summary>
/// <param name="jsTimestamp">JavaScript 时间戳(以秒为单位)。</param>
/// <returns>对应的 DateTime 对象UTC 时间)。</returns>
public static DateTime FromJavaScriptTimestamp(long jsTimestamp)
{
return Epoch.AddSeconds(jsTimestamp);
}
/// <summary>
/// 将 DateTime 对象UTC 时间)转换为 JavaScript 时间戳(以秒为单位)。
/// </summary>
/// <param name="dateTime">DateTime 对象UTC 时间)。</param>
/// <returns>对应的 JavaScript 时间戳(以秒为单位)。</returns>
public static long ToJavaScriptTimestamp(DateTime dateTime)
{
return (long)(dateTime.ToUniversalTime() - Epoch).Seconds;
}
/// <summary>
/// 将 JavaScript 时间戳(以秒为单位)转换为本地时间的 DateTime 对象。
/// </summary>
/// <param name="jsTimestamp">JavaScript 时间戳(以秒为单位)。</param>
/// <returns>对应的本地时间的 DateTime 对象。</returns>
public static DateTime FromJavaScriptTimestampToLocal(long jsTimestamp)
{
DateTime utcDateTime = FromJavaScriptTimestamp(jsTimestamp);
return utcDateTime.ToLocalTime();
}
}
}

View File

@ -46,73 +46,5 @@ namespace WGShare.API.Helpers
/// <returns></returns> /// <returns></returns>
public static int RandomExpired(int minTimeoutSeconds, int maxTimeoutSeconds) => rnd.Value.Next(minTimeoutSeconds, maxTimeoutSeconds); public static int RandomExpired(int minTimeoutSeconds, int maxTimeoutSeconds) => rnd.Value.Next(minTimeoutSeconds, maxTimeoutSeconds);
//public static List<T> HVals<T>(string key) where T : class
//{
// var valueStrings = Instance.HVals(key);
// if (valueStrings.IsNullOrEmpty())
// {
// return null;
// }
// return valueStrings.ToList().ConvertAll(x => JsonConvert.DeserializeObject<T>(x));
//}
//public static T HGet<T>(string key, string field) where T : class
//{
// var valueString = Instance.HGet(key, field);
// if (valueString.IsNullOrEmpty())
// {
// return null;
// }
// return JsonConvert.DeserializeObject<T>(valueString);
//}
//public static List<T> HMGet<T>(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<T>(x));
//}
//public static Dictionary<string, T> HGetAll<T>(string key) where T : class
//{
// var dic = Instance.HGetAll(key);
// if (dic == null || dic.Count == 0)
// {
// return null;
// }
// Dictionary<string, T> result = new Dictionary<string, T>();
// foreach (var kv in dic)
// {
// if (string.IsNullOrWhiteSpace(kv.Key) || string.IsNullOrWhiteSpace(kv.Value))
// {
// continue;
// }
// result.Add(kv.Key, JsonConvert.DeserializeObject<T>(kv.Value));
// }
// return result;
//}
//public static long HSet<T>(string key, string fields, T value) where T : class
//{
// return Instance.HSet(key, fields, value.ToJsonString());
//}
//public static void HMSet<T>(string key, Dictionary<string, T> keyValues) where T : class
//{
// var dic = new Dictionary<string, string>();
// foreach (var kv in keyValues)
// {
// dic.Add(kv.Key,kv.Value.ToJsonString());
// }
// Instance.HMSet(key, dic);
//}
} }
} }

View File

@ -66,7 +66,7 @@ namespace WGShare.API
builder.Services.AddSingleton(new JwtHelper(configuration)); builder.Services.AddSingleton(new JwtHelper(configuration));
builder.Services.AddSingleton(new AgoraHelper(configuration)); builder.Services.AddSingleton(new AgoraHelper(configuration));
builder.Services.AddSingleton(new OssHelper(configuration)); builder.Services.AddSingleton(new OssHelper(configuration));
builder.Services.AddHostedService<OssCleanWorker>(); builder.Services.AddHostedService<AgoraCallbackComsuerService>();
builder.Services.AddAuth(configuration["Jwt:Issuer"], builder.Services.AddAuth(configuration["Jwt:Issuer"],
configuration["Jwt:Audience"], configuration["Jwt:Audience"],
configuration["Jwt:SecretKey"]); configuration["Jwt:SecretKey"]);

View File

@ -29,6 +29,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Update="meetingRecordTemplate.xlsx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Reference\AgoraIO.dll"> <None Update="Reference\AgoraIO.dll">
<CopyToOutputDirectory>Never</CopyToOutputDirectory> <CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None> </None>

View File

@ -66,6 +66,16 @@
Agora接口 Agora接口
</summary> </summary>
</member> </member>
<member name="M:WGShare.API.Controllers.Frontend.AgoraCallbackController.JoinChannelEvent(System.String)">
<summary>
加入频道
</summary>
</member>
<member name="M:WGShare.API.Controllers.Frontend.AgoraCallbackController.LeaveChannelEvent(System.String)">
<summary>
离开频道
</summary>
</member>
<member name="T:WGShare.API.Controllers.Frontend.HomeController"> <member name="T:WGShare.API.Controllers.Frontend.HomeController">
<summary> <summary>
首页接口 首页接口
@ -85,6 +95,13 @@
<param name="inputDTO"></param> <param name="inputDTO"></param>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:WGShare.API.Controllers.Frontend.HomeController.DeleteRoom(System.String)">
<summary>
删除会议室
</summary>
<param name="inputDTO"></param>
<returns></returns>
</member>
<member name="M:WGShare.API.Controllers.Frontend.HomeController.GetRTMToken"> <member name="M:WGShare.API.Controllers.Frontend.HomeController.GetRTMToken">
<summary> <summary>
获取 rtm token 获取 rtm token
@ -97,6 +114,12 @@
</summary> </summary>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:WGShare.API.Controllers.Frontend.HomeController.GetMeetingRecord(System.Int64,System.Int64,System.String)">
<summary>
获取会议记录
</summary>
<returns></returns>
</member>
<member name="T:WGShare.API.Controllers.Frontend.RoomController"> <member name="T:WGShare.API.Controllers.Frontend.RoomController">
<summary> <summary>
会议室接口 会议室接口
@ -335,6 +358,35 @@
</summary> </summary>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:WGShare.API.Helpers.AgoraHelper.CheckSignatureAsync(System.String,System.String)">
<summary>
验证声网回调签名
</summary>
<param name="agoraSig"></param>
<param name="bodyString"></param>
<returns></returns>
</member>
<member name="M:WGShare.API.Helpers.DateTimeUtils.FromJavaScriptTimestamp(System.Int64)">
<summary>
将 JavaScript 时间戳(以秒为单位)转换为 DateTime 对象UTC 时间)。
</summary>
<param name="jsTimestamp">JavaScript 时间戳(以秒为单位)。</param>
<returns>对应的 DateTime 对象UTC 时间)。</returns>
</member>
<member name="M:WGShare.API.Helpers.DateTimeUtils.ToJavaScriptTimestamp(System.DateTime)">
<summary>
将 DateTime 对象UTC 时间)转换为 JavaScript 时间戳(以秒为单位)。
</summary>
<param name="dateTime">DateTime 对象UTC 时间)。</param>
<returns>对应的 JavaScript 时间戳(以秒为单位)。</returns>
</member>
<member name="M:WGShare.API.Helpers.DateTimeUtils.FromJavaScriptTimestampToLocal(System.Int64)">
<summary>
将 JavaScript 时间戳(以秒为单位)转换为本地时间的 DateTime 对象。
</summary>
<param name="jsTimestamp">JavaScript 时间戳(以秒为单位)。</param>
<returns>对应的本地时间的 DateTime 对象。</returns>
</member>
<member name="M:WGShare.API.Helpers.OssHelper.GetUploadUrl(System.String,System.String,System.UInt32)"> <member name="M:WGShare.API.Helpers.OssHelper.GetUploadUrl(System.String,System.String,System.UInt32)">
<summary> <summary>
获取上传url 获取上传url

View File

@ -1,13 +1,14 @@
{ {
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Debug",
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Information"
}, },
"Console": { "Console": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Debug",
"Microsoft": "Warning" "Microsoft": "Warning",
"Hangfire": "Information"
} }
//"FormatterName": "CustomTimePrefixingFormatter", //"FormatterName": "CustomTimePrefixingFormatter",
//"FormatterOptions": { //"FormatterOptions": {
@ -43,6 +44,7 @@
"tokenExpireTimeInSecond": 7200, "tokenExpireTimeInSecond": 7200,
"apiPrefix": "https://api.sd-rtn.com/", "apiPrefix": "https://api.sd-rtn.com/",
"clientId": "80cdc24f7dfa4497a37d98da95a3c4a4", "clientId": "80cdc24f7dfa4497a37d98da95a3c4a4",
"clientSecret": "8323581d4d464114b1f324b26cc62e09" "clientSecret": "8323581d4d464114b1f324b26cc62e09",
"eventSecret": "gPPaFQS1U"
} }
} }

View File

@ -3,6 +3,9 @@
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
},
"Console": {
"TimestampFormat": "[yyyy-MM-dd HH:mm:ss]"
} }
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
@ -33,6 +36,7 @@
"AccessKeySecret": "FKFNYRdS53FwA5ME2wM1585qX5eVEd", "AccessKeySecret": "FKFNYRdS53FwA5ME2wM1585qX5eVEd",
"Endpoint": "oss-cn-chengdu.aliyuncs.com", "Endpoint": "oss-cn-chengdu.aliyuncs.com",
"Host": "https://wgshare.oss-cn-chengdu.aliyuncs.com/", "Host": "https://wgshare.oss-cn-chengdu.aliyuncs.com/",
"BucketName": "wgshare" "BucketName": "wgshare",
"eventSecret": ""
} }
} }

Binary file not shown.

View File

@ -79,5 +79,12 @@ namespace WGShare.Domain.Constant
} }
/// <summary>
/// 发布订阅
/// </summary>
public class PubSub
{
public static string MeetingRecord => "meeting_record";
}
} }
} }

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -27,7 +28,7 @@ namespace WGShare.Domain.DTOs.AgoraCallback
/// <summary> /// <summary>
/// 声网消息服务器向你的服务器发送事件通知的 Unix 时间戳 (ms)。通知重试时该值会更新。 /// 声网消息服务器向你的服务器发送事件通知的 Unix 时间戳 (ms)。通知重试时该值会更新。
/// </summary> /// </summary>
public int notifyMs { get; set; } public long notifyMs { get; set; }
/// <summary> /// <summary>
/// 会话 ID。 /// 会话 ID。
@ -37,7 +38,7 @@ namespace WGShare.Domain.DTOs.AgoraCallback
/// <summary> /// <summary>
/// 通知事件的具体内容。payload 因 eventType 而异,详见频道事件类型。 /// 通知事件的具体内容。payload 因 eventType 而异,详见频道事件类型。
/// </summary> /// </summary>
public string payload { get; set; } public AgoraCallbackPayload payload { get; set; }
} }
public class AgoraCallbackPayload public class AgoraCallbackPayload
@ -46,5 +47,54 @@ namespace WGShare.Domain.DTOs.AgoraCallback
/// 频道名称 /// 频道名称
/// </summary> /// </summary>
public string channelName { get; set; } public string channelName { get; set; }
/// <summary>
/// 该事件在声网业务服务器上发生的 Unix 时间戳 (s)。
/// </summary>
public long ts { get; set; }
/// <summary>
/// 最后一个离开频道的用户 ID。(则声网消息通知可能返回不同的 lastUid此时任选其一即可。)
/// </summary>
public long lastUid { get; set; }
/// <summary>
/// 主播设备所属平台
/// </summary>
public PlatformType platform { get; set; }
/// <summary>
/// 序列号,标识该事件在 App 客户端上发生的顺序,可用于对同一用户的事件进行排序。详见维护用户在线状态。
/// </summary>
public long clientSeq { get; set; }
/// <summary>
/// String 类型的用户 ID。
/// </summary>
public string account { get; set; }
/// <summary>
/// 主播在频道内的时长 (s)。
/// </summary>
public int duration { get; set; }
/// <summary>
/// <para>主播离开频道的原因: </para>
/// <para>1主播正常离开频道。</para>
/// <para>2客户端与声网业务服务器连接超时。判断标准为声网 SD-RTN 超过 10 秒未收到该主播的任何数据包,或连接单台服务器 4 秒超时并在 1 秒内没有完成重连。</para>
/// <para>3权限问题。如被运营人员通过踢人 RESTful API 踢出频道。</para>
/// <para>4声网业务服务器内部原因。如声网业务服务器在调整负载和客户端短暂断开连接之后会重新连接。</para>
/// <para>5主播切换新设备迫使旧设备下线。</para>
/// <para>9由于客户端有多个 IP 地址SDK 主动与声网业务服务器断开连接并重连。此过程用户无感知。请检查用户是否存在多个公网 IP 或使用了 VPN。</para>
/// <para>10由于网络连接问题例如 SDK 超过 4 秒未收到来自声网业务服务器的任何数据包或 socket 连接错误SDK 主动与声网业务服务器断开连接并重连。此过程用户无感知。请检查网络连接状态。</para>
/// <para>999异常用户。例如用户短时间内频繁登录登出频道会被判定为异常用户。信息你的 App 服务端需要在收到 reason 为 999 的 104 事件 60 秒后调用踢人 API 将该用户踢出频道。否则,该用户再次加入频道后,可能无法收到相关事件通知。</para>
/// <para>0其他原因。</para>
/// </summary>
public LeaveReasonEnum reason { get; set; }
/// <summary>
/// 主播在频道内的用户 ID。
/// </summary>
public long uid { get; set; }
} }
} }

View File

@ -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<UserBehavior> 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; }
}
}

View File

@ -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
{
/// <summary>
/// 会议室在会记录
///</summary>
[SugarTable("meeting_record")]
public class MeetingRecord
{
/// <summary>
///
///</summary>
[SugarColumn(ColumnName = "id", IsPrimaryKey = true, IsIdentity = true)]
public long Id { get; set; }
/// <summary>
/// 事件类型
/// </summary>
[SugarColumn(ColumnName = "event_type")]
public EventType EventType { get; set; }
/// <summary>
/// 客户端序列号
/// </summary>
[SugarColumn(ColumnName = "client_seq")]
public long clientSeq { get; set; }
/// <summary>
/// 发生Unix时间戳
/// </summary>
[SugarColumn(ColumnName = "ts")]
public long ts { get; set; }
/// <summary>
/// 用户ID
/// </summary>
[SugarColumn(ColumnName = "uid")]
public string uid { get; set; }
/// <summary>
/// 创建时间
/// </summary>
[SugarColumn(ColumnName = "create_time", IsOnlyIgnoreInsert = true, IsOnlyIgnoreUpdate = true)]
public DateTime CreateTime { get; set; }
/// <summary>
/// 频道号
/// </summary>
[SugarColumn(ColumnName = "channel_name")]
public string channelName { get; set; }
/// <summary>
/// 客户端类型
/// </summary>
[SugarColumn(ColumnName = "platform")]
public PlatformType platform { get; set; }
/// <summary>
/// 离开原因
/// </summary>
[SugarColumn(ColumnName = "leave_reason")]
public LeaveReasonEnum reason { get; set; }
/// <summary>
/// 在会时长
/// </summary>
[SugarColumn(ColumnName = "duration")]
public int duration { get; set; }
/// <summary>
/// 字符串类型 用户id(声网返回)
/// </summary>
[SugarColumn(ColumnName = "account")]
public string account { get; set; }
/// <summary>
/// 用户名称
/// </summary>
[SugarColumn(IsIgnore = true)]
public string userName { get; set; }
/// <summary>
/// 用户账号
/// </summary>
[SugarColumn(IsIgnore = true)]
public string UserAccount { get; set; }
/// <summary>
/// 用户角色名称
/// </summary>
[SugarColumn(IsIgnore = true)]
public string RoleName { get; set; }
}
}

View File

@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WGShare.Domain.Enums
{
/// <summary>
/// 声网回调接口:主播离开频道的原因
/// </summary>
public enum LeaveReasonEnum
{
/// <summary>
/// 主播正常离开频道。
/// </summary>
normal = 1,
/// <summary>
/// 客户端与声网业务服务器连接超时。判断标准为声网 SD-RTN 超过 10 秒未收到该主播的任何数据包,或连接单台服务器 4 秒超时并在 1 秒内没有完成重连。
/// </summary>
timeout = 2,
/// <summary>
/// 权限问题。如被运营人员通过踢人 RESTful API 踢出频道。
/// </summary>
permission = 3,
/// <summary>
/// 声网业务服务器内部原因。如声网业务服务器在调整负载,和客户端短暂断开连接,之后会重新连接。
/// </summary>
agora_internal_error = 4,
/// <summary>
/// 主播切换新设备,迫使旧设备下线。。
/// </summary>
force_logout = 5,
/// <summary>
/// 由于客户端有多个 IP 地址SDK 主动与声网业务服务器断开连接并重连。此过程用户无感知。请检查用户是否存在多个公网 IP 或使用了 VPN。
/// </summary>
multiple_ip = 9,
/// <summary>
/// 由于网络连接问题,例如 SDK 超过 4 秒未收到来自声网业务服务器的任何数据包或 socket 连接错误SDK 主动与声网业务服务器断开连接并重连。此过程用户无感知。请检查网络连接状态。
/// </summary>
network_error = 10,
/// <summary>
/// 异常用户。例如,用户短时间内频繁登录登出频道会被判定为异常用户。
/// </summary>
abnormal_user = 999,
/// <summary>
/// 其他原因
/// </summary>
other = 0
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WGShare.Domain.Enums
{
/// <summary>
/// 声网平台类型
/// </summary>
public enum PlatformType
{
Android = 1,
iOS = 2,
Windows = 5,
Linux = 6,
Web = 7,
MacOS = 8,
Other = 0
}
}

View File

@ -13,7 +13,7 @@
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.6" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.6" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.6" />
<PackageReference Include="MiniExcel" Version="1.34.0" /> <PackageReference Include="MiniExcel" Version="1.34.0" />
<PackageReference Include="SqlSugarCore" Version="5.1.4.158" /> <PackageReference Include="SqlSugarCore" Version="5.1.4.169" />
<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" /> <PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
</ItemGroup> </ItemGroup>