Compare commits

..

No commits in common. 'FRONT_a' and 'main' have entirely different histories.

8
.idea/.gitignore vendored

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

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>

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="attendance" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="attendance" options="-parameters" />
</option>
</component>
</project>

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

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="AliControlFlowStatementWithoutBraces" enabled="true" level="WEAK WARNING" enabled_by_default="true" editorAttributes="INFO_ATTRIBUTES" />
</profile>
</component>

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<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="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<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>

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

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

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

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

@ -0,0 +1,13 @@
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);
}
}

@ -0,0 +1,33 @@
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();
}
}

@ -0,0 +1,24 @@
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);
}
}

@ -0,0 +1,178 @@
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);
}
}

@ -0,0 +1,38 @@
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 状态和错误信息
}
}
}

@ -0,0 +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;
}

@ -0,0 +1,18 @@
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; // 显示结果信息
}

@ -0,0 +1,15 @@
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; // 开启("是")或关闭("否")命运轮盘
}

@ -0,0 +1,17 @@
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; // 积分
}

@ -0,0 +1,15 @@
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;
}

@ -0,0 +1,56 @@
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();
}

@ -0,0 +1,15 @@
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);
}

@ -0,0 +1,10 @@
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); // 处理点名逻辑
}

@ -0,0 +1,23 @@
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();
}

@ -0,0 +1,23 @@
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;
}

@ -0,0 +1,109 @@
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 "倒霉事件"; // 触发倒霉事件
}
}
}

@ -0,0 +1,162 @@
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();
}
}

@ -0,0 +1,37 @@
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);
}
}

@ -0,0 +1,47 @@
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());
}
}

@ -0,0 +1,7 @@
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: 30 MiB

After

Width:  |  Height:  |  Size: 27 MiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

@ -0,0 +1,14 @@
<!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>

Before

Width:  |  Height:  |  Size: 649 KiB

After

Width:  |  Height:  |  Size: 649 KiB

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1727709759865" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6581" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M485.568 906.496c-0.64-0.192-145.088 10.368-209.216-16.256-86.528-35.968-117.184-62.592-117.184-79.488 0-15.936-2.944-98.112 77.248-123.2a4319.36 4319.36 0 0 1 155.264-43.136 25.216 25.216 0 0 0 22.08-24.832V541.888a24.064 24.064 0 0 0-7.68-17.664c-69.248-66.56-97.728-131.456-97.728-243.584 0-53.312 14.656-91.008 39.296-119.744 26.496-31.104 68.032-46.912 123.264-46.912 159.104 0 169.408 128.384 169.408 171.392a291.072 291.072 0 0 1-26.56 122.048c-13.056 28.544-26.624 44.736-26.752 45.12a24.256 24.256 0 0 0 3.776 35.2 26.88 26.88 0 0 0 36.672-3.648c2.56-3.072 65.088-77.696 65.088-198.72 0-65.024-16.832-109.376-52.544-150.72C600.256 88.448 541.76 64 470.976 64 335.808 64 256.256 140.416 256.256 280.704c0 60.864 8.192 104.256 25.024 148.928a340.48 340.48 0 0 0 80.256 122.368v46.784c-36.928 6.72-73.408 15.552-109.312 26.368-39.232 12.16-70.848 25.536-93.824 40.256-34.176 21.632-51.456 117.76-51.456 145.344 0 45.76 48.64 83.968 149.056 125.504 67.712 28.032 213.12 17.664 215.872 18.368l6.848 1.024a25.92 25.92 0 0 0 25.28-18.496 24.704 24.704 0 0 0-18.432-30.656z" p-id="6582" fill="#8755f2"></path><path d="M937.024 515.776a26.624 26.624 0 0 0-37.376 4.16c-9.152 11.456-199.808 266.624-228.224 268.928-27.712 2.432-122.752-90.112-122.752-90.112a26.496 26.496 0 0 0-36.224 4.032 26.56 26.56 0 0 0 1.856 36.48c10.304 9.664 101.056 82.048 123.072 98.304l25.6 18.816c5.12 3.264 10.752 5.376 16.576 4.736a23.68 23.68 0 0 0 17.536-9.152c6.208-7.36 35.52-41.344 35.776-41.792l208.448-257.216a26.56 26.56 0 0 0-4.288-37.184z" p-id="6583" fill="#8755f2"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@ -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);
}
}

@ -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");
}
}

@ -2,9 +2,9 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/点名.png"> <link rel="icon" href="/点名.svg">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>点点小助手</title> <title>Vite App</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

