正式版1.0.0 #11

Merged
hnu202326010319 merged 41 commits from develop into main 4 months ago

38
.gitignore vendored

@ -0,0 +1,38 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

@ -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,216 @@
<?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="发行版1.03">
<change afterPath="$PROJECT_DIR$/doc/测试文档/发行版1.04测试.md" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/main/java/com/pair/util/AsyncRegistrationHelper.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/doc/测试文档/发行版1.03测试.md" beforeDir="false" afterPath="$PROJECT_DIR$/doc/测试文档/发行版1.03测试.md" 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/pair/model/QuizResult.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/pair/model/QuizResult.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/pair/service/UserService.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/pair/service/UserService.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/pair/ui/InfGenPage.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/pair/ui/InfGenPage.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/pair/ui/MainWindow.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/pair/ui/MainWindow.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/pair/ui/NavigablePanel.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/pair/ui/NavigablePanel.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/pair/util/PasswordValidator.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/pair/util/PasswordValidator.java" afterDir="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="JavaFXApplication" />
<option value="Interface" />
<option value="Class" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_BRANCH_BY_REPOSITORY">
<map>
<entry key="$PROJECT_DIR$" value="WuBaiXuan_Branch" />
</map>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="KubernetesApiPersistence">{}</component>
<component name="KubernetesApiProvider">{
&quot;isMigrated&quot;: true
}</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"><![CDATA[{
"keyToString": {
"Application.Test.executor": "Run",
"Application.TestMain.executor": "Run",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.git.unshallow": "true",
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
"git-widget-placeholder": "LiangJunYaoBranch",
"kotlin-language-version-configured": "true",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"project.structure.last.edited": "Artifacts",
"project.structure.proportion": "0.0",
"project.structure.side.proportion": "0.0",
"settings.editor.selected.configurable": "MavenSettings",
"vue.rearranger.settings.migration": "true"
}
}]]></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.pair.Test" />
<module name="MathQuizApp" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="com.pair.*" />
<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>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-jdk-9823dce3aa75-28b599e66164-intellij.indexing.shared.core-IU-242.23726.103" />
<option value="bundled-js-predefined-d6986cc7102b-5c90d61e3bab-JavaScript-IU-242.23726.103" />
</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="4945000" />
<workItem from="1759752200157" duration="6627000" />
<workItem from="1759758921108" duration="6306000" />
<workItem from="1759815278314" duration="2288000" />
<workItem from="1759817587775" duration="661000" />
</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>
<task id="LOCAL-00003" summary="version one ,do not package">
<option name="closed" value="true" />
<created>1759749578708</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1759749578708</updated>
</task>
<task id="LOCAL-00004" summary="发行版1.01">
<option name="closed" value="true" />
<created>1759758970278</created>
<option name="number" value="00004" />
<option name="presentableId" value="LOCAL-00004" />
<option name="project" value="LOCAL" />
<updated>1759758970278</updated>
</task>
<task id="LOCAL-00005" summary="发行版1.03">
<option name="closed" value="true" />
<created>1759820089275</created>
<option name="number" value="00005" />
<option name="presentableId" value="LOCAL-00005" />
<option name="project" value="LOCAL" />
<updated>1759820089275</updated>
</task>
<option name="localTasksCounter" value="6" />
<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" />
<MESSAGE value="version one ,do not package" />
<MESSAGE value="发行版1.01" />
<MESSAGE value="发行版1.03" />
<option name="LAST_COMMIT_MESSAGE" value="发行版1.03" />
</component>
</project>

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

@ -0,0 +1,10 @@
{
"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,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,102 @@
<?xml version="1.0" encoding="UTF-8"?>
<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mathquiz</groupId>
<artifactId>MathQuizApp</artifactId>
<name>Math Quiz Application</name>
<version>1.04</version>
<description>小初高数学学习软件- JavaFX版本</description>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.1</version>
<configuration>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.6.1</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/dependencies</outputDirectory>
<overWriteReleases>false</overWriteReleases>
<overWriteSnapshots>false</overWriteSnapshots>
<overWriteIfNewer>true</overWriteIfNewer>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer>
<mainClass>com.pair.Test</mainClass>
</transformer>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
<finalName>MathQuizApp</finalName>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<mainClass>com.pair.Test</mainClass>
<arguments>
<argument>--add-modules</argument>
<argument>javafx.controls,javafx.fxml,javafx.graphics,javafx.base</argument>
</arguments>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<properties>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.source>21</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<javafx.version>21.0.2</javafx.version>
</properties>
</project>

@ -0,0 +1,15 @@
# 发行版1.03测试
## 新增功能:
- 可发送真实邮箱
## 待实现功能或待修改bug
- 点击注册时窗口界面会卡顿,感觉可以将发送动作放到后台
- 增加发送注册码后60s后才可继续发送
- 注册码发送提示还是错误图标新建一个弹窗提示不混用错误弹窗函数提示改为“已发送到邮箱10分钟内有效”
- 生成题目页面选择初中高中学段生成题目,做完题出来学段选择又变回小学了,页面初始化得根据用户学段调整
- 得分的%去掉,得分是按百分比计算,不是将百分比当作得分
- 注册码简单一点6为随机数字
- 个人信息窗口,直接退出回到登录界面也会保存修改的用户名
- 密码修改成功后窗口提示
- 登录bug目前只能用用户名登录

@ -0,0 +1,7 @@
# 发行版1.04测试
## 新增功能
- 修复登录只能用用户名登录的bug
- 新增初中高中题型
- 新增生成注册码冷却时间60s
- 优化动作提示

@ -0,0 +1,101 @@
# 数学题库生成系统详细设计文档
## 项目概述
### 项目目标
为小初高学生提供一个本地的桌面端图像化应用,支持用户注册和登录,根据学生年级按规则随机生成数学选择题,完成答题后自动计算分数
### 技术栈
| 类型 | 技术 | 说明 |
| -------- | -------------------------- | ---------------------------------------- |
| 开发语言 | Java 11+ | 保证跨平台兼容性,支持 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/
├── src/
│ └── main/
│ └── java/
│ └── com/
│ └── mathquiz/
│ ├── Main.java # 程序入口
│ │
│ ├── model/ # 数据模型
│ │ ├── User.java
│ │ ├── Grade.java
│ │ └── ChoiceQuestion.java
│ │
│ ├── service/ # 后端逻辑无GUI
│ │ ├── UserService.java
│ │ ├── QuestionGenerator.java
│ │ ├── FileIOService.java
│ │ └── QuizService.java
│ │
│ ├── ui/ # 前端 GUI
│ │ ├── MainWindow.java
│ │ ├── RegisterPanel.java
│ │ ├── LoginPanel.java
| | ├── PasswordModifyPanel.java
│ │ ├── GradeSelectPanel.java
│ │ ├── QuizPanel.java
│ │ └── ResultPanel.java
│ │
│ └── util/ # 工具类
│ ├── PasswordValidator.java
| ├── EmailUtil.java
│ ├── RandomUtils.java
│ └── FileUtils.java
├── data/ # 运行时生成(不提交)
│ ├── users/ # 用户信息 JSON
│ └── temp_codes/ # 临时注册码文件
├── pom.xml # Maven 依赖(含 JavaFX
└── README.md
## 详细模块设计
### 模型层设计
#### User类
- String name // 用户ID
- String email // 用户邮箱
- String encryptedPwd // 加密后的用户密码
- Grade grade //学段
#### ChoiceQuestion
String questionId; // 题目唯一IDUUID生成
Grade grade; // 所属学段
String questionContent; // 题干
List<String> options; // 选项列表固定4个顺序随机
String correctAnswer; // 正确答案(如"A"/"B"/"C"/"D"
#### Grade 枚举
PRIMARY("小学", 1)
JUNIOR("初中", 2)
SENIOR("高中", 3)
### 工具层设计
#### PasswordValidator
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ------- | ---------------------------------- | ------- | ------------------------------------------------------------ |
| isValid | String rawPwd | boolean | 校验规则1. 长度 6-10 位2. 包含至少 1 个大写字母3. 包含至少 1 个小写字母4. 包含至少 1 个数字 |
| encrypt | String rawPwd | String | 使用 BCrypt 加密密码(自动生成盐值,无需额外存储盐) |
| matches | String rawPwd, String encryptedPwd | boolean | 校验明文密码与加密密码是否匹配BCrypt 自带校验逻辑) |
#### EmailUtil
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ------------- | ----------------------------------------- | ------- | ------------------------------------------------------------ |
| sendTextEmail | String to, String subject, String content | boolean | 1. 配置 SMTP 服务器主机、端口、SSL2. 设置发件人账号 / 授权码3. 构建邮件内容4. 发送并返回结果 |

