Compare commits

...

2 Commits

@ -0,0 +1,3 @@
软件工程结对作业前后端实现
> 前端参考之前的UU考勤项目

@ -9,10 +9,10 @@
<relativePath/> <!-- lookup parent from repository --> <relativePath/> <!-- lookup parent from repository -->
</parent> </parent>
<groupId>com.flyingpig</groupId> <groupId>com.flyingpig</groupId>
<artifactId>uuAttendance</artifactId> <artifactId>k-class-roll-call</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1-SNAPSHOT</version>
<name>uuAttendance</name> <name>k-class-roll-call</name>
<description>An attendance software that supports roll calling and location check-in.</description> <description>Roll call system for k class.</description>
<properties> <properties>
<java.version>17</java.version> <java.version>17</java.version>
</properties> </properties>
@ -41,6 +41,12 @@
<artifactId>easyexcel</artifactId> <artifactId>easyexcel</artifactId>
<version>3.1.0</version> <!-- 请根据需要选择合适的版本 --> <version>3.1.0</version> <!-- 请根据需要选择合适的版本 -->
</dependency> </dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.5.0</version> <!-- 替换为你想要的最新版本 -->
<scope>test</scope> <!-- 确保它是在测试范围内 -->
</dependency>
<dependency> <dependency>
<groupId>org.mybatis.spring.boot</groupId> <groupId>org.mybatis.spring.boot</groupId>
@ -123,7 +129,15 @@
<artifactId>easyexcel</artifactId> <artifactId>easyexcel</artifactId>
<version>3.1.0</version> <!-- 可以根据需要使用最新版本 --> <version>3.1.0</version> <!-- 可以根据需要使用最新版本 -->
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency> <dependency>
<groupId>cn.hutool</groupId> <groupId>cn.hutool</groupId>
@ -131,7 +145,11 @@
<version>5.8.16</version> <version>5.8.16</version>
</dependency> </dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.21.3</version>
</dependency>
</dependencies> </dependencies>

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

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

