From 3f05dbefc0938abba7b23cd8f32d2a96daf771c4 Mon Sep 17 00:00:00 2001 From: youngq Date: Wed, 10 Jul 2024 16:33:59 +0800 Subject: [PATCH] 1 --- .../BackgroudServices/AgoraDataService.cs | 56 +++ WGShare.API/Controllers/AuthController.cs | 10 +- .../Controllers/Basic/BasicController.cs | 27 ++ .../Controllers/Frontend/HomeController.cs | 62 ++- .../Controllers/Frontend/RoomController.cs | 170 ++++++-- .../Controllers/Frontend/UserController.cs | 23 +- WGShare.API/Helpers/AgoraHelper.cs | 132 ++++++ WGShare.API/Helpers/OssHelper.cs | 99 +++++ WGShare.API/Hubs/IMessageClient.cs | 33 ++ WGShare.API/Hubs/SessionManageHub.cs | 141 +++++++ WGShare.API/Program.cs | 19 +- WGShare.API/Properties/launchSettings.json | 2 +- WGShare.API/Reference/AgoraIO.dll | Bin 0 -> 21504 bytes .../AuthenticationServiceExtensions.cs | 15 + .../CorsAppBuilderExtensions.cs | 2 +- .../HangfireServiceExtensions.cs | 41 ++ .../SwaggerServiceExtensions.cs | 4 +- WGShare.API/WGShare.API.csproj | 21 + WGShare.API/WGShare.API.xml | 381 ++++++++++++++++++ WGShare.API/appsettings.Development.json | 2 +- WGShare.API/appsettings.json | 19 +- .../AgoraApiResult/AgoraResponse.cs | 15 + WGShare.Domain/AgoraApiResult/Channel.cs | 21 + WGShare.Domain/AgoraApiResult/ChannelUser.cs | 28 ++ WGShare.Domain/Constant/RedisKeyConstant.cs | 47 +++ .../DTOs/File/ShareFileOutputDTO.cs | 2 +- WGShare.Domain/DTOs/Room/RoomOutputDTO.cs | 4 + WGShare.Domain/DTOs/User/UserInputDTO.cs | 4 +- WGShare.Domain/DTOs/User/UserOutputDTO.cs | 5 + WGShare.Domain/Entities/ShareFile.cs | 7 +- agora_key_and_secret.txt | 3 + 31 files changed, 1344 insertions(+), 51 deletions(-) create mode 100644 WGShare.API/BackgroudServices/AgoraDataService.cs create mode 100644 WGShare.API/Helpers/AgoraHelper.cs create mode 100644 WGShare.API/Helpers/OssHelper.cs create mode 100644 WGShare.API/Hubs/IMessageClient.cs create mode 100644 WGShare.API/Hubs/SessionManageHub.cs create mode 100644 WGShare.API/Reference/AgoraIO.dll create mode 100644 WGShare.API/ServiceConfigs/HangfireServiceExtensions.cs create mode 100644 WGShare.API/WGShare.API.xml create mode 100644 WGShare.Domain/AgoraApiResult/AgoraResponse.cs create mode 100644 WGShare.Domain/AgoraApiResult/Channel.cs create mode 100644 WGShare.Domain/AgoraApiResult/ChannelUser.cs create mode 100644 WGShare.Domain/Constant/RedisKeyConstant.cs create mode 100644 agora_key_and_secret.txt diff --git a/WGShare.API/BackgroudServices/AgoraDataService.cs b/WGShare.API/BackgroudServices/AgoraDataService.cs new file mode 100644 index 0000000..8af0e34 --- /dev/null +++ b/WGShare.API/BackgroudServices/AgoraDataService.cs @@ -0,0 +1,56 @@ +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/AuthController.cs b/WGShare.API/Controllers/AuthController.cs index 147ff70..b5e4851 100644 --- a/WGShare.API/Controllers/AuthController.cs +++ b/WGShare.API/Controllers/AuthController.cs @@ -18,11 +18,14 @@ namespace WGShare.API.Controllers { private readonly ISqlSugarClient _sqlSugar; private readonly JwtHelper _jwtHelper; + private readonly IConfiguration _configuration; - public AuthController(ISqlSugarClient sqlSugar, JwtHelper jwtHelper) + public AuthController(ISqlSugarClient sqlSugar, JwtHelper jwtHelper, + IConfiguration configuration) { _sqlSugar = sqlSugar; _jwtHelper = jwtHelper; + this._configuration = configuration; } /// @@ -71,6 +74,8 @@ namespace WGShare.API.Controllers btnAutn.Add(new Claim("perm", perms.Sum(x => x.PermValue).ToString())); btnAutn.Add(new Claim("role", user.RoleId)); btnAutn.Add(new Claim("tenant", user.TenantId)); + btnAutn.Add(new Claim("account", user.Account)); + btnAutn.Add(new Claim("uname", user.UserName)); return Ok(new { @@ -78,7 +83,8 @@ namespace WGShare.API.Controllers token = _jwtHelper.CreateToken(user.Id, btnAutn), roleId = user.RoleId, userName = user.UserName, - tenantName = tenant.TenantName + tenantName = tenant.TenantName, + expire= _configuration["Jwt:Expires"].ToInt32() }); } diff --git a/WGShare.API/Controllers/Basic/BasicController.cs b/WGShare.API/Controllers/Basic/BasicController.cs index 776fa73..8746ab7 100644 --- a/WGShare.API/Controllers/Basic/BasicController.cs +++ b/WGShare.API/Controllers/Basic/BasicController.cs @@ -22,6 +22,33 @@ namespace WGShare.API.Controllers.Basic } } + public string Account + { + get + { + var account = HttpContext.User.Claims.FirstOrDefault(x => x.Type == "account").Value; + if (string.IsNullOrWhiteSpace(account)) + { + throw Oops.Oh("用户信息有误,请重新登录"); + } + return account; + } + } + + public string UserName + { + get + { + var account = HttpContext.User.Claims.FirstOrDefault(x => x.Type == "uname").Value; + if (string.IsNullOrWhiteSpace(account)) + { + throw Oops.Oh("用户信息有误,请重新登录"); + } + return account; + } + } + + public string RoleId { get diff --git a/WGShare.API/Controllers/Frontend/HomeController.cs b/WGShare.API/Controllers/Frontend/HomeController.cs index 0dac080..aa16b8b 100644 --- a/WGShare.API/Controllers/Frontend/HomeController.cs +++ b/WGShare.API/Controllers/Frontend/HomeController.cs @@ -1,7 +1,14 @@ -using Mapster; +using AgoraIO.Media; +using AgoraIO.Rtm; +using Mapster; +using Masuit.Tools; using Microsoft.AspNetCore.Mvc; using SqlSugar; +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.Room; using WGShare.Domain.Entities; using WGShare.Domain.FriendlyException; @@ -18,21 +25,33 @@ namespace WGShare.API.Controllers.Frontend public class HomeController : BasicController { private readonly ISqlSugarClient _sqlSugar; + private readonly IConfiguration _configuration; + private readonly IHttpClientFactory _httpClientFactory; + private readonly AgoraHelper _agoraHelper; + private readonly ILogger _logger; - public HomeController(ISqlSugarClient sqlSugar) + public HomeController( + ISqlSugarClient sqlSugar, + IConfiguration configuration, + IHttpClientFactory httpClientFactory, + AgoraHelper agoraHelper, + ILogger logger) { _sqlSugar = sqlSugar; + this._configuration = configuration; + this._httpClientFactory = httpClientFactory; + this._agoraHelper = agoraHelper; + this._logger = logger; } - /// /// 获取会议室列表 /// /// /// [HttpGet("room")] - public async Task> GetRooms(PagedBaseDto dto) + public async Task> GetRooms([FromQuery] PagedBaseDto dto) { RefAsync total = 0; @@ -40,7 +59,19 @@ namespace WGShare.API.Controllers.Frontend .Where(x => x.TenantId == TenantId && x.IsDelete == false) .ToPageListAsync(dto.PageIndex, dto.PageSize, total); - return PagedResult.Create(list.Adapt>(), total.Value); + var result = list.Adapt>(); + if (!result.IsNullOrEmpty()) + { + // 从Redis获取缓存在线人数 拼装数据 + var userCounts = RedisHelper.Instance.HMGet(RedisKeyConstant.SessionManage.GetChannelUserCountKey(TenantId), + result.Select(x => x.RoomNum).ToArray()); + for (int i = 0; i < userCounts.Length; i++) + { + result[i].OnlineUserCount = userCounts[i]; + } + } + + return PagedResult.Create(result, total.Value); } /// @@ -49,8 +80,13 @@ namespace WGShare.API.Controllers.Frontend /// /// [HttpPost("room")] - public async Task CreateRoom([FromBody] RoomInputDTO inputDTO) + public async Task CreateRoom([FromBody] RoomInputDTO inputDTO) { + if (inputDTO.RoomNum.Length != 8 || !int.TryParse(inputDTO.RoomNum, out _)) + { + throw Oops.Oh("会议号必须为8位数字"); + } + var entity = inputDTO.Adapt(); entity.Id = YitIdHelper.NextId().ToString(); entity.TenantId = TenantId; @@ -63,5 +99,19 @@ namespace WGShare.API.Controllers.Frontend return await _sqlSugar.Insertable(entity).ExecuteCommandAsync() > 0; } + /// + /// 获取 rtm token + /// + /// + [HttpGet("tk/rtm"),Obsolete] + public async Task GetRTMToken() + { + uint privilegeExpiredTs = (uint)_configuration["tokenExpireTimeInSecond"].ToInt32() + (uint)Utils.getTimestamp(); + return RtmTokenBuilder.buildToken(_configuration["Agora:appId"], + _configuration["Agora:appSecret"], + UId, + privilegeExpiredTs); + } + } } diff --git a/WGShare.API/Controllers/Frontend/RoomController.cs b/WGShare.API/Controllers/Frontend/RoomController.cs index 0767730..0a1db67 100644 --- a/WGShare.API/Controllers/Frontend/RoomController.cs +++ b/WGShare.API/Controllers/Frontend/RoomController.cs @@ -1,10 +1,15 @@ -using Mapster; +using AgoraIO.Media; +using Mapster; using Masuit.Tools; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; +using Microsoft.IdentityModel.Tokens; using SqlSugar; -using System.CodeDom; using WGShare.API.Controllers.Basic; +using WGShare.API.Helpers; +using WGShare.API.Hubs; +using WGShare.Domain.Constant; using WGShare.Domain.DTOs.File; using WGShare.Domain.DTOs.Room; using WGShare.Domain.DTOs.User; @@ -12,7 +17,6 @@ using WGShare.Domain.Entities; using WGShare.Domain.FriendlyException; using WGShare.Domain.GeneralModel; using Yitter.IdGenerator; -using ZstdSharp.Unsafe; namespace WGShare.API.Controllers.Frontend { @@ -24,24 +28,36 @@ namespace WGShare.API.Controllers.Frontend public class RoomController : BasicController { private readonly ISqlSugarClient _sqlSugar; + private readonly IConfiguration _configuration; + private readonly OssHelper _ossHelper; + private readonly AgoraHelper _agoraHelper; + private readonly IHubContext _hubContext; - public RoomController(ISqlSugarClient sqlSugar) + public RoomController(ISqlSugarClient sqlSugar, + IConfiguration configuration, + OssHelper ossHelper, + AgoraHelper agoraHelper, + IHubContext hubContext) { this._sqlSugar = sqlSugar; + this._configuration = configuration; + this._ossHelper = ossHelper; + this._agoraHelper = agoraHelper; + this._hubContext = hubContext; } - /// - /// 获取会议室管理员 - /// - /// - [HttpGet("manager")] - public async Task> GetRoomManager([FromQuery] string roomId) - { - return await _sqlSugar.Queryable() - .Where(x => x.RoomId == roomId) - .Select(x => x.UserId) - .ToListAsync(); - } + ///// + ///// 获取会议室管理员 + ///// + ///// + //[HttpGet("manager")] + //public async Task> GetRoomManager([FromQuery] string roomId) + //{ + // return await _sqlSugar.Queryable() + // .Where(x => x.RoomId == roomId) + // .Select(x => x.UserId) + // .ToListAsync(); + //} /// /// 设置房间管理员 @@ -59,24 +75,51 @@ namespace WGShare.API.Controllers.Frontend return await _sqlSugar.Insertable(entities).ExecuteCommandAsync() > 0; } + /// + /// 取消房间管理员 + /// + /// + [HttpDelete("manager")] + public async Task DelRoomManager([FromQuery] string roomId, [FromBody] List userIds) + { + return await _sqlSugar.Deleteable() + .Where(x => x.RoomId == roomId && userIds.Contains(x.UserId)) + .ExecuteCommandAsync() > 0; + } + /// /// 查询用户信息 /// /// [HttpGet("user")] - public async Task> GetUsers([FromQuery] string uidStr) + public async Task> GetUsers([FromQuery] string roomNum) { - var uid = uidStr.Split(',', StringSplitOptions.RemoveEmptyEntries); - if (uid.IsNullOrEmpty()) + var data = await _agoraHelper.GetChannelUserList(roomNum); + if (data == null) { - throw Oops.Oh("请输入需要查询的用户id,多id请用英文逗号,分割"); + throw Oops.Oh("请求失败"); + } + if (!data.channel_exist) + { + throw Oops.Oh("频道不存在"); } var users = await _sqlSugar.Queryable() - .Where(x => uid.Contains(x.Id)) + .Where(x => data.users.Contains(x.Account)) .ToListAsync(); - return users.Adapt>(); + var managerIds = await _sqlSugar.Queryable() + .InnerJoin((rm, r) => r.Id == rm.RoomId) + .Where((rm, r) => r.RoomNum == roomNum) + .Select((rm, r) => rm.UserId) + .ToListAsync(); + + + var result = users.Adapt>(); + + result.ForEach(x => x.IsManager = managerIds.Contains(x.Id)); + + return result; } /// @@ -104,6 +147,53 @@ namespace WGShare.API.Controllers.Frontend return room.Adapt(); } + /// + /// 获取房间rtc token + /// + /// + [HttpGet("tk/rtc")] + public async Task GetRTCToken([FromQuery] string roomNum) + { + //var privilegeExpiredTs = _configuration["Agora:tokenExpireTimeInSecond"].ToInt32() + Utils.getTimestamp(); + + return new RtcTokenBuilder2().buildTokenWithUserAccount( + _configuration["Agora:appId"], + _configuration["Agora:appSecret"], + roomNum, + Account, + RtcTokenBuilder2.Role.ROLE_PUBLISHER, + _configuration["Agora:tokenExpireTimeInSecond"].ToInt32(), + _configuration["Agora:tokenExpireTimeInSecond"].ToInt32()); + } + + /// + /// 邀请用户 + /// + /// + [HttpPost("invite")] + public async Task InviteUser([FromQuery] string roomId, [FromBody] string[] inviteeUids) + { + var room = await _sqlSugar.Queryable().FirstAsync(x => x.Id == roomId); + + var connectIds = RedisHelper.Instance.HMGet(RedisKeyConstant.SessionManage.GetOnlineUserKey(TenantId), inviteeUids); + connectIds.RemoveWhere(x => string.IsNullOrWhiteSpace(x)); + + await _hubContext.Clients.Clients(connectIds).Invitation(room.RoomNum, room.RoomName, UserName); + } + + /// + /// 踢出房间 + /// + /// + [HttpGet("kickout")] + public async Task KickOut([FromQuery] string roomNum, [FromQuery] string kickUid) + { + var connectId = RedisHelper.Instance.HGet(RedisKeyConstant.SessionManage.GetOnlineUserKey(TenantId), kickUid); + + await _hubContext.Clients.Client(connectId).ForceExitRoom(roomNum); + } + + #region 文件分享 /// /// 分享上传文件 /// @@ -135,14 +225,17 @@ namespace WGShare.API.Controllers.Frontend /// /// 获取分享文件列表 /// + /// 房间Id + /// /// [HttpGet("file")] - public async Task> GetFilesList([FromQuery] string roomId, [FromBody] PagedBaseDto pagedBaseDto) + public async Task> GetFilesList([FromQuery] string roomId, [FromQuery] string? keyword, [FromQuery] PagedBaseDto pagedBaseDto) { RefAsync total = 0; var list = await _sqlSugar.Queryable() .LeftJoin((sf, u) => sf.UserId == u.Id) - .Where(x => x.IsDelete == false && x.RoomId == roomId) + .Where((sf, u) => sf.IsDelete == false && sf.RoomId == roomId) + .WhereIF(!string.IsNullOrWhiteSpace(keyword), (sf, u) => sf.FileName.Contains(keyword)) .Select((sf, u) => new ShareFileOutputDTO { Id = sf.Id, @@ -150,7 +243,7 @@ namespace WGShare.API.Controllers.Frontend UserName = u.UserName, FileName = sf.FileName, FileUrl = sf.FileUrl, - RoomId = u.Id, + RoomId = sf.RoomId, Size = sf.Size, ModifyTime = sf.ModifyTime, DownloadCount = sf.DownloadCount, @@ -160,5 +253,32 @@ namespace WGShare.API.Controllers.Frontend } + + /// + /// 获取文件上传url + /// + /// + [HttpGet("up-fileurl")] + public async Task GetUploadUrl([FromQuery] string roomNum, [FromQuery] string fileSuffix) + { + return Ok(_ossHelper.GetUploadUrl($@"share_file/{TenantId}/{roomNum}", Guid.NewGuid().ToString("N") + "." + fileSuffix)); + } + + /// + /// 获取文件下载地址 + /// + /// + /// 文件Id + /// + [HttpGet("file-dw-url")] + public async Task GetDownloadUrl([FromQuery] string fileUrl, [FromQuery] string fileId) + { + await _sqlSugar.Updateable() + .SetColumns(x => x.DownloadCount == x.DownloadCount + 1) + .Where(x => x.Id == fileId).ExecuteCommandAsync(); + + return _ossHelper.GetAccessFileUrl(fileUrl); + } + #endregion } } diff --git a/WGShare.API/Controllers/Frontend/UserController.cs b/WGShare.API/Controllers/Frontend/UserController.cs index 95c2ad0..db2329f 100644 --- a/WGShare.API/Controllers/Frontend/UserController.cs +++ b/WGShare.API/Controllers/Frontend/UserController.cs @@ -22,17 +22,32 @@ namespace WGShare.API.Controllers.Frontend } + /// + /// 获取用户列表 + /// + /// 用户名 或 账号 + /// 翻页信息 + /// [HttpGet("list")] - public async Task> GetUserList([FromQuery] string? searchKeywod, [FromQuery] PagedBaseDto pagedBaseDto) + public async Task> GetUserList([FromQuery] string? searchKeywod, [FromQuery] PagedBaseDto pagedBaseDto) { RefAsync total = 0; var list = await _sqlSugar.Queryable() .InnerJoin((u, r) => u.RoleId == r.Id) - .WhereIF(!string.IsNullOrWhiteSpace(searchKeywod), u => u.UserName.Contains(searchKeywod) || u.Account.Contains(searchKeywod)) + .Where((u, r) => u.IsDelete == false) + .WhereIF(!string.IsNullOrWhiteSpace(searchKeywod), (u, r) => u.UserName.Contains(searchKeywod) || u.Account.Contains(searchKeywod)) + .Select((u, r) => new UserOutputDTO + { + Id = u.Id, + UserName = u.UserName, + Account = u.Account, + RoleId = r.Id, + RoleName = r.RoleName + }) .ToPageListAsync(pagedBaseDto.PageIndex, pagedBaseDto.PageSize, total); - return list.Adapt>(); + return PagedResult.Create(list, total.Value); } @@ -42,7 +57,7 @@ namespace WGShare.API.Controllers.Frontend /// /// [HttpPost] - public async Task AddUser([FromQuery] UserInputDTO inputDTO) + public async Task AddUser([FromBody] UserInputDTO inputDTO) { var user = inputDTO.Adapt(); user.Id = YitIdHelper.NextId().ToString(); diff --git a/WGShare.API/Helpers/AgoraHelper.cs b/WGShare.API/Helpers/AgoraHelper.cs new file mode 100644 index 0000000..778acc1 --- /dev/null +++ b/WGShare.API/Helpers/AgoraHelper.cs @@ -0,0 +1,132 @@ +using Masuit.Tools; +using System.Drawing.Printing; +using System.Net.Http; +using System.Text; +using WGShare.Domain.AgoraApiResult; +using WGShare.Domain.FriendlyException; + +namespace WGShare.API.Helpers +{ + public class AgoraHelper + { + public class Constant + { + /// + /// Redis 键,hash ,每个频道用户数量 + /// + public const string REDIS_CHANNEL_USERCOUNT = "channelUserCount"; + } + + private readonly IConfiguration _configuration; + + public AgoraHelper(IConfiguration configuration) + { + this._configuration = configuration; + } + + /// + /// 获取完整的声网API请求路径 + /// + /// api .com 后的路径后缀 + /// + public string GetFullApiUrl(string apiSuffixPath) + { + var apiPrefix = _configuration["agora:apiPrefix"].Trim('/'); + + return apiPrefix + (apiSuffixPath.StartsWith('/') ? apiSuffixPath : ('/' + apiSuffixPath)); + } + + + public string GetBasicCredential() + { + // APP ID + string customerKey = _configuration["Agora:clientId"]; + // 客户密钥 + string customerSecret = _configuration["Agora:clientSecret"]; + // 拼接 APP ID 和APP密钥 + string plainCredential = customerKey + ":" + customerSecret; + + // 使用 base64 进行编码 + var plainTextBytes = Encoding.UTF8.GetBytes(plainCredential); + string encodedCredential = Convert.ToBase64String(plainTextBytes); + // 创建 authorization header + return "Basic " + encodedCredential; + } + + /// + /// 获取频道用户 + /// + /// + /// + public async Task GetChannelUserList(string roomNum) + { + using var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Add("Authorization", GetBasicCredential()); + var apiUrl = GetFullApiUrl($"dev/v1/channel/user/{_configuration["Agora:appId"]}/{roomNum}"); + var responseMessage = await httpClient.GetAsync(apiUrl); + if (!responseMessage.IsSuccessStatusCode) + { + await Console.Out.WriteLineAsync($"声网接口请求失败!请求地址:{apiUrl} 请求结果:{await responseMessage.Content.ReadAsStringAsync()}"); + return null; + } + + var responseData = await responseMessage.Content.ReadFromJsonAsync>(); + if (responseData == null) + { + await Console.Out.WriteLineAsync($"声网接口解析失败!请求地址:{apiUrl} 请求结果:{await responseMessage.Content.ReadAsStringAsync()}"); + return null; + } + + return responseData.Data; + } + + /// + /// 更新每个频道用户在线数量 + /// + /// + public async Task RefreshChannelUserCount() + { + var pageSize = 500D; + var pageIndex = 1; + var totalCount = 0D; + + do + { + using var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Add("Authorization", GetBasicCredential()); + var apiUrl = GetFullApiUrl($"dev/v1/channel/{_configuration["Agora:appId"]}?page_no={pageIndex}&page_size={pageSize}"); + + var responseMessage = await httpClient.GetAsync(apiUrl); + if (!responseMessage.IsSuccessStatusCode) + { + await Console.Out.WriteLineAsync($"声网接口请求失败!请求地址:{apiUrl} 请求结果:{await responseMessage.Content.ReadAsStringAsync()}"); + return; + } + + var responseData = await responseMessage.Content.ReadFromJsonAsync>(); + if (responseData == null) + { + await Console.Out.WriteLineAsync($"声网接口解析失败!请求地址:{apiUrl} 请求结果:{await responseMessage.Content.ReadAsStringAsync()}"); + return; + } + + totalCount = responseData.Data.total_size.ToDouble(); + + var ketValue = responseData.Data.channels.ToDictionary(x => x.channel_name, x => x.user_count); + + if (!ketValue.IsNullOrEmpty()) + { + RedisHelper.Instance.HSet(Constant.REDIS_CHANNEL_USERCOUNT, responseData.Data.channels.ToDictionary(x => x.channel_name, x => x.user_count)); + } + + // 总页数 = 总数/每页数 + //var pageCount = (int)Math.Ceiling(Math.Round(totalCount / pageSize, 3)); + + } while (totalCount > 0 && (int)Math.Ceiling(Math.Round(totalCount / pageSize, 3)) > pageIndex); + + + } + + + } +} diff --git a/WGShare.API/Helpers/OssHelper.cs b/WGShare.API/Helpers/OssHelper.cs new file mode 100644 index 0000000..b7f0f5e --- /dev/null +++ b/WGShare.API/Helpers/OssHelper.cs @@ -0,0 +1,99 @@ +using Aliyun.OSS; +using Masuit.Tools; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.Extensions.Hosting; +using System.Security.Cryptography; +using System.Text; +using WGShare.Domain.FriendlyException; + +namespace WGShare.API.Helpers +{ + public class OssHelper + { + private readonly IConfiguration _configuration; + + private readonly string _accessKeyId; + private readonly string _accessKeySecret; + private readonly string _bucketName; + private readonly string _endpoint; + private readonly OssClient _ossClient; + + public OssHelper(IConfiguration configuration) + { + _configuration = configuration; + _accessKeyId = configuration["OSS:AccessKeyID"]; + _accessKeySecret = configuration["OSS:AccessKeySecret"]; + _bucketName = configuration["OSS:BucketName"]; + _endpoint = configuration["OSS:Endpoint"]; + this._ossClient = new OssClient("https://" + _endpoint, _accessKeyId, _accessKeySecret); + } + + /// + /// 获取上传url + /// + /// bucket 路径 + /// 文件名 + /// 过期时间 秒 + /// + public Dictionary GetUploadUrl(string path, string fileName, uint expireInSecond = 300) + { + //OssClient ossClient = new OssClient("httos://" + _endpoint, _accessKeyId, _accessKeySecret); + var policy = new PolicyConditions(); + // 设置最大文件1000M + policy.AddConditionItem(PolicyConditions.CondContentLengthRange, 0, 1048576000); + policy.AddConditionItem(MatchMode.StartWith, PolicyConditions.CondKey, path.TrimEnd('/') + '/'); + + var postPolicy = _ossClient.GeneratePostPolicy(DateTimeOffset.Now.AddSeconds(expireInSecond).LocalDateTime, policy); + var policyBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(postPolicy)); + + // 计算签名 + var hmac = new HMACSHA1(Encoding.UTF8.GetBytes(_accessKeySecret)); + var bytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(policyBase64)); + var sign = Convert.ToBase64String(bytes); + + var host = $"https://{_bucketName}.{_endpoint}"; + var key = $"{path.TrimEnd('/')}/{fileName}"; + //var fullUrl = $"https://{_bucketName}.{_endpoint}/{key}"; + + return new Dictionary + { + { "OSSAccessKeyId", _configuration["OSS:AccessKeyID"]}, + { "Host",host }, + { "key",key}, + { "policy",policyBase64}, + { "Signature",sign}, + { "success_action_status","200"}, + //{ "fullUrl",fullUrl }, + {"expire",expireInSecond.ToString() }, + { "name",fileName} + }; + } + + public string GetAccessFileUrl(string path) + { + if (string.IsNullOrWhiteSpace(path) ) + { + throw Oops.Oh("路径不能为空!"); + } + + var urlRequest = new GeneratePresignedUriRequest(_bucketName, path) + { + Expiration = DateTimeOffset.Now.AddMinutes(1).LocalDateTime + }; + + return _ossClient.GeneratePresignedUri(urlRequest).ToString(); + } + + public bool DeleteObjects(params string[] remotePathList) + { + if (remotePathList.IsNullOrEmpty()) + { + return true; + } + + var result = _ossClient.DeleteObjects(new DeleteObjectsRequest(_bucketName, remotePathList, true)); + + return result.HttpStatusCode == System.Net.HttpStatusCode.OK; + } + } +} diff --git a/WGShare.API/Hubs/IMessageClient.cs b/WGShare.API/Hubs/IMessageClient.cs new file mode 100644 index 0000000..06f6b7d --- /dev/null +++ b/WGShare.API/Hubs/IMessageClient.cs @@ -0,0 +1,33 @@ +namespace WGShare.API.Hubs +{ + /// + /// 客户端消息 + /// + public interface IMessageClient + { + /// + /// 接受频道消息 + /// + /// + /// + /// + Task ReceiveMessage(string userName, string message); + + + /// + /// 邀请进入会议室 + /// + /// 会议号 + /// 会议名称 + /// 邀请人名 + /// + Task Invitation(string roomNum, string roomName, string InviterName); + + /// + /// 强制退出房间 + /// + /// 会议号 + /// + Task ForceExitRoom(string roomNum); + } +} diff --git a/WGShare.API/Hubs/SessionManageHub.cs b/WGShare.API/Hubs/SessionManageHub.cs new file mode 100644 index 0000000..bc484f1 --- /dev/null +++ b/WGShare.API/Hubs/SessionManageHub.cs @@ -0,0 +1,141 @@ +using Masuit.Tools; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using System.Text; +using WGShare.API.Helpers; +using WGShare.Domain.Constant; + +namespace WGShare.API.Hubs +{ + [Authorize] + public class SessionManageHub : Hub + { + + public async override Task OnConnectedAsync() + { + var tenant = Context.User?.Claims.FirstOrDefault(x => x.Type == "tenant")?.Value; + var uid = Context.User?.Claims.FirstOrDefault(x => x.Type == "uid")?.Value; + + await Console.Out.WriteLineAsync("连接成功 当前租户:" + tenant); + await Console.Out.WriteLineAsync("连接成功 uid:" + uid); + await Console.Out.WriteLineAsync("连接成功 connectId:" + Context.ConnectionId); + + // 存储在线信息 + RedisHelper.Instance.HSet(RedisKeyConstant.SessionManage.GetOnlineUserKey(tenant), uid, Context.ConnectionId); + } + + public async override Task OnDisconnectedAsync(Exception? exception) + { + var tenant = Context.User?.Claims.FirstOrDefault(x => x.Type == "tenant")?.Value; + var uid = Context.User?.Claims.FirstOrDefault(x => x.Type == "uid")?.Value; + + await Console.Out.WriteLineAsync("断开连接 当前租户:" + tenant); + await Console.Out.WriteLineAsync("断开连接 uid:" + uid); + await Console.Out.WriteLineAsync("断开连接 connectId:" + Context.ConnectionId); + + // 获取用户参加得频道 + var roomNums = RedisHelper.Instance.HKeys(RedisKeyConstant.SessionManage.GetUserJoinChannelKey(uid)); + using (var pipe = RedisHelper.Instance.StartPipe()) + { + // 移除在线信息 + pipe.HDel(RedisKeyConstant.SessionManage.GetOnlineUserKey(tenant), uid); + if (!roomNums.IsNullOrEmpty()) + { + await Console.Out.WriteLineAsync($@"uid:{uid} 退出以下频道:{string.Join(',', roomNums)}"); + // 所有房间移除该用户 + roomNums.ForEach(roomNum => + { + pipe.HDel(RedisKeyConstant.SessionManage.GetChannelUserKey(tenant, roomNum), uid); + pipe.HIncrBy(RedisKeyConstant.SessionManage.GetChannelUserCountKey(tenant), roomNum, -1); + }); + } + // 删除用户已加入频道信息 + pipe.Del(RedisKeyConstant.SessionManage.GetUserJoinChannelKey(uid)); + + pipe.EndPipe(); + } + + } + + /// + /// 加入频道 + /// + /// + [HubMethodName("joinChannel")] + public async Task JoinChannel(string roomNum) + { + var tenant = Context.User?.Claims.FirstOrDefault(x => x.Type == "tenant")?.Value; + var uid = Context.User?.Claims.FirstOrDefault(x => x.Type == "uid")?.Value; + + using (var pipe = RedisHelper.Instance.StartPipe()) + { + pipe.HSet(RedisKeyConstant.SessionManage.GetChannelUserKey(tenant, roomNum), uid, Context.ConnectionId); + pipe.HIncrBy(RedisKeyConstant.SessionManage.GetChannelUserCountKey(tenant), roomNum, 1); + pipe.HSet(RedisKeyConstant.SessionManage.GetUserJoinChannelKey(uid), roomNum, 1); + pipe.EndPipe(); + } + + await Groups.AddToGroupAsync(Context.ConnectionId, roomNum); + } + + /// + /// 离开频道 + /// + /// + [HubMethodName("levelChannel")] + public async Task LevelChannel(string roomNum) + { + var tenant = Context.User?.Claims.FirstOrDefault(x => x.Type == "tenant")?.Value; + var uid = Context.User?.Claims.FirstOrDefault(x => x.Type == "uid")?.Value; + + using (var pipe = RedisHelper.Instance.StartPipe()) + { + pipe.HDel(RedisKeyConstant.SessionManage.GetChannelUserKey(tenant, roomNum), uid); + pipe.HDel(RedisKeyConstant.SessionManage.GetUserJoinChannelKey(uid), roomNum); + pipe.HIncrBy(RedisKeyConstant.SessionManage.GetChannelUserCountKey(tenant), roomNum, -1); + pipe.EndPipe(); + } + + await Groups.RemoveFromGroupAsync(Context.ConnectionId, roomNum); + } + + /// + /// 发送频道消息 + /// + /// + /// + /// + [HubMethodName("sendChannelMsg")] + public async Task SenMessage(string rooNum, string msg) + { + var uname = Context.User?.Claims.FirstOrDefault(x => x.Type == "uname")?.Value; + + await Clients.GroupExcept(rooNum, Context.ConnectionId).ReceiveMessage(uname, msg); + } + + ///// + ///// 邀请呼叫 + ///// + ///// 被邀请人id + ///// 会议名称 + ///// 会议号 + ///// + //[HubMethodName("call")] + //public async Task<(bool isSuccess, string msg)> CallUser(string inviteeUid, string roomName, string roomNum) + //{ + // var tenant = Context.User?.Claims.FirstOrDefault(x => x.Type == "tenant")?.Value; + // var uname = Context.User?.Claims.FirstOrDefault(x => x.Type == "uname")?.Value; + + // var connectId = RedisHelper.Instance.HGet(RedisKeyConstant.SessionManage.GetOnlineUserKey(tenant), inviteeUid); + // if (string.IsNullOrWhiteSpace(connectId)) + // { + // return (false, "被邀请人不在线"); + // } + + // await Clients.Client(connectId).Invitation(roomNum, roomName, uname); + + // return (true, "邀请成功"); + //} + + } +} diff --git a/WGShare.API/Program.cs b/WGShare.API/Program.cs index 2df77da..5ef5c5c 100644 --- a/WGShare.API/Program.cs +++ b/WGShare.API/Program.cs @@ -1,9 +1,14 @@ +using Hangfire; +using Hangfire.MemoryStorage; +using Microsoft.AspNetCore.SignalR; using Microsoft.OpenApi.Models; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; using SqlSugar; +using WGShare.API.BackgroudServices; using WGShare.API.Helpers; +using WGShare.API.Hubs; using WGShare.API.ServiceConfigs; using Yitter.IdGenerator; @@ -18,6 +23,7 @@ namespace WGShare.API // Add services to the container. + RedisHelper.Initialization(new FreeRedis.RedisClient(configuration["Redis:master"])); builder.Services.AddControllers(options => { // ȫ쳣ڴ д try catch @@ -37,8 +43,14 @@ namespace WGShare.API }); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddSwagger(); + builder.Services.AddSwagger(); + builder.Services.AddSignalR(); + builder.Services.AddHttpClient(); + builder.Services.ConfigureHangfire(); builder.Services.AddSingleton(new JwtHelper(configuration)); + builder.Services.AddSingleton(new AgoraHelper(configuration)); + builder.Services.AddSingleton(new OssHelper(configuration)); + builder.Services.AddHostedService(); builder.Services.AddAuth(configuration["Jwt:Issuer"], configuration["Jwt:Audience"], configuration["Jwt:SecretKey"]); @@ -57,7 +69,6 @@ namespace WGShare.API IsAutoCloseConnection = true }); YitIdHelper.SetIdGenerator(new IdGeneratorOptions(1)); - RedisHelper.Initialization(new FreeRedis.RedisClient(configuration["Redis:master"])); var app = builder.Build(); @@ -74,13 +85,15 @@ namespace WGShare.API }); app.UseCustomCors(); } - + // Hangfire Dashboard + app.UseHangfireDashboard(); //мUseAuthentication֤Ҫ֤мǰã UseAuthorizationȨ app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); + app.MapHub("/session-manage").RequireCors(q => q.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()); app.Run(); } diff --git a/WGShare.API/Properties/launchSettings.json b/WGShare.API/Properties/launchSettings.json index bb54335..91a6ab0 100644 --- a/WGShare.API/Properties/launchSettings.json +++ b/WGShare.API/Properties/launchSettings.json @@ -14,7 +14,7 @@ "dotnetRunMessages": true, "launchBrowser": false, "launchUrl": "swagger", - "applicationUrl": "http://localhost:5192", + "applicationUrl": "http://*:5192", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/WGShare.API/Reference/AgoraIO.dll b/WGShare.API/Reference/AgoraIO.dll new file mode 100644 index 0000000000000000000000000000000000000000..01a8ba1f1ad281c4d827fa6e661579821c5269ca GIT binary patch literal 21504 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 BJ@o(p literal 0 HcmV?d00001 diff --git a/WGShare.API/ServiceConfigs/AuthenticationServiceExtensions.cs b/WGShare.API/ServiceConfigs/AuthenticationServiceExtensions.cs index 1066293..cfecd94 100644 --- a/WGShare.API/ServiceConfigs/AuthenticationServiceExtensions.cs +++ b/WGShare.API/ServiceConfigs/AuthenticationServiceExtensions.cs @@ -36,6 +36,21 @@ namespace WGShare.API.ServiceConfigs ClockSkew = TimeSpan.FromSeconds(30), //过期时间容错值,解决服务器端时间不同步问题(秒) RequireExpirationTime = true, }; + + //// signalR 鉴权 + options.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + var accessToken = context.Request.Query["access_token"]; + var path = context.HttpContext.Request.Path; + if (!string.IsNullOrEmpty(accessToken) && (path.StartsWithSegments("/session-manage"))) + { + context.Token = accessToken; + } + return Task.CompletedTask; + } + }; }); diff --git a/WGShare.API/ServiceConfigs/CorsAppBuilderExtensions.cs b/WGShare.API/ServiceConfigs/CorsAppBuilderExtensions.cs index b30e0a5..19d112e 100644 --- a/WGShare.API/ServiceConfigs/CorsAppBuilderExtensions.cs +++ b/WGShare.API/ServiceConfigs/CorsAppBuilderExtensions.cs @@ -18,7 +18,7 @@ namespace WGShare.API.ServiceConfigs //var hosts = AppSetting.AllowedHosts.Split("|", StringSplitOptions.RemoveEmptyEntries); app.UseCors(options => { - options.WithOrigins("*") // 允许跨域请求的地址 + options.AllowAnyOrigin() // 允许跨域请求的地址 .AllowAnyHeader() // 允许的请求标头 .AllowAnyMethod(); // 允许跨域请求的类型 (GET,POST等) diff --git a/WGShare.API/ServiceConfigs/HangfireServiceExtensions.cs b/WGShare.API/ServiceConfigs/HangfireServiceExtensions.cs new file mode 100644 index 0000000..4b19e78 --- /dev/null +++ b/WGShare.API/ServiceConfigs/HangfireServiceExtensions.cs @@ -0,0 +1,41 @@ +using Mapster; +using MapsterMapper; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; +using Newtonsoft.Json; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using System.Text; +using System.Reflection.Metadata; +using Microsoft.OpenApi.Models; +using Hangfire; +using Hangfire.MemoryStorage; + +namespace WGShare.API.ServiceConfigs +{ + public static class HangfireServiceExtensions + { + ///

+ /// 添加Hangfire + /// + /// + /// + public static IServiceCollection ConfigureHangfire(this IServiceCollection services) + { + services.AddHangfire(x => x.UseMemoryStorage()); + // Hangfire全局配置 + GlobalConfiguration.Configuration + .UseColouredConsoleLogProvider() + .UseMemoryStorage() + .WithJobExpirationTimeout(TimeSpan.FromDays(7)); + + // Hangfire服务器配置 + services.AddHangfireServer(options => + { + options.HeartbeatInterval = TimeSpan.FromSeconds(10); + }); + + return services; + } + } +} diff --git a/WGShare.API/ServiceConfigs/SwaggerServiceExtensions.cs b/WGShare.API/ServiceConfigs/SwaggerServiceExtensions.cs index caf7cb8..356c089 100644 --- a/WGShare.API/ServiceConfigs/SwaggerServiceExtensions.cs +++ b/WGShare.API/ServiceConfigs/SwaggerServiceExtensions.cs @@ -49,8 +49,8 @@ namespace WGShare.API.ServiceConfigs } }); - //string xmlPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "SwaggerGroup.xml"); - //w.IncludeXmlComments(xmlPath, true); + string xmlPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "WGShare.API.xml"); + w.IncludeXmlComments(xmlPath, true); }); diff --git a/WGShare.API/WGShare.API.csproj b/WGShare.API/WGShare.API.csproj index 948283a..508e775 100644 --- a/WGShare.API/WGShare.API.csproj +++ b/WGShare.API/WGShare.API.csproj @@ -5,9 +5,15 @@ enable enable True + WGShare.API.xml + + + + + @@ -15,4 +21,19 @@ + + + Reference\AgoraIO.dll + + + + + + Never + + + PreserveNewest + + + diff --git a/WGShare.API/WGShare.API.xml b/WGShare.API/WGShare.API.xml new file mode 100644 index 0000000..5ca2021 --- /dev/null +++ b/WGShare.API/WGShare.API.xml @@ -0,0 +1,381 @@ + + + + WGShare.API + + + + + 检查用户名 + + + + + + + 正常账号登录 + + + + + + 匿名登录,直接进入会议室 + + + + + + 登出(暂未处理任何业务逻辑) + + + + + + 管理员登录 + + + + + + 管理员信息 + + + + + + 获取最新权限值 + + + + + + 权限修改 + + + + + + 首页接口 + + + + + 获取会议室列表 + + + + + + + 创建会议室 + + + + + + + 获取 rtm token + + + + + + 会议室接口 + + + + + 设置房间管理员 + + + + + + 取消房间管理员 + + + + + + 查询用户信息 + + + + + + 检验房间是否存在 + + + + + + 获取单个会议室信息 + + + + + + 获取房间rtc token + + + + + + 邀请用户 + + + + + + 踢出房间 + + + + + + 分享上传文件 + + + + + + + 删除文件 + + + + + + 获取分享文件列表 + + 房间Id + + + + + + 获取文件上传url + + + + + + 获取文件下载地址 + + + 文件Id + + + + + 获取用户列表 + + 用户名 或 账号 + 翻页信息 + + + + + 新增用户 + + + + + + + 修改用户信息 + + + + + + + 更改密码 + + + + + + + 删除用户 + + + + + + + 前后端共用接口 + + + + + 角色列表下拉框 + + + + + + Redis 键,hash ,每个频道用户数量 + + + + + 获取完整的声网API请求路径 + + api .com 后的路径后缀 + + + + + 获取频道用户 + + + + + + + 更新每个频道用户在线数量 + + + + + + 获取上传url + + bucket 路径 + 文件名 + 过期时间 秒 + + + + + redis静态访问类 + + + + + redis实例 + + + + + 初始化redis静态访问类 RedisHelper.Initialization(new FreeRedis.RedisClient(\"127.0.0.1:6379,password=123,defaultDatabase=13,maxpoolsize=50,prefix=key前辍\")) + + + + + + 随机秒(防止所有key同一时间过期,雪崩) + + 最小秒数 + 最大秒数 + + + + + 客户端消息 + + + + + 接受频道消息 + + + + + + + + 邀请进入会议室 + + 会议号 + 会议名称 + 邀请人名 + + + + + 强制退出房间 + + 会议号 + + + + + 加入频道 + + + + + + 离开频道 + + + + + + 发送频道消息 + + + + + + + + 添加认证和授权 + + 服务集合 + + + + + 跨域配置扩展服务 + + + + + 允许所有跨域请求 (属于基础服务,若已调用 UseBasicServices 则无需调用) + + + + + + + 全局异常捕获 + + + + + 添加Hangfire + + + + + + + 在Controller的Action执行后执行 + + + + + + 在Controller的Action执行前执行 + + + + + + 添加SqlSugar + + 服务集合 + + + + + 添加认证和授权 + + 服务集合 + + + + diff --git a/WGShare.API/appsettings.Development.json b/WGShare.API/appsettings.Development.json index f5c2cc0..2ca3bb7 100644 --- a/WGShare.API/appsettings.Development.json +++ b/WGShare.API/appsettings.Development.json @@ -10,6 +10,6 @@ "usercenter": "Database=usercenter;Server=192.168.2.9;Port=3306;Uid=root;Pwd=qwe123!@#;AllowZeroDateTime=True;ConvertZeroDateTime=True;" }, "Redis": { - "master": "192.168.2.7:6379,password=qwe123!@#,defaultDatabase=12,name=wgshare,prefix=wgshare" + "master": "192.168.2.7:6379,password=qwe123!@#,defaultDatabase=13,name=wgshare,prefix=wgshare" } } diff --git a/WGShare.API/appsettings.json b/WGShare.API/appsettings.json index f5b8963..de42024 100644 --- a/WGShare.API/appsettings.json +++ b/WGShare.API/appsettings.json @@ -11,13 +11,28 @@ "Issuer": "WGshareApi", "Audience": "WGshareClient", // 过期 秒 - "Expires": 14400 + "Expires": 86400 }, "ConnectionStrings": { "metting": "Database=metting;Server=192.168.2.9;Port=3306;Uid=root;Pwd=qwe123!@#;AllowZeroDateTime=True;ConvertZeroDateTime=True;", "usercenter": "Database=usercenter;Server=192.168.2.9;Port=3306;Uid=root;Pwd=qwe123!@#;AllowZeroDateTime=True;ConvertZeroDateTime=True;" }, "Redis": { - "master": "" + "master": "172.29.33.83:16379,password=poiuyt)(*&^%,defaultDatabase=13,prefix=wgshare" + }, + "Agora": { + "appId": "dcfc466a6ecb4a1f972630065dfb1e75", + "appSecret": "fc77000e329b4be7a0e26fa789e20d00", + "tokenExpireTimeInSecond": 7200, + "apiPrefix": "https://api.sd-rtn.com/", + "clientId": "80cdc24f7dfa4497a37d98da95a3c4a4", + "clientSecret": "8323581d4d464114b1f324b26cc62e09" + }, + "OSS": { + "AccessKeyID": "LTAI5tQYVQHkkXxXTmjwiSDv", + "AccessKeySecret": "FKFNYRdS53FwA5ME2wM1585qX5eVEd", + "Endpoint": "oss-cn-chengdu.aliyuncs.com", + "Host": "https://wgshare.oss-cn-chengdu.aliyuncs.com/", + "BucketName": "wgshare" } } diff --git a/WGShare.Domain/AgoraApiResult/AgoraResponse.cs b/WGShare.Domain/AgoraApiResult/AgoraResponse.cs new file mode 100644 index 0000000..9fde832 --- /dev/null +++ b/WGShare.Domain/AgoraApiResult/AgoraResponse.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WGShare.Domain.AgoraApiResult +{ + public class AgoraResponse + { + public bool success { get; set; } + + public T Data { get; set; } + } +} diff --git a/WGShare.Domain/AgoraApiResult/Channel.cs b/WGShare.Domain/AgoraApiResult/Channel.cs new file mode 100644 index 0000000..cb70bf6 --- /dev/null +++ b/WGShare.Domain/AgoraApiResult/Channel.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WGShare.Domain.AgoraApiResult +{ + public class Channel + { + public List channels { get; set; } + public int total_size { get; set; } + } + + public class ChannelUserCount + { + public string channel_name { get; set; } + + public int user_count { get; set; } + } +} diff --git a/WGShare.Domain/AgoraApiResult/ChannelUser.cs b/WGShare.Domain/AgoraApiResult/ChannelUser.cs new file mode 100644 index 0000000..c8b7e03 --- /dev/null +++ b/WGShare.Domain/AgoraApiResult/ChannelUser.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WGShare.Domain.AgoraApiResult +{ + public class ChannelUser + { + /// + /// 频道是否存在 + /// + public bool channel_exist { get; set; } + + /// + /// 频道场景 1:通信场景 2:直播场景 + /// + public int mode { get; set; } + + /// + /// 频道总人数 + /// + public int total { get; set; } + + public List users { get; set; } + } +} diff --git a/WGShare.Domain/Constant/RedisKeyConstant.cs b/WGShare.Domain/Constant/RedisKeyConstant.cs new file mode 100644 index 0000000..4a39640 --- /dev/null +++ b/WGShare.Domain/Constant/RedisKeyConstant.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WGShare.Domain.Constant +{ + /// + /// redis key 常量值 + /// + public class RedisKeyConstant + { + /// + /// 会话管理 + /// + public class SessionManage + { + /// + /// 在线人数 + /// + public static string GetOnlineUserKey(string tenantId) => $@"te_{tenantId}:OnlieUser"; + + /// + /// 频道用户 + /// + /// + /// + /// + public static string GetChannelUserKey(string tenantId, string roomNum) => $@"te_{tenantId}:ch_{roomNum}"; + + /// + /// 用户参与频道 + /// + /// + /// + public static string GetUserJoinChannelKey(string uid) => $@"u_{uid}_join"; + + /// + /// 频道用户数 + /// + /// + /// + public static string GetChannelUserCountKey(string tenantId) => $@"te_{tenantId}:ChannelUserCount"; + } + } +} diff --git a/WGShare.Domain/DTOs/File/ShareFileOutputDTO.cs b/WGShare.Domain/DTOs/File/ShareFileOutputDTO.cs index a145339..723fc08 100644 --- a/WGShare.Domain/DTOs/File/ShareFileOutputDTO.cs +++ b/WGShare.Domain/DTOs/File/ShareFileOutputDTO.cs @@ -16,7 +16,7 @@ namespace WGShare.Domain.DTOs.File ///
public string FileUrl { get; set; } /// - /// 文件大小 kb + /// 文件大小 字节数 /// public double Size { get; set; } /// diff --git a/WGShare.Domain/DTOs/Room/RoomOutputDTO.cs b/WGShare.Domain/DTOs/Room/RoomOutputDTO.cs index e85a478..bc49fcc 100644 --- a/WGShare.Domain/DTOs/Room/RoomOutputDTO.cs +++ b/WGShare.Domain/DTOs/Room/RoomOutputDTO.cs @@ -20,5 +20,9 @@ namespace WGShare.Domain.DTOs.Room /// 会议号 /// public string RoomNum { get; set; } + /// + /// 在线人数 + /// + public int OnlineUserCount { get; set; } } } diff --git a/WGShare.Domain/DTOs/User/UserInputDTO.cs b/WGShare.Domain/DTOs/User/UserInputDTO.cs index 6576755..b003ca3 100644 --- a/WGShare.Domain/DTOs/User/UserInputDTO.cs +++ b/WGShare.Domain/DTOs/User/UserInputDTO.cs @@ -22,7 +22,7 @@ namespace WGShare.Domain.DTOs.User /// /// 密码 /// - public string Pwd { get; set; } + public string? Pwd { get; set; } /// /// /// @@ -30,6 +30,6 @@ namespace WGShare.Domain.DTOs.User /// /// 租户id /// - public string TenantId { get; set; } + public string? TenantId { get; set; } } } diff --git a/WGShare.Domain/DTOs/User/UserOutputDTO.cs b/WGShare.Domain/DTOs/User/UserOutputDTO.cs index b04cbb1..0f16066 100644 --- a/WGShare.Domain/DTOs/User/UserOutputDTO.cs +++ b/WGShare.Domain/DTOs/User/UserOutputDTO.cs @@ -21,5 +21,10 @@ namespace WGShare.Domain.DTOs.User public string RoleId { get; set; } public string RoleName { get; set; } + /// + /// 是否管理员 + /// + public bool IsManager { get; set; } + } } diff --git a/WGShare.Domain/Entities/ShareFile.cs b/WGShare.Domain/Entities/ShareFile.cs index 3f0bccb..d451541 100644 --- a/WGShare.Domain/Entities/ShareFile.cs +++ b/WGShare.Domain/Entities/ShareFile.cs @@ -44,7 +44,7 @@ namespace WGShare.Domain.Entities [SugarColumn(ColumnName = "user_id")] public string UserId { get; set; } /// - /// 文件大小 kb + /// 文件大小 字节数 /// [SugarColumn(ColumnName = "size")] public double Size { get; set; } @@ -63,5 +63,10 @@ namespace WGShare.Domain.Entities ///
[SugarColumn(ColumnName = "download_count")] public int DownloadCount { get; set; } + /// + /// 文件是否被删除 + /// + [SugarColumn(ColumnName = "is_fileclean")] + public bool IsFileClean { get; set; } } } diff --git a/agora_key_and_secret.txt b/agora_key_and_secret.txt new file mode 100644 index 0000000..6824db3 --- /dev/null +++ b/agora_key_and_secret.txt @@ -0,0 +1,3 @@ +证书在下载后将不再控制台保存或展示,请妥善保管证书 +key:80cdc24f7dfa4497a37d98da95a3c4a4 +secret:8323581d4d464114b1f324b26cc62e09 \ No newline at end of file