@ -0,0 +1,219 @@
# 数学题库生成系统详细设计文档
## 项目概述
### 项目目标
为小初高学生提供一个本地的桌面端图像化应用,支持用户注册和登录,根据学生年级按规则随机生成数学选择题,完成答题后自动计算分数
### 技术栈
| 类型 | 技术 | 说明 |
| -------- | -------------------------- | ---------------------------------------- |
| 开发语言 | 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/
├── src/
│ └── main/
│ └── java/
│ └── com/
│ └── mathquiz/
│ ├── Main.java # 程序入口
│ │
│ ├── model/ # 数据模型
│ │ ├── User.java
│ │ ├── Grade.java
│ │ └── ChoiceQuestion.java
│ │
│ ├── service/ # 后端逻辑无GUI
│ │ ├── UserService.java
│ │ ├── QuestionGenerator.java
│ │ ├── FileIOService.java
│ │ └── QuizService.java
│ │
│ ├── ui/ # 前端 GUI
│ │ ├── MainWindow.java
│ │ ├── RegisterPanel.java
│ │ ├── LoginPanel.java
| | ├── PasswordModifyPanel.java
│ │ ├── GradeSelectPanel.java
│ │ ├── QuizPanel.java
│ │ └── ResultPanel.java
│ │
│ └── util/ # 工具类
│ ├── PasswordValidator.java
| ├── EmailUtil.java
│ ├── RandomUtils.java
│ └── FileUtils.java
├── data/ # 运行时生成(不提交)
│ ├── users/ # 用户信息 JSON
│ └── temp_codes/ # 临时注册码文件
├── pom.xml # Maven 依赖(含 JavaFX
└── README.md
## 详细模块设计
### 模型层设计
#### User类
- String name // 用户ID
- String email // 用户邮箱
- String encryptedPwd // 加密后的用户密码
- Grade grade //学段
#### ChoiceQuestion
String questionId; // 题目唯一IDUUID生成
Grade grade; // 所属学段
String questionContent; // 题干
List<String> options; // 选项列表固定4个顺序随机
String correctAnswer; // 正确答案(如"A"/"B"/"C"/"D"
#### Grade 枚举
PRIMARY("小学", 1)
JUNIOR("初中", 2)
SENIOR("高中", 3)
### 工具层设计
#### PasswordValidator
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| -------------------------- | ------------------------------------------------ | --------- | ------------------------------------------------------------ |
| isValid | String password | boolean | 校验密码:**610位****必须同时包含大写字母、小写字母和数字** |
| encrypt | String password | String | 使用 **SHA-256** 对密码哈希,返回 64 位十六进制字符串 |
| matches | String plainPassword, String encryptedPassword | boolean | 对明文密码加密后与存储的密文比对,判断是否匹配 |
| generateRegistrationCode | — | String | 生成 **610位** 随机字符串,**保证至少含1字母+1数字**,并打乱顺序 |
| shuffleString | String str | String | (私有)对字符串字符顺序进行 Fisher-Yates 打乱 |
#### 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,}$ 验证邮箱格式是否合法 |
#### 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 | 删除文件,若文件不存在也返回 true异常时返回 false 并打印堆栈 |
| listFiles | String dirPath | File[] | 返回目录下所有文件(不含子目录),若路径无效返回空数组 |
| appendToFile | String filePath, String content | void | 以 UTF-8 编码追加内容到文件末尾,自动创建文件(若不存在) |
| copyFile | String sourcePath, String targetPath | void | 复制文件,目标文件存在则覆盖,失败抛出 IOException |
| getFileSize | String filePath | long | 返回文件大小(字节),失败抛出 IOException |
#### RandomUtils
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| -------------- | ------------------------ | --------- | ---------------------------------------------------------- |
| nextInt | int min, int max | int | 生成 [min, max] 范围内的随机整数(含边界) |
| randomChoice | T[] array | T | 从非空数组中随机返回一个元素 |
| randomChoice | List<T> list | T | 从非空列表中随机返回一个元素 |
| shuffle | List<T> list | void | 原地打乱列表顺序(使用 Collections.shuffle |
| nextDouble | double min, double max | double | 生成 [min, max) 范围内的随机双精度浮点数 |
| nextBoolean | — | boolean | 返回 true 或 false各 50% 概率) |
| probability | double probability | boolean | 按指定概率返回 true如 0.7 表示 70% 概率返回 true |
### 服务层设计
#### UserService
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| -------------------- | ----------------------------------------------------------- | ------- | ------------------------------------------------------------ |
| sendRegistrationCode | String email | boolean | 生成 6 位数字注册码,调用 EmailUtil.sendTextEmail()发送(开发阶段可模拟),并将注册码写入 data/temp_codes/{email}.txt |
| verifyCode | String email, String code | boolean | 读取 data/temp_codes/{email}.txt校验注册码是否匹配 |
| setPassword | String email, String pwd1, String pwd2 | boolean | 校验两次密码一致且符合规则610位含大小写+数字),使用 BCrypt加密后保存用户到 data/users/{hash}.json成功后删除临时注册码文件 |
| login | String email, String password | User | 读取用户文件,用 BCrypt.matches() 验证密码,返回 User 对象或 null |
| changePassword | String email, String oldPwd, String newPwd1, String newPwd2 | boolean | 先验证原密码正确,再校验新密码格式,更新加密密码并保存 |
#### QuestionGenerator (抽象类)
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ------------- | ------------- | ------------------- | ------------------------------------------------------------ |
| create | Grade grade | QuestionGenerator | 静态工厂方法根据年级返回对应子类实例PrimaryGenerator / MiddleGenerator / SeniorGenerator |
| generateOne | — | ChoiceQuestion | 抽象方法由子类实现生成一道符合年级要求的选择题含题干、4选项、正确答案 |
#### FileIOService
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ----------------------- | --------------------------- | ------------- | ------------------------------------------------------------ |
| saveUser | User user | void | 将用户序列化为 JSON保存到 data/users/{BCrypt.hash(email).substring(0,16)}.json |
| loadUserByEmail | String email | User | 遍历 data/users/ 下所有 JSON 文件,反序列化后匹配邮箱,返回 User 或 null |
| saveCode | String email, String code | void | 写入 data/temp_codes/{email}.txt |
| loadCode | String email | String | 读取 data/temp_codes/{email}.txt返回注册码或 null |
| loadUsedQuestionStems | String email | Set<String> | 遍历 data/users/{email}/papers/(若存在)下所有 .txt 文件提取题干每行以“1.”、“2.”开头的内容),用于查重 |
#### QuizService
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ------------------- | ---------------------------------------------------------- | ---------------------- | ------------------------------------------------------------ |
| generateQuestions | String email, int count | List<ChoiceQuestion> | 调用 QuestionGenerator.generateOne() 循环生成,确保题干不重复(使用 loadUsedQuestionStems + 当前卷子 Set |
| calculateScore | List<ChoiceQuestion> questions, List<String> userAnswers | int | 比对每题 correctAnswer 与用户答案,计算百分比得分(四舍五入) |
| savePaper | String email, List<ChoiceQuestion> questions | void | 生成时间戳文件名yyyy-MM-dd-HH-mm-ss.txt保存题干到 data/users/{email}/papers/(目录自动创建) |
### UI层接口设计
#### MainWindow
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ---------------------- | --------------------------------------------------------- | ------ | ------------------------------------------------- |
| showPanel | Pane panel | void | 设置 BorderPane.center 为指定面板,实现页面切换 |
| showLoginPanel | — | void | 创建并显示 LoginPanel |
| showRegisterPanel | — | void | 创建并显示 RegisterPanel |
| showGradeSelectPanel | — | void | 创建并显示 GradeSelectPanel |
| showQuizPanel | List<ChoiceQuestion> questions, QuizService quizService | void | 创建并显示 QuizPanel |
| showResultPanel | int score, Runnable onContinue | void | 创建并显示 ResultPanel |
#### RegisterPanel
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ---------------- | ----------------------- | ------ | ------------------------------------------------------------ |
| 构造函数 | MainWindow mainWindow | — | 初始化邮箱、注册码、密码输入框和按钮,绑定事件 |
| sendCodeAction | — | void | 调用 mainWindow.getUserService().sendRegistrationCode(),提示“注册码已发送(模拟)” |
| registerAction | — | void | 调用 setPassword(),成功则跳转到年级选择页 |
#### LoginPanel
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ------------- | ----------------------- | ------ | ----------------------------------------------------------- |
| 构造函数 | MainWindow mainWindow | — | 初始化登录表单,绑定“登录”按钮 |
| loginAction | — | void | 调用 login(),成功则设置 currentUser 并跳转到年级选择页 |
#### GradeSelectPanel
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ----------- | ----------------------- | ------ | ------------------------------------------------------------ |
| 构造函数 | MainWindow mainWindow | — | 创建三个年级按钮 |
| startQuiz | Grade grade | void | 弹出数量输入对话框1030调用 QuizService.generateQuestions(),跳转到答题页 |
#### QuizPanel
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| -------------- | ------------------------------------------------------------ | ------ | ------------------------------------------------------------ |
| 构造函数 | MainWindow mainWindow, List<ChoiceQuestion> questions, QuizService quizService | — | 显示第1题 |
| showQuestion | int index | void | 渲染当前题干和4个选项RadioButton绑定“提交”按钮 |
| submitAction | — | void | 记录答案,若非最后一题则显示下一题,否则计算分数并跳转结果页 |
#### ResultPanel
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ---------------- | ------------------------------------------------------- | ------ | ----------------------------------------------------- |
| 构造函数 | MainWindow mainWindow, int score, Runnable onContinue | — | 显示得分,绑定“退出”和“继续做题”按钮 |
| exitAction | — | void | 跳转到登录页 |
| continueAction | — | void | 执行 onContinue.run()(保存试卷),跳转到年级选择页 |

@ -0,0 +1,189 @@
# 数学题库生成系统详细设计文档
## 项目概述
### 项目目标
为小初高学生提供一个本地的桌面端图像化应用,支持用户注册和登录,根据学生年级按规则随机生成数学选择题,完成答题后自动计算分数
### 技术栈
| 类型 | 技术 | 说明 |
| -------- | -------------------------- | ---------------------------------------- |
| 开发语言 | 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/
├── src/
│ └── main/
│ └── java/
│ └── com/
│ └── mathquiz/
│ ├── Main.java # 程序入口
│ │
│ ├── model/ # 数据模型
│ │ ├── User.java
│ │ ├── Grade.java
│ │ └── ChoiceQuestion.java
│ │
│ ├── service/ # 后端逻辑无GUI
│ │ ├── UserService.java
│ │ ├── QuestionGenerator.java
│ │ ├── FileIOService.java
│ │ └── QuizService.java
│ │
│ ├── ui/ # 前端 GUI
│ │ ├── MainWindow.java
│ │ ├── RegisterPanel.java
│ │ ├── LoginPanel.java
| | ├── PasswordModifyPanel.java
│ │ ├── GradeSelectPanel.java
│ │ ├── QuizPanel.java
│ │ └── ResultPanel.java
│ │
│ └── util/ # 工具类
│ ├── PasswordValidator.java
| ├── EmailUtil.java
│ ├── RandomUtils.java
│ └── FileUtils.java
├── data/ # 运行时生成(不提交)
│ ├── users/ # 用户信息 JSON
│ └── temp_codes/ # 临时注册码文件
├── pom.xml # Maven 依赖(含 JavaFX
└── README.md
## 详细模块设计
### 模型层设计
#### User类
- String name // 用户ID
- String email // 用户邮箱
- String encryptedPwd // 加密后的用户密码
- Grade grade //学段
#### ChoiceQuestion
String questionId; // 题目唯一IDUUID生成
Grade grade; // 所属学段
String questionContent; // 题干
List<String> options; // 选项列表固定4个顺序随机
String correctAnswer; // 正确答案(如"A"/"B"/"C"/"D"
#### Grade 枚举
PRIMARY("小学", 1)
JUNIOR("初中", 2)
SENIOR("高中", 3)
### 工具层设计
#### PasswordValidator
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ------- | ---------------------------------- | ------- | ------------------------------------------------------------ |
| isValid | String rawPwd | boolean | 校验规则1. 长度 6-10 位2. 包含至少 1 个大写字母3. 包含至少 1 个小写字母4. 包含至少 1 个数字 |
| encrypt | String rawPwd | String | 使用 BCrypt 加密密码(自动生成盐值,无需额外存储盐) |
| matches | String rawPwd, String encryptedPwd | boolean | 校验明文密码与加密密码是否匹配BCrypt 自带校验逻辑) |
#### EmailUtil
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ------------- | ----------------------------------------- | ------- | ------------------------------------------------------------ |
| sendTextEmail | String to, String subject, String content | boolean | 1. 配置 SMTP 服务器主机、端口、SSL2. 设置发件人账号 / 授权码3. 构建邮件内容4. 发送并返回结果 |
### 服务层设计
#### UserService
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| -------------------- | ----------------------------------------------------------- | ------- | ------------------------------------------------------------ |
| sendRegistrationCode | String email | boolean | 生成 6 位数字注册码,调用 EmailUtil.sendTextEmail()发送(开发阶段可模拟),并将注册码写入 data/temp_codes/{email}.txt |
| verifyCode | String email, String code | boolean | 读取 data/temp_codes/{email}.txt校验注册码是否匹配 |
| setPassword | String email, String pwd1, String pwd2 | boolean | 校验两次密码一致且符合规则610位含大小写+数字),使用 BCrypt加密后保存用户到 data/users/{hash}.json成功后删除临时注册码文件 |
| login | String email, String password | User | 读取用户文件,用 BCrypt.matches() 验证密码,返回 User 对象或 null |
| changePassword | String email, String oldPwd, String newPwd1, String newPwd2 | boolean | 先验证原密码正确,再校验新密码格式,更新加密密码并保存 |
#### QuestionGenerator (抽象类)
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ------------- | ------------- | ------------------- | ------------------------------------------------------------ |
| create | Grade grade | QuestionGenerator | 静态工厂方法根据年级返回对应子类实例PrimaryGenerator / MiddleGenerator / SeniorGenerator |
| generateOne | — | ChoiceQuestion | 抽象方法由子类实现生成一道符合年级要求的选择题含题干、4选项、正确答案 |
#### FileIOService
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ----------------------- | --------------------------- | ------------- | ------------------------------------------------------------ |
| saveUser | User user | void | 将用户序列化为 JSON保存到 data/users/{BCrypt.hash(email).substring(0,16)}.json |
| loadUserByEmail | String email | User | 遍历 data/users/ 下所有 JSON 文件,反序列化后匹配邮箱,返回 User 或 null |
| saveCode | String email, String code | void | 写入 data/temp_codes/{email}.txt |
| loadCode | String email | String | 读取 data/temp_codes/{email}.txt返回注册码或 null |
| loadUsedQuestionStems | String email | Set<String> | 遍历 data/users/{email}/papers/(若存在)下所有 .txt 文件提取题干每行以“1.”、“2.”开头的内容),用于查重 |
#### QuizService
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ------------------- | ---------------------------------------------------------- | ---------------------- | ------------------------------------------------------------ |
| generateQuestions | String email, int count | List<ChoiceQuestion> | 调用 QuestionGenerator.generateOne() 循环生成,确保题干不重复(使用 loadUsedQuestionStems + 当前卷子 Set |
| calculateScore | List<ChoiceQuestion> questions, List<String> userAnswers | int | 比对每题 correctAnswer 与用户答案,计算百分比得分(四舍五入) |
| savePaper | String email, List<ChoiceQuestion> questions | void | 生成时间戳文件名yyyy-MM-dd-HH-mm-ss.txt保存题干到 data/users/{email}/papers/(目录自动创建) |
### UI层接口设计
#### MainWindow
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ---------------------- | --------------------------------------------------------- | ------ | ------------------------------------------------- |
| showPanel | Pane panel | void | 设置 BorderPane.center 为指定面板,实现页面切换 |
| showLoginPanel | — | void | 创建并显示 LoginPanel |
| showRegisterPanel | — | void | 创建并显示 RegisterPanel |
| showGradeSelectPanel | — | void | 创建并显示 GradeSelectPanel |
| showQuizPanel | List<ChoiceQuestion> questions, QuizService quizService | void | 创建并显示 QuizPanel |
| showResultPanel | int score, Runnable onContinue | void | 创建并显示 ResultPanel |
#### RegisterPanel
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ---------------- | ----------------------- | ------ | ------------------------------------------------------------ |
| 构造函数 | MainWindow mainWindow | — | 初始化邮箱、注册码、密码输入框和按钮,绑定事件 |
| sendCodeAction | — | void | 调用 mainWindow.getUserService().sendRegistrationCode(),提示“注册码已发送(模拟)” |
| registerAction | — | void | 调用 setPassword(),成功则跳转到年级选择页 |
#### LoginPanel
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ------------- | ----------------------- | ------ | ----------------------------------------------------------- |
| 构造函数 | MainWindow mainWindow | — | 初始化登录表单,绑定“登录”按钮 |
| loginAction | — | void | 调用 login(),成功则设置 currentUser 并跳转到年级选择页 |
#### GradeSelectPanel
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ----------- | ----------------------- | ------ | ------------------------------------------------------------ |
| 构造函数 | MainWindow mainWindow | — | 创建三个年级按钮 |
| startQuiz | Grade grade | void | 弹出数量输入对话框1030调用 QuizService.generateQuestions(),跳转到答题页 |
#### QuizPanel
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| -------------- | ------------------------------------------------------------ | ------ | ------------------------------------------------------------ |
| 构造函数 | MainWindow mainWindow, List<ChoiceQuestion> questions, QuizService quizService | — | 显示第1题 |
| showQuestion | int index | void | 渲染当前题干和4个选项RadioButton绑定“提交”按钮 |
| submitAction | — | void | 记录答案,若非最后一题则显示下一题,否则计算分数并跳转结果页 |
#### ResultPanel
| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
| ---------------- | ------------------------------------------------------- | ------ | ----------------------------------------------------- |
| 构造函数 | MainWindow mainWindow, int score, Runnable onContinue | — | 显示得分,绑定“退出”和“继续做题”按钮 |
| exitAction | — | void | 跳转到登录页 |
| continueAction | — | void | 执行 onContinue.run()(保存试卷),跳转到年级选择页 |

@ -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层接口设计
####

@ -0,0 +1,74 @@
# 数学题库生成系统需求分析文档
## 项目概述
为小初高学生提供一个本地的桌面端图像化应用,支持用户注册和登录,根据学生年级按规则随机生成数学选择题,完成答题后自动计算分数
## 用户:
小学、初中和高中学生。
要求界面交互简洁直观、易于上手
## 功能需求:
### 用户注册与登录
#### 注册流程
- 用户输入邮箱地址,点击"获取注册码"后,软件发送邮箱注册码
- 用户输入邮箱里的注册码完成验证
- 验证通过后设置密码6-10位必须包含大小写字母和数字要重新确认密码
- 密码设置完成即完成注册,自动进入学段选择界面
#### 登录功能
- 已注册用户通过"邮箱 + 密码"或"用户名 + 密码"登录
- 登录成功后进入学段选择界面
#### 密码管理
- 登录状态下,用户可以发起修改密码操作
- 修改时需先输入旧密码然后输入两次新密码6-10位必须包含大小写字母和数字
### 学段选择
- 注册成功后,跳转到界面选择界面("小学","初中","高中",点击后可修改用户的学段
- 登录后,点击"切换学段"可跳转界面选择界面
### 试卷生成
- 用户输入题目数量10-30
- 系统根据用户选择的学段,生成对应难度的数字选择题试卷
- 同一用户不能生成重复题目
- 每个题目包含题干和4个选择包含1个正确题目
### 答题流程
- 试卷生成后,界面依次显示题目
- 每题显示题干和4个选项用户选择一个选项后点击"下一题",自动跳转至下一题
- 直到完成最后一题提高后,自动进入分数展示界面
### 分数计算与展示
- 分数计算Z = 答对/总题数 x 100%
- 显示内容:"您答对了X/Y题得分Z%"
- 分数界面提供两个操作选项:"继续答题"返回答题界面,"退出"返回登录界面
## 非功能需求
### 界面要求
- 除了输入题目数量,其他所有功能均通过图形化界面操作,界面简洁、直观,符合中小学生使用习惯
- 操作流程清晰,每个步骤有明确的引导提示("请输入注册码","密码格式错误","题目数量不合法"等)
### 运行环境
- 桌面应用程序支持Windows
### 数据储存约束
- 不使用数据库用户信息邮箱、用户名、密码、学段、注册码等数据通过json文件形式储存
- 需保证用户数据的安全性(如密码加密储存)
### 题目内容约束
- 小学:只能有+,-,*./和()
- 初中:题目中至少有一个平方或开根号的运算符
- 高中题目中至少有一个sin,cos或tan的运算符
## 业务流程说明
### 注册登录流程
新用户:输入邮箱->获取注册码->验证注册码->设置密码->完成注册->选择学段->登录
旧用户:输入邮箱(或用户码)和密码->登录
### 做题流程
输入题目数量->生成试卷->依次答题->完成最后一题->显示分数->选择"继续做题"或"退出"
### 密码修改流程
登录状态->发起修改密码->输入原密码->输入两次新密码->通过密码验证->密码更新

@ -0,0 +1,174 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mathquiz</groupId>
<artifactId>MathQuizApp</artifactId>
<version>1.04</version>
<packaging>jar</packaging>
<name>Math Quiz Application</name>
<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>
<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>
<!-- JavaMail API -->
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>1.6.2</version>
</dependency>
<!-- 新增JavaMail 在 Java 9+ 必需的依赖 -->
<dependency>
<groupId>com.sun.activation</groupId>
<artifactId>javax.activation</artifactId>
<version>1.2.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.1</version>
<configuration>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.6.1</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/dependencies</outputDirectory>
<overWriteReleases>false</overWriteReleases>
<overWriteSnapshots>false</overWriteSnapshots>
<overWriteIfNewer>true</overWriteIfNewer>
</configuration>
</execution>
</executions>
</plugin>
<!-- Maven Shade插件用于创建可执行JAR -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.pair.Test</mainClass>
</transformer>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
<finalName>MathQuizApp</finalName>
</configuration>
</execution>
</executions>
</plugin>
<!-- 支持 mvn javafx:run 启动的插件配置 -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<!-- 指定启动类全路径 -->
<mainClass>com.pair.Test</mainClass>
<!-- 传递JVM参数指定需要的JavaFX模块 -->
<arguments>
<argument>--add-modules</argument>
<argument>javafx.controls,javafx.fxml,javafx.graphics,javafx.base</argument>
</arguments>
</configuration>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,38 @@
package com.pair;// src/main/java/com/pair/Test.java
import com.pair.ui.MainWindow;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
public class Test extends Application {
@Override
public void start(Stage primaryStage) {
try {
System.out.println("✅ 正在初始化 MainWindow...");
MainWindow mainWindow = new MainWindow(primaryStage);
Scene scene = new Scene(mainWindow, 1366, 786);
primaryStage.setTitle("中小学数学答题系统");
primaryStage.setScene(scene);
primaryStage.setResizable(true);
primaryStage.show();
System.out.println("✅ 应用启动成功!");
} catch (Throwable e) { // 捕获所有错误,包括 NoClassDefFoundError
System.err.println("❌ 启动失败,异常信息:");
e.printStackTrace();
// 暂停 15 秒,防止窗口关闭
try {
Thread.sleep(15000);
} catch (InterruptedException ignored) {}
System.exit(1);
}
}
public static void main(String[] args) {
launch(args);
}
}

@ -0,0 +1,74 @@
package com.pair.model;
import java.util.List;
//选择题
public class ChoiceQuestion {
private String questionText; // 题目文本
private Object correctAnswer; // 正确答案
private List<?> options; // 选项列表
private Grade grade; // 所属学段
public ChoiceQuestion(String questionText, double correctAnswer,
List<Double> options, Grade grade) {
this.questionText = questionText;
this.correctAnswer = correctAnswer;
this.options = options;
this.grade = grade;
}
public ChoiceQuestion(String questionText, String correctAnswer,
List<String> options, Grade grade) {
this.questionText = questionText;
this.correctAnswer = correctAnswer;
this.options = options;
this.grade = grade;
}
public String getQuestionText() {
return questionText;
}
public void setQuestionText(String questionText) {
this.questionText = questionText;
}
public Object getCorrectAnswer() {
return correctAnswer;
}
public void setCorrectAnswer(Object correctAnswer) {
this.correctAnswer = correctAnswer;
}
public List<?> getOptions() {
return options;
}
public void setOptions(List<?> options) {
this.options = options;
}
public Grade getGrade() {
return grade;
}
public void setGrade(Grade grade) {
this.grade = grade;
}
@Override
public String toString() {
return "ChoiceQuestion{" +
"questionText='" + questionText + '\'' +
", correctAnswer=" + correctAnswer +
", options=" + options +
", grade=" + grade +
'}';
}
}

@ -0,0 +1,29 @@
package com.pair.model;
public enum Grade {
// 枚举常量,初始化时传入对应的中文描述
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);
}
}