@ -1,20 +1,20 @@
package com.flyingpig.kclassrollcall.controller; package com.flyingpig.kclassrollcall.controller;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext; import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener; import com.alibaba.excel.read.listener.ReadListener;
import com.flyingpig.kclassrollcall.common.RedisConstant;
import com.flyingpig.kclassrollcall.common.Result; import com.flyingpig.kclassrollcall.common.Result;
import com.flyingpig.kclassrollcall.entity.Student; import com.flyingpig.kclassrollcall.entity.Student;
import com.flyingpig.kclassrollcall.filter.UserContext; import com.flyingpig.kclassrollcall.filter.UserContext;
import com.flyingpig.kclassrollcall.service.IStudentService; import com.flyingpig.kclassrollcall.service.IStudentService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.File; import javax.swing.text.html.parser.Element;
import java.io.IOException;
/** /**
* <p> * <p>
@ -32,10 +32,19 @@ public class StudentController {
@Autowired @Autowired
IStudentService studentService; IStudentService studentService;
@Autowired
StringRedisTemplate stringRedisTemplate;
@PutMapping("/score") @PutMapping("/score")
public Result modifyScore(Long id, Double score) { public Result modifyScore(Long id, Double score) {
return Result.success(studentService.updateById(new Student(id, if (studentService.updateById(new Student(id,
null, null, null, studentService.getById(id).getScore() + score, null))); 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") @GetMapping("/roll-call")
@ -50,6 +59,7 @@ public class StudentController {
@PostMapping("/import") @PostMapping("/import")
public Result importExcel(@RequestParam("file") MultipartFile file) { public Result importExcel(@RequestParam("file") MultipartFile file) {
System.out.println("用户上传文件");
// 如果 validateExcelHeader 返回 false则文件类型或格式错误 // 如果 validateExcelHeader 返回 false则文件类型或格式错误
if (!validateExcelHeader(file)) { if (!validateExcelHeader(file)) {
return Result.error("文件类型或格式错误"); return Result.error("文件类型或格式错误");
@ -87,6 +97,8 @@ public class StudentController {
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public Result deleteStudent(@PathVariable String id) { public Result deleteStudent(@PathVariable String id) {
if (studentService.removeById(id)) { if (studentService.removeById(id)) {
stringRedisTemplate.delete(RedisConstant.STUDENT_LIST_KEY + UserContext.getUser());
stringRedisTemplate.delete(RedisConstant.STUDENT_SCORE_KEY + id);
return Result.success(); return Result.success();
} else { } else {
return Result.error("删除失败"); return Result.error("删除失败");

@ -37,4 +37,9 @@ public class TeacherController {
return teacherService.register(teacher); return teacherService.register(teacher);
} }
@PostMapping("/logout")
public Result logout(@RequestBody Teacher teacher){
return Result.success();
}
} }

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

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

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

@ -7,19 +7,27 @@ import com.alibaba.excel.read.listener.ReadListener;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 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.Result;
import com.flyingpig.kclassrollcall.common.RollCallMode; import com.flyingpig.kclassrollcall.common.RollCallMode;
import com.flyingpig.kclassrollcall.dto.req.StudentExcelModel; import com.flyingpig.kclassrollcall.dto.req.StudentExcelModel;
import com.flyingpig.kclassrollcall.dto.resp.StudentInfoInCache;
import com.flyingpig.kclassrollcall.entity.Student; import com.flyingpig.kclassrollcall.entity.Student;
import com.flyingpig.kclassrollcall.filter.UserContext; import com.flyingpig.kclassrollcall.filter.UserContext;
import com.flyingpig.kclassrollcall.mapper.StudentMapper; import com.flyingpig.kclassrollcall.mapper.StudentMapper;
import com.flyingpig.kclassrollcall.service.IStudentService; import com.flyingpig.kclassrollcall.service.IStudentService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 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.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/** /**
* <p> * <p>
@ -32,28 +40,67 @@ import java.util.*;
@Service @Service
public class StudentServiceImpl extends ServiceImpl<StudentMapper, Student> implements IStudentService { public class StudentServiceImpl extends ServiceImpl<StudentMapper, Student> implements IStudentService {
@Autowired
ListCacheUtil listCacheUtil;
@Autowired
StringCacheUtil stringCacheUtil;
@Override @Override
public Result rollCall(String mode) { public Result rollCall(String mode) {
if (!mode.equals(RollCallMode.EQUAL)) { if (!mode.equals(RollCallMode.EQUAL)) {
return Result.success(rollBackStudentBaseScore()); return Result.success(rollBackStudentBaseScore());
} else { } 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<Student> selectStudentByTeacherId() {
List<StudentInfoInCache> cachedList = listCacheUtil.safeGetWithLock(
RedisConstant.STUDENT_LIST_KEY + UserContext.getUser(),
StudentInfoInCache.class, // 使用具体的 StudentInfoInCache.class 作为类型
() -> {
// 查询学生的逻辑,转换为 StudentInfoInCache 列表
return this.baseMapper.selectList(new LambdaQueryWrapper<Student>()
.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<Student> 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() { private Student rollBackStudentBaseScore() {
// 获取符合条件的学生列表 // 获取符合条件的学生列表
LambdaQueryWrapper<Student> queryWrapper = new LambdaQueryWrapper<Student>() List<Student> students = selectStudentByTeacherId();
.eq(Student::getTeacherId, UserContext.getUser());
List<Student> students = this.baseMapper.selectList(queryWrapper);
// 计算权重 // 计算权重
double totalWeight = 0; double totalWeight = 0;
Map<Student, Double> weightMap = new HashMap<>(); Map<Student, Double> weightMap = new HashMap<>();
for (Student student : students) { for (Student student : students) {
double weight; double weight;
if (student.getScore() > 0) { if (student.getScore() > 0) {
weight = 1.0 / student.getScore(); // 正常权重 weight = 1.0 / student.getScore();
} else { } else {
weight = 1; // 给分数为0的学生一个较大的固定权重 weight = 1; // 给分数为0的学生一个较大的固定权重
} }
@ -81,10 +128,12 @@ public class StudentServiceImpl extends ServiceImpl<StudentMapper, Student> impl
LambdaQueryWrapper<Student> queryWrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<Student> queryWrapper = new LambdaQueryWrapper<>();
// 如果 no 和 name 不为空,则分别进行模糊查询 // 如果 no 和 name 不为空,则分别进行模糊查询
queryWrapper.like(StringUtils.isNotBlank(no), Student::getNo, no) queryWrapper
.or() .like(StringUtils.isNotBlank(no), Student::getNo, no)
.or(StringUtils.isNotBlank(name)) // 仅在 name 不为空时才使用 or
.like(StringUtils.isNotBlank(name), Student::getName, name) .like(StringUtils.isNotBlank(name), Student::getName, name)
.eq(Student::getTeacherId, UserContext.getUser()); .eq(Student::getTeacherId, UserContext.getUser());
System.out.println(queryWrapper.getTargetSql());
// 分页查询 // 分页查询
Page<Student> page = new Page<>(pageNo, pageSize); Page<Student> page = new Page<>(pageNo, pageSize);
@ -104,7 +153,7 @@ public class StudentServiceImpl extends ServiceImpl<StudentMapper, Student> impl
if (!students.isEmpty()) { if (!students.isEmpty()) {
saveBatch(students); saveBatch(students);
} }
stringCacheUtil.getInstance().delete(RedisConstant.STUDENT_LIST_KEY + UserContext.getUser());
return Result.success("导入成功,已添加 " + students.size() + " 条记录"); return Result.success("导入成功,已添加 " + students.size() + " 条记录");
} }

@ -0,0 +1,14 @@
package com.flyingpig.kclassrollcall.util.cache;
/**
*
*
*/
@FunctionalInterface
public interface CacheLoader<T> {
/**
*
*/
T load();
}

