Compare commits

..

5 Commits

File diff suppressed because one or more lines are too long

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

@ -9,7 +9,6 @@
<module name="attendance" />
</profile>
</annotationProcessing>
<bytecodeTargetLevel target="17" />
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">

@ -1,14 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="AliAccessStaticViaInstance" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliArrayNamingShouldHaveBracket" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliControlFlowStatementWithoutBraces" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliDeprecation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliEqualsAvoidNull" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliLongLiteralsEndingWithLowercaseL" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliMissingOverrideAnnotation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliWrapperTypeEquality" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="MapOrSetKeyShouldOverrideHashCodeEquals" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliControlFlowStatementWithoutBraces" enabled="true" level="WEAK WARNING" enabled_by_default="true" editorAttributes="INFO_ATTRIBUTES" />
</profile>
</component>

@ -6,11 +6,6 @@
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="http://mirrors.cloud.tencent.com/nexus/repository/maven-public/" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
@ -21,5 +16,10 @@
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://maven.aliyun.com/repository/public" />
</remote-repository>
</component>
</project>

@ -8,7 +8,7 @@
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="corretto-17 (2)" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="corretto-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

@ -1,124 +0,0 @@
<?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>

@ -1,19 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
wrapperVersion=3.3.2
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="3b22332f-54e5-44db-aa06-c42dcb64501b" name="更改" comment="">
<change afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/example/attendance/AttendanceApplication.java" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/example/attendance/config/SecurityConfig.java" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/example/attendance/controller/RollCallController.java" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/example/attendance/controller/StudentController.java" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/example/attendance/controller/TeacherController.java" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/example/attendance/entity/PointsRequest.java" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/example/attendance/entity/RollCallResponse.java" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/example/attendance/entity/RollCallSettings.java" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/example/attendance/entity/Student.java" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/example/attendance/entity/Teacher.java" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/example/attendance/mapper/StudentMapper.java" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/example/attendance/mapper/TeacherMapper.java" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/example/attendance/service/RollCallService.java" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/example/attendance/service/StudentService.java" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/example/attendance/service/TeacherService.java" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/example/attendance/service/impl/RollCallServiceImpl.java" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/example/attendance/service/impl/StudentServiceImpl.java" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/example/attendance/service/impl/TeacherServiceImpl.java" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/com/example/attendance/util/JWTUtil.java" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/resources/application.properties" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/com/example/attendance/AttendanceApplicationTests.java" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/com/example/attendance/PasswordEncryptor.java" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/com/example/attendance/RollCallServiceImplTest.java" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/com/example/attendance/StudentServiceImplTest.java" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/com/example/attendance/TeacherServiceImplTest.java" beforeDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/.." />
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 3
}</component>
<component name="ProjectId" id="2n7TontcBHYq0FZUtB4aOJMP8nL" />
<component name="ProjectViewState">
<option name="openDirectoriesWithSingleClick" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;WebServerToolWindowFactoryState&quot;: &quot;false&quot;,
&quot;git-widget-placeholder&quot;: &quot;main&quot;,
&quot;last_opened_file_path&quot;: &quot;C:/Users/onelastkiss/Desktop/项目/bigbigmarket&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}
}</component>
<component name="RunManager">
<configuration default="true" type="JetRunConfigurationType">
<module name="BACKEND_ATTENDANCE" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<configuration default="true" type="KotlinStandaloneScriptRunConfigurationType">
<module name="BACKEND_ATTENDANCE" />
<option name="filePath" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="应用程序级" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="默认任务">
<changelist id="3b22332f-54e5-44db-aa06-c42dcb64501b" name="更改" comment="" />
<created>1728323291653</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1728323291653</updated>
<workItem from="1728323292792" duration="28000" />
<workItem from="1728372981142" duration="128000" />
<workItem from="1728379806368" duration="345000" />
<workItem from="1728386733302" duration="673000" />
<workItem from="1728402818365" duration="864000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
</project>

@ -1,34 +0,0 @@
@startuml
' 定义实体类
class Student {
+Long id
+String studentNumber
+String name
+BigDecimal points
}
class Teacher {
+Long id
+String username
+String password
}
class RollCallSettings {
+String rollCallMode
+String triggerRandomEvent
+String wheelOfFortune
}
class RollCallResponse {
+String studentId
+String name
+BigDecimal points
+String message
}
class PointsRequest {
+BigDecimal pointsDelta
}
@enduml

@ -23,7 +23,6 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.3.3</version>
</dependency>
<!-- MyBatis Starter -->
@ -45,7 +44,7 @@
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
@ -83,19 +82,20 @@
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT Library -->
<!-- JWT Library -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
@ -108,29 +108,6 @@
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>9.0.75</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.0</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>10.1.28</version>
</dependency>
</dependencies>
@ -159,12 +136,6 @@
</excludes>
</configuration>
</plugin>
<!-- Spring Boot Maven Plugin -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.3.3</version>
</plugin>
</plugins>
</build>
</project>

@ -1,72 +1,33 @@
package com.example.attendance.config;
//import com.example.attendance.filter.JwtAuthenticationFilter;
import com.example.attendance.filter.JwtAuthenticationFilter;
import com.example.attendance.service.TeacherService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final TeacherService teacherService; // 使用构造函数注入
@Autowired
public SecurityConfig(@Lazy TeacherService teacherService) { // 添加 @Lazy 注解
this.teacherService = teacherService;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable()) // 关闭 CSRF 保护
.authorizeHttpRequests(authz -> authz
.requestMatchers(HttpMethod.POST, "/api/teacher/login", "/api/teacher/register").permitAll()
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.anyRequest().authenticated()
.requestMatchers("/api/teacher/register", "/api/teacher/login").permitAll() // 允许注册和登录接口匿名访问
.anyRequest().authenticated() // 其他请求需要认证
)
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.formLogin(form -> form.disable())
.httpBasic(httpBasic -> httpBasic.disable());
.formLogin(form -> form.disable()) // 关闭表单登录
.httpBasic(httpBasic -> httpBasic.disable()); // 关闭基本认证
return http.build();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter(teacherService);
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*")); // 允许所有来源
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}