@ -0,0 +1,79 @@
package com.pair.model;
import java.util.Date;
import java.util.List;
//答题历史记录模型
public class QuizHistory {
private String username; // 用户名
private Date timestamp; // 答题时间
private List<ChoiceQuestion> questions; // 题目列表
private List<Integer> userAnswers; // 用户答案列表
private int score; // 得分
public QuizHistory(String username, Date timestamp,
List<ChoiceQuestion> questions,
List<Integer> userAnswers,
int score) {
this.username = username;
this.timestamp = timestamp;
this.questions = questions;
this.userAnswers = userAnswers;
this.score = score;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Date getTimestamp() {
return timestamp;
}
public void setTimestamp(Date timestamp) {
this.timestamp = timestamp;
}
public List<ChoiceQuestion> getQuestions() {
return questions;
}
public void setQuestions(List<ChoiceQuestion> questions) {
this.questions = questions;
}
public List<Integer> getUserAnswers() {
return userAnswers;
}
public void setUserAnswers(List<Integer> userAnswers) {
this.userAnswers = userAnswers;
}
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
@Override
public String toString() {
return "QuizHistory{" +
"username='" + username + '\'' +
", timestamp=" + timestamp +
", questions=" + (questions != null ? questions.size() : 0) +
", score=" + score +
'}';
}
}

@ -0,0 +1,58 @@
package com.pair.model;
//答题结果
public class QuizResult {
private int totalQuestions; // 总题数
private int correctCount; // 正确题数
private int wrongCount; // 错误题数
private int score; // 得分
public QuizResult(int totalQuestions, int correctCount, int wrongCount, int score) {
this.totalQuestions = totalQuestions;
this.correctCount = correctCount;
this.wrongCount = wrongCount;
this.score = score;
}
public int getTotalQuestions() {
return totalQuestions;
}
public void setTotalQuestions(int totalQuestions) {
this.totalQuestions = totalQuestions;
}
public int getCorrectCount() {
return correctCount;
}
public void setCorrectCount(int correctCount) {
this.correctCount = correctCount;
}
public int getWrongCount() {
return wrongCount;
}
public void setWrongCount(int wrongCount) {
this.wrongCount = wrongCount;
}
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
@Override
public String toString() {
int correctPercent = (int) ((double) correctCount / totalQuestions * 100);
return "您答对了" + correctCount + "/" + totalQuestions + "题,得分:" + correctPercent;
}
}

@ -0,0 +1,121 @@
package com.pair.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; // 邮箱
private Grade grade; // 学段
private int totalQuizzes; // 总答题次数
private double averageScore; // 平均分
private Date registrationDate; // 注册时间
/**
*
*/
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;
this.grade = grade;
this.totalQuizzes = totalQuizzes;
this.averageScore = averageScore;
this.registrationDate = registrationDate;
}
/**
*
*/
public User(String username, String password, String email, Grade grade) {
this.userId = UUID.randomUUID().toString();
this.username = username;
this.password = password;
this.email = email;
this.grade = grade;
this.totalQuizzes = 0;
this.averageScore = 0.0;
this.registrationDate = new Date();
}
public String getUserId() {
return userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username){
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Grade getGrade() {
return grade;
}
public void setGrade(Grade grade) {
this.grade = grade;
}
public int getTotalQuizzes() {
return totalQuizzes;
}
public void setTotalQuizzes(int totalQuizzes) {
this.totalQuizzes = totalQuizzes;
}
public double getAverageScore() {
return averageScore;
}
public void setAverageScore(double averageScore) {
this.averageScore = averageScore;
}
public Date getRegistrationDate() {
return registrationDate;
}
public void setRegistrationDate(Date registrationDate) {
this.registrationDate = registrationDate;
}
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", email='" + email + '\'' +
", grade=" + grade +
", totalQuizzes=" + totalQuizzes +
", averageScore=" + String.format("%.1f", averageScore) +
", registrationDate=" + registrationDate +
'}';
}
}

@ -0,0 +1,213 @@
package com.pair.service;
import com.pair.model.*;
import com.pair.util.AppDataDirectory;
import com.pair.util.FileUtils;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* IO
*
*/
public class FileIOService {
private static final String DATA_DIR = AppDataDirectory.getFullPath("data");
private static final String USERS_DIR = AppDataDirectory.getFullPath("data/users");
private static final String HISTORY_DIR = AppDataDirectory.getFullPath("data/history");
private static final String REGISTRATION_CODES_FILE = AppDataDirectory.getFullPath("data/registration_codes.json");
private static final String USERS_FILE = AppDataDirectory.getFullPath("data/users.json");
private static final String CURRENT_USER_FILE = AppDataDirectory.getFullPath("data/current_user.json");
private static final Gson gson = new GsonBuilder()
.setPrettyPrinting()
.setDateFormat("yyyy-MM-dd HH:mm:ss")
.create();
// ==================== 初始化 ====================
public FileIOService() throws IOException {
initDataDirectory();
}
public void initDataDirectory() throws IOException {
FileUtils.createDirectoryIfNotExists(DATA_DIR);
FileUtils.createDirectoryIfNotExists(USERS_DIR);
FileUtils.createDirectoryIfNotExists(HISTORY_DIR);
FileUtils.ensureFileExists(REGISTRATION_CODES_FILE);
if (!FileUtils.exists(USERS_FILE)) {
Map<String, List<User>> data = new HashMap<>();
data.put("users", new ArrayList<>());
FileUtils.saveAsJson(data, USERS_FILE);
}
System.out.println("✓ 数据目录初始化完成");
}
// ==================== 用户操作 ====================
public void saveUser(User user) throws IOException {
Type type = new TypeToken<Map<String, List<User>>>(){}.getType();
Map<String, List<User>> data = FileUtils.readJsonToObject(USERS_FILE, type);
List<User> users = data.get("users");
boolean found = false;
for (int i = 0; i < users.size(); i++) {
if (users.get(i).getUserId().equals(user.getUserId())) {
users.set(i, user);
found = true;
break;
}
}
if (!found) {
users.add(user);
}
FileUtils.saveAsJson(data, USERS_FILE);
}
public List<User> loadAllUsers() throws IOException {
if (!FileUtils.exists(USERS_FILE)) {
return new ArrayList<>();
}
Type type = new TypeToken<Map<String, List<User>>>(){}.getType();
Map<String, List<User>> data = FileUtils.readJsonToObject(USERS_FILE, type);
return data.get("users");
}
public User findUserByUsername(String username) throws IOException {
List<User> users = loadAllUsers();
for (User user : users) {
if (user.getUsername().equals(username)) {
return user;
}
}
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);
}
public User loadCurrentUser() throws IOException {
if (!FileUtils.exists(CURRENT_USER_FILE)) {
return null;
}
return FileUtils.readJsonToObject(CURRENT_USER_FILE, User.class);
}
public void clearCurrentUser() {
FileUtils.deleteFile(CURRENT_USER_FILE);
}
public List<String> getHistoryQuestions() throws IOException {
List<String> historyQuestions = new ArrayList<>();
File[] files = FileUtils.listFiles(HISTORY_DIR);
Arrays.sort(files, (f1, f2) -> Long.compare(f2.lastModified(), f1.lastModified()));
int count = 0;
for (File file : files) {
if (count++ >= 20) break;
try {
String content = FileUtils.readFileToString(file.getAbsolutePath());
String[] lines = content.split("\n");
for (int i = 0; i < lines.length; i++) {
if (lines[i].startsWith("【题目")) {
if (i + 1 < lines.length) {
String questionText = lines[i + 1].trim();
if (!questionText.isEmpty()) {
historyQuestions.add(questionText);
}
}
}
}
} catch (IOException e) {
System.err.println("读取历史文件失败: " + file.getName());
}
}
return historyQuestions;
}
// ==================== 业务逻辑方法(从 Model 移过来)====================
/**
*
*/
private int calculateCorrectCount(QuizHistory history) {
int count = 0;
List<ChoiceQuestion> questions = history.getQuestions();
List<Integer> userAnswers = history.getUserAnswers();
for (int i = 0; i < questions.size(); i++) {
ChoiceQuestion question = questions.get(i);
Integer userAnswer = userAnswers.get(i);
if (userAnswer != null && userAnswer == getCorrectAnswerIndex(question)) {
count++;
}
}
return count;
}
/**
*
*/
private int getCorrectAnswerIndex(ChoiceQuestion question) {
return question.getOptions().indexOf(question.getCorrectAnswer());
}
// ==================== 工具方法 ====================
private String sanitizeFilename(String filename) {
if (filename == null) {
return "unknown";
}
return filename.replaceAll("[\\\\/:*?\"<>|]", "_");
}
public String getRegistrationCodesFilePath() {
return REGISTRATION_CODES_FILE;
}
}