@ -1,6 +1,5 @@
{ {
"compilerOptions": { "compilerOptions": {
"lib": ["ES2015", "DOM"],
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
} }

@ -1,154 +0,0 @@
.lucky-popup {
/* background-image: url('../assets/image/bgcImg.png'); */
width: 100%;
height: 100%;
/* position: relative; */
/* position: fixed; */
/* background-size: 100% 100%; */
}
/* header */
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 60px;
padding-bottom: 0;
font-size: 50px;
font-weight: bold;
color: #626868;
}
.icon {
width: 45px;
cursor: pointer;
}
.icon:hover {
transform: scale(1.3);
}
/* 姓名学号 */
.lucky-info {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 200px;
}
.lucky-icon {
width: 700px;
position: relative;
}
.stage-icon {
position: absolute;
width: 900px;
top: 44%;
}
.student-info {
position: absolute;
text-align: center; /* 确保文本居中 */
top: 34%;
}
.student-id, .student-name {
font-size: 75px;
font-weight: bold;
color: black;
}
/* .student-id {
position: absolute;
top: 33%;
font-size: 75px;
font-weight: bold;
color: black;
}
.student-name {
position: absolute;
top: 45%;
font-size: 75px;
font-weight: bold;
color: black;
} */
.student-msg {
position: absolute;
left: 5%;
right: 5%;
top: 62.5%;
font-size: 33px;
text-align: center;
font-weight: bold;
color: #333;
}
/* 分数按钮 */
.score-buttons {
display: flex;
justify-content: space-between;
margin: 0 5%;
}
.score-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 18px 115px;
border: none;
border-radius: 5px;
background-color: #A182FF;
color: white;
cursor: pointer;
font-size: 30px;
transition: background-color 0.3s;
}
.score-btn.active, .score-btn:hover {
background-color: #6231F5;
}
.score-btn input {
width: 50px;
height: 28px;
padding: 5px;
border-radius: 5px;
margin-right: 8px;
font-size: 28px;
border: none;
background-color: #EBE4FF;
color: #333;
text-align: center;
}
/* 底部 */
.bottom{
position: absolute;
display: flex; align-items: center;justify-content: space-around;
bottom: 0;
width: 100%;
padding: 20px 0;
background-color: #EBE4FF;
}
.cancel {
width: auto;
border-radius: 5px;
padding: 10px 25px;
font-size: 23px;
background-color: #fff;
color: #A182FF;
cursor: pointer;
}
.cancel:hover {
background-color: #dfdede;
}
.confirm{
width: auto;
border-radius: 5px;
padding: 10px 25px;
font-size: 23px;
background-color: #A182FF;
color: #fff;
cursor: pointer;
}
.confirm:hover {
background-color: #6231F5;
}

@ -1,142 +0,0 @@
.container {
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);
}
/* 搜索框 */
.search-bar {
padding: 20px 40px;
display: flex;
gap: 30px;
}
.search-input {
flex-grow: 1;
padding: 10px;
border: 2px solid #ddd;
border-radius: 10px;
font-size: 20px;
}
.search-btn {
background-color: #A182FF;
color: #fff;
border: none;
padding: 10px 30px;
border-radius: 50px;
cursor: pointer;
font-size: 30px;
}
.search-btn:hover{
background-color: #6231F5;
}
/* 表格滚动容器样式 */
.table-scroll-container {
max-height: 480px; /* 设置一个合适的高度 */
overflow-y: auto; /* 当内容超出时显示滚动条 */
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
text-align: center;
font-size: 20px;
padding: 12px 40px;
border-bottom: 1.5px solid #ddd;
border-right: 1.5px solid #ddd;
/* 防止换行 */
/* white-space: nowrap; */
&:last-child{
border-right: none;
}
}
th {
background-color: #EBE4FF;
font-weight: normal;
color: black;
font-weight: bold;
}
.medal {
font-size: 20px;
}
/* 分页 */
.pagination {
padding: 30px 20px 15px 0px;
text-align: right;
color: #797878;
font-weight: bold;
font-size: 18px;
}
.page-btn {
background-color: #b39cf2;
color: #fff;
border: none;
padding: 8px 15px;
margin: 0 8px;
font-size: 18px;
border-radius: 3px;
cursor: pointer;
}
.page-btn:hover {
background-color: #9272f3;
}
/* 底部 */
.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;
}

@ -1,10 +0,0 @@
<svg width="828" height="480" viewBox="0 0 828 480" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M47 428.984L124.026 432.577" stroke="#F9BA41" stroke-width="93.2383" stroke-linecap="round"/>
<path d="M54.9106 250.873L124.503 295.693" stroke="#F9BA41" stroke-width="93.2383" stroke-linecap="round"/>
<path d="M140.931 103.239L182.5 191.097" stroke="#F9BA41" stroke-width="93.2383" stroke-linecap="round"/>
<path d="M781 428.984L703.974 432.577" stroke="#F9BA41" stroke-width="93.2383" stroke-linecap="round"/>
<path d="M773.089 250.873L703.497 295.693" stroke="#F9BA41" stroke-width="93.2383" stroke-linecap="round"/>
<path d="M687.069 103.239L645.479 191.097" stroke="#F9BA41" stroke-width="93.2383" stroke-linecap="round"/>
<path d="M492.836 52.5361L467.76 151.2" stroke="#F9BA41" stroke-width="93.2383" stroke-linecap="round"/>
<path d="M337.57 47L349.859 150.028" stroke="#F9BA41" stroke-width="93.2383" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 951 B