@ -2,24 +2,23 @@ package com.example.attendance.controller;
import com.example.attendance.entity.RollCallResponse;
import com.example.attendance.entity.RollCallSettings;
import com.example.attendance.entity.Student;
import com.example.attendance.service.RollCallService;
import com.example.attendance.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/rollcall")
public class RollCallController {
@Autowired
private StudentService studentService;
@Autowired
private RollCallService rollCallService;
private final RollCallService rollCallService;
public RollCallController(RollCallService rollCallService) {
this.rollCallService = rollCallService;
}
@PostMapping("/start")
public RollCallResponse startRollCall(@RequestBody RollCallSettings settings) {
// 从数据库获取学生列表
var students = studentService.findAll();
public RollCallResponse startRollCall(@RequestBody List<Student> students, @RequestBody RollCallSettings settings) {
return rollCallService.startRollCall(students, settings);
}
}

@ -3,8 +3,11 @@ package com.example.attendance.controller;
import com.example.attendance.entity.PointsRequest;
import com.example.attendance.entity.Student;
import com.example.attendance.service.StudentService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
@ -19,7 +22,7 @@ import java.util.List;
@RestController
@RequestMapping("/api/students")
public class StudentController {
private static final Logger logger = LoggerFactory.getLogger(StudentController.class);
@Autowired
private StudentService studentService;
@ -77,12 +80,15 @@ public class StudentController {
}
/**
*
* @return
*
* @param page
* @param size
* @return
*/
@GetMapping("/ranking")
public ResponseEntity<List<Student>> getStudentRanking() {
List<Student> ranking = studentService.getStudentRanking();
public ResponseEntity<List<Student>> getStudentRanking(@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
List<Student> ranking = studentService.getStudentRanking(page, size);
return ResponseEntity.ok(ranking);
}
@ -93,19 +99,14 @@ public class StudentController {
* @return
*/
@PutMapping("/{studentNumber}/adjustPoints")
public ResponseEntity<String> adjustStudentPoints(
@PathVariable String studentNumber,
@RequestBody PointsRequest pointsRequest) {
public ResponseEntity<String> adjustStudentPoints(@PathVariable String studentNumber, @RequestBody PointsRequest pointsRequest) {
try {
logger.info("调整的积分: {}", pointsRequest.getPointsDelta());
studentService.adjustPoints(studentNumber, pointsRequest.getPointsDelta());
return ResponseEntity.ok("积分调整成功");
} catch (Exception e) {
logger.error("积分调整失败: ", e);
return ResponseEntity.status(500).body("积分调整失败:" + e.getMessage());
}
}
/**
* Excel
* @param file Excel
@ -123,28 +124,46 @@ public class StudentController {
return "学生数据导入失败:" + e.getMessage();
}
}
/* 导出并清理学生数据 */
/*
*/
@GetMapping("/export-students")
public ResponseEntity<byte[]> exportStudents() throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
studentService.exportStudents(outputStream); // 导出并清理学生数据
// 设置 HTTP 头信息
HttpHeaders headers = new HttpHeaders();
headers.setContentDispositionFormData("attachment", "students.xlsx");
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
// 返回 Excel 文件内容作为响应
return ResponseEntity
.ok()
.headers(headers)
.body(outputStream.toByteArray());
} catch (Exception e) {
return ResponseEntity.badRequest().body(null);
} finally {
outputStream.close();
List<Student> students = studentService.findAll(); // 获取所有学生数据
// 创建 Excel 工作簿
Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("Students");
// 创建表头
Row header = sheet.createRow(0);
header.createCell(0).setCellValue("学号");
header.createCell(1).setCellValue("姓名");
header.createCell(2).setCellValue("积分");
// 填充数据
int rowNum = 1;
for (Student student : students) {
Row row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue(student.getStudentNumber());
row.createCell(1).setCellValue(student.getName());
row.createCell(2).setCellValue(student.getPoints().doubleValue());
}
// 将 Excel 文件写入 ByteArrayOutputStream
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
workbook.write(outputStream);
workbook.close();
// 设置 HTTP 头信息
HttpHeaders headers = new HttpHeaders();
headers.setContentDispositionFormData("attachment", "students.xlsx");
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
// 返回文件内容作为响应
return ResponseEntity
.ok()
.headers(headers)
.body(outputStream.toByteArray());
}
/**
* Excel

@ -1,15 +1,10 @@
package com.example.attendance.controller;
import com.example.attendance.entity.Teacher;
import com.example.attendance.service.TeacherService;
import com.example.attendance.util.JWTUtil;
import io.jsonwebtoken.Claims;
import com.example.attendance.service.impl.TeacherServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/teacher")
public class TeacherController {
@ -19,73 +14,25 @@ public class TeacherController {
// 注册接口
@PostMapping("/register")
public ResponseEntity<?> register(@RequestParam String username, @RequestParam String password) {
public ResponseEntity<String> register(@RequestParam String username, @RequestParam String password) {
try {
// 调用 teacherService 的 register 方法注册用户
teacherService.register(username, password);
return ResponseEntity.ok(Map.of(
"code", 200,
"message", "注册成功",
"data", Map.of("username", username)
));
return ResponseEntity.ok("注册成功"); // 成功时返回 200 状态和消息
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of(
"code", 400,
"message", "注册失败",
"error", e.getMessage()
));
return ResponseEntity.badRequest().body("注册失败: " + e.getMessage()); // 失败时返回 400 状态和错误信息
}
}
// 登录接口
@PostMapping("/login")
public ResponseEntity<?> login(@RequestParam String username, @RequestParam String password) {
public ResponseEntity<String> login(@RequestParam String username, @RequestParam String password) {
try {
// 调用 teacherService 的 login 方法登录
String token = teacherService.login(username, password);
return ResponseEntity.ok(Map.of(
"message", "登录成功",
"token", token
));
return ResponseEntity.ok("登录成功, Token: " + token); // 成功时返回 200 状态和 JWT token
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of(
"message", "登录失败: " + e.getMessage()
));
return ResponseEntity.badRequest().body("登录失败: " + e.getMessage()); // 失败时返回 400 状态和错误信息
}
}
// 获取当前用户信息接口
@GetMapping("/current")
public ResponseEntity<?> getCurrentUser(@RequestHeader("Authorization") String token) {
try {
String username = JWTUtil.getUsernameFromToken(token.replace("Bearer ", ""));
Teacher teacher = teacherService.findByUsername(username);
if (teacher != null) {
return ResponseEntity.ok(Map.of(
"code", 200,
"message", "获取用户信息成功",
"data", Map.of("username", teacher.getUsername())
));
} else {
return ResponseEntity.badRequest().body(Map.of(
"code", 400,
"message", "用户不存在"
));
}
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of(
"code", 400,
"message", "获取用户信息失败",
"error", e.getMessage()
));
}
}
@PostMapping("/logout")
public ResponseEntity<?> logout(@RequestHeader("Authorization") String token) {
String processedToken = token.replace("Bearer ", "");
Claims claims = JWTUtil.extractClaims(processedToken);
return ResponseEntity.ok(Map.of(
"code", 200,
"message", "退出登录成功"
));
}
}

@ -1,16 +1,13 @@
package com.example.attendance.entity;
import java.math.BigDecimal;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Data
public class PointsRequest {
private BigDecimal pointsDelta;
public BigDecimal getPointsDelta() {
return pointsDelta;
}
public void setPointsDelta(BigDecimal pointsDelta) {
this.pointsDelta = pointsDelta;
}
}

@ -1,52 +0,0 @@
package com.example.attendance.filter;
import com.example.attendance.service.TeacherService;
import com.example.attendance.util.JWTUtil;
import com.example.attendance.entity.Teacher;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final TeacherService teacherService;
public JwtAuthenticationFilter(TeacherService teacherService) {
this.teacherService = teacherService;
}
@Override
protected void doFilterInternal(jakarta.servlet.http.HttpServletRequest request, jakarta.servlet.http.HttpServletResponse response, jakarta.servlet.FilterChain filterChain) throws jakarta.servlet.ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
try {
String username = JWTUtil.getUsernameFromToken(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
Teacher teacher = teacherService.findByUsername(username);
if (teacher != null && !JWTUtil.isTokenExpired(token)) {
UserDetails userDetails = new org.springframework.security.core.userdetails.User(
teacher.getUsername(), teacher.getPassword(), new ArrayList<>());
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
} catch (Exception e) {
logger.error("无法设置用户认证: {}", e);
}
}
filterChain.doFilter(request, response);
}
}

@ -9,46 +9,28 @@ import java.util.List;
@Mapper
public interface StudentMapper {
@Select("SELECT * FROM students WHERE id = #{id}")
@Select("SELECT * FROM student WHERE id = #{id}")
Student findById(Long id);
@Select("SELECT * FROM students WHERE student_number = #{studentNumber}")
@Results({
@Result(property = "id", column = "id"),
@Result(property = "studentNumber", column = "student_number"), // 确保映射
@Result(property = "name", column = "name"),
@Result(property = "points", column = "points")
})
@Select("SELECT * FROM student WHERE student_number = #{studentNumber}")
Student findByStudentNumber(String studentNumber);
@Select("SELECT id, student_number, name, points FROM students")
@Results({
@Result(property = "id", column = "id"),
@Result(property = "studentNumber", column = "student_number"),
@Result(property = "name", column = "name"),
@Result(property = "points", column = "points")
})
@Select("SELECT * FROM student")
List<Student> findAll();
@Insert("INSERT INTO students (student_number, name, points) VALUES (#{studentNumber}, #{name}, #{points})")
@Insert("INSERT INTO student (student_number, name, points) VALUES (#{studentNumber}, #{name}, #{points})")
@Options(useGeneratedKeys = true, keyProperty = "id")
void save(Student student);
@Update("UPDATE students SET name = #{name}, points = #{points} WHERE student_number = #{studentNumber}")
@Results({
@Result(property = "id", column = "id"),
@Result(property = "studentNumber", column = "student_number"), // 确保映射
@Result(property = "name", column = "name"),
@Result(property = "points", column = "points")
})
@Update("UPDATE student SET name = #{name}, points = #{points} WHERE student_number = #{studentNumber}")
void update(Student student);
@Delete("DELETE FROM students WHERE id = #{id}")
@Delete("DELETE FROM student WHERE id = #{id}")
void delete(Long id);
@Insert({
"<script>",
"INSERT INTO students (student_number, name, points) VALUES ",
"INSERT INTO student (student_number, name, points) VALUES ",
"<foreach collection='students' item='student' separator=','>",
"(#{student.studentNumber}, #{student.name}, #{student.points})",
"</foreach>",
@ -56,22 +38,19 @@ public interface StudentMapper {
})
void saveStudents(List<Student> students);
@Select("SELECT * FROM students ORDER BY points DESC")
@Results({
@Result(property = "id", column = "id"),
@Result(property = "studentNumber", column = "student_number"), // 确保映射
@Result(property = "name", column = "name"),
@Result(property = "points", column = "points")
})
List<Student> findAllStudentsByRanking();
/**
*
* @param offset
* @param size
* @return
*/
@Select("SELECT * FROM student ORDER BY points DESC LIMIT #{size} OFFSET #{offset}")
List<Student> findStudentsByRanking(@Param("offset") int offset, @Param("size") int size);
/**
*
* @return
*/
@Select("SELECT name FROM students")
@Select("SELECT name FROM student")
String[] findAllStudentNames();
// 删除所有学生数据
@Delete("DELETE FROM students")
void deleteAllStudents();
}

@ -11,13 +11,13 @@ public interface StudentService {
void importStudents(MultipartFile file) throws Exception ;
void exportStudents(OutputStream outputStream) throws Exception;
void adjustPoints(String studentNumber, BigDecimal pointsDelta);
List<Student> getStudentRanking();
List<Student> getStudentRanking(int page, int size);
Student findById(Long id);
Student findByStudentNumber(String studentNumber);
List<Student> findAll();
void save(Student student);
void update(Student student);
void delete(Long id);
String[] getAllStudentNames();
void deleteAllStudents();
}

@ -1,7 +1,5 @@
package com.example.attendance.service;
import com.example.attendance.entity.Teacher;
public interface TeacherService {
/**
@ -22,7 +20,4 @@ public interface TeacherService {
* @throws Exception
*/
String login(String username, String password) throws Exception;
Teacher findByUsername(String username);
}

@ -101,9 +101,9 @@ public class RollCallServiceImpl implements RollCallService {
int eventType = random.nextInt(2); // 0表示赌徒事件1表示倒霉事件
if (eventType == 0) {
return "赌徒事件--学生可以在回答问题前下注一定数量的积分,如果回答正确,则按赌注倍数获得积分;如果错误,则失去赌注积分"; // 触发赌徒事件
return "赌徒事件"; // 触发赌徒事件
} else {
return "倒霉事件--左手边第3位同学替该同学回答问题加减分命运交到别人手中"; // 触发倒霉事件
return "倒霉事件"; // 触发倒霉事件
}
}
}

@ -41,8 +41,8 @@ public class StudentServiceImpl implements StudentService {
if (row == null) continue;
// 读取学生数据
String studentNumber = getCellValueAsString(row.getCell(0));
String name = getCellValueAsString(row.getCell(1));
String studentNumber = row.getCell(0).getStringCellValue();
String name = row.getCell(1).getStringCellValue();
// 如果有积分列,读取积分,如果没有积分列或者积分为空,则默认积分为 0
BigDecimal points = BigDecimal.ZERO;
@ -50,8 +50,6 @@ public class StudentServiceImpl implements StudentService {
Cell pointsCell = row.getCell(2);
if (pointsCell.getCellType() == CellType.NUMERIC) {
points = new BigDecimal(pointsCell.getNumericCellValue());
} else if (pointsCell.getCellType() == CellType.STRING) {
points = new BigDecimal(pointsCell.getStringCellValue());
}
}
@ -68,29 +66,6 @@ public class StudentServiceImpl implements StudentService {
studentMapper.saveStudents(students);
}
// 辅助方法:获取单元格的字符串值
private String getCellValueAsString(Cell cell) {
if (cell == null) {
return "";
}
// 使用 DataFormatter 处理字符串和基本的数字格式问题
DataFormatter formatter = new DataFormatter();
String cellValue = formatter.formatCellValue(cell);
// 额外处理数值型,确保学号不会以科学计数法的形式返回
if (cell.getCellType() == CellType.NUMERIC) {
// 强制将数值转换为不使用科学计数法的字符串
BigDecimal bd = new BigDecimal(cell.getNumericCellValue());
cellValue = bd.toPlainString(); // 使用 toPlainString() 避免科学计数法
}
return cellValue;
}
@Override
public void exportStudents(OutputStream outputStream) throws Exception {
List<Student> students = studentMapper.findAll();
@ -108,19 +83,17 @@ public class StudentServiceImpl implements StudentService {
int rowNum = 1;
for (Student student : students) {
Row row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue(student.getStudentNumber()); // 确保学号不为空
row.createCell(0).setCellValue(student.getStudentNumber());
row.createCell(1).setCellValue(student.getName());
row.createCell(2).setCellValue(student.getPoints() != null ? student.getPoints().doubleValue() : 0.0);
row.createCell(2).setCellValue(student.getPoints().doubleValue());
}
// 将数据写入输出流
workbook.write(outputStream);
}
deleteAllStudents(); // 导出后删除所有学生
}
/**
*
*
* @param studentNumber
* @param pointsDelta
@ -137,12 +110,15 @@ public class StudentServiceImpl implements StudentService {
}
/**
*
* @return
*
* @param page
* @param size
* @return
*/
@Override
public List<Student> getStudentRanking() {
return studentMapper.findAllStudentsByRanking();
public List<Student> getStudentRanking(int page, int size) {
int offset = page * size;
return studentMapper.findStudentsByRanking(offset, size);
}
@Override
@ -183,9 +159,4 @@ public class StudentServiceImpl implements StudentService {
public String[] getAllStudentNames() {
return studentMapper.findAllStudentNames();
}
@Override
public void deleteAllStudents() {
studentMapper.deleteAllStudents();
}
}

@ -11,14 +11,11 @@ import org.springframework.stereotype.Service;
@Service
public class TeacherServiceImpl implements TeacherService {
private final TeacherMapper teacherMapper;
private final PasswordEncoder passwordEncoder; // 构造函数注入
@Autowired
private TeacherMapper teacherMapper;
@Autowired
public TeacherServiceImpl(TeacherMapper teacherMapper, PasswordEncoder passwordEncoder) {
this.teacherMapper = teacherMapper;
this.passwordEncoder = passwordEncoder;
}
private PasswordEncoder passwordEncoder; // 使用 PasswordEncoder 接口
@Override
public void register(String username, String password) {
@ -37,9 +34,4 @@ public class TeacherServiceImpl implements TeacherService {
// 返回 JWT token
return JWTUtil.generateToken(teacher);
}
@Override
public Teacher findByUsername(String username) {
return teacherMapper.findByUsername(username);
}
}

@ -1,19 +1,17 @@
package com.example.attendance.util;
import com.example.attendance.entity.Teacher;
import io.jsonwebtoken.*;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
@Slf4j
public class JWTUtil {
private static final String SECRET_KEY_STRING = "MineVeryLongAndSecureSecretKeyHere1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor(SECRET_KEY_STRING.getBytes(StandardCharsets.UTF_8));
private static final SecretKey SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
// 生成 Token
public static String generateToken(Teacher teacher) {
@ -21,31 +19,19 @@ public class JWTUtil {
.setSubject(teacher.getUsername()) // 设置 Token 主题(用户名)
.setIssuedAt(new Date()) // 设置 Token 签发时间
.setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 1000)) // Token 有效期 1 小时
.signWith(SECRET_KEY, SignatureAlgorithm.HS256) // 使用 HS256 签名算法和 SECRET_KEY 签名
.signWith(SignatureAlgorithm.HS256, SECRET_KEY) // 使用 HS256 签名算法和 SECRET_KEY 签名
.compact();
}
// 从 Token 中提取声明
public static Claims extractClaims(String token) {
try {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.setSigningKey(SECRET_KEY) // 设置签名密钥
.parseClaimsJws(token) // 解析 Token
.getBody();
} catch (io.jsonwebtoken.io.DecodingException e) {
log.error("JWT解码失败: {}", e.getMessage());
throw new RuntimeException("无效的JWT token: 解码失败", e);
} catch (io.jsonwebtoken.security.SecurityException | io.jsonwebtoken.MalformedJwtException e) {
log.error("无效的JWT签名: {}", e.getMessage());
throw new RuntimeException("无效的JWT token: 签名验证失败", e);
} catch (io.jsonwebtoken.ExpiredJwtException e) {
log.error("JWT token已过期: {}", e.getMessage());
throw new RuntimeException("JWT token已过期", e);
} catch (io.jsonwebtoken.UnsupportedJwtException e) {
log.error("不支持的JWT token: {}", e.getMessage());
throw new RuntimeException("不支持的JWT token", e);
} catch (IllegalArgumentException e) {
log.error("JWT claims字符串为空: {}", e.getMessage());
throw new RuntimeException("JWT claims字符串为空", e);
} catch (Exception e) {
throw new RuntimeException("Invalid JWT token", e); // 捕获并抛出异常
}
}

@ -1,13 +1,7 @@
server.port=8080
server.address=0.0.0.0
spring.application.name=attendance
spring.datasource.url=jdbc:mysql://localhost:3306/roll_call_system
spring.datasource.username=root
spring.datasource.password=123456789jk
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# JWT ???????????
jwt.expiration=6048000
# JWT ?????
jwt.tokenHead=Bearer
mybatis.configuration.map-underscore-to-camel-case=true

@ -0,0 +1,24 @@
package com.example.attendance;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class PasswordEncryptor {
public static void main(String[] args) {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
// 这里使用你希望的明文密码
String rawPassword1 = "123456";
String rawPassword2 = "654321";
String rawPassword3 = "admin123";
// 加密密码
String encodedPassword1 = passwordEncoder.encode(rawPassword1);
String encodedPassword2 = passwordEncoder.encode(rawPassword2);
String encodedPassword3 = passwordEncoder.encode(rawPassword3);
// 打印加密后的密码
System.out.println("teacher1 密码: " + encodedPassword1);
System.out.println("teacher2 密码: " + encodedPassword2);
System.out.println("teacher3 密码: " + encodedPassword3);
}
}

@ -1,119 +0,0 @@
package com.example.attendance;
import com.example.attendance.entity.RollCallResponse;
import com.example.attendance.entity.RollCallSettings;
import com.example.attendance.entity.Student;
import com.example.attendance.service.impl.RollCallServiceImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.MockitoAnnotations;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class RollCallServiceImplTest {
@InjectMocks
private RollCallServiceImpl rollCallService;
private List<Student> students;
private RollCallSettings settings;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
// 模拟学生列表
students = Arrays.asList(
createStudent("1", "Alice", 50),
createStudent("2", "Bob", 90),
createStudent("3", "Charlie", 30)
);
// 初始化点名设置
settings = new RollCallSettings();
settings.setRollCallMode("点名");
settings.setTriggerRandomEvent("不触发");
settings.setWheelOfFortune("否");
}
// 测试正常点名模式
@Test
void testStartRollCall_NormalMode() {
for (int i = 0; i < 10; i++) {
RollCallResponse response = rollCallService.startRollCall(students, settings);
// 控制台输出
System.out.println("Test: Normal Mode");
System.out.println("Student ID: " + response.getStudentId());
System.out.println("Message: " + response.getMessage());
assertNotNull(response);
assertNotNull(response.getStudentId());
assertTrue(response.getMessage().contains("没有触发随机事件"));
}
}
// 测试提问模式
@Test
void testStartRollCall_QuestionMode() {
settings.setRollCallMode("提问"); // 设置为提问模式
RollCallResponse response = rollCallService.startRollCall(students, settings);
// 控制台输出
System.out.println("Test: Question Mode");
System.out.println("Student ID: " + response.getStudentId());
System.out.println("Message: " + response.getMessage());
assertNotNull(response);
assertNotNull(response.getStudentId());
assertTrue(response.getMessage().contains("没有触发随机事件"));
}
// 测试命运轮盘模式
@Test
void testStartRollCall_WheelOfFortune() {
settings.setWheelOfFortune("是"); // 开启命运轮盘
RollCallResponse response = rollCallService.startRollCall(students, settings);
// 控制台输出
System.out.println("Test: Wheel of Fortune Mode");
System.out.println("Student ID: " + response.getStudentId());
System.out.println("Message: " + response.getMessage());
assertNotNull(response);
assertNotNull(response.getStudentId());
assertTrue(response.getMessage().contains("命运轮盘"));
}
// 测试触发随机事件
@Test
void testStartRollCall_TriggerRandomEvent() {
settings.setTriggerRandomEvent("触发"); // 触发随机事件
RollCallResponse response = rollCallService.startRollCall(students, settings);
// 控制台输出
System.out.println("Test: Random Event Triggered");
System.out.println("Student ID: " + response.getStudentId());
System.out.println("Message: " + response.getMessage());
assertNotNull(response);
assertNotNull(response.getStudentId());
assertTrue(response.getMessage().contains("触发了随机事件"));
}
// 辅助方法,创建学生
private Student createStudent(String id, String name, int points) {
Student student = new Student();
student.setStudentNumber(id);
student.setName(name);
student.setPoints(BigDecimal.valueOf(points));
return student;
}
}

@ -0,0 +1,162 @@
package com.example.attendance;
import com.example.attendance.entity.Student;
import com.example.attendance.mapper.StudentMapper;
import com.example.attendance.service.impl.StudentServiceImpl;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class StudentServiceImplTest {
@InjectMocks
private StudentServiceImpl studentService;
@Mock
private StudentMapper studentMapper;
@Mock
private MultipartFile file;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void testImportStudents() throws Exception {
// 模拟 Excel 文件内容
XSSFWorkbook workbook = new XSSFWorkbook();
var sheet = workbook.createSheet("Sheet 1");
var header = sheet.createRow(0);
header.createCell(0).setCellValue("Student Number");
header.createCell(1).setCellValue("Name");
header.createCell(2).setCellValue("Points");
var row1 = sheet.createRow(1);
row1.createCell(0).setCellValue("1");
row1.createCell(1).setCellValue("Alice");
row1.createCell(2).setCellValue(50);
var row2 = sheet.createRow(2);
row2.createCell(0).setCellValue("2");
row2.createCell(1).setCellValue("Bob");
row2.createCell(2).setCellValue(80);
// 将 workbook 写入 ByteArrayOutputStream
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
workbook.write(outputStream);
workbook.close(); // 关闭 workbook
// 将 ByteArrayOutputStream 转换为 InputStream
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
when(file.getInputStream()).thenReturn(inputStream);
// 调用 importStudents 方法
studentService.importStudents(file);
// 验证是否保存了学生数据
verify(studentMapper, times(1)).saveStudents(anyList());
}
// 测试调整积分
@Test
void testAdjustPoints() {
// 模拟学生数据
Student student = new Student();
student.setStudentNumber("1");
student.setPoints(BigDecimal.valueOf(50));
when(studentMapper.findByStudentNumber("1")).thenReturn(student);
// 调用 adjustPoints 方法
studentService.adjustPoints("1", BigDecimal.valueOf(10));
// 验证是否更新了学生数据
assertEquals(BigDecimal.valueOf(60), student.getPoints());
verify(studentMapper, times(1)).update(student);
}
// 测试获取学生排行榜
@Test
void testGetStudentRanking() {
List<Student> mockStudents = new ArrayList<>();
Student student1 = new Student();
student1.setName("Alice");
student1.setPoints(BigDecimal.valueOf(100));
Student student2 = new Student();
student2.setName("Bob");
student2.setPoints(BigDecimal.valueOf(90));
mockStudents.add(student1);
mockStudents.add(student2);
when(studentMapper.findStudentsByRanking(0, 2)).thenReturn(mockStudents);
// 调用 getStudentRanking 方法
List<Student> ranking = studentService.getStudentRanking(0, 2);
// 验证返回结果是否正确
assertEquals(2, ranking.size());
assertEquals("Alice", ranking.get(0).getName());
assertEquals("Bob", ranking.get(1).getName());
}
// 测试根据学生编号查找
@Test
void testFindByStudentNumber() {
Student student = new Student();
student.setStudentNumber("1");
student.setName("Alice");
when(studentMapper.findByStudentNumber("1")).thenReturn(student);
// 调用 findByStudentNumber 方法
Student result = studentService.findByStudentNumber("1");
// 验证结果
assertNotNull(result);
assertEquals("Alice", result.getName());
}
// 测试保存学生
@Test
void testSave() {
Student student = new Student();
student.setName("Alice");
// 调用 save 方法
studentService.save(student);
// 验证是否保存了学生
verify(studentMapper, times(1)).save(student);
}
// 测试删除学生
@Test
void testDelete() {
Long id = 1L;
// 调用 delete 方法
studentService.delete(id);
// 验证是否删除了学生
verify(studentMapper, times(1)).delete(id);
}
}

@ -0,0 +1,120 @@
package com.example.attendance;
import com.example.attendance.entity.Teacher;
import com.example.attendance.mapper.TeacherMapper;
import com.example.attendance.service.impl.TeacherServiceImpl;
import com.example.attendance.util.JWTUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.security.crypto.password.PasswordEncoder;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
class TeacherServiceImplTest {
@InjectMocks
private TeacherServiceImpl teacherService; // 使用 @InjectMocks 注入测试的服务类
@Mock
private TeacherMapper teacherMapper; // 模拟 TeacherMapper
@Mock
private PasswordEncoder passwordEncoder; // 模拟 PasswordEncoder
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this); // 初始化 mocks
}
// 测试注册功能
@Test
void testRegister() {
// 模拟加密的密码
String rawPassword = "password123";
String encodedPassword = "encodedPassword123";
when(passwordEncoder.encode(rawPassword)).thenReturn(encodedPassword);
// 调用注册方法
teacherService.register("testUser", rawPassword);
// 验证 teacherMapper.save 是否被调用,并且密码是加密的
verify(teacherMapper, times(1)).save(any(Teacher.class));
verify(passwordEncoder, times(1)).encode(rawPassword);
}
// 测试登录功能
@Test
void testLogin_Success() {
// 模拟数据库中保存的教师
Teacher teacher = new Teacher();
teacher.setUsername("testUser");
teacher.setPassword("encodedPassword123");
// 模拟找到用户,并且密码匹配
when(teacherMapper.findByUsername("testUser")).thenReturn(teacher);
when(passwordEncoder.matches("password123", "encodedPassword123")).thenReturn(true);
// 模拟生成的 JWT token
String token = "mockedJWTToken";
mockStatic(JWTUtil.class); // mock 静态方法
when(JWTUtil.generateToken(teacher)).thenReturn(token);
// 调用登录方法
String resultToken = teacherService.login("testUser", "password123");
// 验证生成的 token 是否正确
assertEquals(token, resultToken);
// 验证方法调用
verify(teacherMapper, times(1)).findByUsername("testUser");
verify(passwordEncoder, times(1)).matches("password123", "encodedPassword123");
}
// 测试登录失败(用户名不正确)
@Test
void testLogin_Fail_UserNotFound() {
// 模拟未找到用户
when(teacherMapper.findByUsername("nonExistentUser")).thenReturn(null);
// 调用登录方法并捕获异常
Exception exception = assertThrows(RuntimeException.class, () -> {
teacherService.login("nonExistentUser", "password123");
});
// 验证异常消息
assertEquals("用户名或密码不正确", exception.getMessage());
// 验证 teacherMapper.findByUsername 被调用一次
verify(teacherMapper, times(1)).findByUsername("nonExistentUser");
}
// 测试登录失败(密码不匹配)
@Test
void testLogin_Fail_WrongPassword() {
// 模拟找到用户,但密码不匹配
Teacher teacher = new Teacher();
teacher.setUsername("testUser");
teacher.setPassword("encodedPassword123");
when(teacherMapper.findByUsername("testUser")).thenReturn(teacher);
when(passwordEncoder.matches("wrongPassword", "encodedPassword123")).thenReturn(false);
// 调用登录方法并捕获异常
Exception exception = assertThrows(RuntimeException.class, () -> {
teacherService.login("testUser", "wrongPassword");
});
// 验证异常消息
assertEquals("用户名或密码不正确", exception.getMessage());
// 验证方法调用
verify(teacherMapper, times(1)).findByUsername("testUser");
verify(passwordEncoder, times(1)).matches("wrongPassword", "encodedPassword123");
}
}

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/点名.svg">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,22 @@
{
"name": "name-nacing-system",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.7.7",
"element-plus": "^2.8.4",
"element-ui": "^2.15.14",
"vue": "^3.4.29",
"vue-router": "^4.3.3"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.5",
"vite": "^5.3.1"
}
}

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

@ -0,0 +1,26 @@
<template>
<div>
<router-view/>
</div>
</template>
<style>
/* 让页面铺满整个屏幕,不出现滚动条 */
html,
body,
#app {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: auto;
}
</style>
<script>
export default {
name: "Layout",
}
</script>