@ -0,0 +1,427 @@
package com.pair.service;
import com.pair.model.*;
import com.pair.service.question_generator.QuestionFactoryManager;
import java.io.IOException;
import java.util.*;
/**
*
*/
public class QuizService {
private final FileIOService fileIOService;
private final UserService userService;
private List<ChoiceQuestion> currentQuestions;
private List<Integer> userAnswers;
private int currentQuestionIndex;
private int answerNumber;
// ==================== 构造方法 ====================
public QuizService() throws IOException {
this.fileIOService = new FileIOService();
this.userService = new UserService(fileIOService);
this.currentQuestions = new ArrayList<>();
this.userAnswers = new ArrayList<>();
this.currentQuestionIndex = 0;
}
public QuizService(FileIOService fileIOService, UserService userService) {
this.fileIOService = fileIOService;
this.userService = userService;
this.currentQuestions = new ArrayList<>();
this.userAnswers = new ArrayList<>();
this.currentQuestionIndex = 0;
}
// ==================== 答题会话管理 ====================
//开始生成
public void startNewQuiz(User user, int questionCount) throws IOException {
currentQuestions.clear();
userAnswers.clear();
currentQuestionIndex = 0;
Set<String> historyQuestions = getRecentHistoryQuestions();
Grade grade = user.getGrade();
currentQuestions = QuestionFactoryManager.generateQuestions(
grade, questionCount, historyQuestions
);
for (int i = 0; i < currentQuestions.size(); i++) {
userAnswers.add(null);
}
System.out.println("✓ 已生成 " + currentQuestions.size() + " 道 " + grade + " 题目");
}
private Set<String> getRecentHistoryQuestions() throws IOException {
List<String> historyList = fileIOService.getHistoryQuestions();
return new HashSet<>(historyList);
}
// ==================== 题目访问 ====================
public ChoiceQuestion getCurrentQuestion() {
if (currentQuestionIndex >= 0 && currentQuestionIndex < currentQuestions.size()) {
return currentQuestions.get(currentQuestionIndex);
}
return null;
}
public ChoiceQuestion getQuestion(int index) {
if (index >= 0 && index < currentQuestions.size()) {
return currentQuestions.get(index);
}
return null;
}
public List<ChoiceQuestion> getAllQuestions() {
return new ArrayList<>(currentQuestions);
}
public boolean nextQuestion() {
if (currentQuestionIndex < currentQuestions.size() - 1) {
currentQuestionIndex++;
return true;
}
return false;
}
public boolean previousQuestion() {
if (currentQuestionIndex > 0) {
currentQuestionIndex--;
return true;
}
return false;
}
public boolean goToQuestion(int index) {
if (index >= 0 && index < currentQuestions.size()) {
currentQuestionIndex = index;
return true;
}
return false;
}
public int getCurrentQuestionIndex() {
return currentQuestionIndex;
}
public int getTotalQuestions() {
return currentQuestions.size();
}
public boolean isFirstQuestion() {
return currentQuestionIndex == 0;
}
public boolean isLastQuestion() {
return currentQuestionIndex == currentQuestions.size() - 1;
}
// ==================== 答题操作 ====================
public boolean submitAnswer(int questionIndex, int optionIndex) {
if (questionIndex < 0 || questionIndex >= currentQuestions.size()) {
throw new IllegalArgumentException("题目索引无效: " + questionIndex);
}
ChoiceQuestion question = currentQuestions.get(questionIndex);
if (optionIndex < 0 || optionIndex >= question.getOptions().size()) {
throw new IllegalArgumentException("选项索引无效: " + optionIndex);
}
userAnswers.set(questionIndex, optionIndex);
return checkAnswer(question, optionIndex);
}
public boolean submitCurrentAnswer(int optionIndex) {
return submitAnswer(currentQuestionIndex, optionIndex);
}
public Integer getUserAnswer(int questionIndex) {
if (questionIndex >= 0 && questionIndex < userAnswers.size()) {
return userAnswers.get(questionIndex);
}
return null;
}
public List<Integer> getAllUserAnswers() {
return new ArrayList<>(userAnswers);
}
public boolean isAllAnswered() {
for (Integer answer : userAnswers) {
if (answer == null) {
return false;
}
}
return true;
}
public int getAnsweredCount() {
int count = 0;
for (Integer answer : userAnswers) {
if (answer != null) {
count++;
}
}
return count;
}
public boolean isAnswered(int questionIndex) {
return userAnswers.get(questionIndex) != null ;
}
// ==================== 成绩计算 ====================
public QuizResult calculateResult() {
int correctCount = 0;
int totalQuestions = currentQuestions.size();
for (int i = 0; i < totalQuestions; i++) {
ChoiceQuestion question = currentQuestions.get(i);
Integer userAnswer = userAnswers.get(i);
if (userAnswer != null && checkAnswer(question, userAnswer)) {
correctCount++;
}
}
int wrongCount = totalQuestions - correctCount;
int score = totalQuestions > 0 ? (correctCount * 100) / totalQuestions : 0;
return new QuizResult(totalQuestions, correctCount, wrongCount, score);
}
public List<Integer> getCorrectQuestionIndices() {
List<Integer> correctIndices = new ArrayList<>();
for (int i = 0; i < currentQuestions.size(); i++) {
ChoiceQuestion question = currentQuestions.get(i);
Integer userAnswer = userAnswers.get(i);
if (userAnswer != null && checkAnswer(question, userAnswer)) {
correctIndices.add(i);
}
}
return correctIndices;
}
public List<Integer> getWrongQuestionIndices() {
List<Integer> wrongIndices = new ArrayList<>();
for (int i = 0; i < currentQuestions.size(); i++) {
ChoiceQuestion question = currentQuestions.get(i);
Integer userAnswer = userAnswers.get(i);
if (userAnswer != null && !checkAnswer(question, userAnswer)) {
wrongIndices.add(i);
}
}
return wrongIndices;
}
public List<Integer> getUnansweredQuestionIndices() {
List<Integer> unansweredIndices = new ArrayList<>();
for (int i = 0; i < userAnswers.size(); i++) {
if (userAnswers.get(i) == null) {
unansweredIndices.add(i);
}
}
return unansweredIndices;
}
// ==================== 业务逻辑方法(从 Model 移过来)====================
/**
*
*/
public boolean checkAnswer(ChoiceQuestion question, int userAnswerIndex) {
if (userAnswerIndex < 0 || userAnswerIndex >= question.getOptions().size()) {
return false;
}
Object userAnswer = question.getOptions().get(userAnswerIndex);
return question.getCorrectAnswer().equals(userAnswer);
}
/**
*
*/
public int getCorrectAnswerIndex(ChoiceQuestion question) {
return question.getOptions().indexOf(question.getCorrectAnswer());
}
/**
*
*/
public String getCorrectAnswerLetter(ChoiceQuestion question) {
int index = getCorrectAnswerIndex(question);
if (index >= 0 && index < 4) {
return String.valueOf((char)('A' + index));
}
return "未知";
}
/**
*
*/
public double getAccuracy(QuizResult result) {
if (result.getTotalQuestions() == 0) {
return 0.0;
}
return (result.getCorrectCount() * 100.0) / result.getTotalQuestions();
}
/**
*
*/
public boolean isPassed(QuizResult result) {
return result.getScore() >= 60;
}
/**
*
*/
public String getGrade(QuizResult result) {
int score = result.getScore();
if (score >= 90) return "优秀";
if (score >= 80) return "良好";
if (score >= 70) return "中等";
if (score >= 60) return "及格";
return "不及格";
}
/**
*
*/
public int getCorrectCount(QuizHistory history) {
int count = 0;
List<ChoiceQuestion> questions = history.getQuestions();
List<Integer> userAnswers = history.getUserAnswers();
for (int i = 0; i < questions.size(); i++) {
ChoiceQuestion question = questions.get(i);
Integer userAnswer = userAnswers.get(i);
if (userAnswer != null && checkAnswer(question, userAnswer)) {
count++;
}
}
return count;
}
/**
*
*/
public int getWrongCount(QuizHistory history) {
return history.getQuestions().size() - getCorrectCount(history);
}
// ==================== 格式化输出 ====================
public String formatQuestion(ChoiceQuestion question) {
StringBuilder sb = new StringBuilder();
sb.append(question.getQuestionText()).append("\n");
List<?> options = question.getOptions();
for (int i = 0; i < options.size(); i++) {
sb.append((char)('A' + i)).append(". ").append(options.get(i));
if (i % 2 == 1) {
sb.append("\n");
} else {
sb.append(" ");
}
}
return sb.toString();
}
public String formatCurrentQuestion() {
ChoiceQuestion question = getCurrentQuestion();
if (question == null) {
return "没有可用的题目";
}
StringBuilder sb = new StringBuilder();
sb.append("第 ").append(currentQuestionIndex + 1)
.append(" / ").append(currentQuestions.size()).append(" 题\n");
sb.append(formatQuestion(question));
return sb.toString();
}
public String formatQuestionWithAnswer(ChoiceQuestion question, Integer userAnswerIndex) {
StringBuilder sb = new StringBuilder();
sb.append(question.getQuestionText()).append("\n");
List<?> options = question.getOptions();
int correctIndex = getCorrectAnswerIndex(question);
for (int i = 0; i < options.size(); i++) {
sb.append((char)('A' + i)).append(". ").append(options.get(i));
if (i == correctIndex) {
sb.append(" ✓");
}
if (userAnswerIndex != null && i == userAnswerIndex) {
boolean isCorrect = checkAnswer(question, userAnswerIndex);
sb.append(isCorrect ? " [您的答案:正确]" : " [您的答案:错误]");
}
if (i % 2 == 1) {
sb.append("\n");
} else {
sb.append(" ");
}
}
return sb.toString();
}
public String formatResult(QuizResult result) {
StringBuilder sb = new StringBuilder();
sb.append("\n========== 答题结束 ==========\n");
sb.append("总题数:").append(result.getTotalQuestions()).append(" 题\n");
sb.append("正确:").append(result.getCorrectCount()).append(" 题\n");
sb.append("错误:").append(result.getWrongCount()).append(" 题\n");
sb.append("得分:").append(result.getScore()).append(" 分\n");
sb.append("正确率:").append(String.format("%.1f%%", getAccuracy(result))).append("\n");
sb.append("评级:").append(getGrade(result)).append("\n");
sb.append("===============================\n");
return sb.toString();
}
// ==================== Getters ====================
public int getAnswerNumber() {
return answerNumber;
}
public void setAnswerNumber(int answerNumber) {
this.answerNumber = answerNumber;
}
public List<ChoiceQuestion> getCurrentQuestions() {
return new ArrayList<>(currentQuestions);
}
public List<Integer> getUserAnswers() {
return new ArrayList<>(userAnswers);
}
}