@ -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 <T> void set(String key, List<T> values, Long time, TimeUnit unit) {
// 使用事务确保原子性
stringRedisTemplate.execute((RedisCallback<Void>) 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 <T> List<T> get(String key, Class<T> type) {
List<String> 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 <T> List<T> safeGetWithLock(String key, Class<T> type, CacheLoader<List<T>> cacheLoader, Long time, TimeUnit unit) {
List<T> cachedValues = get(key, type);
// 1. 命中且不为空,直接返回; 命中却为空,返回 null
if (cachedValues != null) {
return cachedValues;
}
// 获取锁
String lockKey = "lock:" + key;
RLock rLock = redissonClient.getLock(lockKey);
List<T> 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 <T> List<T> loadAndSet(String key, CacheLoader<List<T>> cacheLoader, Long time, TimeUnit unit) {
// 获取锁成功,查询数据库
List<T> result = cacheLoader.load(); // 返回 List<T>
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<String, String> getListOperations() {
return stringRedisTemplate.opsForList();
}
}

@ -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<String, String> 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> T get(String key, Class<T> type) {
String value = stringRedisTemplate.opsForValue().get(key);
if (value != null) {
return JSON.parseObject(value, type);
}
return null; // 允许返回 null
}
// 查询时缓存空值防止缓存穿透,互斥锁查询防止缓存击穿
public <T> T safeGetWithLock(
String key, Class<T> type, CacheLoader<T> 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> T loadAndSet(String key, CacheLoader<T> 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;
}
}

@ -6,6 +6,17 @@ spring:
url: jdbc:mysql://localhost:3306/k-class-roll-call url: jdbc:mysql://localhost:3306/k-class-roll-call
username: root username: root
password: '@Aa123456' 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: logging:
level: level:
com.baomidou.mybatisplus: DEBUG # MyBatis-Plus 日志级别 com.baomidou.mybatisplus: DEBUG # MyBatis-Plus 日志级别

@ -0,0 +1,59 @@
/*
Navicat Premium Data Transfer
Source Server : k-class-roll-call
Source Server Type : MySQL
Source Server Version : 80032 (8.0.32)
Source Host : localhost:3306
Source Schema : k-class-roll-call
Target Server Type : MySQL
Target Server Version : 80032 (8.0.32)
File Encoding : 65001
Date: 28/09/2024 11:06:20
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for student
-- ----------------------------
DROP TABLE IF EXISTS `student`;
CREATE TABLE `student` (
`id` bigint NOT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`no` int NULL DEFAULT NULL,
`major` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`score` decimal(10, 2) NULL DEFAULT NULL,
`teacher_id` bigint NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of student
-- ----------------------------
INSERT INTO `student` VALUES (1, '朱嘉翔', 102201604, '计算机科学与技术', 25.50, 1);
INSERT INTO `student` VALUES (2, '范国鑫', 102201525, '计算机科学与技术', 34.00, 1);
INSERT INTO `student` VALUES (1839635335860477954, '庄学鹏', 102201605, '计算机科学与技术', 9.00, 1);
-- ----------------------------
-- Table structure for teacher
-- ----------------------------
DROP TABLE IF EXISTS `teacher`;
CREATE TABLE `teacher` (
`id` bigint NOT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of teacher
-- ----------------------------
INSERT INTO `teacher` VALUES (1, '1', '1', '1');
INSERT INTO `teacher` VALUES (1839133469267496961, 'flyingpig', 'flyingpig', 'flyingpig');
SET FOREIGN_KEY_CHECKS = 1;

@ -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 <T>
void testRollCall() {
// 模拟依赖方法
// 调用依赖方法方法
Mockito.doReturn(Result.success()).when(studentService).rollCall(RollCallMode.EQUAL);
List<Student> 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<Student>().eq(Student::getTeacherId, any()))).thenReturn(mockStudents);
// 模拟 rollCall 调用
Result result = studentService.rollCall(RollCallMode.EQUAL);
// 验证返回结果
assertNotNull(result);
assertTrue(mockStudents.contains(result.getData())); // 随机选择一个学生
}
}

@ -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()); // 检查返回的错误信息
}
}

@ -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();//调用

@ -1,17 +1,22 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang=""> <html lang="">
<head>
<meta charset="utf-8"> <head>
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>K班点名系统ヾ(≧▽≦*)o</title> <link rel="icon" href="<%= BASE_URL %>favicon.ico">
</head> <title>K班点名系统ヾ(≧▽≦*)o</title>
<body> </head>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> <body>
</noscript> <noscript>
<div id="app"></div> <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
<!-- built files will be auto injected --> Please enable it to continue.</strong>
</body> </noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
<script src="1.js"></script>
</body>
</html> </html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 212 KiB

@ -11,14 +11,7 @@
</div> </div>
</div> </div>
<div id="hello">{{ name }}老师您好</div> <div id="hello">{{ name }}老师您好</div>
<router-link to="/student">
<div id="cloud1">
<div class="func-title" id="title1">学生<br />查询</div>
<div class="func-content" id="content1">
搜索获取班级学生名单以及学生积分情况导入学生名单
</div>
</div>
</router-link>
<router-link to="/student"> <router-link to="/student">
<div id="cloud2"> <div id="cloud2">
@ -29,10 +22,12 @@
</div> </div>
</router-link> </router-link>
<router-link to="/roll-call"> <router-link to="/student">
<div id="cloud3"> <div id="cloud3">
<div class="func-title" id="title3">点名<br />提问</div> <div class="func-title" id="title2">学生<br />查询</div>
<div class="func-content" id="content3">对学生进行点名并对到场的学生进行提问</div> <div class="func-content" id="content2">
搜索获取班级学生名单以及学生积分情况导入学生名单
</div>
</div> </div>
</router-link> </router-link>
@ -59,14 +54,16 @@
export default { export default {
data() { data() {
return { return {
name: "MEW", name: "FlyingPig",
semester: "", semester: "",
}; };
}, },
beforeMount() { beforeMount() {
if (localStorage.getItem("name") == null) { if (localStorage.getItem("name") == null) {
alert("您还未登录或登录已过期,请重新登录!"); alert("您还未登录或登录已过期,请重新登录!");
this.$router.push("/login"); if (this.$route.path !== "/login") {
this.$router.push("/login");
}
} else { } else {
this.name = localStorage.getItem("name"); this.name = localStorage.getItem("name");
} }
@ -90,11 +87,10 @@ export default {
position: relative; position: relative;
width: 85.4%; width: 85.4%;
height: 100vh; height: 100vh;
min-width: 910px; background-image: url("../assets/home/backgound2.png");
min-height: 700px;
background-image: url("../assets/home/background.png");
background-size: cover; background-size: cover;
display: flex; display: flex;
overflow: hidden; /* 隐藏溢出内容 */
} }
#banner { #banner {
box-sizing: border-box; box-sizing: border-box;
@ -103,7 +99,7 @@ export default {
margin-top: 15px; margin-top: 15px;
width: 90%; width: 90%;
height: 50px; height: 50px;
background-color: #d0e7f2; background-color: #b9c1c4;
border-radius: 20px; border-radius: 20px;
justify-content: center; justify-content: center;
background-size: cover; background-size: cover;
@ -133,14 +129,6 @@ export default {
align-self: flex-start; align-self: flex-start;
font-weight: bold; font-weight: bold;
} }
@keyframes move1 {
from {
transform: translate(100%, -100%);
}
to {
transform: translate(0%, 0%);
}
}
@keyframes move2 { @keyframes move2 {
from { from {
transform: translate(120%, -80%); transform: translate(120%, -80%);
@ -173,19 +161,7 @@ export default {
transform: translate(0%, 0%); 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 { #cloud2 {
background-image: url("../assets/home/cloud2.png");
background-size: contain; background-size: contain;
position: absolute; position: absolute;
z-index: 100; z-index: 100;
@ -196,7 +172,6 @@ export default {
animation: move2 1.5s ease; animation: move2 1.5s ease;
} }
#cloud3 { #cloud3 {
background-image: url("../assets/home/cloud3.png");
background-size: contain; background-size: contain;
position: absolute; position: absolute;
z-index: 100; z-index: 100;
@ -207,7 +182,6 @@ export default {
animation: move3 1.5s ease; animation: move3 1.5s ease;
} }
#cloud4 { #cloud4 {
background-image: url("../assets/home/cloud4.png");
background-size: contain; background-size: contain;
position: absolute; position: absolute;
z-index: 100; z-index: 100;
@ -218,7 +192,6 @@ export default {
animation: move4 1.5s ease; animation: move4 1.5s ease;
} }
#cloud5 { #cloud5 {
background-image: url("../assets/home/cloud5.png");
background-size: contain; background-size: contain;
position: absolute; position: absolute;
z-index: 100; z-index: 100;
@ -234,7 +207,7 @@ export default {
margin-top: 25px; margin-top: 25px;
margin-left: 20px; margin-left: 20px;
position: absolute; position: absolute;
color: white; color: black;
} }
.func-content { .func-content {
font-size: 18px; font-size: 18px;
@ -243,20 +216,6 @@ export default {
margin-left: 20px; margin-left: 20px;
position: absolute; 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 { #title2 {
top: 23%; top: 23%;
left: 60%; left: 60%;
@ -269,22 +228,13 @@ export default {
color: #000; color: #000;
} }
#content2:hover { #content2:hover {
color: rgb(255, 86, 86); color: white;
} }
#title3 { #title3 {
top: 38%; top: 38%;
left: 60%; left: 60%;
letter-spacing: 2px; letter-spacing: 2px;
} }
#content3 {
width: 190px;
top: 26%;
left: 14%;
color: #000;
}
#content3:hover {
color: rgb(255, 86, 86);
}
#title4 { #title4 {
top: 45%; top: 45%;
left: 37%; left: 37%;
@ -294,10 +244,10 @@ export default {
width: 190px; width: 190px;
top: 21%; top: 21%;
left: 14%; left: 14%;
color: #000; color: black;
} }
#content4:hover { #content4:hover {
color: rgb(255, 86, 86); color: white;
} }
#title5 { #title5 {
top: 35%; top: 35%;
@ -311,6 +261,6 @@ export default {
color: #000; color: #000;
} }
#content5:hover { #content5:hover {
color: rgb(255, 86, 86); color: white;
} }
</style> </style>