@ -0,0 +1,121 @@
.login-register-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-image: linear-gradient(to right, rgb(219, 219, 245), #f4dfe3);
}
.form-box {
background: rgba(211, 197, 252, 1);
padding: 100px 110px;
padding-top: 15px;
border-radius: 40px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
width: 350px;
}
h2 {
text-align: center;
color: #FFFFFF;
margin-bottom: 30px;
font-size: 45px;
}
.tab-switch {
display: flex;
margin-bottom: 20px;
}
.tab-switch span {
margin-right: 25px;
font-size: 22px;
padding: 3px 7px;
border-radius: 3px;
cursor: pointer;
color: #FFFFFF;
}
.tab-switch span.active {
border-bottom: 4px solid #FFFFFF;
}
.input-group {
position: relative;
display: flex;
align-items: center;
width: 100%;
padding: 10px 0px;
border-radius: 20px;
box-shadow: 0px 0px 10px 0px #D5E8FF;
background-color: rgba(243, 243, 243, 0.5);
border: 1px solid #FFFFFF;
margin-top: 20px;
}
.input-group input:focus {
outline: none;
/* box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.5); */
}
.input-icon {
width: 25px;
height: 25px;
margin-left: 15px;
margin-right: 5px;
}
.placeholder {
color: #FFFFFF;
}
input {
flex: 1;
height: 100%;
border: none;
background: transparent;
padding: 0 10px;
color: #615EF8;
font-size: 16px;
}
.forgot-password {
text-align: right;
margin-top: 15px;
}
.forgot-password a {
color: #FFFFFF;
text-decoration: none;
}
.submit-btn {
width: 100%;
padding: 12px 0;
margin-top: 15px;
background-color: #8755F2;
color: #FFFFFF;
border-radius: 20px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
box-shadow: 0px 0px 10px 0px #D5E8FF;
border: 1px solid #FFFFFF;
}
.submit-btn:hover {
background-color: #6231F5;
}
/* 隐藏默认的密码切换图标 */
input[type="password"]::-ms-reveal,
input[type="password"]::-ms-clear,
input[type="password"]::-webkit-contacts-auto-fill-button,
input[type="password"]::-webkit-credentials-auto-fill-button {
display: none !important;
}
/* 对于Chrome浏览器 */
input[type="password"]::-webkit-inner-spin-button,
input[type="password"]::-webkit-outer-spin-button,
input[type="password"]::-webkit-search-cancel-button {
-webkit-appearance: none;
margin: 0;
}