@ -0,0 +1,517 @@
package com.pair.service;
import com.pair.model.Grade;
import com.pair.model.User;
import com.pair.util.EmailUtil;
import com.pair.util.FileUtils;
import com.pair.util.PasswordValidator;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.pair.util.EmailUtil.validateEmail;
/**
*
*/
public class UserService {
private final FileIOService fileIOService;
private User currentUser;
// ==================== 构造方法 ====================
public UserService() throws IOException {
this.fileIOService = new FileIOService();
this.currentUser = null;
}
public UserService(FileIOService fileIOService) {
this.fileIOService = fileIOService;
this.currentUser = null;
}
// 注册码有效期(毫秒)
private static final long CODE_EXPIRY_TIME = 10 * 60 * 1000; // 10分钟
// ==================== 注册码管理 ====================
/**
*
*
* @param email
* @return
*/
public String generateRegistrationCode(String email) throws IOException {
if (!validateEmail(email)) {
throw new IllegalArgumentException("邮箱格式错误!");
}
// 生成6位注册码
String code = PasswordValidator.generateRegistrationCode();
long expiryTime = System.currentTimeMillis() + CODE_EXPIRY_TIME;
// 保存到文件
System.out.println(expiryTime);
saveRegistrationCodeToFile(email, code, expiryTime);
//发送注册码邮件
boolean isEmailSent = EmailUtil.sendRegistrationCode(email, code);
if (!isEmailSent) {
throw new IllegalArgumentException("邮箱发送失败,请重试");
}
// 打印注册码(实际项目中可以发邮件)
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("========================================");
System.out.println("【注册码】");
System.out.println("邮箱: " + email);
System.out.println("注册码: " + code);
System.out.println("过期时间: " + sdf.format(new Date(expiryTime)));
System.out.println("========================================");
return code;
}
/**
*
*/
private void saveRegistrationCodeToFile(String email, String code, long expiryTime) throws IOException {
// 读取现有的注册码
Map<String, RegistrationCode> codes = loadRegistrationCodesFromFile();
// 添加或更新
codes.put(email, new RegistrationCode(code, expiryTime));
// 保存到文件
StringBuilder content = new StringBuilder();
content.append("# 注册码记录文件\n");
content.append("# 格式: 邮箱|注册码|过期时间戳\n");
content.append("# 过期时间格式: yyyy-MM-dd HH:mm:ss\n\n");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (Map.Entry<String, RegistrationCode> entry : codes.entrySet()) {
String emailKey = entry.getKey();
RegistrationCode regCode = entry.getValue();
content.append(emailKey).append("|")
.append(regCode.code).append("|")
.append(regCode.expiryTime).append("|")
.append(sdf.format(new Date(regCode.expiryTime)))
.append("\n");
}
FileUtils.writeStringToFile(fileIOService.getRegistrationCodesFilePath(), content.toString());
}
/**
*
*/
private void initializeDataDirectory() {
try {
File dataDir = new File("data");
if (!dataDir.exists()) {
dataDir.mkdirs();
System.out.println("✓ 已创建 data 目录");
}
// 确保注册码文件存在(即使是空的也创建)
File codesFile = new File(fileIOService.getRegistrationCodesFilePath());
if (!codesFile.exists()) {
StringBuilder initialContent = new StringBuilder();
initialContent.append("# 注册码记录文件\n");
initialContent.append("# 格式: 邮箱|注册码|过期时间戳|过期时间\n\n");
FileUtils.writeStringToFile(fileIOService.getRegistrationCodesFilePath(), initialContent.toString());
System.out.println("✓ 已创建注册码文件");
}
} catch (IOException e) {
System.err.println(" 初始化数据目录失败: " + e.getMessage());
}
}
/**
*
*/
private Map<String, RegistrationCode> loadRegistrationCodesFromFile() throws IOException {
Map<String, RegistrationCode> codes = new HashMap<>();
System.out.println(fileIOService.getRegistrationCodesFilePath());
if (!FileUtils.exists(fileIOService.getRegistrationCodesFilePath())) {
throw new IOException("目录不存在");
}
String content = FileUtils.readFileToString(fileIOService.getRegistrationCodesFilePath());
String[] lines = content.split("\n");
for (String line : lines) {
line = line.trim();
// 跳过注释和空行
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
String[] parts = line.split("\\|");
if (parts.length >= 3) {
String email = parts[0].trim();
String code = parts[1].trim();
long expiryTime = Long.parseLong(parts[2].trim());
codes.put(email, new RegistrationCode(code, expiryTime));
}
}
return codes;
}
/**
*
*
* @param email
* @param code
* @return true
*/
public boolean verifyRegistrationCode(String email, String code) throws IOException {
Map<String, RegistrationCode> codes = loadRegistrationCodesFromFile();
RegistrationCode regCode = codes.get(email);
if (regCode == null) {
throw new IllegalArgumentException("未找到该邮箱的注册码,请先获取注册码!");
}
// 检查是否过期
if (System.currentTimeMillis() > regCode.expiryTime) {
// 删除过期的注册码
codes.remove(email);
saveAllRegistrationCodes(codes);
throw new IllegalArgumentException("注册码已过期,请重新获取!");
}
// 验证注册码
boolean isValid = regCode.code.equals(code);
// 验证成功后删除注册码(一次性使用)
if (isValid) {
codes.remove(email);
saveAllRegistrationCodes(codes);
}
return isValid;
}
/**
*
*/
private void saveAllRegistrationCodes(Map<String, RegistrationCode> codes) throws IOException {
StringBuilder content = new StringBuilder();
content.append("# 注册码记录文件\n");
content.append("# 格式: 邮箱|注册码|过期时间戳|过期时间\n\n");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (Map.Entry<String, RegistrationCode> entry : codes.entrySet()) {
content.append(entry.getKey()).append("|")
.append(entry.getValue().code).append("|")
.append(entry.getValue().expiryTime).append("|")
.append(sdf.format(new Date(entry.getValue().expiryTime)))
.append("\n");
}
FileUtils.writeStringToFile(fileIOService.getRegistrationCodesFilePath(), content.toString());
}
/**
*
*/
public void cleanExpiredCodes() throws IOException {
Map<String, RegistrationCode> codes = loadRegistrationCodesFromFile();
long now = System.currentTimeMillis();
// 移除过期的
codes.entrySet().removeIf(entry -> now > entry.getValue().expiryTime);
// 保存回文件
saveAllRegistrationCodes(codes);
System.out.println("✓ 已清理过期的注册码");
}
// ==================== 注册码内部类 ====================
private static class RegistrationCode {
String code;
long expiryTime;
RegistrationCode(String code, long expiryTime) {
this.code = code;
this.expiryTime = expiryTime;
}
}
// ==================== 用户注册====================
/**
*
*/
public User register(String password, String email, String verificationCode) throws IOException {
// 1. 验证注册码
if (!verifyRegistrationCode(email, verificationCode)) {
throw new IllegalArgumentException("注册码错误!");
}
//2.验证邮箱格式
if (!validateEmail(email)) {
throw new IllegalArgumentException("邮箱格式错误!");
}
// 3. 验证用户名是否已存在
if (fileIOService.isEmailExists(email)) {
throw new IllegalArgumentException("邮箱已经注册!");
}
// 5. 验证密码格式
try {
PasswordValidator.validatePassword(password);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("密码格式错误!");
}
// 6. 初始化为小学学段
Grade grade = Grade.ELEMENTARY;
// 7. 加密密码
String hashedPassword = PasswordValidator.encrypt(password);
// 8. 创建用户对象
User user = new User(email, hashedPassword, email, grade);
this.setCurrentUser(user);
// 9. 保存到文件
fileIOService.saveUser(user);
return user;
}
public void setCurrentUser(User user) throws IOException {
this.currentUser = user;
fileIOService.saveCurrentUser(user);
}
// ==================== 用户登录 ====================
public User login(String username, String password) throws IOException {
User user;
if (EmailUtil.validateEmail(username)) {
user = fileIOService.findUserByEmail(username);
} else {
user = fileIOService.findUserByUsername(username);
}
if (user == null) {
throw new IllegalArgumentException("用户名或邮箱不存在!");
}
String hashedPassword = PasswordValidator.encrypt(password);
if (!user.getPassword().equals(hashedPassword)) {
throw new IllegalArgumentException("密码错误!");
}
this.currentUser = user;
fileIOService.saveCurrentUser(user);
// System.out.println("✓ 登录成功,欢迎 " + getRealName(user) + "" + getGradeDisplayName(user) + "");
return user;
}
public User autoLogin() throws IOException {
User user = fileIOService.loadCurrentUser();
if (user != null) {
this.currentUser = user;
// System.out.println("✓ 自动登录成功,欢迎回来 " + getRealName(user));
}
return user;
}
public void logout() {
if (currentUser != null) {
// System.out.println("✓ " + getRealName(currentUser) + " 已退出登录");
this.currentUser = null;
fileIOService.clearCurrentUser();
}
}
public User getCurrentUser() {
return currentUser;
}
public boolean isLoggedIn() {
return currentUser != null;
}
// ==================== 密码管理 ====================
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("旧密码错误!");
}
try {
PasswordValidator.validatePassword(newPassword);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage());
}
String hashedNewPassword = PasswordValidator.encrypt(newPassword);
user.setPassword(hashedNewPassword);
fileIOService.saveUser(user);
if (currentUser != null && currentUser.getUsername().equals(user.getUsername())) {
this.currentUser = user;
fileIOService.saveCurrentUser(user);
}
System.out.println("✓ 密码修改成功");
return true;
}
public boolean resetPassword(String username, String email, String newPassword) throws IOException {
User user = fileIOService.findUserByUsername(username);
if (user == null) {
throw new IllegalArgumentException("用户名不存在!");
}
if (!user.getEmail().equals(email)) {
throw new IllegalArgumentException("邮箱验证失败!");
}
try {
PasswordValidator.validatePassword(newPassword);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage());
}
String hashedNewPassword = PasswordValidator.encrypt(newPassword);
user.setPassword(hashedNewPassword);
fileIOService.saveUser(user);
System.out.println("✓ 密码重置成功");
return true;
}
// ==================== 用户信息管理 ====================
public boolean updateEmail(User user, String newEmail) throws IOException {
if (!validateEmail(newEmail)) {
throw new IllegalArgumentException("邮箱格式错误!");
}
user.setEmail(newEmail);
fileIOService.saveUser(user);
if (currentUser != null && currentUser.getUsername().equals(user.getUsername())) {
this.currentUser = user;
fileIOService.saveCurrentUser(user);
}
// 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();
int newTotal = oldTotal + 1;
double newAverage = (oldAverage * oldTotal + score) / newTotal;
user.setTotalQuizzes(newTotal);
user.setAverageScore(newAverage);
fileIOService.saveUser(user);
}
public List<User> getAllUsers() throws IOException {
return fileIOService.loadAllUsers();
}
public User findUser(String username) throws IOException {
return fileIOService.findUserByUsername(username);
}
public String getGradeDisplayName(User user) {
if (user == null || user.getGrade() == null) {
return "未知";
}
switch (user.getGrade()) {
case ELEMENTARY:
return "小学";
case MIDDLE:
return "初中";
case HIGH:
return "高中";
default:
return "未知";
}
}
/**
*
*/
public String getUserStatistics(User user) {
StringBuilder sb = new StringBuilder();
sb.append("========== 用户统计 ==========\n");
sb.append("用户名:").append(user.getUsername()).append("\n");
sb.append("学段:").append(getGradeDisplayName(user)).append("\n");
sb.append("邮箱:").append(user.getEmail()).append("\n");
sb.append("总答题次数:").append(user.getTotalQuizzes()).append(" 次\n");
sb.append("平均分:").append(String.format("%.1f", user.getAverageScore())).append(" 分\n");
sb.append("注册时间:").append(user.getRegistrationDate()).append("\n");
sb.append("=============================\n");
return sb.toString();
}
}

@ -0,0 +1,68 @@
package com.pair.service.question_generator;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.factory.ElementaryQuestionFactory;
import com.pair.service.question_generator.factory.HighQuestionFactory;
import com.pair.service.question_generator.factory.MiddleQuestionFactory;
import com.pair.service.question_generator.factory.QuestionFactory;
import java.util.*;
/**
*
*/
public class QuestionFactoryManager {
private static final Map<Grade, QuestionFactory> factories = new HashMap<>();
static {
factories.put(Grade.ELEMENTARY, new ElementaryQuestionFactory());
factories.put(Grade.MIDDLE, new MiddleQuestionFactory());
factories.put(Grade.HIGH, new HighQuestionFactory());
}
/**
*
*
* @param grade
* @param count
* @param historyQuestions null
* @return
*/
public static List<ChoiceQuestion> generateQuestions(
Grade grade, int count, Set<String> historyQuestions) {
List<ChoiceQuestion> questions = new ArrayList<>();
Set<String> usedQuestions = historyQuestions != null ?
new HashSet<>(historyQuestions) :
new HashSet<>();
int maxAttempts = count * 10;
int attempts = 0;
QuestionFactory factory = factories.get(grade);
if (factory == null) {
throw new IllegalArgumentException("不支持的学段: " + grade);
}
while (questions.size() < count && attempts < maxAttempts) {
ChoiceQuestion question = factory.createQuestion();
String questionText = question.getQuestionText();
if (!usedQuestions.contains(questionText)) {
questions.add(question);
usedQuestions.add(questionText);
}
attempts++;
}
if (questions.size() < count) {
System.out.println("⚠ 警告:只生成了 " + questions.size() +
" 道题,未达到要求的 " + count + " 道");
}
return questions;
}
}

@ -0,0 +1,41 @@
package com.pair.service.question_generator.factory;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.elementary.*;
import com.pair.util.RandomUtils;
import com.pair.service.question_generator.strategy.*;
import java.util.ArrayList;
import java.util.List;
/**
*
*/
public class ElementaryQuestionFactory implements QuestionFactory {
private final List<QuestionStrategy> strategies;
public ElementaryQuestionFactory() {
strategies = new ArrayList<>();
// 注册所有小学题目生成策略
strategies.add(new AdditionStrategy());
strategies.add(new SubtractionStrategy());
strategies.add(new MultiplicationStrategy());
strategies.add(new DivisionStrategy());
strategies.add(new ParenthesesAddStrategy());
strategies.add(new ParenthesesMultiplyStrategy());
}
//重载接口方法
@Override
public ChoiceQuestion createQuestion() {
// 从六个题型list中随机选择一个生成题目
QuestionStrategy strategy = RandomUtils.randomChoice(strategies);
return strategy.generate();
}
@Override
public Grade getSupportedGrade() {
return Grade.ELEMENTARY;
}
}

@ -0,0 +1,43 @@
package com.pair.service.question_generator.factory;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.QuestionStrategy;
import com.pair.service.question_generator.strategy.high.*;
import com.pair.util.RandomUtils;
import java.util.ArrayList;
import java.util.List;
/**
*
*/
public class HighQuestionFactory implements QuestionFactory {
private final List<QuestionStrategy> strategies;
public HighQuestionFactory() {
strategies = new ArrayList<>();
// 注册所有高中题目生成策略
strategies.add(new SinStrategy());
strategies.add(new CosStrategy());
strategies.add(new TanStrategy());
strategies.add(new TrigIdentityStrategy());
strategies.add(new DerivativeStrategy());
strategies.add(new ArithmeticSequenceSumStrategy());
strategies.add(new LogarithmStrategy());
strategies.add(new ProbabilityStrategy());
strategies.add(new FunctionExtremeStrategy());
}
@Override
public ChoiceQuestion createQuestion() {
QuestionStrategy strategy = RandomUtils.randomChoice(strategies);
return strategy.generate();
}
@Override
public Grade getSupportedGrade() {
return Grade.HIGH;
}
}

