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 0000000..955e786 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ 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