diff options
author | crupest <crupest@outlook.com> | 2021-04-25 21:20:04 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2021-04-25 21:20:04 +0800 |
commit | a4a75188bd17e31b39a02511bbd6d628bab5c909 (patch) | |
tree | f2f9c63eb5beabb7a1a2f2605c2d5022f6a72c08 /BackEnd/Timeline/Services/Api | |
parent | 434be212c77bdade04722046e92c3dac25d0aff3 (diff) | |
download | timeline-a4a75188bd17e31b39a02511bbd6d628bab5c909.tar.gz timeline-a4a75188bd17e31b39a02511bbd6d628bab5c909.tar.bz2 timeline-a4a75188bd17e31b39a02511bbd6d628bab5c909.zip |
...
Diffstat (limited to 'BackEnd/Timeline/Services/Api')
-rw-r--r-- | BackEnd/Timeline/Services/Api/BookmarkTimelineService.cs | 205 | ||||
-rw-r--r-- | BackEnd/Timeline/Services/Api/HighlightTimelineService.cs | 194 | ||||
-rw-r--r-- | BackEnd/Timeline/Services/Api/SearchService.cs | 104 |
3 files changed, 503 insertions, 0 deletions
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) { }
+ }
+
+ /// <summary>
+ /// Service interface that manages timeline bookmarks.
+ /// </summary>
+ public interface IBookmarkTimelineService
+ {
+ /// <summary>
+ /// Get bookmarks of a user.
+ /// </summary>
+ /// <param name="userId">User id of bookmark owner.</param>
+ /// <returns>Id of Bookmark timelines in order.</returns>
+ /// <exception cref="UserNotExistException">Thrown when user does not exist.</exception>
+ Task<List<long>> GetBookmarks(long userId);
+
+ /// <summary>
+ /// Check if a timeline is a bookmark.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="timelineId">Timeline id.</param>
+ /// <param name="checkUserExistence">If true it will throw when user does not exist.</param>
+ /// <param name="checkTimelineExistence">If true it will throw when timeline does not exist.</param>
+ /// <returns>True if timeline is a bookmark. Otherwise false.</returns>
+ /// <exception cref="UserNotExistException">Throw if user does not exist and <paramref name="checkUserExistence"/> is true.</exception>
+ /// <exception cref="TimelineNotExistException">Thrown if timeline does not exist and <paramref name="checkTimelineExistence"/> is true.</exception>
+ Task<bool> IsBookmark(long userId, long timelineId, bool checkUserExistence = true, bool checkTimelineExistence = true);
+
+ /// <summary>
+ /// Add a bookmark to tail to a user.
+ /// </summary>
+ /// <param name="userId">User id of bookmark owner.</param>
+ /// <param name="timelineId">Timeline id.</param>
+ /// <returns>True if timeline is added to bookmark. False if it already is.</returns>
+ /// <exception cref="UserNotExistException">Thrown when user does not exist.</exception>
+ /// <exception cref="TimelineNotExistException">Thrown when timeline does not exist.</exception>
+ Task<bool> AddBookmark(long userId, long timelineId);
+
+ /// <summary>
+ /// Remove a bookmark from a user.
+ /// </summary>
+ /// <param name="userId">User id of bookmark owner.</param>
+ /// <param name="timelineId">Timeline id.</param>
+ /// <returns>True if deletion is performed. False if bookmark does not exist.</returns>
+ /// <exception cref="UserNotExistException">Thrown when user does not exist.</exception>
+ /// <exception cref="TimelineNotExistException">Thrown when timeline does not exist.</exception>
+ Task<bool> RemoveBookmark(long userId, long timelineId);
+
+ /// <summary>
+ /// Move bookmark to a new position.
+ /// </summary>
+ /// <param name="userId">User id of bookmark owner.</param>
+ /// <param name="timelineId">Timeline name.</param>
+ /// <param name="newPosition">New position. Starts at 1.</param>
+ /// <exception cref="UserNotExistException">Thrown when user does not exist.</exception>
+ /// <exception cref="TimelineNotExistException">Thrown when timeline does not exist.</exception>
+ /// <exception cref="InvalidBookmarkException">Thrown when the timeline is not a bookmark.</exception>
+ 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<bool> 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<List<long>> 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<bool> 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<bool> 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) { }
+ }
+
+ /// <summary>
+ /// Service that controls highlight timeline.
+ /// </summary>
+ public interface IHighlightTimelineService
+ {
+ /// <summary>
+ /// Get all highlight timelines in order.
+ /// </summary>
+ /// <returns>Id list of all highlight timelines.</returns>
+ Task<List<long>> GetHighlightTimelines();
+
+ /// <summary>
+ /// Check if a timeline is highlight timeline.
+ /// </summary>
+ /// <param name="timelineId">Timeline id.</param>
+ /// <param name="checkTimelineExistence">If true it will throw if timeline does not exist.</param>
+ /// <returns>True if timeline is highlight. Otherwise false.</returns>
+ /// <exception cref="TimelineNotExistException">Thrown when timeline does not exist and <paramref name="checkTimelineExistence"/> is true.</exception>
+ Task<bool> IsHighlightTimeline(long timelineId, bool checkTimelineExistence = true);
+
+ /// <summary>
+ /// Add a timeline to highlight list.
+ /// </summary>
+ /// <param name="timelineId">The timeline id.</param>
+ /// <param name="operatorId">The user id of operator.</param>
+ /// <returns>True if timeline is actually added to highligh. False if it already is.</returns>
+ /// <exception cref="TimelineNotExistException">Thrown when timeline with given id does not exist.</exception>
+ /// <exception cref="UserNotExistException">Thrown when user with given operator id does not exist.</exception>
+ Task<bool> AddHighlightTimeline(long timelineId, long? operatorId);
+
+ /// <summary>
+ /// Remove a timeline from highlight list.
+ /// </summary>
+ /// <param name="timelineId">The timeline id.</param>
+ /// <param name="operatorId">The user id of operator.</param>
+ /// <returns>True if deletion is actually performed. Otherwise false (timeline was not in the list).</returns>
+ /// <exception cref="TimelineNotExistException">Thrown when timeline with given id does not exist.</exception>
+ /// <exception cref="UserNotExistException">Thrown when user with given operator id does not exist.</exception>
+ Task<bool> RemoveHighlightTimeline(long timelineId, long? operatorId);
+
+ /// <summary>
+ /// Move a highlight timeline to a new position.
+ /// </summary>
+ /// <param name="timelineId">The timeline name.</param>
+ /// <param name="newPosition">The new position. Starts at 1.</param>
+ /// <exception cref="TimelineNotExistException">Thrown when timeline with given id does not exist.</exception>
+ /// <exception cref="InvalidHighlightTimelineException">Thrown when given timeline is not a highlight timeline.</exception>
+ /// <remarks>
+ /// If <paramref name="newPosition"/> is smaller than 1. Then move the timeline to head.
+ /// If <paramref name="newPosition"/> is bigger than total count. Then move the timeline to tail.
+ /// </remarks>
+ 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<bool> 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<List<long>> 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<bool> 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<bool> 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<TItem>
+ {
+ public SearchResultItem(TItem item, int score)
+ {
+ Item = item;
+ Score = score;
+ }
+
+ public TItem Item { get; set; } = default!;
+
+ /// <summary>
+ /// Bigger is better.
+ /// </summary>
+ public int Score { get; set; }
+ }
+
+ public class SearchResult<TItem>
+ {
+#pragma warning disable CA2227 // Collection properties should be read only
+ public List<SearchResultItem<TItem>> Items { get; set; } = new();
+#pragma warning restore CA2227 // Collection properties should be read only
+ }
+
+ public interface ISearchService
+ {
+ /// <summary>
+ /// Search timelines whose name or title contains query string.
+ /// </summary>
+ /// <param name="query">String to contain.</param>
+ /// <returns>Search results.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="query"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="query"/> is empty.</exception>
+ /// <remarks>
+ /// Implementation should promise high score is at first.
+ /// </remarks>
+ Task<SearchResult<TimelineEntity>> SearchTimeline(string query);
+
+ /// <summary>
+ /// Search users whose username or nickname contains query string.
+ /// </summary>
+ /// <param name="query">String to contain.</param>
+ /// <returns>Search results.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="query"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="query"/> is empty.</exception>
+ /// <remarks>
+ /// Implementation should promise high score is at first.
+ /// </remarks>
+ Task<SearchResult<UserEntity>> SearchUser(string query);
+ }
+
+ public class SearchService : ISearchService
+ {
+ private readonly DatabaseContext _database;
+
+ public SearchService(DatabaseContext database)
+ {
+ _database = database;
+ }
+
+ public async Task<SearchResult<TimelineEntity>> 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<TimelineEntity>();
+ searchResult.Items.AddRange(nameLikeTimelines.Select(t => new SearchResultItem<TimelineEntity>(t, 2)));
+ searchResult.Items.AddRange(titleLikeTimelines.Select(t => new SearchResultItem<TimelineEntity>(t, 1)));
+
+ return searchResult;
+ }
+
+ public async Task<SearchResult<UserEntity>> 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<UserEntity>();
+ searchResult.Items.AddRange(usernameLikeUsers.Select(u => new SearchResultItem<UserEntity>(u, 2)));
+ searchResult.Items.AddRange(nicknameLikeUsers.Select(u => new SearchResultItem<UserEntity>(u, 1)));
+
+ return searchResult;
+
+ }
+ }
+}
|