@ -0,0 +1,50 @@
package com.pair.service.question_generator.factory;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.elementary.ParenthesesAddStrategy;
import com.pair.service.question_generator.strategy.elementary.ParenthesesMultiplyStrategy;
import com.pair.service.question_generator.strategy.middle.*;
import com.pair.util.RandomUtils;
import com.pair.service.question_generator.strategy.QuestionStrategy;
import java.util.ArrayList;
import java.util.List;
/**
*
*/
public class MiddleQuestionFactory implements QuestionFactory {
private final List<QuestionStrategy> strategies;
public MiddleQuestionFactory() {
strategies = new ArrayList<>();
// 注册所有初中题目生成策略
strategies.add(new SquareStrategy());
strategies.add(new SquareAddStrategy());
strategies.add(new SqrtStrategy());
strategies.add(new SqrtAddStrategy());
strategies.add(new MixedSquareSqrtStrategy());
strategies.add(new ParenthesesAddStrategy());
strategies.add(new ParenthesesMultiplyStrategy());
strategies.add(new LinearEquationStrategy());
strategies.add(new QuadraticEquationStrategy());
strategies.add(new TriangleAreaStrategy());
strategies.add(new CircleAreaStrategy());
strategies.add(new LinearFunctionStrategy());
}
@Override
public ChoiceQuestion createQuestion() {
QuestionStrategy strategy = RandomUtils.randomChoice(strategies);
return strategy.generate();
}
@Override
public Grade getSupportedGrade() {
return Grade.MIDDLE;
}
}

@ -0,0 +1,17 @@
package com.pair.service.question_generator.factory;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
/**
*
*/
public interface QuestionFactory {
//创建题目
ChoiceQuestion createQuestion();
//获取工厂支持的学段
Grade getSupportedGrade();
}

@ -0,0 +1,138 @@
package com.pair.service.question_generator.strategy;
import com.pair.model.Grade;
import com.pair.util.RandomUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
*
*
*/
public abstract class AbstractQuestionStrategy implements QuestionStrategy {
protected final Grade grade;
/**
*
* @param grade
*/
protected AbstractQuestionStrategy(Grade grade) {
this.grade = grade;
}
/**
* 3
* @param correctAnswer
* @return
*/
protected List<Double> generateNumericOptions(double correctAnswer) {
List<Double> options = new ArrayList<>();
options.add(correctAnswer);
// 根据答案大小动态调整干扰项范围
int range = (int) Math.max(10, Math.abs(correctAnswer) * 0.3);
// 生成3个干扰项
for (int i = 0; i < 3; i++) {
double distractor;
int attempts = 0;
do {
int offset = RandomUtils.nextInt(-range, range);
if (offset == 0) offset = (i + 1) * 3;
distractor = correctAnswer + offset;
attempts++;
} while ((options.contains(distractor) || distractor < 0) && attempts < 20);
if (attempts >= 20) {
distractor = correctAnswer + (i + 1) * 5;
}
options.add(distractor);
}
// 确保有4个选项
while (options.size() < 4) {
options.add(correctAnswer + RandomUtils.nextInt(5, 15));
}
Collections.shuffle(options);
return options;
}
/**
*
* @param correctAnswer
* @param commonError
* @return
*/
protected List<Double> generateNumericOptionsWithCommonError(
double correctAnswer, double commonError) {
List<Double> options = new ArrayList<>();
options.add(correctAnswer);
// 添加常见错误项(如果有效)
if (commonError != correctAnswer && commonError > 0 && !Double.isNaN(commonError)) {
options.add(commonError);
}
int range = (int) Math.max(10, Math.abs(correctAnswer) * 0.3);
// 填充其他干扰项
while (options.size() < 4) {
double distractor;
int attempts = 0;
do {
int offset = RandomUtils.nextInt(-range, range);
if (offset == 0) offset = 5;
distractor = correctAnswer + offset;
attempts++;
} while ((options.contains(distractor) || distractor < 0) && attempts < 20);
if (attempts < 20) {
options.add(distractor);
} else {
options.add(correctAnswer + options.size() * 3);
}
}
Collections.shuffle(options);
return options;
}
/**
*
* @param correctAnswer
* @param allPossibleValues
* @return
*/
protected List<String> generateStringOptions(
String correctAnswer, List<String> allPossibleValues) {
List<String> options = new ArrayList<>();
options.add(correctAnswer);
List<String> availableValues = new ArrayList<>(allPossibleValues);
availableValues.remove(correctAnswer);
// 随机选择3个干扰项
while (options.size() < 4 && !availableValues.isEmpty()) {
int randomIndex = RandomUtils.nextInt(0, availableValues.size() - 1);
String distractor = availableValues.get(randomIndex);
options.add(distractor);
availableValues.remove(randomIndex);
}
while (options.size() < 4) {
options.add("未知");
}
Collections.shuffle(options);
return options;
}
}

@ -0,0 +1,12 @@
package com.pair.service.question_generator.strategy;
import com.pair.model.ChoiceQuestion;
//题目生成题型接口
public interface QuestionStrategy {
//生成题目
ChoiceQuestion generate();
//题型
String getStrategyName();
}

@ -0,0 +1,37 @@
package com.pair.service.question_generator.strategy.elementary;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
*/
public class AdditionStrategy extends AbstractQuestionStrategy {
public AdditionStrategy() {
super(Grade.ELEMENTARY);
}
@Override
public ChoiceQuestion generate() {
int num1 = RandomUtils.nextInt(1, 30);
int num2 = RandomUtils.nextInt(1, 30);
String questionText = num1 + " + " + num2;
double answer = num1 + num2;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "加法";
}
}

@ -0,0 +1,39 @@
package com.pair.service.question_generator.strategy.elementary;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
*/
public class DivisionStrategy extends AbstractQuestionStrategy {
public DivisionStrategy() {
super(Grade.ELEMENTARY);
}
@Override
public ChoiceQuestion generate() {
int divisor = RandomUtils.nextInt(2, 10);
int quotient = RandomUtils.nextInt(1, 10);
int dividend = divisor * quotient;
String questionText = dividend + " ÷ " + divisor;
double answer = quotient;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "除法";
}
}

@ -0,0 +1,37 @@
package com.pair.service.question_generator.strategy.elementary;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
*/
public class MultiplicationStrategy extends AbstractQuestionStrategy {
public MultiplicationStrategy() {
super(Grade.ELEMENTARY);
}
@Override
public ChoiceQuestion generate() {
int factor1 = RandomUtils.nextInt(1, 12);
int factor2 = RandomUtils.nextInt(1, 12);
String questionText = factor1 + " × " + factor2;
double answer = factor1 * factor2;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "乘法";
}
}

@ -0,0 +1,40 @@
package com.pair.service.question_generator.strategy.elementary;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* (a + b) × c
*/
public class ParenthesesAddStrategy extends AbstractQuestionStrategy {
public ParenthesesAddStrategy() {
super(Grade.ELEMENTARY);
}
@Override
public ChoiceQuestion generate() {
int num1 = RandomUtils.nextInt(1, 20);
int num2 = RandomUtils.nextInt(1, 20);
int num3 = RandomUtils.nextInt(2, 10);
String questionText = "(" + num1 + " + " + num2 + ") × " + num3;
double answer = (num1 + num2) * num3;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "括号加法乘法";
}
}

@ -0,0 +1,41 @@
package com.pair.service.question_generator.strategy.elementary;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* (a - b) × c
*/
public class ParenthesesMultiplyStrategy extends AbstractQuestionStrategy {
public ParenthesesMultiplyStrategy() {
super(Grade.ELEMENTARY);
}
@Override
public ChoiceQuestion generate() {
int num1 = RandomUtils.nextInt(1, 20);
int num2 = RandomUtils.nextInt(1, 20);
int num3 = RandomUtils.nextInt(2, 10);
int larger = Math.max(num1, num2);
int smaller = Math.min(num1, num2);
String questionText = "(" + larger + " - " + smaller + ") × " + num3;
double answer = (larger - smaller) * num3;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "括号减法乘法";
}
}

@ -0,0 +1,40 @@
package com.pair.service.question_generator.strategy.elementary;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
*/
public class SubtractionStrategy extends AbstractQuestionStrategy {
public SubtractionStrategy() {
super(Grade.ELEMENTARY);
}
@Override
public ChoiceQuestion generate() {
int num1 = RandomUtils.nextInt(1, 30);
int num2 = RandomUtils.nextInt(1, 30);
// 确保结果为正数
int larger = Math.max(num1, num2);
int smaller = Math.min(num1, num2);
String questionText = larger + " - " + smaller;
double answer = larger - smaller;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "减法";
}
}

@ -0,0 +1,41 @@
package com.pair.service.question_generator.strategy.high;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* 3 2 10
*/
public class ArithmeticSequenceSumStrategy extends AbstractQuestionStrategy {
public ArithmeticSequenceSumStrategy() {
super(Grade.HIGH);
}
@Override
public ChoiceQuestion generate() {
int a1 = RandomUtils.nextInt(1, 10); // 首项
int d = RandomUtils.nextInt(1, 5); // 公差
int n = RandomUtils.nextInt(5, 15); // 项数
String questionText = "等差数列首项为 " + a1 + ",公差为 " + d +
",求前 " + n + " 项和";
// Sn = n * a1 + n(n-1)/2 * d
double answer = n * a1 + n * (n - 1) / 2.0 * d;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "等差数列求和";
}
}

@ -0,0 +1,57 @@
package com.pair.service.question_generator.strategy.high;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
import java.util.*;
/**
*
* cos(45°)
*/
public class CosStrategy extends AbstractQuestionStrategy {
// 特殊角的余弦值表
private static final Map<Integer, String> COS_VALUES = new HashMap<>();
static {
COS_VALUES.put(0, "1");
COS_VALUES.put(30, "√3/2");
COS_VALUES.put(45, "√2/2");
COS_VALUES.put(60, "1/2");
COS_VALUES.put(90, "0");
COS_VALUES.put(120, "-1/2");
COS_VALUES.put(135, "-√2/2");
COS_VALUES.put(150, "-√3/2");
COS_VALUES.put(180, "-1");
}
public CosStrategy() {
super(Grade.HIGH);
}
@Override
public ChoiceQuestion generate() {
List<Integer> angles = new ArrayList<>(COS_VALUES.keySet());
int randomIndex = RandomUtils.nextInt(0, angles.size() - 1);
int angle = angles.get(randomIndex);
String questionText = "cos(" + angle + "°) = ?";
String correctAnswer = COS_VALUES.get(angle);
List<String> allValues = new ArrayList<>(COS_VALUES.values());
List<String> options = generateStringOptions(correctAnswer, allValues);
return new ChoiceQuestion(questionText, correctAnswer, options, grade);
}
@Override
public String getStrategyName() {
return "余弦函数";
}
}

@ -0,0 +1,40 @@
package com.pair.service.question_generator.strategy.high;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* f(x) = 3x² + 2x f'(2)
*/
public class DerivativeStrategy extends AbstractQuestionStrategy {
public DerivativeStrategy() {
super(Grade.HIGH);
}
@Override
public ChoiceQuestion generate() {
int a = RandomUtils.nextInt(2, 8);
int b = RandomUtils.nextInt(1, 6);
int x = RandomUtils.nextInt(1, 5);
String questionText = "f(x) = " + a + "x² + " + b + "x求 f'(" + x + ")";
// f'(x) = 2ax + b
double answer = 2 * a * x + b;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "导数计算";
}
}

@ -0,0 +1,41 @@
package com.pair.service.question_generator.strategy.high;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* f(x) = -x² + 4x + 1
*/
public class FunctionExtremeStrategy extends AbstractQuestionStrategy {
public FunctionExtremeStrategy() {
super(Grade.HIGH);
}
@Override
public ChoiceQuestion generate() {
int a = -1; // 开口向下
int b = RandomUtils.nextInt(2, 8) * 2; // 偶数,方便计算
int c = RandomUtils.nextInt(1, 10);
String questionText = "f(x) = -x² + " + b + "x + " + c + ",求最大值";
// 顶点坐标 x = -b/(2a), y = (4ac - b²)/(4a)
double xVertex = -b / (2.0 * a);
double answer = (4.0 * a * c - b * b) / (4.0 * a);
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "函数极值";
}
}

@ -0,0 +1,55 @@
package com.pair.service.question_generator.strategy.high;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* log8 + log9
*/
public class LogarithmStrategy extends AbstractQuestionStrategy {
private static final int[][] LOG_PAIRS = {
{2, 4, 2}, // log₂4 = 2
{2, 8, 3}, // log₂8 = 3
{2, 16, 4}, // log₂16 = 4
{3, 9, 2}, // log₃9 = 2
{3, 27, 3}, // log₃27 = 3
{5, 25, 2}, // log₅25 = 2
{10, 100, 2} // log₁₀100 = 2
};
public LogarithmStrategy() {
super(Grade.HIGH);
}
@Override
public ChoiceQuestion generate() {
int index1 = RandomUtils.nextInt(0, LOG_PAIRS.length - 1);
int index2 = RandomUtils.nextInt(0, LOG_PAIRS.length - 1);
int base1 = LOG_PAIRS[index1][0];
int num1 = LOG_PAIRS[index1][1];
int result1 = LOG_PAIRS[index1][2];
int base2 = LOG_PAIRS[index2][0];
int num2 = LOG_PAIRS[index2][1];
int result2 = LOG_PAIRS[index2][2];
String questionText = "log₍" + base1 + "₎" + num1 + " + log₍" + base2 + "₎" + num2;
double answer = result1 + result2;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "对数运算";
}
}

@ -0,0 +1,40 @@
package com.pair.service.question_generator.strategy.high;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* 5 3
*/
public class ProbabilityStrategy extends AbstractQuestionStrategy {
public ProbabilityStrategy() {
super(Grade.HIGH);
}
@Override
public ChoiceQuestion generate() {
int red = RandomUtils.nextInt(3, 8);
int white = RandomUtils.nextInt(2, 6);
int total = red + white;
String questionText = "袋中有 " + red + " 个红球," + white +
" 个白球,随机抽一个,抽到红球的概率是多少?(保留两位小数)";
double answer = Math.round((double) red / total * 100.0) / 100.0;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "概率计算";
}
}

