This commit is contained in:
chusan
2025-05-16 19:06:18 +08:00
parent 316a3e03e1
commit 2364f29b17
44 changed files with 854 additions and 1 deletions

View File

@@ -0,0 +1,29 @@
import ddd.application.DefaultUserService
import ddd.application.dto.ChangeUsername
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 main() {
val memoryUserRepository = MemoryUserRepository()
val userController = ddd.controller.UserController(
DefaultUserService(
memoryUserRepository,
EmailVerificationService(),
ExistsUserDomainService(memoryUserRepository)
)
)
val changeUsername = userController.changeUsername(ChangeUsername(1L, "nian", "po",""))
println(changeUsername)
}

View File

@@ -0,0 +1,27 @@
package ddd.application
import ddd.application.dto.ChangeUsername
import ddd.domain.User
import ddd.domain.port.VerificationService
import ddd.domain.repository.UserRepository
import ddd.domain.service.ExistsUserDomainService
import ddd.domain.valueobject.Username
import shared.exceptions.NotFoundException
class DefaultUserService(
val userRepository: UserRepository,
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")
user.changeUsername(
Username(userDto.firstName, userDto.lastName),
userDto.verificationCode,
verificationService,
existsUserDomainService
)
userRepository.save(user)
return user
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,74 @@
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.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 shared.exceptions.ChangeUsernameException
import shared.validation.ValidationChain
import java.time.Clock
import java.time.LocalDateTime
class User(
val id: UserId,
var username: Username,
var status: UserStatusEnum,
var lastUsernameChange: LocalDateTime?,
var rank: UserRank,
) {
// 领域方法:修改用户名(入口点)
fun changeUsername(
newUsername: Username,
verificationCode: String,
verificationService: VerificationService,
existsUserService: ExistsUserDomainService,
clock: Clock = Clock.systemDefaultZone()
) {
validateState()
validateChangeUsername(newUsername, verificationCode, verificationService, existsUserService, clock)
executeUsernameChange(newUsername, clock)
}
private fun validateState() {
if (status != UserStatusEnum.ACTIVE) {
throw ChangeUsernameException("用户未激活")
}
}
private fun validateChangeUsername(
newUsername: Username,
verificationCode: String,
verificationService: VerificationService,
existsUserService: ExistsUserDomainService,
clock: Clock
) {
val context = UsernameChangeContext(
user = this,
newUsername = newUsername,
verificationCode = verificationCode,
clock = clock,
verificationService = verificationService,
existsUserService = existsUserService
)
// 组合验证规则(责任链模式)
ValidationChain<UsernameChangeContext>()
.add(EmailVerificationValidation())
.add(RankPolicyValidation())
.add(TimeIntervalValidation())
.add(ExistsUsernameValidation())
.validate(context)
}
private fun executeUsernameChange(newUsername: Username, clock: Clock) {
username = newUsername
lastUsernameChange = LocalDateTime.now(clock)
}
}

View File

@@ -0,0 +1,11 @@
package ddd.domain.entity
import ddd.domain.User
class InternalUserRankPolicy : UserRankPolicy {
override fun canChangeUsername(user: User) = true
override fun requiresEmailVerification() = false
override fun getMaxChangeIntervalDays() = 0
}

View File

@@ -0,0 +1,10 @@
package ddd.domain.entity
import ddd.domain.User
class RegularUserRankPolicy : UserRankPolicy {
override fun canChangeUsername(user: User) = false
override fun requiresEmailVerification() = false
override fun getMaxChangeIntervalDays() = Int.MAX_VALUE
}

View File

@@ -0,0 +1,32 @@
package ddd.domain.entity
import ddd.domain.valueobject.UserRankEnum
sealed interface UserRank {
val value: UserRankEnum
val policy: UserRankPolicy
class RegularUserRank() : UserRank {
override val value: UserRankEnum = UserRankEnum.REGULAR
override val policy = RegularUserRankPolicy()
override fun toString(): String {
return value.toString()
}
}
class VipUserRank() : UserRank {
override val value: UserRankEnum = UserRankEnum.VIP
override val policy = VipUserRankPolicy()
override fun toString(): String {
return value.toString()
}
}
class InternalUserRank() : UserRank {
override val value: UserRankEnum = UserRankEnum.INTERNAL
override val policy = InternalUserRankPolicy()
override fun toString(): String {
return value.toString()
}
}
}

View File

@@ -0,0 +1,9 @@
package ddd.domain.entity
import ddd.domain.User
interface UserRankPolicy {
fun canChangeUsername(user: User): Boolean
fun requiresEmailVerification(): Boolean
fun getMaxChangeIntervalDays(): Int
}

View File

@@ -0,0 +1,9 @@
package ddd.domain.entity
import ddd.domain.User
class VipUserRankPolicy : UserRankPolicy {
override fun canChangeUsername(user: User) = true
override fun requiresEmailVerification() = true
override fun getMaxChangeIntervalDays() = 30
}

View File

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

View File

@@ -0,0 +1,14 @@
package ddd.domain.repository
import ddd.domain.User
import ddd.domain.valueobject.Username
interface UserRepository {
fun save(user: User)
fun findById(id: Long): User?
fun findByUsername(username: Username): User?
fun update(user: User)
}

View File

@@ -0,0 +1,10 @@
package ddd.domain.service
import ddd.domain.repository.UserRepository
import ddd.domain.valueobject.Username
class ExistsUserDomainService(val userRepository: UserRepository) {
fun existsByUsername(username: Username): Boolean{
return userRepository.findByUsername(username) != null
}
}

View File

@@ -0,0 +1,17 @@
package ddd.domain.validation.changeUsername
import shared.exceptions.ChangeUsernameException
import shared.validation.AbstractValidationHandler
class EmailVerificationValidation : AbstractValidationHandler<UsernameChangeContext>() {
override fun validate(context: UsernameChangeContext) {
if(context.user.rank.policy.requiresEmailVerification()){
val emailVerified = context.verificationService.isVerified(context.user.id, context.verificationCode)
if (emailVerified) {
throw ChangeUsernameException("验证码错误")
}
}
next(context)
}
}

View File

@@ -0,0 +1,13 @@
package ddd.domain.validation.changeUsername
import shared.exceptions.ChangeUsernameException
import shared.validation.AbstractValidationHandler
class ExistsUsernameValidation : AbstractValidationHandler<UsernameChangeContext>() {
override fun validate(context: UsernameChangeContext) {
require(!context.existsUserService.existsByUsername(context.newUsername)) {
throw ChangeUsernameException("用户名已存在")
}
next(context)
}
}

View File

@@ -0,0 +1,13 @@
package ddd.domain.validation.changeUsername
import shared.exceptions.ChangeUsernameException
import shared.validation.AbstractValidationHandler
class RankPolicyValidation : AbstractValidationHandler<UsernameChangeContext>() {
override fun validate(context: UsernameChangeContext) {
require (context.user.rank.policy.canChangeUsername(context.user)) {
throw ChangeUsernameException("用户${context.user.id}没有权限修改用户名")
}
next(context)
}
}

View File

@@ -0,0 +1,21 @@
package ddd.domain.validation.changeUsername
import shared.exceptions.ChangeUsernameException
import shared.validation.AbstractValidationHandler
import java.time.Duration
import java.time.LocalDateTime
class TimeIntervalValidation : AbstractValidationHandler<UsernameChangeContext>() {
override fun validate(context: UsernameChangeContext) {
val lastChange = context.user.lastUsernameChange
val policy = context.user.rank.policy
lastChange?.let {
val daysBetween = Duration.between(it, LocalDateTime.now(context.clock)).toDays()
if (daysBetween < policy.getMaxChangeIntervalDays()) {
throw ChangeUsernameException("30天内禁止重复修改")
}
}
next(context) // 传递至下一个验证器
}
}

View File

@@ -0,0 +1,16 @@
package ddd.domain.validation.changeUsername
import ddd.domain.User
import ddd.domain.port.VerificationService
import ddd.domain.service.ExistsUserDomainService
import ddd.domain.valueobject.Username
import java.time.Clock
data class UsernameChangeContext(
val user: User,
val newUsername: Username,
val verificationCode: String,
val clock: Clock,
val verificationService: VerificationService,
val existsUserService: ExistsUserDomainService
)

View File

@@ -0,0 +1,3 @@
package ddd.domain.valueobject
data class UserId(val value: Long)

View File

@@ -0,0 +1,5 @@
package ddd.domain.valueobject
enum class UserRankEnum {
REGULAR, VIP, INTERNAL
}

View File

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

View File

@@ -0,0 +1,14 @@
package ddd.domain.valueobject
data class Username(val firstName: String, val lastName: String) {
override fun toString(): String {
return "$firstName $lastName"
}
val regex = Regex("^[a-zA-Z0-9_]+$")
init {
require(toString().length in 4..20) { "用户名长度需在4-20之间" }
require(regex.matches(firstName) && regex.matches(lastName)) { "包含非法字符" }
}
}

View File

@@ -0,0 +1,40 @@
package ddd.infrastructure.repository
import ddd.domain.User
import ddd.domain.entity.UserRank
import ddd.domain.repository.UserRepository
import ddd.domain.valueobject.UserId
import ddd.domain.valueobject.UserStatusEnum
import ddd.domain.valueobject.Username
class MemoryUserRepository : UserRepository {
val users = mutableMapOf<Long, User>()
init {
save(
User(
UserId(1L),
Username("nian", "chen"),
UserStatusEnum.ACTIVE,
null,
UserRank.RegularUserRank()
)
)
}
override fun save(user: User) {
users += user.id.value to user
}
override fun findById(id: Long): User? {
return users[id]
}
override fun findByUsername(username: Username): User? {
return users.entries.find { it.value.username == username }?.value
}
override fun update(user: User) {
users[user.id.value] = user
}
}

View File

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

View File

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

View File

@@ -0,0 +1,23 @@
package mvc.dao
import mvc.entities.User
class UserRepository {
val users = mutableMapOf<Long, User>()
init {
add(User(1L, "nian", "chen"))
}
fun add(user: User) {
users += user.id to user
}
fun findById(id: Long): User? {
return users[id]
}
fun update(user: User) {
users[user.id] = user
}
}

View File

@@ -0,0 +1,7 @@
package mvc.entities
data class User (
val id: Long,
val firstName: String,
val lastName: String
)

View File

@@ -0,0 +1,18 @@
package mvc.services
import shared.exceptions.NotFoundException
import mvc.dao.UserRepository
import mvc.entities.User
class UserComplexService(
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
}
}

