diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ff6309 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/checkstyle-idea.xml b/.idea/checkstyle-idea.xml new file mode 100644 index 0000000..d14ab4f --- /dev/null +++ b/.idea/checkstyle-idea.xml @@ -0,0 +1,16 @@ + + + + 11.0.1 + JavaOnly + true + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..06e8b35 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..2b63946 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..c1dd12f Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..40ca015 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.5/apache-maven-3.8.5-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar \ No newline at end of file diff --git a/doc/说明文档.markdown b/doc/说明文档.markdown new file mode 100644 index 0000000..6ab05ba --- /dev/null +++ b/doc/说明文档.markdown @@ -0,0 +1,181 @@ +# 数学学习软件系统说明文档 + +## 概述 +数学学习软件是一个基于 JavaFX 开发的桌面应用程序,旨在为不同学段(小学、初中、高中)的学生提供定制化的数学练习平台。系统支持用户注册登录、题目难度选择、答题练习和成绩统计等功能,确保生成的题目符合各学段的教学要求。 + +## 运行环境 + +### 平台要求 +- **操作系统**: Windows 10/11 +- **Java版本**: JDK 17 或更高版本 +- **内存**: 最低 2GB RAM +- **磁盘空间**: 至少 100MB 可用空间 + +### 依赖项 +- JavaFX 17 (已包含在项目中) +- 需要网络连接用于邮箱验证功能 + +## 项目结构 +``` +src/ +├── com.wsf.mathapp/ # 前端界面和控制器 +│ ├── controller/ +│ │ └── SceneManager.java +│ ├── view/ +│ │ ├── LoginView.java +│ │ ├── RegisterView.java +│ │ ├── MainMenuView.java +│ │ ├── LevelSelectionView.java +│ │ ├── QuestionCountView.java +│ │ ├── QuizView.java +│ │ └── ResultView.java +│ ├── service/ +│ │ └── QuestionService.java +│ └── MathApplication.java +├── com.ybw.mathapp/ # 后端核心逻辑 +│ ├── entity/ +│ │ ├── User.java +│ │ └── QuestionWithOptions.java +│ ├── service/ +│ │ ├── QuestionGenerator.java +│ │ ├── PrimarySchoolGenerator.java +│ │ ├── JuniorHighGenerator.java +│ │ ├── SeniorHighGenerator.java +│ │ ├── MultipleChoiceGenerator.java +│ │ └── AdvancedCaculate.java +│ ├── util/ +│ │ ├── Login.java +│ │ ├── Register.java +│ │ ├── ChangePassword.java +│ │ ├── LoginFileUtils.java +│ │ └── EmailService.java +│ └── config/ +│ └── EmailConfig.java +└── Main.java +``` + +## 安装和运行 + +### 方法一:使用 IDE 运行(推荐) +1. **安装 JDK 17** + - 下载并安装 Oracle JDK 17 或 OpenJDK 17 + - 设置 JAVA_HOME 环境变量 + +2. **导入项目** + - 使用 IntelliJ IDEA 或 Eclipse 导入项目 + - 确保配置了 JDK 17 + +3. **运行程序** + - 打开 `Main.java` 或 `MathApplication.java` + - 点击运行按钮启动应用程序 + +### 方法二:使用 JAR 文件 + **直接运行 JAR 文件** +```bash +java -jar .\mathapp-1.0.jar +``` + +## 系统功能 + +### 1. 用户管理 +- **用户注册**: 支持用户名、邮箱注册,通过邮箱验证码验证身份 +- **用户登录**: 支持用户名或邮箱登录,密码验证 +- **密码修改**: 在线修改密码,需验证原密码 +- **用户信息显示**: 动态显示用户头像和用户名 + +### 2. 题目生成 +- **小学题目**: 生成包含四则运算和括号的题目,确保计算结果为非负数 +- **初中题目**: 生成包含平方或开根号运算的题目,确保每道题目都包含高级运算符 +- **高中题目**: 生成包含三角函数的题目,支持复杂的数学表达式 +- **选择题转换**: 将生成的题目转换为选择题形式,包含正确答案和干扰项 + +### 3. 答题评估 +- **实时答题**: 显示题目和四个选项,支持单选 +- **进度跟踪**: 显示当前题号和总题数 +- **自动评分**: 计算正确率并给出相应评语 +- **结果展示**: 根据得分显示不同颜色的分数和鼓励性评语 + +## 使用指南 + +### 第一步:启动应用 +```bash +# 确保在项目根目录下执行 +java -jar .\mathapp-1.0.jar +``` + +### 第二步:用户注册 +1. 点击"注册账号"按钮 +2. 输入用户名(3-20位字母、数字) +3. 输入有效邮箱地址 +4. 获取并输入邮箱验证码 +5. 设置密码(6-10位,包含大小写字母和数字) +6. 确认密码并完成注册 + +### 第三步:选择练习 +1. 登录后进入主菜单 +2. 点击"开始练习" +3. 选择难度级别:小学、初中或高中 +4. 输入题目数量(10-30题) +5. 开始答题 + +### 第四步:答题和查看结果 +1. 阅读题目并从四个选项中选择答案 +2. 点击"下一题"继续 +3. 完成所有题目后查看得分和评语 +4. 可选择"再次练习"或"返回主菜单" + +## 配置说明 + +### 邮箱配置 +系统使用 QQ 邮箱发送验证码,如需修改配置,编辑 `EmailConfig.java`: +```java +public class EmailConfig { + public static final String SMTP_HOST = "smtp.qq.com"; + public static final String SMTP_PORT = "587"; + public static final String SENDER_EMAIL = "your-email@qq.com"; + public static final String SENDER_PASSWORD = "your-authorization-code"; +} +``` + +### 数据存储 +- 用户信息存储在 `users.txt` 文件中 +- 每个用户的答题记录保存在以用户名为名的目录中 + +## 故障排除 + +### 常见问题 +1. **无法启动程序** + - 检查 JDK 版本是否为 17 + - 确认 JavaFX 库路径正确 + +2. **邮箱验证码收不到** + - 检查网络连接 + - 确认邮箱配置正确 + - 查看垃圾邮件文件夹 + +3. **界面显示异常** + - 确保屏幕分辨率不低于 1024x768 + - 检查系统字体设置 + +### 日志查看 +程序运行日志输出到控制台,包含: +- 用户登录/注册信息 +- 题目生成状态 +- 界面切换记录 + +## 技术特性 + +- **模块化设计**: 易于维护和扩展 +- **响应式界面**: 适配不同屏幕尺寸 +- **数据验证**: 完整的输入验证机制 +- **错误处理**: 友好的错误提示信息 +- **性能优化**: 高效的题目生成和去重算法 + +## 注意事项 + +1. 首次使用需要注册账号 +2. 小学题目不包含负数运算 +3. 初中题目避免负数开根号 +4. 邮箱验证码有效期为 5 分钟 +5. 建议在稳定的网络环境下使用邮箱功能 + diff --git a/images/default-avatar.png b/images/default-avatar.png new file mode 100644 index 0000000..e7fe7f4 Binary files /dev/null and b/images/default-avatar.png differ diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..8a8fb22 --- /dev/null +++ b/mvnw @@ -0,0 +1,316 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + 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 + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + 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 + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..1d8ab01 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,188 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..4735d28 --- /dev/null +++ b/pom.xml @@ -0,0 +1,207 @@ + + + 4.0.0 + + com.wsf + MathApp + 1.0-SNAPSHOT + MathApp + + + UTF-8 + 17 + 17 + 5.10.0 + 17.0.6 + win + + + + + + org.openjfx + javafx-controls + ${javafx.version} + ${javafx.platform} + + + org.openjfx + javafx-fxml + ${javafx.version} + ${javafx.platform} + + + org.openjfx + javafx-base + ${javafx.version} + ${javafx.platform} + + + org.openjfx + javafx-graphics + ${javafx.version} + ${javafx.platform} + + + + org.openjfx + javafx-web + ${javafx.version} + ${javafx.platform} + + + org.openjfx + javafx-media + ${javafx.version} + ${javafx.platform} + + + + + org.openjfx + javafx-web + ${javafx.version} + + + org.openjfx + javafx-swing + ${javafx.version} + + + org.openjfx + javafx-media + ${javafx.version} + + + + + org.controlsfx + controlsfx + 11.1.2 + + + com.dlsc.formsfx + formsfx-core + 11.6.0 + + + org.openjfx + * + + + + + net.synedra + validatorfx + 0.4.0 + + + org.openjfx + * + + + + + org.kordamp.ikonli + ikonli-javafx + 12.3.1 + + + org.kordamp.bootstrapfx + bootstrapfx-core + 0.4.0 + + + eu.hansolo + tilesfx + 11.48 + + + org.openjfx + * + + + + + com.github.almasb + fxgl + 17.3 + + + org.openjfx + * + + + + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + com.sun.mail + jakarta.mail + 2.0.1 + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + 17 + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.0 + + + package + + shade + + + + + com.wsf.mathapp.MathApplication + + + + + + + + + + + + + + false + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/wsf/mathapp/Main.java b/src/main/java/com/wsf/mathapp/Main.java new file mode 100644 index 0000000..b0964a0 --- /dev/null +++ b/src/main/java/com/wsf/mathapp/Main.java @@ -0,0 +1,34 @@ +package com.wsf.mathapp; + +import com.wsf.mathapp.controller.SceneManager; +import javafx.application.Application; +import javafx.stage.Stage; + +/** + * 数学学习软件主应用程序类. + * 负责初始化应用程序并启动JavaFX界面. + */ +public class Main extends Application { + + /** + * JavaFX应用程序的入口方法. + * 初始化场景管理器并显示登录界面. + * + * @param primaryStage JavaFX主舞台对象. + */ + @Override + public void start(Stage primaryStage) { + SceneManager sceneManager = new SceneManager(primaryStage); + sceneManager.showLoginView(); + } + + /** + * 应用程序的主方法. + * 启动JavaFX应用程序. + * + * @param args 命令行参数. + */ + public static void main(String[] args) { + launch(args); + } +} \ No newline at end of file diff --git a/src/main/java/com/wsf/mathapp/MathApplication.java b/src/main/java/com/wsf/mathapp/MathApplication.java new file mode 100644 index 0000000..c90037d --- /dev/null +++ b/src/main/java/com/wsf/mathapp/MathApplication.java @@ -0,0 +1,9 @@ +package com.wsf.mathapp; + +public class MathApplication { + + public static void main(String[] args) { + Main.main(args); + } + +} diff --git a/src/main/java/com/wsf/mathapp/controller/SceneManager.java b/src/main/java/com/wsf/mathapp/controller/SceneManager.java new file mode 100644 index 0000000..ff4a040 --- /dev/null +++ b/src/main/java/com/wsf/mathapp/controller/SceneManager.java @@ -0,0 +1,164 @@ +package com.wsf.mathapp.controller; + +import com.wsf.mathapp.view.LevelSelectionView; +import com.wsf.mathapp.view.LoginView; +import com.wsf.mathapp.view.MainMenuView; +import com.wsf.mathapp.view.QuestionCountView; +import com.wsf.mathapp.view.QuizView; +import com.wsf.mathapp.view.RegisterView; +import com.wsf.mathapp.view.ResultView; +import javafx.stage.Stage; + +/** + * 场景管理器类,负责管理应用程序中各个界面之间的切换和导航. + * 维护所有视图实例并提供统一的方法来显示不同的界面. + */ +public class SceneManager { + private final Stage primaryStage; + private final LoginView loginView; + private final RegisterView registerView; + private final MainMenuView mainMenuView; + private final LevelSelectionView levelSelectionView; + private final QuestionCountView questionCountView; + private final QuizView quizView; + private final ResultView resultView; + private String currentUserName; + + /** + * 构造函数,初始化场景管理器. + * + * @param primaryStage JavaFX主舞台,用于显示各个场景. + */ + public SceneManager(Stage primaryStage) { + this.primaryStage = primaryStage; + this.primaryStage.setTitle("数学学习软件"); + this.primaryStage.setResizable(false); + + // 初始化所有视图 + this.loginView = new LoginView(this); + this.registerView = new RegisterView(this); + this.mainMenuView = new MainMenuView(this); + this.levelSelectionView = new LevelSelectionView(this); + this.questionCountView = new QuestionCountView(this); + this.quizView = new QuizView(this); + this.resultView = new ResultView(this); + } + + /** + * 显示登录界面. + * 清空输入字段并将主舞台的场景设置为登录界面. + */ + public void showLoginView() { + System.out.println("切换到登录界面"); + loginView.clearFields(); + primaryStage.setScene(loginView.getScene()); + primaryStage.show(); + } + + /** + * 显示注册界面. + * 清空输入字段并将主舞台的场景设置为注册界面. + */ + public void showRegisterView() { + System.out.println("切换到注册界面"); + registerView.clearFields(); + primaryStage.setScene(registerView.getScene()); + primaryStage.show(); + } + + /** + * 显示主菜单界面. + * 在显示前更新界面上的用户名显示. + */ + public void showMainMenuView() { + System.out.println("切换到主菜单界面"); + // 在显示主菜单前更新用户名 + if (mainMenuView != null) { + mainMenuView.updateUsername(currentUserName); + } + if (mainMenuView != null) { + primaryStage.setScene(mainMenuView.getScene()); + } + primaryStage.show(); // 添加这行 + } + + /** + * 显示级别选择界面. + * 在显示前更新界面上的用户名显示. + */ + public void showLevelSelectionView() { + System.out.println("切换到级别选择界面"); + // 在显示级别选择界面前更新用户名 + if (levelSelectionView != null) { + levelSelectionView.updateUsername(currentUserName); + } + if (levelSelectionView != null) { + primaryStage.setScene(levelSelectionView.getScene()); + } + primaryStage.show(); // 添加这行 + } + + /** + * 显示题目数量选择界面. + * 用户可以选择要回答的题目数量. + */ + public void showQuestionCountView() { + System.out.println("切换到题目数量选择界面"); + primaryStage.setScene(questionCountView.getScene()); + primaryStage.show(); // 添加这行 + } + + /** + * 显示答题界面. + * + * @param level 题目难度级别. + * @param count 题目数量. + */ + public void showQuizView(String level, int count) { + System.out.println("切换到答题界面 - 级别: " + level + ", 题目数量: " + count); + // 设置测验参数 + quizView.setQuizParameters(level, count); + primaryStage.setScene(quizView.getScene()); + primaryStage.show(); + } + + /** + * 显示结果界面. + + * @param score 用户获得的分数. + */ + public void showResultView(double score) { + System.out.println("切换到结果界面,分数: " + score); + resultView.setScore(score); + primaryStage.setScene(resultView.getScene()); + primaryStage.show(); // 添加这行 + } + + /** + * 获取题目数量选择视图实例. + * + * @return QuestionCountView 题目数量选择视图对象. + */ + public QuestionCountView getQuestionCountView() { + return questionCountView; + } + + /** + * 设置当前用户名. + * + * @param currentUserName 当前登录用户的用户名. + */ + public void setCurrentUserName(String currentUserName) { + this.currentUserName = currentUserName; + System.out.println("设置当前用户名: " + currentUserName); + } + + /** + * 获取当前用户名. + * + * @return String 当前登录用户的用户名. + */ + public String getCurrentUserName() { + return this.currentUserName; + } +} \ No newline at end of file diff --git a/src/main/java/com/wsf/mathapp/service/QuestionService.java b/src/main/java/com/wsf/mathapp/service/QuestionService.java new file mode 100644 index 0000000..2067873 --- /dev/null +++ b/src/main/java/com/wsf/mathapp/service/QuestionService.java @@ -0,0 +1,28 @@ +package com.wsf.mathapp.service; + +import com.ybw.mathapp.service.JuniorHighGenerator; +import com.ybw.mathapp.service.PrimarySchoolGenerator; +import com.ybw.mathapp.service.QuestionGenerator; +import com.ybw.mathapp.service.SeniorHighGenerator; + +/** + * 题目服务类,负责根据不同的难度级别创建对应的题目生成器. + */ +public class QuestionService { + + /** + * 根据难度级别创建对应的题目生成器. + * + * @param level 题目难度级别,可选值:"小学"、"初中"、"高中". + * @return QuestionGenerator 对应难度级别的题目生成器实例,如果级别不匹配则返回null. + */ + public static QuestionGenerator createGenerator(String level) { + return switch (level) { + case "小学" -> new PrimarySchoolGenerator(); + case "初中" -> new JuniorHighGenerator(); + case "高中" -> new SeniorHighGenerator(); + default -> null; + }; + } + +} \ No newline at end of file diff --git a/src/main/java/com/wsf/mathapp/view/LevelSelectionView.java b/src/main/java/com/wsf/mathapp/view/LevelSelectionView.java new file mode 100644 index 0000000..b35c1a3 --- /dev/null +++ b/src/main/java/com/wsf/mathapp/view/LevelSelectionView.java @@ -0,0 +1,310 @@ +package com.wsf.mathapp.view; + +import com.wsf.mathapp.controller.SceneManager; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.scene.shape.Circle; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.scene.text.Text; + +/** + * 级别选择视图类,提供用户选择题目难度级别的界面. + * 包含小学、初中、高中三个难度级别的选择按钮. + */ +public class LevelSelectionView { + private Scene scene; + private final SceneManager sceneManager; + private String currentUsername; + + // 添加界面组件引用 + private Label usernameLabel; + private Text avatarText; + + /** + * 构造函数,初始化级别选择视图. + * + * @param sceneManager 场景管理器,用于界面导航. + */ + public LevelSelectionView(SceneManager sceneManager) { + this.sceneManager = sceneManager; + this.currentUsername = sceneManager.getCurrentUserName(); + createScene(); + } + + /** + * 创建主场景,包含用户信息栏和级别选择内容. + */ + private void createScene() { + // 创建主容器 + VBox mainContainer = new VBox(); + mainContainer.setPadding(new Insets(20)); + + // 创建顶部用户信息栏 + HBox userInfoBar = createUserInfoBar(); + + // 创建级别选择内容区域 + VBox selectionContent = createSelectionContent(); + + mainContainer.getChildren().addAll(userInfoBar, selectionContent); + + scene = new Scene(mainContainer, 450, 550); + } + + /** + * 创建用户信息栏,包含用户头像和用户名显示. + * + * @return HBox 用户信息栏布局容器. + */ + private HBox createUserInfoBar() { + HBox userInfoBar = new HBox(15); + userInfoBar.setAlignment(Pos.CENTER_LEFT); + userInfoBar.setPadding(new Insets(0, 0, 30, 0)); + userInfoBar.setStyle("-fx-border-color: #e0e0e0; " + + "-fx-border-width: 0 0 1 0; -fx-padding: 0 0 15 0;"); + // 用户名标签 + usernameLabel = new Label(currentUsername != null ? currentUsername : "用户"); + usernameLabel.setFont(Font.font("Arial", FontWeight.BOLD, 16)); + usernameLabel.setStyle("-fx-text-fill: #2c3e50;"); + // 间隔 + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + // 创建头像容器 + VBox avatarContainer = createAvatarContainer(); + userInfoBar.getChildren().addAll(avatarContainer, usernameLabel, spacer); + return userInfoBar; + } + + /** + * 创建头像容器,包含圆形头像背景和用户首字母. + * + * @return VBox 头像容器布局. + */ + private VBox createAvatarContainer() { + VBox avatarContainer = new VBox(); + avatarContainer.setAlignment(Pos.CENTER); + avatarContainer.setPrefSize(50, 50); + // 添加首字母文本 + avatarText = new Text(getFirstLetter()); + avatarText.setFill(Color.WHITE); + avatarText.setFont(Font.font("Arial", FontWeight.BOLD, 16)); + // 创建圆形头像背景 + Circle avatarCircle = createAvatarCircle(); + // 使用StackPane将文本放在圆形中心 + javafx.scene.layout.StackPane avatarStack = new javafx.scene.layout.StackPane(); + avatarStack.getChildren().addAll(avatarCircle, avatarText); + avatarStack.setPrefSize(44, 44); + avatarStack.setAlignment(Pos.CENTER); + + avatarContainer.getChildren().add(avatarStack); + return avatarContainer; + } + + /** + * 创建圆形头像背景. + * + * @return Circle 圆形头像背景对象. + */ + private Circle createAvatarCircle() { + Circle avatarCircle = new Circle(22); + avatarCircle.setFill(Color.web("#4CAF50")); // 统一的绿色背景 + avatarCircle.setStroke(Color.WHITE); + avatarCircle.setStrokeWidth(2); + + // 添加阴影效果 + avatarCircle.setStyle("-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), 5, 0.3, 2, 2);"); + return avatarCircle; + } + + /** + * 创建级别选择内容区域,包含标题和级别选择按钮. + * + * @return VBox 级别选择内容区域布局容器. + */ + private VBox createSelectionContent() { + VBox selectionContent = new VBox(25); + selectionContent.setPadding(new Insets(30, 20, 20, 20)); + selectionContent.setAlignment(Pos.CENTER); + + // 创建标题区域 + VBox titleSection = createTitleSection(); + + // 创建级别按钮区域 + VBox levelButtonsSection = createLevelButtonsSection(); + + // 创建返回按钮 + Button backButton = createBackButton(); + + selectionContent.getChildren().addAll(titleSection, levelButtonsSection, backButton); + + return selectionContent; + } + + /** + * 创建标题区域,包含主标题和副标题. + * + * @return VBox 标题区域布局容器. + */ + private VBox createTitleSection() { + VBox titleSection = new VBox(5); + titleSection.setAlignment(Pos.CENTER); + + Label titleLabel = new Label("选择题目级别"); + titleLabel.setFont(Font.font("Arial", FontWeight.BOLD, 26)); + titleLabel.setStyle("-fx-text-fill: #2c3e50;"); + + Label subtitleLabel = new Label("请选择适合您的学习级别"); + subtitleLabel.setFont(Font.font("Arial", 14)); + subtitleLabel.setStyle("-fx-text-fill: #7f8c8d; -fx-padding: 0 0 10 0;"); + + titleSection.getChildren().addAll(titleLabel, subtitleLabel); + return titleSection; + } + + /** + * 创建级别按钮区域,包含三个难度级别的选择按钮. + * + * @return VBox 级别按钮区域布局容器. + */ + private VBox createLevelButtonsSection() { + VBox levelButtonsSection = new VBox(15); + levelButtonsSection.setAlignment(Pos.CENTER); + + Button primaryButton = createLevelButton("小学题目", "#4CAF50", "#45a049"); + Button juniorButton = createLevelButton("初中题目", "#2196F3", "#1976D2"); + Button seniorButton = createLevelButton("高中题目", "#9C27B0", "#7B1FA2"); + + setupLevelButtonAction(primaryButton, "小学"); + setupLevelButtonAction(juniorButton, "初中"); + setupLevelButtonAction(seniorButton, "高中"); + + levelButtonsSection.getChildren().addAll(primaryButton, juniorButton, seniorButton); + return levelButtonsSection; + } + + /** + * 设置级别按钮的点击事件. + * + * @param button 级别按钮. + * @param level 对应的难度级别. + */ + private void setupLevelButtonAction(Button button, String level) { + button.setOnAction(e -> { + sceneManager.getQuestionCountView().setLevel(level); + sceneManager.showQuestionCountView(); + }); + } + + /** + * 创建返回按钮. + * + * @return Button 返回主菜单按钮. + */ + private Button createBackButton() { + Button backButton = new Button("返回主菜单"); + backButton.setStyle("-fx-background-color: #95a5a6; -fx-text-fill: " + + "white; -fx-font-size: 14px; -fx-background-radius: 8; -fx-padding: 8 20;"); + backButton.setPrefSize(180, 45); + setupButtonHoverEffect(backButton, "#95a5a6", "#7f8c8d"); + backButton.setOnAction(e -> sceneManager.showMainMenuView()); + + return backButton; + } + + /** + * 创建级别选择按钮. + * + * @param text 按钮显示文本. + * @param color 按钮正常状态颜色. + * @param hoverColor 按钮悬停状态颜色. + * @return Button 配置好的级别选择按钮. + */ + private Button createLevelButton(String text, String color, String hoverColor) { + Button button = new Button(text); + button.setStyle(String.format( + "-fx-background-color: %s; -fx-text-fill: " + + "white; -fx-font-size: 16px; -fx-font-weight: bold;-fx-background-radius: 12;" + + "-fx-padding: 12 30;-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), 8, 0.3, 2, 2);", + color + )); + button.setPrefSize(220, 60); + + setupButtonHoverEffect(button, color, hoverColor); + + return button; + } + + /** + * 设置按钮的悬停效果. + * + * @param button 需要设置悬停效果的按钮. + * @param normalColor 正常状态颜色. + * @param hoverColor 悬停状态颜色. + */ + private void setupButtonHoverEffect(Button button, String normalColor, String hoverColor) { + String normalStyle = String.format( + "-fx-background-color: %s; -fx-text-fill: white; -fx-font-size: %s;" + + " -fx-background-radius: %s; -fx-padding: %s; -fx-effect: " + + "dropshadow(gaussian, rgba(0,0,0,0.2), 8, 0.3, 2, 2);", + normalColor, + button.getStyle().contains("16px") ? "16px" : "14px", + button.getStyle().contains("12") ? "12" : "8", + button.getStyle().contains("12 30") ? "12 30" : "8 20" + ); + + String hoverStyle = String.format( + "-fx-background-color: %s; -fx-text-fill: white; -fx-font-size: %s;" + + " -fx-background-radius: %s; -fx-padding: %s; -fx-effect:" + + " dropshadow(gaussian, rgba(0,0,0,0.3), 10, 0.4, 3, 3);", + hoverColor, + button.getStyle().contains("16px") ? "16px" : "14px", + button.getStyle().contains("12") ? "12" : "8", + button.getStyle().contains("12 30") ? "12 30" : "8 20" + ); + + button.setOnMouseEntered(e -> button.setStyle(hoverStyle)); + button.setOnMouseExited(e -> button.setStyle(normalStyle)); + } + + /** + * 获取用户名的首字母,用于头像显示. + * + * @return String 用户名的首字母大写,如果用户名为空则返回"U". + */ + private String getFirstLetter() { + return currentUsername != null && !currentUsername.isEmpty() + ? currentUsername.substring(0, 1).toUpperCase() : "U"; + } + + /** + * 更新用户名显示. + * + * @param username 新的用户名. + */ + public void updateUsername(String username) { + this.currentUsername = username; + if (usernameLabel != null) { + usernameLabel.setText(username != null ? username : "用户"); + } + if (avatarText != null) { + avatarText.setText(getFirstLetter()); + } + } + + /** + * 获取当前场景. + * + * @return Scene 级别选择界面的场景对象. + */ + public Scene getScene() { + return scene; + } +} \ No newline at end of file diff --git a/src/main/java/com/wsf/mathapp/view/LoginView.java b/src/main/java/com/wsf/mathapp/view/LoginView.java new file mode 100644 index 0000000..1810b61 --- /dev/null +++ b/src/main/java/com/wsf/mathapp/view/LoginView.java @@ -0,0 +1,265 @@ +package com.wsf.mathapp.view; + +import com.wsf.mathapp.controller.SceneManager; +import com.ybw.mathapp.util.Login; +import com.ybw.mathapp.util.LoginFileUtils; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.scene.layout.VBox; +import javafx.scene.text.Font; + +/** + * 登录视图类,提供用户登录功能界面. + * 支持用户名或邮箱登录,包含输入验证和登录状态提示. + */ +public class LoginView { + private Scene scene; + private final SceneManager sceneManager; + + // 将UI组件声明为类的成员变量 + private TextField usernameOrEmailField; + private PasswordField passwordField; + private Label statusLabel; + + /** + * 构造函数,初始化登录视图. + * + * @param sceneManager 场景管理器,用于界面导航. + */ + public LoginView(SceneManager sceneManager) { + this.sceneManager = sceneManager; + createScene(); + } + + /** + * 创建登录界面的主场景. + */ + private void createScene() { + VBox root = new VBox(20); + root.setPadding(new Insets(40)); + root.setAlignment(Pos.CENTER); + + // 创建用户名/邮箱输入框 + usernameOrEmailField = createUsernameField(); + + // 创建密码输入框 + passwordField = createPasswordField(); + + // 创建登录按钮 + Button loginButton = createLoginButton(); + + // 创建注册按钮 + Button registerButton = createRegisterButton(); + + // 创建状态标签 + statusLabel = new Label(); + + // 设置按钮事件 + setupLoginButtonAction(loginButton); + setupRegisterButtonAction(registerButton); + + // 添加回车键登录支持 + setupEnterKeySupport(loginButton); + // 创建界面标题 + Label titleLabel = createTitleLabel(); + + // 创建副标题 + Label subtitleLabel = createSubtitleLabel(); + root.getChildren().addAll( + titleLabel, subtitleLabel, usernameOrEmailField, passwordField, + loginButton, registerButton, statusLabel + ); + scene = new Scene(root, 400, 500); + } + + /** + * 创建主标题标签. + * + * @return Label 主标题标签对象. + */ + private Label createTitleLabel() { + Label titleLabel = new Label("数学学习软件"); + titleLabel.setFont(Font.font(24)); + return titleLabel; + } + + /** + * 创建副标题标签. + * + * @return Label 副标题标签对象. + */ + private Label createSubtitleLabel() { + Label subtitleLabel = new Label("用户登录"); + subtitleLabel.setFont(Font.font(18)); + return subtitleLabel; + } + + /** + * 创建用户名/邮箱输入框. + * + * @return TextField 用户名/邮箱输入框对象. + */ + private TextField createUsernameField() { + TextField field = new TextField(); + field.setPromptText("请输入用户名或邮箱"); + field.setMaxWidth(300); + field.setPrefHeight(40); + return field; + } + + /** + * 创建密码输入框. + * + * @return PasswordField 密码输入框对象. + */ + private PasswordField createPasswordField() { + PasswordField field = new PasswordField(); + field.setPromptText("请输入密码"); + field.setMaxWidth(300); + field.setPrefHeight(40); + return field; + } + + /** + * 创建登录按钮. + * + * @return Button 登录按钮对象. + */ + private Button createLoginButton() { + Button button = new Button("登录"); + button.setStyle("-fx-background-color: #4CAF50; -fx-text-fill: white; -fx-font-size: 14px;"); + button.setPrefSize(300, 40); + return button; + } + + /** + * 创建注册按钮. + * + * @return Button 注册按钮对象. + */ + private Button createRegisterButton() { + Button button = new Button("注册账号"); + button.setStyle("-fx-background-color: #2196F3; -fx-text-fill: white; -fx-font-size: 14px;"); + button.setPrefSize(300, 40); + return button; + } + + /** + * 设置登录按钮的点击事件. + * + * @param loginButton 登录按钮对象. + */ + private void setupLoginButtonAction(Button loginButton) { + loginButton.setOnAction(e -> handleLogin()); + } + + /** + * 设置注册按钮的点击事件. + * + * @param registerButton 注册按钮对象. + */ + private void setupRegisterButtonAction(Button registerButton) { + registerButton.setOnAction(e -> sceneManager.showRegisterView()); + } + + /** + * 设置回车键登录支持. + * + * @param loginButton 登录按钮对象. + */ + private void setupEnterKeySupport(Button loginButton) { + usernameOrEmailField.setOnAction(e -> loginButton.fire()); + passwordField.setOnAction(e -> loginButton.fire()); + } + + /** + * 处理登录逻辑. + */ + private void handleLogin() { + String usernameOrEmail = usernameOrEmailField.getText().trim(); + String password = passwordField.getText(); + + if (usernameOrEmail.isEmpty() || password.isEmpty()) { + showError(statusLabel, "用户名/邮箱和密码不能为空!"); + return; + } + + // 调用后端的登录方法,支持用户名或邮箱登录 + boolean success = Login.login(usernameOrEmail, password); + System.out.println(usernameOrEmail); + System.out.println(password); + + if (success) { + handleLoginSuccess(usernameOrEmail); + } else { + showError(statusLabel, "用户名/邮箱或密码错误!"); + } + } + + /** + * 处理登录成功后的逻辑. + * + * @param usernameOrEmail 登录使用的用户名或邮箱. + */ + private void handleLoginSuccess(String usernameOrEmail) { + showSuccess(statusLabel); + String username = getUsernameFromInput(usernameOrEmail); + sceneManager.setCurrentUserName(username); + sceneManager.showMainMenuView(); + } + + /** + * 根据输入获取用户名. + * + * @param usernameOrEmail 用户输入的用户名或邮箱. + * @return String 对应的用户名. + */ + private String getUsernameFromInput(String usernameOrEmail) { + String foundName = LoginFileUtils.emailFindName(usernameOrEmail); + return foundName == null ? usernameOrEmail : foundName; + } + + /** + * 清空所有输入字段和状态信息. + */ + public void clearFields() { + usernameOrEmailField.clear(); + passwordField.clear(); + statusLabel.setText(""); + } + + /** + * 显示错误信息. + * + * @param label 状态标签对象. + * @param message 错误信息内容. + */ + private void showError(Label label, String message) { + label.setText(message); + label.setStyle("-fx-text-fill: red;"); + } + + /** + * 显示成功信息. + * + * @param label 状态标签对象. + */ + private void showSuccess(Label label) { + label.setText("登录成功!"); + label.setStyle("-fx-text-fill: green;"); + } + + /** + * 获取当前场景. + * + * @return Scene 登录界面的场景对象. + */ + public Scene getScene() { + return scene; + } +} \ No newline at end of file diff --git a/src/main/java/com/wsf/mathapp/view/MainMenuView.java b/src/main/java/com/wsf/mathapp/view/MainMenuView.java new file mode 100644 index 0000000..cad87b8 --- /dev/null +++ b/src/main/java/com/wsf/mathapp/view/MainMenuView.java @@ -0,0 +1,461 @@ +package com.wsf.mathapp.view; + +import com.wsf.mathapp.controller.SceneManager; +import com.ybw.mathapp.util.ChangePassword; +import com.ybw.mathapp.util.Login; +import com.ybw.mathapp.util.Register; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.scene.shape.Circle; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.scene.text.Text; + +/** + * 主菜单视图类,提供应用程序的主导航界面. + * 包含用户信息显示、开始练习、修改密码和退出登录等功能. + */ +public class MainMenuView { + private Scene scene; + private final SceneManager sceneManager; + private String currentUsername; + + // 添加界面组件引用,用于动态更新 + private Label usernameLabel; + private Text avatarText; + private Label welcomeLabel; + + /** + * 构造函数,初始化主菜单视图. + * + * @param sceneManager 场景管理器,用于界面导航. + */ + public MainMenuView(SceneManager sceneManager) { + this.sceneManager = sceneManager; + this.currentUsername = sceneManager.getCurrentUserName(); + createScene(); + } + + /** + * 创建主场景,包含用户信息栏和菜单内容区域. + */ + private void createScene() { + // 创建主容器 + VBox mainContainer = new VBox(); + mainContainer.setPadding(new Insets(20)); + + // 创建顶部用户信息栏 + HBox userInfoBar = createUserInfoBar(); + + // 创建主菜单内容区域 + VBox menuContent = createMenuContent(); + + mainContainer.getChildren().addAll(userInfoBar, menuContent); + + scene = new Scene(mainContainer, 450, 550); + } + + /** + * 创建用户信息栏,包含用户头像和用户名显示. + * + * @return HBox 用户信息栏布局容器. + */ + private HBox createUserInfoBar() { + HBox userInfoBar = new HBox(15); + userInfoBar.setAlignment(Pos.CENTER_LEFT); + userInfoBar.setPadding(new Insets(0, 0, 30, 0)); + userInfoBar.setStyle("-fx-border-color: #e0e0e0; -fx-border-width: 0 0 1 0; " + + "-fx-padding: 0 0 15 0;"); + + // 用户名标签 + usernameLabel = new Label(currentUsername != null ? currentUsername : "用户"); + usernameLabel.setFont(Font.font("Arial", FontWeight.BOLD, 16)); + usernameLabel.setStyle("-fx-text-fill: #2c3e50;"); + + // 间隔 + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + // 创建头像容器 + VBox avatarContainer = createAvatarContainer(); + + userInfoBar.getChildren().addAll(avatarContainer, usernameLabel, spacer); + + return userInfoBar; + } + + /** + * 创建头像容器,包含圆形头像背景和用户首字母. + * + * @return VBox 头像容器布局. + */ + private VBox createAvatarContainer() { + VBox avatarContainer = new VBox(); + avatarContainer.setAlignment(Pos.CENTER); + avatarContainer.setPrefSize(50, 50); + + // 添加首字母文本 + avatarText = new Text(getFirstLetter()); + avatarText.setFill(Color.WHITE); + avatarText.setFont(Font.font("Arial", FontWeight.BOLD, 16)); + + // 创建圆形头像背景 + Circle avatarCircle = createAvatarCircle(); + + // 使用StackPane将文本放在圆形中心 + javafx.scene.layout.StackPane avatarStack = + new javafx.scene.layout.StackPane(); + avatarStack.getChildren().addAll(avatarCircle, avatarText); + avatarStack.setPrefSize(44, 44); + avatarStack.setAlignment(Pos.CENTER); + + avatarContainer.getChildren().add(avatarStack); + return avatarContainer; + } + + /** + * 创建圆形头像背景. + * + * @return Circle 圆形头像背景对象. + */ + private Circle createAvatarCircle() { + Circle avatarCircle = new Circle(22); + avatarCircle.setFill(Color.web("#4CAF50")); + avatarCircle.setStroke(Color.WHITE); + avatarCircle.setStrokeWidth(2); + + // 添加阴影效果 + avatarCircle.setStyle("-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), " + + "5, 0.3, 2, 2);"); + return avatarCircle; + } + + /** + * 创建主菜单内容区域. + * + * @return VBox 菜单内容区域布局容器. + */ + private VBox createMenuContent() { + VBox menuContent = new VBox(25); + menuContent.setPadding(new Insets(20)); + menuContent.setAlignment(Pos.CENTER); + + // 创建标题区域 + VBox titleSection = createTitleSection(); + + // 创建功能按钮区域 + VBox buttonSection = createButtonSection(); + + menuContent.getChildren().addAll(titleSection, buttonSection); + + return menuContent; + } + + /** + * 创建标题区域,包含主标题和欢迎信息. + * + * @return VBox 标题区域布局容器. + */ + private VBox createTitleSection() { + VBox titleSection = new VBox(10); + titleSection.setAlignment(Pos.CENTER); + + Label titleLabel = new Label("数学学习软件"); + titleLabel.setFont(Font.font(28)); + titleLabel.setStyle("-fx-text-fill: #2c3e50;"); + + // 修改欢迎标签,显示用户名 + welcomeLabel = new Label("欢迎," + + (currentUsername != null ? currentUsername : "用户") + "!"); + welcomeLabel.setFont(Font.font(18)); + welcomeLabel.setStyle("-fx-text-fill: #7f8c8d;"); + + titleSection.getChildren().addAll(titleLabel, welcomeLabel); + return titleSection; + } + + /** + * 创建功能按钮区域. + * + * @return VBox 按钮区域布局容器. + */ + private VBox createButtonSection() { + VBox buttonSection = new VBox(15); + buttonSection.setAlignment(Pos.CENTER); + + Button startButton = createStartButton(); + Button changePasswordButton = createChangePasswordButton(); + Button logoutButton = createLogoutButton(); + + setupButtonActions(startButton, changePasswordButton, logoutButton); + + buttonSection.getChildren().addAll( + startButton, changePasswordButton, logoutButton + ); + + return buttonSection; + } + + /** + * 创建开始练习按钮. + * + * @return Button 开始练习按钮对象. + */ + private Button createStartButton() { + Button button = new Button("开始练习"); + button.setStyle("-fx-background-color: #4CAF50; -fx-text-fill: white; " + + "-fx-font-size: 16px; -fx-background-radius: 10;"); + button.setPrefSize(220, 55); + setupButtonHoverEffect(button, "#4CAF50", "#45a049"); + return button; + } + + /** + * 创建修改密码按钮. + * + * @return Button 修改密码按钮对象. + */ + private Button createChangePasswordButton() { + Button button = new Button("修改密码"); + button.setStyle("-fx-background-color: #2196F3; -fx-text-fill: white; " + + "-fx-font-size: 16px; -fx-background-radius: 10;"); + button.setPrefSize(220, 55); + setupButtonHoverEffect(button, "#2196F3", "#1976D2"); + return button; + } + + /** + * 创建退出登录按钮. + * + * @return Button 退出登录按钮对象. + */ + private Button createLogoutButton() { + Button button = new Button("退出登录"); + button.setStyle("-fx-background-color: #f44336; -fx-text-fill: white; " + + "-fx-font-size: 16px; -fx-background-radius: 10;"); + button.setPrefSize(220, 55); + setupButtonHoverEffect(button, "#f44336", "#d32f2f"); + return button; + } + + /** + * 设置按钮的悬停效果. + * + * @param button 需要设置悬停效果的按钮. + * @param normalColor 正常状态颜色. + * @param hoverColor 悬停状态颜色. + */ + private void setupButtonHoverEffect(Button button, String normalColor, + String hoverColor) { + String normalStyle = String.format( + "-fx-background-color: %s; -fx-text-fill: white; -fx-font-size: 16px; " + + "-fx-background-radius: 10;", normalColor); + String hoverStyle = String.format( + "-fx-background-color: %s; -fx-text-fill: white; -fx-font-size: 16px; " + + "-fx-background-radius: 10;", hoverColor); + + button.setOnMouseEntered(e -> button.setStyle(hoverStyle)); + button.setOnMouseExited(e -> button.setStyle(normalStyle)); + } + + /** + * 设置按钮的点击事件. + * + * @param startButton 开始练习按钮. + * @param changePasswordButton 修改密码按钮. + * @param logoutButton 退出登录按钮. + */ + private void setupButtonActions(Button startButton, + Button changePasswordButton, Button logoutButton) { + startButton.setOnAction(e -> sceneManager.showLevelSelectionView()); + changePasswordButton.setOnAction(e -> showChangePasswordDialog()); + logoutButton.setOnAction(e -> sceneManager.showLoginView()); + } + + /** + * 获取用户名的首字母,用于头像显示. + * + * @return String 用户名的首字母大写,如果用户名为空则返回"U". + */ + private String getFirstLetter() { + return currentUsername != null && !currentUsername.isEmpty() + ? currentUsername.substring(0, 1).toUpperCase() : "U"; + } + + /** + * 更新用户名显示. + * + * @param username 新的用户名. + */ + public void updateUsername(String username) { + this.currentUsername = username; + if (usernameLabel != null) { + usernameLabel.setText(username != null ? username : "用户"); + } + if (avatarText != null) { + avatarText.setText(getFirstLetter()); + } + if (welcomeLabel != null) { + welcomeLabel.setText("欢迎," + (username != null ? username : "用户") + "!"); + } + } + + /** + * 显示修改密码对话框. + */ + private void showChangePasswordDialog() { + javafx.scene.control.Dialog dialog = + new javafx.scene.control.Dialog<>(); + dialog.setTitle("修改密码"); + dialog.setHeaderText("请输入密码信息"); + dialog.setGraphic(null); // 移除默认图标 + + // 创建表单 + javafx.scene.layout.GridPane grid = createPasswordForm(); + dialog.getDialogPane().setContent(grid); + + // 添加按钮 + dialog.getDialogPane().getButtonTypes().addAll( + javafx.scene.control.ButtonType.OK, + javafx.scene.control.ButtonType.CANCEL + ); + + dialog.setResultConverter(dialogButton -> { + if (dialogButton == javafx.scene.control.ButtonType.OK) { + handlePasswordChange(grid); + } + return null; + }); + + dialog.showAndWait(); + } + + /** + * 创建密码修改表单. + * + * @return GridPane 密码修改表单布局. + */ + private javafx.scene.layout.GridPane createPasswordForm() { + javafx.scene.layout.GridPane grid = new javafx.scene.layout.GridPane(); + grid.setHgap(15); + grid.setVgap(15); + grid.setPadding(new Insets(20, 30, 10, 30)); + + javafx.scene.control.PasswordField oldPassword = + new javafx.scene.control.PasswordField(); + oldPassword.setPromptText("请输入原密码"); + oldPassword.setPrefWidth(200); + + javafx.scene.control.PasswordField newPassword = + new javafx.scene.control.PasswordField(); + newPassword.setPromptText("请输入新密码(密码要6-10位," + + "包含大,小写字母和数字)"); + newPassword.setPrefWidth(200); + + javafx.scene.control.PasswordField confirmPassword = + new javafx.scene.control.PasswordField(); + confirmPassword.setPromptText("请确认新密码"); + confirmPassword.setPrefWidth(200); + + grid.add(new Label("原密码:"), 0, 0); + grid.add(oldPassword, 1, 0); + grid.add(new Label("新密码:"), 0, 1); + grid.add(newPassword, 1, 1); + grid.add(new Label("确认密码:"), 0, 2); + grid.add(confirmPassword, 1, 2); + + return grid; + } + + /** + * 处理密码修改逻辑. + * + * @param grid 包含密码输入框的表单布局. + */ + private void handlePasswordChange(javafx.scene.layout.GridPane grid) { + javafx.scene.control.PasswordField oldPassword = + (javafx.scene.control.PasswordField) grid.getChildren().get(1); + javafx.scene.control.PasswordField newPassword = + (javafx.scene.control.PasswordField) grid.getChildren().get(3); + javafx.scene.control.PasswordField confirmPassword = + (javafx.scene.control.PasswordField) grid.getChildren().get(5); + + String oldPwd = oldPassword.getText(); + String newPwd = newPassword.getText(); + String confirmPwd = confirmPassword.getText(); + + if (!validatePasswordInput(oldPwd, newPwd, confirmPwd)) { + return; + } + + if (!Login.login(currentUsername, oldPwd)) { + showAlert("错误", "原密码错误!"); + return; + } + + if (!Register.isVaildPassword(newPwd)) { + showAlert("错误", "密码要6-10位,包含大,小写字母和数字"); + return; + } + + if (!ChangePassword.changePassword(currentUsername, confirmPwd)) { + showAlert("错误", "密码修改失败"); + } else { + showAlert("成功", "密码修改成功!"); + } + } + + /** + * 验证密码输入. + * + * @param oldPwd 原密码. + * @param newPwd 新密码. + * @param confirmPwd 确认密码. + * @return boolean 输入是否有效. + */ + private boolean validatePasswordInput(String oldPwd, String newPwd, + String confirmPwd) { + if (oldPwd.isEmpty() || newPwd.isEmpty() || confirmPwd.isEmpty()) { + showAlert("错误", "请填写所有密码字段!"); + return false; + } + + if (!newPwd.equals(confirmPwd)) { + showAlert("错误", "两次输入的新密码不一致!"); + return false; + } + + return true; + } + + /** + * 显示提示对话框. + * + * @param title 对话框标题. + * @param message 对话框内容. + */ + private void showAlert(String title, String message) { + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message); + alert.showAndWait(); + } + + /** + * 获取当前场景. + * + * @return Scene 主菜单界面的场景对象. + */ + public Scene getScene() { + return scene; + } +} \ No newline at end of file diff --git a/src/main/java/com/wsf/mathapp/view/QuestionCountView.java b/src/main/java/com/wsf/mathapp/view/QuestionCountView.java new file mode 100644 index 0000000..3b83f6f --- /dev/null +++ b/src/main/java/com/wsf/mathapp/view/QuestionCountView.java @@ -0,0 +1,219 @@ +package com.wsf.mathapp.view; + +import com.wsf.mathapp.controller.SceneManager; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.layout.VBox; +import javafx.scene.text.Font; + +/** + * 题目数量选择视图类,提供用户选择答题数量的界面. + * 允许用户输入10-30之间的题目数量,并开始答题. + */ +public class QuestionCountView { + private Scene scene; + private final SceneManager sceneManager; + private String level; + + /** + * 构造函数,初始化题目数量选择视图. + * + * @param sceneManager 场景管理器,用于界面导航. + */ + public QuestionCountView(SceneManager sceneManager) { + this.sceneManager = sceneManager; + createScene(); + } + + /** + * 创建主场景,包含题目数量选择界面. + */ + private void createScene() { + VBox root = new VBox(20); + root.setPadding(new Insets(40)); + root.setAlignment(Pos.CENTER); + + // 创建标题标签 + Label titleLabel = createTitleLabel(); + + // 创建级别显示标签 + Label levelLabel = createLevelLabel(); + + // 创建数量输入框 + TextField countField = createCountField(); + + // 创建开始答题按钮 + Button startButton = createStartButton(); + + // 创建返回按钮 + Button backButton = createBackButton(); + + // 创建状态标签 + Label statusLabel = new Label(); + + // 设置按钮事件 + setupStartButtonAction(startButton, countField, statusLabel); + setupBackButtonAction(backButton); + + root.getChildren().addAll( + titleLabel, levelLabel, countField, startButton, backButton, statusLabel + ); + + scene = new Scene(root, 400, 400); + } + + /** + * 创建标题标签. + * + * @return Label 标题标签对象. + */ + private Label createTitleLabel() { + Label titleLabel = new Label("选择题目数量"); + titleLabel.setFont(Font.font(20)); + return titleLabel; + } + + /** + * 创建级别显示标签. + * + * @return Label 级别显示标签对象. + */ + private Label createLevelLabel() { + Label levelLabel = new Label(); + levelLabel.setFont(Font.font(16)); + return levelLabel; + } + + /** + * 创建数量输入框. + * + * @return TextField 数量输入框对象. + */ + private TextField createCountField() { + TextField countField = new TextField(); + countField.setPromptText("请输入题目数量 (10-30)"); + countField.setMaxWidth(250); + countField.setPrefHeight(40); + return countField; + } + + /** + * 创建开始答题按钮. + * + * @return Button 开始答题按钮对象. + */ + private Button createStartButton() { + Button startButton = new Button("开始答题"); + startButton.setStyle("-fx-background-color: #4CAF50; -fx-text-fill: white; " + + "-fx-font-size: 14px;"); + startButton.setPrefSize(250, 45); + return startButton; + } + + /** + * 创建返回按钮. + * + * @return Button 返回按钮对象. + */ + private Button createBackButton() { + Button backButton = new Button("返回"); + backButton.setStyle("-fx-background-color: #757575; -fx-text-fill: white;"); + backButton.setPrefSize(250, 40); + return backButton; + } + + /** + * 设置开始答题按钮的点击事件. + * + * @param startButton 开始答题按钮. + * @param countField 数量输入框. + * @param statusLabel 状态标签. + */ + private void setupStartButtonAction(Button startButton, TextField countField, + Label statusLabel) { + startButton.setOnAction(e -> handleStartQuiz(countField, statusLabel)); + } + + /** + * 设置返回按钮的点击事件. + * + * @param backButton 返回按钮. + */ + private void setupBackButtonAction(Button backButton) { + backButton.setOnAction(e -> sceneManager.showLevelSelectionView()); + } + + /** + * 处理开始答题逻辑. + * + * @param countField 数量输入框. + * @param statusLabel 状态标签. + */ + private void handleStartQuiz(TextField countField, Label statusLabel) { + try { + String countText = countField.getText().trim(); + if (countText.isEmpty()) { + showError(statusLabel, "请输入题目数量!"); + return; + } + + int count = Integer.parseInt(countText); + if (count < 10 || count > 30) { + showError(statusLabel, "题目数量必须在10-30之间!"); + return; + } + + // 直接调用 showQuizView 并传递参数 + sceneManager.showQuizView(level, count); + + } catch (NumberFormatException ex) { + showError(statusLabel, "请输入有效的数字!"); + } + } + + /** + * 设置当前选择的题目级别. + * + * @param level 题目难度级别. + */ + public void setLevel(String level) { + this.level = level; + updateLevelDisplay(); + } + + /** + * 更新界面显示当前选择的级别. + */ + private void updateLevelDisplay() { + if (scene != null) { + VBox root = (VBox) scene.getRoot(); + Label levelLabel = (Label) root.getChildren().get(1); + levelLabel.setText("当前级别: " + level); + levelLabel.setStyle("-fx-text-fill: #2196F3;"); + } + } + + /** + * 显示错误信息. + * + * @param label 状态标签对象. + * @param message 错误信息内容. + */ + private void showError(Label label, String message) { + label.setText(message); + label.setStyle("-fx-text-fill: red;"); + } + + /** + * 获取当前场景. + * + * @return Scene 题目数量选择界面的场景对象. + */ + public Scene getScene() { + return scene; + } +} \ No newline at end of file diff --git a/src/main/java/com/wsf/mathapp/view/QuizView.java b/src/main/java/com/wsf/mathapp/view/QuizView.java new file mode 100644 index 0000000..4382fee --- /dev/null +++ b/src/main/java/com/wsf/mathapp/view/QuizView.java @@ -0,0 +1,338 @@ +package com.wsf.mathapp.view; + +import com.wsf.mathapp.controller.SceneManager; +import com.wsf.mathapp.service.QuestionService; +import com.ybw.mathapp.entity.QuestionWithOptions; +import com.ybw.mathapp.service.MultipleChoiceGenerator; +import com.ybw.mathapp.service.QuestionGenerator; +import java.util.List; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.RadioButton; +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.VBox; +import javafx.scene.text.Font; + + +/** + * 答题视图类,提供用户答题的界面. + * 显示题目、选项,并处理用户的答题逻辑和进度跟踪. + */ +public class QuizView { + private Scene scene; + private final SceneManager sceneManager; + private String currentLevel; + private int questionCount; + private List questions; + private int correctAnswers = 0; + + private Label questionLabel; + private ToggleGroup optionsGroup; + private VBox optionsContainer; + private Button nextButton; + private Label progressLabel; + private Label questionNumberLabel; + + /** + * 构造函数,初始化答题视图. + * + * @param sceneManager 场景管理器,用于界面导航. + */ + public QuizView(SceneManager sceneManager) { + this.sceneManager = sceneManager; + createScene(); + } + + /** + * 创建主场景,包含答题界面的所有组件. + */ + private void createScene() { + VBox root = new VBox(20); + root.setPadding(new Insets(30)); + root.setAlignment(Pos.TOP_CENTER); + + // 创建界面组件 + progressLabel = createProgressLabel(); + questionNumberLabel = createQuestionNumberLabel(); + questionLabel = createQuestionLabel(); + optionsContainer = createOptionsContainer(); + nextButton = createNextButton(); + + root.getChildren().addAll(progressLabel, questionNumberLabel, + questionLabel, optionsContainer, nextButton); + + scene = new Scene(root, 600, 500); + } + + /** + * 创建进度标签. + * + * @return Label 进度标签对象. + */ + private Label createProgressLabel() { + Label label = new Label(); + label.setFont(Font.font(14)); + return label; + } + + /** + * 创建题号标签. + * + * @return Label 题号标签对象. + */ + private Label createQuestionNumberLabel() { + Label label = new Label(); + label.setFont(Font.font(16)); + label.setStyle("-fx-text-fill: #666;"); + return label; + } + + /** + * 创建题目标签. + * + * @return Label 题目标签对象. + */ + private Label createQuestionLabel() { + Label label = new Label(); + label.setFont(Font.font(18)); + label.setWrapText(true); + label.setStyle("-fx-padding: 10 0 20 0;"); + return label; + } + + /** + * 创建选项容器. + * + * @return VBox 选项容器布局. + */ + private VBox createOptionsContainer() { + VBox container = new VBox(15); + container.setAlignment(Pos.CENTER_LEFT); + container.setPadding(new Insets(10)); + return container; + } + + /** + * 创建下一题按钮. + * + * @return Button 下一题按钮对象. + */ + private Button createNextButton() { + Button button = new Button("下一题"); + button.setStyle("-fx-background-color: #4CAF50; -fx-text-fill: white; " + + "-fx-font-size: 14px;"); + button.setPrefSize(150, 40); + button.setDisable(true); + return button; + } + + /** + * 设置测验参数并开始测验. + * + * @param level 题目难度级别. + * @param count 题目数量. + */ + public void setQuizParameters(String level, int count) { + System.out.println("设置测验参数 - 级别: " + level + ", 题目数量: " + count); + + this.currentLevel = level; + this.questionCount = count; + this.correctAnswers = 0; + + // 生成题目 + generateQuestions(); + + // 检查是否成功生成题目 + if (questions == null || questions.isEmpty()) { + System.out.println("题目生成失败,显示空状态"); + showEmptyState(); + return; + } + + System.out.println("成功生成 " + questions.size() + " 道题目,开始显示第一题"); + showQuestion(0); + } + + /** + * 生成题目列表. + */ + private void generateQuestions() { + try { + System.out.println("开始生成题目,级别: " + currentLevel + ", 数量: " + questionCount); + + QuestionGenerator questionGenerator = QuestionService.createGenerator(currentLevel); + if (questionGenerator == null) { + System.err.println("题目生成器创建失败,级别: " + currentLevel); + questions = java.util.Collections.emptyList(); + return; + } + + System.out.println("题目生成器创建成功: " + + questionGenerator.getClass().getSimpleName()); + + MultipleChoiceGenerator multipleChoiceGenerator = new MultipleChoiceGenerator( + questionGenerator, currentLevel); + questions = multipleChoiceGenerator.generateMultipleChoiceQuestions(questionCount); + + System.out.println("题目生成完成,数量: " + + (questions != null ? questions.size() : "null")); + + } catch (Exception e) { + System.err.println("生成题目时出现异常: " + e.getMessage()); + questions = java.util.Collections.emptyList(); + } + } + + /** + * 显示空状态界面. + */ + private void showEmptyState() { + VBox root = (VBox) scene.getRoot(); + root.getChildren().clear(); + + Label emptyLabel = new Label("无法生成题目,请返回重试"); + emptyLabel.setFont(Font.font(16)); + + Button backButton = new Button("返回"); + backButton.setOnAction(e -> sceneManager.showLevelSelectionView()); + + root.getChildren().addAll(emptyLabel, backButton); + } + + /** + * 显示指定索引的题目. + * + * @param index 题目索引. + */ + private void showQuestion(int index) { + if (index >= questions.size()) { + // 所有题目已回答,显示结果 + double score = (double) correctAnswers / questions.size(); + sceneManager.showResultView(score); + return; + } + + QuestionWithOptions question = questions.get(index); + updateQuestionDisplay(index, question); + setupOptions(question); + setupNextButton(index, question); + } + + /** + * 更新题目显示信息. + * + * @param index 题目索引. + * @param question 题目对象. + */ + private void updateQuestionDisplay(int index, QuestionWithOptions question) { + // 更新进度和题号 + progressLabel.setText("进度: " + (index + 1) + "/" + questions.size()); + questionNumberLabel.setText("第 " + (index + 1) + " 题"); + + // 设置题目内容 + String questionText = formatQuestionText(question.getQuestionText()); + questionLabel.setText(questionText + " = ?"); + } + + /** + * 格式化题目文本. + * + * @param questionText 原始题目文本. + * @return String 格式化后的题目文本. + */ + private String formatQuestionText(String questionText) { + // 移除末尾的 "=" + if (questionText.endsWith(" =")) { + questionText = questionText.substring(0, questionText.length() - 2); + } + return questionText; + } + + /** + * 设置选项. + * + * @param question 题目对象. + */ + private void setupOptions(QuestionWithOptions question) { + // 清空选项容器 + optionsContainer.getChildren().clear(); + optionsGroup = new ToggleGroup(); + + // 添加选项 + List options = question.getOptions(); + for (int i = 0; i < options.size(); i++) { + RadioButton radioButton = createOptionRadioButton(i, options.get(i)); + optionsContainer.getChildren().add(radioButton); + } + } + + /** + * 创建选项单选按钮. + * + * @param index 选项索引. + * @param optionText 选项文本. + * @return RadioButton 单选按钮对象. + */ + private RadioButton createOptionRadioButton(int index, String optionText) { + RadioButton radioButton = new RadioButton((char) ('A' + index) + ". " + optionText); + radioButton.setToggleGroup(optionsGroup); + radioButton.setFont(Font.font(14)); + radioButton.setWrapText(true); + radioButton.setPrefWidth(500); + return radioButton; + } + + /** + * 设置下一题按钮. + * + * @param index 当前题目索引. + * @param question 题目对象. + */ + private void setupNextButton(int index, QuestionWithOptions question) { + nextButton.setDisable(true); + nextButton.setText(index == questions.size() - 1 ? "提交答案" : "下一题"); + nextButton.setOnAction(e -> { + checkAnswer(question); + showQuestion(index + 1); + }); + + // 启用下一题按钮当选择了一个选项时 + setupOptionSelectionListener(); + } + + /** + * 设置选项选择监听器. + */ + private void setupOptionSelectionListener() { + optionsGroup.selectedToggleProperty().addListener((obs, oldVal, newVal) -> + nextButton.setDisable(newVal == null)); + } + + /** + * 检查答案是否正确. + * + * @param question 题目对象. + */ + private void checkAnswer(QuestionWithOptions question) { + RadioButton selectedRadioButton = (RadioButton) optionsGroup.getSelectedToggle(); + if (selectedRadioButton != null) { + int selectedIndex = optionsContainer.getChildren().indexOf(selectedRadioButton); + if (selectedIndex == question.getCorrectAnswerIndex()) { + correctAnswers++; + } + } + } + + /** + * 获取当前场景. + * + * @return Scene 答题界面的场景对象. + */ + public Scene getScene() { + return scene; + } + +} \ No newline at end of file diff --git a/src/main/java/com/wsf/mathapp/view/RegisterView.java b/src/main/java/com/wsf/mathapp/view/RegisterView.java new file mode 100644 index 0000000..fa515df --- /dev/null +++ b/src/main/java/com/wsf/mathapp/view/RegisterView.java @@ -0,0 +1,454 @@ +package com.wsf.mathapp.view; + +import com.wsf.mathapp.controller.SceneManager; +import com.ybw.mathapp.util.EmailService; +import com.ybw.mathapp.util.LoginFileUtils; +import com.ybw.mathapp.util.Register; +import java.util.regex.Pattern; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.scene.layout.VBox; +import javafx.scene.text.Font; + + +/** + * 注册视图类,提供用户注册功能的界面. + * 包含用户名、邮箱、验证码、密码等输入字段的验证和注册逻辑. + */ +public class RegisterView { + private Scene scene; + private final SceneManager sceneManager; + + // 将UI组件声明为类的成员变量 + private TextField usernameField; + private TextField emailField; + private TextField codeField; + private PasswordField passwordField; + private PasswordField confirmPasswordField; + private Label statusLabel; + private Button sendCodeButton; + + /** + * 构造函数,初始化注册视图. + * + * @param sceneManager 场景管理器,用于界面导航. + */ + public RegisterView(SceneManager sceneManager) { + this.sceneManager = sceneManager; + createScene(); + } + + /** + * 创建主场景,包含注册界面的所有组件. + */ + private void createScene() { + VBox root = new VBox(15); + root.setPadding(new Insets(30)); + root.setAlignment(Pos.CENTER); + + // 创建界面组件 + usernameField = createUsernameField(); + emailField = createEmailField(); + sendCodeButton = createSendCodeButton(); + codeField = createCodeField(); + passwordField = createPasswordField(); + confirmPasswordField = createConfirmPasswordField(); + Button registerButton = createRegisterButton(); + statusLabel = new Label(); + Button backButton = createBackButton(); + // 设置按钮事件 + setupSendCodeButtonAction(); + setupRegisterButtonAction(registerButton); + setupBackButtonAction(backButton); + Label titleLabel = createTitleLabel(); + root.getChildren().addAll( + titleLabel, + usernameField, + emailField, + sendCodeButton, + codeField, + passwordField, + confirmPasswordField, + registerButton, + backButton, + statusLabel + ); + + scene = new Scene(root, 400, 550); + } + + /** + * 创建标题标签. + * + * @return Label 标题标签对象. + */ + private Label createTitleLabel() { + Label titleLabel = new Label("用户注册"); + titleLabel.setFont(Font.font(20)); + return titleLabel; + } + + /** + * 创建用户名输入框. + * + * @return TextField 用户名输入框对象. + */ + private TextField createUsernameField() { + TextField field = new TextField(); + field.setPromptText("请输入用户名(3-20位字母、数字、下划线)"); + field.setMaxWidth(300); + field.setPrefHeight(35); + return field; + } + + /** + * 创建邮箱输入框. + * + * @return TextField 邮箱输入框对象. + */ + private TextField createEmailField() { + TextField field = new TextField(); + field.setPromptText("请输入邮箱"); + field.setMaxWidth(300); + field.setPrefHeight(35); + return field; + } + + /** + * 创建发送验证码按钮. + * + * @return Button 发送验证码按钮对象. + */ + private Button createSendCodeButton() { + Button button = new Button("发送验证码"); + button.setStyle("-fx-background-color: #FF9800; -fx-text-fill: white;"); + button.setPrefSize(120, 35); + return button; + } + + /** + * 创建验证码输入框. + * + * @return TextField 验证码输入框对象. + */ + private TextField createCodeField() { + TextField field = new TextField(); + field.setPromptText("请输入验证码"); + field.setMaxWidth(300); + field.setPrefHeight(35); + return field; + } + + /** + * 创建密码输入框. + * + * @return PasswordField 密码输入框对象. + */ + private PasswordField createPasswordField() { + PasswordField field = new PasswordField(); + field.setPromptText("请输入密码(6-10位,含大小写字母和数字)"); + field.setMaxWidth(300); + field.setPrefHeight(35); + return field; + } + + /** + * 创建确认密码输入框. + * + * @return PasswordField 确认密码输入框对象. + */ + private PasswordField createConfirmPasswordField() { + PasswordField field = new PasswordField(); + field.setPromptText("请再次输入密码"); + field.setMaxWidth(300); + field.setPrefHeight(35); + return field; + } + + /** + * 创建注册按钮. + * + * @return Button 注册按钮对象. + */ + private Button createRegisterButton() { + Button button = new Button("注册"); + button.setStyle("-fx-background-color: #4CAF50; -fx-text-fill: white; " + + "-fx-font-size: 14px;"); + button.setPrefSize(300, 40); + return button; + } + + /** + * 创建返回登录按钮. + * + * @return Button 返回登录按钮对象. + */ + private Button createBackButton() { + Button button = new Button("返回登录"); + button.setStyle("-fx-background-color: #757575; -fx-text-fill: white;"); + button.setPrefSize(300, 35); + return button; + } + + /** + * 设置发送验证码按钮的点击事件. + */ + private void setupSendCodeButtonAction() { + sendCodeButton.setOnAction(e -> handleSendVerificationCode()); + } + + /** + * 设置注册按钮的点击事件. + * + * @param registerButton 注册按钮对象. + */ + private void setupRegisterButtonAction(Button registerButton) { + registerButton.setOnAction(e -> handleRegistration()); + } + + /** + * 设置返回按钮的点击事件. + * + * @param backButton 返回按钮对象. + */ + private void setupBackButtonAction(Button backButton) { + backButton.setOnAction(e -> sceneManager.showLoginView()); + } + + /** + * 处理发送验证码逻辑. + */ + private void handleSendVerificationCode() { + String username = usernameField.getText().trim(); + String email = emailField.getText().trim(); + + if (!validateUsernameForCode(username)) { + return; + } + + if (!validateEmailForCode(email)) { + return; + } + + // 生成并发送验证码 + boolean sent = EmailService.sendCode(email); + if (sent) { + showSuccess(statusLabel, "验证码已发送到您的邮箱!"); + // 禁用发送按钮60秒 + sendCodeButton.setDisable(true); + startCountdown(sendCodeButton); + } else { + showError(statusLabel, "发送验证码失败,请稍后重试!"); + } + } + + /** + * 验证用户名用于发送验证码. + * + * @param username 用户名. + * @return boolean 用户名是否有效. + */ + private boolean validateUsernameForCode(String username) { + if (isInvalidUsername(username)) { + showError(statusLabel, "用户名只包含英文字母和数字)"); + return false; + } + + if (LoginFileUtils.isNameRegistered(username)) { + showError(statusLabel, "用户名已存在,请选择其他用户名!"); + return false; + } + + return true; + } + + /** + * 验证邮箱用于发送验证码. + * + * @param email 邮箱地址. + * @return boolean 邮箱是否有效. + */ + private boolean validateEmailForCode(String email) { + if (!EmailService.isValidEmail(email)) { + showError(statusLabel, "请输入有效的邮箱地址!"); + return false; + } + + if (LoginFileUtils.isEmailRegistered(email)) { + showError(statusLabel, "该邮箱已注册,请直接登录!"); + return false; + } + + return true; + } + + /** + * 处理用户注册逻辑. + */ + private void handleRegistration() { + String username = usernameField.getText().trim(); + String email = emailField.getText().trim(); + String code = codeField.getText().trim(); + String password = passwordField.getText(); + String confirmPassword = confirmPasswordField.getText(); + + if (!validateRegistrationInput(username, email, code, password, confirmPassword)) { + return; + } + + // 验证验证码 + if (!EmailService.verifyCode(email, code)) { + showError(statusLabel, "验证码错误或已过期!"); + return; + } + + if (!Register.isVaildPassword(password)) { + showError(statusLabel, "密码要6-10位,包含大,小写字母和数字"); + return; + } + + if (!Register.isEqualPassword(password, confirmPassword)) { + showError(statusLabel, "两次密码不一致"); + return; + } + + if (Register.register(username, email, password)) { + showSuccess(statusLabel, "注册成功!用户名: " + username); + sceneManager.setCurrentUserName(username); + sceneManager.showMainMenuView(); + } else { + showError(statusLabel, "注册失败,请检查信息!"); + } + } + + /** + * 验证注册输入字段. + * + * @param username 用户名. + * @param email 邮箱. + * @param code 验证码. + * @param password 密码. + * @param confirmPassword 确认密码. + * @return boolean 输入是否有效. + */ + private boolean validateRegistrationInput(String username, String email, + String code, String password, String confirmPassword) { + if (isInvalidUsername(username)) { + showError(statusLabel, "用户名只包含英文字母和数字"); + return false; + } + + if (LoginFileUtils.isNameRegistered(username)) { + showError(statusLabel, "用户名已存在,请选择其他用户名!"); + return false; + } + + if (!EmailService.isValidEmail(email)) { + showError(statusLabel, "请输入有效的邮箱地址!"); + return false; + } + + if (LoginFileUtils.isEmailRegistered(email)) { + showError(statusLabel, "该邮箱已注册,请直接登录!"); + return false; + } + + if (username.isEmpty() || email.isEmpty() || code.isEmpty() + || password.isEmpty() || confirmPassword.isEmpty()) { + showError(statusLabel, "请填写所有字段!"); + return false; + } + + return true; + } + + /** + * 清空所有输入字段和状态信息. + */ + public void clearFields() { + usernameField.clear(); + emailField.clear(); + codeField.clear(); + passwordField.clear(); + confirmPasswordField.clear(); + statusLabel.setText(""); + + // 重置发送验证码按钮状态 + sendCodeButton.setText("发送验证码"); + sendCodeButton.setDisable(false); + } + + /** + * 开始倒计时. + * + * @param button 需要倒计时的按钮. + */ + private void startCountdown(Button button) { + new Thread(() -> { + try { + for (int i = 60; i > 0; i--) { + int finalI = i; + javafx.application.Platform.runLater(() -> button.setText(finalI + "秒后重发")); + Thread.sleep(1000); + } + javafx.application.Platform.runLater(() -> { + button.setText("发送验证码"); + button.setDisable(false); + }); + } catch (InterruptedException ex) { + // ex.printStackTrace(); + } + }).start(); + } + + /** + * 显示错误信息. + * + * @param label 状态标签对象. + * @param message 错误信息内容. + */ + private void showError(Label label, String message) { + label.setText(message); + label.setStyle("-fx-text-fill: red;"); + } + + /** + * 显示成功信息. + * + * @param label 状态标签对象. + * @param message 成功信息内容. + */ + private void showSuccess(Label label, String message) { + label.setText(message); + label.setStyle("-fx-text-fill: green;"); + } + + /** + * 验证用户名格式. + * + * @param username 用户名. + * @return boolean 用户名格式是否有效. + */ + public static boolean isInvalidUsername(String username) { + if (username == null || username.trim().isEmpty()) { + return true; + } + // 用户名规则:只包含英文字母和数字,不限制长度 + String usernameRegex = "^[a-zA-Z0-9]+$"; + return !Pattern.matches(usernameRegex, username.trim()); + } + + /** + * 获取当前场景. + * + * @return Scene 注册界面的场景对象. + */ + public Scene getScene() { + return scene; + } +} \ No newline at end of file diff --git a/src/main/java/com/wsf/mathapp/view/ResultView.java b/src/main/java/com/wsf/mathapp/view/ResultView.java new file mode 100644 index 0000000..0e3f705 --- /dev/null +++ b/src/main/java/com/wsf/mathapp/view/ResultView.java @@ -0,0 +1,278 @@ +package com.wsf.mathapp.view; + +import com.wsf.mathapp.controller.SceneManager; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.scene.text.Font; + +/** + * 结果视图类,显示用户答题结果的界面. + * 包含分数显示、评语和导航按钮等功能. + */ +public class ResultView { + private Scene scene; + private final SceneManager sceneManager; + private double score; + + /** + * 构造函数,初始化结果视图. + * + * @param sceneManager 场景管理器,用于界面导航. + */ + public ResultView(SceneManager sceneManager) { + this.sceneManager = sceneManager; + createScene(); + } + + /** + * 创建主场景,包含用户信息栏和结果内容区域. + */ + private void createScene() { + // 创建主容器 + VBox mainContainer = new VBox(); + mainContainer.setPadding(new Insets(20)); + + // 创建顶部用户信息栏 + HBox userInfoBar = createUserInfoBar(); + + // 创建结果内容区域 + VBox resultContent = createResultContent(); + + mainContainer.getChildren().addAll(userInfoBar, resultContent); + + scene = new Scene(mainContainer, 500, 550); + } + + /** + * 创建用户信息栏. + * + * @return HBox 用户信息栏布局容器. + */ + private HBox createUserInfoBar() { + HBox userInfoBar = new HBox(10); + userInfoBar.setAlignment(Pos.CENTER_LEFT); + userInfoBar.setPadding(new Insets(0, 0, 20, 0)); + return userInfoBar; + } + + /** + * 创建结果内容区域. + * + * @return VBox 结果内容区域布局容器. + */ + private VBox createResultContent() { + VBox resultContent = new VBox(30); + resultContent.setPadding(new Insets(20)); + resultContent.setAlignment(Pos.CENTER); + + // 创建标题区域 + Label titleLabel = createTitleLabel(); + + // 创建分数显示区域 + VBox scoreSection = createScoreSection(); + + // 创建按钮区域 + VBox buttonSection = createButtonSection(); + + resultContent.getChildren().addAll(titleLabel, scoreSection, buttonSection); + + return resultContent; + } + + /** + * 创建标题标签. + * + * @return Label 标题标签对象. + */ + private Label createTitleLabel() { + Label titleLabel = new Label("答题完成"); + titleLabel.setFont(Font.font(24)); + return titleLabel; + } + + /** + * 创建分数显示区域. + * + * @return VBox 分数显示区域布局容器. + */ + private VBox createScoreSection() { + VBox scoreSection = new VBox(15); + scoreSection.setAlignment(Pos.CENTER); + + Label scoreLabel = new Label(); + scoreLabel.setFont(Font.font(48)); + + Label messageLabel = new Label(); + messageLabel.setFont(Font.font(18)); + + scoreSection.getChildren().addAll(scoreLabel, messageLabel); + return scoreSection; + } + + /** + * 创建按钮区域. + * + * @return VBox 按钮区域布局容器. + */ + private VBox createButtonSection() { + VBox buttonSection = new VBox(15); + buttonSection.setAlignment(Pos.CENTER); + + Button restartButton = createRestartButton(); + Button mainMenuButton = createMainMenuButton(); + Button exitButton = createExitButton(); + + setupButtonActions(restartButton, mainMenuButton, exitButton); + + buttonSection.getChildren().addAll(restartButton, mainMenuButton, exitButton); + return buttonSection; + } + + /** + * 创建再次练习按钮. + * + * @return Button 再次练习按钮对象. + */ + private Button createRestartButton() { + Button button = new Button("再次练习"); + button.setStyle("-fx-background-color: #4CAF50; -fx-text-fill: white; " + + "-fx-font-size: 16px;"); + button.setPrefSize(200, 50); + return button; + } + + /** + * 创建返回主菜单按钮. + * + * @return Button 返回主菜单按钮对象. + */ + private Button createMainMenuButton() { + Button button = new Button("返回主菜单"); + button.setStyle("-fx-background-color: #2196F3; -fx-text-fill: white; " + + "-fx-font-size: 16px;"); + button.setPrefSize(200, 50); + return button; + } + + /** + * 创建退出系统按钮. + * + * @return Button 退出系统按钮对象. + */ + private Button createExitButton() { + Button button = new Button("退出系统"); + button.setStyle("-fx-background-color: #757575; -fx-text-fill: white; " + + "-fx-font-size: 16px;"); + button.setPrefSize(200, 50); + return button; + } + + /** + * 设置按钮的点击事件. + * + * @param restartButton 再次练习按钮. + * @param mainMenuButton 返回主菜单按钮. + * @param exitButton 退出系统按钮. + */ + private void setupButtonActions(Button restartButton, Button mainMenuButton, + Button exitButton) { + restartButton.setOnAction(e -> sceneManager.showLevelSelectionView()); + mainMenuButton.setOnAction(e -> sceneManager.showMainMenuView()); + exitButton.setOnAction(e -> System.exit(0)); + } + + /** + * 设置分数并更新显示. + * + * @param score 用户得分(0-1之间的比例). + */ + public void setScore(double score) { + this.score = score; + updateScoreDisplay(); + } + + /** + * 更新分数显示. + */ + private void updateScoreDisplay() { + if (scene != null) { + VBox mainContainer = (VBox) scene.getRoot(); + VBox resultContent = (VBox) mainContainer.getChildren().get(1); + VBox scoreSection = (VBox) resultContent.getChildren().get(1); + + Label scoreLabel = (Label) scoreSection.getChildren().get(0); + Label messageLabel = (Label) scoreSection.getChildren().get(1); + + updateScoreLabel(scoreLabel); + updateMessageLabel(messageLabel); + } + } + + /** + * 更新分数标签. + * + * @param scoreLabel 分数标签对象. + */ + private void updateScoreLabel(Label scoreLabel) { + int percentage = (int) (score * 100); + scoreLabel.setText(percentage + "分"); + applyScoreColor(scoreLabel); + } + + /** + * 更新评语标签. + * + * @param messageLabel 评语标签对象. + */ + private void updateMessageLabel(Label messageLabel) { + messageLabel.setText(getScoreMessage()); + } + + /** + * 根据分数应用相应的颜色. + * + * @param scoreLabel 分数标签对象. + */ + private void applyScoreColor(Label scoreLabel) { + if (score >= 0.9) { + scoreLabel.setStyle("-fx-text-fill: #4CAF50;"); + } else if (score >= 0.7) { + scoreLabel.setStyle("-fx-text-fill: #FF9800;"); + } else if (score >= 0.6) { + scoreLabel.setStyle("-fx-text-fill: #FFC107;"); + } else { + scoreLabel.setStyle("-fx-text-fill: #f44336;"); + } + } + + /** + * 根据分数获取相应的评语. + * + * @return String 对应的评语. + */ + private String getScoreMessage() { + if (score >= 0.9) { + return "优秀!表现非常出色!"; + } else if (score >= 0.7) { + return "良好!继续加油!"; + } else if (score >= 0.6) { + return "及格!还有进步空间!"; + } else { + return "需要多加练习!"; + } + } + + /** + * 获取当前场景. + * + * @return Scene 结果界面的场景对象. + */ + public Scene getScene() { + return scene; + } +} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/config/EmailConfig.java b/src/main/java/com/ybw/mathapp/config/EmailConfig.java new file mode 100644 index 0000000..d4ba5df --- /dev/null +++ b/src/main/java/com/ybw/mathapp/config/EmailConfig.java @@ -0,0 +1,13 @@ +package com.ybw.mathapp.config; + +public class EmailConfig { + + // 发件人邮箱配置(以QQ邮箱为例) + public static final String SMTP_HOST = "smtp.qq.com"; + public static final String SMTP_PORT = "587"; + public static final String SENDER_EMAIL = "1798231811@qq.com"; // 替换为你的邮箱 + public static final String SENDER_PASSWORD = "dzmfirotgnlceeae"; // 替换为你的授权码 + + public static final String EMAIL_SUBJECT = "【用户注册】验证码"; + public static final int CODE_EXPIRY_MINUTES = 5; +} diff --git a/src/main/java/com/ybw/mathapp/entity/QuestionWithOptions.java b/src/main/java/com/ybw/mathapp/entity/QuestionWithOptions.java new file mode 100644 index 0000000..1c822a0 --- /dev/null +++ b/src/main/java/com/ybw/mathapp/entity/QuestionWithOptions.java @@ -0,0 +1,119 @@ +package com.ybw.mathapp.entity; + +import java.util.List; + +/** + * 选择题实体类,用于封装题干、选项列表和正确答案的索引。 + * + * @author 杨博文 + * @since 2025 + */ +public class QuestionWithOptions { + + /** + * 题干文本。 + */ + private String questionText; + /** + * 选项列表。 + */ + private List options; + /** + * 正确答案的索引 (0-based)。 + */ + private int correctAnswerIndex; + + /** + * 构造函数。 + * + * @param questionText 题干文本 + * @param options 选项列表 + * @param correctAnswerIndex 正确答案的索引 (0-based) + * @throws IllegalArgumentException 如果 options 为 null 或索引超出范围 + */ + public QuestionWithOptions(String questionText, List options, int correctAnswerIndex) { + if (options == null) { + throw new IllegalArgumentException("选项列表不能为 null"); + } + if (correctAnswerIndex < 0 || correctAnswerIndex >= options.size()) { + throw new IllegalArgumentException("正确答案索引超出选项范围"); + } + this.questionText = questionText; + this.options = options; + this.correctAnswerIndex = correctAnswerIndex; + } + + /** + * 获取题干文本。 + * + * @return 题干文本 + */ + public String getQuestionText() { + return questionText; + } + + /** + * 设置题干文本。 + * + * @param questionText 要设置的题干文本 + */ + public void setQuestionText(String questionText) { + this.questionText = questionText; + } + + /** + * 获取选项列表。 + * + * @return 选项列表 + */ + public List getOptions() { + return options; + } + + /** + * 设置选项列表。 + * + * @param options 要设置的选项列表 + * @throws IllegalArgumentException 如果 options 为 null + */ + public void setOptions(List options) { + if (options == null) { + throw new IllegalArgumentException("选项列表不能为 null"); + } + this.options = options; + } + + /** + * 获取正确答案的索引。 + * + * @return 正确答案的索引 (0-based) + */ + public int getCorrectAnswerIndex() { + return correctAnswerIndex; + } + + /** + * 设置正确答案的索引。 + * + * @param correctAnswerIndex 要设置的正确答案索引 (0-based) + * @throws IllegalArgumentException 如果索引超出选项范围 + */ + public void setCorrectAnswerIndex(int correctAnswerIndex) { + if (correctAnswerIndex < 0 || correctAnswerIndex >= options.size()) { + throw new IllegalArgumentException("正确答案索引超出选项范围"); + } + this.correctAnswerIndex = correctAnswerIndex; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Question: ").append(questionText).append("\n"); + for (int i = 0; i < options.size(); i++) { + sb.append("Option ").append((char) ('A' + i)).append(": ").append(options.get(i)) + .append("\n"); + } + sb.append("Correct Answer: ").append((char) ('A' + correctAnswerIndex)); + return sb.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/entity/User.java b/src/main/java/com/ybw/mathapp/entity/User.java new file mode 100644 index 0000000..b886577 --- /dev/null +++ b/src/main/java/com/ybw/mathapp/entity/User.java @@ -0,0 +1,134 @@ +package com.ybw.mathapp.entity; + +/** + * 用户实体类,表示系统中的用户信息。 + * + *