Before

Width:  |  Height:  |  Size: 27 MiB

After

Width:  |  Height:  |  Size: 27 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

@ -0,0 +1,21 @@
// 引入createApp用于创建应用
import { createApp } from 'vue'
// 引入App.vue文件
import App from './App.vue'
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
// new Vue({
// el: '#app',
// render: h => h(App)
// });
const app = createApp(App)
app.use(router)
app.use(ElementPlus)
app.mount('#app')

@ -0,0 +1,38 @@
import { createRouter, createWebHistory} from 'vue-router'
// 引入路由组件
import login from '@/views/login2.vue'
import home from '@/views/home.vue'
import importFile from '@/views/importFile.vue'
import ruleSetting from '@/views/ruleSetting.vue'
// 创建路由器
const router = createRouter({
// 路由器的工作模式
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
// name: 'home',
component: login
},
{
path:'/home',
component: home,
children:[
{
path:'importFile',
component: importFile
},
{
path:'ruleSetting',
component: ruleSetting
}
]
}
]
})
export default router

@ -0,0 +1,18 @@
// 存储、获取、删除token的文件
// 定义 setToken 函数,用于将 token 存储到 localStorage
export function setToken(token) {
localStorage.setItem('authToken', token);
}
// 定义 getToken 函数,用于从 localStorage 获取 token
export function getToken() {
return localStorage.getItem('authToken');
}
// 定义 removeToken 函数,用于从 localStorage 删除 token
export function removeToken() {
localStorage.removeItem('authToken');
}