View File

@@ -0,0 +1,7 @@
package mvc.services
import mvc.entities.User
interface UserService {
fun changeUsername(user: User): User
}

View File

@@ -0,0 +1,14 @@
package mvc.services
import shared.exceptions.NotFoundException
import mvc.dao.UserRepository
import mvc.entities.User
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
}
}

View File

@@ -0,0 +1,4 @@
package shared.exceptions
class ChangeUsernameException(msg: String) : RuntimeException(msg) {
}

View File

@@ -0,0 +1,4 @@
package shared.exceptions
class NotFoundException(msg: String) : RuntimeException(msg) {
}

View File

@@ -0,0 +1,15 @@
package shared.validation
abstract class AbstractValidationHandler<T> : ValidationHandler<T> {
private lateinit var nextHandler: ValidationHandler<T>
override fun setNext(handler: ValidationHandler<T>) {
nextHandler = handler
}
protected fun next(context: T) {
if (::nextHandler.isInitialized) {
nextHandler.validate(context)
}
}
}

View File

@@ -0,0 +1,22 @@
package shared.validation
class ValidationChain<T> {
private var firstHandler: ValidationHandler<T>? = null
private var lastHandler: ValidationHandler<T>? = null
fun add(handler: ValidationHandler<T>): ValidationChain<T> {
if (firstHandler == null) {
firstHandler = handler
lastHandler = handler
} else {
lastHandler?.setNext(handler)
lastHandler = handler
}
return this
}
fun validate(context: T) {
firstHandler?.validate(context)
?: throw IllegalStateException("验证链未初始化")
}
}

View File

@@ -0,0 +1,6 @@
package shared.validation
interface ValidationHandler<T> {
fun validate(context: T)
fun setNext(handler: ValidationHandler<T>)
}