@ -0,0 +1,56 @@
package com.pair.service.question_generator.strategy.high;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.*;
import java.util.List;
/**
*
* sin(30°)
*/
public class SinStrategy extends AbstractQuestionStrategy {
// 特殊角的正弦值表
private static final Map<Integer, String> SIN_VALUES = new HashMap<>();
static {
SIN_VALUES.put(0, "0");
SIN_VALUES.put(30, "1/2");
SIN_VALUES.put(45, "√2/2");
SIN_VALUES.put(60, "√3/2");
SIN_VALUES.put(90, "1");
SIN_VALUES.put(120, "√3/2");
SIN_VALUES.put(135, "√2/2");
SIN_VALUES.put(150, "1/2");
SIN_VALUES.put(180, "0");
}
public SinStrategy() {
super(Grade.HIGH);
}
@Override
public ChoiceQuestion generate() {
List<Integer> angles = new ArrayList<>(SIN_VALUES.keySet());
int randomIndex = RandomUtils.nextInt(0, angles.size() - 1);
int angle = angles.get(randomIndex);
String questionText = "sin(" + angle + "°) = ?";
String correctAnswer = SIN_VALUES.get(angle);
List<String> allValues = new ArrayList<>(SIN_VALUES.values());
List<String> options = generateStringOptions(correctAnswer, allValues);
return new ChoiceQuestion(questionText, correctAnswer, options, grade);
}
@Override
public String getStrategyName() {
return "正弦函数";
}
}

@ -0,0 +1,56 @@
package com.pair.service.question_generator.strategy.high;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
import java.util.*;
/**
*
* tan(45°)
*/
public class TanStrategy extends AbstractQuestionStrategy {
// 特殊角的正切值表
private static final Map<Integer, String> TAN_VALUES = new HashMap<>();
static {
TAN_VALUES.put(0, "0");
TAN_VALUES.put(30, "√3/3");
TAN_VALUES.put(45, "1");
TAN_VALUES.put(60, "√3");
TAN_VALUES.put(120, "-√3");
TAN_VALUES.put(135, "-1");
TAN_VALUES.put(150, "-√3/3");
TAN_VALUES.put(180, "0");
}
public TanStrategy() {
super(Grade.HIGH);
}
@Override
public ChoiceQuestion generate() {
List<Integer> angles = new ArrayList<>(TAN_VALUES.keySet());
int randomIndex = RandomUtils.nextInt(0, angles.size() - 1);
int angle = angles.get(randomIndex);
String questionText = "tan(" + angle + "°) = ?";
String correctAnswer = TAN_VALUES.get(angle);
List<String> allValues = new ArrayList<>(TAN_VALUES.values());
List<String> options = generateStringOptions(correctAnswer, allValues);
return new ChoiceQuestion(questionText, correctAnswer, options, grade);
}
@Override
public String getStrategyName() {
return "正切函数";
}
}

@ -0,0 +1,41 @@
package com.pair.service.question_generator.strategy.high;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
import java.util.*;
/**
*
* sin²(θ) + cos²(θ) = ?
*/
public class TrigIdentityStrategy extends AbstractQuestionStrategy {
public TrigIdentityStrategy() {
super(Grade.HIGH);
}
@Override
public ChoiceQuestion generate() {
int[] angles = {30, 45, 60, 90};
int angle = angles[RandomUtils.nextInt(0, angles.length - 1)];
String questionText = "sin²(" + angle + "°) + cos²(" + angle + "°) = ?";
String correctAnswer = "1"; // 三角恒等式永远等于1
List<String> allValues = Arrays.asList("1", "0", "2", "√2", "1/2");
List<String> options = generateStringOptions(correctAnswer, allValues);
return new ChoiceQuestion(questionText, correctAnswer, options, grade);
}
@Override
public String getStrategyName() {
return "三角恒等式";
}
}

@ -0,0 +1,36 @@
package com.pair.service.question_generator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* 5cm (π3.14)
*/
public class CircleAreaStrategy extends AbstractQuestionStrategy {
public CircleAreaStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
int radius = RandomUtils.nextInt(3, 10);
String questionText = "半径为 " + radius + "cm 的圆,面积是多少?(π取3.14)";
double answer = 3.14 * radius * radius;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "圆的面积";
}
}

@ -0,0 +1,39 @@
package com.pair.service.question_generator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* 2x + 5 = 13 x
*/
public class LinearEquationStrategy extends AbstractQuestionStrategy {
public LinearEquationStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
int a = RandomUtils.nextInt(2, 10); // 系数
int x = RandomUtils.nextInt(1, 10); // 真实的 x 值
int b = RandomUtils.nextInt(1, 15); // 常数项
int result = a * x + b;
String questionText = a + "x + " + b + " = " + result + ",求 x";
double answer = x;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "一元一次方程";
}
}

@ -0,0 +1,41 @@
package com.pair.service.question_generator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* y = 2x + 3 x = 5 y
*/
public class LinearFunctionStrategy extends AbstractQuestionStrategy {
public LinearFunctionStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
int k = RandomUtils.nextInt(2, 8);
int b = RandomUtils.nextInt(-5, 10);
int x = RandomUtils.nextInt(1, 10);
String questionText = "函数 y = " + k + "x " +
(b >= 0 ? "+ " : "") + b +
",当 x = " + x + " 时y 的值是多少?";
double answer = k * x + b;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "一次函数求值";
}
}

@ -0,0 +1,40 @@
package com.pair.service.question_generator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* 49 + 3²
*/
public class MixedSquareSqrtStrategy extends AbstractQuestionStrategy {
public MixedSquareSqrtStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
int sqrtRoot = RandomUtils.nextInt(2, 8);
int sqrtNum = sqrtRoot * sqrtRoot;
int squareBase = RandomUtils.nextInt(2, 6);
String questionText = "√" + sqrtNum + " + " + squareBase + "²";
double answer = sqrtRoot + (squareBase * squareBase);
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "平方开方混合";
}
}

@ -0,0 +1,44 @@
package com.pair.service.question_generator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* x² - 5x + 6 = 0
*/
public class QuadraticEquationStrategy extends AbstractQuestionStrategy {
public QuadraticEquationStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
// 生成两个根
int root1 = RandomUtils.nextInt(1, 6);
int root2 = RandomUtils.nextInt(1, 6);
// 根据韦达定理x² - (root1+root2)x + (root1*root2) = 0
int b = -(root1 + root2);
int c = root1 * root2;
String questionText = "x² " + (b >= 0 ? "+ " : "") + b + "x " +
(c >= 0 ? "+ " : "") + c + " = 0求较大的根";
double answer = Math.max(root1, root2);
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "一元二次方程";
}
}

@ -0,0 +1,39 @@
package com.pair.service.question_generator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* 49 + 5
*/
public class SqrtAddStrategy extends AbstractQuestionStrategy {
public SqrtAddStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
int root = RandomUtils.nextInt(2, 10);
int num = root * root;
int add = RandomUtils.nextInt(1, 20);
String questionText = "√" + num + " + " + add;
double answer = root + add;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "开方加法";
}
}

@ -0,0 +1,39 @@
package com.pair.service.question_generator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* 49
*/
public class SqrtStrategy extends AbstractQuestionStrategy {
public SqrtStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
int root = RandomUtils.nextInt(2, 12);
int num = root * root; // 完全平方数
String questionText = "√" + num;
double answer = root;
// 常见错误把开方当成除以2
double commonError = num / 2.0;
List<Double> options = generateNumericOptionsWithCommonError(answer, commonError);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "开方";
}
}

@ -0,0 +1,37 @@
package com.pair.service.question_generator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* 5² + 10
*/
public class SquareAddStrategy extends AbstractQuestionStrategy {
public SquareAddStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
int base = RandomUtils.nextInt(2, 10);
int add = RandomUtils.nextInt(1, 20);
String questionText = base + "² + " + add;
double answer = base * base + add;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "平方加法";
}
}

@ -0,0 +1,39 @@
package com.pair.service.question_generator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* 5²
*/
public class SquareStrategy extends AbstractQuestionStrategy {
public SquareStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
int num = RandomUtils.nextInt(1, 15);
String questionText = num + "²";
double answer = num * num;
// 常见错误把平方当成乘以2
double commonError = num * 2;
List<Double> options = generateNumericOptionsWithCommonError(answer, commonError);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "平方";
}
}

@ -0,0 +1,37 @@
package com.pair.service.question_generator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* 6cm 8cm
*/
public class TriangleAreaStrategy extends AbstractQuestionStrategy {
public TriangleAreaStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
int base = RandomUtils.nextInt(4, 15);
int height = RandomUtils.nextInt(4, 15);
String questionText = "底为 " + base + "cm高为 " + height + "cm 的三角形面积是多少?";
double answer = (base * height) / 2.0;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "三角形面积";
}
}

@ -0,0 +1,140 @@
// com/ui/InfGenPage.java
package com.pair.ui;
import com.pair.model.Grade;
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("生成题目");
private final Button modifyUsernameButton = new Button("修改用户名"); // 新增
private final Button modifyEmailButton = new Button("修改邮箱"); // 新增
public InfGenPage(Runnable onBack, String currentUsername, String currentEmail, Grade currentGrade) {
super(onBack);
initializeContent();
usernameField.setText(currentUsername);
emailField.setText(currentEmail);
gradeChoice.getSelectionModel().select(currentGrade.ordinal());
}
@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, modifyUsernameButton);
HBox emailRow = createFormRow("邮箱:", emailField, modifyEmailButton);
HBox passwordRow = createFormRow("密码:", passwordLabel, passwordModifyButton);
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);
// 新增按钮样式
modifyUsernameButton.setStyle(UIConstants.BUTTON_STYLE);
modifyUsernameButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
modifyUsernameButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
modifyEmailButton.setStyle(UIConstants.BUTTON_STYLE);
modifyEmailButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
modifyEmailButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
// ========== 添加到表单 ==========
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); // 间距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; }
public Button getModifyUsernameButton() { return modifyUsernameButton; } // 新增
public Button getModifyEmailButton() { return modifyEmailButton; } // 新增
}

@ -0,0 +1,58 @@
// com/ui/LoginPage.java
package com.pair.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;
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; }
}

@ -0,0 +1,296 @@
// com/ui/MainWindow.java
package com.pair.ui;
import com.pair.model.User;
import com.pair.service.QuizService;
import com.pair.service.UserService;
import com.pair.util.AsyncRegistrationHelper;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Toggle;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import java.io.IOException;
public class MainWindow extends BorderPane {
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) throws IOException {
this.primaryStage = primaryStage;
showStartPage();
}
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;
}
// 封装登录页面初始化逻辑
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);
}
// 封装登录核心逻辑从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());
}
}
// 其他页面的初始化方法(保持原有逻辑,统一格式)
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);
}
private void handleSendCodeAction(RegisterPage registerPage) {
String email = registerPage.getEmailField().getText().trim();
Button btn = registerPage.getSendCodeButton();
AsyncRegistrationHelper.sendRegistrationCode(
email,
userService,
() -> {
btn.setDisable(true);
btn.setText("发送中...");
},
text -> btn.setText(text), // 倒计时更新
() -> {
btn.setText("获取注册码");
btn.setDisable(false);
},
msg -> NavigablePanel.showInfoAlert("成功", msg),
msg -> NavigablePanel.showErrorAlert("获取注册码失败", msg)
);
}
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();
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(), userService.getCurrentUser().getGrade());
infGenPage.getGenerateButton().setOnAction(e -> {
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 -> {
navigateTo(Panel.PASSWORDMODIFY);
});
infGenPage.getModifyUsernameButton().setOnAction(e -> handleUsernameModifyAction(infGenPage));
infGenPage.getModifyEmailButton().setOnAction(e -> handleEmailModifyAction(infGenPage));
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());
NavigablePanel.showInfoAlert("成功", "用户名修改成功");
} 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());
NavigablePanel.showInfoAlert("成功","邮箱修改成功");
} catch (IllegalArgumentException ex) {
NavigablePanel.showErrorAlert("邮箱错误", ex.getMessage());
} catch (IOException ex) {
NavigablePanel.showErrorAlert("系统错误", ex.getMessage());
}
}
private void initPasswordModifyPage() {
PasswordModifyPage pwdPage = new PasswordModifyPage(() -> navigateTo(Panel.INF_GEN));
pwdPage.getModifyButton().setOnAction(e -> handlePasswordModify(pwdPage));
this.setCenter(pwdPage);
}
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);
NavigablePanel.showInfoAlert("成功", "密码修改成功");
navigateTo(Panel.INF_GEN);
} catch (IllegalArgumentException ex) {
NavigablePanel.showErrorAlert("修改失败", ex.getMessage());
return;
} catch (IOException ex) {
NavigablePanel.showErrorAlert("系统错误 ", ex.getMessage());
return;
}
}
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);
});
this.setCenter(quizPage);
}
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);
}
// getter和启动方法保持不变
public Panel getCurrentPanel() {
return currentPanel;
}
public static void start(Stage stage) throws IOException {
MainWindow mainWindow = new MainWindow(stage);
Scene scene = new Scene(mainWindow, 800, 600);
stage.setScene(scene);
stage.setTitle("中小学数学答题系统");
stage.show();
}
}

@ -0,0 +1,60 @@
// com/ui/NavigablePanel.java
package com.pair.ui;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
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 static void showInfoAlert(String title, String message) {
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
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.pair.ui;
public enum Panel {
START, // 开始页面
LOGIN, // 登录页面
REGISTER, // 注册页面
INF_GEN, // 个人信息+生成题目页面
PASSWORDMODIFY, // 修改密码页面
QUIZ, // 答题页面
RESULT // 得分页面
}

@ -0,0 +1,52 @@
// com/ui/PasswordModifyPage.java
package com.pair.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; }
}

