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.dto.ChangeUsername
import shared.dto.ChangeUsernameDto
import ddd.domain.service.ExistsUserDomainService
import ddd.infrastructure.repository.MemoryUserRepository
import ddd.infrastructure.verification.EmailVerificationService
import mvc.controllers.UserController
import mvc.dao.UserRepository
import mvc.entities.User
import mvc.services.UserSimpleService
//fun main() {
// val controller = UserController(UserSimpleService(UserRepository()))
// val changeUsername = controller.changeUsername(User(1L, "nian", "3"))
// println(changeUsername)
//}
fun mvc() {
val controller = mvc.controllers.UserController(mvc.services.UserComplexService(mvc.dao.UserRepository()))
val changeUsername1 = controller.changeUsername(ChangeUsernameDto(1L, "nian", "abc","1234"))
println(changeUsername1)
val changeUsername2 = controller.changeUsername(ChangeUsernameDto(1L, "nian", "def","1234"))
println(changeUsername2)
}
fun main() {
fun ddd() {
val memoryUserRepository = MemoryUserRepository()
val userController = ddd.controller.UserController(
DefaultUserService(
memoryUserRepository,
@@ -24,6 +21,12 @@ fun main() {
ExistsUserDomainService(memoryUserRepository)
)
)
val changeUsername = userController.changeUsername(ChangeUsername(1L, "nian", "po",""))
println(changeUsername)
val changeUsername1 = userController.changeUsername(ChangeUsernameDto(1L, "nian", "po","1234"))
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
import ddd.application.dto.ChangeUsername
import shared.dto.ChangeUsernameDto
import ddd.domain.User
import ddd.domain.port.VerificationService
import ddd.domain.adapter.VerificationService
import ddd.domain.repository.UserRepository
import ddd.domain.service.ExistsUserDomainService
import ddd.domain.valueobject.Username
@@ -13,14 +13,16 @@ class DefaultUserService(
val verificationService: VerificationService,
val existsUserDomainService: ExistsUserDomainService,
) : UserService {
override fun changeUsername(userDto: ChangeUsername): User {
val user = userRepository.findById(userDto.id) ?: throw NotFoundException("User with id ${userDto.id} not found")
override fun changeUsername(userDto: ChangeUsernameDto): User {
val user = userRepository.findById(userDto.id) ?: throw NotFoundException("用户${userDto.id}不存在")
user.changeUsername(
Username(userDto.firstName, userDto.lastName),
userDto.verificationCode,
verificationService,
existsUserDomainService
)
userRepository.save(user)
return user
}

View File

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

View File

@@ -1,4 +1,4 @@
package ddd.domain.port
package ddd.domain.adapter
import ddd.domain.valueobject.UserId

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
package ddd.domain.validation.changeUsername
import ddd.domain.User
import ddd.domain.port.VerificationService
import ddd.domain.adapter.VerificationService
import ddd.domain.service.ExistsUserDomainService
import ddd.domain.valueobject.Username
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
}

View File

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

View File

@@ -1,9 +1,9 @@
package ddd.infrastructure.verification
import ddd.domain.port.VerificationService
import ddd.domain.adapter.VerificationService
import ddd.domain.valueobject.UserId
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")
}

View File

@@ -1,8 +1,8 @@
package mvc.controllers
import mvc.entities.User
import mvc.services.UserService
import shared.dto.ChangeUsernameDto
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
import mvc.entities.User
import mvc.entities.UserRankEnum
import mvc.entities.UserStatusEnum
class UserRepository {
val users = mutableMapOf<Long, User>()
init {
add(User(1L, "nian", "chen"))
add(User(1L, "nian", "chen", UserStatusEnum.ACTIVE, null, UserRankEnum.VIP))
}
fun add(user: User) {
@@ -20,4 +22,8 @@ class UserRepository {
fun update(user: 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
import java.time.LocalDateTime
data class User (
val id: Long,
val firstName: String,
val lastName: String
var firstName: 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 mvc.dao.UserRepository
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(
val repository: UserRepository
val repository: UserRepository,
) : UserService {
// 用户
override fun changeUsername(user: User): User {
val findUser = repository.findById(user.id) ?: throw NotFoundException("User ${user.id} not found")
val copy = findUser.copy(firstName = user.firstName, lastName = user.lastName)
repository.update(copy)
return copy
override fun changeUsername(userDto: ChangeUsernameDto): User {
val user = repository.findById(userDto.id) ?: throw NotFoundException("用户${userDto.id}不存在")
// 判断用户状态
if (user.status != UserStatusEnum.ACTIVE) throw ChangeUsernameException("用户未激活")
// 用户名长度
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
import mvc.entities.User
import shared.dto.ChangeUsernameDto
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 mvc.dao.UserRepository
import mvc.entities.User
import shared.dto.ChangeUsernameDto
class UserSimpleService(val repository: UserRepository) : UserService {
override fun changeUsername(user: User): User {
val findUser = repository.findById(user.id) ?: throw NotFoundException("User ${user.id} not found")
val copy = findUser.copy(firstName = user.firstName, lastName = user.lastName)
repository.update(copy)
return copy
override fun changeUsername(userDto: ChangeUsernameDto): User {
val findUser = repository.findById(userDto.id) ?: throw NotFoundException("用户${userDto.id}不存在")
findUser.firstName = userDto.firstName
findUser.lastName = userDto.lastName
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)