@ -0,0 +1,89 @@
// axiosConfig.js
import axios from 'axios';
import {getToken} from '@/token/auth' // 注意这里使用了解构赋值来导入getToken函数
// 创建axios实例
const service = axios.create({
baseURL: 'http://example.com/api', // 配置基础URL
timeout: 5000, // 请求超时时间
});
// 请求拦截器
service.interceptors.request.use(
config => {
// 在发送请求之前做些什么
const token = getToken(); // 获取token的方式取决于你的应用
if (token) {
config.headers['Authorization'] = `Bearer ${token}`; // 设置token
}
return config;
},
error => {
// 对请求错误做些什么
console.error('Request Error:', error);
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
response => {
// 对响应数据做点什么
const res = response.data;
// 你可以根据实际情况在这里添加一些通用的响应处理逻辑
// 例如,根据返回的状态码判断请求是否成功
if (res.code !== 200) {
// 业务错误处理,比如弹窗提示等
return Promise.reject(new Error(res.message || 'Error'));
} else {
return res;
}
},
error => {
// 对响应错误做点什么
if (error.response) {
// 请求已发出但服务器响应的状态码不在2xx的范围
console.error('Error status:', error.response.status);
console.error('Error data:', error.response.data);
} else if (error.request) {
// 请求已发出,但没有收到响应
console.error('Error request:', error.request);
} else {
// 在设置请求时触发错误
console.error('Error message:', error.message);
}
return Promise.reject(error);
}
);
export default service;
// 使用
// 导入封装好的axios实例
// import axios from './axiosConfig';
// 登录方法
// methods: {
// async login() {
// try {
// const response = await axios.post('/api/login', {
// username: this.username,
// password: this.password
// });
// // 假设token在响应的data字段中
// const token = response.data.token;
// // 存储token
// setToken(token);
// // 登录成功后的操作,比如跳转到主页
// this.$router.push('/home');
// } catch (error) {
// // 处理登录错误
// console.error('Login Error:', error);
// }
// }
// }

@ -0,0 +1,80 @@
<template>
<div class="home">
<div class="content">
<!-- 展示区 -->
<div class="leftIcon">
<RouterView></RouterView>
</div>
<!-- 导航栏 -->
<div class="select">
<div @click="gotoImpt" class="selectItem">
导入文件
</div>
<div class="selectItem">
开始点名
</div>
<div @click="gotoRule" class="selectItem">
规则设置
</div>
<div class="selectItem">
查看排行
</div>
</div>
</div>
</div>
</template>
<script>
import { RouterLink, RouterView } from 'vue-router';
export default {
data() {
return {
}
},
methods: {
gotoImpt(){
this.$router.push('/home/importFile')
},
gotoRule() {
this.$router.push('/home/ruleSetting')
}
}
}
</script>
<style scoped>
.home {
background-image: url('../assets/image/bgcImg.png'); /* 图片路径 */
width:100%;
height:100%;
position:fixed;
background-size:100% 100%;
}
.content {
height:100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 120px;
}
.leftIcon {
width: 700px;
height: 800px;
/* background-color: red; */
}
.selectItem {
padding: 25px 130px;
border-radius: 50px;
background-color: #A182FF;
color: #FFFFFF;
font-size: 35px;
font-weight: bold;
margin-bottom: 65px;
cursor: pointer;
text-decoration: none;
}
.selectItem:hover {
background-color: #6231F5;
}
</style>

@ -0,0 +1,206 @@
<template>
<div class="imptFile">
<!-- 头部 -->
<div class="header">
<div>导入学生名单</div>
<img src="../assets/image/close.png" alt="" class="icon" @click="close">
</div>
<div style="border: 1.5px solid #C8C1C1;"></div>
<!-- 上传文件 -->
<el-upload
ref="uploadRef"
class="upload-demo"
action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
:auto-upload="false"
:limit="1"
accept=".xlsx, .xls"
:on-exceed="handleExceed"
>
<div style="font-size: 30px; color: #7D7878;">选择文件</div>
<input placeholder="请选择上传的文件" class="inputFile" />
<!-- <template #trigger> -->
<el-button type="primary" color="#A182FF" class="btn">选取文件</el-button>
<!-- </template> -->
<!-- 按钮下面放置文件区 -->
<!-- <el-button class="ml-3" type="success" @click="submitUpload">
upload to server
</el-button> -->
<template #tip>
<div class="el-upload__tip">
请上传Excel文件文件大小不超过500KB
</div>
</template>
</el-upload>
<!-- 下载模板 -->
<a
class="download"
href="后端提供的下载URL"
download="student-template.xlsx"
@click="onDownload"
>
模板下载
</a>
<!-- 模板示例 -->
<div>
<div style="color: #8755F2; font-size: 28px; margin: 30px 0 0 40px;">模板示例</div>
<div style="display: flex; justify-content: center; margin-top:10px;">
<img src="../assets/image/muban.png" alt="" style="height: 250px;">
</div>
</div>
<!-- 底部 -->
<div class="bottom">
<div style="display: flex;">
<div class="confirm" @click="submitUpload"></div>
<div class="cancel" @click="close"></div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { genFileId } from 'element-plus'
import type { UploadInstance, UploadProps, UploadRawFile } from 'element-plus'
import router from '@/router';
const uploadRef = ref<UploadInstance>()
//
const handleExceed: UploadProps['onExceed'] = (files) => {
uploadRef.value!.clearFiles()
const file = files[0] as UploadRawFile
file.uid = genFileId()
uploadRef.value!.handleStart(file)
}
//
const submitUpload = () => {
uploadRef.value!.submit()
}
//
const onDownload = (event: MouseEvent) => {
//
//
// <a>href
event.preventDefault();
// 使fetchhref
window.open("后端提供的下载URL", '_blank');
}
function close() {
router.push('/home')
}
</script>
<style scoped>
.imptFile{
position: relative;
background-color: #fff;
border-radius: 40px;
height: 100%;
padding-top: 10px;
box-shadow: 5px 10px 10px 5px rgba(2, 2, 2, 0.1);
overflow: hidden;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 60px;
font-size: 35px;
font-weight: bold;
}
.icon {
width: 45px;
cursor: pointer;
}
.icon:hover {
transform: scale(1.3);
}
.upload-demo {
margin-top: 35px;
padding: 0 20px;
}
.inputFile {
height: 60px;
width: 300px;
font-size: 23px;
margin-left: 20px;
border-radius: 10px;
border: 1.5px solid #272636;
padding-left: 20px;
}
.inputFile::placeholder {
padding-left: 0;
}
.btn {
height: 70px;
border-radius: 10px;
padding: 10px;
font-size: 30px;
margin-left: 30px;
color: #fff;
}
.btn:hover {
color: #fff;
transform: scale(1.1);
}
.el-upload__tip {
font-size: 23px;
margin: 25px 0 25px 70px;
}
.download{
color: #8755F2;
font-size: 28px;
margin: 20px 0 0 40px;
cursor: pointer;
text-decoration: none;
}
.download:hover {
color: #6231F5;
text-decoration: underline;
}
.bottom{
position: absolute;
display: flex; align-items: center; justify-content: right;
bottom: 0;
width: 100%;
padding: 30px 30px;
background-color: #EBE4FF;
}
.cancel {
width: auto;
border-radius: 5px;
padding: 20px 35px;
margin-right: 70px;
font-size: 28px;
background-color: #fff;
color: #A182FF;
cursor: pointer;
}
.cancel:hover {
background-color: #dfdede;
}
.confirm{
width: auto;
border-radius: 5px;
padding: 20px 35px;
margin-right: 30px;
font-size: 28px;
background-color: #A182FF;
color: #fff;
cursor: pointer;
}
.confirm:hover {
background-color: #6231F5;
}
</style>

@ -0,0 +1,187 @@
<template>
<div class="login-register-container">
<div class="form-box">
<h2>让我们开始点名</h2>
<div class="tab-switch">
<span
:class="{ active: activeTab === 'login' }"
@click="switchTab('login')"
>
登录
</span>
<span
:class="{ active: activeTab === 'register' }"
@click="switchTab('register')"
>
注册
</span>
</div>
<!-- 登录表单 -->
<form v-if="activeTab === 'login'" @submit.prevent="handleLogin">
<div class="input-group">
<img src="../assets/image/person.png" alt="" class="input-icon">
<input
v-model="loginForm.phone"
type="text"
placeholder="请输入您的手机号"
placeholder-class="placeholder"
>
</div>
<div class="input-group">
<img src="../assets/image/password.png" alt="" class="input-icon">
<input
v-model="loginForm.password"
:type="showPassword ? 'text' : 'password'"
placeholder="请输入您的密码"
>
<img src="../assets/image/eye.png"
v-if="showPassword"
class="input-icon"
@click="togglePasswordVisibility"
style="cursor: pointer;margin-right: 15px;"
>
<img src="../assets/image/eyeClose.png"
v-if="!showPassword"
class="input-icon"
@click="togglePasswordVisibility"
style="cursor: pointer;margin-right: 15px;"
>
</div>
<div class="forgot-password">
<a href="#">忘记密码?</a>
</div>
<button type="submit" class="submit-btn"> </button>
</form>
<!-- 注册表单 -->
<form v-else @submit.prevent="handleRegister">
<div class="input-group">
<img src="../assets/image/person.png" alt="" class="input-icon">
<input
v-model="registerForm.phone"
type="text"
placeholder="请输入您的手机号"
>
</div>
<div class="input-group">
<img src="../assets/image/password.png" alt="" class="input-icon">
<input
v-model="registerForm.password"
:type="showPassword ? 'text' : 'password'"
placeholder="请输入您的密码"
>
<img src="../assets/image/eye.png"
v-if="showPassword"
class="input-icon"
@click="togglePasswordVisibility"
style="cursor: pointer;margin-right: 15px;"
>
<img src="../assets/image/eyeClose.png"
v-if="!showPassword"
class="input-icon"
@click="togglePasswordVisibility"
style="cursor: pointer;margin-right: 15px;"
>
</div>
<div class="input-group">
<img src="../assets/image/password.png" alt="" class="input-icon">
<input
v-model="registerForm.confirmPassword"
:type="showConfirmPassword ? 'text' : 'password'"
placeholder="再次确认密码"
>
<img src="../assets/image/eye.png"
v-if="showPassword"
class="input-icon"
@click="togglePasswordVisibility"
style="cursor: pointer;margin-right: 15px;"
>
<img src="../assets/image/eyeClose.png"
v-if="!showPassword"
class="input-icon"
@click="togglePasswordVisibility"
style="cursor: pointer;margin-right: 15px;"
>
</div>
<button type="submit" class="submit-btn" style="margin-top: 25px;">注册</button>
</form>
</div>
</div>
</template>
<script>
import axios from '@/utils/axiosConfig';
import {setToken} from '@/token/auth'
export default {
data() {
return {
activeTab: 'login',
showPassword: false,
showConfirmPassword: false,
loginForm: {
phone: '',
password: ''
},
registerForm: {
phone: '',
password: '',
confirmPassword: ''
}
}
},
methods: {
switchTab(tab) {
this.activeTab = tab;
},
togglePasswordVisibility() {
this.showPassword = !this.showPassword;
},
//
async handleLogin() {
try {
const response = await axios.post('/teacher/login', {
username: this.loginForm.phone,
password: this.loginForm.password
});
// tokendata
const token = response.data.token;
// token
setToken(token);
//
this.$router.push('/home');
} catch (error) {
//
console.error('Login Error:', error);
}
},
//
async handleRegister() {
if (this.registerForm.password !== this.registerForm.confirmPassword) {
alert('两次输入的密码不一致');
return;
}
try {
//
const response = await axios.post('/teacher/register', {
username: this.registerForm.phone,
password: this.registerForm.password,
});
//
console.log('注册成功', response);
} catch (error) {
//
console.error('注册失败', error);
}
}
}
}
</script>
<style scoped>
@import '../assets/css/login.css'
</style>

@ -0,0 +1,192 @@
<template>
<div class="imptFile">
<!-- 头部 -->
<div class="header">
<div>规则设置</div>
<img src="../assets/image/close.png" alt="" class="icon" @click="close">
</div>
<div style="border: 1.5px solid #C8C1C1;"></div>
<!-- 问题 -->
<el-form :model="form" label-position="top">
<el-form-item label="进行点名 or 提问 ">
<el-radio-group v-model="form.nameOrQuestion">
<el-radio label="点名">点名</el-radio>
<el-radio label="提问">提问</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="是否触发随机事件 ">
<el-radio-group v-model="form.triggerRandomEvent">
<el-radio label="触发">触发</el-radio>
<el-radio label="不触发">不触发</el-radio>
</el-radio-group>
</el-form-item>
<!-- 最后一个问题 -->
<el-tooltip
content="今天众生平等!所有人被抽到的概率相等"
placement="bottom-start"
effect="dark"
>
<div class="lastQues">是否开启&quot;命运轮盘&quot; </div>
</el-tooltip>
<el-radio-group v-model="form.enableFateWheel" class="lastOpt">
<el-radio label="是"></el-radio>
<el-radio label="否"></el-radio>
</el-radio-group>
</el-form>
<!-- 底部 -->
<div class="bottom">
<div style="display: flex;">
<div class="confirm" @click="confirmSetting"></div>
<div class="cancel" @click="close"></div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import router from '@/router';
import { ref } from 'vue'
import { ElTooltip } from 'element-plus'
const form = ref({
nameOrQuestion: '点名',
triggerRandomEvent: '触发',
enableFateWheel: '否'
})
const confirmSetting = () => {
// ,
const data = {
nameOrQuestion: form.value.nameOrQuestion,
triggerRandomEvent: form.value.triggerRandomEvent,
enableFateWheel: form.value.enableFateWheel,
}
// form
console.log(form.value.nameOrQuestion)
// API
console.log('发送到后端的数据:', data)
}
function close() {
router.push('/home')
}
</script>
<style scoped>
.imptFile{
position: relative;
background-color: #fff;
border-radius: 40px;
height: 100%;
padding-top: 10px;
box-shadow: 5px 10px 10px 5px rgba(2, 2, 2, 0.1);
overflow: hidden;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 60px;
font-size: 35px;
font-weight: bold;
}
.icon {
width: 45px;
cursor: pointer;
}
.icon:hover {
transform: scale(1.3);
}
.bottom{
position: absolute;
display: flex; align-items: center; justify-content: right;
bottom: 0;
width: 100%;
padding: 30px 30px;
background-color: #EBE4FF;
}
.cancel {
width: auto;
border-radius: 5px;
padding: 20px 35px;
margin-right: 70px;
font-size: 28px;
background-color: #fff;
color: #A182FF;
cursor: pointer;
}
.cancel:hover {
background-color: #dfdede;
}
.confirm{
width: auto;
border-radius: 5px;
padding: 20px 35px;
margin-right: 30px;
font-size: 28px;
background-color: #A182FF;
color: #fff;
cursor: pointer;
}
.confirm:hover {
background-color: #6231F5;
}
/* add */
.el-form-item {
margin: 50px 0 0 60px;
}
.el-radio {
margin: 45px 80px;
margin-bottom: 0;
}
/* 最后一个问题 */
.lastQues {
margin: 50px 0 0 60px;
font-size: 35px; /* 调整为你想要的大小 */
font-weight: bold;
color: #7D7878;
cursor: pointer;
}
.lastOpt{
margin-left: 60px;
}
:deep(.el-form-item__label) {
font-size: 35px; /* 调整为你想要的大小 */
font-weight: bold;
color: #7D7878;
}
/* 如果你也想改变输入框内的文字大小 */
:deep(.el-radio__label) {
color: #8755F2;
font-size: 35px;font-weight: bold;
}
:deep(.el-radio__inner) {
width: 20px; /* 调整单选按钮的宽度 */
height: 20px; /* 调整单选按钮的高度 */
border-color: #8755F2;font-weight: bold;
}
/* 改变选中时的颜色 */
/* 单选框 */
:deep(.el-radio__input.is-checked .el-radio__inner) {
border-color: #6231F5;
background: #6231F5;
}
/* 选项文字 */
:deep(.el-radio__input.is-checked+.el-radio__label) {
color: #6231F5;
}
</style>

@ -0,0 +1,16 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})