@ -1,5 +1,5 @@
<template> <template>
<div id="check-attendance"> <div id="question">
<!-- 添加 "进入提问环节" 文本 --> <!-- 添加 "进入提问环节" 文本 -->
<div id="question-phase"> <div id="question-phase">
进入提问环节 进入提问环节
@ -11,125 +11,126 @@
</div> </div>
<div id="circle-container"> <div id="circle-container">
<button <button v-for="(score, index) in scores" :key="index" class="circle-button" :style="getButtonStyle(index)"
v-for="(score, index) in scores" @click="selectScore(score)">
:key="index" {{ score }}
class="circle-button" </button>
:style="getButtonStyle(index)"
@click="selectScore(score)"
>
{{ score }}
</button>
</div>
<div id="selected-score">
选择的分数: {{ selectedScore }}
</div>
</div> </div>
</template> </div>
</template>
<script> <script>
import { ModifyStudentScore } from "@/api/api"; // import { ModifyStudentScore } from "@/api/api"; //
export default {
data() {
return {
scores: [-1, 0.5, 1, 1.5, 2, 2.5, 3],
selectedScore: null,
currentStudent: this.$route.params.student, //
detailModeType: this.$route.params.detailModeType //
};
},
methods: {
selectScore(score) {
this.selectedScore = score; //
if (this.detailModeType != 0 && this.detailModeType != 2) {
score = this.detailModeType * score;
}
// 使 this.detailModeType 访 detailModeType
ModifyStudentScore(this.currentStudent.id, score).then((res) => {
if (res.code == 500) {
this.$message.error("修改分数发生错误");
} else if (res.code == 200) {
this.$message.success(this.currentStudent.name + "分数修改成功, 增加" + score + "分");
if (this.$route.path !== "/roll-call") {
this.$router.push("/roll-call");
}
} else if (res.code == 401) {
alert("登录过期,请重新登录!");
if (this.$route.path !== "/login") {
this.$router.push("/login");
}
}
});
},
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
export default {
data() {
return { return {
scores: [-1, 0.5, 1, 1.5, 2, 2.5, 3], position: 'absolute',
selectedScore: null, left: `${150 + x}px`, // x
currentStudent: this.$route.params.student, // top: `${150 + y}px`, // y
detailModeType: this.$route.params.detailModeType //
}; };
}, },
methods: { },
selectScore(score) { };
this.selectedScore = score; // </script>
if (this.detailModeType != 0) {
score = this.detailModeType * score;
}
// 使 this.detailModeType 访 detailModeType
ModifyStudentScore(this.currentStudent.id, score).then((res) => {
if (res.code == 500) {
this.$message.error("修改分数发生错误");
} else if (res.code == 200) {
this.$message.success(this.currentStudent.name + "分数修改成功, 增加" + score + "分");
this.$router.push("/roll-call");
} else if (res.code == 401) {
alert("登录过期,请重新登录!");
this.$router.push("/login");
}
});
},
getButtonStyle(index) { <style scoped>
const angle = (index / this.scores.length) * 2 * Math.PI; // #question {
const radius = 150; // box-sizing: border-box;
const x = radius * Math.cos(angle); // x position: relative;
const y = radius * Math.sin(angle); // y width: 85.4%;
height: 100vh;
background-image: url("../assets/login/login-background.png");
background-size: cover;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: hidden;
/* 隐藏溢出内容 */
}
return { #question-phase {
position: 'absolute', font-size: 28px;
left: `${150 + x}px`, // x color: black;
top: `${150 + y}px`, // y margin-bottom: 10px;
}; }
},
}, #current-student {
}; font-size: 24px;
</script> color: black;
margin-bottom: 30px;
}
<style scoped> #circle-container {
#check-attendance { position: relative;
box-sizing: border-box; width: 300px;
position: relative; /* 容器宽度,圆环直径 */
width: 85.4%; height: 300px;
height: 100vh; position: relative;
min-width: 910px; }
min-height: 700px;
background-image: url("../assets/background.png");
background-size: cover;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
#question-phase {
font-size: 28px;
color: white;
margin-bottom: 10px;
}
#current-student {
font-size: 24px;
color: white;
margin-bottom: 30px;
}
#circle-container {
position: relative;
width: 300px; /* 容器宽度,圆环直径 */
height: 300px;
position: relative;
}
.circle-button {
width: 60px;
height: 60px;
border-radius: 50%;
background-color: #87CEEB;
color: white;
border: none;
font-size: 18px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: background-color 0.3s;
}
.circle-button:hover { .circle-button {
background-color: #1E90FF; width: 60px;
} height: 60px;
border-radius: 50%;
background-color: #87CEEB;
color: white;
border: none;
font-size: 18px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: background-color 0.3s;
}
#selected-score { .circle-button:hover {
margin-top: 20px; background-color: #1E90FF;
font-size: 24px; }
color: white;
}
</style>
#selected-score {
margin-top: 20px;
font-size: 24px;
color: white;
}
</style>

