教师考核档案管理软件:从零搭建全栈Web应用实战指南

一、技术选型与项目初始化

我们采用前后端分离架构,前端使用Vue 3 + Element Plus,后端使用Spring Boot + MySQL。这个组合能快速开发出稳定易用的管理系统。

1.1 环境准备

安装Node.js 18.17.0和Java 17:

 检查Node.js版本
node -v
检查Java版本
java -version

安装MySQL 8.0并创建数据库:

CREATE DATABASE teacher_assessment;
CREATE USER 'teacher_admin'@'localhost' IDENTIFIED BY 'YourPassword123!';
GRANT ALL PRIVILEGES ON teacher_assessment. TO 'teacher_admin'@'localhost';

1.2 项目结构创建

创建项目根目录并初始化前后端项目:

mkdir teacher-assessment-system
cd teacher-assessment-system
前端项目
vue create frontend --default
cd frontend
npm install element-plus axios
后端项目
cd ..
spring init --name=backend --dependencies=web,mysql,jpa,security backend

二、数据库设计与实现

2.1 核心表结构

在MySQL中执行以下SQL创建表:

教师考核档案管理软件:从零搭建全栈Web应用实战指南

CREATE TABLE teachers (
id INT PRIMARY KEY AUTO_INCREMENT,
teacher_number VARCHAR(20) UNIQUE NOT NULL,
name VARCHAR(50) NOT NULL,
department VARCHAR(100),
position VARCHAR(50),
hire_date DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE assessment_records (
id INT PRIMARY KEY AUTO_INCREMENT,
teacher_id INT NOT NULL,
assessment_year YEAR NOT NULL,
teaching_score DECIMAL(5,2),
research_score DECIMAL(5,2),
service_score DECIMAL(5,2),
total_score DECIMAL(5,2),
assessment_grade VARCHAR(10),
reviewer VARCHAR(50),
review_date DATE,
remarks TEXT,
FOREIGN KEY (teacher_id) REFERENCES teachers(id) ON DELETE CASCADE,
UNIQUE KEY unique_teacher_year (teacher_id, assessment_year)
);
CREATE TABLE assessment_files (
id INT PRIMARY KEY AUTO_INCREMENT,
record_id INT NOT NULL,
file_name VARCHAR(255) NOT NULL,
file_path VARCHAR(500) NOT NULL,
file_type VARCHAR(50),
upload_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (record_id) REFERENCES assessment_records(id) ON DELETE CASCADE
);

2.2 JPA实体类配置

在backend/src/main/java/com/teacher/entity/目录下创建实体类:

// Teacher.java
@Entity
@Table(name = "teachers")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Teacher {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "teacher_number", unique = true, nullable = false)
private String teacherNumber;
@Column(nullable = false)
private String name;
private String department;
private String position;
@Column(name = "hire_date")
private Date hireDate;
@Column(name = "created_at")
private Timestamp createdAt;
@PrePersist
protected void onCreate() {
createdAt = new Timestamp(System.currentTimeMillis());
}
}
// AssessmentRecord.java
@Entity
@Table(name = "assessment_records")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AssessmentRecord {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@ManyToOne
@JoinColumn(name = "teacher_id", nullable = false)
private Teacher teacher;
@Column(name = "assessment_year", nullable = false)
private Integer assessmentYear;
@Column(name = "teaching_score", precision = 5, scale = 2)
private BigDecimal teachingScore;
@Column(name = "research_score", precision = 5, scale = 2)
private BigDecimal researchScore;
@Column(name = "service_score", precision = 5, scale = 2)
private BigDecimal serviceScore;
@Column(name = "total_score", precision = 5, scale = 2)
private BigDecimal totalScore;
@Column(name = "assessment_grade", length = 10)
private String assessmentGrade;
private String reviewer;
@Column(name = "review_date")
private Date reviewDate;
private String remarks;
@PreUpdate
@PrePersist
public void calculateTotalScore() {
if (teachingScore != null && researchScore != null && serviceScore != null) {
totalScore = teachingScore.add(researchScore).add(serviceScore);
}
}
}