@ -1,141 +0,0 @@
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.3</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>attendance</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>attendance</name>
<description>attendance</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis Starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!-- Spring Boot DevTools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- MySQL Connector -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Boot Starter Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- MyBatis Starter Test -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>3.0.3</version>
<scope>test</scope>
</dependency>
<!-- EasyExcel -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.0.5</version>
</dependency>
<!-- Spring Security for password encryption -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT Library -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- BCrypt for password hashing -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Compiler Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>17</release> <!-- 设置 release 参数来简化配置 -->
</configuration>
</plugin>
<!-- Spring Boot Maven Plugin -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

@ -1,13 +0,0 @@
package com.example.attendance;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AttendanceApplication {
public static void main(String[] args) {
SpringApplication.run(AttendanceApplication.class, args);
}
}

@ -1,33 +0,0 @@
package com.example.attendance.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // 关闭 CSRF 保护
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/teacher/register", "/api/teacher/login").permitAll() // 允许注册和登录接口匿名访问
.anyRequest().authenticated() // 其他请求需要认证
)
.formLogin(form -> form.disable()) // 关闭表单登录
.httpBasic(httpBasic -> httpBasic.disable()); // 关闭基本认证
return http.build();
}
}

@ -1,24 +0,0 @@
package com.example.attendance.controller;
import com.example.attendance.entity.RollCallResponse;
import com.example.attendance.entity.RollCallSettings;
import com.example.attendance.entity.Student;
import com.example.attendance.service.RollCallService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/rollcall")
public class RollCallController {
private final RollCallService rollCallService;
public RollCallController(RollCallService rollCallService) {
this.rollCallService = rollCallService;
}
@PostMapping("/start")
public RollCallResponse startRollCall(@RequestBody List<Student> students, @RequestBody RollCallSettings settings) {
return rollCallService.startRollCall(students, settings);
}
}