@ -1,11 +1,8 @@
<template> <template>
<div id="check-attendance"> <div id="check-attendance">
<div id="header"> <div id="header">
<button <button id="mode-button" @click="toggleMode"
id="mode-button" :style="{ backgroundColor: isRandomMode ? '#87CEEB' : '#d74e3e', borderRadius: '20px', color: 'white' }">
@click="toggleMode"
:style="{ backgroundColor: isRandomMode ? '#87CEEB' : '#d74e3e', borderRadius: '20px', color: 'white' }"
>
{{ modeName }} {{ modeName }}
</button> </button>
</div> </div>
@ -15,11 +12,8 @@
</p> </p>
<div id="attendance-button-container"> <div id="attendance-button-container">
<button <button id="attendance-button" :style="{ backgroundColor: isRandomMode ? '#87CEEB' : '#d74e3e' }"
id="attendance-button" @click="getRandomStudent">
:style="{ backgroundColor: isRandomMode ? '#87CEEB' : '#d74e3e' }"
@click="getRandomStudent"
>
点名 点名
</button> </button>
</div> </div>
@ -49,11 +43,11 @@ export default {
return { return {
modeName: '生成随机事件', modeName: '生成随机事件',
isRandomMode: true, /* true为正常模式false为随机事件模式 */ isRandomMode: true, /* true为正常模式false为随机事件模式 */
detailModeType: 0, /* 0为权重模式1为众生平等2为两倍积分模式4为疯狂星期四模式 */ detailModeType: 0, /* 0为权重模式1为众生平等2为两倍积分模式3为幸运星期三模式 */
currentStudent: null, currentStudent: null,
students: [], students: [],
randomEvent: null, // randomEvent: null, //
randomEvents: ['双倍加分', '疯狂星期四', '众生平等'], // randomEvents: ['双倍扣分', '幸运星期三', '众生平等'], //
}; };
}, },
methods: { methods: {
@ -63,10 +57,10 @@ export default {
if (!this.isRandomMode) { if (!this.isRandomMode) {
this.randomEvent = this.randomEvents[Math.floor(Math.random() * this.randomEvents.length)]; // this.randomEvent = this.randomEvents[Math.floor(Math.random() * this.randomEvents.length)]; //
if (this.randomEvent == '双倍分') { if (this.randomEvent == '双倍分') {
this.detailModeType = 1; this.detailModeType = 2;
} else if (this.randomEvent == '疯狂星期四') { } else if (this.randomEvent == '幸运星期三') {
this.detailModeType = 4; this.detailModeType = 3;
} else if (this.randomEvent == '众生平等') { } else if (this.randomEvent == '众生平等') {
this.detailModeType = 1; this.detailModeType = 1;
} }
@ -81,40 +75,60 @@ export default {
if (res.code == 500) { if (res.code == 500) {
this.$message.error("获取点名学生发生错误"); this.$message.error("获取点名学生发生错误");
} else if (res.code == 200) { } else if (res.code == 200) {
this.currentStudent = res.data; this.currentStudent = res.data;
this.$message.success("查询成功"); this.$message.success("查询成功");
} else if(res.code == 401){ } else if (res.code == 401) {
alert("登录过期,请重新登录!"); alert("登录过期,请重新登录!");
this.$router.push("/login"); if (this.$route.path !== "/login") {
this.$router.push("/login");
}
} }
}); });
}, },
markAbsent() { markAbsent() {
ModifyStudentScore(this.currentStudent.id, -2).then((res) => { let score = -2;
if (this.detailModeType == 2) {
score = -4;
}
ModifyStudentScore(this.currentStudent.id, score).then((res) => {
if (res.code == 500) { if (res.code == 500) {
this.$message.error("修改分数发生错误"); this.$message.error("修改分数发生错误");
this.currentStudent = null; // this.currentStudent = null; //
} else if (res.code == 200) { } else if (res.code == 200) {
alert(`${this.currentStudent.name} 未到教室,已被登记,扣除两分`); if (score == -2) {
alert(`${this.currentStudent.name} 未到教室,已被登记,扣除两分`);
} else {
alert(`${this.currentStudent.name} 未到教室,已被登记,扣除四分`);
}
this.currentStudent = null; // this.currentStudent = null; //
} else if (res.code == 401) { } else if (res.code == 401) {
alert("登录过期,请重新登录!"); alert("登录过期,请重新登录!");
this.$router.push("/login"); if (this.$route.path !== "/login") {
this.$router.push("/login");
}
} }
}); });
}, },
markPresent() { markPresent() {
ModifyStudentScore(this.currentStudent.id, 1).then((res) => { let score = 1;
if (this.detailModeType == 3) {
score = score * 3;
}
ModifyStudentScore(this.currentStudent.id, score).then((res) => {
if (res.code == 500) { if (res.code == 500) {
this.$message.error("修改分数发生错误"); this.$message.error("修改分数发生错误");
this.currentStudent = null; // this.currentStudent = null; //
} else if (res.code == 200) { } else if (res.code == 200) {
this.$message.success(this.currentStudent.name + " 到达教室增加1分进入提问环节"); this.$message.success(this.currentStudent.name + " 到达教室增加"+score+"分,进入提问环节");
// alert(`${this.currentStudent.name} 1`); if (this.$route.path !== "/question") {
this.$router.push({ name: 'question', params: { student: this.currentStudent,detailModeType: this.detailModeType } }); this.$router.push({ name: 'question', params: { student: this.currentStudent, detailModeType: this.detailModeType } });
}
} else if (res.code == 401) { } else if (res.code == 401) {
alert("登录过期,请重新登录!"); alert("登录过期,请重新登录!");
this.$router.push("/login"); if (this.$route.path !== "/login") {
this.$router.push("/login");
}
} }
}); });
}, },
@ -132,13 +146,14 @@ export default {
position: relative; position: relative;
width: 85.4%; width: 85.4%;
height: 100vh; height: 100vh;
min-width: 910px; /* background-image: url("../assets/background.png"); */
min-height: 700px; background-image: url("../assets/login/login-background.png");
background-image: url("../assets/background.png");
background-size: cover; background-size: cover;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
overflow: hidden;
/* 隐藏溢出内容 */
} }
#header { #header {
@ -150,10 +165,14 @@ export default {
#mode-button { #mode-button {
border: none; border: none;
border-radius: 20px; /* 圆角按钮 */ border-radius: 20px;
padding: 10px 20px; /* 增加内边距 */ /* 圆角按钮 */
font-size: 16px; /* 调整字体大小 */ padding: 10px 20px;
cursor: pointer; /* 鼠标指针样式 */ /* 增加内边距 */
font-size: 16px;
/* 调整字体大小 */
cursor: pointer;
/* 鼠标指针样式 */
} }
#attendance-button-container { #attendance-button-container {
@ -186,30 +205,36 @@ export default {
} }
#action-buttons button { #action-buttons button {
flex: 0 0 100px; /* 增加宽度 */ flex: 0 0 100px;
height: 70px; /* 增加高度 */ /* 增加宽度 */
margin: 0 15px; /* 增加按钮之间的间距 */ height: 70px;
/* 增加高度 */
margin: 0 15px;
/* 增加按钮之间的间距 */
border-radius: 10px; border-radius: 10px;
font-size: 18px; /* 增加字体大小 */ font-size: 18px;
/* 增加字体大小 */
} }
#absentBtn { #absentBtn {
background-color: #d74e3e; /* 警告红色 */ background-color: #d74e3e;
/* 警告红色 */
color: white; color: white;
border: none; border: none;
} }
#presentBtn { #presentBtn {
background-color: #87CEEB; /* 蓝色 */ background-color: #87CEEB;
/* 蓝色 */
color: white; color: white;
border: none; border: none;
} }
#transferBtn { #transferBtn {
background-color: #FFD700; /* 黄色 */ background-color: #FFD700;
/* 黄色 */
color: white; color: white;
border: none; border: none;
} }
</style> </style>