三、后端API开发

3.1 配置文件

配置application.properties:

 数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/teacher_assessment?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8
spring.datasource.username=teacher_admin
spring.datasource.password=YourPassword123!
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
JPA配置
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.jpa.properties.hibernate.format_sql=true
文件上传配置
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB
跨域配置(开发环境)
cors.allowed-origins=http://localhost:8080

3.2 控制器实现

创建TeacherController.java:

@RestController
@RequestMapping("/api/teachers")
@CrossOrigin(origins = "${cors.allowed-origins}")
public class TeacherController {
@Autowired
private TeacherRepository teacherRepository;
@GetMapping
public ResponseEntity> getAllTeachers() {
return ResponseEntity.ok(teacherRepository.findAll());
}
@GetMapping("/{id}")
public ResponseEntity getTeacherById(@PathVariable Integer id) {
return teacherRepository.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity createTeacher(@Valid @RequestBody Teacher teacher) {
Teacher savedTeacher = teacherRepository.save(teacher);
return ResponseEntity.status(HttpStatus.CREATED).body(savedTeacher);
}
@PutMapping("/{id}")
public ResponseEntity updateTeacher(@PathVariable Integer id,
@Valid @RequestBody Teacher teacherDetails) {
return teacherRepository.findById(id)
.map(teacher -> {
teacher.setName(teacherDetails.getName());
teacher.setDepartment(teacherDetails.getDepartment());
teacher.setPosition(teacherDetails.getPosition());
teacher.setHireDate(teacherDetails.getHireDate());
Teacher updatedTeacher = teacherRepository.save(teacher);
return ResponseEntity.ok(updatedTeacher);
})
.orElse(ResponseEntity.notFound().build());
}
@GetMapping("/search")
public ResponseEntity> searchTeachers(
@RequestParam(required = false) String name,
@RequestParam(required = false) String department) {
Specification spec = Specification.where(null);
if (name != null && !name.isEmpty()) {
spec = spec.and((root, query, cb) ->
cb.like(root.get("name"), "%" + name + "%"));
}
if (department != null && !department.isEmpty()) {
spec = spec.and((root, query, cb) ->
cb.equal(root.get("department"), department));
}
return ResponseEntity.ok(teacherRepository.findAll(spec));
}
}

3.3 文件上传接口

创建FileController.java处理考核文件上传:

@RestController
@RequestMapping("/api/files")
public class FileController {
@Value("${file.upload-dir}")
private String uploadDir;
@PostMapping("/upload")
public ResponseEntity> uploadFile(
@RequestParam("file") MultipartFile file,
@RequestParam("recordId") Integer recordId) {
try {
// 创建上传目录
Path uploadPath = Paths.get(uploadDir);
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}
// 生成唯一文件名
String originalFilename = file.getOriginalFilename();
String fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
String uniqueFilename = UUID.randomUUID().toString() + fileExtension;
// 保存文件
Path filePath = uploadPath.resolve(uniqueFilename);
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
// 保存文件信息到数据库
AssessmentFile assessmentFile = new AssessmentFile();
assessmentFile.setRecordId(recordId);
assessmentFile.setFileName(originalFilename);
assessmentFile.setFilePath(filePath.toString());
assessmentFile.setFileType(fileExtension.substring(1));
// 保存到数据库的代码...
Map response = new HashMap<>();
response.put("success", true);
response.put("filename", uniqueFilename);
response.put("originalName", originalFilename);
return ResponseEntity.ok(response);
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("success", false, "error", e.getMessage()));
}
}
}

四、前端界面开发

4.1 项目配置

配置vue.config.js:

module.exports = {
devServer: {
port: 8080,
proxy: {
'/api': {
target: 'http://localhost:8081',
changeOrigin: true
}
}
}
}

4.2 教师管理组件

创建TeacherList.vue: