发行版1.01 #7

Merged
hnu202326010319 merged 5 commits from LiangJunYaoBranch into develop 4 months ago

@ -0,0 +1,20 @@
<component name="ArtifactManager">
<artifact type="jar" name="MathQuizApp:jar">
<output-path>$PROJECT_DIR$/out/artifacts/MathQuizApp_jar</output-path>
<root id="archive" name="MathQuizApp.jar">
<element id="directory" name="META-INF">
<element id="file-copy" path="$PROJECT_DIR$/META-INF/MANIFEST.MF" />
</element>
<element id="module-output" name="MathQuizApp" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/com/google/code/gson/gson/2.10.1/gson-2.10.1.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/openjfx/javafx-base/21.0.2/javafx-base-21.0.2-win.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/openjfx/javafx-fxml/21.0.2/javafx-fxml-21.0.2-win.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/openjfx/javafx-graphics/21.0.2/javafx-graphics-21.0.2.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/openjfx/javafx-controls/21.0.2/javafx-controls-21.0.2-win.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/openjfx/javafx-base/21.0.2/javafx-base-21.0.2.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/openjfx/javafx-controls/21.0.2/javafx-controls-21.0.2.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/openjfx/javafx-fxml/21.0.2/javafx-fxml-21.0.2.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/openjfx/javafx-graphics/21.0.2/javafx-graphics-21.0.2-win.jar" path-in-jar="/" />
</root>
</artifact>
</component>

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Palette2">
<group name="Swing">
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
</item>
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
</item>
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true">
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
</item>
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
<initial-values>
<property name="text" value="Button" />
</initial-values>
</item>
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="RadioButton" />
</initial-values>
</item>
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="CheckBox" />
</initial-values>
</item>
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
<initial-values>
<property name="text" value="Label" />
</initial-values>
</item>
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
</item>
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
</item>
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
<preferred-size width="-1" height="20" />
</default-constraints>
</item>
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
</item>
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
</item>
</group>
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

@ -0,0 +1,195 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ArtifactsWorkspaceSettings">
<artifacts-to-build>
<artifact name="MathQuizApp:jar" />
</artifacts-to-build>
</component>
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="ad9e49ad-e421-455c-8a89-0f35a7a15146" name="Changes" comment="partial ui implication">
<change afterPath="$PROJECT_DIR$/.idea/artifacts/MathQuizApp_jar.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/META-INF/MANIFEST.MF" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/data/users.json" beforeDir="false" afterPath="$PROJECT_DIR$/data/users.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pom.xml" beforeDir="false" afterPath="$PROJECT_DIR$/pom.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/Test.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/Test.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/model/Grade.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/model/Grade.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/model/QuizResult.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/model/QuizResult.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/service/UserService.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/service/UserService.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/ui/MainWindow.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/ui/MainWindow.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/ui/QuizPage.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/ui/QuizPage.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/ui/ResultPage.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/ui/ResultPage.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/TestMain.java" beforeDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Kotlin Class" />
<option value="Class" />
<option value="JavaFXApplication" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="KubernetesApiPersistence">{}</component>
<component name="KubernetesApiProvider">{
&quot;isMigrated&quot;: true
}</component>
<component name="MavenImportPreferences">
<option name="generalSettings">
<MavenGeneralSettings>
<option name="localRepository" value="C:\Users\28032\.m2\repository" />
<option name="userSettingsFile" value="C:\Users\28032\.m2\settings.xml" />
</MavenGeneralSettings>
</option>
</component>
<component name="ProblemsViewState">
<option name="selectedTabId" value="CurrentFile" />
</component>
<component name="ProjectColorInfo">{
&quot;customColor&quot;: &quot;&quot;,
&quot;associatedIndex&quot;: 1
}</component>
<component name="ProjectId" id="33aD4Tx1DVbp880Km6dEp5mEUbo" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;Application.Test.executor&quot;: &quot;Run&quot;,
&quot;Application.TestMain.executor&quot;: &quot;Run&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;LiangJunYaoBranch&quot;,
&quot;kotlin-language-version-configured&quot;: &quot;true&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;project.structure.last.edited&quot;: &quot;Artifacts&quot;,
&quot;project.structure.proportion&quot;: &quot;0.0&quot;,
&quot;project.structure.side.proportion&quot;: &quot;0.0&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;MavenSettings&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}
}</component>
<component name="RecentsManager">
<key name="MoveFile.RECENT_KEYS">
<recent name="C:\Users\28032\OneDrive\Desktop\PAIR\.idea" />
</key>
</component>
<component name="RunManager" selected="Application.Test">
<configuration name="Test" type="Application" factoryName="Application" temporary="true" nameIsGenerated="true">
<option name="MAIN_CLASS_NAME" value="com.Test" />
<module name="MathQuizApp" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="com.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<configuration name="TestMain" type="Application" factoryName="Application" temporary="true" nameIsGenerated="true">
<option name="MAIN_CLASS_NAME" value="TestMain" />
<module name="MathQuizApp" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<recent_temporary>
<list>
<item itemvalue="Application.Test" />
<item itemvalue="Application.TestMain" />
</list>
</recent_temporary>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-jdk-9823dce3aa75-fdfe4dae3a2d-intellij.indexing.shared.core-IU-243.21565.193" />
<option value="bundled-js-predefined-d6986cc7102b-e768b9ed790e-JavaScript-IU-243.21565.193" />
</set>
</attachedChunks>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="ad9e49ad-e421-455c-8a89-0f35a7a15146" name="Changes" comment="" />
<created>1759546098925</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1759546098925</updated>
<workItem from="1759546100740" duration="23850000" />
<workItem from="1759587998528" duration="511000" />
<workItem from="1759588522412" duration="122000" />
<workItem from="1759644297457" duration="32827000" />
<workItem from="1759720493898" duration="12215000" />
<workItem from="1759745765663" duration="2792000" />
</task>
<task id="LOCAL-00001" summary="ui design v1">
<option name="closed" value="true" />
<created>1759588559366</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1759588559366</updated>
</task>
<task id="LOCAL-00002" summary="partial ui implication">
<option name="closed" value="true" />
<created>1759684484435</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1759684484435</updated>
</task>
<option name="localTasksCounter" value="3" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="Vcs.Log.Tabs.Properties">
<option name="TAB_STATES">
<map>
<entry key="MAIN">
<value>
<State>
<option name="FILTERS">
<map>
<entry key="branch">
<value>
<list>
<option value="LiangJunYaoBranch" />
</list>
</value>
</entry>
</map>
</option>
</State>
</value>
</entry>
</map>
</option>
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="ui design v1" />
<MESSAGE value="partial ui implication" />
<option name="LAST_COMMIT_MESSAGE" value="partial ui implication" />
</component>
</project>

@ -0,0 +1,3 @@
Manifest-Version: 1.0
Main-Class: com.Test

@ -0,0 +1,77 @@
========== 答题记录 ==========
用户:初中-王五
时间2025-10-04 12:30:47
总分100 分
正确10 题 错误0 题
=============================
【题目 1】
√4 + 3²
A. 20.0 B. 14.0 C. 6.0 D. 11.0
正确答案D
用户答案D
结果:✓ 正确
【题目 2】
A. 6.0 B. 8.0 C. 4.0 D. 9.0
正确答案C
用户答案C
结果:✓ 正确
【题目 3】
√100 + 15
A. 24.0 B. 26.0 C. 25.0 D. 21.0
正确答案C
用户答案C
结果:✓ 正确
【题目 4】
√36
A. 6.0 B. 8.0 C. 7.0 D. 18.0
正确答案A
用户答案A
结果:✓ 正确
【题目 5】
A. 64.0 B. 71.0 C. 16.0 D. 73.0
正确答案A
用户答案A
结果:✓ 正确
【题目 6】
√16
A. 4.0 B. 8.0 C. 13.0 D. 6.0
正确答案A
用户答案A
结果:✓ 正确
【题目 7】
12²
A. 144.0 B. 165.0 C. 149.0 D. 24.0
正确答案A
用户答案A
结果:✓ 正确
【题目 8】
√16 + 2
A. 11.0 B. 1.0 C. 12.0 D. 6.0
正确答案D
用户答案D
结果:✓ 正确
【题目 9】
√81
A. 14.0 B. 40.5 C. 9.0 D. 2.0
正确答案C
用户答案C
结果:✓ 正确
【题目 10】
3² + 16
A. 31.0 B. 25.0 C. 30.0 D. 20.0
正确答案B
用户答案B
结果:✓ 正确

@ -0,0 +1,4 @@
# 注册码记录文件
# 格式: 邮箱|注册码|过期时间戳|过期时间
test002@example.com|4R8uZYiQ|1759552847711|2025-10-04 12:40:47

@ -0,0 +1,104 @@
{
"users": [
{
"userId": "a1b2c3d4-5678-90ef-ghij-klmnopqrstuv",
"username": "小学-测试",
"password": "encrypted123",
"email": "test@test.com",
"grade": "ELEMENTARY",
"totalQuizzes": 0,
"averageScore": 0.0,
"registrationDate": "Oct 4, 2025, 12:30:47PM"
},
{
"userId": "b2c3d4e5-6789-01fg-hijk-lmnopqrstuvw",
"username": "小学-张三测试",
"password": "9a931c55ac02bf216550c464b1992a30c522dfabf6cb31deada5c716bc13a263",
"email": "test001@example.com",
"grade": "ELEMENTARY",
"totalQuizzes": 0,
"averageScore": 0.0,
"registrationDate": "Oct 4, 2025, 12:30:47PM"
},
{
"userId": "c3d4e5f6-7890-12hi-jklm-nopqrstuvwxy",
"username": "初中-李明",
"password": "222711cc1da343bafd214b51a33d189a425e801b5d45774a941bbf68a1116d5c",
"email": "middle@test.com",
"grade": "MIDDLE",
"totalQuizzes": 0,
"averageScore": 0.0,
"registrationDate": "Oct 4, 2025, 12:30:47PM"
},
{
"userId": "d4e5f6g7-8901-23jk-lmno-pqrstuvwxyz",
"username": "高中-王华",
"password": "9f3abee248c95d9ed301ee5a5b71318c0838c994ac5f66e70bfc9ee7ecad0150",
"email": "high@test.com",
"grade": "HIGH",
"totalQuizzes": 0,
"averageScore": 0.0,
"registrationDate": "Oct 4, 2025, 12:30:47PM"
},
{
"userId": "e5f6g7h8-9012-34lm-mnop-qrstuvwxyza",
"username": "小学-重载测试",
"password": "bd3910fa48dc018fb9884e1d78649396d96882f6fcd8cbcd87b3e0c8bfc86e15",
"email": "reload@test.com",
"grade": "ELEMENTARY",
"totalQuizzes": 0,
"averageScore": 0.0,
"registrationDate": "Oct 4, 2025, 12:30:47PM"
},
{
"userId": "f6g7h8i9-0123-45no-opqr-stuvwxyzabc",
"username": "小学-李四",
"password": "encrypted",
"email": "lisi@test.com",
"grade": "ELEMENTARY",
"totalQuizzes": 0,
"averageScore": 0.0,
"registrationDate": "Oct 4, 2025, 12:30:47PM"
},
{
"userId": "g7h8i9j0-1234-56op-pqrs-tuvwxyzabcd",
"username": "初中-王五",
"password": "9a931c55ac02bf216550c464b1992a30c522dfabf6cb31deada5c716bc13a263",
"email": "wangwu@test.com",
"grade": "MIDDLE",
"totalQuizzes": 1,
"averageScore": 100.0,
"registrationDate": "Oct 4, 2025, 12:30:47PM"
},
{
"userId": "h8i9j0k1-2345-67pq-qrst-uvwxyzabcde",
"username": "2803234009@qq.com",
"password": "ef6d562d372a2978b827d204932668d860691727821e9a36f6f532df6fb581bd",
"email": "2803234009@qq.com",
"grade": "ELEMENTARY",
"totalQuizzes": 0,
"averageScore": 0.0,
"registrationDate": "Oct 5, 2025, 7:12:50PM"
},
{
"userId": "3fff6ec1-76f9-4ce6-9880-4a1859b4f5a3",
"username": "111",
"password": "950d32ffc1f6079e12d28efdc0e8db995129e30a9dd4f91eae31a81f13389caa",
"email": "ljy.sbp@gmail.com",
"grade": "ELEMENTARY",
"totalQuizzes": 0,
"averageScore": 0.0,
"registrationDate": "Oct 5, 2025, 9:35:09PM"
},
{
"userId": "75fd5144-97f1-44eb-bf34-d339a0cffd1c",
"username": "1111",
"password": "b6e4e03a35a862ae38516e7b6d0e0ae8c0a6444bd3d33713e7d212e8ecd5531a",
"email": "111@123.com",
"grade": "ELEMENTARY",
"totalQuizzes": 0,
"averageScore": 0.0,
"registrationDate": "Oct 6, 2025, 1:35:39PM"
}
]
}

