Skip to content

Commit

Permalink
refactor: extract RatingService
Browse files Browse the repository at this point in the history
  • Loading branch information
Handiwork committed Apr 13, 2024
1 parent 6921a84 commit fdcaa25
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 144 deletions.
28 changes: 4 additions & 24 deletions src/main/kotlin/plus/maa/backend/service/CommentsAreaService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,10 @@ import plus.maa.backend.controller.response.comments.CommentsInfo
import plus.maa.backend.controller.response.comments.SubCommentsInfo
import plus.maa.backend.repository.CommentsAreaRepository
import plus.maa.backend.repository.CopilotRepository
import plus.maa.backend.repository.RatingRepository
import plus.maa.backend.repository.UserRepository
import plus.maa.backend.repository.entity.CommentsArea
import plus.maa.backend.repository.entity.Copilot
import plus.maa.backend.repository.entity.MaaUser
import plus.maa.backend.repository.entity.Rating
import plus.maa.backend.service.model.RatingType
import java.time.LocalDateTime

Expand All @@ -31,7 +29,7 @@ import java.time.LocalDateTime
@Service
class CommentsAreaService(
private val commentsAreaRepository: CommentsAreaRepository,
private val ratingRepository: RatingRepository,
private val ratingService: RatingService,
private val copilotRepository: CopilotRepository,
private val userRepository: UserRepository,
private val emailService: EmailService,
Expand Down Expand Up @@ -107,31 +105,13 @@ class CommentsAreaService(
val commentId = commentsRatingDTO.commentId
val commentsArea = requireCommentsAreaById(commentId)

val rating = ratingRepository.findByTypeAndKeyAndUserId(
Rating.KeyType.COMMENT,
val ratingChange = ratingService.rateComment(
commentId,
userId,
) ?: Rating(
null,
Rating.KeyType.COMMENT,
commentId,
userId,
RatingType.NONE,
LocalDateTime.now(),
RatingType.fromRatingType(commentsRatingDTO.rating),
)

val prevType = rating.rating
val nextType = RatingType.fromRatingType(commentsRatingDTO.rating)
// 如果评分未发生变化则返回
if (nextType == prevType) return

rating.rating = nextType
rating.rateTime = LocalDateTime.now()
ratingRepository.save(rating)

// 更新评分后更新评论的点赞数
val likeCountChange = nextType.countLike() - prevType.countLike()
val dislikeCountChange = nextType.countDislike() - prevType.countDislike()
val (likeCountChange, dislikeCountChange) = ratingService.calcLikeChange(ratingChange)

// 点赞数不需要在高并发下特别精准,大概就行,但是也得避免特别离谱的数字
commentsArea.likeCount = (commentsArea.likeCount + likeCountChange).coerceAtLeast(0)
Expand Down
173 changes: 53 additions & 120 deletions src/main/kotlin/plus/maa/backend/service/CopilotService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package plus.maa.backend.service

import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.ObjectMapper
import com.google.common.collect.Sets
import io.github.oshai.kotlinlogging.KotlinLogging
import org.apache.commons.lang3.StringUtils
import org.springframework.data.domain.PageRequest
Expand All @@ -28,7 +27,6 @@ import plus.maa.backend.controller.response.copilot.CopilotInfo
import plus.maa.backend.controller.response.copilot.CopilotPageInfo
import plus.maa.backend.repository.CommentsAreaRepository
import plus.maa.backend.repository.CopilotRepository
import plus.maa.backend.repository.RatingRepository
import plus.maa.backend.repository.RedisCache
import plus.maa.backend.repository.UserRepository
import plus.maa.backend.repository.entity.Copilot
Expand All @@ -37,11 +35,9 @@ import plus.maa.backend.repository.entity.Rating
import plus.maa.backend.repository.findByUsersId
import plus.maa.backend.service.model.RatingCache
import plus.maa.backend.service.model.RatingType
import java.math.BigDecimal
import java.math.RoundingMode
import java.time.LocalDateTime
import java.time.temporal.ChronoUnit
import java.util.Objects
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.atomic.AtomicReference
Expand All @@ -60,7 +56,7 @@ private val log = KotlinLogging.logger { }
@Service
class CopilotService(
private val copilotRepository: CopilotRepository,
private val ratingRepository: RatingRepository,
private val ratingService: RatingService,
private val mongoTemplate: MongoTemplate,
private val mapper: ObjectMapper,
private val levelService: ArkLevelService,
Expand Down Expand Up @@ -171,48 +167,29 @@ class CopilotService(
* 指定查询
*/
fun getCopilotById(userIdOrIpAddress: String, id: Long): CopilotInfo? {
// 根据ID获取作业, 如作业不存在则抛出异常返回
val copilotOptional = copilotRepository.findByCopilotIdAndDeleteIsFalse(id)
return copilotOptional?.let { copilot: Copilot ->
// 60分钟内限制同一个用户对访问量的增加
val cache = redisCache.getCache("views:$userIdOrIpAddress", RatingCache::class.java)
if (Objects.isNull(cache) || Objects.isNull(cache!!.copilotIds) || !cache.copilotIds.contains(id)) {
val query = Query.query(Criteria.where("copilotId").`is`(id))
val update = Update()
// 增加一次views
update.inc("views")
mongoTemplate.updateFirst(query, update, Copilot::class.java)
if (cache == null) {
redisCache.setCache("views:$userIdOrIpAddress", RatingCache(Sets.newHashSet(id)))
} else {
redisCache.updateCache(
"views:$userIdOrIpAddress",
RatingCache::class.java,
cache,
{ updateCache: RatingCache? ->
updateCache!!.copilotIds.add(id)
updateCache
},
60,
TimeUnit.MINUTES,
)
}
}
val maaUser = userRepository.findByUserId(copilot.uploaderId!!)

// 新评分系统
val ratingType = ratingRepository.findByTypeAndKeyAndUserId(
Rating.KeyType.COPILOT,
copilot.copilotId.toString(),
userIdOrIpAddress,
)?.rating
formatCopilot(
copilot,
ratingType,
maaUser!!.userName,
commentsAreaRepository.countByCopilotIdAndDelete(copilot.copilotId!!, false),
)
val copilot = copilotRepository.findByCopilotIdAndDeleteIsFalse(id) ?: return null

// 60分钟内限制同一个用户对访问量的增加
val viewCacheKey = "views:$userIdOrIpAddress"
val viewCache = redisCache.getCache(viewCacheKey, RatingCache::class.java)
if (viewCache?.copilotIds?.contains(id) != true) {
val query = Query.query(Criteria.where("copilotId").`is`(id))
val update = Update()
update.inc("views")
mongoTemplate.updateFirst(query, update, Copilot::class.java)
// 实际上读写并没有锁,因此是否调用 update 意义都不大
val vc = viewCache ?: RatingCache(mutableSetOf())
vc.copilotIds.add(id)
redisCache.setCache(viewCacheKey, vc, 60, TimeUnit.MINUTES)
}

val maaUser = userRepository.findByUserId(copilot.uploaderId!!)
return formatCopilot(
copilot,
ratingService.findPersonalRatingOfCopilot(userIdOrIpAddress, id),
maaUser!!.userName,
commentsAreaRepository.countByCopilotIdAndDelete(copilot.copilotId!!, false),
)
}

/**
Expand All @@ -231,7 +208,8 @@ class CopilotService(
if (request.page <= 3 &&
request.document == null &&
request.levelKeyword == null &&
request.uploaderId == null && request.operator == null &&
request.uploaderId == null &&
request.operator == null &&
request.copilotIds.isNullOrEmpty()
) {
request.orderBy?.takeIf { orderBy -> orderBy.isNotBlank() }
Expand Down Expand Up @@ -341,9 +319,7 @@ class CopilotService(
val copilots = mongoTemplate.find(queryObj.with(pageable), Copilot::class.java)

// 填充前端所需信息
val copilotIds = copilots.map {
it.copilotId!!
}.toSet()
val copilotIds = copilots.map { it.copilotId!! }.toSet()
val maaUsers = userRepository.findByUsersId(copilots.map { it.uploaderId!! }.toList())
val commentsCount = commentsAreaRepository.findByCopilotIdInAndDelete(copilotIds, false).groupBy {
it.copilotId
Expand All @@ -356,7 +332,7 @@ class CopilotService(
copilot,
null,
maaUsers[copilot.uploaderId]!!.userName,
commentsCount[copilot.copilotId],
commentsCount[copilot.copilotId] ?: 0,
)
}.toList()

Expand Down Expand Up @@ -403,51 +379,14 @@ class CopilotService(
* @param userIdOrIpAddress 用于已登录用户作出评分
*/
fun rates(userIdOrIpAddress: String, request: CopilotRatingReq) {
val rating = request.rating
requireNotNull(copilotRepository.existsCopilotsByCopilotId(request.id)) { "作业id不存在" }

Assert.isTrue(copilotRepository.existsCopilotsByCopilotId(request.id), "作业id不存在")

var likeCountChange = 0
var dislikeCountChange = 0
val ratingOptional = ratingRepository.findByTypeAndKeyAndUserId(
Rating.KeyType.COPILOT,
request.id.toString(),
val ratingChange = ratingService.rateCopilot(
request.id,
userIdOrIpAddress,
RatingType.fromRatingType(request.rating),
)
// 如果评分存在则更新评分
if (ratingOptional != null) {
// 如果评分相同,则不做任何操作
if (ratingOptional.rating == RatingType.fromRatingType(rating)) {
return
}
// 如果评分不同则更新评分
val oldRatingType = ratingOptional.rating
ratingOptional.rating = RatingType.fromRatingType(rating)
ratingOptional.rateTime = LocalDateTime.now()
ratingRepository.save(ratingOptional)
// 计算评分变化
likeCountChange =
if (ratingOptional.rating == RatingType.LIKE) 1 else (if (oldRatingType != RatingType.LIKE) 0 else -1)
dislikeCountChange =
if (ratingOptional.rating == RatingType.DISLIKE) 1 else (if (oldRatingType != RatingType.DISLIKE) 0 else -1)
}

// 不存在评分 则添加新的评分
if (ratingOptional == null) {
val newRating = Rating(
null,
Rating.KeyType.COPILOT,
request.id.toString(),
userIdOrIpAddress,
RatingType.fromRatingType(rating),
LocalDateTime.now(),
)

ratingRepository.insert(newRating)
// 计算评分变化
likeCountChange = if (newRating.rating == RatingType.LIKE) 1 else 0
dislikeCountChange = if (newRating.rating == RatingType.DISLIKE) 1 else 0
}
val (likeCountChange, dislikeCountChange) = ratingService.calcLikeChange(ratingChange)

// 获取只包含评分的作业
var query = Query.query(
Expand All @@ -456,18 +395,15 @@ class CopilotService(
// 排除 _id,防止误 save 该不完整作业后原有数据丢失
query.fields().include("likeCount", "dislikeCount").exclude("_id")
val copilot = mongoTemplate.findOne(query, Copilot::class.java)
Assert.notNull(copilot, "作业不存在")
checkNotNull(copilot) { "作业不存在" }

// 计算评分相关
var likeCount = copilot!!.likeCount + likeCountChange
likeCount = if (likeCount < 0) 0 else likeCount
var ratingCount = likeCount + copilot.dislikeCount + dislikeCountChange
ratingCount = if (ratingCount < 0) 0 else ratingCount
val likeCount = (copilot.likeCount + likeCountChange).coerceAtLeast(0)
val ratingCount = (likeCount + copilot.dislikeCount + dislikeCountChange).coerceAtLeast(0)

val rawRatingLevel = if (ratingCount != 0L) likeCount.toDouble() / ratingCount else 0.0
val bigDecimal = BigDecimal(rawRatingLevel)
// 只取一位小数点
val ratingLevel = bigDecimal.setScale(1, RoundingMode.HALF_UP).toDouble()
val ratingLevel = rawRatingLevel.toBigDecimal().setScale(1, RoundingMode.HALF_UP).toDouble()
// 更新数据
query = Query.query(
Criteria.where("copilotId").`is`(request.id).and("delete").`is`(false),
Expand All @@ -493,27 +429,24 @@ class CopilotService(
* 将数据库内容转换为前端所需格式 <br></br>
* 新版评分系统
*/
private fun formatCopilot(copilot: Copilot, ratingType: RatingType?, userName: String, commentsCount: Long?): CopilotInfo {
val info = copilotConverter.toCopilotInfo(
copilot,
userName,
copilot.copilotId!!,
commentsCount,
private fun formatCopilot(copilot: Copilot, rating: Rating?, userName: String, commentsCount: Long): CopilotInfo {
return CopilotInfo(
id = copilot.copilotId!!,
uploadTime = copilot.uploadTime!!,
uploaderId = copilot.uploaderId!!,
uploader = userName,
views = copilot.views,
hotScore = copilot.hotScore,
available = true,
ratingLevel = copilot.ratingLevel,
notEnoughRating = copilot.likeCount + copilot.dislikeCount <= properties.copilot.minValueShowNotEnoughRating,
ratingRatio = copilot.ratingRatio,
ratingType = (rating?.rating ?: RatingType.NONE).display,
commentsCount = commentsCount,
content = copilot.content ?: "",
like = copilot.likeCount,
dislike = copilot.dislikeCount,
)

info.ratingRatio = copilot.ratingRatio
info.ratingLevel = copilot.ratingLevel
if (ratingType != null) {
info.ratingType = ratingType.display
}
// 评分数少于一定数量
info.notEnoughRating = copilot.likeCount + copilot.dislikeCount <= properties.copilot.minValueShowNotEnoughRating

info.available = true

// 兼容客户端, 将作业ID替换为数字ID
copilot.id = copilot.copilotId.toString()
return info
}

fun notificationStatus(userId: String?, copilotId: Long, status: Boolean) {
Expand Down
63 changes: 63 additions & 0 deletions src/main/kotlin/plus/maa/backend/service/RatingService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package plus.maa.backend.service

import org.springframework.stereotype.Service
import plus.maa.backend.repository.RatingRepository
import plus.maa.backend.repository.entity.Rating
import plus.maa.backend.service.model.RatingType
import java.time.LocalDateTime

@Service
class RatingService(private val ratingRepository: RatingRepository) {
/**
* Update rating of target object
*
* @param keyType Target key type
* @param key Key
* @param raterId Rater's ID
* @param ratingType Target rating type
* @return A pair, previous one and the target one.
*/
fun rate(keyType: Rating.KeyType, key: String, raterId: String, ratingType: RatingType): Pair<Rating, Rating> {
val rating = ratingRepository.findByTypeAndKeyAndUserId(
keyType,
key,
raterId,
) ?: Rating(
null,
keyType,
key,
raterId,
RatingType.NONE,
LocalDateTime.now(),
)

if (ratingType == rating.rating) return rating to rating

val prev = rating.copy()
rating.rating = ratingType
rating.rateTime = LocalDateTime.now()
ratingRepository.save(rating)
return prev to rating
}

/**
* Calculate like/dislike counts from rating change.
* @param ratingChange Pair of previous rating and current rating
* @return Pair of like count change and dislike count change
*/
fun calcLikeChange(ratingChange: Pair<Rating, Rating>): Pair<Long, Long> {
val (prev, next) = ratingChange
val likeCountChange = next.rating.countLike() - prev.rating.countLike()
val dislikeCountChange = next.rating.countDislike() - prev.rating.countDislike()
return likeCountChange to dislikeCountChange
}

fun rateComment(commentId: String, raterId: String, ratingType: RatingType): Pair<Rating, Rating> =
rate(Rating.KeyType.COMMENT, commentId, raterId, ratingType)

fun rateCopilot(copilotId: Long, raterId: String, ratingType: RatingType): Pair<Rating, Rating> =
rate(Rating.KeyType.COPILOT, copilotId.toString(), raterId, ratingType)

fun findPersonalRatingOfCopilot(raterId: String, copilotId: Long) =
ratingRepository.findByTypeAndKeyAndUserId(Rating.KeyType.COPILOT, copilotId.toString(), raterId)
}

0 comments on commit fdcaa25

Please sign in to comment.