From 2364f29b1776f9293d568ae1f4fb46d0494ea61e Mon Sep 17 00:00:00 2001 From: chusan Date: Fri, 16 May 2025 19:06:18 +0800 Subject: [PATCH] init --- .gitignore | 3 +- build.gradle.kts | 22 +++ gradle.properties | 12 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 56921 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 176 ++++++++++++++++++ gradlew.bat | 84 +++++++++ settings.gradle.kts | 16 ++ src/main/kotlin/Program.kt | 29 +++ .../ddd/application/DefaultUserService.kt | 27 +++ .../kotlin/ddd/application/UserService.kt | 8 + .../ddd/application/dto/ChangeUsername.kt | 3 + .../kotlin/ddd/controller/UserController.kt | 9 + src/main/kotlin/ddd/domain/User.kt | 74 ++++++++ .../domain/entity/InternalUserRankPolicy.kt | 11 ++ .../domain/entity/RegularUserRankPolicy.kt | 10 + src/main/kotlin/ddd/domain/entity/UserRank.kt | 32 ++++ .../ddd/domain/entity/UserRankPolicy.kt | 9 + .../ddd/domain/entity/VipUserRankPolicy.kt | 9 + .../ddd/domain/port/VerificationService.kt | 8 + .../ddd/domain/repository/UserRepository.kt | 14 ++ .../domain/service/ExistsUserDomainService.kt | 10 + .../EmailVerificationValidation.kt | 17 ++ .../ExistsUsernameValidation.kt | 13 ++ .../changeUsername/RankPolicyValidation.kt | 13 ++ .../changeUsername/TimeIntervalValidation.kt | 21 +++ .../changeUsername/UsernameChangeContext.kt | 16 ++ .../kotlin/ddd/domain/valueobject/UserId.kt | 3 + .../ddd/domain/valueobject/UserRankEnum.kt | 5 + .../domain/valueobject/UserStatusEnum.java | 5 + .../kotlin/ddd/domain/valueobject/Username.kt | 14 ++ .../repository/MemoryUserRepository.kt | 40 ++++ .../verification/EmailVerificationService.kt | 9 + .../kotlin/mvc/controllers/UserController.kt | 8 + src/main/kotlin/mvc/dao/UserRepository.kt | 23 +++ src/main/kotlin/mvc/entities/User.kt | 7 + .../kotlin/mvc/services/UserComplexService.kt | 18 ++ src/main/kotlin/mvc/services/UserService.kt | 7 + .../kotlin/mvc/services/UserSimpleService.kt | 14 ++ .../exceptions/ChangeUsernameException.kt | 4 + .../shared/exceptions/NotFoundException.kt | 4 + .../validation/AbstractValidationHandler.kt | 15 ++ .../shared/validation/ValidationChain.kt | 22 +++ .../shared/validation/ValidationHandler.kt | 6 + 44 files changed, 854 insertions(+), 1 deletion(-) create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts create mode 100644 src/main/kotlin/Program.kt create mode 100644 src/main/kotlin/ddd/application/DefaultUserService.kt create mode 100644 src/main/kotlin/ddd/application/UserService.kt create mode 100644 src/main/kotlin/ddd/application/dto/ChangeUsername.kt create mode 100644 src/main/kotlin/ddd/controller/UserController.kt create mode 100644 src/main/kotlin/ddd/domain/User.kt create mode 100644 src/main/kotlin/ddd/domain/entity/InternalUserRankPolicy.kt create mode 100644 src/main/kotlin/ddd/domain/entity/RegularUserRankPolicy.kt create mode 100644 src/main/kotlin/ddd/domain/entity/UserRank.kt create mode 100644 src/main/kotlin/ddd/domain/entity/UserRankPolicy.kt create mode 100644 src/main/kotlin/ddd/domain/entity/VipUserRankPolicy.kt create mode 100644 src/main/kotlin/ddd/domain/port/VerificationService.kt create mode 100644 src/main/kotlin/ddd/domain/repository/UserRepository.kt create mode 100644 src/main/kotlin/ddd/domain/service/ExistsUserDomainService.kt create mode 100644 src/main/kotlin/ddd/domain/validation/changeUsername/EmailVerificationValidation.kt create mode 100644 src/main/kotlin/ddd/domain/validation/changeUsername/ExistsUsernameValidation.kt create mode 100644 src/main/kotlin/ddd/domain/validation/changeUsername/RankPolicyValidation.kt create mode 100644 src/main/kotlin/ddd/domain/validation/changeUsername/TimeIntervalValidation.kt create mode 100644 src/main/kotlin/ddd/domain/validation/changeUsername/UsernameChangeContext.kt create mode 100644 src/main/kotlin/ddd/domain/valueobject/UserId.kt create mode 100644 src/main/kotlin/ddd/domain/valueobject/UserRankEnum.kt create mode 100644 src/main/kotlin/ddd/domain/valueobject/UserStatusEnum.java create mode 100644 src/main/kotlin/ddd/domain/valueobject/Username.kt create mode 100644 src/main/kotlin/ddd/infrastructure/repository/MemoryUserRepository.kt create mode 100644 src/main/kotlin/ddd/infrastructure/verification/EmailVerificationService.kt create mode 100644 src/main/kotlin/mvc/controllers/UserController.kt create mode 100644 src/main/kotlin/mvc/dao/UserRepository.kt create mode 100644 src/main/kotlin/mvc/entities/User.kt create mode 100644 src/main/kotlin/mvc/services/UserComplexService.kt create mode 100644 src/main/kotlin/mvc/services/UserService.kt create mode 100644 src/main/kotlin/mvc/services/UserSimpleService.kt create mode 100644 src/main/kotlin/shared/exceptions/ChangeUsernameException.kt create mode 100644 src/main/kotlin/shared/exceptions/NotFoundException.kt create mode 100644 src/main/kotlin/shared/validation/AbstractValidationHandler.kt create mode 100644 src/main/kotlin/shared/validation/ValidationChain.kt create mode 100644 src/main/kotlin/shared/validation/ValidationHandler.kt diff --git a/.gitignore b/.gitignore index 0296a22..7e0ef2f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ .mtj.tmp/ # Package Files # -*.jar *.war *.nar *.ear @@ -26,3 +25,5 @@ replay_pid* # Kotlin Gradle plugin data, see https://kotlinlang.org/docs/whatsnew20.html#new-directory-for-kotlin-data-in-gradle-projects .kotlin/ +.gradle +.idea diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..4d824e7 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + kotlin("jvm") version "1.9.10" + application +} + +kotlin { + jvmToolchain { + languageVersion.set( + JavaLanguageVersion.of(17) + ) + } +} + +application { + mainClass.set("ProgramKt") + + val isDevelopment: Boolean = project.ext.has("development") + applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") +} + +dependencies { +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..5b69144 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,12 @@ +kotlin.code.style = official +org.gradle.caching = true +org.gradle.parallel=true +org.gradle.vfs.watch = true +version = 0.0.1 + + +#systemProp.http.proxyHost = 10.255.255.254 +#systemProp.http.proxyPort = 10808 +# +#systemProp.https.proxyHost = 10.255.255.254 +#systemProp.https.proxyPort = 10808 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..955e78691802d891ed0aee5033b575d9cc6c6870 GIT binary patch literal 56921 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 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..77017c1 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.4-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..17a9170 --- /dev/null +++ b/gradlew @@ -0,0 +1,176 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +if $JAVACMD --add-opens java.base/java.lang=ALL-UNNAMED -version ; then + DEFAULT_JVM_OPTS="--add-opens java.base/java.lang=ALL-UNNAMED $DEFAULT_JVM_OPTS" +fi + +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..d7b5f67 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + gradlePluginPortal() + google() + mavenCentral() + } +} +rootProject.name = "ddd-demo" diff --git a/src/main/kotlin/Program.kt b/src/main/kotlin/Program.kt new file mode 100644 index 0000000..7c0bcf2 --- /dev/null +++ b/src/main/kotlin/Program.kt @@ -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) +} \ No newline at end of file diff --git a/src/main/kotlin/ddd/application/DefaultUserService.kt b/src/main/kotlin/ddd/application/DefaultUserService.kt new file mode 100644 index 0000000..7655c4a --- /dev/null +++ b/src/main/kotlin/ddd/application/DefaultUserService.kt @@ -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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/ddd/application/UserService.kt b/src/main/kotlin/ddd/application/UserService.kt new file mode 100644 index 0000000..cee7c1a --- /dev/null +++ b/src/main/kotlin/ddd/application/UserService.kt @@ -0,0 +1,8 @@ +package ddd.application + +import ddd.application.dto.ChangeUsername +import ddd.domain.User + +interface UserService { + fun changeUsername(userDto: ChangeUsername): User +} \ No newline at end of file diff --git a/src/main/kotlin/ddd/application/dto/ChangeUsername.kt b/src/main/kotlin/ddd/application/dto/ChangeUsername.kt new file mode 100644 index 0000000..9bf215d --- /dev/null +++ b/src/main/kotlin/ddd/application/dto/ChangeUsername.kt @@ -0,0 +1,3 @@ +package ddd.application.dto + +data class ChangeUsername(val id: Long ,val firstName: String, val lastName: String, val verificationCode: String) \ No newline at end of file diff --git a/src/main/kotlin/ddd/controller/UserController.kt b/src/main/kotlin/ddd/controller/UserController.kt new file mode 100644 index 0000000..9d851ba --- /dev/null +++ b/src/main/kotlin/ddd/controller/UserController.kt @@ -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) +} \ No newline at end of file diff --git a/src/main/kotlin/ddd/domain/User.kt b/src/main/kotlin/ddd/domain/User.kt new file mode 100644 index 0000000..d9c6bf5 --- /dev/null +++ b/src/main/kotlin/ddd/domain/User.kt @@ -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() + .add(EmailVerificationValidation()) + .add(RankPolicyValidation()) + .add(TimeIntervalValidation()) + .add(ExistsUsernameValidation()) + .validate(context) + } + + private fun executeUsernameChange(newUsername: Username, clock: Clock) { + username = newUsername + lastUsernameChange = LocalDateTime.now(clock) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ddd/domain/entity/InternalUserRankPolicy.kt b/src/main/kotlin/ddd/domain/entity/InternalUserRankPolicy.kt new file mode 100644 index 0000000..9c68fc3 --- /dev/null +++ b/src/main/kotlin/ddd/domain/entity/InternalUserRankPolicy.kt @@ -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 +} \ No newline at end of file diff --git a/src/main/kotlin/ddd/domain/entity/RegularUserRankPolicy.kt b/src/main/kotlin/ddd/domain/entity/RegularUserRankPolicy.kt new file mode 100644 index 0000000..88ab010 --- /dev/null +++ b/src/main/kotlin/ddd/domain/entity/RegularUserRankPolicy.kt @@ -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 +} + diff --git a/src/main/kotlin/ddd/domain/entity/UserRank.kt b/src/main/kotlin/ddd/domain/entity/UserRank.kt new file mode 100644 index 0000000..6af1a98 --- /dev/null +++ b/src/main/kotlin/ddd/domain/entity/UserRank.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ddd/domain/entity/UserRankPolicy.kt b/src/main/kotlin/ddd/domain/entity/UserRankPolicy.kt new file mode 100644 index 0000000..0d41059 --- /dev/null +++ b/src/main/kotlin/ddd/domain/entity/UserRankPolicy.kt @@ -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 +} \ No newline at end of file diff --git a/src/main/kotlin/ddd/domain/entity/VipUserRankPolicy.kt b/src/main/kotlin/ddd/domain/entity/VipUserRankPolicy.kt new file mode 100644 index 0000000..1207a34 --- /dev/null +++ b/src/main/kotlin/ddd/domain/entity/VipUserRankPolicy.kt @@ -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 +} \ No newline at end of file diff --git a/src/main/kotlin/ddd/domain/port/VerificationService.kt b/src/main/kotlin/ddd/domain/port/VerificationService.kt new file mode 100644 index 0000000..918525b --- /dev/null +++ b/src/main/kotlin/ddd/domain/port/VerificationService.kt @@ -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) +} \ No newline at end of file diff --git a/src/main/kotlin/ddd/domain/repository/UserRepository.kt b/src/main/kotlin/ddd/domain/repository/UserRepository.kt new file mode 100644 index 0000000..b47e186 --- /dev/null +++ b/src/main/kotlin/ddd/domain/repository/UserRepository.kt @@ -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) +} \ No newline at end of file diff --git a/src/main/kotlin/ddd/domain/service/ExistsUserDomainService.kt b/src/main/kotlin/ddd/domain/service/ExistsUserDomainService.kt new file mode 100644 index 0000000..ce29198 --- /dev/null +++ b/src/main/kotlin/ddd/domain/service/ExistsUserDomainService.kt @@ -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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/ddd/domain/validation/changeUsername/EmailVerificationValidation.kt b/src/main/kotlin/ddd/domain/validation/changeUsername/EmailVerificationValidation.kt new file mode 100644 index 0000000..016e46d --- /dev/null +++ b/src/main/kotlin/ddd/domain/validation/changeUsername/EmailVerificationValidation.kt @@ -0,0 +1,17 @@ +package ddd.domain.validation.changeUsername + +import shared.exceptions.ChangeUsernameException +import shared.validation.AbstractValidationHandler + +class EmailVerificationValidation : AbstractValidationHandler() { + 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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ddd/domain/validation/changeUsername/ExistsUsernameValidation.kt b/src/main/kotlin/ddd/domain/validation/changeUsername/ExistsUsernameValidation.kt new file mode 100644 index 0000000..c4c1857 --- /dev/null +++ b/src/main/kotlin/ddd/domain/validation/changeUsername/ExistsUsernameValidation.kt @@ -0,0 +1,13 @@ +package ddd.domain.validation.changeUsername + +import shared.exceptions.ChangeUsernameException +import shared.validation.AbstractValidationHandler + +class ExistsUsernameValidation : AbstractValidationHandler() { + override fun validate(context: UsernameChangeContext) { + require(!context.existsUserService.existsByUsername(context.newUsername)) { + throw ChangeUsernameException("用户名已存在") + } + next(context) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ddd/domain/validation/changeUsername/RankPolicyValidation.kt b/src/main/kotlin/ddd/domain/validation/changeUsername/RankPolicyValidation.kt new file mode 100644 index 0000000..790f290 --- /dev/null +++ b/src/main/kotlin/ddd/domain/validation/changeUsername/RankPolicyValidation.kt @@ -0,0 +1,13 @@ +package ddd.domain.validation.changeUsername + +import shared.exceptions.ChangeUsernameException +import shared.validation.AbstractValidationHandler + +class RankPolicyValidation : AbstractValidationHandler() { + override fun validate(context: UsernameChangeContext) { + require (context.user.rank.policy.canChangeUsername(context.user)) { + throw ChangeUsernameException("用户${context.user.id}没有权限修改用户名") + } + next(context) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ddd/domain/validation/changeUsername/TimeIntervalValidation.kt b/src/main/kotlin/ddd/domain/validation/changeUsername/TimeIntervalValidation.kt new file mode 100644 index 0000000..b156ccb --- /dev/null +++ b/src/main/kotlin/ddd/domain/validation/changeUsername/TimeIntervalValidation.kt @@ -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() { + 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) // 传递至下一个验证器 + } +} \ No newline at end of file diff --git a/src/main/kotlin/ddd/domain/validation/changeUsername/UsernameChangeContext.kt b/src/main/kotlin/ddd/domain/validation/changeUsername/UsernameChangeContext.kt new file mode 100644 index 0000000..c06a3ba --- /dev/null +++ b/src/main/kotlin/ddd/domain/validation/changeUsername/UsernameChangeContext.kt @@ -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 +) \ No newline at end of file diff --git a/src/main/kotlin/ddd/domain/valueobject/UserId.kt b/src/main/kotlin/ddd/domain/valueobject/UserId.kt new file mode 100644 index 0000000..de9ce1d --- /dev/null +++ b/src/main/kotlin/ddd/domain/valueobject/UserId.kt @@ -0,0 +1,3 @@ +package ddd.domain.valueobject + +data class UserId(val value: Long) \ No newline at end of file diff --git a/src/main/kotlin/ddd/domain/valueobject/UserRankEnum.kt b/src/main/kotlin/ddd/domain/valueobject/UserRankEnum.kt new file mode 100644 index 0000000..d050522 --- /dev/null +++ b/src/main/kotlin/ddd/domain/valueobject/UserRankEnum.kt @@ -0,0 +1,5 @@ +package ddd.domain.valueobject + +enum class UserRankEnum { + REGULAR, VIP, INTERNAL +} \ No newline at end of file diff --git a/src/main/kotlin/ddd/domain/valueobject/UserStatusEnum.java b/src/main/kotlin/ddd/domain/valueobject/UserStatusEnum.java new file mode 100644 index 0000000..396235c --- /dev/null +++ b/src/main/kotlin/ddd/domain/valueobject/UserStatusEnum.java @@ -0,0 +1,5 @@ +package ddd.domain.valueobject; + +public enum UserStatusEnum { + ACTIVE,DEACTIVATED,BANNED +} diff --git a/src/main/kotlin/ddd/domain/valueobject/Username.kt b/src/main/kotlin/ddd/domain/valueobject/Username.kt new file mode 100644 index 0000000..1aea7cc --- /dev/null +++ b/src/main/kotlin/ddd/domain/valueobject/Username.kt @@ -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)) { "包含非法字符" } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ddd/infrastructure/repository/MemoryUserRepository.kt b/src/main/kotlin/ddd/infrastructure/repository/MemoryUserRepository.kt new file mode 100644 index 0000000..5a9dc6d --- /dev/null +++ b/src/main/kotlin/ddd/infrastructure/repository/MemoryUserRepository.kt @@ -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() + + 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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/ddd/infrastructure/verification/EmailVerificationService.kt b/src/main/kotlin/ddd/infrastructure/verification/EmailVerificationService.kt new file mode 100644 index 0000000..49fcc3e --- /dev/null +++ b/src/main/kotlin/ddd/infrastructure/verification/EmailVerificationService.kt @@ -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") +} \ No newline at end of file diff --git a/src/main/kotlin/mvc/controllers/UserController.kt b/src/main/kotlin/mvc/controllers/UserController.kt new file mode 100644 index 0000000..ac5f288 --- /dev/null +++ b/src/main/kotlin/mvc/controllers/UserController.kt @@ -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) +} \ No newline at end of file diff --git a/src/main/kotlin/mvc/dao/UserRepository.kt b/src/main/kotlin/mvc/dao/UserRepository.kt new file mode 100644 index 0000000..5ec0424 --- /dev/null +++ b/src/main/kotlin/mvc/dao/UserRepository.kt @@ -0,0 +1,23 @@ +package mvc.dao + +import mvc.entities.User + +class UserRepository { + val users = mutableMapOf() + + 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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/mvc/entities/User.kt b/src/main/kotlin/mvc/entities/User.kt new file mode 100644 index 0000000..498c326 --- /dev/null +++ b/src/main/kotlin/mvc/entities/User.kt @@ -0,0 +1,7 @@ +package mvc.entities + +data class User ( + val id: Long, + val firstName: String, + val lastName: String +) \ No newline at end of file diff --git a/src/main/kotlin/mvc/services/UserComplexService.kt b/src/main/kotlin/mvc/services/UserComplexService.kt new file mode 100644 index 0000000..5b96561 --- /dev/null +++ b/src/main/kotlin/mvc/services/UserComplexService.kt @@ -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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/mvc/services/UserService.kt b/src/main/kotlin/mvc/services/UserService.kt new file mode 100644 index 0000000..68b06a4 --- /dev/null +++ b/src/main/kotlin/mvc/services/UserService.kt @@ -0,0 +1,7 @@ +package mvc.services + +import mvc.entities.User + +interface UserService { + fun changeUsername(user: User): User +} \ No newline at end of file diff --git a/src/main/kotlin/mvc/services/UserSimpleService.kt b/src/main/kotlin/mvc/services/UserSimpleService.kt new file mode 100644 index 0000000..bcf60a5 --- /dev/null +++ b/src/main/kotlin/mvc/services/UserSimpleService.kt @@ -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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/shared/exceptions/ChangeUsernameException.kt b/src/main/kotlin/shared/exceptions/ChangeUsernameException.kt new file mode 100644 index 0000000..131dacc --- /dev/null +++ b/src/main/kotlin/shared/exceptions/ChangeUsernameException.kt @@ -0,0 +1,4 @@ +package shared.exceptions + +class ChangeUsernameException(msg: String) : RuntimeException(msg) { +} \ No newline at end of file diff --git a/src/main/kotlin/shared/exceptions/NotFoundException.kt b/src/main/kotlin/shared/exceptions/NotFoundException.kt new file mode 100644 index 0000000..9d14989 --- /dev/null +++ b/src/main/kotlin/shared/exceptions/NotFoundException.kt @@ -0,0 +1,4 @@ +package shared.exceptions + +class NotFoundException(msg: String) : RuntimeException(msg) { +} \ No newline at end of file diff --git a/src/main/kotlin/shared/validation/AbstractValidationHandler.kt b/src/main/kotlin/shared/validation/AbstractValidationHandler.kt new file mode 100644 index 0000000..9e06d99 --- /dev/null +++ b/src/main/kotlin/shared/validation/AbstractValidationHandler.kt @@ -0,0 +1,15 @@ +package shared.validation + +abstract class AbstractValidationHandler : ValidationHandler { + private lateinit var nextHandler: ValidationHandler + + override fun setNext(handler: ValidationHandler) { + nextHandler = handler + } + + protected fun next(context: T) { + if (::nextHandler.isInitialized) { + nextHandler.validate(context) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/shared/validation/ValidationChain.kt b/src/main/kotlin/shared/validation/ValidationChain.kt new file mode 100644 index 0000000..beff787 --- /dev/null +++ b/src/main/kotlin/shared/validation/ValidationChain.kt @@ -0,0 +1,22 @@ +package shared.validation + +class ValidationChain { + private var firstHandler: ValidationHandler? = null + private var lastHandler: ValidationHandler? = null + + fun add(handler: ValidationHandler): ValidationChain { + if (firstHandler == null) { + firstHandler = handler + lastHandler = handler + } else { + lastHandler?.setNext(handler) + lastHandler = handler + } + return this + } + + fun validate(context: T) { + firstHandler?.validate(context) + ?: throw IllegalStateException("验证链未初始化") + } +} \ No newline at end of file diff --git a/src/main/kotlin/shared/validation/ValidationHandler.kt b/src/main/kotlin/shared/validation/ValidationHandler.kt new file mode 100644 index 0000000..86fbabc --- /dev/null +++ b/src/main/kotlin/shared/validation/ValidationHandler.kt @@ -0,0 +1,6 @@ +package shared.validation + +interface ValidationHandler { + fun validate(context: T) + fun setNext(handler: ValidationHandler) +} \ No newline at end of file