@ -0,0 +1,436 @@
# 数学题库生成系统详细设计文档
## 项目概述
### 项目目标
为小初高学生提供一个本地的桌面端图像化应用,支持用户注册和登录,根据学生年级按规则随机生成数学选择题,完成答题后自动计算分数
### 技术栈
| 类型 | 技术 | 说明 |
| -------- | -------------------------- | ---------------------------------------- |
| 开发语言 | Java 21+ | 保证跨平台兼容性,支持 JavaFX |
| GUI框架 | JavaFX 17+ | 实现桌面图形化界面,提供组件化开发能力 |
| 依赖管理 | Maven | 管理 JavaFX、JSON 解析等依赖 |
| JSON解析 | Gson 2.10+ | 序列化 / 反序列化用户数据、题目数据 |
| 邮件发送 | JavaMail API + 第三方 SMTP | 发送注册码(如 QQ 邮箱 / 网易邮箱 SMTP |
| 数据加密 | BCrypt 0.9.0+ | 加密存储用户密码,避免明文泄露 |
| 构建打包 | Maven Shade Plugin | 打包成可执行 JAR支持直接运行 |
### 总体设计
#### 项目目录结构
MathQuizApp/
├── .gitignore
├── pom.xml
├── README.md
├── doc/
│ ├── 需求分析/
│ │ └── 需求分析第一版.md
│ └── 设计文档/
│ ├── 设计文档第一版.md
│ ├── 设计文档第二版.md
│ ├── 设计文档第三版.md
│ └── 设计文档第四版.md
├── data/ # 运行时数据(不提交)
│ ├── users/ # 用户 JSON 文件(按邮箱哈希命名)
│ ├── history/ # 答题记录(如:初中-王五_1759552247819.txt
│ ├── temp_codes/ # 【建议新增】临时注册码目录(替代 registration_codes.txt
│ └── users.json # 【建议移除】单文件存储易冲突,应按用户分文件
└── src/
└── main/
└── java/
└── com/
└── mathquiz/ # ← 包名应全小写
├── Main.java # 程序入口
├── model/ # 数据模型
│ ├── User.java
│ ├── Grade.java
│ ├── ChoiceQuestion.java
│ ├── QuizResult.java # 答题结果(含分数、时间等)
│ └── QuizHistory.java # 历史记录(可选)
├── service/ # 业务逻辑
│ ├── UserService.java
│ ├── QuizService.java
│ ├── FileIOService.java
│ │
│ └── question_generator/
│ ├── QuestionFactoryManager.java
│ │
│ ├── factory/ # 工厂类(按年级)
│ │ ├── QuestionFactory.java (interface)
│ │ ├── ElementaryQuestionFactory.java
│ │ ├── MiddleQuestionFactory.java
│ │ └── HighQuestionFactory.java
│ │
│ └── strategy/ # 策略类(按运算类型)
│ ├── QuestionStrategy.java (interface)
│ ├── AbstractQuestionStrategy.java
│ │
│ ├── elementary/
│ │ ├── AdditionStrategy.java
│ │ ├── SubtractionStrategy.java
│ │ ├── MultiplicationStrategy.java
│ │ ├── DivisionStrategy.java
│ │ ├── ParenthesesAddStrategy.java
│ │ └── ParenthesesMultiplyStrategy.java
│ │
│ ├── middle/
│ │ ├── SquareStrategy.java
│ │ ├── SqrtStrategy.java
│ │ ├── SquareAddStrategy.java
│ │ ├── SqrtAddStrategy.java
│ │ └── MixedSquareSqrtStrategy.java
│ │
│ └── high/
│ ├── SinStrategy.java
│ ├── CosStrategy.java
│ ├── TanStrategy.java
│ └── TrigIdentityStrategy.java
├── ui/ # JavaFX 界面
│ ├── MainWindow.java
│ ├── RegisterPanel.java
│ ├── LoginPanel.java
│ ├── PasswordModifyPanel.java
│ ├── GradeSelectPanel.java
│ ├── QuizPanel.java
│ └── ResultPanel.java
└── util/ # 工具类
├── PasswordValidator.java
├── EmailUtil.java
├── RandomUtils.java
└── FileUtils.java
## 详细模块设计
### 模型层设计
#### User类
- String username
- String password // 加密后的密码
- String email
- Grade grade
- int totalQuizzes // 总答题次数
- double averageScore // 平均分
- Date registrationDate // 注册时间
#### ChoiceQuestion
- Sting questionText // 题目文本
- Object correctAnswer // 正确答案
- List<?> options // 选项列表
- Grade grade // 所属学段
#### Grade 枚举
- ELEMENTARY // 小学
- MIDDLE // 初中
- HIGH // 高中
#### QuizResult
- int totalQuestions; // 总题数
- int correctCount; // 正确题数
- int wrongCount; // 错误题数
- int score; // 得分
#### QuizHistory
- String username; // 用户名
- Date timestamp; // 答题时间
- List<ChoiceQuestion> questions; // 题目列表
- List<Integer> userAnswers; // 用户答案列表
- int score; // 得分
### 工具层设计
#### EmailUtil
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| -------------------- | --------------------------------------- | ------- | ------------------------------------------------------------ |
| sendRegistrationCode | String toEmail, String registrationCode | boolean | 模拟发送注册码邮件(预留接口),打印收件人邮箱和注册码,返回 true。 |
| sendPasswordReset | String toEmail, String newPassword | boolean | 模拟发送密码重置邮件(预留接口),打印收件人邮箱和新密码,返回 true。 |
| isValidEmail | String email | boolean | 验证邮箱格式,使用正则表达式`^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$`检查,为空或不匹配则返回 false。 |
#### RandomUtils
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ------------ | ---------------------- | ------- | ------------------------------------------------------------ |
| nextInt | int min, int max | int | 生成`[min, max]`范围内的随机整数,若 min > max 则抛 IllegalArgumentException。 |
| randomChoice | T[] array | T | 从数组中随机选择一个元素,数组为空则抛 IllegalArgumentException。 |
| randomChoice | List<T> list | T | 从列表中随机选择一个元素,列表为空则抛 IllegalArgumentException。 |
| shuffle | List<T> list | void | 打乱列表元素顺序(使用 Collections.shuffle。 |
| nextDouble | double min, double max | double | 生成`[min, max)`范围内的随机双精度浮点数,若 min > max 则抛异常。 |
| nextBoolean | 无参数 | boolean | 生成随机布尔值。 |
| probability | double probability | boolean | 按给定概率0.0-1.0)返回 true概率超出范围则抛异常。 |
#### PasswordValidator
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ------------------------ | ---------------------------------------------- | ------- | ------------------------------------------------------------ |
| validatePassword | String password | String | 验证密码格式返回错误信息null 表示有效。检查非空、长度6-20 位)、无空格、包含字母和数字。 |
| isValid | String password | boolean | 调用 validatePassword返回密码是否有效错误信息为 null 则返回 true。 |
| getPasswordStrength | String password | String | 评估密码强度(弱 / 中 / 强),基于长度、大小写字母、数字、特殊字符等评分。 |
| encrypt | String password | String | 使用 SHA-256 加密密码,返回 16 进制字符串,密码为 null 则抛异常。 |
| matches | String plainPassword, String encryptedPassword | boolean | 验证明文密码加密后是否与加密密码匹配。 |
| generateRegistrationCode | 无参数 | String | 生成 6-10 位随机注册码(包含大小写字母和数字)。 |
| generateRegistrationCode | int minLen, int maxLen | String | 生成指定长度范围的注册码,确保至少包含 1 个大写、1 个小写字母和 1 个数字,最终打乱顺序。 |
| generateRandomPassword | int length, boolean includeSpecialChars | String | 生成固定长度随机密码,可包含特殊字符,确保至少 1 个字母和 1 个数字,最终打乱顺序。 |
| shuffleString | String str | String | 私有方法,使用 Fisher-Yates 算法打乱字符串顺序。 |
| isWeakPassword | String password | boolean | 检查密码是否为常见弱密码(如 123456或连续字符如 abcdef。 |
| getPasswordSuggestion | String password | String | 根据密码情况返回建议(错误信息或强度提升建议)。 |
#### FileUtils
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| -------------------------- | ------------------------------------ | ------- | ------------------------------------------------------------ |
| readFileToString | String filePath | String | 读取文件内容为字符串UTF-8 编码),失败则抛 IOException。 |
| writeStringToFile | String filePath, String content | void | 将字符串写入文件UTF-8 编码),失败则抛 IOException。 |
| createDirectoryIfNotExists | String dirPath | void | 若目录不存在则创建(包括父目录),失败则抛 IOException。 |
| exists | String filePath | boolean | 检查文件是否存在。 |
| deleteFile | String filePath | boolean | 删除文件,返回是否成功(失败打印异常)。 |
| listFiles | String dirPath | File[] | 获取目录下所有文件,目录不存在或非目录则返回空数组。 |
| appendToFile | String filePath, String content | void | 追加内容到文件末尾UTF-8 编码),失败则抛 IOException。 |
| copyFile | String sourcePath, String targetPath | void | 复制文件(覆盖目标文件),失败则抛 IOException。 |
| getFileSize | String filePath | long | 获取文件大小(字节),失败则抛 IOException。 |
| saveAsJson | Object data, String filePath | void | 将对象序列化为 JSON 并保存到文件,失败则抛 IOException。 |
| readJsonToObject | String filePath, Class<T> classOfT | T | 从 JSON 文件反序列化为指定类型对象,失败则抛 IOException。 |
| readJsonToObject | String filePath, Type typeOfT | T | 从 JSON 文件反序列化为泛型对象(支持泛型),失败则抛 IOException。 |
| toJson | Object data | String | 将对象转换为 JSON 字符串。 |
| fromJson | String json, Class<T> classOfT | T | 将 JSON 字符串反序列化为指定类型对象。 |
| fromJson | String json, Type typeOfT | T | 将 JSON 字符串反序列化为泛型对象(支持泛型)。 |
### 服务层设计
#### UserService
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ----------------------------- | ------------------------------------------------------------ | ----------------------------- | ------------------------------------------------------------ |
| generateRegistrationCode | String email | String | 验证邮箱格式,生成 6-10 位注册码及过期时间10 分钟),保存到文件并返回注册码 |
| saveRegistrationCodeToFile | String email, String code, long expiryTime | void | 读取现有注册码,添加 / 更新当前邮箱的注册码,保存到文件 |
| loadRegistrationCodesFromFile | 无 | Map<String, RegistrationCode> | 从文件读取注册码,解析为邮箱 - 注册码映射返回 |
| verifyRegistrationCode | String email, String code | boolean | 验证邮箱对应的注册码是否有效(未过期且匹配),有效则删除注册码 |
| saveAllRegistrationCodes | Map<String, RegistrationCode> codes | void | 将所有注册码保存到文件 |
| cleanExpiredCodes | 无 | void | 清理过期的注册码并保存到文件 |
| register | String username, String password, String email, String verificationCode | User | 验证注册码、用户名格式、用户名唯一性、密码强度、邮箱格式,提取学段,加密密码,创建并保存用户 |
| login | String username, String password | User | 验证用户名存在性及密码正确性,设置当前用户并保存 |
| autoLogin | 无 | User | 从文件加载当前用户并返回(未完全展示) |
#### QuizService
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ---------------------------- | -------------------------------------------- | -------------- | ------------------------------------------------------------ |
| startNewQuiz | User user, int questionCount | void | 初始化答题会话(清空题目、答案,重置索引),获取用户历史题目,根据用户学段生成指定数量的新题目(排除历史题目) |
| getRecentHistoryQuestions | 无 | Set | 从文件读取历史题目,转换为 Set 返回 |
| getCurrentQuestion | 无 | ChoiceQuestion | 返回当前索引对应的题目,索引无效则返回 null |
| getQuestion | int index | ChoiceQuestion | 返回指定索引的题目,索引无效则返回 null |
| getAllQuestions | 无 | List | 返回当前所有题目的副本 |
| nextQuestion | 无 | boolean | 当前索引加 1移至下一题成功返回 true否则 false |
| previousQuestion | 无 | boolean | 当前索引减 1移至上一题成功返回 true否则 false |
| goToQuestion | int index | boolean | 跳至指定索引题目,成功返回 true否则 false |
| getCurrentQuestionIndex | 无 | int | 返回当前题目索引 |
| getTotalQuestions | 无 | int | 返回总题目数量 |
| isFirstQuestion | 无 | boolean | 判断当前是否为第一题 |
| isLastQuestion | 无 | boolean | 判断当前是否为最后一题 |
| submitAnswer | int questionIndex, int optionIndex | boolean | 提交指定题目的答案,验证索引有效性后保存答案,返回答案是否正确 |
| submitCurrentAnswer | int optionIndex | boolean | 提交当前题目的答案,调用 submitAnswer (currentQuestionIndex, optionIndex) |
| getUserAnswer | int questionIndex | Integer | 返回指定题目的用户答案,索引无效返回 null |
| getAllUserAnswers | 无 | List | 返回所有用户答案的副本 |
| isAllAnswered | 无 | boolean | 判断所有题目是否都已作答 |
| getAnsweredCount | 无 | int | 返回已作答的题目数量 |
| calculateResult | 无 | QuizResult | 计算答题结果(总题数、正确数、错误数、分数)并返回 |
| getCorrectQuestionIndices | 无 | List | 返回所有回答正确的题目索引 |
| getWrongQuestionIndices | 无 | List | 返回所有回答错误的题目索引 |
| getUnansweredQuestionIndices | 无 | List | 返回所有未作答的题目索引 |
| checkAnswer | ChoiceQuestion question, int userAnswerIndex | boolean | 验证用户答案是否正确(比较选项与正确答案) |
| getCorrectAnswerIndex | ChoiceQuestion question | int | 返回题目的正确答案在选项中的索引 |
| getCorrectAnswerLetter | ChoiceQuestion question | String | 返回正确答案的字母形式A/B/C/D |
| getAccuracy | QuizResult result | double | 计算答题正确率(正确数 / 总数 ×100% |
| isPassed | QuizResult result | boolean | 判断是否及格分数≥60 |
| getGrade | QuizResult result | String | 根据分数返回评级(优秀 / 良好 / 中等 / 及格 / 不及格) |
| getCorrectCount | QuizHistory history | int | 计算历史记录中回答正确的题目数量 |
| getWrongCount | QuizHistory history | int | 计算历史记录中回答错误的题目数量 |
| formatQuestion | ChoiceQuestion question | String | 格式化题目文本及选项(未完全展示) |
#### FileIOService
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| --------------------- | ----------------------- | ------- | ------------------------------------------------------------ |
| initDataDirectory | 无 | void | 初始化数据目录data、users、history若用户文件不存在则创建空文件 |
| saveUser | User user | void | 读取用户列表,若用户已存在则更新,否则添加,保存回文件 |
| loadAllUsers | 无 | List | 从文件读取所有用户并返回 |
| findUserByUsername | String username | User | 查找并返回指定用户名的用户,不存在则返回 null |
| isUsernameExists | String username | boolean | 判断用户名是否已存在 |
| saveCurrentUser | User user | void | 将当前用户保存到文件 |
| loadCurrentUser | 无 | User | 从文件加载当前用户,不存在则返回 null |
| clearCurrentUser | 无 | void | 删除当前用户文件 |
| saveQuizHistory | QuizHistory history | void | 将答题历史格式化并保存到文件(包含题目、答案、结果等) |
| getHistoryQuestions | 无 | List | 读取最近 20 个历史文件,提取题目文本并返回 |
| calculateCorrectCount | QuizHistory history | int | 计算历史记录中正确的题目数量 |
| getCorrectAnswerIndex | ChoiceQuestion question | int | 返回题目的正确答案在选项中的索引 |
| sanitizeFilename | String filename | String | 清理文件名中的特殊字符(替换为下划线) |
#### QuestionFactoryManager
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ----------------- | -------------------------------------------- | ------ | ------------------------------------------------------------ |
| generateQuestions | Grade grade, int count, Set historyQuestions | List | 根据学段获取对应工厂,生成指定数量的题目(排除历史题目),最多尝试 count×10 次,返回生成的题目列表 |
#### QuestionFactory接口
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ----------------- | -------- | -------------- | ---------------------------------------------- |
| createQuestion | 无 | ChoiceQuestion | 生成并返回一道选择题(接口方法,由实现类实现) |
| getSupportedGrade | 无 | Grade | 返回工厂支持的学段(接口方法,由实现类实现) |
#### ElementaryQuestionFactory
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| -------------- | -------- | -------------- | ------------------------------------------------------------ |
| createQuestion | 无 | ChoiceQuestion | 从策略列表strategies中随机选择一个题目生成策略QuestionStrategy调用其 generate () 方法生成并返回选择题 |
#### MiddleQuestionFactory
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ------------------- | -------- | -------------- | ------------------------------------------------------------ |
| HighQuestionFactory | 无 | 无(构造方法) | 初始化策略列表`strategies`,并注册所有高中题目生成策略(`SinStrategy`、`CosStrategy`、`TanStrategy`、`TrigIdentityStrategy` |
| createQuestion | 无 | ChoiceQuestion | 通过`RandomUtils`从策略列表中随机选择一个题目生成策略,调用该策略的`generate()`方法生成并返回选择题 |
| getSupportedGrade | 无 | Grade | 返回`Grade.HIGH`,表示该工厂支持高中年级的题目生成 |
#### HighQuestionFactory
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| --------------------- | -------- | -------------- | ------------------------------------------------------------ |
| MiddleQuestionFactory | 无 | 无(构造方法) | 初始化策略列表`strategies`,并注册所有初中题目生成策略(`SquareStrategy`、`SquareAddStrategy`、`SqrtStrategy`、`SqrtAddStrategy`、`MixedSquareSqrtStrategy` |
| createQuestion | 无 | ChoiceQuestion | 通过`RandomUtils`从策略列表中随机选择一个题目生成策略,调用该策略的`generate()`方法生成并返回选择题 |
| getSupportedGrade | 无 | Grade | 返回`Grade.MIDDLE`,表示该工厂支持初中年级的题目生成 |
#### QuestionStrategy接口
| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
| --------------- | -------- | -------------- | -------------------------- |
| generate | 无 | ChoiceQuestion | 定义生成题目的接口方法 |
| getStrategyName | 无 | String | 定义获取策略名称的接口方法 |
#### AbstractQuestionStrategy
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ------------------------------------- | ---------------------------------------------------- | ------------ | ------------------------------------------------------------ |
| generateNumericOptions | double correctAnswer | List<Double> | 生成包含正确答案和 3 个干扰项的数值选项,打乱顺序后返回 |
| generateNumericOptionsWithCommonError | double correctAnswer, double commonError | List<Double> | 生成包含正确答案、常见错误答案和其他干扰项的数值选项,打乱顺序后返回 |
| generateStringOptions | String correctAnswer, List<String> allPossibleValues | List<String> | 生成包含正确答案和 3 个干扰项的字符串选项(如三角函数值),打乱顺序后返回 |
#### AdditionStrategy
| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
| --------------- | -------- | -------------- | ------------------------------------------------------------ |
| generate | 无 | ChoiceQuestion | 生成加法题(如 3 + 5随机生成两个加数1-30计算和作为答案生成选项 |
| getStrategyName | 无 | String | 返回策略名称 “加法” |
#### DivisionStrategy
| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
| --------------- | -------- | -------------- | ------------------------------------------------------------ |
| generate | 无 | ChoiceQuestion | 生成除法题(确保整除,如 8 ÷ 2随机生成除数2-10和商1-10计算被除数除数 × 商),生成选项 |
| getStrategyName | 无 | String | 返回策略名称 “除法” |
#### MultiplicationStrategy
| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
| --------------- | -------- | -------------- | ------------------------------------------------------------ |
| generate | 无 | ChoiceQuestion | 生成乘法题(如 3 × 4随机生成两个因数1-12计算乘积作为答案生成选项 |
| getStrategyName | 无 | String | 返回策略名称 “乘法” |
#### ParenthesesAddStrategy
| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
| --------------- | -------- | -------------- | ------------------------------------------------------------ |
| generate | 无 | ChoiceQuestion | 生成带括号的加法乘法混合题(如 (a + b) × c随机生成两个加数1-20和乘数2-10计算和与乘数的积作为答案生成选项 |
| getStrategyName | 无 | String | 返回策略名称 “括号加法乘法” |
#### ParenthesesMultiplyStrategy
| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
| --------------- | -------- | -------------- | ------------------------------------------------------------ |
| generate | 无 | ChoiceQuestion | 生成带括号的减法乘法混合题(如 (a - b) × c随机生成两个数1-20和乘数2-10取大数减小数的差与乘数相乘作为答案生成选项 |
| getStrategyName | 无 | String | 返回策略名称 “括号减法乘法” |
#### SubtractionStrategy
| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
| --------------- | -------- | -------------- | ------------------------------------------------------------ |
| generate | 无 | ChoiceQuestion | 生成减法题(如 5 - 3随机生成两个数1-30取大数减小数确保结果为正生成选项 |
| getStrategyName | 无 | String | 返回策略名称 “减法” |
#### MixedSquareSqrtStrategy
| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
| --------------- | -------- | -------------- | ------------------------------------------------------------ |
| generate | 无 | ChoiceQuestion | 生成平方与开方混合题如√49 + 3²随机生成开方根值2-8和平方底数2-6计算开方值与平方值的和作为答案生成选项 |
| getStrategyName | 无 | String | 返回策略名称 “平方开方混合” |
#### SqrtAddStrategy
| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
| --------------- | -------- | -------------- | ------------------------------------------------------------ |
| generate | 无 | ChoiceQuestion | 生成开方与加法混合题如√49 + 5随机生成开方根值2-10和加数1-20计算根值与加数的和作为答案生成选项 |
| getStrategyName | 无 | String | 返回策略名称 “开方加法” |
#### SqrtStrategy
| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
| --------------- | -------- | -------------- | ------------------------------------------------------------ |
| generate | 无 | ChoiceQuestion | 生成开方运算题如√49随机生成根值2-12计算开方结果作为答案选项包含常见错误被开方数 ÷2 |
| getStrategyName | 无 | String | 返回策略名称 “开方” |
#### SquareAddStrategy
| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
| --------------- | -------- | -------------- | ------------------------------------------------------------ |
| generate | 无 | ChoiceQuestion | 生成平方与加法混合题(如 5² + 10随机生成平方底数2-10和加数1-20计算平方值与加数的和作为答案生成选项 |
| getStrategyName | 无 | String | 返回策略名称 “平方加法” |
#### SquareStrategy
| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
| --------------- | -------- | -------------- | ------------------------------------------------------------ |
| generate | 无 | ChoiceQuestion | 生成平方运算题(如 5²随机生成底数1-15计算平方值作为答案选项包含常见错误底数 ×2 |
| getStrategyName | 无 | String | 返回策略名称 “平方” |
#### CosStrategy
| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
| --------------- | -------- | -------------- | ------------------------------------------------------------ |
| generate | 无 | ChoiceQuestion | 生成余弦函数题(如 cos (45°)从预设特殊角0°、30° 等)中随机选择,答案为对应角的余弦值,从所有余弦值中生成选项 |
| getStrategyName | 无 | String | 返回策略名称 “余弦函数” |
#### SinStrategy
| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
| --------------- | -------- | -------------- | ------------------------------------------------------------ |
| generate | 无 | ChoiceQuestion | 生成正弦函数题(如 sin (30°)从预设特殊角0°、30° 等)中随机选择,答案为对应角的正弦值,从所有正弦值中生成选项 |
| getStrategyName | 无 | String | 返回策略名称 “正弦函数” |
#### TanStrategy
| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
| --------------- | -------- | -------------- | ------------------------------------------------------------ |
| generate | 无 | ChoiceQuestion | 生成正切函数题(如 tan (45°)从预设特殊角0°、30° 等)中随机选择,答案为对应角的正切值,从所有正切值中生成选项 |
| getStrategyName | 无 | String | 返回策略名称 “正切函数” |
#### TrigIdentityStrategy
| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
| --------------- | -------- | -------------- | ------------------------------------------------------------ |
| generate | 无 | ChoiceQuestion | 生成三角恒等式题(如 sin²(30°) + cos²(30°) = ?),从 30°、45° 等特殊角中选择,答案固定为 1从给定值列表生成选项 |
| getStrategyName | 无 | String | 返回策略名称 “三角恒等式” |
### UI层接口设计
####

@ -2,7 +2,7 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mathquiz</groupId>
@ -11,74 +11,100 @@
<packaging>jar</packaging>
<name>Math Quiz Application</name>
<description>小初高数学学习软件 - Swing版本</description>
<description>小初高数学学习软件 - JavaFX版本</description>
<!-- 新增 JitPack 仓库配置 -->
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<javafx.version>21.0.2</javafx.version>
</properties>
<dependencies>
<!-- Gson for JSON -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-base</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
<version>${javafx.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Maven Compiler Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<!-- Maven Jar Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.1</version>
<configuration>
<archive>
<manifest>
<mainClass>com.mathquiz.Main</mainClass>
<addClasspath>true</addClasspath>
</manifest>
</archive>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<!-- Maven Shade Plugin - 打包为可执行JAR -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<configuration>
<mainClass>com.mathquiz.MathQuizApp/com.Test</mainClass>
<native-bundling>
<winConsole>false</winConsole>
<appName>${project.name}</appName>
<vendor>com.mathquiz</vendor>
<appVersion>${project.version}</appVersion>
<winShortcut>true</winShortcut>
<winMenu>true</winMenu>
</native-bundling>
</configuration>
<executions>
<execution>
<phase>package</phase>
<id>default-cli</id>
<goals>
<goal>shade</goal>
<goal>jpackage</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.mathquiz.Main</mainClass>
</transformer>
</transformers>
<finalName>MathQuizApp</finalName>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,24 @@
// src/main/java/com/Main.java
package com;
import com.ui.MainWindow;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Test extends Application {
@Override
public void start(Stage primaryStage) {
MainWindow mainWindow = new MainWindow(primaryStage);
Scene scene = new Scene(mainWindow, 1366, 786);
primaryStage.setTitle("中小学数学答题系统");
primaryStage.setScene(scene);
primaryStage.setResizable(true);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

@ -1,9 +1,29 @@
package com.model;
//学段
public enum Grade {
ELEMENTARY, // 小学
MIDDLE, // 初中
HIGH // 高中
}
// 枚举常量,初始化时传入对应的中文描述
ELEMENTARY("小学"),
MIDDLE("初中"),
HIGH("高中");
private final String chineseName;
Grade(String chineseName) {
this.chineseName = chineseName;
}
public String getChineseName() {
return chineseName;
}
public static Grade valueOfChinese(String chineseName) {
// 遍历所有枚举常量,匹配中文描述
for (Grade grade : Grade.values()) {
if (grade.chineseName.equals(chineseName)) {
return grade;
}
}
throw new IllegalArgumentException("不存在对应的年级:" + chineseName);
}
}

@ -52,11 +52,7 @@ public class QuizResult {
@Override
public String toString() {
return "QuizResult{" +
"totalQuestions=" + totalQuestions +
", correctCount=" + correctCount +
", wrongCount=" + wrongCount +
", score=" + score +
'}';
int correctPercent = (int) ((double) correctCount / totalQuestions * 100);
return "您答对了" + correctCount + "/" + totalQuestions + "题,得分:" + correctPercent + "%";
}
}

@ -1,11 +1,13 @@
package com.model;
import java.util.Date;
import java.util.UUID;
//用户
public class User {
private final String userId; // 不可变,账号唯一标识
private String username; // 用户名
private String password; // 密码(加密后)
private String email; // 邮箱
@ -19,8 +21,9 @@ public class User {
/**
*
*/
public User(String username, String password, String email, Grade grade,
public User(String userId, String username, String password, String email, Grade grade,
int totalQuizzes, double averageScore, Date registrationDate) {
this.userId = userId;
this.username = username;
this.password = password;
this.email = email;
@ -34,6 +37,7 @@ public class User {
*
*/
public User(String username, String password, String email, Grade grade) {
this.userId = UUID.randomUUID().toString();
this.username = username;
this.password = password;
this.email = email;
@ -43,13 +47,15 @@ public class User {
this.registrationDate = new Date();
}
public String getUserId() {
return userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
public void setUsername(String username){
this.username = username;
}

@ -57,7 +57,7 @@ public class FileIOService {
boolean found = false;
for (int i = 0; i < users.size(); i++) {
if (users.get(i).getUsername().equals(user.getUsername())) {
if (users.get(i).getUserId().equals(user.getUserId())) {
users.set(i, user);
found = true;
break;
@ -94,10 +94,26 @@ public class FileIOService {
return null;
}
public User findUserByEmail(String email) throws IOException {
List<User> users = loadAllUsers();
for (User user : users) {
if (user.getEmail().equals(email)) {
return user;
}
}
return null;
}
public boolean isUsernameExists(String username) throws IOException {
return findUserByUsername(username) != null;
}
public boolean isEmailExists(String email) throws IOException {
return findUserByEmail(email) != null;
}
public void saveCurrentUser(User user) throws IOException {
FileUtils.saveAsJson(user, CURRENT_USER_FILE);
}

@ -16,6 +16,7 @@ public class QuizService {
private List<ChoiceQuestion> currentQuestions;
private List<Integer> userAnswers;
private int currentQuestionIndex;
private int answerNumber;
// ==================== 构造方法 ====================
@ -173,6 +174,10 @@ public class QuizService {
return count;
}
public boolean isAnswered(int questionIndex) {
return userAnswers.get(questionIndex) != null ;
}
// ==================== 成绩计算 ====================
public QuizResult calculateResult() {
@ -422,6 +427,14 @@ public class QuizService {
// ==================== Getters ====================
public int getAnswerNumber() {
return answerNumber;
}
public void setAnswerNumber(int answerNumber) {
this.answerNumber = answerNumber;
}
public List<ChoiceQuestion> getCurrentQuestions() {
return new ArrayList<>(currentQuestions);
}

@ -25,7 +25,10 @@ public class UserService {
private final FileIOService fileIOService;
private User currentUser;
private static final Pattern USERNAME_PATTERN = Pattern.compile("^(小学|初中|高中)-([\\u4e00-\\u9fa5a-zA-Z]+)$");
// [学段]-[姓名]
// private static final Pattern USERNAME_PATTERN = Pattern.compile("^(小学|初中|高中)-([\\u4e00-\\u9fa5a-zA-Z]+)$");
// [用户名]@[域名主体].[顶级域名]
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$");
// ==================== 构造方法 ====================
@ -233,27 +236,27 @@ public class UserService {
/**
*
*/
public User register(String username, String password, String email, String verificationCode) throws IOException {
public User register(String password, String email, String verificationCode) throws IOException {
// 1. 验证注册码
if (!verifyRegistrationCode(email, verificationCode)) {
throw new IllegalArgumentException("注册码错误!");
}
// 2. 验证用户名格式
if (!validateUsername(username)) {
throw new IllegalArgumentException("用户名格式错误!正确格式:学段-姓名(如:小学-张三)");
}
// // 2. 验证用户名格式
// if (!validateUsername(username)) {
// throw new IllegalArgumentException("用户名格式错误!正确格式:学段-姓名(如:小学-张三)");
// }
// 3. 验证用户名是否已存在
if (fileIOService.isUsernameExists(username)) {
throw new IllegalArgumentException("用户名已存在");
if (fileIOService.isEmailExists(email)) {
throw new IllegalArgumentException("邮箱已经注册");
}
// 4. 验证密码强度
String passwordError = PasswordValidator.validatePassword(password);
if (passwordError != null) {
throw new IllegalArgumentException(passwordError);
}
// // 4. 验证密码强度
// String passwordError = PasswordValidator.validatePassword(password);
// if (passwordError != null) {
// throw new IllegalArgumentException(passwordError);
// }
// 5. 验证邮箱格式
if (!validateEmail(email)) {
@ -261,21 +264,28 @@ public class UserService {
}
// 6. 从用户名中提取学段
Grade grade = extractGradeFromUsername(username);
// Grade grade = extractGradeFromUsername(username);
Grade grade = Grade.ELEMENTARY;
// 7. 加密密码
String hashedPassword = hashPassword(password);
String hashedPassword = PasswordValidator.encrypt(password);
// 8. 创建用户对象
User user = new User(username, hashedPassword, email, grade);
User user = new User(email, hashedPassword, email, grade);
this.setCurrentUser(user);
// 9. 保存到文件
fileIOService.saveUser(user);
System.out.println("✓ 用户注册成功:" + username);
// System.out.println("✓ 用户注册成功:" + email);
return user;
}
public void setCurrentUser(User user) throws IOException {
this.currentUser = user;
fileIOService.saveCurrentUser(user);
}
// ==================== 用户登录 ====================
public User login(String username, String password) throws IOException {
@ -285,7 +295,7 @@ public class UserService {
throw new IllegalArgumentException("用户名不存在!");
}
String hashedPassword = hashPassword(password);
String hashedPassword = PasswordValidator.encrypt(password);
if (!user.getPassword().equals(hashedPassword)) {
throw new IllegalArgumentException("密码错误!");
}
@ -293,7 +303,7 @@ public class UserService {
this.currentUser = user;
fileIOService.saveCurrentUser(user);
System.out.println("✓ 登录成功,欢迎 " + getRealName(user) + "" + getGradeDisplayName(user) + "");
// System.out.println("✓ 登录成功,欢迎 " + getRealName(user) + "" + getGradeDisplayName(user) + "");
return user;
}
@ -302,7 +312,7 @@ public class UserService {
if (user != null) {
this.currentUser = user;
System.out.println("✓ 自动登录成功,欢迎回来 " + getRealName(user));
// System.out.println("✓ 自动登录成功,欢迎回来 " + getRealName(user));
}
return user;
@ -310,7 +320,7 @@ public class UserService {
public void logout() {
if (currentUser != null) {
System.out.println("✓ " + getRealName(currentUser) + " 已退出登录");
// System.out.println("✓ " + getRealName(currentUser) + " 已退出登录");
this.currentUser = null;
fileIOService.clearCurrentUser();
}
@ -326,8 +336,8 @@ public class UserService {
// ==================== 密码管理 ====================
public boolean changePassword(User user, String oldPassword, String newPassword) throws IOException {
String hashedOldPassword = hashPassword(oldPassword);
public boolean changePassword(User user, String oldPassword, String newPassword, String confirmPassword) throws IOException {
String hashedOldPassword = PasswordValidator.encrypt(oldPassword);
if (!user.getPassword().equals(hashedOldPassword)) {
throw new IllegalArgumentException("旧密码错误!");
}
@ -341,7 +351,11 @@ public class UserService {
throw new IllegalArgumentException("新密码不能与旧密码相同!");
}
String hashedNewPassword = hashPassword(newPassword);
if (!oldPassword.equals(confirmPassword)) {
throw new IllegalArgumentException("两次新密码不同!");
}
String hashedNewPassword = PasswordValidator.encrypt(newPassword);
user.setPassword(hashedNewPassword);
fileIOService.saveUser(user);
@ -371,7 +385,7 @@ public class UserService {
throw new IllegalArgumentException(passwordError);
}
String hashedNewPassword = hashPassword(newPassword);
String hashedNewPassword = PasswordValidator.encrypt(newPassword);
user.setPassword(hashedNewPassword);
fileIOService.saveUser(user);
@ -395,10 +409,38 @@ public class UserService {
fileIOService.saveCurrentUser(user);
}
System.out.println("✓ 邮箱更新成功");
// System.out.println("✓ 邮箱更新成功");
return true;
}
public void updateUsername(User user, String newUsername) throws IOException {
if (newUsername.isEmpty()) {
throw new IllegalArgumentException("用户名不为空!");
}
user.setUsername(newUsername);
fileIOService.saveUser(user);
if (currentUser != null && currentUser.getUsername().equals(user.getUsername())) {
this.currentUser = user;
fileIOService.saveCurrentUser(user);
}
}
public void updateGrade(User user, String chinesename) throws IOException {
if (chinesename.isEmpty()) {
throw new IllegalArgumentException("学段中文名为空");
}
Grade newGrade = Grade.valueOfChinese(chinesename);
user.setGrade(newGrade);
fileIOService.saveUser(user);
if (currentUser != null && currentUser.getUsername().equals(user.getUsername())) {
this.currentUser = user;
fileIOService.saveCurrentUser(user);
}
}
public void updateUserStatistics(User user, int score) throws IOException {
int oldTotal = user.getTotalQuizzes();
double oldAverage = user.getAverageScore();
@ -422,23 +464,23 @@ public class UserService {
// ==================== 业务逻辑方法====================
/**
*
*/
public String getRealName(User user) {
if (user == null || user.getUsername() == null) {
return "";
}
String username = user.getUsername();
int dashIndex = username.indexOf('-');
if (dashIndex > 0 && dashIndex < username.length() - 1) {
return username.substring(dashIndex + 1);
}
return username;
}
// /**
// * 从用户名提取真实姓名
// */
// public String getRealName(User user) {
// if (user == null || user.getUsername() == null) {
// return "";
// }
//
// String username = user.getUsername();
// int dashIndex = username.indexOf('-');
//
// if (dashIndex > 0 && dashIndex < username.length() - 1) {
// return username.substring(dashIndex + 1);
// }
//
// return username;
// }
/**
*
@ -467,7 +509,6 @@ public class UserService {
StringBuilder sb = new StringBuilder();
sb.append("========== 用户统计 ==========\n");
sb.append("用户名:").append(user.getUsername()).append("\n");
sb.append("真实姓名:").append(getRealName(user)).append("\n");
sb.append("学段:").append(getGradeDisplayName(user)).append("\n");
sb.append("邮箱:").append(user.getEmail()).append("\n");
sb.append("总答题次数:").append(user.getTotalQuizzes()).append(" 次\n");
@ -480,14 +521,14 @@ public class UserService {
// ==================== 验证工具方法 ====================
private boolean validateUsername(String username) {
if (username == null || username.trim().isEmpty()) {
return false;
}
Matcher matcher = USERNAME_PATTERN.matcher(username);
return matcher.matches();
}
// private boolean validateUsername(String username) {
// if (username == null || username.trim().isEmpty()) {
// return false;
// }
//
// Matcher matcher = USERNAME_PATTERN.matcher(username);
// return matcher.matches();
// }
private boolean validateEmail(String email) {
if (email == null || email.trim().isEmpty()) {
@ -498,35 +539,15 @@ public class UserService {
return matcher.matches();
}
private Grade extractGradeFromUsername(String username) {
if (username.startsWith("小学-")) {
return Grade.ELEMENTARY;
} else if (username.startsWith("初中-")) {
return Grade.MIDDLE;
} else if (username.startsWith("高中-")) {
return Grade.HIGH;
}
throw new IllegalArgumentException("无法识别的学段");
}
private String hashPassword(String password) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(password.getBytes());
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("密码加密失败", e);
}
}
// private Grade extractGradeFromUsername(String username) {
// if (username.startsWith("小学-")) {
// return Grade.ELEMENTARY;
// } else if (username.startsWith("初中-")) {
// return Grade.MIDDLE;
// } else if (username.startsWith("高中-")) {
// return Grade.HIGH;
// }
//
// throw new IllegalArgumentException("无法识别的学段");
// }
}

@ -1,7 +1,7 @@
package com.mathquiz.ui;
package com.ui;
import com.mathquiz.model.Grade;
import com.mathquiz.service.QuizService;
import com.model.Grade;
import com.service.QuizService;
import javafx.geometry.Insets;
import javafx.scene.control.Button;
import javafx.scene.control.TextInputDialog;
@ -9,50 +9,4 @@ import javafx.scene.layout.VBox;
public class GradeSelectPanel extends VBox {
public GradeSelectPanel(MainWindow mainWindow) {
setPadding(new Insets(20));
setSpacing(20);
getChildren().addAll(
new Button("小学") {{
setOnAction(e -> startQuiz(mainWindow, Grade.PRIMARY));
}},
new Button("初中") {{
setOnAction(e -> startQuiz(mainWindow, Grade.JUNIOR));
}},
new Button("高中") {{
setOnAction(e -> startQuiz(mainWindow, Grade.SENIOR));
}},
new Button("修改密码") {{
setOnAction(e -> mainWindow.showPasswordModifyPanel());
}}
);
}
private void startQuiz(MainWindow mainWindow, Grade grade) {
TextInputDialog dialog = new TextInputDialog("20");
dialog.setTitle("题目数量");
dialog.setHeaderText("请输入题目数量10-30");
dialog.setContentText("数量:");
dialog.showAndWait().ifPresent(input -> {
try {
int count = Integer.parseInt(input);
if (count < 10 || count > 30) {
throw new NumberFormatException();
}
// 创建 QuizService未来可注入
QuizService quizService = new QuizService(grade, mainWindow.getFileIOService());
var questions = quizService.generateQuestions(
mainWindow.getCurrentUser().getEmail(), count
);
mainWindow.showQuizPanel(questions, quizService);
} catch (NumberFormatException e) {
new Alert(Alert.AlertType.ERROR, "请输入10-30之间的整数").showAndWait();
}
});
}
}

@ -0,0 +1,126 @@
// com/ui/InfGenPage.java
package com.ui;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.TextAlignment;
public class InfGenPage extends NavigablePanel {
private final TextField usernameField = new TextField();
private final TextField emailField = new TextField();
private final Label passwordLabel = new Label("******");
private final ChoiceBox<String> gradeChoice = new ChoiceBox<>();
private final Spinner<Integer> questionCountSpinner = new Spinner<>(10, 30, 10);
private final Button passwordModifyButton = new Button("修改密码");
private final Button generateButton = new Button("生成题目");
public InfGenPage(Runnable onBack, String currentUsername, String currentEmail) {
super(onBack);
initializeContent();
usernameField.setText(currentUsername);
emailField.setText(currentEmail);
}
@Override
protected void buildContent() {
// 外层容器VBox 居中,带内边距和圆角阴影
VBox form = new VBox(UIConstants.DEFAULT_SPACING * 1.5);
form.setAlignment(Pos.CENTER);
form.setPadding(UIConstants.DEFAULT_PADDING);
form.setStyle(UIConstants.FORM_STYLE);
form.setMaxWidth(500);
// 标题居中
Label titleLabel = new Label("中小学数学答题系统");
titleLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.TITLE_FONT_SIZE));
titleLabel.setTextAlignment(TextAlignment.CENTER);
titleLabel.setMaxWidth(Double.MAX_VALUE);
// ========== 创建表单项行(关键:标签左对齐 + 固定宽度)==========
HBox usernameRow = createFormRow("用户名:", usernameField, null);
HBox passwordRow = createFormRow("密码:", passwordLabel, passwordModifyButton);
HBox emailRow = createFormRow("邮箱:", emailField, null);
HBox gradeRow = createFormRow("学段选择:", gradeChoice, null);
HBox countRow = createFormRow("题目数量:", questionCountSpinner, generateButton);
// ========== 配置控件样式 ==========
// 用户名输入框
usernameField.setStyle(UIConstants.INPUT_STYLE);
usernameField.setPrefWidth(200);
usernameField.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.INPUT_FONT_SIZE));
// 密码标签
passwordLabel.setStyle("-fx-font-size: " + UIConstants.INPUT_FONT_SIZE + "px;");
// 邮箱输入框
emailField.setStyle(UIConstants.INPUT_STYLE);
emailField.setPrefWidth(200);
emailField.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.INPUT_FONT_SIZE));
// 学段选择框
gradeChoice.getItems().addAll("小学", "初中", "高中");
gradeChoice.setValue("小学");
gradeChoice.setStyle(UIConstants.INPUT_STYLE);
gradeChoice.setPrefWidth(200);
// 题目数量Spinner
questionCountSpinner.setEditable(true);
questionCountSpinner.setPrefWidth(200);
questionCountSpinner.getEditor().setStyle(UIConstants.INPUT_STYLE);
questionCountSpinner.getEditor().setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.INPUT_FONT_SIZE));
// 按钮样式统一
passwordModifyButton.setStyle(UIConstants.BUTTON_STYLE);
generateButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
generateButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
generateButton.setStyle(UIConstants.BUTTON_STYLE);
// ========== 添加到表单 ==========
form.getChildren().addAll(
titleLabel,
usernameRow,
emailRow,
passwordRow,
gradeRow,
countRow
);
this.setCenter(form);
}
/**
*
*/
private HBox createFormRow(String labelText, Control content, Button rightButton) {
HBox row = new HBox(15);
row.setAlignment(Pos.CENTER_LEFT); // ← 关键:让整行左对齐
row.setMaxWidth(Double.MAX_VALUE);
// 标签:左对齐,固定宽度,字体统一
Label label = new Label(labelText);
label.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.NORMAL, UIConstants.LABEL_ITEM_TITLE_SIZE));
label.setTextAlignment(TextAlignment.LEFT); // ← 关键:标签文字左对齐
label.setPrefWidth(120); // 固定宽度,确保所有标签对齐
row.getChildren().addAll(label, content);
if (rightButton != null) {
row.getChildren().add(rightButton);
}
return row;
}
// Getters...
public TextField getUsernameField() { return usernameField; }
public TextField getEmailField() { return emailField; }
public ChoiceBox<String> getGradeChoice() { return gradeChoice; }
public Spinner<Integer> getQuestionCountSpinner() { return questionCountSpinner; }
public Button getGenerateButton() { return generateButton; }
public Button getPasswordModifyButton() { return passwordModifyButton; }
}

@ -0,0 +1,61 @@
// com/ui/LoginPage.java
package com.ui;
import com.model.User;
import com.service.UserService;
import javafx.concurrent.Task;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
public class LoginPage extends NavigablePanel {
private final TextField usernameOrEmailField = new TextField();
private final PasswordField passwordField = new PasswordField();
private final Button loginButton = new Button("登录");
private final Hyperlink registerLink = new Hyperlink("注册");
public LoginPage(Runnable onBack) {
super(onBack);
initializeContent(); // 字段初始化
}
@Override
protected void buildContent() {
VBox form = new VBox(UIConstants.DEFAULT_SPACING);
form.setAlignment(Pos.CENTER);
form.setPadding(UIConstants.DEFAULT_PADDING);
form.setStyle(UIConstants.FORM_STYLE);
Label titleLabel = new Label("中小学数学答题系统");
titleLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.TITLE_FONT_SIZE));
usernameOrEmailField.setPromptText("邮箱/用户名");
usernameOrEmailField.setStyle(UIConstants.INPUT_STYLE);
passwordField.setPromptText("密码6-10位含大小写字母和数字");
passwordField.setStyle(UIConstants.INPUT_STYLE);
loginButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
loginButton.setStyle(UIConstants.BUTTON_STYLE);
loginButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
loginButton.setOnMouseEntered(e -> loginButton.setStyle(UIConstants.BUTTON_STYLE + UIConstants.BUTTON_HOVER_STYLE));
loginButton.setOnMouseExited(e -> loginButton.setStyle(UIConstants.BUTTON_STYLE));
registerLink.setStyle("-fx-text-fill: " + UIConstants.COLOR_ACCENT + ";");
HBox linkBox = new HBox(registerLink);
linkBox.setAlignment(Pos.CENTER);
form.getChildren().addAll(titleLabel, usernameOrEmailField, passwordField, loginButton, linkBox);
this.setCenter(form);
}
public TextField getUsernameOrEmailField() { return usernameOrEmailField; }
public PasswordField getPasswordField() { return passwordField; }
public Button getLoginButton() { return loginButton; }
public Hyperlink getRegisterLink() { return registerLink; }
}

@ -1,44 +0,0 @@
package com.mathquiz.ui;
import com.mathquiz.model.User;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
public class LoginPanel extends VBox {
private final TextField emailField = new TextField();
private final PasswordField pwdField = new PasswordField();
public LoginPanel(MainWindow mainWindow) {
setPadding(new Insets(20));
setSpacing(10);
getChildren().addAll(
new Label("登录"),
new Label("邮箱:"),
emailField,
new Label("密码:"),
pwdField,
new Button("登录") {{
setOnAction(e -> loginAction(mainWindow));
}},
new Hyperlink("没有账号?去注册") {{
setOnAction(e -> mainWindow.showRegisterPanel());
}}
);
}
private void loginAction(MainWindow mainWindow) {
String email = emailField.getText().trim();
String password = pwdField.getText();
User user = mainWindow.getUserService().login(email, password);
if (user != null) {
mainWindow.setCurrentUser(user);
mainWindow.showGradeSelectPanel();
} else {
new Alert(Alert.AlertType.ERROR, "邮箱或密码错误").showAndWait();
}
}
}

@ -1,77 +1,286 @@
package com.mathquiz.ui;
// com/ui/MainWindow.java
package com.ui;
import com.mathquiz.model.User;
import com.mathquiz.service.*;
import com.model.ChoiceQuestion;
import com.model.User;
import com.service.QuizService;
import com.service.UserService;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Toggle;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import java.io.IOException;
import java.util.List;
/**
*
* UI
*/
public class MainWindow extends BorderPane {
// 服务层引用(便于后期替换实现)
private final UserService userService;
private final FileIOService fileIOService;
private final UserService userService = new UserService();
private final QuizService quizService = new QuizService();
private User currentUser;
private final Stage primaryStage;
private Panel currentPanel;
public MainWindow(Stage primaryStage) {
// 初始化服务(未来可替换为 DI 容器)
this.fileIOService = new FileIOService();
this.userService = new UserService(fileIOService);
this.primaryStage = primaryStage;
showStartPage();
}
// 默认显示登录页
showLoginPanel();
private void showStartPage() {
StartPage startPage = new StartPage(() -> navigateTo(Panel.LOGIN));
this.setCenter(startPage);
currentPanel = Panel.START;
}
// ---------------- 公共方法:面板切换 ----------------
private void navigateTo(Panel target) {
switch (target) {
case START -> showStartPage();
case LOGIN -> initLoginPage();
case REGISTER -> initRegisterPage();
case INF_GEN -> initInfGenPage();
case PASSWORDMODIFY -> initPasswordModifyPage();
case QUIZ -> initQuizPage();
case RESULT -> initResultPage();
}
currentPanel = target;
}
public void showPanel(javafx.scene.layout.Pane panel) {
this.setCenter(panel);
// 封装登录页面初始化逻辑
private void initLoginPage() {
LoginPage loginPage = new LoginPage(() -> navigateTo(Panel.START));
// 注册链接跳转
loginPage.getRegisterLink().setOnAction(e -> navigateTo(Panel.REGISTER));
// 登录按钮点击事件(绑定封装的登录处理方法)
loginPage.getLoginButton().setOnAction(e -> handleLoginAction(loginPage));
this.setCenter(loginPage);
}
public void showLoginPanel() {
showPanel(new LoginPanel(this));
// 封装登录核心逻辑从UI获取数据→调用服务层→处理结果
private void handleLoginAction(LoginPage loginPage) {
String username = loginPage.getUsernameOrEmailField().getText().trim();
String password = loginPage.getPasswordField().getText().trim();
if (username.isEmpty()) {
NavigablePanel.showErrorAlert("输入错误", "请输入用户名");
return ;
}
if (password.isEmpty()) {
NavigablePanel.showErrorAlert("输入错误", "请输入密码");
return ;
}
try {
this.currentUser = userService.login(username, password);; // 保存当前用户
navigateTo(Panel.INF_GEN); // 登录成功跳转
} catch (IllegalArgumentException ex) {
NavigablePanel.showErrorAlert("登录失败", ex.getMessage());
} catch (IOException ex) {
NavigablePanel.showErrorAlert("系统错误", "登录过程中发生错误:" + ex.getMessage());
}
}
public void showRegisterPanel() {
showPanel(new RegisterPanel(this));
// 其他页面的初始化方法(保持原有逻辑,统一格式)
private void initRegisterPage() {
RegisterPage registerPage = new RegisterPage(() -> navigateTo(Panel.LOGIN));
registerPage.getSendCodeButton().setOnAction(e -> handleSendCodeAction(registerPage));
registerPage.getRegisterButton().setOnAction(e -> handleRegisterAction(registerPage));
this.setCenter(registerPage);
}
public void showPasswordModifyPanel() {
if (currentUser != null) {
showPanel(new PasswordModifyPanel(this, currentUser.getEmail()));
private void handleSendCodeAction(RegisterPage registerPage) {
String email = registerPage.getEmailField().getText().trim();
try {
userService.generateRegistrationCode(email);
NavigablePanel.showErrorAlert("成功", "注册码已生成10分钟内有效");
} catch (IllegalArgumentException ex) {
NavigablePanel.showErrorAlert("获取注册码失败", ex.getMessage());
return;
} catch (IOException ex) {
NavigablePanel.showErrorAlert("系统错误", ex.getMessage());
return;
}
}
private void handleRegisterAction(RegisterPage registerPage) {
String email = registerPage.getEmailField().getText().trim();
String code = registerPage.getCodeField().getText().trim();
String password = registerPage.getPasswordField().getText().trim();
String confirmPassword = registerPage.getConfirmPasswordField().getText().trim();
public void showGradeSelectPanel() {
showPanel(new GradeSelectPanel(this));
if (password.isEmpty()) {
NavigablePanel.showErrorAlert("密码错误","密码不能为空");
return ;
}
if (confirmPassword.isEmpty()) {
NavigablePanel.showErrorAlert("密码错误", "请重复密码");
return ;
}
if (!password.equals(confirmPassword)) {
NavigablePanel.showErrorAlert("密码错误", "两次密码不同");
return ;
}
try {
this.currentUser = userService.register(password, email, code);
navigateTo(Panel.INF_GEN);
} catch (IllegalArgumentException ex) {
NavigablePanel.showErrorAlert("注册失败", ex.getMessage());
} catch (IOException ex) {
NavigablePanel.showErrorAlert("系统错误", ex.getMessage());
}
}
private void initInfGenPage() {
InfGenPage infGenPage = new InfGenPage(() -> navigateTo(Panel.LOGIN), userService.getCurrentUser().getUsername(),
userService.getCurrentUser().getEmail());
infGenPage.getGenerateButton().setOnAction(e -> {
handleUsernameModifyAction(infGenPage);
handleEmailModifyAction(infGenPage);
int count = infGenPage.getQuestionCountSpinner().getValue();
try {
userService.updateGrade(userService.getCurrentUser(), infGenPage.getGradeChoice().getValue());
} catch (IllegalArgumentException ex) {
NavigablePanel.showErrorAlert("学段错误", ex.getMessage());
return;
} catch (IOException ex) {
NavigablePanel.showErrorAlert("系统错误", ex.getMessage());
}
if (count < 10 || count > 30) {
NavigablePanel.showErrorAlert("输入错误", "题数必须为10-30");
return;
}
quizService.setAnswerNumber(count);
navigateTo(Panel.QUIZ);
});
infGenPage.getPasswordModifyButton().setOnAction(e -> {
handleUsernameModifyAction(infGenPage);
handleEmailModifyAction(infGenPage);
navigateTo(Panel.PASSWORDMODIFY);
});
this.setCenter(infGenPage);
this.setStyle("-fx-background-color: " + UIConstants.COLOR_BACKGROUND + ";");
}
private void handleUsernameModifyAction(InfGenPage infGenPage) {
try {
userService.updateUsername(userService.getCurrentUser(), infGenPage.getUsernameField().getText().trim());
} catch (IllegalArgumentException ex) {
NavigablePanel.showErrorAlert("用户名错误", ex.getMessage());
} catch (IOException ex) {
NavigablePanel.showErrorAlert("系统错误", ex.getMessage());
}
}
private void handleEmailModifyAction(InfGenPage infGenPage) {
try {
userService.updateEmail(userService.getCurrentUser(), infGenPage.getEmailField().getText().trim());
} catch (IllegalArgumentException ex) {
NavigablePanel.showErrorAlert("邮箱错误", ex.getMessage());
} catch (IOException ex) {
NavigablePanel.showErrorAlert("系统错误", ex.getMessage());
}
}
public void showQuizPanel(java.util.List<com.mathquiz.model.ChoiceQuestion> questions, QuizService quizService) {
showPanel(new QuizPanel(this, questions, quizService));
private void initPasswordModifyPage() {
PasswordModifyPage pwdPage = new PasswordModifyPage(() -> navigateTo(Panel.INF_GEN));
pwdPage.getModifyButton().setOnAction(e -> handlePasswordModify(pwdPage));
this.setCenter(pwdPage);
}
public void showResultPanel(int score, Runnable onContinue) {
showPanel(new ResultPanel(this, score, onContinue));
private void handlePasswordModify(PasswordModifyPage pwdPage) {
String oldPassword = pwdPage.getOldPasswordField().getText().trim();
String newPassword = pwdPage.getNewPasswordField().getText().trim();
String confirmPassword = pwdPage.getConfirmNewPasswordField().getText().trim();
try {
userService.changePassword(userService.getCurrentUser(),oldPassword, newPassword, confirmPassword);
navigateTo(Panel.INF_GEN);
} catch (IllegalArgumentException ex) {
NavigablePanel.showErrorAlert("修改失败", ex.getMessage());
return;
} catch (IOException ex) {
NavigablePanel.showErrorAlert("系统错误 ", ex.getMessage());
return;
}
}
// ---------------- Getter ----------------
private void initQuizPage() {
try {
quizService.startNewQuiz(currentUser, quizService.getAnswerNumber());
} catch (IllegalArgumentException ex) {
NavigablePanel.showErrorAlert("生成题目错误", ex.getMessage());
} catch (IOException ex) {
NavigablePanel.showErrorAlert("系统错误", ex.getMessage());
navigateTo(Panel.INF_GEN);
return;
}
QuizPage quizPage = new QuizPage(() -> navigateTo(Panel.INF_GEN), quizService);
quizPage.setTotalQuestions(quizService.getTotalQuestions());
quizPage.goToQuestion(0);
quizPage.getPrevButton().setOnAction(e -> {
if (quizService.previousQuestion()) {
quizPage.goToQuestion(quizService.getCurrentQuestionIndex());
}
});
quizPage.getNextButton().setOnAction(e -> {
// 获取用户当前选择(可能为 null
Toggle selected = quizPage.getOptionGroup().getSelectedToggle();
// 如果有选择,则提交答案
if (selected != null) {
int selectedIndex = -1;
for (int i = 0; i < 4; i++) {
if (quizPage.getOptions()[i] == selected) {
selectedIndex = i;
break;
}
}
quizService.submitCurrentAnswer(selectedIndex);
}
// 无论是否选择,都尝试跳转到下一题
if (quizService.nextQuestion()) {
quizPage.goToQuestion(quizService.getCurrentQuestionIndex());
} else {
// 已是最后一题,显示“交卷”按钮
quizPage.getSubmitButton().setVisible(true);
quizPage.getNextButton().setVisible(false);
}
});
quizPage.getSubmitButton().setOnAction(e -> {
Toggle selected = quizPage.getOptionGroup().getSelectedToggle();
if (selected != null) {
int selectedIndex = -1;
for (int i = 0; i < 4; i++) {
if (quizPage.getOptions()[i] == selected) {
selectedIndex = i;
break;
}
}
quizService.submitCurrentAnswer(selectedIndex);
}
// 直接交卷,不强制所有题都作答
navigateTo(Panel.RESULT);
});
public UserService getUserService() {
return userService;
this.setCenter(quizPage);
}
public FileIOService getFileIOService() {
return fileIOService;
private void initResultPage() {
ResultPage resultPage = new ResultPage(() -> navigateTo(Panel.INF_GEN), quizService);
resultPage.getExitButton().setOnAction(e -> navigateTo(Panel.START));
resultPage.getContinueButton().setOnAction(e -> navigateTo(Panel.INF_GEN));
resultPage.updateResult();
this.setCenter(resultPage);
}
public User getCurrentUser() {
return currentUser;
// getter和启动方法保持不变
public Panel getCurrentPanel() {
return currentPanel;
}
public void setCurrentUser(User user) {
this.currentUser = user;
public static void start(Stage stage) {
MainWindow mainWindow = new MainWindow(stage);
Scene scene = new Scene(mainWindow, 800, 600);
stage.setScene(scene);
stage.setTitle("中小学数学答题系统");
stage.show();
}
}

@ -0,0 +1,52 @@
// com/ui/NavigablePanel.java
package com.ui;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.text.Font;
public abstract class NavigablePanel extends BorderPane {
public NavigablePanel(Runnable onBack) {
Button backButton = new Button("←");
backButton.setOnAction(e -> onBack.run());
backButton.setPrefSize(UIConstants.BACK_BUTTON_WIDTH, UIConstants.BACK_BUTTON_HEIGHT);
backButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
backButton.setStyle(
"-fx-background-radius: 50; " +
"-fx-background-color: " + UIConstants.COLOR_ACCENT + "; " +
"-fx-text-fill: white; " +
"-fx-font-weight: bold;"
);
HBox topBar = new HBox(10);
topBar.setPadding(UIConstants.TOP_BAR_PADDING);
topBar.setAlignment(Pos.CENTER_LEFT);
topBar.getChildren().add(backButton);
this.setTop(topBar);
}
protected static void showErrorAlert(String title, String message) {
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
});
}
// 子类构造函数中调用
protected final void initializeContent() {
buildContent();
}
protected abstract void buildContent();
}

@ -0,0 +1,11 @@
package com.ui;
public enum Panel {
START, // 开始页面
LOGIN, // 登录页面
REGISTER, // 注册页面
INF_GEN, // 个人信息+生成题目页面
PASSWORDMODIFY, // 修改密码页面
QUIZ, // 答题页面
RESULT // 得分页面
}

@ -0,0 +1,52 @@
// com/ui/PasswordModifyPage.java
package com.ui;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
public class PasswordModifyPage extends NavigablePanel {
private final PasswordField oldPasswordField = new PasswordField();
private final PasswordField newPasswordField = new PasswordField();
private final PasswordField confirmNewPasswordField = new PasswordField();
private final Button modifyButton = new Button("修改");
public PasswordModifyPage(Runnable onBack) {
super(onBack);
initializeContent();
}
@Override
protected void buildContent() {
VBox form = new VBox(UIConstants.DEFAULT_SPACING);
form.setAlignment(Pos.CENTER);
form.setPadding(UIConstants.DEFAULT_PADDING);
form.setStyle(UIConstants.FORM_STYLE);
Label titleLabel = new Label("中小学数学答题系统");
titleLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.TITLE_FONT_SIZE));
oldPasswordField.setPromptText("旧密码");
oldPasswordField.setStyle(UIConstants.INPUT_STYLE);
newPasswordField.setPromptText("新密码6-10位");
newPasswordField.setStyle(UIConstants.INPUT_STYLE);
confirmNewPasswordField.setPromptText("确认新密码");
confirmNewPasswordField.setStyle(UIConstants.INPUT_STYLE);
modifyButton.setStyle(UIConstants.BUTTON_STYLE);
modifyButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
modifyButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
form.getChildren().addAll(
titleLabel, oldPasswordField, newPasswordField, confirmNewPasswordField, modifyButton
);
this.setCenter(form);
}
public PasswordField getOldPasswordField() { return oldPasswordField; }
public PasswordField getNewPasswordField() { return newPasswordField; }
public PasswordField getConfirmNewPasswordField() { return confirmNewPasswordField; }
public Button getModifyButton() { return modifyButton; }
}

@ -1,46 +0,0 @@
package com.mathquiz.ui;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
public class PasswordModifyPanel extends VBox {
private final String email;
private final PasswordField oldPwdField = new PasswordField();
private final PasswordField newPwd1Field = new PasswordField();
private final PasswordField newPwd2Field = new PasswordField();
public PasswordModifyPanel(MainWindow mainWindow, String email) {
this.email = email;
setPadding(new Insets(20));
setSpacing(10);
getChildren().addAll(
new Label("修改密码"),
new Label("原密码:"),
oldPwdField,
new Label("新密码6-10位含大小写+数字):"),
newPwd1Field,
new Label("确认新密码:"),
newPwd2Field,
new Button("确认修改") {{
setOnAction(e -> changePassword(mainWindow));
}},
new Button("返回") {{
setOnAction(e -> mainWindow.showGradeSelectPanel());
}}
);
}
private void changePassword(MainWindow mainWindow) {
boolean success = mainWindow.getUserService()
.changePassword(email, oldPwdField.getText(), newPwd1Field.getText(), newPwd2Field.getText());
if (success) {
new Alert(Alert.AlertType.INFORMATION, "密码修改成功!").showAndWait();
mainWindow.showGradeSelectPanel();
} else {
new Alert(Alert.AlertType.ERROR, "原密码错误或新密码不符合要求").showAndWait();
}
}
}

@ -0,0 +1,315 @@
// com/ui/QuizPage.java
package com.ui;
import com.model.ChoiceQuestion;
import com.service.QuizService;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import java.util.List;
public class QuizPage extends NavigablePanel {
private final QuizService quizService;
private final Label titleLabel = new Label("中小学数学答题系统");
private final Label progressLabel = new Label("完成 0/10");
private final Label questionLabel = new Label("题目加载中...");
private final ToggleGroup optionGroup = new ToggleGroup();
private final RadioButton[] options = new RadioButton[4];
private final Button prevButton = new Button("上一题");
private final Button nextButton = new Button("下一题");
private final Button submitButton = new Button("交卷");
// 题目导航矩阵容器
private final GridPane questionNavGrid = new GridPane();
private int totalQuestions = 10; // 默认10题由外部设置
private int currentQuestionIndex = 0; // 当前题号0-based
public QuizPage(Runnable onBack, QuizService quizService) {
super(onBack);
this.quizService = quizService;
initializeContent();
}
@Override
protected void buildContent() {
// 设置整体布局BorderPane
this.setPadding(new Insets(20));
this.setStyle("-fx-background-color: " + UIConstants.COLOR_BACKGROUND + ";");
// 顶部标题栏
HBox topBar = createTopBar();
this.setTop(topBar);
// 主体内容:左右分栏
HBox mainContent = new HBox(20);
mainContent.setAlignment(Pos.CENTER);
mainContent.setPadding(new Insets(10));
// 左侧:题目内容区
VBox leftPanel = createLeftPanel();
leftPanel.setMaxWidth(600);
// 右侧:题目导航矩阵
VBox rightPanel = createRightPanel();
rightPanel.setMaxWidth(300);
rightPanel.setMinWidth(280);
HBox.setHgrow(leftPanel, Priority.ALWAYS);
HBox.setHgrow(rightPanel, Priority.NEVER);
mainContent.getChildren().addAll(leftPanel, rightPanel);
this.setCenter(mainContent);
// 底部按钮
VBox bottomBarContainer = new VBox(10);
bottomBarContainer.setAlignment(Pos.CENTER);
bottomBarContainer.setPadding(new Insets(10));
nextButton.setStyle(UIConstants.BUTTON_STYLE);
submitButton.setStyle(UIConstants.BUTTON_STYLE + "-fx-background-color: " + UIConstants.COLOR_ERROR + ";");
prevButton.setStyle(UIConstants.BUTTON_STYLE);
nextButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
submitButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
prevButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
HBox buttonRow = new HBox(20);
buttonRow.setAlignment(Pos.CENTER);
buttonRow.getChildren().addAll(prevButton, nextButton, submitButton);
bottomBarContainer.getChildren().add(buttonRow);
this.setBottom(bottomBarContainer);
// 初始化按钮状态
updateButtonVisibility();
}
/**
*
*/
private HBox createTopBar() {
HBox topBar = new HBox(20);
topBar.setAlignment(Pos.CENTER_LEFT);
topBar.setPadding(new Insets(10));
topBar.setStyle("-fx-background-color: white; -fx-border-width: 0 0 1 0; -fx-border-color: #bdc3c7;");
titleLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.TITLE_FONT_SIZE));
progressLabel.setStyle("-fx-font-size: " + UIConstants.HINT_FONT_SIZE + "px; -fx-text-fill: " + UIConstants.COLOR_HINT + ";");
topBar.getChildren().addAll(titleLabel, progressLabel);
return topBar;
}
/**
*
*/
private VBox createLeftPanel() {
// 直接使用带样式的 VBox 作为内容容器
VBox content = new VBox(UIConstants.DEFAULT_SPACING);
content.setPadding(new Insets(20));
content.setStyle(UIConstants.FORM_STYLE);
content.setPrefWidth(550);
content.setMinWidth(550);
content.setMaxWidth(550);
content.setPrefHeight(400);
content.setMinHeight(400);
content.setMaxHeight(400);
questionLabel.setWrapText(true);
questionLabel.setPrefWidth(500);
questionLabel.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.QUIZ_TITLE_FONT_SIZE));
questionLabel.setStyle("-fx-font-size: " + UIConstants.QUIZ_TITLE_FONT_SIZE + "px; -fx-text-fill: " + UIConstants.COLOR_PRIMARY + ";");
VBox optionsBox = new VBox(10);
optionsBox.setAlignment(Pos.CENTER_LEFT);
for (int i = 0; i < 4; i++) {
options[i] = new RadioButton("选项 " + (char)('A' + i));
options[i].setToggleGroup(optionGroup);
options[i].setStyle("-fx-font-size: " + UIConstants.LABEL_FONT_SIZE + "px;");
optionsBox.getChildren().add(options[i]);
}
content.getChildren().addAll(questionLabel, optionsBox);
return content;
}
/**
*
*/
private VBox createRightPanel() {
VBox rightPanel = new VBox(10);
rightPanel.setAlignment(Pos.TOP_CENTER);
rightPanel.setPadding(new Insets(20));
rightPanel.setStyle(UIConstants.FORM_STYLE);
Label navTitle = new Label("题目导航");
navTitle.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.SUBTITLE_FONT_SIZE));
navTitle.setAlignment(Pos.CENTER);
// 初始化题目导航矩阵
initQuestionNavGrid();
rightPanel.getChildren().addAll(navTitle, questionNavGrid);
return rightPanel;
}
/**
*
*/
private void initQuestionNavGrid() {
questionNavGrid.getChildren().clear();
questionNavGrid.setHgap(5);
questionNavGrid.setVgap(5);
questionNavGrid.setAlignment(Pos.CENTER);
int cols = 5;
int rows = (totalQuestions + cols - 1) / cols;
for (int i = 0; i < totalQuestions; i++) {
int row = i / cols;
int col = i % cols;
String text = String.valueOf(i + 1);
Button btn = new Button(text);
btn.setPrefSize(50, 40);
btn.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.LABEL_FONT_SIZE));
btn.setStyle(getButtonStyleForStatus(i));
final int index = i;
btn.setOnAction(e -> goToQuestion(index));
questionNavGrid.add(btn, col, row);
}
}
/**
*
* @param index 0-based
* @return CSS
*/
private String getButtonStyleForStatus(int index) {
if (index == currentQuestionIndex) {
// 当前题
return "-fx-background-color: " + UIConstants.COLOR_ACCENT + "; -fx-text-fill: white; -fx-font-weight: bold;";
} else if (quizService.isAnswered(index)) {
// 已作答
return "-fx-background-color: #2ecc71; -fx-text-fill: white;";
} else {
// 未作答
return "-fx-background-color: #ecf0f1; -fx-text-fill: #2c3e50;";
}
}
/**
*
*/
private void updateButtonVisibility() {
if (currentQuestionIndex == totalQuestions - 1) {
nextButton.setVisible(false);
submitButton.setVisible(true);
} else {
nextButton.setVisible(true);
submitButton.setVisible(false);
}
}
/**
*
* @param index 0-based
*/
public void goToQuestion(int index) {
currentQuestionIndex = index;
quizService.goToQuestion(index);
updateProgressLabel();
updateQuestionNavButtons();
updateButtonVisibility();
loadQuestion(index);
}
/**
*
*/
private void updateProgressLabel() {
int answeredCount = quizService.getAnsweredCount();
progressLabel.setText("完成 " + answeredCount + "/" + totalQuestions + " 题");
}
/**
*
*/
private void updateQuestionNavButtons() {
for (Node node : questionNavGrid.getChildren()) {
if (node instanceof Button) {
Button btn = (Button) node;
int index = Integer.parseInt(btn.getText()) - 1; // 转换为0-based
btn.setStyle(getButtonStyleForStatus(index));
}
}
}
/**
* QuizService
* @param index
*/
private void loadQuestion(int index) {
System.out.println("🔄 加载题目 " + index + ", options[0]=" + options[0]);
if (options[0] == null) {
System.err.println("⚠️ RadioButton 未初始化,跳过题目加载");
return; // 防止 NPE
}
ChoiceQuestion question = quizService.getQuestion(index);
if (question == null) return;
// 显示题目
questionLabel.setText("第 " + (index + 1) + " 题:\n" + question.getQuestionText());
List<?> optionsList = question.getOptions();
for (int i = 0; i < 4; i++) {
Object option = optionsList.get(i);
String optionText = option != null ? option.toString() : "未知";
options[i].setText((char)('A' + i) + ". " + optionText);
}
Integer userAnswer = quizService.getUserAnswer(index);
if (userAnswer != null && userAnswer >= 0 && userAnswer < 4) {
optionGroup.selectToggle(options[userAnswer]);
} else {
optionGroup.selectToggle(null);
}
// ✅ 强制刷新 UI可选通常不需要
// Platform.runLater(() -> {
// this.requestLayout(); // 触发重新布局
// });
}
// ========== Getter 方法 ==========
public Label getProgressLabel() { return progressLabel; }
public Label getQuestionLabel() { return questionLabel; }
public RadioButton[] getOptions() { return options; }
public ToggleGroup getOptionGroup() { return optionGroup; }
public Button getNextButton() { return nextButton; }
public Button getPrevButton() { return prevButton; }
public Button getSubmitButton() { return submitButton; }
// ========== 设置器方法 ==========
public void setTotalQuestions(int total) {
this.totalQuestions = total;
initQuestionNavGrid(); // 重新初始化导航矩阵
updateProgressLabel();
}
public void setCurrentQuestionIndex(int index) {
this.currentQuestionIndex = index;
updateQuestionNavButtons();
updateButtonVisibility();
loadQuestion(index);
}
}

@ -1,67 +0,0 @@
package com.mathquiz.ui;
import com.mathquiz.model.ChoiceQuestion;
import com.mathquiz.service.QuizService;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import java.util.ArrayList;
import java.util.List;
public class QuizPanel extends VBox {
private final List<ChoiceQuestion> questions;
private final List<String> userAnswers = new ArrayList<>();
private final QuizService quizService;
private final MainWindow mainWindow;
private int currentIndex = 0;
public QuizPanel(MainWindow mainWindow, List<ChoiceQuestion> questions, QuizService quizService) {
this.mainWindow = mainWindow;
this.questions = questions;
this.quizService = quizService;
setPadding(new Insets(20));
showQuestion(currentIndex);
}
private void showQuestion(int index) {
getChildren().clear();
ChoiceQuestion q = questions.get(index);
getChildren().add(new Label("第 " + (index + 1) + " 题 / " + questions.size()));
getChildren().add(new Label(q.getQuestionContent()));
ToggleGroup group = new ToggleGroup();
for (int i = 0; i < 4; i++) {
RadioButton rb = new RadioButton(
(char)('A' + i) + ". " + q.getOptions().get(i)
);
rb.setToggleGroup(group);
getChildren().add(rb);
}
Button submitBtn = new Button("提交");
submitBtn.setOnAction(e -> {
RadioButton selected = (RadioButton) group.getSelectedToggle();
if (selected != null) {
String answer = selected.getText().substring(0, 1); // "A"
userAnswers.add(answer);
if (index + 1 < questions.size()) {
showQuestion(index + 1);
} else {
int score = quizService.calculateScore(questions, userAnswers);
Runnable onContinue = () -> {
quizService.savePaper(mainWindow.getCurrentUser().getEmail(), questions);
};
mainWindow.showResultPanel(score, onContinue);
}
} else {
new Alert(Alert.AlertType.WARNING, "请选择一个选项").showAndWait();
}
});
getChildren().add(submitBtn);
}
}

@ -0,0 +1,63 @@
// com/ui/RegisterPage.java
package com.ui;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
public class RegisterPage extends NavigablePanel {
private final TextField emailField = new TextField();
private final PasswordField passwordField = new PasswordField();
private final PasswordField confirmPasswordField = new PasswordField();
private final TextField codeField = new TextField();
private final Button sendCodeButton = new Button("获取注册码");
private final Button registerButton = new Button("注册");
public RegisterPage(Runnable onBack) {
super(onBack);
initializeContent();
}
@Override
protected void buildContent() {
VBox form = new VBox(UIConstants.DEFAULT_SPACING);
form.setAlignment(Pos.CENTER);
form.setPadding(UIConstants.DEFAULT_PADDING);
form.setStyle(UIConstants.FORM_STYLE);
Label titleLabel = new Label("中小学数学答题系统");
titleLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.TITLE_FONT_SIZE));
emailField.setPromptText("邮箱");
emailField.setStyle(UIConstants.INPUT_STYLE);
codeField.setPromptText("注册码");
codeField.setStyle(UIConstants.INPUT_STYLE);
passwordField.setPromptText("密码6-10位");
passwordField.setStyle(UIConstants.INPUT_STYLE);
confirmPasswordField.setPromptText("确认密码");
confirmPasswordField.setStyle(UIConstants.INPUT_STYLE);
sendCodeButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
sendCodeButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
sendCodeButton.setStyle(UIConstants.BUTTON_STYLE);
registerButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
registerButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
registerButton.setStyle(UIConstants.BUTTON_STYLE);
form.getChildren().addAll(
titleLabel, emailField, sendCodeButton, codeField,
passwordField, confirmPasswordField, registerButton
);
this.setCenter(form);
}
// getters for controller
public TextField getEmailField() { return emailField; }
public TextField getCodeField() { return codeField; }
public PasswordField getPasswordField() { return passwordField; }
public PasswordField getConfirmPasswordField() { return confirmPasswordField; }
public Button getSendCodeButton() { return sendCodeButton; }
public Button getRegisterButton() { return registerButton; }
}

@ -1,80 +0,0 @@
package com.mathquiz.ui;
import com.mathquiz.util.EmailUtil;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
/**
*
*/
public class RegisterPanel extends VBox {
private final TextField emailField = new TextField();
private final TextField codeField = new TextField();
private final PasswordField pwd1Field = new PasswordField();
private final PasswordField pwd2Field = new PasswordField();
public RegisterPanel(MainWindow mainWindow) {
setPadding(new Insets(20));
setSpacing(10);
getChildren().addAll(
new Label("注册"),
new Label("邮箱:"),
emailField,
new Button("发送注册码") {{
setOnAction(e -> sendCodeAction(mainWindow));
}},
new Label("注册码:"),
codeField,
new Label("密码6-10位含大小写+数字):"),
pwd1Field,
new Label("确认密码:"),
pwd2Field,
new Button("完成注册") {{
setOnAction(e -> registerAction(mainWindow));
}},
new Hyperlink("已有账号?去登录") {{
setOnAction(e -> mainWindow.showLoginPanel());
}}
);
}
private void sendCodeAction(MainWindow mainWindow) {
String email = emailField.getText().trim();
if (email.isEmpty() || !EmailUtil.isValidEmail(email)) {
showAlert("请输入有效的邮箱地址");
return;
}
// 调用服务层
boolean sent = mainWindow.getUserService().sendRegistrationCode(email);
if (sent) {
showAlert("注册码已发送(模拟)");
}
}
private void registerAction(MainWindow mainWindow) {
String email = emailField.getText().trim();
String code = codeField.getText().trim();
String pwd1 = pwd1Field.getText();
String pwd2 = pwd2Field.getText();
if (!mainWindow.getUserService().verifyCode(email, code)) {
showAlert("注册码错误");
return;
}
boolean success = mainWindow.getUserService().setPassword(email, pwd1, pwd2);
if (success) {
showAlert("注册成功!");
mainWindow.showGradeSelectPanel();
} else {
showAlert("密码不符合要求6-10位含大小写字母和数字");
}
}
private void showAlert(String message) {
new Alert(Alert.AlertType.INFORMATION, message).showAndWait();
}
}

@ -0,0 +1,63 @@
// com/ui/ResultPage.java
package com.ui;
import com.model.QuizResult;
import com.service.QuizService;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
public class ResultPage extends NavigablePanel {
private final QuizService quizService;
private final Label resultLabel = new Label("您答对了 8/10 题得分80%");
private final Label gradeLabel = new Label("评级:优秀");
private final Button continueButton = new Button("继续答题");
private final Button exitButton = new Button("退出");
public ResultPage(Runnable onBack, QuizService quizService) {
super(onBack);
initializeContent();
this.quizService = quizService;
}
@Override
protected void buildContent() {
VBox form = new VBox(UIConstants.DEFAULT_SPACING);
form.setAlignment(Pos.CENTER);
form.setPadding(UIConstants.DEFAULT_PADDING);
form.setStyle(UIConstants.FORM_STYLE);
Label titleLabel = new Label("中小学数学答题系统");
titleLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.TITLE_FONT_SIZE));
resultLabel.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.TITLE_FONT_SIZE - 4));
resultLabel.setStyle("-fx-font-size: " + UIConstants.SCORE_FONT_SIZE + "px; -fx-text-fill: " + UIConstants.COLOR_ACCENT + ";");
gradeLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.SUBTITLE_FONT_SIZE));
gradeLabel.setStyle("-fx-font-size: " + UIConstants.SUBTITLE_FONT_SIZE + "px; -fx-text-fill: " + UIConstants.COLOR_PRIMARY + ";");
continueButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
continueButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
continueButton.setStyle(UIConstants.BUTTON_STYLE);
exitButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
exitButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
exitButton.setStyle(UIConstants.BUTTON_STYLE);
form.getChildren().addAll(titleLabel, resultLabel, gradeLabel, continueButton, exitButton);
this.setCenter(form);
}
public void updateResult() {
QuizResult result = quizService.calculateResult();
resultLabel.setText(result.toString());
gradeLabel.setText(quizService.getGrade(result));
}
public Label getResultLabel() { return resultLabel; }
public Label getGradeLabel() { return gradeLabel; }
public Button getContinueButton() { return continueButton; }
public Button getExitButton() { return exitButton; }
}

@ -1,32 +0,0 @@
package com.mathquiz.ui;
import javafx.geometry.Insets;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
public class ResultPanel extends VBox {
public ResultPanel(MainWindow mainWindow, int score, Runnable onContinue) {
setPadding(new Insets(20));
setSpacing(20);
getChildren().addAll(
new Label("答题结束!"),
new Label("您的得分: " + score + " 分"),
new Button("继续做题") {{
setOnAction(e -> {
onContinue.run(); // 保存试卷
mainWindow.showGradeSelectPanel();
});
}},
new Button("退出") {{
setOnAction(e -> {
mainWindow.setCurrentUser(null);
mainWindow.showLoginPanel();
});
}}
);
}
}

@ -0,0 +1,37 @@
// com/ui/StartPage.java
package com.ui;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
public class StartPage extends VBox {
private final Button startButton;
public StartPage(Runnable onStart) {
this.setAlignment(Pos.CENTER);
this.setSpacing(UIConstants.DEFAULT_SPACING);
this.setPadding(UIConstants.DEFAULT_PADDING);
Label titleLabel = new Label("中小学数学答题系统");
titleLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.TITLE_FONT_SIZE));
Label subtitleLabel = new Label("HNU@梁峻耀 吴佰轩");
subtitleLabel.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.SUBTITLE_FONT_SIZE));
subtitleLabel.setStyle("-fx-text-fill: gray;");
startButton = new Button("开始");
startButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
startButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
startButton.setOnAction(e -> onStart.run());
startButton.setStyle(UIConstants.BUTTON_STYLE);
startButton.setOnMouseEntered(e -> startButton.setStyle(UIConstants.BUTTON_STYLE + UIConstants.BUTTON_HOVER_STYLE));
startButton.setOnMouseExited(e -> startButton.setStyle(UIConstants.BUTTON_STYLE));
this.getChildren().addAll(titleLabel, subtitleLabel, startButton);
}
}

@ -0,0 +1,70 @@
// UIConstants.java
package com.ui;
import javafx.geometry.Insets;
import javafx.scene.paint.Color;
public final class UIConstants {
private UIConstants() {}
public static final double LABEL_ITEM_TITLE_SIZE = 16.0;
// 间距与边距
public static final double DEFAULT_SPACING = 15.0;
public static final Insets DEFAULT_PADDING = new Insets(40);
public static final Insets SMALL_PADDING = new Insets(20);
public static final Insets TOP_BAR_PADDING = new Insets(10);
// 字体
public static final String FONT_FAMILY = "Microsoft YaHei";
public static final double TITLE_FONT_SIZE = 26.0;
public static final double SUBTITLE_FONT_SIZE = 16.0;
public static final double BUTTON_FONT_SIZE = 15.0;
public static final double LABEL_FONT_SIZE = 14.0;
public static final double INPUT_FONT_SIZE = 14.0;
public static final double HINT_FONT_SIZE = 12.0;
public static final double ERROR_FONT_SIZE = 12.0;
public static final double QUIZ_TITLE_FONT_SIZE = 20.0;
public static final double SCORE_FONT_SIZE = 32.0;
// 按钮尺寸
public static final double BUTTON_WIDTH = 140.0;
public static final double BUTTON_HEIGHT = 40.0;
public static final double BACK_BUTTON_WIDTH = 80.0;
public static final double BACK_BUTTON_HEIGHT = 30.0;
// 颜色
public static final String COLOR_PRIMARY = "#2c3e50";
public static final String COLOR_ACCENT = "#3498db";
public static final String COLOR_ERROR = "#e74c3c";
public static final String COLOR_HINT = "#7f8c8d";
public static final String COLOR_BACKGROUND = "#ecf0f1";
// 按钮样式
public static final String BUTTON_STYLE =
"-fx-background-color: " + COLOR_ACCENT + "; " +
"-fx-text-fill: white; " +
"-fx-background-radius: 8; " +
"-fx-font-size: " + BUTTON_FONT_SIZE + "px; " +
"-fx-font-family: '" + FONT_FAMILY + "'; " +
"-fx-cursor: hand;";
public static final String BUTTON_HOVER_STYLE =
"-fx-background-color: #2980b9;";
// 输入框样式
public static final String INPUT_STYLE =
"-fx-background-radius: 8; " +
"-fx-border-radius: 8; " +
"-fx-border-color: #bdc3c7; " +
"-fx-padding: 8; " +
"-fx-font-size: " + INPUT_FONT_SIZE + "px; " +
"-fx-font-family: '" + FONT_FAMILY + "';";
// 表单容器样式
public static final String FORM_STYLE =
"-fx-background-color: white; " +
"-fx-background-radius: 12; " +
"-fx-border-radius: 12; " +
"-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.1), 10, 0, 0, 5);";
}

@ -12,7 +12,7 @@ public class PasswordValidator {
// 密码长度限制
private static final int MIN_LENGTH = 6;
private static final int MAX_LENGTH = 20; // 改为20位更安全
private static final int MAX_LENGTH = 10;
// 用于生成随机注册码的字符集
private static final String UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
@ -20,8 +20,6 @@ public class PasswordValidator {
private static final String DIGITS = "0123456789";
private static final String ALL_CHARS = UPPERCASE + LOWERCASE + DIGITS;
// 使用 SecureRandom 替代 Math.random(),更安全
private static final SecureRandom random = new SecureRandom();
// ==================== 密码验证 ====================
@ -48,16 +46,20 @@ public class PasswordValidator {
return "密码不能包含空格!";
}
// 检查是否包含字母
boolean hasLetter = password.matches(".*[a-zA-Z].*");
// 检查是否包含数字
boolean hasDigit = password.matches(".*\\d.*");
// 检查是否包含小写字母
boolean hasLowerLetter = password.matches(".*[a-z].*");
if (!hasLowerLetter) {
return "必须包含小写字母!";
}
if (!hasLetter) {
return "密码必须包含字母!";
// 检查是否包含大写字母
boolean hasUpperLetter = password.matches(".*[A-Z].*");
if (!hasUpperLetter) {
return "必须包含大写字母!";
}
// 检查是否包含数字
boolean hasDigit = password.matches(".*\\d.*");
if (!hasDigit) {
return "密码必须包含数字!";
}
@ -75,39 +77,39 @@ public class PasswordValidator {
return validatePassword(password) == null;
}
/**
*
*
* @param password
* @return
*/
public static String getPasswordStrength(String password) {
if (password == null || password.length() < MIN_LENGTH) {
return "弱";
}
int score = 0;
// 长度加分
if (password.length() >= 8) score++;
if (password.length() >= 12) score++;
// 包含小写字母
if (password.matches(".*[a-z].*")) score++;
// 包含大写字母
if (password.matches(".*[A-Z].*")) score++;
// 包含数字
if (password.matches(".*\\d.*")) score++;
// 包含特殊字符
if (password.matches(".*[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?].*")) score++;
if (score <= 2) return "弱";
if (score <= 4) return "中";
return "强";
}
// /**
// * 检查密码强度等级
// *
// * @param password 密码
// * @return 强度等级:弱、中、强
// */
// public static String getPasswordStrength(String password) {
// if (password == null || password.length() < MIN_LENGTH) {
// return "弱";
// }
//
// int score = 0;
//
// // 长度加分
// if (password.length() >= 8) score++;
// if (password.length() >= 12) score++;
//
// // 包含小写字母
// if (password.matches(".*[a-z].*")) score++;
//
// // 包含大写字母
// if (password.matches(".*[A-Z].*")) score++;
//
// // 包含数字
// if (password.matches(".*\\d.*")) score++;
//
// // 包含特殊字符
// if (password.matches(".*[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?].*")) score++;
//
// if (score <= 2) return "弱";
// if (score <= 4) return "中";
// return "强";
// }
// ==================== 密码加密 ====================
@ -164,7 +166,7 @@ public class PasswordValidator {
* @return
*/
public static String generateRegistrationCode() {
return generateRegistrationCode(MIN_LENGTH, 10);
return generateRegistrationCode(MIN_LENGTH, MAX_LENGTH);
}
/**
@ -179,151 +181,151 @@ public class PasswordValidator {
throw new IllegalArgumentException("长度参数无效");
}
int length = minLen + random.nextInt(maxLen - minLen + 1);
int length = RandomUtils.nextInt(minLen, maxLen);
StringBuilder code = new StringBuilder(length);
// 确保至少有一个大写字母
code.append(UPPERCASE.charAt(random.nextInt(UPPERCASE.length())));
code.append(UPPERCASE.charAt(RandomUtils.nextInt(0, UPPERCASE.length() - 1)));
// 确保至少有一个小写字母
code.append(LOWERCASE.charAt(random.nextInt(LOWERCASE.length())));
code.append(LOWERCASE.charAt(RandomUtils.nextInt(0, LOWERCASE.length() - 1)));
// 确保至少有一个数字
code.append(DIGITS.charAt(random.nextInt(DIGITS.length())));
code.append(DIGITS.charAt(RandomUtils.nextInt(0, DIGITS.length() - 1)));
// 填充剩余字符
for (int i = 3; i < length; i++) {
code.append(ALL_CHARS.charAt(random.nextInt(ALL_CHARS.length())));
code.append(ALL_CHARS.charAt(RandomUtils.nextInt(0, ALL_CHARS.length() - 1)));
}
// 打乱字符顺序
return shuffleString(code.toString());
return RandomUtils.shuffleString(code.toString());
}
/**
*
*
* @param length
* @param includeSpecialChars
* @return
*/
public static String generateRandomPassword(int length, boolean includeSpecialChars) {
if (length < MIN_LENGTH) {
throw new IllegalArgumentException("密码长度不能少于 " + MIN_LENGTH);
}
String chars = ALL_CHARS;
if (includeSpecialChars) {
chars += "!@#$%^&*()_+-=[]{}";
}
StringBuilder password = new StringBuilder(length);
// 确保至少包含一个字母和一个数字
password.append(UPPERCASE.charAt(random.nextInt(UPPERCASE.length())));
password.append(DIGITS.charAt(random.nextInt(DIGITS.length())));
// 填充剩余字符
for (int i = 2; i < length; i++) {
password.append(chars.charAt(random.nextInt(chars.length())));
}
return shuffleString(password.toString());
}
// /**
// * 生成固定长度的随机密码
// *
// * @param length 密码长度
// * @param includeSpecialChars 是否包含特殊字符
// * @return 随机密码
// */
// public static String generateRandomPassword(int length, boolean includeSpecialChars) {
// if (length < MIN_LENGTH) {
// throw new IllegalArgumentException("密码长度不能少于 " + MIN_LENGTH);
// }
//
// String chars = ALL_CHARS;
// if (includeSpecialChars) {
// chars += "!@#$%^&*()_+-=[]{}";
// }
//
// StringBuilder password = new StringBuilder(length);
//
// // 确保至少包含一个字母和一个数字
// password.append(UPPERCASE.charAt(random.nextInt(UPPERCASE.length())));
// password.append(DIGITS.charAt(random.nextInt(DIGITS.length())));
//
// // 填充剩余字符
// for (int i = 2; i < length; i++) {
// password.append(chars.charAt(random.nextInt(chars.length())));
// }
//
// return shuffleString(password.toString());
// }
// ==================== 工具方法 ====================
/**
* 使 Fisher-Yates
*
* @param str
* @return
*/
private static String shuffleString(String str) {
if (str == null || str.length() <= 1) {
return str;
}
char[] chars = str.toCharArray();
for (int i = chars.length - 1; i > 0; i--) {
int j = random.nextInt(i + 1);
// 交换
char temp = chars[i];
chars[i] = chars[j];
chars[j] = temp;
}
return new String(chars);
}
/**
*
*
* @param password
* @return true
*/
public static boolean isWeakPassword(String password) {
if (password == null) {
return true;
}
String lowerPassword = password.toLowerCase();
// 常见弱密码列表
String[] weakPasswords = {
"123456", "password", "123456789", "12345678", "12345",
"111111", "1234567", "sunshine", "qwerty", "iloveyou",
"princess", "admin", "welcome", "666666", "abc123",
"football", "123123", "monkey", "654321", "!@#$%^&*",
"charlie", "aa123456", "donald", "password1", "qwerty123"
};
for (String weak : weakPasswords) {
if (lowerPassword.equals(weak) || lowerPassword.contains(weak)) {
return true;
}
}
// 检查是否是连续数字或字母
if (password.matches("^(\\d)\\1+$") || // 全是相同数字
password.matches("^(.)\\1+$") || // 全是相同字符
password.matches("^(0123456789|123456789|987654321|abcdefghij|qwertyuiop).*")) { // 连续字符
return true;
}
return false;
}
/**
*
*
* @param password
* @return
*/
public static String getPasswordSuggestion(String password) {
if (password == null || password.isEmpty()) {
return "请输入密码";
}
String error = validatePassword(password);
if (error != null) {
return error;
}
String strength = getPasswordStrength(password);
if ("弱".equals(strength)) {
return "密码强度较弱,建议:\n" +
"• 使用至少8位字符\n" +
"• 同时包含大小写字母、数字\n" +
"• 添加特殊字符";
} else if ("中".equals(strength)) {
return "密码强度中等,可以考虑添加特殊字符提高安全性";
} else {
return "密码强度良好!";
}
}
//
// /**
// * 打乱字符串(使用 Fisher-Yates 算法)
// *
// * @param str 原字符串
// * @return 打乱后的字符串
// */
// private static String shuffleString(String str) {
// if (str == null || str.length() <= 1) {
// return str;
// }
//
// char[] chars = str.toCharArray();
//
// for (int i = chars.length - 1; i > 0; i--) {
// int j = random.nextInt(i + 1);
//
// // 交换
// char temp = chars[i];
// chars[i] = chars[j];
// chars[j] = temp;
// }
//
// return new String(chars);
// }
// /**
// * 检查密码是否包含常见弱密码
// *
// * @param password 密码
// * @return true表示是弱密码
// */
// public static boolean isWeakPassword(String password) {
// if (password == null) {
// return true;
// }
//
// String lowerPassword = password.toLowerCase();
//
// // 常见弱密码列表
// String[] weakPasswords = {
// "123456", "password", "123456789", "12345678", "12345",
// "111111", "1234567", "sunshine", "qwerty", "iloveyou",
// "princess", "admin", "welcome", "666666", "abc123",
// "football", "123123", "monkey", "654321", "!@#$%^&*",
// "charlie", "aa123456", "donald", "password1", "qwerty123"
// };
//
// for (String weak : weakPasswords) {
// if (lowerPassword.equals(weak) || lowerPassword.contains(weak)) {
// return true;
// }
// }
//
// // 检查是否是连续数字或字母
// if (password.matches("^(\\d)\\1+$") || // 全是相同数字
// password.matches("^(.)\\1+$") || // 全是相同字符
// password.matches("^(0123456789|123456789|987654321|abcdefghij|qwertyuiop).*")) { // 连续字符
// return true;
// }
//
// return false;
// }
// /**
// * 生成密码建议
// *
// * @param password 密码
// * @return 建议文本
// */
// public static String getPasswordSuggestion(String password) {
// if (password == null || password.isEmpty()) {
// return "请输入密码";
// }
//
// String error = validatePassword(password);
// if (error != null) {
// return error;
// }
//
// String strength = getPasswordStrength(password);
//
// if ("弱".equals(strength)) {
// return "密码强度较弱,建议:\n" +
// "• 使用至少8位字符\n" +
// "• 同时包含大小写字母、数字\n" +
// "• 添加特殊字符";
// } else if ("中".equals(strength)) {
// return "密码强度中等,可以考虑添加特殊字符提高安全性";
// } else {
// return "密码强度良好!";
// }
// }
}

@ -43,6 +43,28 @@ public class RandomUtils {
Collections.shuffle(list, random);
}
/**
* 使Fisher-Yates
* @param str
* @return
*/
public static String shuffleString(String str) {
if (str == null || str.length() <= 1) {
return str;
}
char[] chars = str.toCharArray();
for (int i = chars.length - 1; i > 0; i--) {
int j = random.nextInt(i + 1);
// 交换字符
char temp = chars[i];
chars[i] = chars[j];
chars[j] = temp;
}
return new String(chars);
}
//生成指定范围内的随机双精度浮点数
public static double nextDouble(double min, double max) {

@ -1,774 +0,0 @@
import com.model.*;
import com.service.*;
import com.service.question_generator.QuestionFactoryManager;
import com.util.PasswordValidator;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
*
*
*/
public class TestMain {
private static int testsPassed = 0;
private static int testsFailed = 0;
public static void main(String[] args) {
System.out.println("========================================");
System.out.println(" 数学答题系统 - 完整测试");
System.out.println("========================================\n");
try {
// 1. 测试工具类
testPasswordValidator();
// 2. 测试文件服务
testFileIOService();
// 3. 测试用户服务
testUserService();
// 4. 测试题目生成
testQuestionGeneration();
// 5. 测试答题服务
testQuizService();
// 6. 测试完整流程
testCompleteWorkflow();
// 输出测试结果
printTestSummary();
} catch (Exception e) {
System.err.println("测试过程中发生错误:" + e.getMessage());
e.printStackTrace();
}
}
// ==================== 1. 测试密码验证工具 ====================
private static void testPasswordValidator() {
System.out.println("【测试1】密码验证工具");
System.out.println("----------------------------------------");
// 测试1.1: 有效密码
test("有效密码验证",
PasswordValidator.isValid("Abc123456"),
"密码 'Abc123456' 应该有效");
// 测试1.2: 密码太短
test("密码太短检测",
!PasswordValidator.isValid("Abc12"),
"密码 'Abc12' 应该无效(太短)");
// 测试1.3: 缺少数字
test("缺少数字检测",
!PasswordValidator.isValid("Abcdefgh"),
"密码 'Abcdefgh' 应该无效(缺少数字)");
// 测试1.4: 密码加密
String encrypted1 = PasswordValidator.encrypt("test123");
String encrypted2 = PasswordValidator.encrypt("test123");
test("密码加密一致性",
encrypted1.equals(encrypted2),
"相同密码加密结果应该一致");
// 测试1.5: 密码匹配
test("密码匹配验证",
PasswordValidator.matches("test123", encrypted1),
"密码匹配应该成功");
// 测试1.6: 生成注册码
String code = PasswordValidator.generateRegistrationCode();
test("注册码生成",
code.length() >= 6 && code.length() <= 10,
"注册码长度应该在6-10位之间实际" + code.length());
// 测试1.7: 密码强度检测
String strength = PasswordValidator.getPasswordStrength("Abc123!@#");
test("密码强度检测",
strength != null && !strength.isEmpty(),
"密码强度应该返回有效值,实际:" + strength);
System.out.println();
}
// ==================== 2. 测试文件IO服务 ====================
private static void testFileIOService() throws IOException {
System.out.println("【测试2】文件IO服务");
System.out.println("----------------------------------------");
FileIOService fileService = new FileIOService();
// 测试2.1: 初始化目录
try {
fileService.initDataDirectory();
test("初始化数据目录", true, "数据目录初始化成功");
} catch (Exception e) {
test("初始化数据目录", false, "失败:" + e.getMessage());
}
// 测试2.2: 保存和加载用户
User testUser = new User("小学-测试", "encrypted123", "test@test.com", Grade.ELEMENTARY);
try {
fileService.saveUser(testUser);
User loaded = fileService.findUserByUsername("小学-测试");
test("保存和加载用户",
loaded != null && loaded.getUsername().equals("小学-测试"),
"用户数据应该正确保存和加载");
} catch (Exception e) {
test("保存和加载用户", false, "失败:" + e.getMessage());
}
// 测试2.3: 检查用户名是否存在
try {
boolean exists = fileService.isUsernameExists("小学-测试");
test("检查用户名存在", exists, "用户名应该存在");
} catch (Exception e) {
test("检查用户名存在", false, "失败:" + e.getMessage());
}
// 测试2.4: 查找不存在的用户
try {
User notFound = fileService.findUserByUsername("不存在的用户");
test("查找不存在的用户",
notFound == null,
"不存在的用户应该返回null");
} catch (Exception e) {
test("查找不存在的用户", false, "失败:" + e.getMessage());
}
System.out.println();
}
// ==================== 3. 测试用户服务====================
private static void testUserService() throws IOException {
System.out.println("【测试3】用户服务包含验证码");
System.out.println("----------------------------------------");
UserService userService = new UserService();
// ========== 3.1 测试注册码生成和保存 ==========
String testEmail1 = "test001@example.com";
String registrationCode1 = null;
try {
registrationCode1 = userService.generateRegistrationCode(testEmail1);
test("生成注册码",
registrationCode1 != null && registrationCode1.length() >= 6,
"注册码:" + registrationCode1);
} catch (Exception e) {
test("生成注册码", false, "失败:" + e.getMessage());
}
// ========== 3.2 测试注册码文件存储 ==========
try {
boolean fileExists = com.util.FileUtils.exists("data/registration_codes.txt");
test("注册码文件创建",
fileExists,
"注册码应该保存到文件");
} catch (Exception e) {
test("注册码文件创建", false, "失败:" + e.getMessage());
}
// ========== 3.3 测试用户注册(带验证码)==========
String testUsername = "小学-张三测试";
String testPassword = "Test123456";
try {
User user = userService.register(testUsername, testPassword, testEmail1, registrationCode1);
test("用户注册(带验证码)",
user != null && user.getUsername().equals(testUsername),
"用户注册成功");
} catch (Exception e) {
test("用户注册(带验证码)", false, "失败:" + e.getMessage());
}
// ========== 3.4 测试注册码一次性使用 ==========
try {
// 尝试用同一个注册码再次注册
userService.register("小学-李四", "Test123456", testEmail1, registrationCode1);
test("注册码一次性使用", false, "应该抛出异常");
} catch (IllegalArgumentException e) {
test("注册码一次性使用",
e.getMessage().contains("未找到"),
"注册码使用后应该被删除");
} catch (Exception e) {
test("注册码一次性使用", false, "异常类型错误:" + e.getMessage());
}
// ========== 3.5 测试错误的注册码 ==========
String testEmail2 = "test002@example.com";
try {
String code = userService.generateRegistrationCode(testEmail2);
// 故意使用错误的注册码
userService.register("小学-王五", "Test123456", testEmail2, "wrongCode123");
test("错误注册码检测", false, "应该抛出异常");
} catch (IllegalArgumentException e) {
test("错误注册码检测",
e.getMessage().contains("注册码错误"),
"应该检测到错误的注册码");
} catch (Exception e) {
test("错误注册码检测", false, "失败:" + e.getMessage());
}
// ========== 3.6 测试未获取注册码就注册 ==========
try {
userService.register("小学-赵六", "Test123456", "nocode@test.com", "randomCode");
test("未获取注册码检测", false, "应该抛出异常");
} catch (IllegalArgumentException e) {
test("未获取注册码检测",
e.getMessage().contains("未找到"),
"应该检测到未获取注册码");
} catch (Exception e) {
test("未获取注册码检测", false, "失败:" + e.getMessage());
}
// ========== 3.7 测试重复注册检测 ==========
String testEmail3 = "test003@example.com";
try {
String code = userService.generateRegistrationCode(testEmail3);
userService.register(testUsername, "Test123456", testEmail3, code);
test("重复注册检测", false, "应该抛出异常");
} catch (IllegalArgumentException e) {
test("重复注册检测",
e.getMessage().contains("已存在"),
"应该检测到用户名已存在");
} catch (Exception e) {
test("重复注册检测", false, "失败:" + e.getMessage());
}
// ========== 3.8 测试用户登录 ==========
try {
User user = userService.login(testUsername, testPassword);
test("用户登录",
user != null && userService.isLoggedIn(),
"用户登录成功");
} catch (Exception e) {
test("用户登录", false, "失败:" + e.getMessage());
}
// ========== 3.9 测试错误密码登录 ==========
try {
userService.logout(); // 先退出
userService.login(testUsername, "WrongPassword123");
test("错误密码登录", false, "应该抛出异常");
} catch (IllegalArgumentException e) {
test("错误密码登录",
e.getMessage().contains("密码错误"),
"应该检测到密码错误");
} catch (Exception e) {
test("错误密码登录", false, "失败:" + e.getMessage());
}
// ========== 3.10 测试不存在的用户登录 ==========
try {
userService.login("小学-不存在", "Test123456");
test("不存在用户登录", false, "应该抛出异常");
} catch (IllegalArgumentException e) {
test("不存在用户登录",
e.getMessage().contains("不存在"),
"应该检测到用户名不存在");
} catch (Exception e) {
test("不存在用户登录", false, "失败:" + e.getMessage());
}
// ========== 3.11 测试获取当前用户 ==========
try {
userService.login(testUsername, testPassword);
User current = userService.getCurrentUser();
test("获取当前用户",
current != null && current.getUsername().equals(testUsername),
"应该返回当前登录用户");
} catch (Exception e) {
test("获取当前用户", false, "失败:" + e.getMessage());
}
// ========== 3.12 测试提取真实姓名 ==========
User user = userService.getCurrentUser();
String realName = userService.getRealName(user);
test("提取真实姓名",
realName.equals("张三测试"),
"应该正确提取真实姓名,实际:" + realName);
// ========== 3.13 测试获取学段显示名 ==========
String gradeName = userService.getGradeDisplayName(user);
test("获取学段显示名",
gradeName.equals("小学"),
"应该返回'小学',实际:" + gradeName);
// ========== 3.14 测试退出登录 ==========
userService.logout();
test("退出登录",
!userService.isLoggedIn(),
"退出后应该未登录状态");
// ========== 3.15 测试完整注册流程(不同学段)==========
// 初中学生注册
try {
String middleEmail = "middle@test.com";
String middleCode = userService.generateRegistrationCode(middleEmail);
User middleUser = userService.register("初中-李明", "Middle123", middleEmail, middleCode);
test("初中学生注册",
middleUser != null && middleUser.getGrade() == Grade.MIDDLE,
"初中学生注册成功");
} catch (Exception e) {
test("初中学生注册", false, "失败:" + e.getMessage());
}
// 高中学生注册
try {
String highEmail = "high@test.com";
String highCode = userService.generateRegistrationCode(highEmail);
User highUser = userService.register("高中-王华", "High123456", highEmail, highCode);
test("高中学生注册",
highUser != null && highUser.getGrade() == Grade.HIGH,
"高中学生注册成功");
} catch (Exception e) {
test("高中学生注册", false, "失败:" + e.getMessage());
}
// ========== 3.16 测试密码强度验证 ==========
try {
String weakEmail = "weak@test.com";
String weakCode = userService.generateRegistrationCode(weakEmail);
userService.register("小学-弱密码", "123", weakEmail, weakCode);
test("密码强度验证", false, "应该拒绝弱密码");
} catch (IllegalArgumentException e) {
test("密码强度验证",
e.getMessage().contains("密码"),
"应该检测到密码不符合要求");
} catch (Exception e) {
test("密码强度验证", false, "失败:" + e.getMessage());
}
// ========== 3.17 测试邮箱格式验证 ==========
try {
userService.generateRegistrationCode("invalid-email");
test("邮箱格式验证", false, "应该拒绝无效邮箱");
} catch (IllegalArgumentException e) {
test("邮箱格式验证",
e.getMessage().contains("邮箱"),
"应该检测到邮箱格式错误");
} catch (Exception e) {
test("邮箱格式验证", false, "失败:" + e.getMessage());
}
// ========== 3.18 测试用户名格式验证 ==========
try {
String invalidEmail = "invalid@test.com";
String invalidCode = userService.generateRegistrationCode(invalidEmail);
userService.register("错误格式", "Test123456", invalidEmail, invalidCode);
test("用户名格式验证", false, "应该拒绝错误格式的用户名");
} catch (IllegalArgumentException e) {
test("用户名格式验证",
e.getMessage().contains("格式"),
"应该检测到用户名格式错误");
} catch (Exception e) {
test("用户名格式验证", false, "失败:" + e.getMessage());
}
// ========== 3.19 测试清理过期注册码 ==========
try {
userService.cleanExpiredCodes();
test("清理过期注册码", true, "清理操作成功");
} catch (Exception e) {
test("清理过期注册码", false, "失败:" + e.getMessage());
}
// ========== 3.20 测试从文件重新加载注册码 ==========
try {
String reloadEmail = "reload@test.com";
String reloadCode = userService.generateRegistrationCode(reloadEmail);
// 创建新的 UserService 实例(模拟重启)
UserService newUserService = new UserService();
// 使用之前保存的注册码
User reloadUser = newUserService.register("小学-重载测试", "Reload123", reloadEmail, reloadCode);
test("从文件重载注册码",
reloadUser != null,
"应该能从文件读取注册码");
} catch (Exception e) {
test("从文件重载注册码", false, "失败:" + e.getMessage());
}
// ========== 3.21 查看注册码文件内容 ==========
try {
if (com.util.FileUtils.exists("data/registration_codes.txt")) {
String fileContent = com.util.FileUtils.readFileToString(
"data/registration_codes.txt"
);
System.out.println("\n 【注册码文件内容预览】");
String[] lines = fileContent.split("\n");
int lineCount = 0;
for (String line : lines) {
if (lineCount++ < 10) { // 显示前10行
System.out.println(" " + line);
}
}
if (lines.length > 10) {
System.out.println(" ... (共 " + lines.length + " 行)");
}
test("注册码文件格式",
fileContent.contains("#") && fileContent.contains("|"),
"文件格式正确");
}
} catch (Exception e) {
test("查看文件内容", false, "失败:" + e.getMessage());
}
System.out.println();
}
// ==================== 4. 测试题目生成 ====================
private static void testQuestionGeneration() {
System.out.println("【测试4】题目生成");
System.out.println("----------------------------------------");
// 测试4.1: 生成小学题目(不去重)
try {
List<ChoiceQuestion> questions = QuestionFactoryManager.generateQuestions(
Grade.ELEMENTARY, 1, null
);
test("生成小学题目",
questions.size() == 1 && questions.get(0).getQuestionText() != null,
"应该生成1道有效的小学题目");
System.out.println(" 示例题目:" + questions.get(0).getQuestionText());
} catch (Exception e) {
test("生成小学题目", false, "失败:" + e.getMessage());
}
// 测试4.2: 生成初中题目
try {
List<ChoiceQuestion> questions = QuestionFactoryManager.generateQuestions(
Grade.MIDDLE, 1, null
);
test("生成初中题目",
questions.size() == 1,
"应该生成1道有效的初中题目");
System.out.println(" 示例题目:" + questions.get(0).getQuestionText());
} catch (Exception e) {
test("生成初中题目", false, "失败:" + e.getMessage());
}
// 测试4.3: 生成高中题目
try {
List<ChoiceQuestion> questions = QuestionFactoryManager.generateQuestions(
Grade.HIGH, 1, null
);
test("生成高中题目",
questions.size() == 1,
"应该生成1道有效的高中题目");
System.out.println(" 示例题目:" + questions.get(0).getQuestionText());
} catch (Exception e) {
test("生成高中题目", false, "失败:" + e.getMessage());
}
// 测试4.4: 批量生成题目
try {
List<ChoiceQuestion> questions = QuestionFactoryManager.generateQuestions(
Grade.ELEMENTARY, 10, null
);
test("批量生成题目",
questions.size() == 10,
"应该生成10道题目实际" + questions.size());
} catch (Exception e) {
test("批量生成题目", false, "失败:" + e.getMessage());
}
// 测试4.5: 题目去重功能
try {
// 第一次生成
List<ChoiceQuestion> firstBatch = QuestionFactoryManager.generateQuestions(
Grade.ELEMENTARY, 5, null
);
// 收集已生成的题目文本
Set<String> historyQuestions = new HashSet<>();
for (ChoiceQuestion q : firstBatch) {
historyQuestions.add(q.getQuestionText());
}
// 第二次生成(带去重)
List<ChoiceQuestion> secondBatch = QuestionFactoryManager.generateQuestions(
Grade.ELEMENTARY, 5, historyQuestions
);
// 检查第二次生成的题目是否与第一次重复
boolean noDuplicate = true;
for (ChoiceQuestion q : secondBatch) {
if (historyQuestions.contains(q.getQuestionText())) {
noDuplicate = false;
break;
}
}
test("题目去重功能",
noDuplicate,
"第二次生成的题目不应与第一次重复");
} catch (Exception e) {
test("题目去重功能", false, "失败:" + e.getMessage());
}
System.out.println();
}
// ==================== 5. 测试答题服务 ====================
private static void testQuizService() throws IOException {
System.out.println("【测试5】答题服务");
System.out.println("----------------------------------------");
FileIOService fileService = new FileIOService();
UserService userService = new UserService(fileService);
QuizService quizService = new QuizService(fileService, userService);
// 创建测试用户
User testUser = new User("小学-李四", "encrypted", "lisi@test.com", Grade.ELEMENTARY);
fileService.saveUser(testUser);
// 测试5.1: 开始答题
try {
quizService.startNewQuiz(testUser, 5);
test("开始答题会话",
quizService.getTotalQuestions() == 5,
"应该生成5道题目");
} catch (Exception e) {
test("开始答题会话", false, "失败:" + e.getMessage());
}
// 测试5.2: 获取当前题目
ChoiceQuestion current = quizService.getCurrentQuestion();
test("获取当前题目",
current != null,
"应该返回当前题目");
// 测试5.3: 提交答案
try {
boolean correct = quizService.submitCurrentAnswer(0);
test("提交答案",
true, // 只要不抛异常就算通过
"提交答案应该成功,结果:" + (correct ? "正确" : "错误"));
} catch (Exception e) {
test("提交答案", false, "失败:" + e.getMessage());
}
// 测试5.4: 题目导航
boolean canNext = quizService.nextQuestion();
test("下一题导航",
canNext,
"应该能够移动到下一题");
boolean canPrev = quizService.previousQuestion();
test("上一题导航",
canPrev,
"应该能够移动到上一题");
// 测试5.5: 检查答案
ChoiceQuestion question = quizService.getCurrentQuestion();
int correctIndex = quizService.getCorrectAnswerIndex(question);
boolean isCorrect = quizService.checkAnswer(question, correctIndex);
test("检查正确答案",
isCorrect,
"正确答案应该通过验证");
// 测试5.6: 答题进度
quizService.goToQuestion(0);
quizService.submitCurrentAnswer(0);
quizService.nextQuestion();
quizService.submitCurrentAnswer(1);
int answered = quizService.getAnsweredCount();
test("答题进度统计",
answered == 2,
"应该有2道题已作答实际" + answered);
// 测试5.7: 完成所有题目并计算成绩
for (int i = 0; i < quizService.getTotalQuestions(); i++) {
quizService.goToQuestion(i);
quizService.submitCurrentAnswer(0);
}
QuizResult result = quizService.calculateResult();
test("计算成绩",
result.getTotalQuestions() == 5,
"成绩统计应该正确,总题数:" + result.getTotalQuestions());
System.out.println(" 得分:" + result.getScore());
System.out.println(" 正确:" + result.getCorrectCount());
System.out.println(" 错误:" + result.getWrongCount());
// 测试5.8: 格式化输出
String formatted = quizService.formatResult(result);
test("格式化结果输出",
formatted != null && formatted.contains("答题结束"),
"应该返回格式化的结果文本");
System.out.println();
}
// ==================== 6. 测试完整流程 ====================
private static void testCompleteWorkflow() throws IOException {
System.out.println("【测试6】完整答题流程");
System.out.println("----------------------------------------");
FileIOService fileService = new FileIOService();
UserService userService = new UserService(fileService);
QuizService quizService = new QuizService(fileService, userService);
try {
// ========== 步骤1: 注册新用户 ==========
System.out.println("步骤1: 注册新用户...");
String username = "初中-王五";
String password = "Test123456";
String email = "wangwu@test.com";
// 1.1 生成注册码
String registrationCode = userService.generateRegistrationCode(email);
System.out.println(" 获取注册码:" + registrationCode);
// 1.2 使用注册码注册
User user = userService.register(username, password, email, registrationCode);
test("完整流程-注册", user != null, "用户注册成功");
// ========== 步骤2: 用户登录 ==========
System.out.println("步骤2: 用户登录...");
userService.login(username, password);
test("完整流程-登录", userService.isLoggedIn(), "用户登录成功");
// ========== 步骤3: 开始答题 ==========
System.out.println("步骤3: 开始答题10道题...");
quizService.startNewQuiz(user, 10);
test("完整流程-生成题目",
quizService.getTotalQuestions() == 10,
"题目生成成功");
// ========== 步骤4: 答题(模拟全部答对)==========
System.out.println("步骤4: 模拟答题过程...");
for (int i = 0; i < 10; i++) {
quizService.goToQuestion(i);
ChoiceQuestion q = quizService.getCurrentQuestion();
int correctIndex = quizService.getCorrectAnswerIndex(q);
quizService.submitAnswer(i, correctIndex);
}
test("完整流程-答题", quizService.isAllAnswered(), "所有题目已作答");
// ========== 步骤5: 计算成绩 ==========
System.out.println("步骤5: 计算成绩...");
QuizResult result = quizService.calculateResult();
test("完整流程-计算成绩",
result.getScore() == 100,
"全部答对应该得100分实际" + result.getScore());
System.out.println(quizService.formatResult(result));
// ========== 步骤6: 保存记录 ==========
System.out.println("步骤6: 保存答题记录...");
quizService.saveQuizHistory(user);
// 验证用户统计是否更新
User updatedUser = fileService.findUserByUsername(username);
test("完整流程-保存记录",
updatedUser.getTotalQuizzes() == 1,
"用户答题次数应该增加,实际:" + updatedUser.getTotalQuizzes());
test("完整流程-平均分更新",
updatedUser.getAverageScore() == 100.0,
"平均分应该更新,实际:" + updatedUser.getAverageScore());
// ========== 步骤7: 退出登录 ==========
System.out.println("步骤7: 退出登录...");
userService.logout();
test("完整流程-退出", !userService.isLoggedIn(), "退出登录成功");
System.out.println("\n✓ 完整流程测试通过!");
} catch (Exception e) {
test("完整流程", false, "失败:" + e.getMessage());
e.printStackTrace();
}
System.out.println();
}
// ==================== 测试工具方法 ====================
private static void test(String testName, boolean condition, String message) {
if (condition) {
System.out.println(" ✓ " + testName + ": 通过");
if (message != null && !message.isEmpty()) {
System.out.println(" " + message);
}
testsPassed++;
} else {
System.out.println(" ✗ " + testName + ": 失败");
if (message != null && !message.isEmpty()) {
System.out.println(" " + message);
}
testsFailed++;
}
}
private static void printTestSummary() {
System.out.println("========================================");
System.out.println(" 测试结果汇总");
System.out.println("========================================");
System.out.println("总测试数:" + (testsPassed + testsFailed));
System.out.println("通过:" + testsPassed + " 项");
System.out.println("失败:" + testsFailed + " 项");
if (testsFailed == 0) {
System.out.println("\n🎉 所有测试通过项目功能正常可以开始开发UI了");
} else {
System.out.println("\n⚠ 有 " + testsFailed + " 项测试失败,请检查并修复问题");
}
System.out.println("========================================");
}
}
Loading…
Cancel
Save