@ -1,178 +0,0 @@
package com.example.attendance.controller;
import com.example.attendance.entity.PointsRequest;
import com.example.attendance.entity.Student;
import com.example.attendance.service.StudentService;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
@RestController
@RequestMapping("/api/students")
public class StudentController {
@Autowired
private StudentService studentService;
//根据id获取学生信息
@GetMapping("/{id}")
public ResponseEntity<Student> getStudentById(@PathVariable Long id) {
Student student = studentService.findById(id);
return ResponseEntity.ok(student);
}
//根据学生编号获取学生信息
@GetMapping("/studentNumber/{studentNumber}")
public ResponseEntity<Student> getStudentByStudentNumber(@PathVariable String studentNumber) {
Student student = studentService.findByStudentNumber(studentNumber);
return ResponseEntity.ok(student);
}
//获取所有学生信息
@GetMapping
public ResponseEntity<List<Student>> getAllStudents() {
List<Student> students = studentService.findAll();
return ResponseEntity.ok(students);
}
/**
*
* @return
*/
@GetMapping("/names")
public ResponseEntity<String[]> getAllStudentNames() {
String[] studentNames = studentService.getAllStudentNames();
return ResponseEntity.ok(studentNames);
}
//添加学生信息
@PostMapping
public ResponseEntity<String> addStudent(@RequestBody Student student) {
studentService.save(student);
return ResponseEntity.ok("Student added successfully");
}
//更新学生信息
@PutMapping("/{studentNumber}")
public ResponseEntity<String> updateStudent(@PathVariable String studentNumber, @RequestBody Student student) {
student.setStudentNumber(studentNumber);
studentService.update(student);
return ResponseEntity.ok("Student updated successfully");
}
//删除学生信息
@DeleteMapping("/{id}")
public ResponseEntity<String> deleteStudent(@PathVariable Long id) {
studentService.delete(id);
return ResponseEntity.ok("Student deleted successfully");
}
/**
*
* @param page
* @param size
* @return
*/
@GetMapping("/ranking")
public ResponseEntity<List<Student>> getStudentRanking(@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
List<Student> ranking = studentService.getStudentRanking(page, size);
return ResponseEntity.ok(ranking);
}
/**
*
* @param studentNumber
* @param pointsRequest
* @return
*/
@PutMapping("/{studentNumber}/adjustPoints")
public ResponseEntity<String> adjustStudentPoints(@PathVariable String studentNumber, @RequestBody PointsRequest pointsRequest) {
try {
studentService.adjustPoints(studentNumber, pointsRequest.getPointsDelta());
return ResponseEntity.ok("积分调整成功");
} catch (Exception e) {
return ResponseEntity.status(500).body("积分调整失败:" + e.getMessage());
}
}
/**
* Excel
* @param file Excel
* @return
*/
@PostMapping("/import")
public String importStudents(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return "请上传有效的Excel文件";
}
try {
studentService.importStudents(file);
return "学生数据导入成功";
} catch (Exception e) {
return "学生数据导入失败:" + e.getMessage();
}
}
/*
*/
@GetMapping("/export-students")
public ResponseEntity<byte[]> exportStudents() throws IOException {
List<Student> students = studentService.findAll(); // 获取所有学生数据
// 创建 Excel 工作簿
Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("Students");
// 创建表头
Row header = sheet.createRow(0);
header.createCell(0).setCellValue("学号");
header.createCell(1).setCellValue("姓名");
header.createCell(2).setCellValue("积分");
// 填充数据
int rowNum = 1;
for (Student student : students) {
Row row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue(student.getStudentNumber());
row.createCell(1).setCellValue(student.getName());
row.createCell(2).setCellValue(student.getPoints().doubleValue());
}
// 将 Excel 文件写入 ByteArrayOutputStream
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
workbook.write(outputStream);
workbook.close();
// 设置 HTTP 头信息
HttpHeaders headers = new HttpHeaders();
headers.setContentDispositionFormData("attachment", "students.xlsx");
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
// 返回文件内容作为响应
return ResponseEntity
.ok()
.headers(headers)
.body(outputStream.toByteArray());
}
/**
* Excel
* @return
*/
@GetMapping("/download-template")
public ResponseEntity<String> getTemplateDownloadLink() {
// 返回百度网盘的模板下载链接
String downloadLink = "https://pan.baidu.com/s/1NUukdPo4qUVbM4V9MWTx2g?pwd=1234";
return ResponseEntity.ok(downloadLink);
}
}

@ -1,38 +0,0 @@
package com.example.attendance.controller;
import com.example.attendance.service.TeacherService;
import com.example.attendance.service.impl.TeacherServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/teacher")
public class TeacherController {
@Autowired
private TeacherService teacherService;
// 注册接口
@PostMapping("/register")
public ResponseEntity<String> register(@RequestParam String username, @RequestParam String password) {
try {
// 调用 teacherService 的 register 方法注册用户
teacherService.register(username, password);
return ResponseEntity.ok("注册成功"); // 成功时返回 200 状态和消息
} catch (Exception e) {
return ResponseEntity.badRequest().body("注册失败: " + e.getMessage()); // 失败时返回 400 状态和错误信息
}
}
// 登录接口
@PostMapping("/login")
public ResponseEntity<String> login(@RequestParam String username, @RequestParam String password) {
try {
// 调用 teacherService 的 login 方法登录
String token = teacherService.login(username, password);
return ResponseEntity.ok("登录成功, Token: " + token); // 成功时返回 200 状态和 JWT token
} catch (Exception e) {
return ResponseEntity.badRequest().body("登录失败: " + e.getMessage()); // 失败时返回 400 状态和错误信息
}
}
}

@ -1,13 +0,0 @@
package com.example.attendance.entity;
import java.math.BigDecimal;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Data
public class PointsRequest {
private BigDecimal pointsDelta;
}

@ -1,18 +0,0 @@
package com.example.attendance.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@NoArgsConstructor
@AllArgsConstructor
@Data
public class RollCallResponse {
private String studentId;
private String name;
private BigDecimal points;
private String message; // 显示结果信息
}

@ -1,15 +0,0 @@
package com.example.attendance.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Data
public class RollCallSettings {
private String rollCallMode; // 点名("点名")或提问("提问")
private String triggerRandomEvent; // 触发("触发")或不触发("不触发")随机事件
private String wheelOfFortune; // 开启("是")或关闭("否")命运轮盘
}

@ -1,17 +0,0 @@
package com.example.attendance.entity;
import java.math.BigDecimal;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Student {
private Long id;
private String studentNumber; // 学号
private String name; // 姓名
private BigDecimal points; // 积分
}

@ -1,15 +0,0 @@
package com.example.attendance.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Teacher {
private Long id;
private String username;
private String password;
}

@ -1,56 +0,0 @@
package com.example.attendance.mapper;
import com.example.attendance.entity.Student;
import org.apache.ibatis.annotations.*;
import java.math.BigDecimal;
import java.util.List;
@Mapper
public interface StudentMapper {
@Select("SELECT * FROM student WHERE id = #{id}")
Student findById(Long id);
@Select("SELECT * FROM student WHERE student_number = #{studentNumber}")
Student findByStudentNumber(String studentNumber);
@Select("SELECT * FROM student")
List<Student> findAll();
@Insert("INSERT INTO student (student_number, name, points) VALUES (#{studentNumber}, #{name}, #{points})")
@Options(useGeneratedKeys = true, keyProperty = "id")
void save(Student student);
@Update("UPDATE student SET name = #{name}, points = #{points} WHERE student_number = #{studentNumber}")
void update(Student student);
@Delete("DELETE FROM student WHERE id = #{id}")
void delete(Long id);
@Insert({
"<script>",
"INSERT INTO student (student_number, name, points) VALUES ",
"<foreach collection='students' item='student' separator=','>",
"(#{student.studentNumber}, #{student.name}, #{student.points})",
"</foreach>",
"</script>"
})
void saveStudents(List<Student> students);
/**
*
* @param offset
* @param size
* @return
*/
@Select("SELECT * FROM student ORDER BY points DESC LIMIT #{size} OFFSET #{offset}")
List<Student> findStudentsByRanking(@Param("offset") int offset, @Param("size") int size);
/**
*
* @return
*/
@Select("SELECT name FROM student")
String[] findAllStudentNames();
}

@ -1,15 +0,0 @@
package com.example.attendance.mapper;
import com.example.attendance.entity.Teacher;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface TeacherMapper {
@Insert("INSERT INTO teacher (username, password) VALUES (#{username}, #{password})")
void save(Teacher teacher);
@Select("SELECT * FROM teacher WHERE username = #{username}")
Teacher findByUsername(String username);
}

@ -1,10 +0,0 @@
package com.example.attendance.service;
import com.example.attendance.entity.RollCallResponse;
import com.example.attendance.entity.RollCallSettings;
import com.example.attendance.entity.Student;
import java.util.List;
public interface RollCallService {
RollCallResponse startRollCall(List<Student> students, RollCallSettings settings); // 处理点名逻辑
}

@ -1,23 +0,0 @@
package com.example.attendance.service;
import com.example.attendance.entity.Student;
import org.springframework.web.multipart.MultipartFile;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.util.List;
public interface StudentService {
void importStudents(MultipartFile file) throws Exception ;
void exportStudents(OutputStream outputStream) throws Exception;
void adjustPoints(String studentNumber, BigDecimal pointsDelta);
List<Student> getStudentRanking(int page, int size);
Student findById(Long id);
Student findByStudentNumber(String studentNumber);
List<Student> findAll();
void save(Student student);
void update(Student student);
void delete(Long id);
String[] getAllStudentNames();
}

@ -1,23 +0,0 @@
package com.example.attendance.service;
public interface TeacherService {
/**
*
*
* @param username
* @param password
* @throws Exception
*/
void register(String username, String password) throws Exception;
/**
*
*
* @param username
* @param password
* @return JWT token
* @throws Exception
*/
String login(String username, String password) throws Exception;
}

