This commit is contained in:
xueque
2025-05-16 23:03:47 +08:00
parent 069bbede0e
commit 70c56f8192
25 changed files with 186 additions and 66 deletions

View File

@@ -1,22 +1,19 @@
import ddd.application.DefaultUserService import ddd.application.DefaultUserService
import ddd.application.dto.ChangeUsername import shared.dto.ChangeUsernameDto
import ddd.domain.service.ExistsUserDomainService import ddd.domain.service.ExistsUserDomainService
import ddd.infrastructure.repository.MemoryUserRepository import ddd.infrastructure.repository.MemoryUserRepository
import ddd.infrastructure.verification.EmailVerificationService import ddd.infrastructure.verification.EmailVerificationService
import mvc.controllers.UserController
import mvc.dao.UserRepository
import mvc.entities.User
import mvc.services.UserSimpleService
//fun main() { fun mvc() {
// val controller = UserController(UserSimpleService(UserRepository())) val controller = mvc.controllers.UserController(mvc.services.UserComplexService(mvc.dao.UserRepository()))
// val changeUsername = controller.changeUsername(User(1L, "nian", "3")) val changeUsername1 = controller.changeUsername(ChangeUsernameDto(1L, "nian", "abc","1234"))
// println(changeUsername) println(changeUsername1)
//} val changeUsername2 = controller.changeUsername(ChangeUsernameDto(1L, "nian", "def","1234"))
println(changeUsername2)
}
fun main() { fun ddd() {
val memoryUserRepository = MemoryUserRepository() val memoryUserRepository = MemoryUserRepository()
val userController = ddd.controller.UserController( val userController = ddd.controller.UserController(
DefaultUserService( DefaultUserService(
memoryUserRepository, memoryUserRepository,
@@ -24,6 +21,12 @@ fun main() {
ExistsUserDomainService(memoryUserRepository) ExistsUserDomainService(memoryUserRepository)
) )
) )
val changeUsername = userController.changeUsername(ChangeUsername(1L, "nian", "po","")) val changeUsername1 = userController.changeUsername(ChangeUsernameDto(1L, "nian", "po","1234"))
println(changeUsername) println(changeUsername1)
val changeUsername2 = userController.changeUsername(ChangeUsernameDto(1L, "nian", "po1","1234"))
println(changeUsername2)
}
fun main(){
mvc()
} }

View File

@@ -1,8 +1,8 @@
package ddd.application package ddd.application
import ddd.application.dto.ChangeUsername import shared.dto.ChangeUsernameDto
import ddd.domain.User import ddd.domain.User
import ddd.domain.port.VerificationService import ddd.domain.adapter.VerificationService
import ddd.domain.repository.UserRepository import ddd.domain.repository.UserRepository
import ddd.domain.service.ExistsUserDomainService import ddd.domain.service.ExistsUserDomainService
import ddd.domain.valueobject.Username import ddd.domain.valueobject.Username
@@ -13,14 +13,16 @@ class DefaultUserService(
val verificationService: VerificationService, val verificationService: VerificationService,
val existsUserDomainService: ExistsUserDomainService, val existsUserDomainService: ExistsUserDomainService,
) : UserService { ) : UserService {
override fun changeUsername(userDto: ChangeUsername): User { override fun changeUsername(userDto: ChangeUsernameDto): User {
val user = userRepository.findById(userDto.id) ?: throw NotFoundException("User with id ${userDto.id} not found") val user = userRepository.findById(userDto.id) ?: throw NotFoundException("用户${userDto.id}不存在")
user.changeUsername( user.changeUsername(
Username(userDto.firstName, userDto.lastName), Username(userDto.firstName, userDto.lastName),
userDto.verificationCode, userDto.verificationCode,
verificationService, verificationService,
existsUserDomainService existsUserDomainService
) )
userRepository.save(user) userRepository.save(user)
return user return user
} }

View File

@@ -1,8 +1,8 @@
package ddd.application package ddd.application
import ddd.application.dto.ChangeUsername import shared.dto.ChangeUsernameDto
import ddd.domain.User import ddd.domain.User
interface UserService { interface UserService {
fun changeUsername(userDto: ChangeUsername): User fun changeUsername(userDto: ChangeUsernameDto): User
} }

View File

@@ -1,3 +0,0 @@
package ddd.application.dto
data class ChangeUsername(val id: Long ,val firstName: String, val lastName: String, val verificationCode: String)

View File

@@ -1,9 +1,9 @@
package ddd.controller package ddd.controller
import ddd.application.UserService import ddd.application.UserService
import ddd.application.dto.ChangeUsername import shared.dto.ChangeUsernameDto
class UserController(val service: UserService) { class UserController(val service: UserService) {
fun changeUsername(changeUsername: ChangeUsername) = service.changeUsername(changeUsername) fun changeUsername(changeUsername: ChangeUsernameDto) = service.changeUsername(changeUsername)
} }

View File

@@ -2,27 +2,28 @@ package ddd.domain
import ddd.domain.entity.UserRank import ddd.domain.entity.UserRank
import ddd.domain.valueobject.UserId import ddd.domain.valueobject.UserId
import ddd.domain.valueobject.UserStatusEnum
import ddd.domain.valueobject.Username import ddd.domain.valueobject.Username
import ddd.domain.port.VerificationService import ddd.domain.adapter.VerificationService
import ddd.domain.service.ExistsUserDomainService import ddd.domain.service.ExistsUserDomainService
import ddd.domain.validation.changeUsername.EmailVerificationValidation import ddd.domain.validation.changeUsername.EmailVerificationValidation
import ddd.domain.validation.changeUsername.ExistsUsernameValidation import ddd.domain.validation.changeUsername.ExistsUsernameValidation
import ddd.domain.validation.changeUsername.RankPolicyValidation import ddd.domain.validation.changeUsername.RankPolicyValidation
import ddd.domain.validation.changeUsername.TimeIntervalValidation import ddd.domain.validation.changeUsername.TimeIntervalValidation
import ddd.domain.validation.changeUsername.UsernameChangeContext import ddd.domain.validation.changeUsername.UsernameChangeContext
import ddd.domain.valueobject.UserStatusEnum
import shared.AggregateRoot
import shared.exceptions.ChangeUsernameException import shared.exceptions.ChangeUsernameException
import shared.validation.ValidationChain import shared.validation.ValidationChain
import java.time.Clock import java.time.Clock
import java.time.LocalDateTime import java.time.LocalDateTime
class User( class User(
val id: UserId, override val id: UserId,
var username: Username, var username: Username,
var status: UserStatusEnum, var status: UserStatusEnum,
var lastUsernameChange: LocalDateTime?, var lastUsernameChange: LocalDateTime?,
var rank: UserRank, var rank: UserRank,
) { ) : AggregateRoot<UserId>() {
// 领域方法:修改用户名(入口点) // 领域方法:修改用户名(入口点)
fun changeUsername( fun changeUsername(
newUsername: Username, newUsername: Username,
@@ -32,7 +33,7 @@ class User(
clock: Clock = Clock.systemDefaultZone() clock: Clock = Clock.systemDefaultZone()
) { ) {
validateState() validateState()
validateChangeUsername(newUsername, verificationCode, verificationService, existsUserService, clock) validateUsername(newUsername, verificationCode, verificationService, existsUserService, clock)
executeUsernameChange(newUsername, clock) executeUsernameChange(newUsername, clock)
} }
@@ -42,7 +43,7 @@ class User(
} }
} }
private fun validateChangeUsername( private fun validateUsername(
newUsername: Username, newUsername: Username,
verificationCode: String, verificationCode: String,
verificationService: VerificationService, verificationService: VerificationService,
@@ -60,8 +61,8 @@ class User(
// 组合验证规则(责任链模式) // 组合验证规则(责任链模式)
ValidationChain<UsernameChangeContext>() ValidationChain<UsernameChangeContext>()
.add(EmailVerificationValidation())
.add(RankPolicyValidation()) .add(RankPolicyValidation())
.add(EmailVerificationValidation())
.add(TimeIntervalValidation()) .add(TimeIntervalValidation())
.add(ExistsUsernameValidation()) .add(ExistsUsernameValidation())
.validate(context) .validate(context)
@@ -71,4 +72,8 @@ class User(
username = newUsername username = newUsername
lastUsernameChange = LocalDateTime.now(clock) lastUsernameChange = LocalDateTime.now(clock)
} }
override fun toString(): String {
return "User(id=$id, username=$username, status=$status, lastUsernameChange=$lastUsernameChange, rank=${rank.value})"
}
} }

View File

@@ -1,8 +1,8 @@
package ddd.domain.port package ddd.domain.adapter
import ddd.domain.valueobject.UserId import ddd.domain.valueobject.UserId
interface VerificationService { interface VerificationService {
fun isVerified(userId: UserId, code: String): Boolean fun isVerified(userId: UserId, code: String): Boolean
fun sendVerificationCode(userId: UserId) fun sendVerificationCode(userId: UserId)
} }

View File

@@ -4,8 +4,6 @@ import ddd.domain.User
class InternalUserRankPolicy : UserRankPolicy { class InternalUserRankPolicy : UserRankPolicy {
override fun canChangeUsername(user: User) = true override fun canChangeUsername(user: User) = true
override fun requiresEmailVerification() = false override fun requiresEmailVerification() = false
override fun getMaxChangeIntervalDays() = 0 override fun getMaxChangeIntervalDays() = 0
} }

View File

@@ -7,7 +7,7 @@ class EmailVerificationValidation : AbstractValidationHandler<UsernameChangeCont
override fun validate(context: UsernameChangeContext) { override fun validate(context: UsernameChangeContext) {
if(context.user.rank.policy.requiresEmailVerification()){ if(context.user.rank.policy.requiresEmailVerification()){
val emailVerified = context.verificationService.isVerified(context.user.id, context.verificationCode) val emailVerified = context.verificationService.isVerified(context.user.id, context.verificationCode)
if (emailVerified) { if (!emailVerified) {
throw ChangeUsernameException("验证码错误") throw ChangeUsernameException("验证码错误")
} }
} }

View File

@@ -5,6 +5,9 @@ import shared.validation.AbstractValidationHandler
class ExistsUsernameValidation : AbstractValidationHandler<UsernameChangeContext>() { class ExistsUsernameValidation : AbstractValidationHandler<UsernameChangeContext>() {
override fun validate(context: UsernameChangeContext) { override fun validate(context: UsernameChangeContext) {
check(context.user.username != context.newUsername){
throw ChangeUsernameException("不能修改为当前用户名")
}
require(!context.existsUserService.existsByUsername(context.newUsername)) { require(!context.existsUserService.existsByUsername(context.newUsername)) {
throw ChangeUsernameException("用户名已存在") throw ChangeUsernameException("用户名已存在")
} }

View File

@@ -1,7 +1,7 @@
package ddd.domain.validation.changeUsername package ddd.domain.validation.changeUsername
import ddd.domain.User import ddd.domain.User
import ddd.domain.port.VerificationService import ddd.domain.adapter.VerificationService
import ddd.domain.service.ExistsUserDomainService import ddd.domain.service.ExistsUserDomainService
import ddd.domain.valueobject.Username import ddd.domain.valueobject.Username
import java.time.Clock import java.time.Clock

View File

@@ -1,5 +1,5 @@
package ddd.domain.valueobject; package ddd.domain.valueobject
public enum UserStatusEnum { enum class UserStatusEnum {
ACTIVE,DEACTIVATED,BANNED ACTIVE,DEACTIVATED,BANNED
} }

View File

@@ -17,7 +17,7 @@ class MemoryUserRepository : UserRepository {
Username("nian", "chen"), Username("nian", "chen"),
UserStatusEnum.ACTIVE, UserStatusEnum.ACTIVE,
null, null,
UserRank.RegularUserRank() UserRank.VipUserRank()
) )
) )
} }

View File

@@ -1,9 +1,9 @@
package ddd.infrastructure.verification package ddd.infrastructure.verification
import ddd.domain.port.VerificationService import ddd.domain.adapter.VerificationService
import ddd.domain.valueobject.UserId import ddd.domain.valueobject.UserId
class EmailVerificationService : VerificationService { class EmailVerificationService : VerificationService {
override fun isVerified(userId: UserId, code: String) = true override fun isVerified(userId: UserId, code: String) = code == "1234"
override fun sendVerificationCode(userId: UserId) = println("sending verification email") override fun sendVerificationCode(userId: UserId) = println("sending verification email")
} }

View File

@@ -1,8 +1,8 @@
package mvc.controllers package mvc.controllers
import mvc.entities.User
import mvc.services.UserService import mvc.services.UserService
import shared.dto.ChangeUsernameDto
class UserController(val service: UserService) { class UserController(val service: UserService) {
fun changeUsername(user: User) = service.changeUsername(user) fun changeUsername(userDto: ChangeUsernameDto) = service.changeUsername(userDto)
} }

View File

@@ -1,12 +1,14 @@
package mvc.dao package mvc.dao
import mvc.entities.User import mvc.entities.User
import mvc.entities.UserRankEnum
import mvc.entities.UserStatusEnum
class UserRepository { class UserRepository {
val users = mutableMapOf<Long, User>() val users = mutableMapOf<Long, User>()
init { init {
add(User(1L, "nian", "chen")) add(User(1L, "nian", "chen", UserStatusEnum.ACTIVE, null, UserRankEnum.VIP))
} }
fun add(user: User) { fun add(user: User) {
@@ -20,4 +22,8 @@ class UserRepository {
fun update(user: User) { fun update(user: User) {
users[user.id] = user users[user.id] = user
} }
fun findByUsername(firstName: String, lastName: String): User? {
return users.entries.find { it.value.firstName == firstName && it.value.lastName == lastName }?.value
}
} }

View File

@@ -1,7 +1,12 @@
package mvc.entities package mvc.entities
import java.time.LocalDateTime
data class User ( data class User (
val id: Long, val id: Long,
val firstName: String, var firstName: String,
val lastName: String var lastName: String,
var status: UserStatusEnum,
var lastUsernameChange: LocalDateTime?,
var rank: UserRankEnum,
) )

View File

@@ -0,0 +1,5 @@
package mvc.entities
enum class UserRankEnum {
REGULAR, VIP, INTERNAL
}

View File

@@ -0,0 +1,5 @@
package mvc.entities
enum class UserStatusEnum {
ACTIVE,DEACTIVATED,BANNED
}

View File

@@ -3,16 +3,89 @@ package mvc.services
import shared.exceptions.NotFoundException import shared.exceptions.NotFoundException
import mvc.dao.UserRepository import mvc.dao.UserRepository
import mvc.entities.User import mvc.entities.User
import mvc.entities.UserRankEnum
import mvc.entities.UserStatusEnum
import shared.dto.ChangeUsernameDto
import shared.exceptions.ChangeUsernameException
import java.time.Clock
import java.time.Duration
import java.time.LocalDateTime
class UserComplexService( class UserComplexService(
val repository: UserRepository val repository: UserRepository,
) : UserService { ) : UserService {
// 用户 // 用户
override fun changeUsername(user: User): User { override fun changeUsername(userDto: ChangeUsernameDto): User {
val findUser = repository.findById(user.id) ?: throw NotFoundException("User ${user.id} not found") val user = repository.findById(userDto.id) ?: throw NotFoundException("用户${userDto.id}不存在")
val copy = findUser.copy(firstName = user.firstName, lastName = user.lastName) // 判断用户状态
repository.update(copy) if (user.status != UserStatusEnum.ACTIVE) throw ChangeUsernameException("用户未激活")
return copy // 用户名长度
if( !(user.firstName.length + user.lastName.length in 4..20)) {
throw ChangeUsernameException("用户名长度需在4-20之间")
}
// 检测非法字符
val regex = Regex("^[a-zA-Z0-9_]+$")
if(!regex.matches(userDto.firstName) || !regex.matches(userDto.lastName)) {
throw ChangeUsernameException("包含非法字符")
}
// 检测权限
var canChangeUsername = false
var requiresEmailVerification = false
var maxChangeIntervalDays = 0
when (user.rank) {
UserRankEnum.REGULAR -> {
canChangeUsername = false
requiresEmailVerification = false
maxChangeIntervalDays = 0
}
UserRankEnum.VIP -> {
canChangeUsername = true
requiresEmailVerification = true
maxChangeIntervalDays = 30
}
UserRankEnum.INTERNAL -> {
canChangeUsername = true
requiresEmailVerification = false
maxChangeIntervalDays = 0
}
}
if (!canChangeUsername) throw ChangeUsernameException("用户没有权限修改用户名")
if (requiresEmailVerification) {
val emailVerified = VerificationService().isVerified(user.id, userDto.verificationCode)
if (!emailVerified) {
throw ChangeUsernameException("验证码错误")
}
}
user.lastUsernameChange?.let{
val daysBetween =
Duration.between(user.lastUsernameChange, LocalDateTime.now(Clock.systemDefaultZone())).toDays()
if (daysBetween < maxChangeIntervalDays) {
throw ChangeUsernameException("30天内禁止重复修改")
}
}
// 检测相同名
if(user.firstName == userDto.firstName && user.lastName == userDto.lastName) {
throw ChangeUsernameException("不能修改为当前用户名")
}
val findByUsername = repository.findByUsername(userDto.firstName, userDto.lastName)
if(findByUsername != null) {
throw ChangeUsernameException("用户名已存在")
}
user.firstName = userDto.firstName
user.lastName = userDto.lastName
user.lastUsernameChange = LocalDateTime.now(Clock.systemDefaultZone())
repository.update(user)
return user
} }
} }

View File

@@ -1,7 +1,8 @@
package mvc.services package mvc.services
import mvc.entities.User import mvc.entities.User
import shared.dto.ChangeUsernameDto
interface UserService { interface UserService {
fun changeUsername(user: User): User fun changeUsername(userDto: ChangeUsernameDto): User
} }

View File

@@ -3,12 +3,14 @@ package mvc.services
import shared.exceptions.NotFoundException import shared.exceptions.NotFoundException
import mvc.dao.UserRepository import mvc.dao.UserRepository
import mvc.entities.User import mvc.entities.User
import shared.dto.ChangeUsernameDto
class UserSimpleService(val repository: UserRepository) : UserService { class UserSimpleService(val repository: UserRepository) : UserService {
override fun changeUsername(user: User): User { override fun changeUsername(userDto: ChangeUsernameDto): User {
val findUser = repository.findById(user.id) ?: throw NotFoundException("User ${user.id} not found") val findUser = repository.findById(userDto.id) ?: throw NotFoundException("用户${userDto.id}不存在")
val copy = findUser.copy(firstName = user.firstName, lastName = user.lastName) findUser.firstName = userDto.firstName
repository.update(copy) findUser.lastName = userDto.lastName
return copy repository.update(findUser)
return findUser
} }
} }

View File

@@ -0,0 +1,7 @@
package mvc.services
class VerificationService {
fun isVerified(userId: Long, code: String): Boolean {
return "1234" == code
}
}

View File

@@ -0,0 +1,5 @@
package shared
abstract class AggregateRoot<T>{
abstract val id: T
}

View File

@@ -0,0 +1,3 @@
package shared.dto
data class ChangeUsernameDto(val id: Long, val firstName: String, val lastName: String, val verificationCode: String)