From a4a75188bd17e31b39a02511bbd6d628bab5c909 Mon Sep 17 00:00:00 2001 From: crupest Date: Sun, 25 Apr 2021 21:20:04 +0800 Subject: ... --- .../Services/Api/BookmarkTimelineService.cs | 205 +++++++++++++++++++++ .../Services/Api/HighlightTimelineService.cs | 194 +++++++++++++++++++ BackEnd/Timeline/Services/Api/SearchService.cs | 104 +++++++++++ 3 files changed, 503 insertions(+) create mode 100644 BackEnd/Timeline/Services/Api/BookmarkTimelineService.cs create mode 100644 BackEnd/Timeline/Services/Api/HighlightTimelineService.cs create mode 100644 BackEnd/Timeline/Services/Api/SearchService.cs (limited to 'BackEnd/Timeline/Services/Api') diff --git a/BackEnd/Timeline/Services/Api/BookmarkTimelineService.cs b/BackEnd/Timeline/Services/Api/BookmarkTimelineService.cs new file mode 100644 index 00000000..0d4cc0a6 --- /dev/null +++ b/BackEnd/Timeline/Services/Api/BookmarkTimelineService.cs @@ -0,0 +1,205 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Services.Timeline; +using Timeline.Services.User; + +namespace Timeline.Services.Api +{ + + [Serializable] + public class InvalidBookmarkException : Exception + { + public InvalidBookmarkException() { } + public InvalidBookmarkException(string message) : base(message) { } + public InvalidBookmarkException(string message, Exception inner) : base(message, inner) { } + protected InvalidBookmarkException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + } + + /// + /// Service interface that manages timeline bookmarks. + /// + public interface IBookmarkTimelineService + { + /// + /// Get bookmarks of a user. + /// + /// User id of bookmark owner. + /// Id of Bookmark timelines in order. + /// Thrown when user does not exist. + Task> GetBookmarks(long userId); + + /// + /// Check if a timeline is a bookmark. + /// + /// The user id. + /// Timeline id. + /// If true it will throw when user does not exist. + /// If true it will throw when timeline does not exist. + /// True if timeline is a bookmark. Otherwise false. + /// Throw if user does not exist and is true. + /// Thrown if timeline does not exist and is true. + Task IsBookmark(long userId, long timelineId, bool checkUserExistence = true, bool checkTimelineExistence = true); + + /// + /// Add a bookmark to tail to a user. + /// + /// User id of bookmark owner. + /// Timeline id. + /// True if timeline is added to bookmark. False if it already is. + /// Thrown when user does not exist. + /// Thrown when timeline does not exist. + Task AddBookmark(long userId, long timelineId); + + /// + /// Remove a bookmark from a user. + /// + /// User id of bookmark owner. + /// Timeline id. + /// True if deletion is performed. False if bookmark does not exist. + /// Thrown when user does not exist. + /// Thrown when timeline does not exist. + Task RemoveBookmark(long userId, long timelineId); + + /// + /// Move bookmark to a new position. + /// + /// User id of bookmark owner. + /// Timeline name. + /// New position. Starts at 1. + /// Thrown when user does not exist. + /// Thrown when timeline does not exist. + /// Thrown when the timeline is not a bookmark. + Task MoveBookmark(long userId, long timelineId, long newPosition); + } + + public class BookmarkTimelineService : IBookmarkTimelineService + { + private readonly DatabaseContext _database; + private readonly IBasicUserService _userService; + private readonly IBasicTimelineService _timelineService; + + public BookmarkTimelineService(DatabaseContext database, IBasicUserService userService, IBasicTimelineService timelineService) + { + _database = database; + _userService = userService; + _timelineService = timelineService; + } + + public async Task AddBookmark(long userId, long timelineId) + { + if (!await _userService.CheckUserExistence(userId)) + throw new UserNotExistException(userId); + + if (!await _timelineService.CheckExistence(timelineId)) + throw new TimelineNotExistException(timelineId); + + if (await _database.BookmarkTimelines.AnyAsync(t => t.TimelineId == timelineId && t.UserId == userId)) + return false; + + _database.BookmarkTimelines.Add(new BookmarkTimelineEntity + { + TimelineId = timelineId, + UserId = userId, + Rank = (await _database.BookmarkTimelines.CountAsync(t => t.UserId == userId)) + 1 + }); + + await _database.SaveChangesAsync(); + return true; + } + + public async Task> GetBookmarks(long userId) + { + if (!await _userService.CheckUserExistence(userId)) + throw new UserNotExistException(userId); + + var entities = await _database.BookmarkTimelines.Where(t => t.UserId == userId).OrderBy(t => t.Rank).Select(t => new { t.TimelineId }).ToListAsync(); + + return entities.Select(e => e.TimelineId).ToList(); + } + + public async Task IsBookmark(long userId, long timelineId, bool checkUserExistence = true, bool checkTimelineExistence = true) + { + if (checkUserExistence && !await _userService.CheckUserExistence(userId)) + throw new UserNotExistException(userId); + + if (checkTimelineExistence && !await _timelineService.CheckExistence(timelineId)) + throw new TimelineNotExistException(timelineId); + + return await _database.BookmarkTimelines.AnyAsync(b => b.TimelineId == timelineId && b.UserId == userId); + } + + public async Task MoveBookmark(long userId, long timelineId, long newPosition) + { + if (!await _userService.CheckUserExistence(userId)) + throw new UserNotExistException(userId); + + if (!await _timelineService.CheckExistence(timelineId)) + throw new TimelineNotExistException(timelineId); + + var entity = await _database.BookmarkTimelines.SingleOrDefaultAsync(t => t.TimelineId == timelineId && t.UserId == userId); + + if (entity == null) throw new InvalidBookmarkException("You can't move a non-bookmark timeline."); + + var oldPosition = entity.Rank; + + if (newPosition < 1) + { + newPosition = 1; + } + else + { + var totalCount = await _database.BookmarkTimelines.CountAsync(t => t.UserId == userId); + if (newPosition > totalCount) newPosition = totalCount; + } + + if (oldPosition == newPosition) return; + + await using var transaction = await _database.Database.BeginTransactionAsync(); + + if (newPosition > oldPosition) + { + await _database.Database.ExecuteSqlRawAsync("UPDATE `bookmark_timelines` SET `rank` = `rank` - 1 WHERE `rank` BETWEEN {0} AND {1} AND `user` = {2}", oldPosition + 1, newPosition, userId); + await _database.Database.ExecuteSqlRawAsync("UPDATE `bookmark_timelines` SET `rank` = {0} WHERE `id` = {1}", newPosition, entity.Id); + } + else + { + await _database.Database.ExecuteSqlRawAsync("UPDATE `bookmark_timelines` SET `rank` = `rank` + 1 WHERE `rank` BETWEEN {0} AND {1} AND `user` = {2}", newPosition, oldPosition - 1, userId); + await _database.Database.ExecuteSqlRawAsync("UPDATE `bookmark_timelines` SET `rank` = {0} WHERE `id` = {1}", newPosition, entity.Id); + } + + await transaction.CommitAsync(); + } + + public async Task RemoveBookmark(long userId, long timelineId) + { + if (!await _userService.CheckUserExistence(userId)) + throw new UserNotExistException(userId); + + if (!await _timelineService.CheckExistence(timelineId)) + throw new TimelineNotExistException(timelineId); + + var entity = await _database.BookmarkTimelines.SingleOrDefaultAsync(t => t.UserId == userId && t.TimelineId == timelineId); + + if (entity == null) return false; + + await using var transaction = await _database.Database.BeginTransactionAsync(); + + var rank = entity.Rank; + + _database.BookmarkTimelines.Remove(entity); + await _database.SaveChangesAsync(); + + await _database.Database.ExecuteSqlRawAsync("UPDATE `bookmark_timelines` SET `rank` = `rank` - 1 WHERE `rank` > {0}", rank); + + await transaction.CommitAsync(); + + return true; + } + } +} diff --git a/BackEnd/Timeline/Services/Api/HighlightTimelineService.cs b/BackEnd/Timeline/Services/Api/HighlightTimelineService.cs new file mode 100644 index 00000000..9ef8ea84 --- /dev/null +++ b/BackEnd/Timeline/Services/Api/HighlightTimelineService.cs @@ -0,0 +1,194 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Services.Timeline; +using Timeline.Services.User; + +namespace Timeline.Services.Api +{ + + [Serializable] + public class InvalidHighlightTimelineException : Exception + { + public InvalidHighlightTimelineException() { } + public InvalidHighlightTimelineException(string message) : base(message) { } + public InvalidHighlightTimelineException(string message, Exception inner) : base(message, inner) { } + protected InvalidHighlightTimelineException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + } + + /// + /// Service that controls highlight timeline. + /// + public interface IHighlightTimelineService + { + /// + /// Get all highlight timelines in order. + /// + /// Id list of all highlight timelines. + Task> GetHighlightTimelines(); + + /// + /// Check if a timeline is highlight timeline. + /// + /// Timeline id. + /// If true it will throw if timeline does not exist. + /// True if timeline is highlight. Otherwise false. + /// Thrown when timeline does not exist and is true. + Task IsHighlightTimeline(long timelineId, bool checkTimelineExistence = true); + + /// + /// Add a timeline to highlight list. + /// + /// The timeline id. + /// The user id of operator. + /// True if timeline is actually added to highligh. False if it already is. + /// Thrown when timeline with given id does not exist. + /// Thrown when user with given operator id does not exist. + Task AddHighlightTimeline(long timelineId, long? operatorId); + + /// + /// Remove a timeline from highlight list. + /// + /// The timeline id. + /// The user id of operator. + /// True if deletion is actually performed. Otherwise false (timeline was not in the list). + /// Thrown when timeline with given id does not exist. + /// Thrown when user with given operator id does not exist. + Task RemoveHighlightTimeline(long timelineId, long? operatorId); + + /// + /// Move a highlight timeline to a new position. + /// + /// The timeline name. + /// The new position. Starts at 1. + /// Thrown when timeline with given id does not exist. + /// Thrown when given timeline is not a highlight timeline. + /// + /// If is smaller than 1. Then move the timeline to head. + /// If is bigger than total count. Then move the timeline to tail. + /// + Task MoveHighlightTimeline(long timelineId, long newPosition); + } + + public class HighlightTimelineService : IHighlightTimelineService + { + private readonly DatabaseContext _database; + private readonly IBasicUserService _userService; + private readonly IBasicTimelineService _timelineService; + private readonly IClock _clock; + + public HighlightTimelineService(DatabaseContext database, IBasicUserService userService, IBasicTimelineService timelineService, IClock clock) + { + _database = database; + _userService = userService; + _timelineService = timelineService; + _clock = clock; + } + + public async Task AddHighlightTimeline(long timelineId, long? operatorId) + { + if (!await _timelineService.CheckExistence(timelineId)) + throw new TimelineNotExistException(timelineId); + + if (operatorId.HasValue && !await _userService.CheckUserExistence(operatorId.Value)) + { + throw new UserNotExistException(null, operatorId.Value, "User with given operator id does not exist.", null); + } + + var alreadyIs = await _database.HighlightTimelines.AnyAsync(t => t.TimelineId == timelineId); + + if (alreadyIs) return false; + + _database.HighlightTimelines.Add(new HighlightTimelineEntity { TimelineId = timelineId, OperatorId = operatorId, AddTime = _clock.GetCurrentTime(), Order = await _database.HighlightTimelines.CountAsync() + 1 }); + await _database.SaveChangesAsync(); + return true; + } + + public async Task> GetHighlightTimelines() + { + var entities = await _database.HighlightTimelines.OrderBy(t => t.Order).Select(t => new { t.TimelineId }).ToListAsync(); + + return entities.Select(e => e.TimelineId).ToList(); + } + + public async Task RemoveHighlightTimeline(long timelineId, long? operatorId) + { + if (!await _timelineService.CheckExistence(timelineId)) + throw new TimelineNotExistException(timelineId); + + if (operatorId.HasValue && !await _userService.CheckUserExistence(operatorId.Value)) + { + throw new UserNotExistException(null, operatorId.Value, "User with given operator id does not exist.", null); + } + + var entity = await _database.HighlightTimelines.SingleOrDefaultAsync(t => t.TimelineId == timelineId); + + if (entity == null) return false; + + await using var transaction = await _database.Database.BeginTransactionAsync(); + + var order = entity.Order; + + _database.HighlightTimelines.Remove(entity); + await _database.SaveChangesAsync(); + + await _database.Database.ExecuteSqlRawAsync("UPDATE highlight_timelines SET `order` = `order` - 1 WHERE `order` > {0}", order); + + await transaction.CommitAsync(); + + return true; + } + + public async Task MoveHighlightTimeline(long timelineId, long newPosition) + { + if (!await _timelineService.CheckExistence(timelineId)) + throw new TimelineNotExistException(timelineId); + + var entity = await _database.HighlightTimelines.SingleOrDefaultAsync(t => t.TimelineId == timelineId); + + if (entity == null) throw new InvalidHighlightTimelineException("You can't move a non-highlight timeline."); + + var oldPosition = entity.Order; + + if (newPosition < 1) + { + newPosition = 1; + } + else + { + var totalCount = await _database.HighlightTimelines.CountAsync(); + if (newPosition > totalCount) newPosition = totalCount; + } + + if (oldPosition == newPosition) return; + + await using var transaction = await _database.Database.BeginTransactionAsync(); + + if (newPosition > oldPosition) + { + await _database.Database.ExecuteSqlRawAsync("UPDATE highlight_timelines SET `order` = `order` - 1 WHERE `order` BETWEEN {0} AND {1}", oldPosition + 1, newPosition); + await _database.Database.ExecuteSqlRawAsync("UPDATE highlight_timelines SET `order` = {0} WHERE id = {1}", newPosition, entity.Id); + } + else + { + await _database.Database.ExecuteSqlRawAsync("UPDATE highlight_timelines SET `order` = `order` + 1 WHERE `order` BETWEEN {0} AND {1}", newPosition, oldPosition - 1); + await _database.Database.ExecuteSqlRawAsync("UPDATE highlight_timelines SET `order` = {0} WHERE id = {1}", newPosition, entity.Id); + } + + await transaction.CommitAsync(); + } + + public async Task IsHighlightTimeline(long timelineId, bool checkTimelineExistence = true) + { + if (checkTimelineExistence && !await _timelineService.CheckExistence(timelineId)) + throw new TimelineNotExistException(timelineId); + + return await _database.HighlightTimelines.AnyAsync(t => t.TimelineId == timelineId); + } + } +} diff --git a/BackEnd/Timeline/Services/Api/SearchService.cs b/BackEnd/Timeline/Services/Api/SearchService.cs new file mode 100644 index 00000000..eec5001f --- /dev/null +++ b/BackEnd/Timeline/Services/Api/SearchService.cs @@ -0,0 +1,104 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; + +namespace Timeline.Services.Api +{ + public class SearchResultItem + { + public SearchResultItem(TItem item, int score) + { + Item = item; + Score = score; + } + + public TItem Item { get; set; } = default!; + + /// + /// Bigger is better. + /// + public int Score { get; set; } + } + + public class SearchResult + { +#pragma warning disable CA2227 // Collection properties should be read only + public List> Items { get; set; } = new(); +#pragma warning restore CA2227 // Collection properties should be read only + } + + public interface ISearchService + { + /// + /// Search timelines whose name or title contains query string. + /// + /// String to contain. + /// Search results. + /// Thrown when is null. + /// Thrown when is empty. + /// + /// Implementation should promise high score is at first. + /// + Task> SearchTimeline(string query); + + /// + /// Search users whose username or nickname contains query string. + /// + /// String to contain. + /// Search results. + /// Thrown when is null. + /// Thrown when is empty. + /// + /// Implementation should promise high score is at first. + /// + Task> SearchUser(string query); + } + + public class SearchService : ISearchService + { + private readonly DatabaseContext _database; + + public SearchService(DatabaseContext database) + { + _database = database; + } + + public async Task> SearchTimeline(string query) + { + if (query is null) + throw new ArgumentNullException(nameof(query)); + if (query.Length == 0) + throw new ArgumentException("Query string can't be empty.", nameof(query)); + + var nameLikeTimelines = await _database.Timelines.Include(t => t.Owner).Where(t => t.Name == null ? t.Owner.Username.Contains(query) : t.Name.Contains(query)).ToListAsync(); + var titleLikeTimelines = await _database.Timelines.Where(t => t.Title != null && t.Title.Contains(query)).ToListAsync(); + + var searchResult = new SearchResult(); + searchResult.Items.AddRange(nameLikeTimelines.Select(t => new SearchResultItem(t, 2))); + searchResult.Items.AddRange(titleLikeTimelines.Select(t => new SearchResultItem(t, 1))); + + return searchResult; + } + + public async Task> SearchUser(string query) + { + if (query is null) + throw new ArgumentNullException(nameof(query)); + if (query.Length == 0) + throw new ArgumentException("Query string can't be empty.", nameof(query)); + + var usernameLikeUsers = await _database.Users.Where(u => u.Username.Contains(query)).ToListAsync(); + var nicknameLikeUsers = await _database.Users.Where(u => u.Nickname != null && u.Nickname.Contains(query)).ToListAsync(); + + var searchResult = new SearchResult(); + searchResult.Items.AddRange(usernameLikeUsers.Select(u => new SearchResultItem(u, 2))); + searchResult.Items.AddRange(nicknameLikeUsers.Select(u => new SearchResultItem(u, 1))); + + return searchResult; + + } + } +} -- cgit v1.2.3