@ -1,109 +0,0 @@
package com.example.attendance.service.impl;
import com.example.attendance.entity.RollCallResponse;
import com.example.attendance.entity.RollCallSettings;
import com.example.attendance.entity.Student;
import com.example.attendance.service.RollCallService;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Random;
@Service
public class RollCallServiceImpl implements RollCallService {
@Override
public RollCallResponse startRollCall(List<Student> students, RollCallSettings settings) {
// 1. 根据设定选择点名或提问模式
String mode = "点名".equals(settings.getRollCallMode()) ? "点名" : "提问";
System.out.println("当前模式:" + mode);
// 2. 处理命运轮盘 (所有人概率相等)
if ("是".equals(settings.getWheelOfFortune())) {
return handleWheelOfFortune(students);
}
// 3. 正常点名逻辑,使用权重随机选择学生
Student selectedStudent = selectWeightedRandomStudent(students);
RollCallResponse response = new RollCallResponse();
response.setStudentId(selectedStudent.getStudentNumber());
response.setName(selectedStudent.getName());
response.setPoints(selectedStudent.getPoints());
// 4. 判断是否触发随机事件
if ("触发".equals(settings.getTriggerRandomEvent())) {
response.setMessage("触发了随机事件: " + triggerRandomEvent());
} else {
response.setMessage("没有触发随机事件," + selectedStudent.getName() + " 被点了!");
}
return response;
}
// 权重随机选择学生,积分越高概率越低
private Student selectWeightedRandomStudent(List<Student> students) {
Random random = new Random();
int totalWeight = 0;
// 计算所有学生的总权重,假设权重为 100 - currentPoints
for (Student student : students) {
int weight = 100 - student.getPoints().intValue();
if (weight < 1) {
weight = 1; // 确保权重最低为1
}
totalWeight += weight; // 累计总权重
}
// 如果 totalWeight 为 0 或负数,则无法进行随机选择
if (totalWeight <= 0) {
throw new IllegalArgumentException("总权重必须为正数");
}
// 随机生成一个 0 到 totalWeight 之间的随机数
int randomIndex = random.nextInt(totalWeight);
int currentWeightSum = 0;
// 遍历学生列表,根据累计权重确定选中的学生
for (Student student : students) {
int weight = 100 - student.getPoints().intValue();
if (weight < 1) {
weight = 1;
}
currentWeightSum += weight;
// 当累计权重超过随机数时,选择当前学生
if (currentWeightSum > randomIndex) {
return student;
}
}
// 如果没有选中任何学生,兜底返回第一个学生(理论上不会发生)
return students.get(0);
}
// 命运轮盘处理(所有学生概率相等)
private RollCallResponse handleWheelOfFortune(List<Student> students) {
Random random = new Random();
Student selectedStudent = students.get(random.nextInt(students.size()));
RollCallResponse response = new RollCallResponse();
response.setStudentId(selectedStudent.getStudentNumber());
response.setName(selectedStudent.getName());
response.setPoints(selectedStudent.getPoints());
response.setMessage("命运轮盘: " + selectedStudent.getName() + " 被选中了!");
return response;
}
// 触发随机事件:赌徒事件或倒霉事件
private String triggerRandomEvent() {
Random random = new Random();
int eventType = random.nextInt(2); // 0表示赌徒事件1表示倒霉事件
if (eventType == 0) {
return "赌徒事件"; // 触发赌徒事件
} else {
return "倒霉事件"; // 触发倒霉事件
}
}
}

@ -1,162 +0,0 @@
package com.example.attendance.service.impl;
import com.example.attendance.entity.Student;
import com.example.attendance.mapper.StudentMapper;
import com.example.attendance.service.StudentService;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@Service
public class StudentServiceImpl implements StudentService {
@Autowired
private StudentMapper studentMapper;
@Override
public void importStudents(MultipartFile file) throws Exception {
List<Student> students = new ArrayList<>();
// 获取上传的 Excel 文件流
try (InputStream inputStream = file.getInputStream();
Workbook workbook = new XSSFWorkbook(inputStream)) {
// 读取第一个工作表
Sheet sheet = workbook.getSheetAt(0);
// 检查文件中是否包含积分列
boolean hasPointsColumn = sheet.getRow(0).getLastCellNum() > 2;
// 遍历每一行,从第二行开始(假设第一行是标题)
for (int i = 1; i <= sheet.getLastRowNum(); i++) {
Row row = sheet.getRow(i);
if (row == null) continue;
// 读取学生数据
String studentNumber = row.getCell(0).getStringCellValue();
String name = row.getCell(1).getStringCellValue();
// 如果有积分列,读取积分,如果没有积分列或者积分为空,则默认积分为 0
BigDecimal points = BigDecimal.ZERO;
if (hasPointsColumn && row.getCell(2) != null) {
Cell pointsCell = row.getCell(2);
if (pointsCell.getCellType() == CellType.NUMERIC) {
points = new BigDecimal(pointsCell.getNumericCellValue());
}
}
Student student = new Student();
student.setStudentNumber(studentNumber);
student.setName(name);
student.setPoints(points);
students.add(student);
}
}
// 将学生列表保存到数据库
studentMapper.saveStudents(students);
}
@Override
public void exportStudents(OutputStream outputStream) throws Exception {
List<Student> students = studentMapper.findAll();
try (Workbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet("Students");
// 创建标题行
Row headerRow = sheet.createRow(0);
headerRow.createCell(0).setCellValue("学号");
headerRow.createCell(1).setCellValue("姓名");
headerRow.createCell(2).setCellValue("积分");
// 填充数据
int rowNum = 1;
for (Student student : students) {
Row row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue(student.getStudentNumber());
row.createCell(1).setCellValue(student.getName());
row.createCell(2).setCellValue(student.getPoints().doubleValue());
}
// 将数据写入输出流
workbook.write(outputStream);
}
}
/**
*
* @param studentNumber
* @param pointsDelta
*/
@Override
public void adjustPoints(String studentNumber, BigDecimal pointsDelta) {
Student student = studentMapper.findByStudentNumber(studentNumber);
if (student == null) {
throw new RuntimeException("未找到该学生");
}
BigDecimal updatedPoints = student.getPoints().add(pointsDelta);
student.setPoints(updatedPoints);
studentMapper.update(student);
}
/**
*
* @param page
* @param size
* @return
*/
@Override
public List<Student> getStudentRanking(int page, int size) {
int offset = page * size;
return studentMapper.findStudentsByRanking(offset, size);
}
@Override
public Student findById(Long id) {
return studentMapper.findById(id);
}
@Override
public Student findByStudentNumber(String studentNumber) {
return studentMapper.findByStudentNumber(studentNumber);
}
@Override
public List<Student> findAll() {
return studentMapper.findAll();
}
@Override
public void save(Student student) {
studentMapper.save(student);
}
@Override
public void update(Student student) {
studentMapper.update(student);
}
@Override
public void delete(Long id) {
studentMapper.delete(id);
}
/**
*
* @return
*/
@Override
public String[] getAllStudentNames() {
return studentMapper.findAllStudentNames();
}
}

@ -1,37 +0,0 @@
package com.example.attendance.service.impl;
import com.example.attendance.mapper.TeacherMapper;
import com.example.attendance.entity.Teacher;
import com.example.attendance.service.TeacherService;
import com.example.attendance.util.JWTUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class TeacherServiceImpl implements TeacherService {
@Autowired
private TeacherMapper teacherMapper;
@Autowired
private PasswordEncoder passwordEncoder; // 使用 PasswordEncoder 接口
@Override
public void register(String username, String password) {
Teacher teacher = new Teacher();
teacher.setUsername(username);
teacher.setPassword(passwordEncoder.encode(password)); // 密码加密
teacherMapper.save(teacher);
}
@Override
public String login(String username, String password) {
Teacher teacher = teacherMapper.findByUsername(username);
if (teacher == null || !passwordEncoder.matches(password, teacher.getPassword())) {
throw new RuntimeException("用户名或密码不正确");
}
// 返回 JWT token
return JWTUtil.generateToken(teacher);
}
}

@ -1,47 +0,0 @@
package com.example.attendance.util;
import com.example.attendance.entity.Teacher;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import java.util.Date;
public class JWTUtil {
private static final SecretKey SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
// 生成 Token
public static String generateToken(Teacher teacher) {
return Jwts.builder()
.setSubject(teacher.getUsername()) // 设置 Token 主题(用户名)
.setIssuedAt(new Date()) // 设置 Token 签发时间
.setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 1000)) // Token 有效期 1 小时
.signWith(SignatureAlgorithm.HS256, SECRET_KEY) // 使用 HS256 签名算法和 SECRET_KEY 签名
.compact();
}
// 从 Token 中提取声明
public static Claims extractClaims(String token) {
try {
return Jwts.parser()
.setSigningKey(SECRET_KEY) // 设置签名密钥
.parseClaimsJws(token) // 解析 Token
.getBody();
} catch (Exception e) {
throw new RuntimeException("Invalid JWT token", e); // 捕获并抛出异常
}
}
// 从 Token 中获取用户名
public static String getUsernameFromToken(String token) {
return extractClaims(token).getSubject();
}
// 判断 Token 是否过期
public static boolean isTokenExpired(String token) {
return extractClaims(token).getExpiration().before(new Date());
}
}

@ -1,7 +0,0 @@
spring.application.name=attendance
spring.datasource.url=jdbc:mysql://localhost:3306/roll_call_system
spring.datasource.username=root
spring.datasource.password=123456789jk
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.2 MiB

@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/点名.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>点点小助手</title>
<script type="module" crossorigin src="/assets/index-Cb5FnDWH.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B1hDzaLh.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 649 KiB

@ -1,7 +0,0 @@
spring.application.name=attendance
spring.datasource.url=jdbc:mysql://localhost:3306/roll_call_system
spring.datasource.username=root
spring.datasource.password=123456789jk
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save