/// <summary>
        /// 获取指定目标获取的认可数
        /// </summary>
        /// <param name="targetId">目标 ID</param>
        /// <param name="targetType">目标类型</param>
        /// <exception cref="ArgumentNullException"><paramref name="targetId"/> 为 null</exception>
        /// <returns>指定目标获取的认可数</returns>
        public async Task <int> GetTargetLikeCountAsync([NotNull] string targetId, LikeTargetType targetType)
        {
            if (targetId == null)
            {
                throw new ArgumentNullException(nameof(targetId));
            }

            var cacheKey     = TargetLikeCountCacheKey(targetId, targetType);
            var redisDb      = _redis.GetDatabase();
            var cachedResult = await redisDb.StringGetAsync(cacheKey);

            if (cachedResult.HasValue)
            {
                await redisDb.KeyExpireAsync(cacheKey, CachedDataProvider.DefaultTtl);

                return((int)cachedResult);
            }

            var likeCount =
                await _dbContext.Likes.CountAsync(l => l.TargetId == targetId && l.TargetType == targetType);

            await redisDb.StringSetAsync(cacheKey, likeCount, CachedDataProvider.DefaultTtl);

            return(likeCount);
        }
        private async Task IncreaseTargetLikeCountAsync([NotNull] string targetId, LikeTargetType targetType, long value)
        {
            var cacheKey = TargetLikeCountCacheKey(targetId, targetType);
            var redisDb  = _redis.GetDatabase();

            if (await redisDb.KeyExistsAsync(cacheKey))
            {
                if (value >= 0)
                {
                    await redisDb.StringIncrementAsync(cacheKey, value);
                }
                else
                {
                    await redisDb.StringDecrementAsync(cacheKey, -value);
                }
            }
        }
        /// <summary>
        /// 判断指定用户是否认可过指定目标
        /// </summary>
        /// <param name="userId">用户 ID</param>
        /// <param name="targetId">目标 ID</param>
        /// <param name="targetType">目标类型</param>
        /// <returns>如果用户认可过,返回 <c>true</c></returns>
        public async Task <bool> IsLikedAsync(string userId, string targetId, LikeTargetType targetType)
        {
            if (userId == null || targetId == null)
            {
                return(false);
            }

            var cacheKey = UserLikedTargetsCacheKey(userId);
            var redisDb  = _redis.GetDatabase();

            if (!await redisDb.KeyExistsAsync(cacheKey))
            {
                foreach (var like in await _dbContext.Likes.Where(l => l.OperatorId == userId)
                         .Select(l => new { l.TargetId, l.TargetType }).ToListAsync())
                {
                    await redisDb.SetAddAsync(cacheKey, UserLikedTargetCacheValue(like.TargetId, like.TargetType));
                }
            }
            await redisDb.KeyExpireAsync(cacheKey, CachedDataProvider.DefaultTtl);

            return(await redisDb.SetContainsAsync(cacheKey, UserLikedTargetCacheValue(targetId, targetType)));
        }
 private static string UserLikedTargetCacheValue(string targetId, LikeTargetType targetType) =>
 $"{targetType.ToString().ToCase(NameConventionCase.PascalCase, NameConventionCase.DashedCase)}:{targetId}";
 private static string TargetLikeCountCacheKey(string targetId, LikeTargetType targetType) =>
 $"target-like-count:{targetType.ToString().ToCase(NameConventionCase.PascalCase, NameConventionCase.DashedCase)}:{targetId}";
        /// <summary>
        /// 撤销一个操作者发出的认可
        /// </summary>
        /// <param name="operatorId">操作者 ID</param>
        /// <param name="targetId">目标 ID</param>
        /// <param name="targetType">目标类型</param>
        /// <exception cref="ArgumentNullException">有参数为 null</exception>
        public async Task RemoveAsync([NotNull] string operatorId, [NotNull] string targetId,
                                      LikeTargetType targetType)
        {
            if (operatorId == null)
            {
                throw new ArgumentNullException(nameof(operatorId));
            }
            if (targetId == null)
            {
                throw new ArgumentNullException(nameof(targetId));
            }

            if (!await IsLikedAsync(operatorId, targetId, targetType))
            {
                return;
            }

            string likeReceiverId;

            switch (targetType)
            {
            case LikeTargetType.Article:
                likeReceiverId = await _dbContext.Articles.Where(a => a.Id == targetId)
                                 .Select(a => a.AuthorId).SingleAsync();

                break;

            case LikeTargetType.ArticleComment:
                likeReceiverId = await _dbContext.ArticleComments.Where(c => c.Id == targetId)
                                 .Select(c => c.CommentatorId).SingleAsync();

                break;

            case LikeTargetType.Activity:
                likeReceiverId = await _dbContext.Activities.Where(a => a.Id == targetId)
                                 .Select(a => a.AuthorId).SingleAsync();

                break;

            case LikeTargetType.ActivityComment:
                likeReceiverId = await _dbContext.ActivityComments.Where(c => c.Id == targetId)
                                 .Select(c => c.CommentatorId).SingleAsync();

                break;

            case LikeTargetType.ConferenceEntry:
                likeReceiverId = await _dbContext.ConferenceEntries.Where(e => e.Id == targetId)
                                 .Select(e => e.AuthorId).SingleAsync();

                break;

            default:
                throw new ArgumentOutOfRangeException(nameof(targetType), targetType, null);
            }

            var likes = await _dbContext.Likes
                        .Where(l => l.OperatorId == operatorId && l.TargetId == targetId && l.TargetType == targetType)
                        .ToListAsync();

            _dbContext.Likes.RemoveRange(likes);
            await _dbContext.SaveChangesAsync();

            var redisDb  = _redis.GetDatabase();
            var cacheKey = UserLikedTargetsCacheKey(operatorId);

            foreach (var like in likes)
            {
                await redisDb.SetRemoveAsync(cacheKey, UserLikedTargetCacheValue(like.TargetId, like.TargetType));
            }
            await IncreaseUserLikeCountAsync(likeReceiverId, -likes.Count);
            await IncreaseTargetLikeCountAsync(targetId, targetType, -likes.Count);

            if (targetType == LikeTargetType.Article)
            {
                var receiver = await _dbContext.Users.Where(u => u.Id == likeReceiverId).SingleAsync();

                bool saveFailed;
                do
                {
                    try
                    {
                        saveFailed = false;
                        receiver.SeasonLikeCount--;
                        await _dbContext.SaveChangesAsync();
                    }
                    catch (DbUpdateConcurrencyException e)
                    {
                        saveFailed = true;
                        await e.Entries.Single().ReloadAsync();
                    }
                } while (saveFailed);
            }
        }
        /// <summary>
        /// 添加一个新认可
        /// </summary>
        /// <param name="operatorId">认可操作者 ID</param>
        /// <param name="targetId">目标 ID</param>
        /// <param name="targetType">目标类型</param>
        /// <returns>认可成功返回认可 ID,失败返回 <c>null</c></returns>
        /// <exception cref="ArgumentNullException">有参数为 null</exception>
        public async Task <string> AddAsync([NotNull] string operatorId, [NotNull] string targetId,
                                            LikeTargetType targetType)
        {
            if (operatorId == null)
            {
                throw new ArgumentNullException(nameof(operatorId));
            }
            if (targetId == null)
            {
                throw new ArgumentNullException(nameof(targetId));
            }

            if (await IsLikedAsync(operatorId, targetId, targetType))
            {
                return(null);
            }

            string likeReceiverId;

            switch (targetType)
            {
            case LikeTargetType.Article:
                likeReceiverId = await _dbContext.Articles.Where(a => a.Id == targetId)
                                 .Select(a => a.AuthorId).SingleAsync();

                break;

            case LikeTargetType.ArticleComment:
                likeReceiverId = await _dbContext.ArticleComments.Where(c => c.Id == targetId)
                                 .Select(c => c.CommentatorId).SingleAsync();

                break;

            case LikeTargetType.Activity:
                likeReceiverId = await _dbContext.Activities.Where(a => a.Id == targetId)
                                 .Select(a => a.AuthorId).SingleAsync();

                break;

            case LikeTargetType.ActivityComment:
                likeReceiverId = await _dbContext.ActivityComments.Where(c => c.Id == targetId)
                                 .Select(c => c.CommentatorId).SingleAsync();

                break;

            case LikeTargetType.ConferenceEntry:
                likeReceiverId = await _dbContext.ConferenceEntries.Where(e => e.Id == targetId)
                                 .Select(e => e.AuthorId).SingleAsync();

                break;

            default:
                throw new ArgumentOutOfRangeException(nameof(targetType), targetType, null);
            }

            if (operatorId == likeReceiverId)
            {
                return(null);
            }

            var like = new Like
            {
                OperatorId = operatorId,
                TargetId   = targetId,
                TargetType = targetType
            };

            _dbContext.Likes.Add(like);
            await _dbContext.SaveChangesAsync();

            var redisDb = _redis.GetDatabase();
            await redisDb.SetAddAsync(UserLikedTargetsCacheKey(operatorId),
                                      UserLikedTargetCacheValue(targetId, targetType));

            await IncreaseUserLikeCountAsync(likeReceiverId, 1);
            await IncreaseTargetLikeCountAsync(targetId, targetType, 1);

            if (targetType == LikeTargetType.Article)
            {
                var receiver = await _dbContext.Users.Where(u => u.Id == likeReceiverId).SingleAsync();

                bool saveFailed;
                do
                {
                    try
                    {
                        saveFailed = false;
                        receiver.SeasonLikeCount++;
                        await _dbContext.SaveChangesAsync();
                    }
                    catch (DbUpdateConcurrencyException e)
                    {
                        saveFailed = true;
                        await e.Entries.Single().ReloadAsync();
                    }
                } while (saveFailed);
            }

            return(like.Id);
        }
        public async Task <IHttpActionResult> CreateOne(string targetId, LikeTargetType targetType)
        {
            var operatorId = User.Identity.GetUserId();
            var @operator  = await _userManager.FindByIdAsync(operatorId);

            if (@operator.FreeLike <= 0 && !await _coupon.CanTriggerEventAsync(operatorId, CouponEvent.发出认可))
            {
                return(Unauthorized());
            }

            var likeId = await _cachedData.Likes.AddAsync(operatorId, targetId, targetType);

            var free = string.Empty;

            if (likeId != null)
            {
                if (targetType == LikeTargetType.Article || targetType == LikeTargetType.Activity)
                {
                    _mqChannel.SendMessage(string.Empty, MqClientProvider.PushHubRequestQueue, new PushHubRequestDto
                    {
                        Type      = ContentPushType.Like,
                        ContentId = likeId
                    });
                }

                KeylolUser  targetUser;
                bool        notify, steamNotify;
                MessageType messageType;
                string      steamNotifyText;
                object      couponDescriptionForTargetUser, couponDescriptionForOperator;
                switch (targetType)
                {
                case LikeTargetType.Article:
                    messageType = MessageType.ArticleLike;
                    var article = await _dbContext.Articles.Where(a => a.Id == targetId)
                                  .Select(a => new
                    {
                        a.Id,
                        a.Title,
                        a.Author,
                        a.SidForAuthor
                    }).SingleAsync();

                    targetUser      = article.Author;
                    notify          = targetUser.NotifyOnArticleLiked;
                    steamNotify     = targetUser.SteamNotifyOnArticleLiked;
                    steamNotifyText =
                        $"{@operator.UserName} 认可了你的文章《{article.Title}》:\nhttps://www.keylol.com/article/{targetUser.IdCode}/{article.SidForAuthor}";
                    couponDescriptionForTargetUser = new
                    {
                        ArticleId  = article.Id,
                        OperatorId = operatorId
                    };
                    couponDescriptionForOperator = new { ArticleId = article.Id };
                    break;

                case LikeTargetType.ArticleComment:
                    messageType = MessageType.ArticleCommentLike;
                    var articleComment = await _dbContext.ArticleComments.Where(c => c.Id == targetId)
                                         .Select(c => new
                    {
                        c.Id,
                        c.Commentator,
                        c.SidForArticle,
                        ArticleTitle        = c.Article.Title,
                        ArticleSidForAuthor = c.Article.SidForAuthor,
                        ArticleAuthorIdCode = c.Article.Author.IdCode
                    }).SingleAsync();

                    targetUser      = articleComment.Commentator;
                    notify          = targetUser.NotifyOnCommentLiked;
                    steamNotify     = targetUser.SteamNotifyOnCommentLiked;
                    steamNotifyText =
                        $"{@operator.UserName} 认可了你在《{articleComment.ArticleTitle}》下的评论:\nhttps://www.keylol.com/article/{articleComment.ArticleAuthorIdCode}/{articleComment.ArticleSidForAuthor}#{articleComment.SidForArticle}";
                    couponDescriptionForTargetUser = new
                    {
                        ArticleCommentId = articleComment.Id,
                        OperatorId       = operatorId
                    };
                    couponDescriptionForOperator = new { ArticleCommentId = articleComment.Id };
                    break;

                case LikeTargetType.Activity:
                    messageType = MessageType.ActivityLike;
                    var activity = await _dbContext.Activities.Include(a => a.Author)
                                   .Where(a => a.Id == targetId).SingleAsync();

                    targetUser      = activity.Author;
                    notify          = targetUser.NotifyOnActivityLiked;
                    steamNotify     = targetUser.SteamNotifyOnActivityLiked;
                    steamNotifyText =
                        $"{@operator.UserName} 认可了你的动态「{PostOfficeMessageList.CollapseActivityContent(activity)}」:\nhttps://www.keylol.com/activity/{targetUser.IdCode}/{activity.SidForAuthor}";
                    couponDescriptionForTargetUser = new
                    {
                        ActivityId = activity.Id,
                        OperatorId = operatorId
                    };
                    couponDescriptionForOperator = new { ActivityId = activity.Id };
                    break;

                case LikeTargetType.ActivityComment:
                    messageType = MessageType.ActivityCommentLike;
                    var activityComment = await _dbContext.ActivityComments.Where(c => c.Id == targetId)
                                          .Select(c => new
                    {
                        c.Id,
                        c.Commentator,
                        c.SidForActivity,
                        c.Activity,
                        ActivityAuthorIdCode = c.Activity.Author.IdCode
                    }).SingleAsync();

                    targetUser      = activityComment.Commentator;
                    notify          = targetUser.NotifyOnCommentLiked;
                    steamNotify     = targetUser.SteamNotifyOnCommentLiked;
                    steamNotifyText =
                        $"{@operator.UserName} 认可了你在「{PostOfficeMessageList.CollapseActivityContent(activityComment.Activity)}」下的评论:\nhttps://www.keylol.com/activity/{activityComment.ActivityAuthorIdCode}/{activityComment.Activity.SidForAuthor}#{activityComment.SidForActivity}";
                    couponDescriptionForTargetUser = new
                    {
                        ActivityCommentId = activityComment.Id,
                        OperatorId        = operatorId
                    };
                    couponDescriptionForOperator = new { ActivityCommentId = activityComment.Id };
                    break;

                default:
                    throw new ArgumentOutOfRangeException(nameof(targetType), targetType, null);
                }
                if (@operator.FreeLike > 0)
                {
                    @operator.FreeLike--;
                    free = "Free";
                }
                else
                {
                    await _coupon.UpdateAsync(@operator, CouponEvent.发出认可, couponDescriptionForOperator);
                }
                await _coupon.UpdateAsync(targetUser, CouponEvent.获得认可, couponDescriptionForTargetUser);

                if (notify)
                {
                    var message = new Message
                    {
                        Type        = messageType,
                        OperatorId  = operatorId,
                        ReceiverId  = targetUser.Id,
                        Count       = await _cachedData.Likes.GetUserLikeCountAsync(targetUser.Id),
                        SecondCount = await _cachedData.Likes.GetTargetLikeCountAsync(targetId, targetType)
                    };
                    switch (targetType)
                    {
                    case LikeTargetType.Article:
                        message.ArticleId = targetId;
                        break;

                    case LikeTargetType.ArticleComment:
                        message.ArticleCommentId = targetId;
                        break;

                    case LikeTargetType.Activity:
                        message.ActivityId = targetId;
                        break;

                    case LikeTargetType.ActivityComment:
                        message.ActivityCommentId = targetId;
                        break;

                    default:
                        throw new ArgumentOutOfRangeException(nameof(targetType), targetType, null);
                    }
                    await _cachedData.Messages.AddAsync(message);
                }
                if (steamNotify)
                {
                    await _userManager.SendSteamChatMessageAsync(targetUser, steamNotifyText);
                }
            }
            return(Ok(free));
        }