diff --git a/backend/pom.xml b/backend/pom.xml
index 5d9c334..6ac14a8 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -9,10 +9,10 @@
com.flyingpig
- uuAttendance
+ k-class-roll-call
0.0.1-SNAPSHOT
- uuAttendance
- An attendance software that supports roll calling and location check-in.
+ k-class-roll-call
+ Roll call system for k class.
17
@@ -41,6 +41,12 @@
easyexcel
3.1.0
+
+ org.mockito
+ mockito-core
+ 5.5.0
+ test
+
org.mybatis.spring.boot
@@ -123,7 +129,15 @@
easyexcel
3.1.0
-
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+ ch.qos.logback
+ logback-classic
+ 1.2.3
+
cn.hutool
@@ -131,7 +145,11 @@
5.8.16
-
+
+ org.redisson
+ redisson
+ 3.21.3
+
diff --git a/backend/src/main/java/com/flyingpig/kclassrollcall/common/RedisConstant.java b/backend/src/main/java/com/flyingpig/kclassrollcall/common/RedisConstant.java
new file mode 100644
index 0000000..75c77ca
--- /dev/null
+++ b/backend/src/main/java/com/flyingpig/kclassrollcall/common/RedisConstant.java
@@ -0,0 +1,10 @@
+package com.flyingpig.kclassrollcall.common;
+
+public class RedisConstant {
+
+ public static final String STUDENT_LIST_KEY = "student:list:";
+
+ public static final String STUDENT_SCORE_KEY = "student:score:";
+
+
+}
diff --git a/backend/src/main/java/com/flyingpig/kclassrollcall/config/RedissonConfig.java b/backend/src/main/java/com/flyingpig/kclassrollcall/config/RedissonConfig.java
new file mode 100644
index 0000000..fa0029f
--- /dev/null
+++ b/backend/src/main/java/com/flyingpig/kclassrollcall/config/RedissonConfig.java
@@ -0,0 +1,19 @@
+package com.flyingpig.kclassrollcall.config;
+
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class RedissonConfig {
+ @Bean
+ public RedissonClient redissonClient(){
+ // 配置
+ Config config = new Config();
+ config.useSingleServer().setAddress("redis://localhost:6379");
+ // 创建RedissonClient对象
+ return Redisson.create(config);
+ }
+}
diff --git a/backend/src/main/java/com/flyingpig/kclassrollcall/controller/StudentController.java b/backend/src/main/java/com/flyingpig/kclassrollcall/controller/StudentController.java
index 3496094..bf0603a 100644
--- a/backend/src/main/java/com/flyingpig/kclassrollcall/controller/StudentController.java
+++ b/backend/src/main/java/com/flyingpig/kclassrollcall/controller/StudentController.java
@@ -3,13 +3,19 @@ package com.flyingpig.kclassrollcall.controller;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
+import com.flyingpig.kclassrollcall.common.RedisConstant;
import com.flyingpig.kclassrollcall.common.Result;
import com.flyingpig.kclassrollcall.entity.Student;
+import com.flyingpig.kclassrollcall.filter.UserContext;
import com.flyingpig.kclassrollcall.service.IStudentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
+
+import javax.swing.text.html.parser.Element;
+
/**
*
* 前端控制器
@@ -26,10 +32,19 @@ public class StudentController {
@Autowired
IStudentService studentService;
+ @Autowired
+ StringRedisTemplate stringRedisTemplate;
+
@PutMapping("/score")
public Result modifyScore(Long id, Double score) {
- return Result.success(studentService.updateById(new Student(id,
- null, null, null, studentService.getById(id).getScore() + score, null)));
+ if (studentService.updateById(new Student(id,
+ null, null, null, studentService.getById(id).getScore() + score, null))) {
+ stringRedisTemplate.delete(RedisConstant.STUDENT_SCORE_KEY + id);
+ return Result.success();
+ } else {
+ return Result.error("修改分数出错");
+ }
+
}
@GetMapping("/roll-call")
@@ -44,6 +59,7 @@ public class StudentController {
@PostMapping("/import")
public Result importExcel(@RequestParam("file") MultipartFile file) {
+ System.out.println("用户上传文件");
// 如果 validateExcelHeader 返回 false,则文件类型或格式错误
if (!validateExcelHeader(file)) {
return Result.error("文件类型或格式错误");
@@ -81,6 +97,8 @@ public class StudentController {
@DeleteMapping("/{id}")
public Result deleteStudent(@PathVariable String id) {
if (studentService.removeById(id)) {
+ stringRedisTemplate.delete(RedisConstant.STUDENT_LIST_KEY + UserContext.getUser());
+ stringRedisTemplate.delete(RedisConstant.STUDENT_SCORE_KEY + id);
return Result.success();
} else {
return Result.error("删除失败");
diff --git a/backend/src/main/java/com/flyingpig/kclassrollcall/controller/TeacherController.java b/backend/src/main/java/com/flyingpig/kclassrollcall/controller/TeacherController.java
index d8f25c3..b62b887 100644
--- a/backend/src/main/java/com/flyingpig/kclassrollcall/controller/TeacherController.java
+++ b/backend/src/main/java/com/flyingpig/kclassrollcall/controller/TeacherController.java
@@ -37,4 +37,9 @@ public class TeacherController {
return teacherService.register(teacher);
}
+ @PostMapping("/logout")
+ public Result logout(@RequestBody Teacher teacher){
+ return Result.success();
+ }
+
}
diff --git a/backend/src/main/java/com/flyingpig/kclassrollcall/dto/resp/StudentInfoInCache.java b/backend/src/main/java/com/flyingpig/kclassrollcall/dto/resp/StudentInfoInCache.java
new file mode 100644
index 0000000..8a36342
--- /dev/null
+++ b/backend/src/main/java/com/flyingpig/kclassrollcall/dto/resp/StudentInfoInCache.java
@@ -0,0 +1,19 @@
+package com.flyingpig.kclassrollcall.dto.resp;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class StudentInfoInCache {
+ private Long id;
+
+ private String name;
+
+ private Integer no;
+
+ private String major;
+
+}
diff --git a/backend/src/main/java/com/flyingpig/kclassrollcall/init/InitializeDispatcherServletController.java b/backend/src/main/java/com/flyingpig/kclassrollcall/init/InitializeDispatcherServletController.java
new file mode 100644
index 0000000..8279b2b
--- /dev/null
+++ b/backend/src/main/java/com/flyingpig/kclassrollcall/init/InitializeDispatcherServletController.java
@@ -0,0 +1,17 @@
+package com.flyingpig.kclassrollcall.init;
+
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+
+@Slf4j(topic = "Initialize DispatcherServlet")
+@RestController
+public final class InitializeDispatcherServletController {
+
+ @GetMapping("/initialize/dispatcher-servlet")
+ public void initializeDispatcherServlet() {
+ log.info("Initialized the dispatcherServlet to improve the first response time of the interface...");
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/flyingpig/kclassrollcall/init/InitializeDispatcherServletHandler.java b/backend/src/main/java/com/flyingpig/kclassrollcall/init/InitializeDispatcherServletHandler.java
new file mode 100644
index 0000000..1965787
--- /dev/null
+++ b/backend/src/main/java/com/flyingpig/kclassrollcall/init/InitializeDispatcherServletHandler.java
@@ -0,0 +1,29 @@
+package com.flyingpig.kclassrollcall.init;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.core.env.ConfigurableEnvironment;
+import org.springframework.http.HttpMethod;
+import org.springframework.web.client.RestTemplate;
+
+@RequiredArgsConstructor
+public final class InitializeDispatcherServletHandler implements CommandLineRunner {
+
+ private final RestTemplate restTemplate;
+
+ private final ConfigurableEnvironment configurableEnvironment;
+
+ @Override
+ public void run(String... args) throws Exception {
+ String url = String.format("http://127.0.0.1:%s%s",
+ configurableEnvironment.getProperty("server.port", "8080") + configurableEnvironment.getProperty("server.servlet.context-path", ""),
+ "/initialize/dispatcher-servlet");
+ try {
+ restTemplate.execute(url, HttpMethod.GET, null, null);
+ } catch (Throwable ignored) {
+ }
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/flyingpig/kclassrollcall/service/impl/StudentServiceImpl.java b/backend/src/main/java/com/flyingpig/kclassrollcall/service/impl/StudentServiceImpl.java
index 3979a32..3c00f31 100644
--- a/backend/src/main/java/com/flyingpig/kclassrollcall/service/impl/StudentServiceImpl.java
+++ b/backend/src/main/java/com/flyingpig/kclassrollcall/service/impl/StudentServiceImpl.java
@@ -7,19 +7,27 @@ import com.alibaba.excel.read.listener.ReadListener;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.flyingpig.kclassrollcall.common.RedisConstant;
import com.flyingpig.kclassrollcall.common.Result;
import com.flyingpig.kclassrollcall.common.RollCallMode;
import com.flyingpig.kclassrollcall.dto.req.StudentExcelModel;
+import com.flyingpig.kclassrollcall.dto.resp.StudentInfoInCache;
import com.flyingpig.kclassrollcall.entity.Student;
import com.flyingpig.kclassrollcall.filter.UserContext;
import com.flyingpig.kclassrollcall.mapper.StudentMapper;
import com.flyingpig.kclassrollcall.service.IStudentService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.flyingpig.kclassrollcall.util.cache.ListCacheUtil;
+import com.flyingpig.kclassrollcall.util.cache.StringCacheUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
/**
*
@@ -32,28 +40,67 @@ import java.util.*;
@Service
public class StudentServiceImpl extends ServiceImpl implements IStudentService {
+
+ @Autowired
+ ListCacheUtil listCacheUtil;
+
+ @Autowired
+ StringCacheUtil stringCacheUtil;
+
@Override
public Result rollCall(String mode) {
if (!mode.equals(RollCallMode.EQUAL)) {
return Result.success(rollBackStudentBaseScore());
} else {
- return Result.success(this.baseMapper.rollCall(UserContext.getUser(), new Random().nextInt(count())));
+ return Result.success(selectStudentByTeacherId().get(new Random().nextInt(count()) - 1));
}
}
+
+ private List selectStudentByTeacherId() {
+ List cachedList = listCacheUtil.safeGetWithLock(
+ RedisConstant.STUDENT_LIST_KEY + UserContext.getUser(),
+ StudentInfoInCache.class, // 使用具体的 StudentInfoInCache.class 作为类型
+ () -> {
+ // 查询学生的逻辑,转换为 StudentInfoInCache 列表
+ return this.baseMapper.selectList(new LambdaQueryWrapper()
+ .eq(Student::getTeacherId, UserContext.getUser())).stream()
+ .map(student -> new StudentInfoInCache(student.getId(), student.getName(), student.getNo(), student.getMajor()))
+ .collect(Collectors.toList());
+ },
+ 30L,
+ TimeUnit.MINUTES
+ );
+ List students = new ArrayList<>();
+ for (StudentInfoInCache studentInfo : cachedList) {
+ Student newStudent = new Student();
+ BeanUtil.copyProperties(studentInfo, newStudent);
+ newStudent.setScore(stringCacheUtil.safeGetWithLock(
+ RedisConstant.STUDENT_SCORE_KEY + studentInfo.getId(),
+ Double.class, // 传入正确的类型
+ () -> {
+ return this.getBaseMapper().selectById(studentInfo.getId()).getScore();
+ },
+ 30L,
+ TimeUnit.MINUTES
+ ));
+ newStudent.setTeacherId(Long.parseLong(UserContext.getUser()));
+ students.add(newStudent);
+ }
+ return students;
+ }
+
+
private Student rollBackStudentBaseScore() {
// 获取符合条件的学生列表
- LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper()
- .eq(Student::getTeacherId, UserContext.getUser());
- List students = this.baseMapper.selectList(queryWrapper);
-
+ List students = selectStudentByTeacherId();
// 计算权重
double totalWeight = 0;
Map weightMap = new HashMap<>();
for (Student student : students) {
double weight;
if (student.getScore() > 0) {
- weight = 1.0 / student.getScore(); // 正常权重
+ weight = 1.0 / student.getScore();
} else {
weight = 1; // 给分数为0的学生一个较大的固定权重
}
@@ -81,10 +128,12 @@ public class StudentServiceImpl extends ServiceImpl impl
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
// 如果 no 和 name 不为空,则分别进行模糊查询
- queryWrapper.like(StringUtils.isNotBlank(no), Student::getNo, no)
- .or()
+ queryWrapper
+ .like(StringUtils.isNotBlank(no), Student::getNo, no)
+ .or(StringUtils.isNotBlank(name)) // 仅在 name 不为空时才使用 or
.like(StringUtils.isNotBlank(name), Student::getName, name)
.eq(Student::getTeacherId, UserContext.getUser());
+ System.out.println(queryWrapper.getTargetSql());
// 分页查询
Page page = new Page<>(pageNo, pageSize);
@@ -104,7 +153,7 @@ public class StudentServiceImpl extends ServiceImpl impl
if (!students.isEmpty()) {
saveBatch(students);
}
-
+ stringCacheUtil.getInstance().delete(RedisConstant.STUDENT_LIST_KEY + UserContext.getUser());
return Result.success("导入成功,已添加 " + students.size() + " 条记录");
}
diff --git a/backend/src/main/java/com/flyingpig/kclassrollcall/util/cache/CacheLoader.java b/backend/src/main/java/com/flyingpig/kclassrollcall/util/cache/CacheLoader.java
new file mode 100644
index 0000000..3d5f75a
--- /dev/null
+++ b/backend/src/main/java/com/flyingpig/kclassrollcall/util/cache/CacheLoader.java
@@ -0,0 +1,14 @@
+package com.flyingpig.kclassrollcall.util.cache;
+
+/**
+ * 缓存加载器
+ * 用于将数据库信息加载到缓存中
+ */
+@FunctionalInterface
+public interface CacheLoader {
+
+ /**
+ * 加载缓存
+ */
+ T load();
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/flyingpig/kclassrollcall/util/cache/ListCacheUtil.java b/backend/src/main/java/com/flyingpig/kclassrollcall/util/cache/ListCacheUtil.java
new file mode 100644
index 0000000..62b714d
--- /dev/null
+++ b/backend/src/main/java/com/flyingpig/kclassrollcall/util/cache/ListCacheUtil.java
@@ -0,0 +1,101 @@
+package com.flyingpig.kclassrollcall.util.cache;
+
+import com.alibaba.fastjson.JSON;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.ListOperations;
+import org.springframework.data.redis.core.RedisCallback;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+@Component
+public class ListCacheUtil {
+
+ @Autowired
+ private StringRedisTemplate stringRedisTemplate;
+
+ @Autowired
+ private RedissonClient redissonClient;
+
+ public void set(String key, List values, Long time, TimeUnit unit) {
+ // 使用事务确保原子性
+ stringRedisTemplate.execute((RedisCallback) connection -> {
+ // 清空现有列表内容
+ stringRedisTemplate.opsForList().trim(key, 0, -1);
+ // 将列表中的每个值存储到 Redis 列表中
+ values.forEach(value -> stringRedisTemplate.opsForList().rightPush(key, JSON.toJSONString(value)));
+ // 设置列表的过期时间
+ stringRedisTemplate.expire(key, time, unit);
+ return null;
+ });
+ }
+
+ public List get(String key, Class type) {
+ List value = stringRedisTemplate.opsForList().range(key, 0, -1);
+ return (value == null || value.isEmpty()) ? null : value.stream()
+ .map(json -> JSON.parseObject(json, type))
+ .collect(Collectors.toList());
+ }
+
+ public List safeGetWithLock(String key, Class type, CacheLoader> cacheLoader, Long time, TimeUnit unit) {
+ List cachedValues = get(key, type);
+
+ // 1. 命中且不为空,直接返回; 命中却为空,返回 null
+ if (cachedValues != null) {
+ return cachedValues;
+ }
+
+ // 获取锁
+ String lockKey = "lock:" + key;
+ RLock rLock = redissonClient.getLock(lockKey);
+ List result = null;
+
+ try {
+ if (rLock.tryLock()) { // 尝试获取锁,避免死锁
+ // 再次查询 Redis,双重判定
+ cachedValues = get(key, type);
+ if (cachedValues != null) {
+ return cachedValues;
+ }
+
+ // 获取锁成功,查询数据库
+ result = loadAndSet(key, cacheLoader, time, unit);
+ }
+ } catch (Exception e) {
+ // 记录日志
+ // logger.error("Error occurred while accessing cache with key: {}", key, e);
+ } finally {
+ // 确保释放锁
+ if (rLock.isHeldByCurrentThread()) {
+ rLock.unlock();
+ }
+ }
+
+ return result;
+ }
+
+ private List loadAndSet(String key, CacheLoader> cacheLoader, Long time, TimeUnit unit) {
+ // 获取锁成功,查询数据库
+ List result = cacheLoader.load(); // 返回 List
+
+ if (result == null || result.isEmpty()) {
+ // 将空值写入 Redis,返回 null
+ stringRedisTemplate.opsForList().rightPush(key, ""); // 存储一个空字符串作为占位符
+ stringRedisTemplate.expire(key, time, unit);
+ return null;
+ }
+
+ // 存在,写入 Redis
+ set(key, result, time, unit);
+ return result;
+ }
+
+ public ListOperations getListOperations() {
+ return stringRedisTemplate.opsForList();
+ }
+}
diff --git a/backend/src/main/java/com/flyingpig/kclassrollcall/util/cache/StringCacheUtil.java b/backend/src/main/java/com/flyingpig/kclassrollcall/util/cache/StringCacheUtil.java
new file mode 100644
index 0000000..d9ff816
--- /dev/null
+++ b/backend/src/main/java/com/flyingpig/kclassrollcall/util/cache/StringCacheUtil.java
@@ -0,0 +1,101 @@
+package com.flyingpig.kclassrollcall.util.cache;
+
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.fastjson.JSON;
+import lombok.extern.slf4j.Slf4j;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@Component
+public class StringCacheUtil {
+
+ @Autowired
+ private StringRedisTemplate stringRedisTemplate;
+
+ @Autowired
+ private RedissonClient redissonClient;
+
+ public ValueOperations getValueOperations() {
+ return stringRedisTemplate.opsForValue();
+ }
+
+ public StringRedisTemplate getInstance() {
+ return stringRedisTemplate;
+ }
+
+ public void set(String key, Object value, Long time, TimeUnit unit) {
+ stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(value), time, unit);
+ }
+
+ public T get(String key, Class type) {
+ String value = stringRedisTemplate.opsForValue().get(key);
+ if (value != null) {
+ return JSON.parseObject(value, type);
+ }
+ return null; // 允许返回 null
+ }
+
+ // 查询时缓存空值防止缓存穿透,互斥锁查询防止缓存击穿
+ public T safeGetWithLock(
+ String key, Class type, CacheLoader cacheLoader, Long time, TimeUnit unit) {
+ String json = stringRedisTemplate.opsForValue().get(key);
+
+ // 命中且不为空字符串,直接返回;命中却为空字符串,返回 null
+ if (StrUtil.isNotBlank(json)) {
+ return JSON.parseObject(json, type);
+ } else if (json != null) {
+ return null; // 允许返回 null
+ }
+
+ // 获取锁
+ String lockKey = String.format("lock:%s", key);
+ T result = null;
+ RLock rLock = redissonClient.getLock(lockKey);
+
+ try {
+ rLock.lock();
+
+ // 再次查询redis,双重判定
+ json = stringRedisTemplate.opsForValue().get(key);
+ if (StrUtil.isNotBlank(json)) {
+ return JSON.parseObject(json, type);
+ } else if (json != null) {
+ return null; // 允许返回 null
+ }
+
+ // 获取锁成功,查询数据库
+ result = loadAndSet(key, cacheLoader, time, unit);
+ } catch (Exception e) {
+ // 记录日志
+ log.error("Error occurred while accessing cache with key: {}", key, e);
+ } finally {
+ if (rLock.isHeldByCurrentThread()) {
+ rLock.unlock();
+ }
+ }
+
+ return result;
+ }
+
+ private T loadAndSet(String key, CacheLoader cacheLoader, Long time, TimeUnit unit) {
+ T result = cacheLoader.load();
+
+ // 不存在,将空值写入redis,返回 null
+ if (result == null) {
+ stringRedisTemplate.opsForValue().set(key, "", time, TimeUnit.MINUTES);
+ } else {
+ this.set(key, result, time, unit);
+ }
+
+ return result;
+ }
+
+
+}
diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml
index f6f323b..358d912 100644
--- a/backend/src/main/resources/application.yml
+++ b/backend/src/main/resources/application.yml
@@ -6,6 +6,17 @@ spring:
url: jdbc:mysql://localhost:3306/k-class-roll-call
username: root
password: '@Aa123456'
+ hikari:
+ minimum-idle: 5 # 最小空闲连接数
+ maximum-pool-size: 10 # 连接池最大连接数
+ connection-timeout: 30000 # 连接超时时间
+ idle-timeout: 600000 # 空闲连接的存活时间
+ max-lifetime: 1800000 # 连接的最长存活时间
+
+ redis:
+ host: localhost
+ port: 6379
+ database: 0
logging:
level:
com.baomidou.mybatisplus: DEBUG # MyBatis-Plus 日志级别
diff --git a/backend/src/main/resources/mapper/StudentMapper.xml b/backend/src/main/resources/mapper/CacheUtil.xml
similarity index 100%
rename from backend/src/main/resources/mapper/StudentMapper.xml
rename to backend/src/main/resources/mapper/CacheUtil.xml
diff --git a/backend/src/test/java/com/flyingpig/kclassrollcall/StudentServiceImplTest.java b/backend/src/test/java/com/flyingpig/kclassrollcall/StudentServiceImplTest.java
new file mode 100644
index 0000000..c85ea3e
--- /dev/null
+++ b/backend/src/test/java/com/flyingpig/kclassrollcall/StudentServiceImplTest.java
@@ -0,0 +1,72 @@
+package com.flyingpig.kclassrollcall;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.flyingpig.kclassrollcall.common.RedisConstant;
+import com.flyingpig.kclassrollcall.common.Result;
+import com.flyingpig.kclassrollcall.common.RollCallMode;
+import com.flyingpig.kclassrollcall.dto.resp.StudentInfoInCache;
+import com.flyingpig.kclassrollcall.entity.Student;
+import com.flyingpig.kclassrollcall.filter.UserContext;
+import com.flyingpig.kclassrollcall.mapper.StudentMapper;
+import com.flyingpig.kclassrollcall.service.impl.StudentServiceImpl;
+import com.flyingpig.kclassrollcall.util.cache.ListCacheUtil;
+import com.flyingpig.kclassrollcall.util.cache.StringCacheUtil;
+import com.google.common.cache.CacheLoader;
+import org.apache.poi.ss.formula.functions.T;
+import org.junit.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.*;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.*;
+import static org.mockito.ArgumentMatchers.any;
+
+@ExtendWith(MockitoExtension.class)
+public class StudentServiceImplTest {
+
+ @Mock
+ private StudentMapper studentMapper;
+
+ @Mock
+ private ListCacheUtil listCacheUtil;
+
+ @Mock
+ private StringCacheUtil stringCacheUtil;
+
+
+
+ @InjectMocks
+ private StudentServiceImpl studentService;
+
+ @Test
+ public
+
+ void testRollCall() {
+ // 模拟依赖方法
+ // 调用依赖方法方法
+ Mockito.doReturn(Result.success()).when(studentService).rollCall(RollCallMode.EQUAL);
+ List mockStudents = Arrays.asList(
+ new Student(1L, "Alice", 1, "Math", 50.0, 100L),
+ new Student(2L, "Bob", 2, "Science", 0.0, 100L)
+ );
+ when(studentMapper.selectList(new LambdaQueryWrapper().eq(Student::getTeacherId, any()))).thenReturn(mockStudents);
+ // 模拟 rollCall 调用
+ Result result = studentService.rollCall(RollCallMode.EQUAL);
+
+ // 验证返回结果
+ assertNotNull(result);
+ assertTrue(mockStudents.contains(result.getData())); // 随机选择一个学生
+ }
+}
diff --git a/backend/src/test/java/com/flyingpig/kclassrollcall/TeacherServiceImplTest.java b/backend/src/test/java/com/flyingpig/kclassrollcall/TeacherServiceImplTest.java
new file mode 100644
index 0000000..7a2dd27
--- /dev/null
+++ b/backend/src/test/java/com/flyingpig/kclassrollcall/TeacherServiceImplTest.java
@@ -0,0 +1,70 @@
+package com.flyingpig.kclassrollcall;
+
+import com.flyingpig.kclassrollcall.common.Result;
+import com.flyingpig.kclassrollcall.dto.req.LoginReq;
+import com.flyingpig.kclassrollcall.dto.resp.LoginResp;
+import com.flyingpig.kclassrollcall.entity.Teacher;
+import com.flyingpig.kclassrollcall.mapper.TeacherMapper;
+import com.flyingpig.kclassrollcall.service.ITeacherService;
+import com.flyingpig.kclassrollcall.service.impl.TeacherServiceImpl;
+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.boot.test.context.SpringBootTest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+@SpringBootTest
+public class TeacherServiceImplTest {
+
+ @InjectMocks
+ private TeacherServiceImpl teacherService; // 替换为你的实际服务类
+
+ @Mock
+ private TeacherMapper teacherMapper; // 替换为你的 TeacherMapper 类
+
+ @BeforeEach
+ public void setUp() {
+ MockitoAnnotations.openMocks(this);
+ }
+
+ @Test
+ public void testLoginSuccess() {
+ // Arrange
+ LoginReq loginReq = new LoginReq("flyingpig", "flyingpig");
+ Teacher teacher = new Teacher();
+ teacher.setId(1L);
+ teacher.setName("John Doe");
+ teacher.setUsername("flyingpig");
+ teacher.setPassword("flyingpig");
+
+ when(teacherMapper.selectOne(any())).thenReturn(teacher); // 模拟数据库返回教师对象
+
+ // Act
+ Result result = teacherService.login(loginReq);
+
+ // Assert
+ assertEquals(200, result.getCode()); // 检查返回代码
+ assertEquals("John Doe", ((LoginResp) result.getData()).getName()); // 检查返回的教师名称
+ }
+
+ @Test
+ public void testLoginFailure() {
+ // Arrange
+ LoginReq loginReq = new LoginReq("username", "wrongpassword");
+
+ when(teacherMapper.selectOne(any())).thenReturn(null);
+
+ // Act
+ Result result = teacherService.login(loginReq);
+
+ // Assert
+ assertEquals(500, result.getCode()); // 检查返回代码
+ assertEquals("账号或密码错误", result.getMsg()); // 检查返回的错误信息
+ }
+
+}
\ No newline at end of file
diff --git a/frontend/public/1.js b/frontend/public/1.js
new file mode 100644
index 0000000..0fb966e
--- /dev/null
+++ b/frontend/public/1.js
@@ -0,0 +1,135 @@
+function clickEffect() {
+ let balls = [];
+ let longPressed = false;
+ let longPress;
+ let multiplier = 0;
+ let width, height;
+ let origin;
+ let normal;
+ let ctx;
+ const colours = ["#F73859", "#14FFEC", "#00E0FF", "#FF99FE", "#FAF15D"];
+ const canvas = document.createElement("canvas");
+ document.body.appendChild(canvas);
+ canvas.setAttribute("style", "width: 100%; height: 100%; top: 0; left: 0; z-index: 99999; position: fixed; pointer-events: none;");
+ const pointer = document.createElement("span");
+ pointer.classList.add("pointer");
+ document.body.appendChild(pointer);
+
+ if (canvas.getContext && window.addEventListener) {
+ ctx = canvas.getContext("2d");
+ updateSize();
+ window.addEventListener('resize', updateSize, false);
+ loop();
+ window.addEventListener("mousedown", function(e) {
+ pushBalls(randBetween(10, 20), e.clientX, e.clientY);
+ document.body.classList.add("is-pressed");
+ longPress = setTimeout(function() {
+ document.body.classList.add("is-longpress");
+ longPressed = true;
+ }, 500);
+ }, false);
+ window.addEventListener("mouseup", function(e) {
+ clearInterval(longPress);
+ if (longPressed == true) {
+ document.body.classList.remove("is-longpress");
+ pushBalls(randBetween(50 + Math.ceil(multiplier), 100 + Math.ceil(multiplier)), e.clientX, e.clientY);
+ longPressed = false;
+ }
+ document.body.classList.remove("is-pressed");
+ }, false);
+ window.addEventListener("mousemove", function(e) {
+ let x = e.clientX;
+ let y = e.clientY;
+ pointer.style.top = y + "px";
+ pointer.style.left = x + "px";
+ }, false);
+ } else {
+ console.log("canvas or addEventListener is unsupported!");
+ }
+
+
+ function updateSize() {
+ canvas.width = window.innerWidth * 2;
+ canvas.height = window.innerHeight * 2;
+ canvas.style.width = window.innerWidth + 'px';
+ canvas.style.height = window.innerHeight + 'px';
+ ctx.scale(2, 2);
+ width = (canvas.width = window.innerWidth);
+ height = (canvas.height = window.innerHeight);
+ origin = {
+ x: width / 2,
+ y: height / 2
+ };
+ normal = {
+ x: width / 2,
+ y: height / 2
+ };
+ }
+ class Ball {
+ constructor(x = origin.x, y = origin.y) {
+ this.x = x;
+ this.y = y;
+ this.angle = Math.PI * 2 * Math.random();
+ if (longPressed == true) {
+ this.multiplier = randBetween(14 + multiplier, 15 + multiplier);
+ } else {
+ this.multiplier = randBetween(6, 12);
+ }
+ this.vx = (this.multiplier + Math.random() * 0.5) * Math.cos(this.angle);
+ this.vy = (this.multiplier + Math.random() * 0.5) * Math.sin(this.angle);
+ this.r = randBetween(8, 12) + 3 * Math.random();
+ this.color = colours[Math.floor(Math.random() * colours.length)];
+ }
+ update() {
+ this.x += this.vx - normal.x;
+ this.y += this.vy - normal.y;
+ normal.x = -2 / window.innerWidth * Math.sin(this.angle);
+ normal.y = -2 / window.innerHeight * Math.cos(this.angle);
+ this.r -= 0.3;
+ this.vx *= 0.9;
+ this.vy *= 0.9;
+ }
+ }
+
+ function pushBalls(count = 1, x = origin.x, y = origin.y) {
+ for (let i = 0; i < count; i++) {
+ balls.push(new Ball(x, y));
+ }
+ }
+
+ function randBetween(min, max) {
+ return Math.floor(Math.random() * max) + min;
+ }
+
+ function loop() {
+ ctx.fillStyle = "rgba(255, 255, 255, 0)";
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ for (let i = 0; i < balls.length; i++) {
+ let b = balls[i];
+ if (b.r < 0) continue;
+ ctx.fillStyle = b.color;
+ ctx.beginPath();
+ ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2, false);
+ ctx.fill();
+ b.update();
+ }
+ if (longPressed == true) {
+ multiplier += 0.2;
+ } else if (!longPressed && multiplier >= 0) {
+ multiplier -= 0.4;
+ }
+ removeBall();
+ requestAnimationFrame(loop);
+ }
+
+ function removeBall() {
+ for (let i = 0; i < balls.length; i++) {
+ let b = balls[i];
+ if (b.x + b.r < 0 || b.x - b.r > width || b.y + b.r < 0 || b.y - b.r > height || b.r < 0) {
+ balls.splice(i, 1);
+ }
+ }
+ }
+}
+clickEffect();//调用
+
diff --git a/frontend/public/index.html b/frontend/public/index.html
index 36c68ab..93db778 100644
--- a/frontend/public/index.html
+++ b/frontend/public/index.html
@@ -1,17 +1,22 @@
-
-
-
-
-
- K班点名系统ヾ(≧▽≦*)o
-
-
-
-
-
-
-
+
+
+
+
+
+
+ K班点名系统ヾ(≧▽≦*)o
+
+
+
+
+
+
+
+
+
+