该类包含用户的基本信息,如用户名、密码和学习级别。 + * 用户级别可以是小学、初中或高中。 + * + * @author 杨博文 + * @version 1.0 + * @since 2025 + */ +public class User { + + /** + * 用户名,不可修改。 + */ + private final String name; + + /** + * 邮箱,不可修改。 + */ + private final String email; + + /** + * 用户密码,不可修改。 + */ + private final String password; + + /** + * 用户当前的学习级别,可以修改。 + */ + private String level; + + /** + * 构造一个新的用户对象。 + * + * @param name 用户名,不能为空 + * @param email 邮箱,不能为空 + * @param password 用户密码,不能为空 + * @throws IllegalArgumentException 如果 name、email 或 password 为 null 或空字符串 + */ + public User(String name, String email, String password) { + if (name == null || name.trim().isEmpty()) { + throw new IllegalArgumentException("用户名不能为空"); + } + if (email == null || email.trim().isEmpty()) { + throw new IllegalArgumentException("邮箱不能为空"); + } + if (password == null || password.trim().isEmpty()) { + throw new IllegalArgumentException("密码不能为空"); + } + this.name = name; + this.email = email; + this.password = password; + } + + /** + * 从字符串创建用户对象。 + * + *

字符串格式应为 "name,email,password"。 + * + * @param line 包含用户信息的字符串 + * @return 解析成功的用户对象,如果解析失败则返回 null + */ + public static User fromString(String line) { + if (line == null || line.trim().isEmpty()) { + return null; + } + + String[] parts = line.split(",", 3); // 最多分割成3部分 + if (parts.length == 3) { + return new User(parts[0].trim(), parts[1].trim(), parts[2].trim()); + } + return null; + } + + /** + * 获取用户名。 + * + * @return 用户名 + */ + public String getName() { + return name; + } + + /** + * 获取用户密码。 + * + * @return 用户密码 + */ + public String getPassword() { + return password; + } + + /** + * 获取用户当前的学习级别。 + * + * @return 用户学习级别 + */ + public String getLevel() { + return level; + } + + /** + * 设置用户的学习级别。 + * + * @param newLevel 新的学习级别,支持"小学"、"初中"、"高中" + */ + public void setLevel(String newLevel) { + level = newLevel; + } + + /** + * 获取用户邮箱。 + * + * @return 用户邮箱 + */ + public String getEmail() { + return email; + } + + /** + * 返回用户信息的字符串表示。 + * + *

格式为 "name,email,password"。 + * + * @return 包含用户名、邮箱和密码的字符串 + */ + @Override + public String toString() { + return name + "," + email + "," + password; + } +} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/service/AdvancedCaculate.java b/src/main/java/com/ybw/mathapp/service/AdvancedCaculate.java new file mode 100644 index 0000000..2f41901 --- /dev/null +++ b/src/main/java/com/ybw/mathapp/service/AdvancedCaculate.java @@ -0,0 +1,248 @@ +package com.ybw.mathapp.service; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Stack; + +/** + * 扩展的计算类,支持基础四则运算、括号、平方、开根号、三角函数 (sin, cos, tan)。 + * + *

运算符优先级定义如下: + *

    + *
  1. 括号 {@code ( )} 优先级最高。
  2. + *
  3. 高级运算符 (平方, 开根号, sin, cos, tan) 优先级高于四则运算。
  4. + *
  5. 平方是后置运算符,开根号和三角函数是前置运算符。
  6. + *
+ * + *

例如: + *

    + *
  • {@code "3 + 开根号 16 平方"} 解释为 {@code "3 + (sqrt(16))^2"} = 19
  • + *
  • {@code "开根号 ( 4 + 5 ) 平方"} 解释为 {@code "(sqrt(4+5))^2"} = 9
  • + *
  • {@code "sin 30 + 5"} 解释为 {@code "sin(30) + 5"} (假设30是度数)
  • + *
+ * + *

注意: 在计算开根号时,若操作数为负数,则抛出 ArithmeticException。 + * + * @author 杨博文 + * @since 2025 + */ +public class AdvancedCaculate { + + private static final Set BASIC_OPERATORS = new HashSet<>( + Arrays.asList("+", "-", "*", "/")); + private static final Set ADVANCED_OPERATORS = new HashSet<>( + Arrays.asList("平方", "开根号", "sin", "cos", "tan")); + private static final Map PRECEDENCE = new HashMap<>(); + + static { + PRECEDENCE.put("+", 1); + PRECEDENCE.put("-", 1); + PRECEDENCE.put("*", 2); + PRECEDENCE.put("/", 2); + // 高级运算符优先级更高 + PRECEDENCE.put("平方", 3); // 后置 + PRECEDENCE.put("开根号", 3); // 前置 + PRECEDENCE.put("sin", 3); // 前置 + PRECEDENCE.put("cos", 3); // 前置 + PRECEDENCE.put("tan", 3); // 前置 + } + + /** + * 计算给定表达式的值。 + * + *

表达式分词列表例如: ["3", "+", "开根号", "16", "平方"] + * + * @param expressionTokens 表达式的分词列表 + * @return 计算结果 + * @throws IllegalArgumentException 如果表达式无效(例如括号不匹配、操作数不足) + * @throws ArithmeticException 如果计算过程中出现错误(如除零、负数开根号) + */ + public static double calculate(List expressionTokens) { + Stack numberStack = new Stack<>(); + Stack operatorStack = new Stack<>(); + + for (String token : expressionTokens) { + if (isNumeric(token)) { + numberStack.push(Double.parseDouble(token)); + } else if (token.equals("(")) { + operatorStack.push(token); + } else if (token.equals(")")) { + handleClosingParenthesis(numberStack, operatorStack); + } else if (ADVANCED_OPERATORS.contains(token)) { + handleAdvancedOperator(token, numberStack, operatorStack); + } else if (BASIC_OPERATORS.contains(token)) { + handleBasicOperator(token, numberStack, operatorStack); + } else { + throw new IllegalArgumentException("Unknown token: " + token); + } + } + + // 处理栈中剩余的所有运算符 + while (!operatorStack.isEmpty()) { + String op = operatorStack.pop(); + if (op.equals("(") || op.equals(")")) { + throw new IllegalArgumentException("Mismatched parentheses in expression."); + } + processOperator(numberStack, op); + } + + if (numberStack.size() != 1 || !operatorStack.isEmpty()) { + throw new IllegalArgumentException( + "Invalid expression: " + String.join(" ", expressionTokens)); + } + return numberStack.pop(); + } + + /** + * 处理遇到右括号 ')' 的情况 + */ + private static void handleClosingParenthesis(Stack numberStack, + Stack operatorStack) { + while (!operatorStack.isEmpty() && !operatorStack.peek().equals("(")) { + processOperator(numberStack, operatorStack.pop()); + } + if (!operatorStack.isEmpty()) { // Pop the "(" + operatorStack.pop(); + } else { + throw new IllegalArgumentException("Mismatched parentheses in expression."); + } + } + + /** + * 处理高级运算符(开根号, sin, cos, tan, 平方) + */ + private static void handleAdvancedOperator(String token, Stack numberStack, + Stack operatorStack) { + if ("平方".equals(token)) { + if (numberStack.isEmpty()) { + throw new IllegalArgumentException("Invalid expression: '平方' lacks an operand."); + } + double operand = numberStack.pop(); + numberStack.push(Math.pow(operand, 2)); + } else { // "开根号", "sin", "cos", "tan" 是前置运算符 + operatorStack.push(token); + } + } + + /** + * 处理基础四则运算符(+, -, *, /) + */ + private static void handleBasicOperator(String token, Stack numberStack, + Stack operatorStack) { + // 处理四则运算符,遵循优先级 + while (!operatorStack.isEmpty() + && !operatorStack.peek().equals("(") + && PRECEDENCE.get(token) <= PRECEDENCE.getOrDefault(operatorStack.peek(), 0)) { + processOperator(numberStack, operatorStack.pop()); + } + operatorStack.push(token); + } + + /** + * 执行一次运算操作。 + * + * @param numberStack 数字栈 + * @param operator 运算符 + * @throws IllegalArgumentException 如果运算符缺少操作数或为未知运算符 + * @throws ArithmeticException 如果计算过程中出现错误(如除零、负数开根号) + */ + private static void processOperator(Stack numberStack, String operator) { + if (numberStack.isEmpty()) { + throw new IllegalArgumentException( + "Invalid expression: operator '" + operator + "' lacks operand(s)."); + } + + if (ADVANCED_OPERATORS.contains(operator)) { + processAdvancedOperator(numberStack, operator); + } else if (BASIC_OPERATORS.contains(operator)) { + processBasicOperator(numberStack, operator); + } else { + throw new IllegalArgumentException("Unexpected operator in process: " + operator); + } + } + + /** + * 执行高级运算操作(开根号, sin, cos, tan) + */ + private static void processAdvancedOperator(Stack numberStack, String operator) { + double operand = numberStack.pop(); + double result; + switch (operator) { + case "开根号": + if (operand < 0) { + // 抛出异常,让调用者(MultipleChoiceGenerator)处理 + throw new ArithmeticException("Cannot take square root of negative number: " + operand); + } + result = Math.sqrt(operand); + break; + case "sin": + result = Math.sin(Math.toRadians(operand)); // 假设输入是度数 + break; + case "cos": + result = Math.cos(Math.toRadians(operand)); + break; + case "tan": + // tan(90 + n*180) 会趋向无穷,这里不特别处理,让其返回 Infinity 或 -Infinity + result = Math.tan(Math.toRadians(operand)); + break; + default: + throw new IllegalArgumentException("Unknown advanced operator: " + operator); + } + numberStack.push(result); + } + + /** + * 执行基础四则运算操作(+, -, *, /) + */ + private static void processBasicOperator(Stack numberStack, String operator) { + if (numberStack.size() < 2) { + throw new IllegalArgumentException( + "Invalid expression: operator '" + operator + "' lacks operand(s)."); + } + double b = numberStack.pop(); + double a = numberStack.pop(); + double result; + switch (operator) { + case "+": + result = a + b; + break; + case "-": + result = a - b; + break; + case "*": + result = a * b; + break; + case "/": + if (b == 0) { + throw new ArithmeticException("Division by zero"); + } + result = a / b; + break; + default: + throw new IllegalArgumentException("Unknown operator: " + operator); + } + numberStack.push(result); + } + + /** + * 判断给定字符串是否为数字。 + * + * @param str 待判断的字符串 + * @return 如果是数字则返回 true,否则返回 false + */ + public static boolean isNumeric(String str) { + if (str == null || str.isEmpty()) { + return false; + } + try { + Double.parseDouble(str); + return true; + } catch (NumberFormatException e) { + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/service/JuniorHighGenerator.java b/src/main/java/com/ybw/mathapp/service/JuniorHighGenerator.java new file mode 100644 index 0000000..a14790e --- /dev/null +++ b/src/main/java/com/ybw/mathapp/service/JuniorHighGenerator.java @@ -0,0 +1,214 @@ +package com.ybw.mathapp.service; + +import static com.ybw.mathapp.service.AdvancedCaculate.isNumeric; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * 初中题目生成器,负责生成包含平方或开根号运算的初中级别数学题目。 + * + *

该生成器确保每道题目都包含至少一个高级运算符(平方或开根号), + * 题目结构包含基本的四则运算和高级运算的组合。 + * + * @author 杨博文 + * @version 1.1 + * @since 2025 + */ +public class JuniorHighGenerator implements QuestionGenerator { + + /** + * 高级运算符数组,包含"平方"和"开根号"。 + */ + private static final String[] ADVANCED_OPS = {"平方", "开根号"}; + + /** + * 基本运算符数组,包含四则运算符号。 + */ + private static final String[] OPERATORS = {"+", "-", "*", "/"}; + + /** + * 随机数生成器,用于生成随机题目。 + */ + private final Random random = new Random(); + + @Override + public List generateQuestions(int count) { + List questions = new ArrayList<>(); + for (int i = 0; i < count; i++) { + String question = generateSingleQuestion(); + questions.add(question); + } + return questions; + } + + /** + * 生成单个初中级别的数学题目。 + * + *

该方法确保生成的题目包含至少一个高级运算符(平方或开根号), + * 并根据操作数数量采用不同的生成策略。 + * + * @return 生成的数学题目字符串 + */ + private String generateSingleQuestion() { + List parts = new ArrayList<>(); + int operandCount = random.nextInt(5) + 1; + parts = generateBase(operandCount, parts); + + boolean hasAdvancedOp = false; + + if (operandCount == 1) { + // 对于单个操作数,直接添加高级运算符 + if ("平方".equals(ADVANCED_OPS[random.nextInt(ADVANCED_OPS.length)])) { + parts.add("平方"); + } else { + // 为单个操作数的开根号添加括号 + parts.set(0, "开根号 ( " + parts.get(0) + " )"); + } + hasAdvancedOp = true; + } else { + // 遍历所有可能的操作数位置 (索引为偶数) + // 修复:循环条件确保最后一个操作数也能被检查 + for (int i = 0; i < parts.size(); i += 2) { // 只检查操作数索引 (0, 2, 4, ...) + // 该位置要为操作数且随机添加括号 + if (isNumeric(parts.get(i)) && random.nextBoolean()) { + // 随机选择高级运算符 + if ("开根号".equals(ADVANCED_OPS[random.nextInt(ADVANCED_OPS.length)])) { + parts = generateRoot(parts, i); + } else { // 平方运算 + parts = generateSquare(parts, i); + } + hasAdvancedOp = true; + break; // 添加成功后退出循环 + } + } + } + + // 启动保底强制加入一个高级运算符 + if (!hasAdvancedOp) { + parts = forceAddAdvancedOp(parts); + } + return String.join(" ", parts) + " ="; + } + + /** + * 生成基本的四则运算表达式部分。 + * + *

该方法生成指定数量的操作数和运算符,构成基础的数学表达式。 + * + * @param operandCount 操作数的数量 + * @param parts 用于存储表达式各部分的列表 + * @return 包含基本运算表达式的列表 + */ + public List generateBase(int operandCount, List parts) { + for (int i = 0; i < operandCount; i++) { + int num = random.nextInt(100) + 1; + parts.add(String.valueOf(num)); + if (i < operandCount - 1) { + parts.add(OPERATORS[random.nextInt(OPERATORS.length)]); + } + } + return parts; + } + + /** + * 强制在表达式中添加一个高级运算符作为保底机制。 + * + *

当随机生成过程中没有添加高级运算符时,使用此方法确保 + * 每道题目都包含至少一个高级运算符。 + * + * @param parts 包含表达式各部分的列表 + * @return 添加了高级运算符的表达式列表 + */ + public List forceAddAdvancedOp(List parts) { + String advancedOp = ADVANCED_OPS[random.nextInt(ADVANCED_OPS.length)]; + if ("平方".equals(advancedOp)) { + // 对整个表达式进行平方 + parts.add(0, "("); // 在开头添加左括号 + parts.add(") 平方"); // 在末尾添加右括号和"平方" + } else { // 开根号 + // 对整个表达式进行开根号 + parts.add(0, "开根号 ( "); // 在开头添加"开根号 ( " + parts.add(" )"); // 在末尾添加" )" + } + return parts; + } + + /** + * 在指定位置生成开根号运算。 + * + *

该方法在表达式指定位置添加开根号运算,可能只对单个操作数 + * 进行开根号,或者对一段子表达式进行开根号。 + * + * @param parts 包含表达式各部分的列表 + * @param i 开根号运算的起始位置 (操作数索引) + * @return 添加了开根号运算的表达式列表 + */ + public List generateRoot(List parts, int i) { + if (random.nextBoolean()) { + // 对单个操作数进行开根号 + parts.set(i, "开根号 ( " + parts.get(i) + " )"); + } else { + // 对子表达式进行开根号 + parts.set(i, "开根号 ( " + parts.get(i)); // 在起始操作数前添加左括号和"开根号 (" + int endIndex = findMatchingEndIndex(parts, i); // 查找匹配的结束操作数索引 + String currentEnd = parts.get(endIndex); + parts.set(endIndex, currentEnd + " )"); // 在结束操作数后添加右括号 + } + return parts; + } + + /** + * 在指定位置生成平方运算。 + * + *

该方法在表达式指定位置添加平方运算,对一段子表达式进行平方运算。 + * + * @param parts 包含表达式各部分的列表 + * @param i 平方运算的起始位置 (操作数索引) + * @return 添加了平方运算的表达式列表 + */ + public List generateSquare(List parts, int i) { + parts.set(i, "(" + parts.get(i)); // 在起始操作数前添加左括号 + int endIndex = findMatchingEndIndex(parts, i); // 查找匹配的结束操作数索引 + String currentEnd = parts.get(endIndex); + parts.set(endIndex, currentEnd + " ) 平方"); // 在结束操作数后添加右括号和"平方" + return parts; + } + + /** + * 辅助方法:查找子表达式的结束操作数索引。 从起始操作数索引 i 开始,随机决定子表达式的结束位置 (必须是操作数索引)。 为了简化,这里选择从 i+2 + * 开始到最后一个操作数之间的随机一个操作数索引。 + * + * @param parts 表达式各部分列表 + * @param i 起始操作数索引 + * @return 结束操作数索引 + */ + private int findMatchingEndIndex(List parts, int i) { + // 获取最后一个操作数的索引 + int lastOperandIndex = parts.size() - 1; + if (lastOperandIndex % 2 != 0) { + lastOperandIndex--; // 如果列表长度是偶数,最后一个元素是运算符,需要减1得到最后一个操作数索引 + } + + // 确保范围是有效的操作数索引 + if (i >= lastOperandIndex) { + return lastOperandIndex; // 如果i已经是最后一个或超出,返回最后一个 + } + + // 在 [i+2, lastOperandIndex] 范围内选择一个操作数索引 (步长为2) + // 确保至少包含一个运算符,所以从i+2开始 + List possibleEndIndices = new ArrayList<>(); + for (int idx = i + 2; idx <= lastOperandIndex; idx += 2) { + possibleEndIndices.add(idx); + } + + if (!possibleEndIndices.isEmpty()) { + // 随机选择一个可能的结束索引 + return possibleEndIndices.get(random.nextInt(possibleEndIndices.size())); + } else { + // 理论上不应该到达这里,如果到达则返回起始索引 + return i; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/service/MultipleChoiceGenerator.java b/src/main/java/com/ybw/mathapp/service/MultipleChoiceGenerator.java new file mode 100644 index 0000000..d84b603 --- /dev/null +++ b/src/main/java/com/ybw/mathapp/service/MultipleChoiceGenerator.java @@ -0,0 +1,252 @@ +package com.ybw.mathapp.service; + +import com.ybw.mathapp.entity.QuestionWithOptions; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; + + +/** + * 通用选择题生成器,将基础题目生成器的结果转换为带选项的选择题。 + * + *

该类确保生成的题目列表中不包含重复的题干。 + * 特殊处理规则: + *

    + *
  • 初中题:在计算过程中避免负数开根号(由 {@link AdvancedCaculate} 抛出异常,此处理捕获并跳过)。
  • + *
  • 小学题:生成的选项避免出现负数。
  • + *
+ * + * @author 杨博文 + * @since 2025 + */ +public class MultipleChoiceGenerator { + + /** + * 用于格式化数字的格式化器,保留两位小数。 + */ + private static final DecimalFormat df = new DecimalFormat("#0.00"); + /** + * 基础题目生成器。 + */ + private final QuestionGenerator baseGenerator; + /** + * 随机数生成器。 + */ + private final Random random = new Random(); + /** + * 当前题目类型,用于特殊处理逻辑。 + */ + private final String level; + + /** + * 构造一个新的选择题生成器。 + * + * @param baseGenerator 基础题目生成器 + * @param level 题目所属的级别 ("小学", "初中", "高中") + */ + public MultipleChoiceGenerator(QuestionGenerator baseGenerator, String level) { + this.baseGenerator = baseGenerator; + this.level = level; + } + + /** + * 生成指定数量的选择题,确保题干不重复。 + * + * @param count 要生成的题目数量 + * @return 生成的选择题列表(已去重) + */ + public List generateMultipleChoiceQuestions(int count) { + List mcQuestions = new ArrayList<>(); + Set seenQuestionTexts = new HashSet<>(); // 用于存储已生成的题干,保证唯一性 + + while (mcQuestions.size() < count) { + String baseQuestion = generateUniqueBaseQuestion(seenQuestionTexts); + if (baseQuestion == null) { + // 如果无法生成不重复的基础题目,可能基础生成器的可能组合已用尽 + break; // 退出循环 + } + QuestionWithOptions mcq = generateSingleMcq(baseQuestion); + if (mcq != null) { + mcQuestions.add(mcq); + seenQuestionTexts.add(baseQuestion); // 添加成功生成的题干 + } + // 如果 generateSingleMCQ 返回 null,说明计算或生成选项失败,循环会继续尝试下一个基础题目 + } + return mcQuestions; + } + + /** + * 生成一个唯一的、未处理过的基础题目。 + * + * @param seenQuestionTexts 已生成题干的集合 + * @return 一个唯一的题干字符串,如果在限定尝试次数内无法找到则返回 null + */ + private String generateUniqueBaseQuestion(Set seenQuestionTexts) { + int attempts = 0; + int maxAttempts = 1000; // 防止无限循环,如果生成太多重复题则退出 + while (attempts < maxAttempts) { + List baseQuestionList = baseGenerator.generateQuestions(1); + String baseQuestion = baseQuestionList.get(0); + if (!seenQuestionTexts.contains(baseQuestion)) { + return baseQuestion; + } + attempts++; + } + return null; // 达到最大尝试次数仍未找到唯一题目 + } + + /** + * 从单个基础题目生成选择题对象。 + * + * @param baseQuestion 基础题干字符串,例如 "3 + 5 = ?" + * @return 生成的选择题对象,如果计算或生成选项失败则返回 null + */ + private QuestionWithOptions generateSingleMcq(String baseQuestion) { + try { + // 从基础题干中提取表达式部分,例如 "3 + 5 = ?" -> "3 + 5" + String expression = baseQuestion.substring(0, baseQuestion.lastIndexOf(" =")).trim(); + List tokens = tokenizeExpression(expression); + // 计算正确答案 + double correctAnswer = AdvancedCaculate.calculate(tokens); + + // 生成选项列表 + List options = generateOptions(correctAnswer); + if (options == null) { + // 无法生成足够的有效选项 + return null; + } + + // 随机打乱选项顺序 + Collections.shuffle(options); + // 找到正确答案在打乱后列表中的索引 + int correctIndex = options.indexOf(df.format(correctAnswer)); + + return new QuestionWithOptions(baseQuestion, options, correctIndex); + + } catch (ArithmeticException | IllegalArgumentException e) { + // 计算或表达式格式错误,跳过此题 + // System.out.println("计算或表达式错误,跳过题目: " + baseQuestion + ", Error: " + e.getMessage()); + return null; // 返回 null 表示生成失败 + } + } + + /** + * 生成选项列表 (正确答案 + 错误答案)。 + * + *

对于小学级别,错误答案不会包含负数。 + * + * @param correctAnswer 正确答案 + * @return 包含正确答案和错误答案的字符串列表,如果无法生成足够选项则返回 null + */ + private List generateOptions(double correctAnswer) { + Set wrongAnswers = new HashSet<>(); + int attempts = 0; + int maxAttempts = 100; // 防止无限循环 + int numWrongOptions = 3; // 假设总共4个选项,需要3个错误答案 + + while (wrongAnswers.size() < numWrongOptions && attempts < maxAttempts) { + int offset = random.nextInt(20) + 1; // 生成 1-20 的偏移量 + if (random.nextBoolean()) { + offset = -offset; // 随机正负 + } + double wrongAnswer = correctAnswer + offset; + + // 确保错误答案与正确答案不同,并且对于小学题不为负数 + if (Math.abs(df.format(wrongAnswer).compareTo(df.format(correctAnswer))) != 0) { + if (!level.equals("小学") || wrongAnswer >= 0) { + wrongAnswers.add(wrongAnswer); + } + } + attempts++; + } + + if (wrongAnswers.size() < numWrongOptions) { + return null; // 无法生成足够选项 + } + + // 将正确答案和错误答案合并 + List allAnswers = new ArrayList<>(); + allAnswers.add(correctAnswer); + allAnswers.addAll(wrongAnswers); + + // 格式化所有答案为字符串 + List options = new ArrayList<>(); + for (Double ans : allAnswers) { + options.add(df.format(ans)); + } + return options; + } + + // ... (其他类成员和方法保持不变) + + // --- 表达式分词逻辑 --- + // 将 "3 + 开根号 ( 4 ) 平方 - sin 30" 分割成 ["3", "+", "开根号", "(", "4", ")", "平方", "-", "sin", "30"] + + /** + * 将表达式字符串分割成标记列表。 + * + * @param expression 表达式字符串 + * @return 标记列表 + */ + private List tokenizeExpression(String expression) { + List tokens = new ArrayList<>(); + int i = 0; + while (i < expression.length()) { + String token = findNextToken(expression, i); + if (token != null) { + tokens.add(token); + i += token.length(); + } else { + i++; + } /* else { + // 如果找不到匹配的 token,可能是单个字符或未知格式 + tokens.add(String.valueOf(expression.charAt(i))); + i++; + } + */ + } + return tokens; + } + + /** + * 查找从指定位置开始的下一个 token。 + * + * @param expression 表达式字符串 + * @param startPos 起始查找位置 + * @return 找到的 token,如果未找到则返回 null + */ + private String findNextToken(String expression, int startPos) { + char c = expression.charAt(startPos); + + if (Character.isWhitespace(c)) { + return null; // 空格由调用者处理 + } + if (c == '(' || c == ')') { + return String.valueOf(c); + } + if (Character.isDigit(c) || c == '.') { + // 查找连续的数字或小数点 + int j = startPos; + while (j < expression.length() && (Character.isDigit(expression.charAt(j)) + || expression.charAt(j) == '.')) { + j++; + } + return expression.substring(startPos, j); + } + + String[] advancedOps = {"平方", "开根号", "sin", "cos", "tan"}; + // 检查多字符运算符 + for (String op : advancedOps) { + if (expression.startsWith(op, startPos)) { + return op; + } + } + + // 如果不是多字符运算符,则认为是单字符运算符 (+, -, *, / 等) + return String.valueOf(c); + } +} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/service/PrimarySchoolGenerator.java b/src/main/java/com/ybw/mathapp/service/PrimarySchoolGenerator.java new file mode 100644 index 0000000..3b2bb81 --- /dev/null +++ b/src/main/java/com/ybw/mathapp/service/PrimarySchoolGenerator.java @@ -0,0 +1,106 @@ +package com.ybw.mathapp.service; + +import static com.ybw.mathapp.service.AdvancedCaculate.isNumeric; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * 小学题目生成器,负责生成包含四则运算和括号的小学级别数学题目。 + * + *

该生成器专门用于生成适合小学生的数学题目,题目仅包含加减乘除四则运算 + * 和括号,确保计算结果为非负数。 + * + * @author 杨博文 + * @version 1.0 + * @since 2025 + */ +public class PrimarySchoolGenerator implements QuestionGenerator { + + /** 运算符数组,包含四则运算符号。 */ + private static final String[] OPERATORS = {"+", "-", "*", "/"}; + + /** 随机数生成器,用于生成随机题目。 */ + private final Random random = new Random(); + + @Override + public List generateQuestions(int count) { + List questions = new ArrayList<>(); + for (int i = 0; i < count; i++) { + String question = generateSingleQuestion(); + questions.add(question); + } + return questions; + } + + /** + * 生成单个小级别的数学题目. + * + *

该方法生成包含2-5个操作数的四则运算表达式,可能包含括号, + * 并确保计算结果为非负数。如果计算结果为负数,则重新生成。 + * + * @return 生成的小学数学题目字符串 + */ + private String generateSingleQuestion() { + int operandCount = random.nextInt(4) + 2; // 2-5个操作数 + List parts = new ArrayList<>(); + while (true) { + // 生成基础操作 + parts = generateBase(operandCount, parts); + // 简单添加括号逻辑:随机加一个括号 + if (operandCount > 2 && random.nextBoolean()) { + // 遍历查找左括号的合理位置 + for (int i = 0; i < parts.size() - 2; i++) { + // 该位置要为操作数且随机添加括号 + if (isNumeric(parts.get(i)) && random.nextBoolean()) { + parts.add(i, "("); + i++; + // 为避免随机数上限出现0,此处要单独判断一下左括号正好括住倒数第二个操作数的情况 + if (i == parts.size() - 3) { + parts.add(")"); + } else { + while (true) { + int i2 = random.nextInt(parts.size() - 3 - i) + 2; + if (isNumeric(parts.get(i + i2))) { + parts.add(i + i2 + 1, ")"); + break; + } + } + } + break; + } + } + } + try { + if (AdvancedCaculate.calculate(parts) >= 0) { + return String.join(" ", parts) + " ="; + } else { + parts.clear(); + } + } catch (ArithmeticException | IllegalArgumentException e) { + parts.clear(); + } + } + } + + /** + * 生成基本的四则运算表达式部分。 + * + *

该方法生成指定数量的操作数和运算符,构成基础的数学表达式。 + * + * @param operandCount 操作数的数量 + * @param parts 用于存储表达式各部分的列表 + * @return 包含基本运算表达式的列表 + */ + public List generateBase(int operandCount, List parts) { + for (int i = 0; i < operandCount; i++) { + int num = random.nextInt(100) + 1; + parts.add(String.valueOf(num)); + if (i < operandCount - 1) { + parts.add(OPERATORS[random.nextInt(OPERATORS.length)]); + } + } + return parts; + } +} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/service/QuestionGenerator.java b/src/main/java/com/ybw/mathapp/service/QuestionGenerator.java new file mode 100644 index 0000000..91c9436 --- /dev/null +++ b/src/main/java/com/ybw/mathapp/service/QuestionGenerator.java @@ -0,0 +1,29 @@ +package com.ybw.mathapp.service; + +import java.util.List; + +/** + * 题目生成器接口,定义了题目生成器的标准方法。 + * + *

所有具体的题目生成器都应该实现此接口,提供统一的题目生成功能。 + * 不同级别的题目生成器(如小学、初中、高中)可以根据各自的特点 + * 实现不同的生成算法。 + * + * @author 杨博文 + * @version 1.0 + * @since 2025 + */ +public interface QuestionGenerator { + + /** + * 生成指定数量的数学题目。 + * + *

该方法根据实现类的特定规则生成指定数量的数学题目。 + * 生成的题目应该符合对应教育级别的难度要求。 + * + * @param count 需要生成的题目数量 + * @return 包含生成题目的字符串列表 + */ + List generateQuestions(int count); + +} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/service/SeniorHighGenerator.java b/src/main/java/com/ybw/mathapp/service/SeniorHighGenerator.java new file mode 100644 index 0000000..62ab5bb --- /dev/null +++ b/src/main/java/com/ybw/mathapp/service/SeniorHighGenerator.java @@ -0,0 +1,127 @@ +package com.ybw.mathapp.service; + +import static com.ybw.mathapp.service.AdvancedCaculate.isNumeric; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * 高中题目生成器,负责生成包含三角函数运算的高中级别数学题目。 + * + *

该生成器确保每道题目都包含至少一个三角函数运算符(sin、cos或tan), + * 题目结构包含基本的四则运算和三角函数运算的组合。 + * + * @author 杨博文 + * @version 1.0 + * @since 2025 + */ +public class SeniorHighGenerator implements QuestionGenerator { + + /** 三角函数运算符数组,包含"sin"、"cos"和"tan"。 */ + private static final String[] TRIG_FUNCS = {"sin", "cos", "tan"}; + + /** 基本运算符数组,包含四则运算符号。 */ + private static final String[] OPERATORS = {"+", "-", "*", "/"}; + + /** 随机数生成器,用于生成随机题目。 */ + private final Random random = new Random(); + + @Override + public List generateQuestions(int count) { + List questions = new ArrayList<>(); + for (int i = 0; i < count; i++) { + String question = generateSingleQuestion(); + questions.add(question); + } + return questions; + } + + /** + * 生成单个高中级别的数学题目。 + * + *

该方法确保生成的题目包含至少一个三角函数运算符, + * 并根据操作数数量采用不同的生成策略。 + * + * @return 生成的数学题目字符串 + */ + private String generateSingleQuestion() { + List parts = new ArrayList<>(); + int operandCount = random.nextInt(5) + 1; + parts = generateBase(operandCount, parts); + String advancedOp; + if (operandCount == 1) { + advancedOp = TRIG_FUNCS[random.nextInt(TRIG_FUNCS.length)]; + parts.set(0, advancedOp + " " + parts.get(0)); + } else { + // 遍历查找左括号的合理位置 + for (int i = 0; i < parts.size(); i++) { + // 最后一次循环保底生成高中三角函数 + if (i == parts.size() - 1) { + advancedOp = TRIG_FUNCS[random.nextInt(TRIG_FUNCS.length)]; + parts.set(i, advancedOp + " " + parts.get(i)); + } else if (isNumeric(parts.get(i)) && random.nextBoolean()) { // 随机数看是否为操作数且随即进入生成程序 + // 进入随机生成tan\sin\cos的程序 + parts = generateTrig(parts, i); + break; + } + } + } + return String.join(" ", parts) + " ="; + } + + /** + * 生成基本的四则运算表达式部分。 + * + *

该方法生成指定数量的操作数和运算符,构成基础的数学表达式。 + * + * @param operandCount 操作数的数量 + * @param parts 用于存储表达式各部分的列表 + * @return 包含基本运算表达式的列表 + */ + // 产生基本操作 + public List generateBase(int operandCount, List parts) { + for (int i = 0; i < operandCount; i++) { + int num = random.nextInt(100) + 1; + parts.add(String.valueOf(num)); + if (i < operandCount - 1) { + parts.add(OPERATORS[random.nextInt(OPERATORS.length)]); + } + } + return parts; + } + + /** + * 在指定位置生成三角函数运算。 + * + *

该方法在表达式指定位置添加三角函数运算,可能只对单个操作数 + * 进行三角函数运算,或者对一段子表达式进行三角函数运算。 + * + * @param parts 包含表达式各部分的列表 + * @param i 三角函数运算的位置 + * @return 添加了三角函数运算的表达式列表 + */ + // 产生三角函数运算符 + public List generateTrig(List parts, int i) { + String trigOp = TRIG_FUNCS[random.nextInt(TRIG_FUNCS.length)]; + if (random.nextBoolean()) { + parts.set(i, trigOp + " " + parts.get(i)); + } else { + parts.set(i, trigOp + " ( " + parts.get(i)); + // 为避免随机数上限出现0,此处要单独判断一下左括号正好括住倒数第二个操作数的情况 + if (i == parts.size() - 3) { + parts.set(parts.size() - 1, parts.get(parts.size() - 1) + " )"); + } else { + while (true) { + int i2 = random.nextInt(parts.size() - 3 - i) + 2; + if (isNumeric(parts.get(i + i2))) { + parts.set(i + i2, parts.get(i + i2) + " )"); + break; + } + } + } + } + return parts; + } + +} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/util/ChangePassword.java b/src/main/java/com/ybw/mathapp/util/ChangePassword.java new file mode 100644 index 0000000..e4872cb --- /dev/null +++ b/src/main/java/com/ybw/mathapp/util/ChangePassword.java @@ -0,0 +1,139 @@ +package com.ybw.mathapp.util; + +import static com.ybw.mathapp.util.LoginFileUtils.USER_FILE; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * 修改用户密码的工具类。 + * + *

该类提供了一个静态方法来更新用户文件中指定用户的密码。 + * 它通过读取整个文件,找到目标用户,修改其密码,然后将整个内容写回文件来实现。 + * + * @author 杨博文 + * @since 2025 + */ +public class ChangePassword { + + /** + * 用于缓存文件所有行内容的列表。 + */ + private static final List lines = new ArrayList<>(); + /** + * 存储找到的用户信息行。 + */ + private static String userLine = null; + /** + * 存储找到的用户行在文件中的行号(从0开始)。 + */ + private static int userLineNumber = -1; + + /** + * 修改指定用户的密码。 + * + *

该方法会读取用户文件,查找与给定用户名匹配的行,更新该行的密码字段, + * 然后将更新后的内容写回文件。 + * + * @param name 要修改密码的用户名 + * @param newPassword 新密码 + * @return 如果修改成功返回 true,否则返回 false + */ + public static boolean changePassword(String name, String newPassword) { + File file = new File(USER_FILE); + if (!file.exists()) { + System.out.println("用户文件不存在: " + USER_FILE); + return false; + } + + // 1. 读取文件,查找用户 + lines.clear(); // 清空上一次的缓存 + userLine = null; + userLineNumber = -1; + if (!findUserLine(name, file)) { + return false; + } + + if (userLine == null || userLineNumber == -1) { + // 用户未找到 + System.out.println("用户 '" + name + "' 不存在,修改失败。"); + return false; + } + + // 2. 更新找到的用户行中的密码 + String[] parts = userLine.split(","); + if (parts.length != 3) { + System.err.println("用户文件中用户 '" + name + "' 的数据格式不正确,无法修改密码。"); + return false; + } + parts[2] = newPassword; // 假设密码是第三个字段 + String updatedLine = String.join(",", parts); + + lines.set(userLineNumber, updatedLine); // 替换列表中的旧行 + + // 3. 将更新后的内容写回文件 + if (!writeBack(lines, file)) { + return false; + } + + return true; + } + + /** + * 将给定的行列表写回指定的文件。 + * + * @param lines 要写入的行列表 + * @param file 目标文件 + * @return 如果写入成功返回 true,否则返回 false + */ + private static boolean writeBack(List lines, File file) { + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { + for (String line : lines) { + writer.write(line); + writer.newLine(); + } + } catch (IOException e) { + System.err.println("写入文件时出错: " + e.getMessage()); + return false; // 如果写回失败,认为修改未成功 + } + return true; + } + + /** + * 在指定文件中查找包含给定用户名的行。 + * + *

此方法会将文件的所有行读入 {@code lines} 列表,并设置 + * {@code userLine} 和 {@code userLineNumber}。 + * + * @param name 要查找的用户名 + * @param file 要搜索的文件 + * @return 如果读取文件过程无异常则返回 true,否则返回 false + */ + private static boolean findUserLine(String name, File file) { + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + int currentLineNum = 0; + while ((line = reader.readLine()) != null) { + lines.add(line); + String[] parts = line.split(","); + // 假设格式为: username,email,password + if (parts.length >= 3 && parts[0].trim().equals(name.trim())) { + userLine = line; // 找到用户行 + userLineNumber = currentLineNum; + // break; // 找到后可以退出循环,但为了读取所有行到 lines,不在此处 break + } + currentLineNum++; + } + } catch (IOException e) { + System.err.println("读取文件时出错: " + e.getMessage()); + return false; + } + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/util/EmailService.java b/src/main/java/com/ybw/mathapp/util/EmailService.java new file mode 100644 index 0000000..956d5de --- /dev/null +++ b/src/main/java/com/ybw/mathapp/util/EmailService.java @@ -0,0 +1,250 @@ +package com.ybw.mathapp.util; + +import com.ybw.mathapp.config.EmailConfig; +import jakarta.mail.Authenticator; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.PasswordAuthentication; +import jakarta.mail.Session; +import jakarta.mail.Transport; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Random; + +/** + * 邮件服务类,用于发送验证码和验证验证码。 + * + *

该类提供生成验证码、发送邮件、验证验证码以及清理过期验证码的功能。 + * 验证码的有效期由 {@link EmailConfig} 配置。 + * + * @author 杨博文 + * @since 2025 + */ +public class EmailService { + + /** + * 存储邮箱地址与验证码信息的映射。 + */ + private static final Map verificationCodes = new HashMap<>(); + + /** + * 生成一个6位的随机数字验证码。 + * + * @return 6位数字验证码字符串 + */ + public static String generateVerificationCode() { + Random random = new Random(); + int code = 100000 + random.nextInt(900000); + return String.valueOf(code); + } + + /** + * 发送验证码邮件到指定邮箱。 + * + *

此方法使用 {@link EmailConfig} 中的SMTP配置来发送邮件。 + * 发送成功后,会将验证码和时间戳存储到内存中。 + * + * @param recipientEmail 接收验证码的邮箱地址 + * @param code 要发送的验证码 + * @return 如果邮件发送成功返回 true,否则返回 false + */ + public static boolean sendVerificationCode(String recipientEmail, String code) { + try { + // 创建邮件会话 + Properties props = new Properties(); + props.put("mail.smtp.host", EmailConfig.SMTP_HOST); + props.put("mail.smtp.port", EmailConfig.SMTP_PORT); + props.put("mail.smtp.auth", "true"); + props.put("mail.smtp.starttls.enable", "true"); + props.put("mail.smtp.ssl.protocols", "TLSv1.2"); + + // 创建认证器 + Authenticator auth = new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication( + EmailConfig.SENDER_EMAIL, + EmailConfig.SENDER_PASSWORD + ); + } + }; + + Session session = Session.getInstance(props, auth); + + // 创建邮件消息 + Message message = new MimeMessage(session); + message.setFrom(new InternetAddress(EmailConfig.SENDER_EMAIL)); + message.setRecipients(Message.RecipientType.TO, + InternetAddress.parse(recipientEmail)); + message.setSubject(EmailConfig.EMAIL_SUBJECT); + + // 创建邮件内容 + String emailContent = createEmailContent(code); + message.setContent(emailContent, "text/html; charset=utf-8"); + + // 发送邮件 + Transport.send(message); + + // 存储验证码信息 + verificationCodes.put(recipientEmail, + new VerificationCodeInfo(code, System.currentTimeMillis())); + + System.out.println("验证码已发送到邮箱: " + recipientEmail); + return true; + + } catch (MessagingException e) { + System.err.println("发送邮件失败: " + e.getMessage()); + e.printStackTrace(); + return false; + } + } + + /** + * 创建HTML格式的邮件内容。 + * + * @param code 要嵌入到邮件中的验证码 + * @return HTML格式的邮件正文字符串 + */ + private static String createEmailContent(String code) { + return "" + + "" + + "" + + "" + + "" + + "" + + "" + + "

" + + "
" + + "

用户注册验证码

" + + "
" + + "
" + + "

您好!

" + + "

您正在注册账户,验证码如下:

" + + "
" + code + "
" + + "

验证码有效期为 " + EmailConfig.CODE_EXPIRY_MINUTES + " 分钟,请勿泄露给他人。

" + + "

如果这不是您本人的操作,请忽略此邮件。

" + + "
" + + "" + + "
" + + "" + + ""; + } + + /** + * 验证用户输入的验证码是否正确且未过期。 + * + * @param email 发送验证码的邮箱地址 + * @param inputCode 用户输入的验证码 + * @return 如果验证码正确且未过期返回 true,否则返回 false + */ + public static boolean verifyCode(String email, String inputCode) { + VerificationCodeInfo codeInfo = verificationCodes.get(email); + if (codeInfo == null) { + return false; + } + + // 检查验证码是否过期 + long currentTime = System.currentTimeMillis(); + if (currentTime - codeInfo.timestamp > EmailConfig.CODE_EXPIRY_MINUTES * 60 * 1000) { + verificationCodes.remove(email); + return false; + } + + return codeInfo.code.equals(inputCode); + } + + /** + * 清理内存中所有已过期的验证码。 + */ + public static void cleanupExpiredCodes() { + long currentTime = System.currentTimeMillis(); + Iterator> iterator = + verificationCodes.entrySet().iterator(); + + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (currentTime - entry.getValue().timestamp + > EmailConfig.CODE_EXPIRY_MINUTES * 60 * 1000) { + iterator.remove(); + } + } + } + + /** + * 验证邮箱地址格式是否正确(简单验证)。 + * + * @param email 待验证的邮箱地址 + * @return 如果邮箱格式正确返回 true,否则返回 false + */ + public static boolean isValidEmail(String email) { + if (email == null || email.isEmpty()) { + System.out.println("邮箱地址不能为空!"); + return false; + } + if (!(email.contains("@") && email.contains("."))) { + System.out.println("邮箱格式不正确!"); + return false; + } + return true; + } + + /** + * 前端接口:发送验证码到指定邮箱。 + * + *

此方法会生成验证码并尝试发送邮件。 + * + * @param email 接收验证码的邮箱地址 + * @return 如果验证码生成并发送邮件成功返回 true,否则返回 false + */ + public static boolean sendCode(String email) { + // 生成验证码 + String verificationCode = EmailService.generateVerificationCode(); + // 尝试发送邮件 + if (!EmailService.sendVerificationCode(email, verificationCode)) { + // 如果发送失败,sendVerificationCode 已经打印了错误信息 + return false; + } + return true; + } + + /** + * 存储验证码及其生成时间的内部类。 + */ + private static class VerificationCodeInfo { + + /** + * 验证码字符串。 + */ + String code; + /** + * 验证码生成的时间戳(毫秒)。 + */ + long timestamp; + + /** + * 构造一个新的验证码信息对象。 + * + * @param code 验证码 + * @param timestamp 生成时间戳 + */ + VerificationCodeInfo(String code, long timestamp) { + this.code = code; + this.timestamp = timestamp; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/util/Login.java b/src/main/java/com/ybw/mathapp/util/Login.java new file mode 100644 index 0000000..66a1a7d --- /dev/null +++ b/src/main/java/com/ybw/mathapp/util/Login.java @@ -0,0 +1,32 @@ +package com.ybw.mathapp.util; + +/** + * 用户登录处理类。 + * + *

该类提供一个静态方法用于验证用户凭据并处理登录逻辑。 + * + * @author 杨博文 + * @since 2025 + */ +public class Login { + + /** + * 尝试使用给定的用户名和密码进行登录。 + * + *

此方法通过 {@link LoginFileUtils#validateUser(String, String)} 验证用户凭据。 + * 登录成功或失败时会打印相应的控制台消息。 + * + * @param name 用户名 + * @param password 用户密码 + * @return 如果登录成功返回 true,否则返回 false + */ + public static boolean login(String name, String password) { + if (LoginFileUtils.validateUser(name, password)) { + System.out.println("登录成功!欢迎回来," + name); + return true; + } else { + System.out.println("用户名或密码错误!"); + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/util/LoginFileUtils.java b/src/main/java/com/ybw/mathapp/util/LoginFileUtils.java new file mode 100644 index 0000000..e22d705 --- /dev/null +++ b/src/main/java/com/ybw/mathapp/util/LoginFileUtils.java @@ -0,0 +1,155 @@ +package com.ybw.mathapp.util; + +import com.ybw.mathapp.entity.User; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +/** + * 用户文件操作工具类。 + * + *

该类负责与用户信息文件 ({@code users.txt}) 进行交互, + * 提供读取用户、保存用户、检查用户是否存在以及验证用户登录等功能。 + * + * @author 杨博文 + * @since 2025 + */ +public class LoginFileUtils { + + /** + * 用户信息文件的路径。 + */ + public static final String USER_FILE = "users.txt"; + + /** + * 从用户文件中读取所有用户信息。 + * + *

如果文件不存在,将尝试创建一个新文件并返回一个空列表。 + * + * @return 包含所有用户对象的列表 + */ + public static List readUsers() { + List users = new ArrayList<>(); + File file = new File(USER_FILE); + + if (!file.exists()) { + try { + file.createNewFile(); + } catch (IOException e) { + System.err.println("创建用户文件失败: " + e.getMessage()); + } + return users; + } + + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) { + continue; + } + + User user = User.fromString(line); + if (user != null) { + users.add(user); + } + } + } catch (IOException e) { + System.err.println("读取用户文件失败: " + e.getMessage()); + } + return users; + } + + /** + * 将一个新用户信息追加保存到用户文件中。 + * + * @param user 要保存的用户对象 + */ + public static void saveUser(User user) { + try (PrintWriter writer = new PrintWriter(new FileWriter(USER_FILE, true))) { + writer.println(user.toString()); + } catch (IOException e) { + System.err.println("保存用户信息失败: " + e.getMessage()); + } + } + + /** + * 检查指定的邮箱是否已经注册。 + * + *

比较时不区分大小写。 + * + * @param email 要检查的邮箱地址 + * @return 如果邮箱已注册返回 true,否则返回 false + */ + public static boolean isEmailRegistered(String email) { + List users = readUsers(); + for (User user : users) { + if (user.getEmail().equalsIgnoreCase(email)) { + return true; + } + } + return false; + } + + /** + * 检查指定的用户名是否已经注册。 + * + *

比较时不区分大小写。 + * + * @param name 要检查的用户名 + * @return 如果用户名已注册返回 true,否则返回 false + */ + public static boolean isNameRegistered(String name) { + List users = readUsers(); + for (User user : users) { + if (user.getName().equalsIgnoreCase(name)) { + return true; + } + } + return false; + } + + /** + * 验证用户登录凭据。 + * + *

支持使用邮箱或用户名进行登录。比较邮箱/用户名时不区分大小写。 + * + * @param emailOrName 用户输入的邮箱或用户名 + * @param password 用户输入的密码 + * @return 如果凭据有效返回 true,否则返回 false + */ + public static boolean validateUser(String emailOrName, String password) { + List users = readUsers(); + for (User user : users) { + if ((user.getEmail().equalsIgnoreCase(emailOrName) + || user.getName().equalsIgnoreCase(emailOrName)) + && user.getPassword().equals(password)) { + return true; + } + } + return false; + } + + /** + * 根据邮箱地址查找对应的用户名。 + * + *

比较邮箱时不区分大小写。 + * + * @param email 要查找的邮箱地址 + * @return 如果找到用户则返回其用户名,否则返回 null + */ + public static String emailFindName(String email) { + List users = readUsers(); + for (User user : users) { + if (user.getEmail().equalsIgnoreCase(email)) { + return user.getName(); + } + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/ybw/mathapp/util/Register.java b/src/main/java/com/ybw/mathapp/util/Register.java new file mode 100644 index 0000000..02d4e5f --- /dev/null +++ b/src/main/java/com/ybw/mathapp/util/Register.java @@ -0,0 +1,70 @@ +package com.ybw.mathapp.util; + +import com.ybw.mathapp.entity.User; +import java.util.regex.Pattern; + + +/** + * 用户注册处理类。 + * + *

该类提供用户注册、密码格式验证和两次密码匹配验证等功能。 + * + * @author 杨博文 + * @since 2025 + */ +public class Register { + + /** + * 完成用户注册流程。 + * + *

此方法会创建一个新的用户对象并将其保存到用户文件中。 + * + * @param name 用户名 + * @param email 用户邮箱 + * @param password1 用户密码 + * @return 注册成功返回 true(当前实现总是返回 true,即使保存可能失败) + */ + public static boolean register(String name, String email, String password1) { + User user = new User(name, email, password1); + LoginFileUtils.saveUser(user); + System.out.println("注册成功!您可以使用邮箱和密码登录了。"); + return true; + } + + /** + * 验证密码格式是否符合要求。 + * + *

密码要求: + *

    + *
  • 长度为 6 到 10 个字符
  • + *
  • 只能包含字母和数字
  • + *
  • 必须同时包含至少一个大写字母、一个小写字母和一个数字
  • + *
+ * + * @param password1 待验证的密码 + * @return 如果密码格式符合要求返回 true,否则返回 false + */ + public static boolean isVaildPassword(String password1) { + // 使用正则表达式验证:长度6-10,只包含字母数字,且包含大小写字母和数字 + String regex = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{6,10}$"; + if (!Pattern.matches(regex, password1)) { + return false; + } + return true; + } + + /** + * 验证两次输入的密码是否一致。 + * + * @param password1 第一次输入的密码 + * @param password2 第二次输入的密码 + * @return 如果两次密码相同返回 true,否则返回 false 并打印错误信息 + */ + public static boolean isEqualPassword(String password1, String password2) { + if (!password1.equals(password2)) { + System.out.println("两次输入的密码不一致!"); + return false; + } + return true; + } +} \ No newline at end of file diff --git a/src/main/resources/META-INF/javamail.default.address.map b/src/main/resources/META-INF/javamail.default.address.map new file mode 100644 index 0000000..e3baea0 --- /dev/null +++ b/src/main/resources/META-INF/javamail.default.address.map @@ -0,0 +1,2 @@ +rfc822=smtp +smtp=smtp \ No newline at end of file