From 2952e289507d8cdcc103f702ef81c1e4dd06b8e3 Mon Sep 17 00:00:00 2001 From: "xuyutong.csu" Date: Mon, 21 Apr 2025 18:02:00 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat:=E5=85=B3=E6=B3=A8=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=88=E5=8F=AA=E8=83=BD=E8=8E=B7=E5=8F=96=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E7=94=A8=E6=88=B7=E7=9A=84=E5=85=B3=E6=B3=A8?= =?UTF-8?q?=E4=B8=8E=E7=B2=89=E4=B8=9D=E5=88=97=E8=A1=A8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/UserFollowController.kt | 59 +++++++ .../controller/response/user/MaaUserInfo.kt | 2 + .../backend/repository/UserFansRepository.kt | 8 + .../repository/UserFollowingRepository.kt | 8 + .../maa/backend/repository/entity/MaaUser.kt | 2 + .../maa/backend/repository/entity/UserFans.kt | 15 ++ .../repository/entity/UserFollowing.kt | 15 ++ .../maa/backend/service/UserFollowService.kt | 154 ++++++++++++++++++ 8 files changed, 263 insertions(+) create mode 100644 src/main/kotlin/plus/maa/backend/controller/UserFollowController.kt create mode 100644 src/main/kotlin/plus/maa/backend/repository/UserFansRepository.kt create mode 100644 src/main/kotlin/plus/maa/backend/repository/UserFollowingRepository.kt create mode 100644 src/main/kotlin/plus/maa/backend/repository/entity/UserFans.kt create mode 100644 src/main/kotlin/plus/maa/backend/repository/entity/UserFollowing.kt create mode 100644 src/main/kotlin/plus/maa/backend/service/UserFollowService.kt diff --git a/src/main/kotlin/plus/maa/backend/controller/UserFollowController.kt b/src/main/kotlin/plus/maa/backend/controller/UserFollowController.kt new file mode 100644 index 00000000..8ea3493c --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/UserFollowController.kt @@ -0,0 +1,59 @@ +package plus.maa.backend.controller + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import plus.maa.backend.config.doc.RequireJwt +import plus.maa.backend.config.security.AuthenticationHelper +import plus.maa.backend.controller.response.MaaResult +import plus.maa.backend.controller.response.MaaResult.Companion.success +import plus.maa.backend.controller.response.user.MaaUserInfo +import plus.maa.backend.service.UserFollowService + +@RestController +@RequestMapping("/follow") +@Tag(name = "UserFollow", description = "用户关注管理接口") +class UserFollowController( + private val userFollowService: UserFollowService, + private val helper: AuthenticationHelper, +) { + + @Operation(summary = "关注用户") + @ApiResponse(description = "关注结果") + @RequireJwt + @PostMapping("/follow/{followUserId}") + fun follow(@PathVariable followUserId: String): MaaResult = success( + userFollowService.follow(helper.requireUserId(), followUserId), + ) + + @Operation(summary = "取消关注") + @ApiResponse(description = "取消关注结果") + @RequireJwt + @PostMapping("/unfollow/{followUserId}") + fun unfollow(@PathVariable followUserId: String): MaaResult = success( + userFollowService.unfollow(helper.requireUserId(), followUserId), + ) + + @Operation(summary = "获取关注列表") + @ApiResponse(description = "关注列表") + @RequireJwt + @GetMapping("/followingList") + fun getFollowingList(pageable: Pageable): MaaResult> = success( + userFollowService.getFollowingList(helper.requireUserId(), pageable), + ) + + @Operation(summary = "获取粉丝列表") + @ApiResponse(description = "粉丝列表") + @RequireJwt + @GetMapping("/fansList") + fun getFansList(pageable: Pageable): MaaResult> = success( + userFollowService.getFansList(helper.requireUserId(), pageable), + ) +} diff --git a/src/main/kotlin/plus/maa/backend/controller/response/user/MaaUserInfo.kt b/src/main/kotlin/plus/maa/backend/controller/response/user/MaaUserInfo.kt index 3c4a2dd2..e11f7565 100644 --- a/src/main/kotlin/plus/maa/backend/controller/response/user/MaaUserInfo.kt +++ b/src/main/kotlin/plus/maa/backend/controller/response/user/MaaUserInfo.kt @@ -11,6 +11,8 @@ data class MaaUserInfo( val id: String, val userName: String, val activated: Boolean = false, + var followingCount: Int = 0, + var fansCount: Int = 0, ) { constructor(user: MaaUser) : this(user.userId!!, user.userName, user.status == 1) } diff --git a/src/main/kotlin/plus/maa/backend/repository/UserFansRepository.kt b/src/main/kotlin/plus/maa/backend/repository/UserFansRepository.kt new file mode 100644 index 00000000..3b59b111 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/UserFansRepository.kt @@ -0,0 +1,8 @@ +package plus.maa.backend.repository + +import org.springframework.data.mongodb.repository.MongoRepository +import plus.maa.backend.repository.entity.UserFans + +interface UserFansRepository : MongoRepository { + fun findByUserId(userId: String): UserFans? +} diff --git a/src/main/kotlin/plus/maa/backend/repository/UserFollowingRepository.kt b/src/main/kotlin/plus/maa/backend/repository/UserFollowingRepository.kt new file mode 100644 index 00000000..26d8ef02 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/UserFollowingRepository.kt @@ -0,0 +1,8 @@ +package plus.maa.backend.repository + +import org.springframework.data.mongodb.repository.MongoRepository +import plus.maa.backend.repository.entity.UserFollowing + +interface UserFollowingRepository : MongoRepository { + fun findByUserId(userId: String): UserFollowing? +} diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/MaaUser.kt b/src/main/kotlin/plus/maa/backend/repository/entity/MaaUser.kt index 3dd15612..1d8db785 100644 --- a/src/main/kotlin/plus/maa/backend/repository/entity/MaaUser.kt +++ b/src/main/kotlin/plus/maa/backend/repository/entity/MaaUser.kt @@ -23,6 +23,8 @@ data class MaaUser( var password: String, var status: Int = 0, var pwdUpdateTime: Instant = Instant.MIN, + var followingCount: Int = 0, + var fansCount: Int = 0, ) : Serializable { companion object { diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/UserFans.kt b/src/main/kotlin/plus/maa/backend/repository/entity/UserFans.kt new file mode 100644 index 00000000..11e7fb00 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/UserFans.kt @@ -0,0 +1,15 @@ +package plus.maa.backend.repository.entity + +import org.springframework.data.annotation.Id +import org.springframework.data.mongodb.core.mapping.Document +import plus.maa.backend.controller.response.user.MaaUserInfo +import java.time.LocalDateTime + +@Document("user_fans") +data class UserFans( + @Id + val id: String? = null, + val userId: String, + val fansList: MutableSet = mutableSetOf(), + var updatedAt: LocalDateTime = LocalDateTime.now(), +) diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/UserFollowing.kt b/src/main/kotlin/plus/maa/backend/repository/entity/UserFollowing.kt new file mode 100644 index 00000000..83005405 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/UserFollowing.kt @@ -0,0 +1,15 @@ +package plus.maa.backend.repository.entity + +import org.springframework.data.annotation.Id +import org.springframework.data.mongodb.core.mapping.Document +import plus.maa.backend.controller.response.user.MaaUserInfo +import java.time.LocalDateTime + +@Document("user_following") +data class UserFollowing( + @Id + val id: String? = null, + val userId: String, + val followList: MutableSet = mutableSetOf(), + var updatedAt: LocalDateTime = LocalDateTime.now(), +) diff --git a/src/main/kotlin/plus/maa/backend/service/UserFollowService.kt b/src/main/kotlin/plus/maa/backend/service/UserFollowService.kt new file mode 100644 index 00000000..9dfa4bff --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/UserFollowService.kt @@ -0,0 +1,154 @@ +package plus.maa.backend.service + +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.Pageable +import org.springframework.data.mongodb.core.MongoTemplate +import org.springframework.data.mongodb.core.query.Criteria +import org.springframework.data.mongodb.core.query.Query +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import plus.maa.backend.controller.response.MaaResultException +import plus.maa.backend.controller.response.user.MaaUserInfo +import plus.maa.backend.repository.UserFansRepository +import plus.maa.backend.repository.UserFollowingRepository +import plus.maa.backend.repository.entity.MaaUser +import plus.maa.backend.repository.entity.UserFans +import plus.maa.backend.repository.entity.UserFollowing +import java.time.LocalDateTime + +@Service +class UserFollowService( + private val userFollowingRepository: UserFollowingRepository, + private val userFansRepository: UserFansRepository, + private val userService: UserService, + private val mongoTemplate: MongoTemplate, +) { + @Transactional + fun follow(userId: String, followUserId: String) { + require(userId != followUserId) { "不能关注自己" } + + // 检查被关注用户是否存在 + val targetUser = userService.findByUserIdOrDefault(followUserId) + if (targetUser == MaaUser.UNKNOWN) { + throw MaaResultException(404, "目标用户不存在") + } + + // 获取当前用户信息 + val currentUser = userService.findByUserIdOrDefault(userId) + + // 将 MaaUser 转换为 MaaUserInfo + val targetUserInfo = MaaUserInfo( + id = targetUser.userId!!, + userName = targetUser.userName, + activated = targetUser.status == 1, + followingCount = targetUser.followingCount, + fansCount = targetUser.fansCount, + ) + val currentUserInfo = MaaUserInfo( + id = currentUser.userId!!, + userName = currentUser.userName, + activated = currentUser.status == 1, + followingCount = currentUser.followingCount, + fansCount = currentUser.fansCount, + ) + + // 更新关注列表 + val following = userFollowingRepository.findByUserId(userId) + ?: UserFollowing(userId = userId) + if (!following.followList.any { it.id == followUserId }) { + following.followList.add(targetUserInfo) + following.updatedAt = LocalDateTime.now() + userFollowingRepository.save(following) + + // 更新当前用户的关注数(基于集合大小) + val currentUserQuery = Query.query(Criteria.where("userId").`is`(userId)) + val currentUserUpdate = org.springframework.data.mongodb.core.query.Update().set("followingCount", following.followList.size) + mongoTemplate.updateFirst(currentUserQuery, currentUserUpdate, MaaUser::class.java) + } + + // 更新粉丝列表 + val fans = userFansRepository.findByUserId(followUserId) + ?: UserFans(userId = followUserId) + if (!fans.fansList.any { it.id == userId }) { + fans.fansList.add(currentUserInfo) + fans.updatedAt = LocalDateTime.now() + userFansRepository.save(fans) + + // 更新目标用户的粉丝数(基于集合大小) + val targetUserQuery = Query.query(Criteria.where("userId").`is`(followUserId)) + val targetUserUpdate = org.springframework.data.mongodb.core.query.Update().set("fansCount", fans.fansList.size) + mongoTemplate.updateFirst(targetUserQuery, targetUserUpdate, MaaUser::class.java) + } + } + + @Transactional + fun unfollow(userId: String, followUserId: String) { + require(userId != followUserId) { "不能取关自己" } + + // 更新关注列表 + userFollowingRepository.findByUserId(userId)?.let { following -> + if (following.followList.removeIf { it.id == followUserId }) { + following.updatedAt = LocalDateTime.now() + userFollowingRepository.save(following) + + // 更新当前用户的关注数(基于集合大小) + val currentUserQuery = Query.query(Criteria.where("userId").`is`(userId)) + val currentUserUpdate = org.springframework.data.mongodb.core.query.Update().set( + "followingCount", + following.followList.size, + ) + mongoTemplate.updateFirst(currentUserQuery, currentUserUpdate, MaaUser::class.java) + } + } + + // 更新粉丝列表 + userFansRepository.findByUserId(followUserId)?.let { fans -> + if (fans.fansList.removeIf { it.id == userId }) { + fans.updatedAt = LocalDateTime.now() + userFansRepository.save(fans) + + // 更新目标用户的粉丝数(基于集合大小) + val targetUserQuery = Query.query(Criteria.where("userId").`is`(followUserId)) + val targetUserUpdate = org.springframework.data.mongodb.core.query.Update().set("fansCount", fans.fansList.size) + mongoTemplate.updateFirst(targetUserQuery, targetUserUpdate, MaaUser::class.java) + } + } + } + + fun getFollowingList(userId: String, pageable: Pageable): Page { + val following = userFollowingRepository.findByUserId(userId) + ?: return Page.empty(pageable) + + val totalElements = following.followList.size.toLong() + val start = pageable.pageNumber * pageable.pageSize + val end = minOf(start + pageable.pageSize, totalElements.toInt()) + + if (start >= totalElements) { + return Page.empty(pageable) + } + + val content = following.followList.toMutableList() + .subList(start, end) + + return PageImpl(content, pageable, totalElements) + } + + fun getFansList(userId: String, pageable: Pageable): Page { + val fans = userFansRepository.findByUserId(userId) + ?: return Page.empty(pageable) + + val totalElements = fans.fansList.size.toLong() + val start = pageable.pageNumber * pageable.pageSize + val end = minOf(start + pageable.pageSize, totalElements.toInt()) + + if (start >= totalElements) { + return Page.empty(pageable) + } + + val content = fans.fansList.toMutableList() + .subList(start, end) + + return PageImpl(content, pageable, totalElements) + } +} From 0b3381d3208682e1803ad11ff0ab683f49a5df33 Mon Sep 17 00:00:00 2001 From: "xuyutong.csu" Date: Mon, 21 Apr 2025 19:21:21 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat:=E5=8F=AA=E6=9F=A5=E8=AF=A2=E5=85=B3?= =?UTF-8?q?=E6=B3=A8=E8=80=85=E7=9A=84=E4=BD=9C=E4=B8=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/request/copilot/CopilotQueriesRequest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotQueriesRequest.kt b/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotQueriesRequest.kt index 96d5e866..4ddeac43 100644 --- a/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotQueriesRequest.kt +++ b/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotQueriesRequest.kt @@ -6,7 +6,7 @@ import plus.maa.backend.service.model.CopilotSetStatus /** * @author LoMu - * Date 2022-12-26 2:48 + * Date 2022-12-26 2:48 */ data class CopilotQueriesRequest( val page: Int = 0, @@ -22,4 +22,5 @@ data class CopilotQueriesRequest( val language: String? = null, @BindParam("copilot_ids") var copilotIds: List? = null, val status: CopilotSetStatus? = null, + val onlyFollowing: Boolean = false, ) From 6b458b022d2e603200aa89f908801dac695fce76 Mon Sep 17 00:00:00 2001 From: Pleasurecruise <3196812536@qq.com> Date: Wed, 23 Apr 2025 01:25:29 +0800 Subject: [PATCH 3/5] feat: add copilotsetquery following filter --- .../request/copilotset/CopilotSetQuery.kt | 2 ++ .../plus/maa/backend/service/CopilotSetService.kt | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetQuery.kt b/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetQuery.kt index ae6a0ce9..f0080cb0 100644 --- a/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetQuery.kt +++ b/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetQuery.kt @@ -22,6 +22,8 @@ data class CopilotSetQuery( val keyword: String? = null, @Schema(title = "创建者id") val creatorId: String? = null, + @Schema(title="仅查询关注者的作业集") + var onlyFollowing: Boolean? = false, @Schema(title = "需要包含的作业id列表") val copilotIds: List? = null, ) diff --git a/src/main/kotlin/plus/maa/backend/service/CopilotSetService.kt b/src/main/kotlin/plus/maa/backend/service/CopilotSetService.kt index c9b78b12..5903174c 100644 --- a/src/main/kotlin/plus/maa/backend/service/CopilotSetService.kt +++ b/src/main/kotlin/plus/maa/backend/service/CopilotSetService.kt @@ -18,6 +18,7 @@ import plus.maa.backend.controller.request.copilotset.CopilotSetUpdateReq import plus.maa.backend.controller.response.copilotset.CopilotSetPageRes import plus.maa.backend.controller.response.copilotset.CopilotSetRes import plus.maa.backend.repository.CopilotSetRepository +import plus.maa.backend.repository.UserFollowingRepository import plus.maa.backend.repository.entity.CopilotSet import plus.maa.backend.service.model.CopilotSetStatus import java.time.LocalDateTime @@ -31,6 +32,7 @@ import java.util.regex.Pattern class CopilotSetService( private val idComponent: IdComponent, private val converter: CopilotSetConverter, + private val userFollowingRepository: UserFollowingRepository, private val repository: CopilotSetRepository, private val userService: UserService, private val mongoTemplate: MongoTemplate, @@ -123,6 +125,17 @@ class CopilotSetService( } andList.add(permissionCriterion) andList.add(Criteria.where("delete").`is`(false)) + + if (req.onlyFollowing == true && userId != null) { + val userFollowing = userFollowingRepository.findByUserId(userId) + val followingIds = userFollowing?.followList?.map { it.id } ?: emptyList() + if (followingIds.isEmpty()) { + return CopilotSetPageRes(false, 0, 0, mutableListOf()) + } + + andList.add(Criteria.where("creatorId").`in`(followingIds)) + } + if (!req.copilotIds.isNullOrEmpty()) { andList.add(Criteria.where("copilotIds").all(req.copilotIds)) } From 8436fb576077b20e7abb8b3fe5b72a559fdd8933 Mon Sep 17 00:00:00 2001 From: "xuyutong.csu" Date: Wed, 23 Apr 2025 15:51:37 +0800 Subject: [PATCH 4/5] =?UTF-8?q?adv:=201.=20=E7=B2=89=E4=B8=9D=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E9=87=87=E7=94=A8List=E5=AD=98=E5=82=A8=EF=BC=8C?= =?UTF-8?q?=E4=B8=94=E5=AD=98=E5=82=A8=E7=B2=89=E4=B8=9D=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=9A=84id=E8=80=8C=E9=9D=9E=E7=B2=89=E4=B8=9D=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E7=9A=84UserInfo=202.=20=E8=8E=B7=E5=8F=96=E7=B2=89?= =?UTF-8?q?=E4=B8=9D=E5=88=97=E8=A1=A8=E6=97=B6=EF=BC=8C=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E6=95=B4=E4=B8=AA=E7=B2=89=E4=B8=9D=E5=88=97?= =?UTF-8?q?=E8=A1=A8=EF=BC=88=E5=AD=98=E5=82=A8ID=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=EF=BC=8C=E6=95=B0=E9=87=8F=E4=B8=8D=E4=BC=9A=E5=A4=AA=E5=A4=A7?= =?UTF-8?q?=EF=BC=89=EF=BC=8C=E5=86=8D=E5=88=86=E9=A1=B5=E6=9F=A5User?= =?UTF-8?q?=E8=A1=A8=E8=BF=94=E5=9B=9E=E7=B2=89=E4=B8=9D=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=203.=20=E6=89=8B=E5=86=99=E5=88=86=E9=A1=B5?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E5=85=B3=E6=B3=A8=E5=88=97=E8=A1=A8=E7=B2=89?= =?UTF-8?q?=E4=B8=9D=E5=88=97=E8=A1=A8controller=E5=85=A5=E5=8F=82?= =?UTF-8?q?=EF=BC=8C=E4=B8=8E=E4=B9=8B=E5=89=8DAPI=E5=88=86=E9=A1=B5?= =?UTF-8?q?=E8=B5=B7=E5=A7=8B=E4=BD=8D=E7=BD=AE1=E4=BF=9D=E6=8C=81?= =?UTF-8?q?=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/UserFollowController.kt | 36 +++- .../request/copilotset/CopilotSetQuery.kt | 2 +- .../controller/response/user/MaaUserInfo.kt | 12 +- .../maa/backend/repository/entity/UserFans.kt | 7 +- .../repository/entity/UserFollowing.kt | 7 +- .../maa/backend/service/CopilotSetService.kt | 2 +- .../maa/backend/service/UserFollowService.kt | 202 +++++++++++------- 7 files changed, 167 insertions(+), 101 deletions(-) diff --git a/src/main/kotlin/plus/maa/backend/controller/UserFollowController.kt b/src/main/kotlin/plus/maa/backend/controller/UserFollowController.kt index 8ea3493c..8806c93d 100644 --- a/src/main/kotlin/plus/maa/backend/controller/UserFollowController.kt +++ b/src/main/kotlin/plus/maa/backend/controller/UserFollowController.kt @@ -4,15 +4,17 @@ import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.data.domain.Page -import org.springframework.data.domain.Pageable +import org.springframework.data.domain.PageRequest import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController import plus.maa.backend.config.doc.RequireJwt import plus.maa.backend.config.security.AuthenticationHelper import plus.maa.backend.controller.response.MaaResult +import plus.maa.backend.controller.response.MaaResult.Companion.fail import plus.maa.backend.controller.response.MaaResult.Companion.success import plus.maa.backend.controller.response.user.MaaUserInfo import plus.maa.backend.service.UserFollowService @@ -45,15 +47,35 @@ class UserFollowController( @ApiResponse(description = "关注列表") @RequireJwt @GetMapping("/followingList") - fun getFollowingList(pageable: Pageable): MaaResult> = success( - userFollowService.getFollowingList(helper.requireUserId(), pageable), - ) + fun getFollowingList(@RequestParam page: Int = 1, @RequestParam size: Int = 10): MaaResult> { + // 之前的API约定分页从1开始,与Spring默认约定不同,在此转换 + if (page < 1) { + return fail(422, "页数请从1开始") + } + val realPageable = PageRequest.of( + page - 1, + size, + ) + return success( + userFollowService.getFollowingList(helper.requireUserId(), realPageable), + ) + } @Operation(summary = "获取粉丝列表") @ApiResponse(description = "粉丝列表") @RequireJwt @GetMapping("/fansList") - fun getFansList(pageable: Pageable): MaaResult> = success( - userFollowService.getFansList(helper.requireUserId(), pageable), - ) + fun getFansList(@RequestParam page: Int = 1, @RequestParam size: Int = 10): MaaResult> { + // 之前的API约定分页从1开始,与Spring默认约定不同,在此转换 + if (page < 1) { + return fail(422, "页数请从1开始") + } + val realPageable = PageRequest.of( + page - 1, + size, + ) + return success( + userFollowService.getFansList(helper.requireUserId(), realPageable), + ) + } } diff --git a/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetQuery.kt b/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetQuery.kt index f0080cb0..0ff648a0 100644 --- a/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetQuery.kt +++ b/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetQuery.kt @@ -22,7 +22,7 @@ data class CopilotSetQuery( val keyword: String? = null, @Schema(title = "创建者id") val creatorId: String? = null, - @Schema(title="仅查询关注者的作业集") + @Schema(title = "仅查询关注者的作业集") var onlyFollowing: Boolean? = false, @Schema(title = "需要包含的作业id列表") val copilotIds: List? = null, diff --git a/src/main/kotlin/plus/maa/backend/controller/response/user/MaaUserInfo.kt b/src/main/kotlin/plus/maa/backend/controller/response/user/MaaUserInfo.kt index e11f7565..a272927a 100644 --- a/src/main/kotlin/plus/maa/backend/controller/response/user/MaaUserInfo.kt +++ b/src/main/kotlin/plus/maa/backend/controller/response/user/MaaUserInfo.kt @@ -11,8 +11,14 @@ data class MaaUserInfo( val id: String, val userName: String, val activated: Boolean = false, - var followingCount: Int = 0, - var fansCount: Int = 0, + val followingCount: Int = 0, + val fansCount: Int = 0, ) { - constructor(user: MaaUser) : this(user.userId!!, user.userName, user.status == 1) + constructor(user: MaaUser) : this( + id = user.userId!!, + userName = user.userName, + activated = user.status == 1, + followingCount = user.followingCount, + fansCount = user.fansCount, + ) } diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/UserFans.kt b/src/main/kotlin/plus/maa/backend/repository/entity/UserFans.kt index 11e7fb00..deba11e4 100644 --- a/src/main/kotlin/plus/maa/backend/repository/entity/UserFans.kt +++ b/src/main/kotlin/plus/maa/backend/repository/entity/UserFans.kt @@ -2,14 +2,13 @@ package plus.maa.backend.repository.entity import org.springframework.data.annotation.Id import org.springframework.data.mongodb.core.mapping.Document -import plus.maa.backend.controller.response.user.MaaUserInfo -import java.time.LocalDateTime +import java.time.Instant @Document("user_fans") data class UserFans( @Id val id: String? = null, val userId: String, - val fansList: MutableSet = mutableSetOf(), - var updatedAt: LocalDateTime = LocalDateTime.now(), + val fansList: MutableList = mutableListOf(), + var updatedAt: Instant = Instant.now(), ) diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/UserFollowing.kt b/src/main/kotlin/plus/maa/backend/repository/entity/UserFollowing.kt index 83005405..3b32f202 100644 --- a/src/main/kotlin/plus/maa/backend/repository/entity/UserFollowing.kt +++ b/src/main/kotlin/plus/maa/backend/repository/entity/UserFollowing.kt @@ -2,14 +2,13 @@ package plus.maa.backend.repository.entity import org.springframework.data.annotation.Id import org.springframework.data.mongodb.core.mapping.Document -import plus.maa.backend.controller.response.user.MaaUserInfo -import java.time.LocalDateTime +import java.time.Instant @Document("user_following") data class UserFollowing( @Id val id: String? = null, val userId: String, - val followList: MutableSet = mutableSetOf(), - var updatedAt: LocalDateTime = LocalDateTime.now(), + val followList: MutableList = mutableListOf(), + var updatedAt: Instant = Instant.now(), ) diff --git a/src/main/kotlin/plus/maa/backend/service/CopilotSetService.kt b/src/main/kotlin/plus/maa/backend/service/CopilotSetService.kt index 5903174c..0e068e1f 100644 --- a/src/main/kotlin/plus/maa/backend/service/CopilotSetService.kt +++ b/src/main/kotlin/plus/maa/backend/service/CopilotSetService.kt @@ -128,7 +128,7 @@ class CopilotSetService( if (req.onlyFollowing == true && userId != null) { val userFollowing = userFollowingRepository.findByUserId(userId) - val followingIds = userFollowing?.followList?.map { it.id } ?: emptyList() + val followingIds = userFollowing?.followList ?: emptyList() if (followingIds.isEmpty()) { return CopilotSetPageRes(false, 0, 0, mutableListOf()) } diff --git a/src/main/kotlin/plus/maa/backend/service/UserFollowService.kt b/src/main/kotlin/plus/maa/backend/service/UserFollowService.kt index 9dfa4bff..4c088bdf 100644 --- a/src/main/kotlin/plus/maa/backend/service/UserFollowService.kt +++ b/src/main/kotlin/plus/maa/backend/service/UserFollowService.kt @@ -6,6 +6,7 @@ import org.springframework.data.domain.Pageable import org.springframework.data.mongodb.core.MongoTemplate import org.springframework.data.mongodb.core.query.Criteria import org.springframework.data.mongodb.core.query.Query +import org.springframework.data.mongodb.core.query.Update import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import plus.maa.backend.controller.response.MaaResultException @@ -15,7 +16,7 @@ import plus.maa.backend.repository.UserFollowingRepository import plus.maa.backend.repository.entity.MaaUser import plus.maa.backend.repository.entity.UserFans import plus.maa.backend.repository.entity.UserFollowing -import java.time.LocalDateTime +import java.time.Instant @Service class UserFollowService( @@ -34,121 +35,160 @@ class UserFollowService( throw MaaResultException(404, "目标用户不存在") } - // 获取当前用户信息 - val currentUser = userService.findByUserIdOrDefault(userId) - - // 将 MaaUser 转换为 MaaUserInfo - val targetUserInfo = MaaUserInfo( - id = targetUser.userId!!, - userName = targetUser.userName, - activated = targetUser.status == 1, - followingCount = targetUser.followingCount, - fansCount = targetUser.fansCount, - ) - val currentUserInfo = MaaUserInfo( - id = currentUser.userId!!, - userName = currentUser.userName, - activated = currentUser.status == 1, - followingCount = currentUser.followingCount, - fansCount = currentUser.fansCount, + // 检查是否已经关注 + val followQuery = Query.query( + Criteria.where("userId").`is`(userId) + .and("followList").`in`(followUserId), ) + if (mongoTemplate.exists(followQuery, UserFollowing::class.java)) { + print("已关注 不可重复关注!") + return + } // 更新关注列表 - val following = userFollowingRepository.findByUserId(userId) - ?: UserFollowing(userId = userId) - if (!following.followList.any { it.id == followUserId }) { - following.followList.add(targetUserInfo) - following.updatedAt = LocalDateTime.now() - userFollowingRepository.save(following) - - // 更新当前用户的关注数(基于集合大小) - val currentUserQuery = Query.query(Criteria.where("userId").`is`(userId)) - val currentUserUpdate = org.springframework.data.mongodb.core.query.Update().set("followingCount", following.followList.size) - mongoTemplate.updateFirst(currentUserQuery, currentUserUpdate, MaaUser::class.java) - } + val followUpdate = Update() + .addToSet("followList", followUserId) + .set("updatedAt", Instant.now()) + + mongoTemplate.upsert( + Query.query(Criteria.where("userId").`is`(userId)), + followUpdate, + UserFollowing::class.java, + ) // 更新粉丝列表 - val fans = userFansRepository.findByUserId(followUserId) - ?: UserFans(userId = followUserId) - if (!fans.fansList.any { it.id == userId }) { - fans.fansList.add(currentUserInfo) - fans.updatedAt = LocalDateTime.now() - userFansRepository.save(fans) - - // 更新目标用户的粉丝数(基于集合大小) - val targetUserQuery = Query.query(Criteria.where("userId").`is`(followUserId)) - val targetUserUpdate = org.springframework.data.mongodb.core.query.Update().set("fansCount", fans.fansList.size) - mongoTemplate.updateFirst(targetUserQuery, targetUserUpdate, MaaUser::class.java) - } + val fansUpdate = Update() + .addToSet("fansList", userId) + .set("updatedAt", Instant.now()) + + mongoTemplate.upsert( + Query.query(Criteria.where("userId").`is`(followUserId)), + fansUpdate, + UserFans::class.java, + ) + + // 更新关注数量和粉丝数量 + + val followingCount = userFollowingRepository.findByUserId(userId)?.followList?.size ?: 0 + val fansCount = userFansRepository.findByUserId(followUserId)?.fansList?.size ?: 0 + + mongoTemplate.updateFirst( + Query.query(Criteria.where("userId").`is`(userId)), + Update().set("followingCount", followingCount), + MaaUser::class.java, + ) + + mongoTemplate.updateFirst( + Query.query(Criteria.where("userId").`is`(followUserId)), + Update().set("fansCount", fansCount), + MaaUser::class.java, + ) } @Transactional fun unfollow(userId: String, followUserId: String) { require(userId != followUserId) { "不能取关自己" } - // 更新关注列表 - userFollowingRepository.findByUserId(userId)?.let { following -> - if (following.followList.removeIf { it.id == followUserId }) { - following.updatedAt = LocalDateTime.now() - userFollowingRepository.save(following) - - // 更新当前用户的关注数(基于集合大小) - val currentUserQuery = Query.query(Criteria.where("userId").`is`(userId)) - val currentUserUpdate = org.springframework.data.mongodb.core.query.Update().set( - "followingCount", - following.followList.size, - ) - mongoTemplate.updateFirst(currentUserQuery, currentUserUpdate, MaaUser::class.java) - } + // 检查是否已经关注 + val followQuery = Query.query( + Criteria.where("userId").`is`(userId) + .and("followList").`in`(followUserId), + ) + if (!mongoTemplate.exists(followQuery, UserFollowing::class.java)) { + return } + // 更新关注列表 + val followUpdate = Update() + .pull("followList", followUserId) + .set("updatedAt", Instant.now()) + + mongoTemplate.updateFirst( + Query.query(Criteria.where("userId").`is`(userId)), + followUpdate, + UserFollowing::class.java, + ) + // 更新粉丝列表 - userFansRepository.findByUserId(followUserId)?.let { fans -> - if (fans.fansList.removeIf { it.id == userId }) { - fans.updatedAt = LocalDateTime.now() - userFansRepository.save(fans) - - // 更新目标用户的粉丝数(基于集合大小) - val targetUserQuery = Query.query(Criteria.where("userId").`is`(followUserId)) - val targetUserUpdate = org.springframework.data.mongodb.core.query.Update().set("fansCount", fans.fansList.size) - mongoTemplate.updateFirst(targetUserQuery, targetUserUpdate, MaaUser::class.java) - } - } + val fansUpdate = Update() + .pull("fansList", userId) + .set("updatedAt", Instant.now()) + + mongoTemplate.updateFirst( + Query.query(Criteria.where("userId").`is`(followUserId)), + fansUpdate, + UserFans::class.java, + ) + + // 更新关注数量和粉丝数量 + + val followingCount = userFollowingRepository.findByUserId(userId)?.followList?.size ?: 0 + val fansCount = userFansRepository.findByUserId(followUserId)?.fansList?.size ?: 0 + + mongoTemplate.updateFirst( + Query.query(Criteria.where("userId").`is`(userId)), + Update().set("followingCount", followingCount), + MaaUser::class.java, + ) + + mongoTemplate.updateFirst( + Query.query(Criteria.where("userId").`is`(followUserId)), + Update().set("fansCount", fansCount), + MaaUser::class.java, + ) } fun getFollowingList(userId: String, pageable: Pageable): Page { val following = userFollowingRepository.findByUserId(userId) ?: return Page.empty(pageable) - val totalElements = following.followList.size.toLong() - val start = pageable.pageNumber * pageable.pageSize - val end = minOf(start + pageable.pageSize, totalElements.toInt()) + val followIds = following.followList + val total = followIds.size.toLong() + val start = pageable.offset.coerceAtMost(total) + val end = (start + pageable.pageSize).coerceAtMost(total) - if (start >= totalElements) { + if (start >= total) { return Page.empty(pageable) } - val content = following.followList.toMutableList() - .subList(start, end) + val pageIds = followIds.subList(start.toInt(), end.toInt()) + val users = mongoTemplate.find( + Query.query(Criteria.where("userId").`in`(pageIds)), // 注意这里用 userId 字段查询 + MaaUser::class.java, + ) + + val userMap = users.associateBy { it.userId } + val userInfos = pageIds.mapNotNull { id -> + userMap[id]?.let { MaaUserInfo(it) } + } - return PageImpl(content, pageable, totalElements) + return PageImpl(userInfos, pageable, total) } fun getFansList(userId: String, pageable: Pageable): Page { val fans = userFansRepository.findByUserId(userId) ?: return Page.empty(pageable) - val totalElements = fans.fansList.size.toLong() - val start = pageable.pageNumber * pageable.pageSize - val end = minOf(start + pageable.pageSize, totalElements.toInt()) + val fanIds = fans.fansList + val total = fanIds.size.toLong() + val start = pageable.offset.coerceAtMost(total) + val end = (start + pageable.pageSize).coerceAtMost(total) - if (start >= totalElements) { + if (start >= total) { return Page.empty(pageable) } - val content = fans.fansList.toMutableList() - .subList(start, end) + val pageIds = fanIds.subList(start.toInt(), end.toInt()) + val users = mongoTemplate.find( + Query.query(Criteria.where("userId").`in`(pageIds)), + MaaUser::class.java, + ) + + val userMap = users.associateBy { it.userId } + val userInfos = pageIds.mapNotNull { id -> + userMap[id]?.let { MaaUserInfo(it) } + } - return PageImpl(content, pageable, totalElements) + return PageImpl(userInfos, pageable, total) } } From 50bb1618a2e9476e401621e83d406ce46122a579 Mon Sep 17 00:00:00 2001 From: makotoxu <3369726918@qq.com> Date: Thu, 29 May 2025 14:04:14 +0800 Subject: [PATCH 5/5] =?UTF-8?q?feat:=E5=8F=AA=E6=9F=A5=E8=AF=A2=E5=85=B3?= =?UTF-8?q?=E6=B3=A8=E8=80=85=E4=BD=9C=E4=B8=9A+comment=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=EF=BC=88=E8=BF=98=E6=B2=A1=E6=B5=8B=E8=AF=95=EF=BC=8C=E7=AD=89?= =?UTF-8?q?=E6=9C=89=E7=8E=AF=E5=A2=83=E4=BA=86=E8=87=AA=E6=B5=8B=E5=86=8D?= =?UTF-8?q?commit=E4=B8=80=E6=AC=A1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/controller/UserFollowController.kt | 9 +++++++-- .../request/copilotset/CopilotSetQuery.kt | 2 +- .../plus/maa/backend/service/CopilotService.kt | 15 ++++++++++++++- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/plus/maa/backend/controller/UserFollowController.kt b/src/main/kotlin/plus/maa/backend/controller/UserFollowController.kt index 8806c93d..1f2d8f48 100644 --- a/src/main/kotlin/plus/maa/backend/controller/UserFollowController.kt +++ b/src/main/kotlin/plus/maa/backend/controller/UserFollowController.kt @@ -3,6 +3,7 @@ package plus.maa.backend.controller import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.constraints.Max import org.springframework.data.domain.Page import org.springframework.data.domain.PageRequest import org.springframework.web.bind.annotation.GetMapping @@ -47,7 +48,9 @@ class UserFollowController( @ApiResponse(description = "关注列表") @RequireJwt @GetMapping("/followingList") - fun getFollowingList(@RequestParam page: Int = 1, @RequestParam size: Int = 10): MaaResult> { + fun getFollowingList( + @RequestParam page: Int = 1, + @RequestParam @Max(value = 50, message = "单页大小不得超过50") size: Int = 10): MaaResult> { // 之前的API约定分页从1开始,与Spring默认约定不同,在此转换 if (page < 1) { return fail(422, "页数请从1开始") @@ -65,7 +68,9 @@ class UserFollowController( @ApiResponse(description = "粉丝列表") @RequireJwt @GetMapping("/fansList") - fun getFansList(@RequestParam page: Int = 1, @RequestParam size: Int = 10): MaaResult> { + fun getFansList( + @RequestParam page: Int = 1, + @RequestParam @Max(value = 50, message = "单页大小不得超过50") size: Int = 10): MaaResult> { // 之前的API约定分页从1开始,与Spring默认约定不同,在此转换 if (page < 1) { return fail(422, "页数请从1开始") diff --git a/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetQuery.kt b/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetQuery.kt index 0ff648a0..dce7e1ab 100644 --- a/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetQuery.kt +++ b/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetQuery.kt @@ -23,7 +23,7 @@ data class CopilotSetQuery( @Schema(title = "创建者id") val creatorId: String? = null, @Schema(title = "仅查询关注者的作业集") - var onlyFollowing: Boolean? = false, + var onlyFollowing: Boolean = false, @Schema(title = "需要包含的作业id列表") val copilotIds: List? = null, ) diff --git a/src/main/kotlin/plus/maa/backend/service/CopilotService.kt b/src/main/kotlin/plus/maa/backend/service/CopilotService.kt index 60d15934..5fbe8e50 100644 --- a/src/main/kotlin/plus/maa/backend/service/CopilotService.kt +++ b/src/main/kotlin/plus/maa/backend/service/CopilotService.kt @@ -30,6 +30,7 @@ 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.RedisCache +import plus.maa.backend.repository.UserFollowingRepository import plus.maa.backend.repository.entity.Copilot import plus.maa.backend.repository.entity.Copilot.OperationGroup import plus.maa.backend.repository.entity.MaaUser @@ -70,6 +71,7 @@ class CopilotService( private val copilotConverter: CopilotConverter, private val sensitiveWordService: SensitiveWordService, private val segmentService: SegmentService, + private val userFollowingRepository: UserFollowingRepository, ) { private val log = KotlinLogging.logger { } @@ -198,7 +200,8 @@ class CopilotService( request.levelKeyword.isNullOrBlank() && request.uploaderId.isNullOrBlank() && request.operator.isNullOrBlank() && - request.copilotIds.isNullOrEmpty() + request.copilotIds.isNullOrEmpty() && + !request.onlyFollowing ) { request.orderBy?.blankAsNull() ?.let { key -> HOME_PAGE_CACHE_CONFIG[key] } @@ -234,6 +237,16 @@ class CopilotService( andQueries.add(Criteria.where("delete").`is`(false)) + if (request.onlyFollowing && userId != null) { + val userFollowing = userFollowingRepository.findByUserId(userId) + val followingIds = userFollowing?.followList ?: emptyList() + + if (followingIds.isEmpty()) { + return CopilotPageInfo(false, 0, 0, emptyList()) + } + // 添加查询范围为关注者 + andQueries.add(Criteria.where("uploaderId").`in`(followingIds)) + } // 仅查询自己的作业时才展示所有数据,否则只查询公开作业 if (request.uploaderId == "me" && userId != null) { if (request.status != null) {