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 + + + + +
+ + + + + \ No newline at end of file diff --git a/frontend/src/assets/home/BackGound2.webp b/frontend/src/assets/home/BackGound2.webp new file mode 100644 index 0000000..b4e49c6 Binary files /dev/null and b/frontend/src/assets/home/BackGound2.webp differ diff --git a/frontend/src/assets/home/BackGround.webp b/frontend/src/assets/home/BackGround.webp new file mode 100644 index 0000000..014ef4c Binary files /dev/null and b/frontend/src/assets/home/BackGround.webp differ diff --git a/frontend/src/assets/home/backgound2.png b/frontend/src/assets/home/backgound2.png new file mode 100644 index 0000000..cad0eda Binary files /dev/null and b/frontend/src/assets/home/backgound2.png differ diff --git a/frontend/src/assets/home/cloud1.png b/frontend/src/assets/home/cloud1.png deleted file mode 100644 index a7aca1b..0000000 Binary files a/frontend/src/assets/home/cloud1.png and /dev/null differ diff --git a/frontend/src/assets/home/cloud2.png b/frontend/src/assets/home/cloud2.png deleted file mode 100644 index aa19360..0000000 Binary files a/frontend/src/assets/home/cloud2.png and /dev/null differ diff --git a/frontend/src/assets/home/cloud3.png b/frontend/src/assets/home/cloud3.png deleted file mode 100644 index 410583f..0000000 Binary files a/frontend/src/assets/home/cloud3.png and /dev/null differ diff --git a/frontend/src/assets/home/cloud4.png b/frontend/src/assets/home/cloud4.png deleted file mode 100644 index 4ca5389..0000000 Binary files a/frontend/src/assets/home/cloud4.png and /dev/null differ diff --git a/frontend/src/assets/home/cloud5.png b/frontend/src/assets/home/cloud5.png deleted file mode 100644 index a9d79e9..0000000 Binary files a/frontend/src/assets/home/cloud5.png and /dev/null differ diff --git a/frontend/src/assets/home/leave-icon.png b/frontend/src/assets/home/leave-icon.png deleted file mode 100644 index a86ca48..0000000 Binary files a/frontend/src/assets/home/leave-icon.png and /dev/null differ diff --git a/frontend/src/assets/logo.png b/frontend/src/assets/logo.png index f3d2503..9218f2e 100644 Binary files a/frontend/src/assets/logo.png and b/frontend/src/assets/logo.png differ diff --git a/frontend/src/components/HomePart.vue b/frontend/src/components/HomePart.vue index 182375f..414e531 100644 --- a/frontend/src/components/HomePart.vue +++ b/frontend/src/components/HomePart.vue @@ -11,14 +11,7 @@
{{ name }}老师您好:
- -
-
学生
查询
-
- 搜索获取班级学生名单以及学生积分情况,导入学生名单 -
-
-
+
@@ -29,10 +22,12 @@
- +
-
点名
提问
-
对学生进行点名,并对到场的学生进行提问
+
学生
查询
+
+ 搜索获取班级学生名单以及学生积分情况,导入学生名单 +
@@ -59,14 +54,16 @@ export default { data() { return { - name: "MEW", + name: "FlyingPig", semester: "", }; }, beforeMount() { if (localStorage.getItem("name") == null) { alert("您还未登录或登录已过期,请重新登录!"); - this.$router.push("/login"); + if (this.$route.path !== "/login") { + this.$router.push("/login"); + } } else { this.name = localStorage.getItem("name"); } @@ -90,11 +87,10 @@ export default { position: relative; width: 85.4%; height: 100vh; - min-width: 910px; - min-height: 700px; - background-image: url("../assets/home/background.png"); + background-image: url("../assets/home/backgound2.png"); background-size: cover; display: flex; + overflow: hidden; /* 隐藏溢出内容 */ } #banner { box-sizing: border-box; @@ -103,7 +99,7 @@ export default { margin-top: 15px; width: 90%; height: 50px; - background-color: #d0e7f2; + background-color: #b9c1c4; border-radius: 20px; justify-content: center; background-size: cover; @@ -133,14 +129,6 @@ export default { align-self: flex-start; font-weight: bold; } -@keyframes move1 { - from { - transform: translate(100%, -100%); - } - to { - transform: translate(0%, 0%); - } -} @keyframes move2 { from { transform: translate(120%, -80%); @@ -173,19 +161,7 @@ export default { transform: translate(0%, 0%); } } -#cloud1 { - background-image: url("../assets/home/cloud1.png"); - background-size: contain; - position: absolute; - z-index: 100; - width: 439px; - height: 259px; - top: 70.9%; - left: 6%; - animation: move1 1.5s ease; -} #cloud2 { - background-image: url("../assets/home/cloud2.png"); background-size: contain; position: absolute; z-index: 100; @@ -196,7 +172,6 @@ export default { animation: move2 1.5s ease; } #cloud3 { - background-image: url("../assets/home/cloud3.png"); background-size: contain; position: absolute; z-index: 100; @@ -207,7 +182,6 @@ export default { animation: move3 1.5s ease; } #cloud4 { - background-image: url("../assets/home/cloud4.png"); background-size: contain; position: absolute; z-index: 100; @@ -218,7 +192,6 @@ export default { animation: move4 1.5s ease; } #cloud5 { - background-image: url("../assets/home/cloud5.png"); background-size: contain; position: absolute; z-index: 100; @@ -234,7 +207,7 @@ export default { margin-top: 25px; margin-left: 20px; position: absolute; - color: white; + color: black; } .func-content { font-size: 18px; @@ -243,20 +216,6 @@ export default { margin-left: 20px; position: absolute; } -#title1 { - top: 23%; - left: 60%; - letter-spacing: 2px; -} -#content1 { - width: 190px; - top: 40%; - left: 12%; - color: #000; -} -#content1:hover { - color: rgb(255, 86, 86); -} #title2 { top: 23%; left: 60%; @@ -269,22 +228,13 @@ export default { color: #000; } #content2:hover { - color: rgb(255, 86, 86); + color: white; } #title3 { top: 38%; left: 60%; letter-spacing: 2px; } -#content3 { - width: 190px; - top: 26%; - left: 14%; - color: #000; -} -#content3:hover { - color: rgb(255, 86, 86); -} #title4 { top: 45%; left: 37%; @@ -294,10 +244,10 @@ export default { width: 190px; top: 21%; left: 14%; - color: #000; + color: black; } #content4:hover { - color: rgb(255, 86, 86); + color: white; } #title5 { top: 35%; @@ -311,6 +261,6 @@ export default { color: #000; } #content5:hover { - color: rgb(255, 86, 86); + color: white; } \ No newline at end of file diff --git a/frontend/src/components/Question.vue b/frontend/src/components/Question.vue index 3b0b93c..6a6bcd0 100644 --- a/frontend/src/components/Question.vue +++ b/frontend/src/components/Question.vue @@ -1,5 +1,5 @@ - - - getButtonStyle(index) { - const angle = (index / this.scores.length) * 2 * Math.PI; // 计算每个按钮的角度 - const radius = 150; // 圆环的半径 - const x = radius * Math.cos(angle); // 按钮的 x 坐标 - const y = radius * Math.sin(angle); // 按钮的 y 坐标 - - return { - position: 'absolute', - left: `${150 + x}px`, // 将 x 坐标平移至容器中心 - top: `${150 + y}px`, // 将 y 坐标平移至容器中心 - }; - }, - }, - }; - - - - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/src/components/RollCall.vue b/frontend/src/components/RollCall.vue index 6a39da4..6f7fd65 100644 --- a/frontend/src/components/RollCall.vue +++ b/frontend/src/components/RollCall.vue @@ -1,11 +1,8 @@