@ -0,0 +1,314 @@
// com/ui/QuizPage.java
package com.pair.ui;
import com.pair.model.ChoiceQuestion;
import com.pair.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.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);
}
}

@ -0,0 +1,63 @@
// com/ui/RegisterPage.java
package com.pair.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; }
}

@ -0,0 +1,63 @@
// com/ui/ResultPage.java
package com.pair.ui;
import com.pair.model.QuizResult;
import com.pair.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; }
}

@ -0,0 +1,37 @@
// com/ui/StartPage.java
package com.pair.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,69 @@
// UIConstants.java
package com.pair.ui;
import javafx.geometry.Insets;
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);";
}

@ -0,0 +1,43 @@
package com.pair.util;
/**
*
*
*/
public class AppDataDirectory {
private static final String APP_NAME = "Math-Quiz-App"; // 替换为你的应用名
/**
*
*/
public static String getApplicationDataDirectory() {
String os = System.getProperty("os.name").toLowerCase();
String basePath;
if (os.contains("win")) {
// Windows
String appData = System.getenv("APPDATA");
basePath = (appData != null) ? appData : System.getProperty("user.home") + "/AppData/Roaming";
} else if (os.contains("mac")) {
// macOS
basePath = System.getProperty("user.home") + "/Library/Application Support";
} else {
// Linux/Unix
String xdgDataHome = System.getenv("XDG_DATA_HOME");
if (xdgDataHome == null) {
xdgDataHome = System.getProperty("user.home") + "/.local/share";
}
basePath = xdgDataHome;
}
return basePath + "/" + APP_NAME;
}
/**
*
*/
public static String getFullPath(String relativePath) {
String appDataDir = getApplicationDataDirectory();
return appDataDir + "/" + relativePath;
}
}

@ -0,0 +1,68 @@
// AsyncRegistrationHelper.java
package com.pair.util;
import com.pair.service.UserService;
import javafx.application.Platform;
import javafx.concurrent.Task;
import java.io.IOException;
import java.util.function.Consumer;
public class
AsyncRegistrationHelper {
private static final int COUNTDOWN_SECONDS = 60;
public static void sendRegistrationCode(
String email,
UserService userService,
Runnable onPrepare,
Consumer<String> onCountdown,
Runnable onComplete,
Consumer<String> onSuccess,
Consumer<String> onFailure) {
Platform.runLater(onPrepare);
Task<Void> task = new Task<>() {
@Override
protected Void call() throws Exception {
userService.generateRegistrationCode(email);
return null;
}
};
task.setOnSucceeded(e -> {
onSuccess.accept("注册码已发送到邮箱10分钟内有效");
new Thread(() -> {
try {
for (int i = COUNTDOWN_SECONDS; i >= 0; i--) {
int sec = i;
Platform.runLater(() -> {
if (sec == 0) onComplete.run();
else onCountdown.accept(sec + "秒后重试");
});
Thread.sleep(1000);
}
} catch (InterruptedException ignored) {
Platform.runLater(onComplete);
Thread.currentThread().interrupt();
}
}).start();
});
task.setOnFailed(e -> {
Throwable ex = task.getException();
String msg = switch (ex) {
case IllegalArgumentException iae -> iae.getMessage();
case IOException ioe -> "系统错误:" + ioe.getMessage();
default -> "发送失败,请检查网络或稍后重试";
};
onFailure.accept(msg);
Platform.runLater(onComplete);
});
new Thread(task).start();
}
}

@ -0,0 +1,103 @@
package com.pair.util;
import javax.mail.*;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.util.Date;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class EmailUtil {
// [用户名]@[域名主体].[顶级域名]
public static final Pattern EMAIL_PATTERN = Pattern.compile("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$");
//验证邮箱格式
public static boolean validateEmail(String email) {
if (email == null || email.trim().isEmpty()) {
return false;
}
Matcher matcher = EMAIL_PATTERN.matcher(email);
return matcher.matches();
}
// ==================== 邮箱配置 ====================
private static final String SENDER_EMAIL = "2936213174@qq.com"; // 我的QQ邮箱
private static final String SENDER_PASSWORD = "gxfjdzqviasuddci"; // QQ邮箱授权码
private static final String SMTP_HOST = "smtp.qq.com"; // QQ邮箱SMTP服务器
private static final String SMTP_PORT = "587"; // 端口
/**
*
* @param toEmail
* @param registrationCode
* @return truefalse
*/
public static boolean sendRegistrationCode(String toEmail, String registrationCode) {
try {
// 1. 配置邮件服务器
Properties props = new Properties();
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.host", SMTP_HOST);
props.put("mail.smtp.port", SMTP_PORT);
// 2. 创建会话
Session session = Session.getInstance(props, new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(SENDER_EMAIL, SENDER_PASSWORD);
}
});
// 3. 创建邮件
Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(SENDER_EMAIL));
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(toEmail));
message.setSubject("注册验证码");
// 邮件内容
String content = "您的注册验证码是:" + registrationCode + "\n" +
"验证码有效期为10分钟请尽快使用。\n" +
"如非本人操作,请忽略此邮件。";
message.setText(content);
// 4. 发送邮件
Transport.send(message);
System.out.println("✓ 验证码已发送到:" + toEmail);
return true;
} catch (Exception e) {
System.err.println("✗ 邮件发送失败:" + e.getMessage());
e.printStackTrace();
return false;
}
}
/**
*
*
* @param toEmail
* @param newPassword
* @return true
*/
public static boolean sendPasswordReset(String toEmail, String newPassword) {
// TODO: 将来如果需要"找回密码"功能,可以在这里实现
System.out.println("【模拟】发送密码重置邮件");
System.out.println("收件人: " + toEmail);
System.out.println("新密码: " + newPassword);
return true;
}
}

@ -0,0 +1,210 @@
package com.pair.util;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.lang.reflect.Type;
/**
*
* JSON
*/
public class FileUtils {
private static final Gson gson = new GsonBuilder()
.setPrettyPrinting() // 格式化输出JSON
.create();
/**
*
* @param filePath
* @return
* @throws IOException
*/
public static String readFileToString(String filePath) throws IOException {
return Files.readString(Paths.get(filePath), StandardCharsets.UTF_8);
}
/**
*
* @param filePath
* @param content
* @throws IOException
*/
public static void writeStringToFile(String filePath, String content) throws IOException {
File file = new File(filePath);
// 确保父目录存在
File parentDir = file.getParentFile();
if (parentDir != null && !parentDir.exists()) {
parentDir.mkdirs();
}
// 写入文件
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
writer.write(content);
}
}
/**
*
* @param dirPath
* @throws IOException
*/
public static void createDirectoryIfNotExists(String dirPath) throws IOException {
Path path = Paths.get(dirPath);
if (!Files.exists(path)) {
System.out.println(1);
Files.createDirectories(path);
System.out.println(2);
}
}
public static void ensureFileExists(String filePath) throws IOException {
if (!FileUtils.exists(filePath)) {
// 自动创建父目录
String parentDir = Paths.get(filePath).getParent().toString();
FileUtils.createDirectoryIfNotExists(parentDir);
// 创建空文件
FileUtils.writeStringToFile(filePath, "");
}
}
/**
*
* @param filePath
* @return true
*/
public static boolean exists(String filePath) {
return Files.exists(Paths.get(filePath));
}
/**
*
* @param filePath
* @return true
*/
public static boolean deleteFile(String filePath) {
try {
return Files.deleteIfExists(Paths.get(filePath));
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
/**
*
* @param dirPath
* @return
*/
public static File[] listFiles(String dirPath) {
File dir = new File(dirPath);
if (dir.exists() && dir.isDirectory()) {
return dir.listFiles();
}
return new File[0];
}
/**
*
* @param filePath
* @param content
* @throws IOException
*/
public static void appendToFile(String filePath, String content) throws IOException {
Files.writeString(Paths.get(filePath), content, StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
}
/**
*
* @param sourcePath
* @param targetPath
* @throws IOException
*/
public static void copyFile(String sourcePath, String targetPath) throws IOException {
Files.copy(Paths.get(sourcePath), Paths.get(targetPath), StandardCopyOption.REPLACE_EXISTING);
}
/**
*
* @param filePath
* @return
* @throws IOException
*/
public static long getFileSize(String filePath) throws IOException {
return Files.size(Paths.get(filePath));
}
// ==================== JSON 操作方法 ====================
/**
* JSON
* @param data
* @param filePath
* @throws IOException
*/
public static void saveAsJson(Object data, String filePath) throws IOException {
String json = gson.toJson(data);
writeStringToFile(filePath, json);
}
/**
* JSON
* @param filePath
* @param classOfT Class
* @return
* @throws IOException
*/
public static <T> T readJsonToObject(String filePath, Class<T> classOfT) throws IOException {
String json = readFileToString(filePath);
return gson.fromJson(json, classOfT);
}
/**
* JSON
* @param filePath
* @param typeOfT Type
* @return
* @throws IOException
*/
public static <T> T readJsonToObject(String filePath, Type typeOfT) throws IOException {
String json = readFileToString(filePath);
return gson.fromJson(json, typeOfT);
}
/**
* JSON
* @param data
* @return JSON
*/
public static String toJson(Object data) {
return gson.toJson(data);
}
/**
* JSON
* @param json JSON
* @param classOfT Class
* @return
*/
public static <T> T fromJson(String json, Class<T> classOfT) {
return gson.fromJson(json, classOfT);
}
/**
* JSON
* @param json JSON
* @param typeOfT Type
* @return
*/
public static <T> T fromJson(String json, Type typeOfT) {
return gson.fromJson(json, typeOfT);
}
}

@ -0,0 +1,146 @@
package com.pair.util;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
*
*
*/
public class PasswordValidator {
// 密码长度限制
private static final int CODE_LENGTH = 6;
private static final int MIN_LENGTH = 6;
private static final int MAX_LENGTH = 10;
// 用于生成随机注册码的字符集
private static final String UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final String LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
private static final String DIGITS = "0123456789";
private static final String ALL_CHARS = UPPERCASE + LOWERCASE + DIGITS;
// ==================== 密码验证 ====================
/**
*
*
* @param password
* @return null
*/
public static void validatePassword(String password) {
if (password == null || password.isEmpty()) {
throw new IllegalArgumentException("密码不能为空!");
}
if (password.length() < MIN_LENGTH) {
throw new IllegalArgumentException("密码长度不能少于 " + MIN_LENGTH + " 位!");
}
if (password.length() > MAX_LENGTH) {
throw new IllegalArgumentException("密码长度不能超过 " + MAX_LENGTH + " 位!");
}
if (password.contains(" ")) {
throw new IllegalArgumentException("密码不能包含空格!");
}
// 检查是否包含小写字母
boolean hasLowerLetter = password.matches(".*[a-z].*");
if (!hasLowerLetter) {
throw new IllegalArgumentException("必须包含小写字母!");
}
// 检查是否包含大写字母
boolean hasUpperLetter = password.matches(".*[A-Z].*");
if (!hasUpperLetter) {
throw new IllegalArgumentException("必须包含大写字母!");
}
// 检查是否包含数字
boolean hasDigit = password.matches(".*\\d.*");
if (!hasDigit) {
throw new IllegalArgumentException("密码必须包含数字!");
}
}
// ==================== 密码加密 ====================
/**
* 使SHA-256
*
* @param password
* @return 16
*/
public static String encrypt(String password) {
if (password == null) {
throw new IllegalArgumentException("密码不能为null");
}
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("SHA-256算法不可用", e);
}
}
/**
*
*
* @param plainPassword
* @param encryptedPassword
* @return true
*/
public static boolean matches(String plainPassword, String encryptedPassword) {
if (plainPassword == null || encryptedPassword == null) {
return false;
}
return encrypt(plainPassword).equals(encryptedPassword);
}
// ==================== 注册码生成 ====================
/**
* 6-10
*
* @return
*/
public static String generateRegistrationCode() {
return generateRegistrationCode(CODE_LENGTH);
}
/**
*
*
* @param @
* @return
*/
public static String generateRegistrationCode(int codeLength) {
StringBuilder code = new StringBuilder(codeLength);
// 填充剩余字符
for (int i = 0; i < codeLength; i++) {
code.append(DIGITS.charAt(RandomUtils.nextInt(0, DIGITS.length() - 1)));
}
// 打乱字符顺序
return RandomUtils.shuffleString(code.toString());
}
}

@ -0,0 +1,91 @@
package com.pair.util;
import java.util.Collections;
import java.util.List;
import java.util.Random;
//各种随机数生成
public class RandomUtils {
private static final Random random = new Random();
//生成[min, max]范围内的随机整数(包含边界)
public static int nextInt(int min, int max) {
if (min > max) {
throw new IllegalArgumentException("min不能大于max");
}
return min + random.nextInt(max - min + 1);
}
//从数组中随机选择一个元素(模板类)
public static <T> T randomChoice(T[] array) {
if (array == null || array.length == 0) {
throw new IllegalArgumentException("数组不能为空");
}
return array[random.nextInt(array.length)];
}
//从列表中随机选择一个元素
public static <T> T randomChoice(List<T> list) {
if (list == null || list.isEmpty()) {
throw new IllegalArgumentException("列表不能为空");
}
return list.get(random.nextInt(list.size()));
}
/**
*
* @param list
*/
public static <T> void shuffle(List<T> list) {
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) {
if (min > max) {
throw new IllegalArgumentException("min不能大于max");
}
return min + (max - min) * random.nextDouble();
}
//生成随机布尔值
public static boolean nextBoolean() {
return random.nextBoolean();
}
//按概率返回true题目生成概率
//示例probability(0.7) 有70%概率返回true
public static boolean probability(double probability) {
if (probability < 0.0 || probability > 1.0) {
throw new IllegalArgumentException("概率必须在0.0-1.0之间");
}
return random.nextDouble() < probability;
}
}
Loading…
Cancel
Save