@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1728590895946" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6825" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M0 192v640c0 70.7 57.3 128 128 128h352c17.7 0 32-14.3 32-32s-14.3-32-32-32H128c-35.3 0-64-28.7-64-64V192c0-35.3 28.7-64 64-64h352c17.7 0 32-14.3 32-32s-14.3-32-32-32H128C57.3 64 0 121.3 0 192z" p-id="6826" fill="#9276cf"></path><path d="M1013.3 535.7L650.9 863.3c-41.1 37.2-106.9 8-106.9-47.5V685c0-4.4-3.6-8-8-8H224c-17.7 0-32-14.3-32-32V379c0-17.7 14.3-32 32-32h312c4.4 0 8-3.6 8-8V208.1c0-55.5 65.8-84.7 106.9-47.5l362.4 327.6c14.1 12.8 14.1 34.8 0 47.5z" p-id="6827" fill="#9276cf"></path></svg>

Before

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 909 KiB

@ -1,10 +0,0 @@
<svg width="1061" height="192" viewBox="0 0 1061 192" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_238_16)">
<path d="M901.075 0L895.903 2.00872L1024.8 63.9442L823.499 165.887H245.856L40.5782 62.1029L165.495 2.00872L160.324 0L0.795654 76.6661L222.782 191.833H839.014L1061 76.6661L901.075 0ZM241.878 168.063L243.071 168.565H827.079L1029.57 65.9529L1038.32 70.1377L829.864 178.943H235.91L25.4608 69.3008L35.8043 64.279L241.878 168.063ZM835.433 188.987H225.965L9.94564 76.8335L21.0848 71.4769L231.534 181.454L232.728 181.956H833.046L1042.7 72.4813L1051.85 76.8335L835.433 188.987Z" fill="#F8B642"/>
</g>
<defs>
<clipPath id="clip0_238_16">
<rect width="1061" height="192" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 739 B

@ -5,9 +5,6 @@ import login from '@/views/login2.vue'
import home from '@/views/home.vue' import home from '@/views/home.vue'
import importFile from '@/views/importFile.vue' import importFile from '@/views/importFile.vue'
import ruleSetting from '@/views/ruleSetting.vue' import ruleSetting from '@/views/ruleSetting.vue'
import seeChart from '@/views/seeChart.vue'
import beginCall from '@/views/beginCall.vue'
import result from '@/views/result.vue'
// 创建路由器 // 创建路由器
const router = createRouter({ const router = createRouter({
@ -31,20 +28,8 @@ const router = createRouter({
{ {
path:'ruleSetting', path:'ruleSetting',
component: ruleSetting component: ruleSetting
},
{
path:'seeChart',
component:seeChart
} }
] ]
},
{
path:'/beginCall',
component:beginCall
},
{
path:'/result',
component:result
} }
] ]

