Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 35 additions & 8 deletions src/main/kotlin/plus/maa/backend/controller/UserController.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package plus.maa.backend.controller
package plus.maa.backend.controller

import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.media.Content
Expand Down Expand Up @@ -48,7 +48,7 @@ class UserController(
private val helper: AuthenticationHelper,
) {
/**
* 更新当前用户的密码(根据原密码)
* 更新当前用户的密码根据原密码
*
* @return http响应
*/
Expand Down Expand Up @@ -77,7 +77,7 @@ class UserController(
}

/**
* 邮箱重设密码
* 邮箱重置密码
*
* @param passwordResetDTO 通过邮箱修改密码请求
* @return 成功响应
Expand Down Expand Up @@ -156,17 +156,44 @@ class UserController(
fun login(@RequestBody user: @Valid LoginDTO): MaaResult<MaaLoginRsp> = success("登陆成功", userService.login(user))

/**
* 查询用户信息
* 获取当前登录用户信息
*/
@GetMapping("/me")
@Operation(summary = "获取当前登录用户信息")
@ApiResponse(description = "当前用户详情信息")
@RequireJwt
fun getMe(): MaaResult<MaaUserInfo> {
return success(userService.getMe(helper.userId))
}

/**
* 查询用户信息(附带与当前用户的关系)
*/
@GetMapping("/info")
@Operation(summary = "查询用户信息")
@ApiResponse(responseCode = "200", description = "用户详情信息")
@ApiResponse(responseCode = "404", content = [Content()])
fun getUserInfo(@RequestParam userId: String): MaaResult<MaaUserInfo> {
val userInfo =
userService.get(userId.toLongOrNull() ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid user ID"))
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
return success(userInfo)
val targetId = userId.toLongOrNull()
?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid user ID")
val currentUserId = helper.obtainUserId()?.toLongOrNull()
return success(userService.getWithRelation(targetId, currentUserId))
}

/**
* 批量获取用户信息
*/
@GetMapping("/batch")
@Operation(summary = "批量获取用户信息")
@ApiResponse(description = "用户信息列表")
fun getBatchUserInfo(
@RequestParam ids: List<Long>,
): MaaResult<List<MaaUserInfo>> {
if (ids.size > 50) {
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "单次查询用户量不能超过50")
}
val currentUserId = helper.obtainUserId()?.toLongOrNull()
return success(userService.getBatchUserInfos(ids, currentUserId))
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package plus.maa.backend.controller.response.user
package plus.maa.backend.controller.response.user

import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import plus.maa.backend.repository.entity.MaaUser
import plus.maa.backend.repository.entity.UserEntity
import java.time.Instant

/**
* 用户可对外公开的信息
Expand All @@ -16,6 +18,8 @@ data class MaaUserInfo(
val activated: Boolean = false,
val followingCount: Int = 0,
val fansCount: Int = 0,
val relation: RelationType? = null,
@Contextual val followedAt: Instant? = null,
) {
constructor(user: MaaUser) : this(
id = user.userId!!,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package plus.maa.backend.controller.response.user

import kotlinx.serialization.Serializable

@Serializable
enum class RelationType {
SELF, NONE, FOLLOWING, FOLLOWED_BY, MUTUAL
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import org.ktorm.dsl.inList
import org.ktorm.dsl.insert
import org.ktorm.dsl.limit
import org.ktorm.dsl.map
import org.ktorm.dsl.mapNotNull
import org.ktorm.dsl.minus
import org.ktorm.dsl.plus
import org.ktorm.dsl.select
Expand Down Expand Up @@ -164,6 +165,62 @@ class UserKtormRepository(
}
}

/**
* 查询 userId 关注了 targetIds 中的哪些用户,返回 followUserId -> updatedAt 的映射
*/
fun getFollowUpdatedAtMap(userId: Long, targetIds: List<Long>): Map<Long, LocalDateTime> {
if (targetIds.isEmpty()) return emptyMap()
return database.from(UserFollows)
.select(UserFollows.followUserId, UserFollows.updatedAt)
.where { (UserFollows.userId eq userId) and (UserFollows.followUserId inList targetIds) }
.map { row -> row[UserFollows.followUserId]!! to row[UserFollows.updatedAt]!! }
.toMap()
}

/**
* 查询 fanIds 中谁关注了 userId,返回 fanId -> updatedAt 的映射
*/
fun getFansUpdatedAtMap(fanIds: List<Long>, userId: Long): Map<Long, LocalDateTime> {
if (fanIds.isEmpty()) return emptyMap()
return database.from(UserFollows)
.select(UserFollows.userId, UserFollows.updatedAt)
.where { (UserFollows.userId inList fanIds) and (UserFollows.followUserId eq userId) }
.map { row -> row[UserFollows.userId]!! to row[UserFollows.updatedAt]!! }
.toMap()
}

/**
* 查询 userId 关注了 targetIds 中的哪些用户,返回被关注的 targetIds 子集
*/
fun getFollowedTargetIds(userId: Long, targetIds: List<Long>): Set<Long> {
if (targetIds.isEmpty()) return emptySet()
return database.from(UserFollows)
.select(UserFollows.followUserId)
.where { (UserFollows.userId eq userId) and (UserFollows.followUserId inList targetIds) }
.mapNotNull { it[UserFollows.followUserId] }
.toSet()
}

/**
* 查询 targetIds 中哪些用户关注了 userId,返回关注了 userId 的 targetIds 子集
*/
fun getFollowerTargetIds(targetIds: List<Long>, userId: Long): Set<Long> {
if (targetIds.isEmpty()) return emptySet()
return database.from(UserFollows)
.select(UserFollows.userId)
.where { (UserFollows.userId inList targetIds) and (UserFollows.followUserId eq userId) }
.mapNotNull { it[UserFollows.userId] }
.toSet()
}

fun isFollowing(userId: Long, followUserId: Long): Boolean {
return database.from(UserFollows).select(UserFollows.userId)
.where { (UserFollows.userId eq userId) and (UserFollows.followUserId eq followUserId) }
.limit(1)
.map { it[UserFollows.userId] }
.isNotEmpty()
}

override fun save(entity: UserEntity): UserEntity {
return if (isNewEntity(entity)) {
insertEntity(entity)
Expand Down
81 changes: 75 additions & 6 deletions src/main/kotlin/plus/maa/backend/service/UserService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import org.ktorm.entity.sortedBy
import org.ktorm.entity.take
import org.ktorm.entity.toList
import org.springframework.dao.DuplicateKeyException
import org.springframework.http.HttpStatus
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Service
import org.springframework.web.server.ResponseStatusException
import plus.maa.backend.common.MaaStatusCode
import plus.maa.backend.common.extensions.toMaaUser
import plus.maa.backend.controller.request.user.LoginDTO
Expand All @@ -24,6 +26,7 @@ import plus.maa.backend.controller.request.user.UserInfoUpdateDTO
import plus.maa.backend.controller.response.MaaResultException
import plus.maa.backend.controller.response.user.MaaLoginRsp
import plus.maa.backend.controller.response.user.MaaUserInfo
import plus.maa.backend.controller.response.user.RelationType
import plus.maa.backend.repository.entity.MaaUser
import plus.maa.backend.repository.entity.UserEntity
import plus.maa.backend.repository.entity.users
Expand Down Expand Up @@ -117,7 +120,7 @@ class UserService(
*/
fun register(registerDTO: RegisterDTO): MaaUserInfo {
val userName = registerDTO.userName.trim()
check(userName.length >= 4) { "用户名长度应在4-24位之间" }
check(userName.length in 4..24) { "用户名长度应在4-24位之间" }
check(!userKtormRepository.existsByUserName(userName)) {
"用户名已存在,请重新取个名字吧"
}
Expand All @@ -127,17 +130,17 @@ class UserService(

val encoded = passwordEncoder.encode(registerDTO.password)!!

val user = MaaUser(
val maaUser = MaaUser(
userName = userName,
email = registerDTO.email,
password = encoded,
status = 1,
pwdUpdateTime = Instant.now(),
)
return try {
val userEntity = userKtormRepository.createFromMaaUser(user)
userKtormRepository.save(userEntity)
MaaUserInfo(userEntity).also {
val entity = userKtormRepository.createFromMaaUser(maaUser)
userKtormRepository.save(entity)
MaaUserInfo(entity).also {
Cache.invalidateMaaUserById(it.id)
}
} catch (_: DuplicateKeyException) {
Expand All @@ -154,11 +157,11 @@ class UserService(
fun updateUserInfo(userId: Long, updateDTO: UserInfoUpdateDTO) {
val userEntity = userKtormRepository.findById(userId) ?: return
val newName = updateDTO.userName.trim()
check(newName.length >= 4) { "用户名长度应在4-24位之间" }
if (newName == userEntity.userName) {
// 暂时只支持修改用户名,如果有其他字段修改需要同步修改该逻辑
return
}
check(newName.length in 4..24) { "用户名长度应在4-24位之间" }
// 用户名需要trim
check(!userKtormRepository.existsByUserName(newName)) {
"用户名已存在,请重新取个名字吧"
Expand Down Expand Up @@ -277,6 +280,72 @@ class UserService(
.toList()
}

/**
* 获取当前登录用户信息
*/
fun getMe(userId: Long): MaaUserInfo {
val userEntity = userKtormRepository.findById(userId)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
return MaaUserInfo(userEntity)
}

/**
* 获取用户信息并附带与当前用户的关系
*/
fun getWithRelation(targetId: Long, currentUserId: Long?): MaaUserInfo {
val userEntity = userKtormRepository.findById(targetId)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
val base = MaaUserInfo(userEntity)
if (currentUserId == null) return base
val relation = resolveRelation(currentUserId, targetId)
return base.copy(relation = relation)
}

/**
* 批量获取用户信息(可选附带关系)
*/
fun getBatchUserInfos(ids: List<Long>, currentUserId: Long?): List<MaaUserInfo> {
if (ids.isEmpty()) return emptyList()
val users = userKtormRepository.findAllById(ids)
// 保证结果顺序与输入 ids 一致
val userMap = users.associateBy { it.userId }
if (currentUserId == null) {
return ids.mapNotNull { userMap[it] }.map { MaaUserInfo(it) }
}
// 当前用户关注了哪些目标
val iFollowIds = userKtormRepository.getFollowedTargetIds(currentUserId, ids)
// 哪些目标关注了当前用户
val theyFollowMeIds = userKtormRepository.getFollowerTargetIds(ids, currentUserId)
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
return ids.mapNotNull { userMap[it] }.map { user ->
val uid = user.userId
val iFollow = uid in iFollowIds
val theyFollow = uid in theyFollowMeIds
val relation = when {
uid == currentUserId -> RelationType.SELF
iFollow && theyFollow -> RelationType.MUTUAL
iFollow -> RelationType.FOLLOWING
theyFollow -> RelationType.FOLLOWED_BY
else -> RelationType.NONE
}
MaaUserInfo(user).copy(relation = relation)
}
}

/**
* 解析当前用户与目标用户的关系
*/
private fun resolveRelation(currentUserId: Long, targetUserId: Long): RelationType {
if (currentUserId == targetUserId) return RelationType.SELF
val iFollow = userKtormRepository.isFollowing(currentUserId, targetUserId)
val theyFollow = userKtormRepository.isFollowing(targetUserId, currentUserId)
return when {
iFollow && theyFollow -> RelationType.MUTUAL
iFollow -> RelationType.FOLLOWING
theyFollow -> RelationType.FOLLOWED_BY
else -> RelationType.NONE
}
}

@Suppress("unused")
private fun isAllChinese(input: String): Boolean {
return input.all { it in '\u4e00'..'\u9fa5' }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import org.springframework.stereotype.Service
import plus.maa.backend.common.extensions.paginate
import plus.maa.backend.common.extensions.toMaaUserInfo
import plus.maa.backend.controller.response.user.MaaUserInfo
import plus.maa.backend.controller.response.user.RelationType
import plus.maa.backend.repository.ktorm.UserKtormRepository
import java.time.ZoneId

@Service
class UserFollowService(
Expand All @@ -15,7 +17,7 @@ class UserFollowService(

fun follow(userId: Long, followUserId: Long) {
check(userId != followUserId) {
"不能关注自己哦~"
"不能关注自己哦"
}
val followUser = userKtormRepository.findById(followUserId)
check(followUser != null && followUser.status > 0) {
Expand All @@ -30,11 +32,35 @@ class UserFollowService(

fun getFollowingList(userId: Long, pageable: Pageable): PageImpl<MaaUserInfo> {
val res = userKtormRepository.follows(userId).paginate(pageable)
return PageImpl(res.map { it.toMaaUserInfo() }.toList(), pageable, res.totalElements)
val users = res.toList()
val targetIds = users.map { it.userId }
// 查询关注时间
val updatedAtMap = userKtormRepository.getFollowUpdatedAtMap(userId, targetIds)
// 查询哪些目标也关注了我(用于判断 MUTUAL)
val mutualIds = userKtormRepository.getFollowerTargetIds(targetIds, userId)
val enriched = users.map { user ->
val info = user.toMaaUserInfo()
val relation = if (user.userId in mutualIds) RelationType.MUTUAL else RelationType.FOLLOWING
val followedAt = updatedAtMap[user.userId]?.atZone(ZoneId.systemDefault())?.toInstant()
info.copy(relation = relation, followedAt = followedAt)
}
return PageImpl(enriched, pageable, res.totalElements)
}

fun getFansList(userId: Long, pageable: Pageable): PageImpl<MaaUserInfo> {
val res = userKtormRepository.fans(userId).paginate(pageable)
return PageImpl(res.map { it.toMaaUserInfo() }.toList(), pageable, res.totalElements)
val users = res.toList()
val fanIds = users.map { it.userId }
// 批量查询粉丝关注我的时间
val fanUpdatedAtMap = userKtormRepository.getFansUpdatedAtMap(fanIds, userId)
// 查询我关注了哪些粉丝(用于判断 MUTUAL)
val iFollowBackIds = userKtormRepository.getFollowedTargetIds(userId, fanIds)
val enriched = users.map { user ->
val info = user.toMaaUserInfo()
val relation = if (user.userId in iFollowBackIds) RelationType.MUTUAL else RelationType.FOLLOWED_BY
val followedAt = fanUpdatedAtMap[user.userId]?.atZone(ZoneId.systemDefault())?.toInstant()
info.copy(relation = relation, followedAt = followedAt)
}
return PageImpl(enriched, pageable, res.totalElements)
}
}