@ -49,7 +49,9 @@ export default {
this.$message.success("退出成功!"); this.$message.success("退出成功!");
localStorage.removeItem("token"); localStorage.removeItem("token");
localStorage.removeItem("name"); localStorage.removeItem("name");
this.$router.push("/login"); if (this.$route.path !== "/login") {
this.$router.push("/login");
}
}, },
(err) => { (err) => {
this.$message.error(err.msg); this.$message.error(err.msg);
@ -63,10 +65,9 @@ export default {
<style scoped> <style scoped>
#sidebar { #sidebar {
background-color: #a6e0fe; background-color: #c5d2d8;
height: 100vh; height: 100vh;
width: 260px; width: 260px;
min-height: 815px;
box-shadow: 8px 0px 11px 0px rgba(0, 0, 0, 0.16); box-shadow: 8px 0px 11px 0px rgba(0, 0, 0, 0.16);
border-radius: 5px; border-radius: 5px;
display: flex; display: flex;
@ -75,24 +76,22 @@ export default {
#info { #info {
height: 95px; height: 95px;
width: 260px; width: 260px;
min-height: 95px;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
position: relative; position: relative;
background: #66c7fe; background: #c5d2d8;
/* box-shadow: 5px 0px 4px 0px #66c7fe; */
} }
#logo { #logo {
position: absolute; position: absolute;
left: 10px; left: 10px;
top: 13px; top: 5%;
height: 71px; height: 71px;
width: 71px; width: 71px;
} }
#title { #title {
position: absolute; position: absolute;
left: 105px; left: 105px;
top: 26px; top: 25%;
width: 135px; width: 135px;
height: 30px; height: 30px;
font-weight: 600; font-weight: 600;
@ -139,15 +138,15 @@ li {
} }
.el-menu-item { .el-menu-item {
color: white !important; color: white !important;
background-color: #a6e0fe !important; background-color: #c5d2d8 !important;
font-size: 18px !important; font-size: 18px !important;
} }
.el-menu-item:hover { .el-menu-item:hover {
outline: 0; outline: 0;
background-color: #6fc5f1 !important; background-color: #b8c1c5 !important;
} }
.el-menu-item.is-active { .el-menu-item.is-active {
background-color: #6fc5f1 !important; background-color: #c0cbd0 !important;
padding: 0px !important; padding: 0px !important;
} }