@ -4,7 +4,7 @@ import {getToken} from '@/token/auth' // 注意这里使用了解构赋值来导
// 创建axios实例 // 创建axios实例
const service = axios.create({ const service = axios.create({
baseURL: 'http://hup3hr.natappfree.cc/api', // 配置基础URL baseURL: 'http://example.com/api', // 配置基础URL
timeout: 5000, // 请求超时时间 timeout: 5000, // 请求超时时间
}); });
@ -13,13 +13,9 @@ service.interceptors.request.use(
config => { config => {
// 在发送请求之前做些什么 // 在发送请求之前做些什么
const token = getToken(); // 获取token的方式取决于你的应用 const token = getToken(); // 获取token的方式取决于你的应用
// console.log('请求拦截器的token是' +token)
console.log('Request Config:', config);
if (token) { if (token) {
config.headers['Authorization'] = `Bearer ${token}`; // 设置token config.headers['Authorization'] = `Bearer ${token}`; // 设置token
// axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
} }
config.headers['Accept'] = 'application/json';
return config; return config;
}, },
error => { error => {
@ -33,32 +29,61 @@ service.interceptors.request.use(
service.interceptors.response.use( service.interceptors.response.use(
response => { response => {
// 对响应数据做点什么 // 对响应数据做点什么
// 注意这里返回已经包含data
const res = response.data; const res = response.data;
// 你可以根据实际情况在这里添加一些通用的响应处理逻辑 // 你可以根据实际情况在这里添加一些通用的响应处理逻辑
// 例如,根据返回的状态码判断请求是否成功 // 例如,根据返回的状态码判断请求是否成功
// if (res.code !== 200) { if (res.code !== 200) {
// 业务错误处理,比如弹窗提示等 // 业务错误处理,比如弹窗提示等
// return Promise.reject(new Error(res.message || 'Error')); return Promise.reject(new Error(res.message || 'Error'));
// } else { } else {
return res; return res;
// } }
}, },
error => { error => {
// 对响应错误做点什么 // 对响应错误做点什么
// if (error.response) { if (error.response) {
// 请求已发出但服务器响应的状态码不在2xx的范围 // 请求已发出但服务器响应的状态码不在2xx的范围
// console.error('Error status:', error.response.status); console.error('Error status:', error.response.status);
// console.error('Error data:', error.response.data); console.error('Error data:', error.response.data);
// } else if (error.request) { } else if (error.request) {
// 请求已发出,但没有收到响应 // 请求已发出,但没有收到响应
// console.error('Error request:', error.request); console.error('Error request:', error.request);
// } else { } else {
// 在设置请求时触发错误 // 在设置请求时触发错误
console.error('响应拦截器errorMessage:', error.message); console.error('Error message:', error.message);
// } }
return Promise.reject(error); return Promise.reject(error);
} }
); );
export default service; 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);
// }
// }
// }

@ -1,202 +0,0 @@
<template>
<div class="beginCall">
<img src="../assets/image/coffee.png" alt="Coffee Image">
<div class="text-container">
<div
v-for="(text, index) in texts"
:key="index"
class="text"
:style="text.style"
@animationend="removeText(index)"
>
{{ text.content }}
</div>
</div>
<div class="btnBack" @click="back"></div>
<div class="btnStop" @click="stop"></div>
</div>
</template>
<script>
import axios from '@/utils/axiosConfig';
import { ref } from 'vue';
export default {
data() {
// const names = ref([])
return {
//
maxTexts: 80,
names: [],
texts: [],
};
},
mounted() {
this.fetchNames();
// this.names = ['1', '2', '3', '4', '5'];
this.startRain();
},
methods: {
// names
async fetchNames() {
try {
const response = await axios.get('/students/names',null);
// this.names = response.data;
// console.log('' + response)
if (Array.isArray(response)) {
this.names = response;
console.log('后端传值response', this.names);
} else if (typeof response === 'object' && Array.isArray(response.data)) {
this.names = response.data;
}else{
console.error('后端返回的数据不是数组格式');
this.names = ['文字1', '文字2', '文字3', '文字4', '文字5']; //
console.log('实际接收到的数据:', response.data);
}
if (this.names.length === 0 || this.names.every(name => name.trim() === '')) {
alert('请先导入学生名单');
this.back()
//
// this.names = ['1', '2']; //
return; //
}
} catch (error) {
console.error('获取names失败:', error);
//
// this.names = ['', '', '', '4', '5'];
}
},
//
getTextStyle() {
const left = Math.floor(Math.random() * 80) + 5; // 10%90%
const size = Math.random() * 1.8 + 0.5; // 0.5em1.5em
const duration = Math.random() * 2 + 5; // 2-7
return {
left: `${left}%`,
fontSize: `${size}em`,
animationDuration: `${duration}s`,
};
},
//
startRain() {
// 200
setInterval(() => {
if (this.texts.length < this.maxTexts) {
// names
const content = this.names[Math.floor(Math.random() * this.names.length)];
// getTextStyle()
const style = this.getTextStyle();
// texts
this.texts.push({ content, style });
}
}, 100); // 1s
},
//
removeText(index) {
this.texts.splice(index, 1);
},
//
stop() {
this.$router.push('/result')
},
//
back() {
this.$router.push('/home')
}
},
};
</script>
<style scoped>
.beginCall {
background-image: url('../assets/image/bgcImg.png');
width: 100%;
height: 100%;
position: fixed;
background-size: 100% 100%;
}
.beginCall img {
position: absolute;
top: 0;
left: 50%;
width: 30%;
height: auto;
}
.text-container {
position: absolute;
top: 35%;
left: 15%;
width: 70%;
height: 70%;
overflow: hidden;
}
.text {
position: absolute;
animation: rain 8s linear infinite;
opacity: 0; /* 初始状态设为透明 */
}
@keyframes rain {
0%, 100% {
transform: translateY(-10%) scale(1);
opacity: 0;
color: #f13f69;
}
10% {
/* transform: translateY(0) scale(1); */
opacity: 1;
color: #f13f69;
}
50% {
color: #e34166;
}
80% {
transform: translateY(60vh) scale(1.3);
opacity: 1;
color: #ccb2f9; /* 中间颜色 */
}
100% {
transform: translateY(50vh) scale(1.3);
opacity: 0;
color: #F5B2C2;
}
}
.btnStop {
position: absolute;
bottom: 4%;
right: 3%;
padding: 10px 100px;
border-radius: 50px;
font-size: 35px;
font-weight: bold;
color: #fff;
background-color: #A182FF;
cursor: pointer;
}
.btnStop:hover, .btnBack:hover {
background-color: #6231F5;
}
.btnBack {
position: absolute;
bottom: 4%;
left: 3%;
padding: 10px 100px;
border-radius: 50px;
font-size: 35px;
font-weight: bold;
color: #fff;
background-color: #A182FF;
cursor: pointer;
}
</style>

@ -3,8 +3,6 @@
<div class="content"> <div class="content">
<!-- 展示区 --> <!-- 展示区 -->
<div class="leftIcon"> <div class="leftIcon">
<div class="text-style">点点小助手</div>
<img src="../assets/image/left-icon.svg" alt="" class="dian-icon">
<RouterView></RouterView> <RouterView></RouterView>
</div> </div>
<!-- 导航栏 --> <!-- 导航栏 -->
@ -12,31 +10,22 @@
<div @click="gotoImpt" class="selectItem"> <div @click="gotoImpt" class="selectItem">
导入文件 导入文件
</div> </div>
<div @click="gotoCall" class="selectItem"> <div class="selectItem">
开始点名 开始点名
</div> </div>
<div @click="gotoRule" class="selectItem"> <div @click="gotoRule" class="selectItem">
规则设置 规则设置
</div> </div>
<div @click="gotoChart" class="selectItem"> <div class="selectItem">
查看排行 查看排行
</div> </div>
<div class="back" @click="goLogin">
<div>退出登录</div>
<img src="../assets/image/back.svg" alt="" style="width: 40px;margin-left: 15px;">
</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { RouterLink, RouterView } from 'vue-router'; import { RouterLink, RouterView } from 'vue-router';
import axios from '@/utils/axiosConfig';
export default { export default {
data() { data() {
return { return {
@ -49,23 +38,6 @@ export default {
}, },
gotoRule() { gotoRule() {
this.$router.push('/home/ruleSetting') this.$router.push('/home/ruleSetting')
},
gotoChart() {
this.$router.push('/home/seeChart')
},
gotoCall() {
this.$router.push('/beginCall')
},
async goLogin() {
try {
const response = await axios.post('/teacher/logout');
console.log('退出登录成功', response)
// 退
this.$router.push('/');
} catch (error) {
//
console.error('退出登录错误:', error);
}
} }
} }
} }
@ -91,21 +63,6 @@ export default {
height: 800px; height: 800px;
/* background-color: red; */ /* background-color: red; */
} }
.dian-icon {
position: absolute;
width: 690px;
/* top: 35%; */
left: 6%;
bottom: 5%;
}
.text-style {
position: absolute;
top: 4%;
left: 5%;
font-size: 73px;
font-weight: bold;
color: #8755F2;
}
.selectItem { .selectItem {
padding: 25px 130px; padding: 25px 130px;
border-radius: 50px; border-radius: 50px;
@ -120,19 +77,4 @@ export default {
.selectItem:hover { .selectItem:hover {
background-color: #6231F5; background-color: #6231F5;
} }
.back {
display: flex;
position: absolute;
font-size: 28px;
font-weight: bold;
color: #9276CF;
right: 2.5%;
bottom: 4%;
cursor: pointer;
}
.back:hover {
color: #6231F5;
}
</style> </style>

@ -11,11 +11,11 @@
<el-upload <el-upload
ref="uploadRef" ref="uploadRef"
class="upload-demo" class="upload-demo"
action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
:auto-upload="false"
:limit="1" :limit="1"
accept=".xlsx, .xls" accept=".xlsx, .xls"
:on-exceed="handleExceed" :on-exceed="handleExceed"
:http-request="customUpload"
:auto-upload="false"
> >
<div style="font-size: 30px; color: #7D7878;">选择文件</div> <div style="font-size: 30px; color: #7D7878;">选择文件</div>
<input placeholder="请选择上传的文件" class="inputFile" /> <input placeholder="请选择上传的文件" class="inputFile" />
@ -38,6 +38,7 @@
<!-- 下载模板 --> <!-- 下载模板 -->
<a <a
class="download" class="download"
href="后端提供的下载URL"
download="student-template.xlsx" download="student-template.xlsx"
@click="onDownload" @click="onDownload"
> >
@ -58,18 +59,17 @@
<div class="confirm" @click="submitUpload"></div> <div class="confirm" @click="submitUpload"></div>
<div class="cancel" @click="close"></div> <div class="cancel" @click="close"></div>
</div> </div>
</div>
</div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue' import { ref } from 'vue'
import { genFileId, ElMessage } from 'element-plus' import { genFileId } from 'element-plus'
import type { UploadInstance, UploadProps, UploadRawFile } from 'element-plus' import type { UploadInstance, UploadProps, UploadRawFile } from 'element-plus'
import router from '@/router'; import router from '@/router';
import axios from '@/utils/axiosConfig';
const uploadRef = ref<UploadInstance>() const uploadRef = ref<UploadInstance>()
// //
@ -79,72 +79,21 @@ const handleExceed: UploadProps['onExceed'] = (files) => {
file.uid = genFileId() file.uid = genFileId()
uploadRef.value!.handleStart(file) uploadRef.value!.handleStart(file)
} }
//
const customUpload = (options) => {
const { file } = options;
const formData = new FormData();
formData.append('file', file);
console.log('formData是' + formData)
axios.post('/students/import', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
.then(response => {
ElMessage({
type: 'success',
message: '文件导入成功!'
});
close()
console.log('导入结果:', response.data);
})
.catch(error => {
ElMessage({
type: 'error',
message: '文件导入失败,请重试!'
});
console.error('导入失败:', error);
});
}
// //
const submitUpload = () => { const submitUpload = () => {
uploadRef.value!.submit() uploadRef.value!.submit()
} }
//
async function getTemplateDownloadLink(): Promise<string> {
try {
const response = await axios.get('/students/download-template');
return response; //
} catch (error) {
console.error('获取下载链接失败:', error);
throw error; //
}
}
// //
const onDownload = async (event: MouseEvent) => { const onDownload = (event: MouseEvent) => {
event.preventDefault(); // <a> //
//
try {
const downloadLink = await getTemplateDownloadLink(); //
if (downloadLink) {
// 使window.open<a>href
window.open(downloadLink, '_blank');
} else {
//
alert('无法获取下载链接,请稍后再试。');
}
} catch (error) {
//
console.error('下载模板时出错:', error);
alert('下载模板失败,请稍后再试。');
}
};
// <a>href
event.preventDefault();
// 使fetchhref
window.open("后端提供的下载URL", '_blank');
}
function close() { function close() {
router.push('/home') router.push('/home')
} }

@ -50,7 +50,7 @@
> >
</div> </div>
<div class="forgot-password"> <div class="forgot-password">
<!-- <a href="#">忘记密码?</a> --> <a href="#">忘记密码?</a>
</div> </div>
<button type="submit" class="submit-btn"> </button> <button type="submit" class="submit-btn"> </button>
</form> </form>
@ -91,7 +91,7 @@
<img src="../assets/image/password.png" alt="" class="input-icon"> <img src="../assets/image/password.png" alt="" class="input-icon">
<input <input
v-model="registerForm.confirmPassword" v-model="registerForm.confirmPassword"
:type="showPassword ? 'text' : 'password'" :type="showConfirmPassword ? 'text' : 'password'"
placeholder="再次确认密码" placeholder="再次确认密码"
> >
<img src="../assets/image/eye.png" <img src="../assets/image/eye.png"
@ -115,7 +115,7 @@
<script> <script>
import axios from '@/utils/axiosConfig'; import axios from '@/utils/axiosConfig';
import {getToken, setToken} from '@/token/auth' import {setToken} from '@/token/auth'
export default { export default {
data() { data() {
return { return {
@ -143,27 +143,20 @@
// //
async handleLogin() { async handleLogin() {
try { try {
const response = await axios.post('/teacher/login', null, { const response = await axios.post('/teacher/login', {
params: { username: this.loginForm.phone,
username: this.loginForm.phone, password: this.loginForm.password
password: this.loginForm.password
}
}); });
console.log('登录成功', response)
// tokendata // tokendata
// !!! const token = response.data.token;
const token = response.token;
// token // token
setToken(token); setToken(token);
// console.log( 'token' + getToken() )
// //
this.$router.push('/home'); this.$router.push('/home');
} catch (error) { } catch (error) {
// //
console.error('登录错误:', error); console.error('Login Error:', error);
} }
}, },
// //
@ -174,14 +167,11 @@
} }
try { try {
// //
const response = await axios.post('/teacher/register', null, { const response = await axios.post('/teacher/register', {
params:{ username: this.registerForm.phone,
username: this.registerForm.phone, password: this.registerForm.password,
password: this.registerForm.password,
}
}); });
// //
alert('注册成功');
console.log('注册成功', response); console.log('注册成功', response);
} catch (error) { } catch (error) {
// //

@ -1,189 +0,0 @@
<template>
<div class="lucky-popup">
<!-- header -->
<div class="header">
<div>幸运儿出现了 </div>
<img src="../assets/image/close.png" alt="" class="icon" @click="close">
</div>
<div class="lucky-info">
<img src="../assets/image/group.svg" alt="" class="lucky-icon">
<div class="student-info">
<div class="student-id">{{ studentId }}</div>
<div class="student-name">{{ studentName }}</div>
</div>
<img src="../assets/image/stage.svg" alt="" class="stage-icon">
<div class="student-msg" v-if="nameOrQuestion === '提问'">{{ message }}</div>
</div>
<!-- 选择分数 -->
<div class="score-buttons" v-if="nameOrQuestion === '提问'">
<button
v-for="(score, index) in qsScores"
:key="index"
:class="['score-btn', { active: selectedScore === score.value }]"
@click="selectScore(score.value)"
>
{{ score.label }}
<input
v-if="index === 2"
type="number"
v-model="customScore"
@input="selectCustomScore"
><div v-if="index === 2"></div>
</button>
</div>
<!-- 点名的选项 -->
<div class="score-buttons" v-else-if="nameOrQuestion === ''">
<button
v-for="(score, index) in dmScores"
:key="index"
:class="['score-btn', { active: selectedScore === score.value }]"
@click="selectScore(score.value)"
>
{{ score.label }}
</button>
</div>
<!-- 底部 -->
<div class="bottom">
<div class="confirm" @click="confirmAdjustment"></div>
<div class="cancel" @click="close"></div>
</div>
</div>
</template>
<script>
import axios from '@/utils/axiosConfig';
import router from '@/router';
// import { da } from 'element-plus/es/locale';
export default {
data() {
return {
// studentId: '102201338',
// studentName: '',
// message: '--',
studentId:'',
studentName:'',
message:'',
selectedScore: null,
customScore: '',
//
nameOrQuestion:'',
triggerRandomEvent:'',
enableFateWheel:'',
qsScores: [
{ label: '+0.5分', value: 0.5 },
{ label: '-1分', value: -1 },
{ label: '', value: null }
],
dmScores: [
{ label: '已到达课堂加1分', value: 1 },
{ label: '未到达课堂减1分', value: -1 },
],
};
},
mounted() {
this.loadStudentData();
},
methods: {
async loadStudentData () {
this.nameOrQuestion = localStorage.getItem('nameOrQuestion');
console.log('nameOrQuestion的值为' + this.nameOrQuestion)
this.triggerRandomEvent = localStorage.getItem('triggerRandomEvent');
console.log('triggerRandomEvent的值为' + this.triggerRandomEvent)
this.enableFateWheel = localStorage.getItem('enableFateWheel');
console.log('enableFateWheel的值为' + this.enableFateWheel)
const data = {
rollCallMode: this.nameOrQuestion,
triggerRandomEvent: this.triggerRandomEvent,
wheelOfFortune: this.enableFateWheel,
}
//
try {
// const response = await axios.post('/rollcall/start', null, {
// params: {
// rollCallMode: this.nameOrQuestion,
// triggerRandomEvent: this.triggerRandomEvent,
// wheelOfFortune: this.enableFateWheel,
// }
// });
const response = await axios.post('/rollcall/start', data);
console.log('后端响应:', response);
this.studentId = response.studentId;
this.studentName = response.name;
this.message = response.message;
} catch (error) {
console.error('发送请求时出错:', error);
if (error.response) {
console.error('错误状态码:', error.response.status);
console.error('错误数据:', error.response.data);
}
}
},
//
selectScore(value) {
this.selectedScore = value;
if (value !== null) {
this.customScore = '';
}
},
selectCustomScore() {
if (this.customScore !== '') {
this.selectedScore = parseFloat(this.customScore);
} else {
this.selectedScore = null;
}
},
//
async confirmAdjustment() {
let pointsDelta = this.selectedScore;
if (this.customScore !== '') {
pointsDelta = parseFloat(this.customScore);
}
if (pointsDelta === null || isNaN(pointsDelta)) {
alert('请选择或输入有效的调整分数');
return;
}
console.log('调整的积分是:' + pointsDelta)
try {
// const response = await axios.post('/students/${this.studentId}/adjustPoints', pointsDelta)
await axios.put(`/students/${this.studentId}/adjustPoints`, {
pointsDelta: pointsDelta
},{
headers: {
'Content-Type': 'application/json'
}
});
console.log(this.studentId)
alert('积分调整成功');
this.close();
} catch (error) {
console.error('积分调整失败:', error);
alert('积分调整失败,请重试');
}
},
close() {
this.$router.push('/beginCall')
}
}
};
</script>
<style scoped>
@import '../assets/css/result.css'
</style>

@ -16,7 +16,7 @@
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="是否触发随机事件 " v-if="form.nameOrQuestion === '提问'"> <el-form-item label="是否触发随机事件 ">
<el-radio-group v-model="form.triggerRandomEvent"> <el-radio-group v-model="form.triggerRandomEvent">
<el-radio label="触发">触发</el-radio> <el-radio label="触发">触发</el-radio>
<el-radio label="不触发">不触发</el-radio> <el-radio label="不触发">不触发</el-radio>
@ -54,55 +54,24 @@
import router from '@/router'; import router from '@/router';
import { ref } from 'vue' import { ref } from 'vue'
import { ElTooltip } from 'element-plus' import { ElTooltip } from 'element-plus'
import axios from '@/utils/axiosConfig';
const form = ref({ const form = ref({
nameOrQuestion: '点名', nameOrQuestion: '点名',
triggerRandomEvent: '触发', triggerRandomEvent: '触发',
enableFateWheel: '否' enableFateWheel: '否'
}) })
const confirmSetting = () => {
// ,
const confirmSetting = async () => { const data = {
nameOrQuestion: form.value.nameOrQuestion,
if(form.value.nameOrQuestion === '点名') { triggerRandomEvent: form.value.triggerRandomEvent,
form.value.triggerRandomEvent = '不触发' enableFateWheel: form.value.enableFateWheel,
} }
// localStorage // form
localStorage.setItem('nameOrQuestion', form.value.nameOrQuestion); console.log(form.value.nameOrQuestion)
localStorage.setItem('triggerRandomEvent', form.value.triggerRandomEvent); // API
localStorage.setItem('enableFateWheel', form.value.enableFateWheel); console.log('发送到后端的数据:', data)
// const nameOrQuestion1 = localStorage.getItem('nameOrQuestion');
// console.log('nameOrQuestion' + nameOrQuestion1)
// const triggerRandomEvent1 = localStorage.getItem('triggerRandomEvent');
// console.log('triggerRandomEvent' + triggerRandomEvent1)
// const enableFateWheel1 = localStorage.getItem('enableFateWheel');
// console.log('enableFateWheel' + enableFateWheel1)
router.push('/home')
// ,
// const data = {
// isRollCall: form.value.nameOrQuestion,
// triggerRandomEvent: form.value.triggerRandomEvent,
// wheelOfFortune: form.value.enableFateWheel,
// }
// console.log(form.value.nameOrQuestion)
// try {
// const response = await axios.post('/rollcall/start', data);
// console.log(':', response.data);
// } catch (error) {
// console.error(':', error);
// }
// console.log(':', data)
} }
function close() { function close() {
router.push('/home') router.push('/home')
} }

@ -1,147 +0,0 @@
<template>
<div class="container">
<!-- 头部 -->
<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>
<!-- 搜索框 -->
<div class="search-bar">
<input
v-model="searchInput"
type="text"
class="search-input"
placeholder="输入需要查询积分同学的学号"
/>
<button class="search-btn" @click="searchStudent"></button>
</div>
<!-- 表格滚动容器 -->
<div class="table-scroll-container">
<!-- 排行榜 -->
<table>
<thead>
<tr>
<th>名次</th>
<th>学号</th>
<th>姓名</th>
<th>积分</th>
</tr>
</thead>
<tbody>
<tr v-for="(student, index) in paginatedStudents" :key="student.studentNumber">
<td v-if="currentPage === 1 && index === 0"><span class="medal">🥇</span></td>
<td v-else-if="currentPage === 1 && index === 1"><span class="medal">🥈</span></td>
<td v-else-if="currentPage === 1 && index === 2"><span class="medal">🥉</span></td>
<td v-else class="center-align">{{ (currentPage - 1) * pageSize + (index + 1) }}</td>
<td>{{ student.studentNumber }}</td>
<td>{{ student.name }}</td>
<td>{{ student.points }}</td>
</tr>
</tbody>
</table>
<!-- 分页 -->
<div class="pagination">
{{ totalPages }}当前是第{{ currentPage }}
<button class="page-btn" @click="prevPage" :disabled="currentPage === 1">上一页</button>
<button class="page-btn" @click="nextPage" :disabled="currentPage === totalPages">下一页</button>
</div>
</div>
<!-- 底部 -->
<div class="bottom">
<div style="display: flex;">
<div class="confirm" @click="exportData"></div>
<div class="cancel" @click="close"></div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import router from '@/router';
import axios from '@/utils/axiosConfig';
const allStudents = ref([]);
const currentPage = ref(1);
const pageSize = 6;
const searchInput = ref('');
const fetchStudents = async () => {
try {
const response = await axios.get('/students/ranking');
allStudents.value = response;
} catch (error) {
console.error('获取学生数据失败:', error);
}
};
const filteredStudents = computed(() => {
if (!searchInput.value) return allStudents.value;
return allStudents.value.filter(student =>
student.studentNumber.includes(searchInput.value)
);
});
const totalPages = computed(() => {
return Math.ceil(filteredStudents.value.length / pageSize);
});
const paginatedStudents = computed(() => {
const start = (currentPage.value - 1) * pageSize;
const end = start + pageSize;
return filteredStudents.value.slice(start, end);
});
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--;
}
};
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++;
}
};
const searchStudent = () => {
currentPage.value = 1;
};
//
const exportData = async () => {
try {
const response = await axios.get('/students/export-students', {
responseType: 'blob',
});
const url = window.URL.createObjectURL(new Blob([response]));
const a = document.createElement('a');
// a.style.display = 'none';
a.href = url;
a.download = 'students.xlsx';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('导出数据失败:', error);
}
};
function close() {
router.push('/home');
}
onMounted(() => {
fetchStudents();
});
</script>
<style scoped>
@import '../assets/css/seeChart.css';
</style>
Loading…
Cancel
Save