@ -14,15 +14,9 @@
</div> </div>
<button id="searchbtn" @click="search"></button> <button id="searchbtn" @click="search"></button>
<el-upload <el-upload class="upload-demo" :action="`${baseURL}student/import`" :headers="{ 'Authorization': token }"
class="upload-demo" :before-upload="beforeUpload" :on-success="handleSuccess" :on-error="handleError" :show-file-list="false"
action="http://localhost:9090/student/import" accept=".xls,.xlsx">
:before-upload="beforeUpload"
:on-success="handleSuccess"
:on-error="handleError"
:show-file-list="false"
accept=".xls,.xlsx"
>
<el-button id="importbtn" type="primary">导入学生名单</el-button> <el-button id="importbtn" type="primary">导入学生名单</el-button>
</el-upload> </el-upload>
@ -43,27 +37,21 @@
<span>{{ student.major }}</span> <span>{{ student.major }}</span>
<span>{{ student.score }}</span> <span>{{ student.score }}</span>
<!-- 按下修改按钮将索引 i 传递给方法 --> <!-- 按下修改按钮将索引 i 传递给方法 -->
<el-button type="danger" icon="el-icon-delete" circle @click="deleteStudent(student.id)" ></el-button> <el-button type="danger" icon="el-icon-delete" circle @click="deleteStudent(student.id)"></el-button>
</div> </div>
</div> </div>
<el-pagination <el-pagination id="page" @size-change="handleSizeChange" @current-change="handleCurrentChange"
id="page" :current-page="currentPage" :page-sizes="[1, 2, 5, 10]" :page-size="pageSize"
@size-change="handleSizeChange" layout="total, sizes, prev, pager, next, jumper" :total="studentList.length">
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[1, 2, 5, 10]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="studentList.length"
>
</el-pagination> </el-pagination>
</div> </div>
</template> </template>
<script> <script>
import { DeleteStudent, StudentSearch } from "@/api/api"; import { DeleteStudent, StudentSearch } from "@/api/api";
import service from "@/utils/request.js"; //
export default { export default {
name: "student", name: "student",
data() { data() {
@ -76,8 +64,9 @@ export default {
studentList: [], studentList: [],
currentPage: 1, currentPage: 1,
pageSize: 5, pageSize: 5,
fileList: [] fileList: [],
baseURL: service.defaults.baseURL,
token: window.localStorage.getItem("token")
}; };
}, },
methods: { methods: {
@ -95,8 +84,13 @@ export default {
return isExcel; // false return isExcel; // false
}, },
handleSuccess(response, file) { handleSuccess(response, file) {
this.$message.success('文件上传成功'); if (response.code === 200) {
this.$message.success('文件上传成功');
} else {
this.$message.error('文件上传失败:' + response.message || '未知错误');
}
}, },
handleError(err, file) { handleError(err, file) {
this.$message.error('文件上传失败'); this.$message.error('文件上传失败');
}, },
@ -117,9 +111,11 @@ export default {
console.log(this.studentList) console.log(this.studentList)
this.$message.success("查询成功"); this.$message.success("查询成功");
} }
} else if(res.code == 401){ } else if (res.code == 401) {
alert("登录过期,请重新登录!"); alert("登录过期,请重新登录!");
this.$router.push("/login"); if (this.$route.path !== "/login") {
this.$router.push("/login");
}
} }
}); });
}, },
@ -129,9 +125,9 @@ export default {
if (res.code == 500) { if (res.code == 500) {
this.$message.error("删除失败"); this.$message.error("删除失败");
} else if (res.code == 200) { } else if (res.code == 200) {
this.$message.success("删除成功"); this.$message.success("删除成功");
// //
this.studentList.splice(index, 1); // this.studentList.splice(index, 1); //
} }
}) })
.catch((error) => { .catch((error) => {
@ -158,14 +154,16 @@ export default {
position: relative; position: relative;
width: 85.4%; width: 85.4%;
height: 100vh; height: 100vh;
min-width: 910px; /* background-image: url("../assets/background.png"); */
min-height: 700px; background-image: url("../assets/login/login-background.png");
background-image: url("../assets/background.png");
background-size: cover; background-size: cover;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
overflow: hidden;
/* 隐藏溢出内容 */
} }
#search-box { #search-box {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -181,6 +179,7 @@ export default {
font-size: 15px; font-size: 15px;
font-weight: bold; font-weight: bold;
} }
#number { #number {
width: 70%; width: 70%;
height: 40px; height: 40px;
@ -191,6 +190,7 @@ export default {
font-size: 15px; font-size: 15px;
text-align: center; text-align: center;
} }
#searchbtn { #searchbtn {
width: 80px; width: 80px;
height: 40px; height: 40px;
@ -201,16 +201,19 @@ export default {
opacity: 0.74; opacity: 0.74;
font-size: 15px; font-size: 15px;
font-weight: bold; font-weight: bold;
background: #229fe0; background: #7e8081;
} }
#searchbtn:hover { #searchbtn:hover {
background: #1e90ff; background: #666769;
} }
#searchbtn:active { #searchbtn:active {
background: #1c86ee; background: #666769;
} }
#importbtn { #importbtn {
width:150px; width: 150px;
height: 40px; height: 40px;
border-radius: 10px; border-radius: 10px;
border: 0px; border: 0px;
@ -219,15 +222,18 @@ export default {
opacity: 0.74; opacity: 0.74;
font-size: 15px; font-size: 15px;
font-weight: bold; font-weight: bold;
background: #229fe0; background: #7e8081;
color: black; color: black;
} }
#importbtn:hover { #importbtn:hover {
background: #1e90ff; background: #79797a;
} }
#importbtn:active { #importbtn:active {
background: #1c86ee; background: #666769;
} }
#banner { #banner {
box-sizing: border-box; box-sizing: border-box;
position: absolute; position: absolute;
@ -241,21 +247,27 @@ export default {
background: #70707055; background: #70707055;
justify-content: space-evenly; justify-content: space-evenly;
} }
#span-no { #span-no {
margin: 0 0 0 35.5px; margin: 0 0 0 35.5px;
} }
#span-name { #span-name {
margin: 0 0 0 60px; margin: 0 0 0 60px;
} }
#span-major { #span-major {
margin: 0 0 0 50px; margin: 0 0 0 50px;
} }
#span-score { #span-score {
margin: 0 15px 0 50px; margin: 0 15px 0 50px;
} }
#delete { #delete {
margin: 0 30px 0 15px; margin: 0 30px 0 15px;
} }
#list { #list {
box-sizing: border-box; box-sizing: border-box;
position: absolute; position: absolute;
@ -269,6 +281,7 @@ export default {
justify-content: flex-start; justify-content: flex-start;
background: transparent; background: transparent;
} }
.list-item { .list-item {
box-sizing: border-box; box-sizing: border-box;
margin-top: 10px; margin-top: 10px;
@ -281,11 +294,17 @@ export default {
background: #fffffff5; background: #fffffff5;
box-shadow: 0 -3px 3px 0 #d4d2d2 inset; box-shadow: 0 -3px 3px 0 #d4d2d2 inset;
} }
.list-item:hover { .list-item:hover {
background: #95daff; background: #95daff;
} }
#page { #page {
margin: auto; margin: auto;
margin-top: -80px; margin-top: -80px;
z-index: 10;
/* 添加此行 */
position: relative;
/* 确保 z-index 生效 */
} }
</style> </style>

@ -37,8 +37,6 @@ export default new VueRouter({
name: 'question', name: 'question',
component: Question, // 这是你的组件 component: Question, // 这是你的组件
} }
], ],
redirect: '/home' redirect: '/home'

Loading…
Cancel
Save