Java全栈项目-企业人力资源管理系统

项目简介

本项目是一个基于Spring Boot + Vue.js的现代化企业人力资源管理系统,采用前后端分离架构,旨在为企业提供全方位的人力资源解决方案。

技术栈

后端技术

  • Spring Boot 2.7.x
  • Spring Security
  • MyBatis-Plus
  • MySQL 8.0
  • Redis
  • JWT认证

前端技术

  • Vue 3
  • Element Plus
  • Axios
  • Vuex
  • Vue Router
  • ECharts

核心功能模块

1. 员工管理

  • 员工信息的CRUD操作
  • 员工档案管理
  • 员工关系图谱
  • 员工考勤管理

2. 招聘管理

  • 招聘需求发布
  • 简历筛选
  • 面试流程管理
  • 入职管理

3. 薪资管理

  • 薪资结构配置
  • 工资单生成
  • 薪资报表统计
  • 社保公积金管理

4. 培训发展

  • 培训计划制定
  • 课程资源管理
  • 培训效果评估
  • 职业发展规划

5. 绩效考核

  • KPI指标管理
  • 绩效评估流程
  • 360度评价
  • 绩效分析报告

系统特点

1. 安全性

  • 基于Spring Security的权限管理
  • 数据访问控制
  • 操作日志记录
  • 敏感数据加密

2. 可扩展性

  • 模块化设计
  • 插件化架构
  • 灵活的配置管理
  • 易于集成第三方服务

3. 用户体验

  • 响应式设计
  • 直观的操作界面
  • 丰富的数据可视化
  • 便捷的批量操作

4. 系统性能

  • Redis缓存优化
  • 数据库读写分离
  • 定时任务调度
  • 大数据量处理能力

项目亮点

1. 智能化功能

  • 智能简历筛选
  • 员工画像分析
  • 预测性人才流失预警
  • 智能排班推荐

2. 流程自动化

  • 审批流程自动化
  • 薪资计算自动化
  • 考勤统计自动化
  • 报表生成自动化

3. 数据分析

  • 人力资源数据大盘
  • 多维度数据分析
  • 决策支持系统
  • 预测性分析

部署方案

1. 开发环境

  • JDK 11+
  • Node.js 16+
  • Maven 3.6+
  • IDE推荐:IntelliJ IDEA

2. 生产环境

  • Docker容器化部署
  • Nginx反向代理
  • Jenkins持续集成
  • 阿里云服务器

项目价值

1. 管理效率提升

  • 减少人工操作
  • 提高数据准确性
  • 加快处理速度
  • 降低管理成本

2. 决策支持

  • 数据可视化
  • 趋势分析
  • 风险预警
  • 科学决策

3. 员工体验

  • 自助服务
  • 移动端访问
  • 便捷的信息查询
  • 透明的流程管理

未来展望

1. AI赋能

  • 智能面试官
  • 员工情绪分析
  • 智能客服
  • 自动化报告生成

2. 生态集成

  • 钉钉/企业微信集成
  • 第三方招聘平台对接
  • 财务系统集成
  • 办公自动化集成

总结

本项目采用主流的Java全栈技术栈,实现了一个功能完善、性能优异的企业级人力资源管理系统。通过现代化的技术架构和智能化的功能设计,为企业提供了一站式的人力资源管理解决方案,有效提升了人力资源管理的效率和质量。

Directory Content Summary

Source Directory: ./hrm-system

Directory Structure

hrm-system/
  README.md
  backend/
    pom.xml
    src/
      main/
        java/
          com/
            example/
              hrm/
                HrmApplication.java
                controller/
                  EmployeeController.java
                  RecruitmentController.java
                entity/
                  Assessment360Relation.java
                  AssessmentPeriod.java
                  AssessmentPlan.java
                  AssessmentResult.java
                  AssessmentScore.java
                  CareerPlan.java
                  CareerProgress.java
                  Course.java
                  Employee.java
                  EmployeeSalaryConfig.java
                  HousingFundConfig.java
                  InsuranceConfig.java
                  InsuranceFundRecord.java
                  Interview.java
                  KpiIndicator.java
                  Onboarding.java
                  RecruitmentDemand.java
                  Resume.java
                  SalarySheet.java
                  SalaryStructure.java
                  TrainingClass.java
                  TrainingEvaluation.java
                  TrainingPlan.java
                mapper/
                  EmployeeMapper.java
                service/
                  EmployeeService.java
                  PerformanceService.java
                  RecruitmentService.java
                  SalaryService.java
                  TrainingService.java
                  impl/
                    EmployeeServiceImpl.java
                    RecruitmentServiceImpl.java
        resources/
          application.yml
    target/
      classes/
        application.yml
        com/
          example/
            hrm/
              controller/
              entity/
              mapper/
              service/
                impl/
      generated-sources/
        annotations/
      generated-test-sources/
        test-annotations/
      test-classes/
  frontend/
    package.json
    src/
      App.vue
      main.js
      router/
        index.js
      views/
        EmployeeList.vue
        KpiIndicatorList.vue
        OrgChart.vue
        TrainingPlanList.vue
        recruitment/
          DemandList.vue
          InterviewList.vue
          OnboardingList.vue
          ResumeList.vue
  sql/
    hrm.sql
    performance.sql
    recruitment.sql
    salary.sql
    training.sql

File Contents

backend\pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.9</version>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>hrm-system</artifactId>
    <version>1.0.0</version>

    <properties>
        <java.version>11</java.version>
        <mybatis-plus.version>3.5.2</mybatis-plus.version>
        <mysql.version>8.0.31</mysql.version>
        <hutool.version>5.8.11</hutool.version>
    </properties>

    <dependencies>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!-- Database -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>${mysql.version}</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>

        <!-- Tools -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>${hutool.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

backend\src\main\java\com\example\hrm\HrmApplication.java

package com.example.hrm;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.example.hrm.mapper")
public class HrmApplication {
    public static void main(String[] args) {
        SpringApplication.run(HrmApplication.class, args);
    }
}

backend\src\main\java\com\example\hrm\controller\EmployeeController.java

package com.example.hrm.controller;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.hrm.entity.Employee;
import com.example.hrm.service.EmployeeService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;

@RestController
@RequestMapping("/api/employees")
@CrossOrigin
public class EmployeeController {
    @Resource
    private EmployeeService employeeService;

    @GetMapping("/{id}")
    public Employee getById(@PathVariable Long id) {
        return employeeService.getById(id);
    }

    @GetMapping
    public Page<Employee> getPage(
            @RequestParam(defaultValue = "1") int pageNum,
            @RequestParam(defaultValue = "10") int pageSize,
            @RequestParam(required = false) String keyword) {
        return employeeService.getPage(pageNum, pageSize, keyword);
    }

    @PostMapping
    public boolean save(@RequestBody Employee employee) {
        return employeeService.save(employee);
    }

    @PutMapping
    public boolean update(@RequestBody Employee employee) {
        return employeeService.update(employee);
    }

    @DeleteMapping("/{id}")
    public boolean remove(@PathVariable Long id) {
        return employeeService.removeById(id);
    }

    @GetMapping("/department/{departmentId}")
    public List<Employee> getDepartmentEmployees(@PathVariable Long departmentId) {
        return employeeService.getDepartmentEmployees(departmentId);
    }

    @PostMapping("/{id}/check-in")
    public void checkIn(@PathVariable Long id) {
        employeeService.checkIn(id);
    }

    @PostMapping("/{id}/check-out")
    public void checkOut(@PathVariable Long id) {
        employeeService.checkOut(id);
    }
}

backend\src\main\java\com\example\hrm\controller\RecruitmentController.java

package com.example.hrm.controller;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.hrm.entity.*;
import com.example.hrm.service.RecruitmentService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.time.LocalDate;

@RestController
@RequestMapping("/api/recruitment")
@CrossOrigin
public class RecruitmentController {
    @Resource
    private RecruitmentService recruitmentService;

    // 招聘需求相关接口
    @GetMapping("/demands")
    public Page<RecruitmentDemand> getDemandPage(
            @RequestParam(defaultValue = "1") int pageNum,
            @RequestParam(defaultValue = "10") int pageSize,
            @RequestParam(required = false) String keyword) {
        return recruitmentService.getDemandPage(pageNum, pageSize, keyword);
    }

    @GetMapping("/demands/{id}")
    public RecruitmentDemand getDemandById(@PathVariable Long id) {
        return recruitmentService.getDemandById(id);
    }

    @PostMapping("/demands")
    public boolean saveDemand(@RequestBody RecruitmentDemand demand) {
        return recruitmentService.saveDemand(demand);
    }

    @PutMapping("/demands")
    public boolean updateDemand(@RequestBody RecruitmentDemand demand) {
        return recruitmentService.updateDemand(demand);
    }

    @DeleteMapping("/demands/{id}")
    public boolean removeDemand(@PathVariable Long id) {
        return recruitmentService.removeDemand(id);
    }

    // 简历相关接口
    @GetMapping("/resumes")
    public Page<Resume> getResumePage(
            @RequestParam(defaultValue = "1") int pageNum,
            @RequestParam(defaultValue = "10") int pageSize,
            @RequestParam(required = false) Long demandId,
            @RequestParam(required = false) Integer status) {
        return recruitmentService.getResumePage(pageNum, pageSize, demandId, status);
    }

    @GetMapping("/resumes/{id}")
    public Resume getResumeById(@PathVariable Long id) {
        return recruitmentService.getResumeById(id);
    }

    @PostMapping("/resumes")
    public boolean saveResume(@RequestBody Resume resume) {
        return recruitmentService.saveResume(resume);
    }

    @PutMapping("/resumes")
    public boolean updateResume(@RequestBody Resume resume) {
        return recruitmentService.updateResume(resume);
    }

    @DeleteMapping("/resumes/{id}")
    public boolean removeResume(@PathVariable Long id) {
        return recruitmentService.removeResume(id);
    }

    @PutMapping("/resumes/{id}/status")
    public boolean updateResumeStatus(
            @PathVariable Long id,
            @RequestParam Integer status) {
        return recruitmentService.updateResumeStatus(id, status);
    }

    // 面试相关接口
    @GetMapping("/interviews")
    public Page<Interview> getInterviewPage(
            @RequestParam(defaultValue = "1") int pageNum,
            @RequestParam(defaultValue = "10") int pageSize,
            @RequestParam(required = false) Long resumeId) {
        return recruitmentService.getInterviewPage(pageNum, pageSize, resumeId);
    }

    @GetMapping("/interviews/{id}")
    public Interview getInterviewById(@PathVariable Long id) {
        return recruitmentService.getInterviewById(id);
    }

    @PostMapping("/interviews")
    public boolean saveInterview(@RequestBody Interview interview) {
        return recruitmentService.saveInterview(interview);
    }

    @PutMapping("/interviews")
    public boolean updateInterview(@RequestBody Interview interview) {
        return recruitmentService.updateInterview(interview);
    }

    @DeleteMapping("/interviews/{id}")
    public boolean removeInterview(@PathVariable Long id) {
        return recruitmentService.removeInterview(id);
    }

    @PutMapping("/interviews/{id}/result")
    public boolean updateInterviewResult(
            @PathVariable Long id,
            @RequestParam Integer result,
            @RequestParam String evaluation) {
        return recruitmentService.updateInterviewResult(id, result, evaluation);
    }

    // 入职相关接口
    @GetMapping("/onboarding")
    public Page<Onboarding> getOnboardingPage(
            @RequestParam(defaultValue = "1") int pageNum,
            @RequestParam(defaultValue = "10") int pageSize,
            @RequestParam(required = false) Integer status) {
        return recruitmentService.getOnboardingPage(pageNum, pageSize, status);
    }

    @GetMapping("/onboarding/{id}")
    public Onboarding getOnboardingById(@PathVariable Long id) {
        return recruitmentService.getOnboardingById(id);
    }

    @PostMapping("/onboarding")
    public boolean saveOnboarding(@RequestBody Onboarding onboarding) {
        return recruitmentService.saveOnboarding(onboarding);
    }

    @PutMapping("/onboarding")
    public boolean updateOnboarding(@RequestBody Onboarding onboarding) {
        return recruitmentService.updateOnboarding(onboarding);
    }

    @DeleteMapping("/onboarding/{id}")
    public boolean removeOnboarding(@PathVariable Long id) {
        return recruitmentService.removeOnboarding(id);
    }

    @PutMapping("/onboarding/{id}/complete")
    public boolean completeOnboarding(
            @PathVariable Long id,
            @RequestParam LocalDate actualEntryTime) {
        return recruitmentService.completeOnboarding(id, actualEntryTime);
    }
}

backend\src\main\java\com\example\hrm\entity\Assessment360Relation.java

package com.example.hrm.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;

@Data
@TableName("assessment_360_relation")
public class Assessment360Relation {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long planId;
    private Long employeeId;
    private String superiorIds;
    private String peerIds;
    private String subordinateIds;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

backend\src\main\java\com\example\hrm\entity\AssessmentPeriod.java

package com.example.hrm.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;

@Data
@TableName("assessment_period")
public class AssessmentPeriod {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String name;
    private Integer type;
    private LocalDate startDate;
    private LocalDate endDate;
    private Integer status;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

backend\src\main\java\com\example\hrm\entity\AssessmentPlan.java

package com.example.hrm.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;

@Data
@TableName("assessment_plan")
public class AssessmentPlan {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long periodId;
    private String title;
    private Long departmentId;
    private String targetEmployees;
    private String indicatorSettings;
    private LocalDate selfAssessmentStart;
    private LocalDate selfAssessmentEnd;
    private LocalDate managerAssessmentStart;
    private LocalDate managerAssessmentEnd;
    private LocalDate peerAssessmentStart;
    private LocalDate peerAssessmentEnd;
    private Integer status;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

backend\src\main\java\com\example\hrm\entity\AssessmentResult.java

package com.example.hrm.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
@TableName("assessment_result")
public class AssessmentResult {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long planId;
    private Long employeeId;
    private BigDecimal selfScore;
    private BigDecimal superiorScore;
    private BigDecimal peerScore;
    private BigDecimal subordinateScore;
    private BigDecimal finalScore;
    private String rankLevel;
    private String evaluation;
    private String improvementPlan;
    private Integer status;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

backend\src\main\java\com\example\hrm\entity\AssessmentScore.java

package com.example.hrm.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
@TableName("assessment_score")
public class AssessmentScore {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long planId;
    private Long employeeId;
    private Long assessorId;
    private Integer assessmentType;
    private String scores;
    private BigDecimal totalScore;
    private String comments;
    private Integer status;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

backend\src\main\java\com\example\hrm\entity\CareerPlan.java

package com.example.hrm.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;

@Data
@TableName("career_plan")
public class CareerPlan {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long employeeId;
    private String currentPosition;
    private String targetPosition;
    private Integer planPeriod;
    private LocalDate startDate;
    private LocalDate endDate;
    private String skillRequirements;
    private String developmentPath;
    private String trainingNeeds;
    private String milestones;
    private Integer status;
    private Long mentorId;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

backend\src\main\java\com\example\hrm\entity\CareerProgress.java

package com.example.hrm.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;

@Data
@TableName("career_progress")
public class CareerProgress {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long planId;
    private String milestoneId;
    private LocalDate completionDate;
    private Integer completionStatus;
    private String evaluation;
    private String nextSteps;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

backend\src\main\java\com\example\hrm\entity\Course.java

package com.example.hrm.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;

@Data
@TableName("course")
public class Course {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String title;
    private Integer type;
    private Integer category;
    private Integer level;
    private Integer duration;
    private Long lecturerId;
    private Integer maxParticipants;
    private String location;
    private String materials;
    private String objectives;
    private String description;
    private Integer status;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

backend\src\main\java\com\example\hrm\entity\Employee.java

package com.example.hrm.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;

@Data
@TableName("employee")
public class Employee {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String employeeNo;
    private String name;
    private String gender;
    private LocalDate birthDate;
    private String idCard;
    private String phone;
    private String email;
    private String address;
    private Long departmentId;
    private String position;
    private LocalDate entryDate;
    private Integer status;
    
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createdTime;
    
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updatedTime;
}

backend\src\main\java\com\example\hrm\entity\EmployeeSalaryConfig.java

package com.example.hrm.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;

@Data
@TableName("employee_salary_config")
public class EmployeeSalaryConfig {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long employeeId;
    private BigDecimal baseSalary;
    private String structureIds;  // JSON string of structure IDs
    private LocalDate effectiveDate;
    private Integer status;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

backend\src\main\java\com\example\hrm\entity\HousingFundConfig.java

package com.example.hrm.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;

@Data
@TableName("housing_fund_config")
public class HousingFundConfig {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String cityCode;
    private BigDecimal companyRatio;
    private BigDecimal personalRatio;
    private BigDecimal minBase;
    private BigDecimal maxBase;
    private LocalDate effectiveDate;
    private Integer status;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

backend\src\main\java\com\example\hrm\entity\InsuranceConfig.java

package com.example.hrm.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;

@Data
@TableName("insurance_config")
public class InsuranceConfig {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String cityCode;
    private Integer insuranceType;
    private BigDecimal companyRatio;
    private BigDecimal personalRatio;
    private BigDecimal minBase;
    private BigDecimal maxBase;
    private LocalDate effectiveDate;
    private Integer status;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

backend\src\main\java\com\example\hrm\entity\InsuranceFundRecord.java

package com.example.hrm.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
@TableName("insurance_fund_record")
public class InsuranceFundRecord {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long employeeId;
    private Integer year;
    private Integer month;
    private String cityCode;
    private BigDecimal insuranceBase;
    private BigDecimal housingFundBase;
    private BigDecimal pensionCompany;
    private BigDecimal pensionPersonal;
    private BigDecimal medicalCompany;
    private BigDecimal medicalPersonal;
    private BigDecimal unemploymentCompany;
    private BigDecimal unemploymentPersonal;
    private BigDecimal injuryCompany;
    private BigDecimal maternityCompany;
    private BigDecimal housingFundCompany;
    private BigDecimal housingFundPersonal;
    private Integer status;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

backend\src\main\java\com\example\hrm\entity\Interview.java

package com.example.hrm.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;

@Data
@TableName("interview")
public class Interview {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long resumeId;
    private Integer round;
    private LocalDateTime interviewTime;
    private String interviewerIds;
    private Integer interviewType;
    private String location;
    private Integer status;
    private String evaluation;
    private Integer score;
    private Integer result;
    
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createdTime;
    
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updatedTime;
}

backend\src\main\java\com\example\hrm\entity\KpiIndicator.java

package com.example.hrm.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
@TableName("kpi_indicator")
public class KpiIndicator {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String name;
    private Integer category;
    private Long parentId;
    private BigDecimal weight;
    private String unit;
    private String targetValue;
    private String minValue;
    private String maxValue;
    private String scoringCriteria;
    private String description;
    private Integer status;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

backend\src\main\java\com\example\hrm\entity\Onboarding.java

package com.example.hrm.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;

@Data
@TableName("onboarding")
public class Onboarding {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long resumeId;
    private LocalDateTime offerTime;
    private LocalDate plannedEntryTime;
    private LocalDate actualEntryTime;
    private String position;
    private Long departmentId;
    private BigDecimal salary;
    private Integer status;
    private Long mentorId;
    private String checklist;
    private String notes;
    
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createdTime;
    
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updatedTime;
}

backend\src\main\java\com\example\hrm\entity\RecruitmentDemand.java

package com.example.hrm.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;

@Data
@TableName("recruitment_demand")
public class RecruitmentDemand {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String title;
    private Long departmentId;
    private String positionType;
    private Integer headCount;
    private String salaryRange;
    private String experienceRequirement;
    private String educationRequirement;
    private String skillsRequirement;
    private String jobDescription;
    private Integer status;
    private Long publisherId;
    private LocalDateTime publishTime;
    private LocalDateTime deadline;
    
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createdTime;
    
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updatedTime;
}

backend\src\main\java\com\example\hrm\entity\Resume.java

package com.example.hrm.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;

@Data
@TableName("resume")
public class Resume {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long demandId;
    private String name;
    private String gender;
    private LocalDate birthDate;
    private String phone;
    private String email;
    private String education;
    private String school;
    private String major;
    private String workExperience;
    private String skills;
    private String selfEvaluation;
    private String attachmentUrl;
    private Integer status;
    
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createdTime;
    
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updatedTime;
}

backend\src\main\java\com\example\hrm\entity\SalarySheet.java

package com.example.hrm.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
@TableName("salary_sheet")
public class SalarySheet {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long employeeId;
    private Integer year;
    private Integer month;
    private BigDecimal baseSalary;
    private BigDecimal allowances;
    private BigDecimal bonus;
    private BigDecimal insurance;
    private BigDecimal housingFund;
    private BigDecimal tax;
    private BigDecimal others;
    private BigDecimal totalSalary;
    private String details;  // JSON string of salary details
    private Integer status;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

backend\src\main\java\com\example\hrm\entity\SalaryStructure.java

package com.example.hrm.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;

@Data
@TableName("salary_structure")
public class SalaryStructure {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String name;
    private Integer type;
    private Integer calculationType;
    private String calculationValue;
    private Boolean isPlus;
    private Boolean isTaxable;
    private Integer status;
    private String description;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

backend\src\main\java\com\example\hrm\entity\TrainingClass.java

package com.example.hrm.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;

@Data
@TableName("training_class")
public class TrainingClass {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long planId;
    private Long courseId;
    private String className;
    private LocalDateTime startTime;
    private LocalDateTime endTime;
    private String participants;
    private String actualParticipants;
    private Integer status;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

backend\src\main\java\com\example\hrm\entity\TrainingEvaluation.java

package com.example.hrm.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;

@Data
@TableName("training_evaluation")
public class TrainingEvaluation {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long classId;
    private Long employeeId;
    private Integer courseScore;
    private Integer lecturerScore;
    private Integer materialScore;
    private Integer arrangementScore;
    private Integer effectivenessScore;
    private String knowledgeImprovement;
    private String suggestions;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

backend\src\main\java\com\example\hrm\entity\TrainingPlan.java

package com.example.hrm.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;

@Data
@TableName("training_plan")
public class TrainingPlan {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String title;
    private Integer type;
    private Long departmentId;
    private String targetEmployees;
    private LocalDate startDate;
    private LocalDate endDate;
    private BigDecimal budget;
    private String objectives;
    private String description;
    private Integer status;
    private Long creatorId;
    private Long approverId;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

backend\src\main\java\com\example\hrm\mapper\EmployeeMapper.java

package com.example.hrm.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.hrm.entity.Employee;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface EmployeeMapper extends BaseMapper<Employee> {
}

backend\src\main\java\com\example\hrm\service\EmployeeService.java

package com.example.hrm.service;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.hrm.entity.Employee;
import java.util.List;

public interface EmployeeService {
    // 基本CRUD
    Employee getById(Long id);
    Page<Employee> getPage(int pageNum, int pageSize, String keyword);
    boolean save(Employee employee);
    boolean update(Employee employee);
    boolean removeById(Long id);
    
    // 员工关系图谱
    List<Employee> getDepartmentEmployees(Long departmentId);
    
    // 考勤相关
    void checkIn(Long employeeId);
    void checkOut(Long employeeId);
}

backend\src\main\java\com\example\hrm\service\PerformanceService.java

package com.example.hrm.service;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.hrm.entity.*;
import java.util.List;
import java.util.Map;

public interface PerformanceService {
    // KPI指标管理
    Page<KpiIndicator> getKpiIndicators(int pageNum, int pageSize, Integer category);
    List<KpiIndicator> getKpiTree(Integer category);
    boolean saveKpiIndicator(KpiIndicator indicator);
    boolean updateKpiIndicator(KpiIndicator indicator);
    boolean deleteKpiIndicator(Long id);
    
    // 考核周期管理
    Page<AssessmentPeriod> getAssessmentPeriods(int pageNum, int pageSize);
    boolean saveAssessmentPeriod(AssessmentPeriod period);
    boolean updateAssessmentPeriod(AssessmentPeriod period);
    boolean startAssessmentPeriod(Long id);
    boolean endAssessmentPeriod(Long id);
    
    // 考核计划管理
    Page<AssessmentPlan> getAssessmentPlans(int pageNum, int pageSize, Long periodId);
    AssessmentPlan getAssessmentPlan(Long id);
    boolean saveAssessmentPlan(AssessmentPlan plan);
    boolean updateAssessmentPlan(AssessmentPlan plan);
    boolean startAssessmentPlan(Long id);
    boolean endAssessmentPlan(Long id);
    
    // 360度评价关系管理
    Assessment360Relation get360Relation(Long planId, Long employeeId);
    boolean save360Relation(Assessment360Relation relation);
    boolean update360Relation(Assessment360Relation relation);
    
    // 考核评分管理
    Page<AssessmentScore> getAssessmentScores(int pageNum, int pageSize, Long planId, Long employeeId);
    boolean saveAssessmentScore(AssessmentScore score);
    boolean updateAssessmentScore(AssessmentScore score);
    
    // 考核结果管理
    Page<AssessmentResult> getAssessmentResults(int pageNum, int pageSize, Long planId);
    AssessmentResult getEmployeeResult(Long planId, Long employeeId);
    boolean calculateFinalResult(Long planId, Long employeeId);
    boolean confirmResult(Long planId, Long employeeId);
    
    // 统计分析
    Map<String, Object> getPerformanceStatistics(Long periodId);
    Map<String, Object> getDepartmentStatistics(Long periodId, Long departmentId);
    Map<String, Object> getEmployeePerformanceTrend(Long employeeId, Integer year);
}

backend\src\main\java\com\example\hrm\service\RecruitmentService.java

package com.example.hrm.service;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.hrm.entity.RecruitmentDemand;
import com.example.hrm.entity.Resume;
import com.example.hrm.entity.Interview;
import com.example.hrm.entity.Onboarding;

public interface RecruitmentService {
    // 招聘需求管理
    Page<RecruitmentDemand> getDemandPage(int pageNum, int pageSize, String keyword);
    RecruitmentDemand getDemandById(Long id);
    boolean saveDemand(RecruitmentDemand demand);
    boolean updateDemand(RecruitmentDemand demand);
    boolean removeDemand(Long id);

    // 简历管理
    Page<Resume> getResumePage(int pageNum, int pageSize, Long demandId, Integer status);
    Resume getResumeById(Long id);
    boolean saveResume(Resume resume);
    boolean updateResume(Resume resume);
    boolean removeResume(Long id);
    boolean updateResumeStatus(Long id, Integer status);

    // 面试管理
    Page<Interview> getInterviewPage(int pageNum, int pageSize, Long resumeId);
    Interview getInterviewById(Long id);
    boolean saveInterview(Interview interview);
    boolean updateInterview(Interview interview);
    boolean removeInterview(Long id);
    boolean updateInterviewResult(Long id, Integer result, String evaluation);

    // 入职管理
    Page<Onboarding> getOnboardingPage(int pageNum, int pageSize, Integer status);
    Onboarding getOnboardingById(Long id);
    boolean saveOnboarding(Onboarding onboarding);
    boolean updateOnboarding(Onboarding onboarding);
    boolean removeOnboarding(Long id);
    boolean completeOnboarding(Long id, LocalDate actualEntryTime);
}

backend\src\main\java\com\example\hrm\service\SalaryService.java

package com.example.hrm.service;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.hrm.entity.*;
import java.util.List;

public interface SalaryService {
    // 薪资结构配置
    Page<SalaryStructure> getSalaryStructures(int pageNum, int pageSize);
    boolean saveSalaryStructure(SalaryStructure structure);
    boolean updateSalaryStructure(SalaryStructure structure);
    boolean deleteSalaryStructure(Long id);

    // 员工薪资配置
    Page<EmployeeSalaryConfig> getEmployeeSalaryConfigs(int pageNum, int pageSize, Long employeeId);
    boolean saveEmployeeSalaryConfig(EmployeeSalaryConfig config);
    boolean updateEmployeeSalaryConfig(EmployeeSalaryConfig config);

    // 工资单管理
    Page<SalarySheet> getSalarySheets(int pageNum, int pageSize, Integer year, Integer month, Long employeeId);
    boolean generateSalarySheets(Integer year, Integer month);
    boolean updateSalarySheet(SalarySheet sheet);
    boolean confirmSalarySheet(Long id);
    boolean revokeSalarySheet(Long id);

    // 社保配置
    List<InsuranceConfig> getInsuranceConfigs(String cityCode);
    boolean saveInsuranceConfig(InsuranceConfig config);
    boolean updateInsuranceConfig(InsuranceConfig config);

    // 公积金配置
    List<HousingFundConfig> getHousingFundConfigs(String cityCode);
    boolean saveHousingFundConfig(HousingFundConfig config);
    boolean updateHousingFundConfig(HousingFundConfig config);

    // 社保公积金记录
    Page<InsuranceFundRecord> getInsuranceFundRecords(int pageNum, int pageSize, Integer year, Integer month, Long employeeId);
    boolean generateInsuranceFundRecords(Integer year, Integer month);
    boolean updateInsuranceFundRecord(InsuranceFundRecord record);
    boolean confirmInsuranceFundRecord(Long id);

    // 薪资统计报表
    List<SalarySheet> getSalaryStatistics(Integer year, Integer month);
    List<InsuranceFundRecord> getInsuranceFundStatistics(Integer year, Integer month);
}

backend\src\main\java\com\example\hrm\service\TrainingService.java

package com.example.hrm.service;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.hrm.entity.*;
import java.util.List;

public interface TrainingService {
    // 培训计划管理
    Page<TrainingPlan> getTrainingPlans(int pageNum, int pageSize, Integer type, Integer status);
    boolean saveTrainingPlan(TrainingPlan plan);
    boolean updateTrainingPlan(TrainingPlan plan);
    boolean deleteTrainingPlan(Long id);
    boolean approveTrainingPlan(Long id, Long approverId);
    boolean startTrainingPlan(Long id);
    boolean completeTrainingPlan(Long id);
    boolean cancelTrainingPlan(Long id);

    // 课程资源管理
    Page<Course> getCourses(int pageNum, int pageSize, Integer type, Integer category);
    boolean saveCourse(Course course);
    boolean updateCourse(Course course);
    boolean deleteCourse(Long id);
    
    // 培训班次管理
    Page<TrainingClass> getTrainingClasses(int pageNum, int pageSize, Long planId);
    boolean saveTrainingClass(TrainingClass trainingClass);
    boolean updateTrainingClass(TrainingClass trainingClass);
    boolean startTrainingClass(Long id);
    boolean completeTrainingClass(Long id);
    boolean cancelTrainingClass(Long id);
    
    // 培训评估管理
    Page<TrainingEvaluation> getTrainingEvaluations(int pageNum, int pageSize, Long classId);
    boolean saveTrainingEvaluation(TrainingEvaluation evaluation);
    boolean updateTrainingEvaluation(TrainingEvaluation evaluation);
    
    // 职业发展规划
    Page<CareerPlan> getCareerPlans(int pageNum, int pageSize, Long employeeId);
    boolean saveCareerPlan(CareerPlan plan);
    boolean updateCareerPlan(CareerPlan plan);
    boolean startCareerPlan(Long id);
    boolean completeCareerPlan(Long id);
    boolean terminateCareerPlan(Long id);
    
    // 发展进度管理
    List<CareerProgress> getCareerProgresses(Long planId);
    boolean saveCareerProgress(CareerProgress progress);
    boolean updateCareerProgress(CareerProgress progress);
    
    // 统计分析
    Map<String, Object> getTrainingStatistics(Integer year, Integer month);
    Map<String, Object> getCareerDevelopmentStatistics(Integer year);
}

backend\src\main\java\com\example\hrm\service\impl\EmployeeServiceImpl.java

package com.example.hrm.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.hrm.entity.Employee;
import com.example.hrm.mapper.EmployeeMapper;
import com.example.hrm.service.EmployeeService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;

@Service
public class EmployeeServiceImpl implements EmployeeService {
    @Resource
    private EmployeeMapper employeeMapper;

    @Override
    public Employee getById(Long id) {
        return employeeMapper.selectById(id);
    }

    @Override
    public Page<Employee> getPage(int pageNum, int pageSize, String keyword) {
        Page<Employee> page = new Page<>(pageNum, pageSize);
        LambdaQueryWrapper<Employee> wrapper = new LambdaQueryWrapper<>();
        if (keyword != null && !keyword.isEmpty()) {
            wrapper.like(Employee::getName, keyword)
                  .or()
                  .like(Employee::getEmployeeNo, keyword);
        }
        return employeeMapper.selectPage(page, wrapper);
    }

    @Override
    @Transactional
    public boolean save(Employee employee) {
        return employeeMapper.insert(employee) > 0;
    }

    @Override
    @Transactional
    public boolean update(Employee employee) {
        return employeeMapper.updateById(employee) > 0;
    }

    @Override
    @Transactional
    public boolean removeById(Long id) {
        return employeeMapper.deleteById(id) > 0;
    }

    @Override
    public List<Employee> getDepartmentEmployees(Long departmentId) {
        LambdaQueryWrapper<Employee> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Employee::getDepartmentId, departmentId);
        return employeeMapper.selectList(wrapper);
    }

    @Override
    @Transactional
    public void checkIn(Long employeeId) {
        // 实现考勤打卡逻辑
    }

    @Override
    @Transactional
    public void checkOut(Long employeeId) {
        // 实现考勤打卡逻辑
    }
}

backend\src\main\java\com\example\hrm\service\impl\RecruitmentServiceImpl.java

package com.example.hrm.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.hrm.entity.*;
import com.example.hrm.mapper.*;
import com.example.hrm.service.RecruitmentService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDate;
import java.time.LocalDateTime;

@Service
public class RecruitmentServiceImpl implements RecruitmentService {
    @Resource
    private RecruitmentDemandMapper demandMapper;
    @Resource
    private ResumeMapper resumeMapper;
    @Resource
    private InterviewMapper interviewMapper;
    @Resource
    private OnboardingMapper onboardingMapper;

    // 招聘需求管理
    @Override
    public Page<RecruitmentDemand> getDemandPage(int pageNum, int pageSize, String keyword) {
        Page<RecruitmentDemand> page = new Page<>(pageNum, pageSize);
        LambdaQueryWrapper<RecruitmentDemand> wrapper = new LambdaQueryWrapper<>();
        if (keyword != null && !keyword.isEmpty()) {
            wrapper.like(RecruitmentDemand::getTitle, keyword)
                  .or()
                  .like(RecruitmentDemand::getPositionType, keyword);
        }
        wrapper.orderByDesc(RecruitmentDemand::getCreatedTime);
        return demandMapper.selectPage(page, wrapper);
    }

    @Override
    public RecruitmentDemand getDemandById(Long id) {
        return demandMapper.selectById(id);
    }

    @Override
    @Transactional
    public boolean saveDemand(RecruitmentDemand demand) {
        demand.setPublishTime(LocalDateTime.now());
        return demandMapper.insert(demand) > 0;
    }

    @Override
    @Transactional
    public boolean updateDemand(RecruitmentDemand demand) {
        return demandMapper.updateById(demand) > 0;
    }

    @Override
    @Transactional
    public boolean removeDemand(Long id) {
        return demandMapper.deleteById(id) > 0;
    }

    // 简历管理
    @Override
    public Page<Resume> getResumePage(int pageNum, int pageSize, Long demandId, Integer status) {
        Page<Resume> page = new Page<>(pageNum, pageSize);
        LambdaQueryWrapper<Resume> wrapper = new LambdaQueryWrapper<>();
        if (demandId != null) {
            wrapper.eq(Resume::getDemandId, demandId);
        }
        if (status != null) {
            wrapper.eq(Resume::getStatus, status);
        }
        wrapper.orderByDesc(Resume::getCreatedTime);
        return resumeMapper.selectPage(page, wrapper);
    }

    @Override
    public Resume getResumeById(Long id) {
        return resumeMapper.selectById(id);
    }

    @Override
    @Transactional
    public boolean saveResume(Resume resume) {
        return resumeMapper.insert(resume) > 0;
    }

    @Override
    @Transactional
    public boolean updateResume(Resume resume) {
        return resumeMapper.updateById(resume) > 0;
    }

    @Override
    @Transactional
    public boolean removeResume(Long id) {
        return resumeMapper.deleteById(id) > 0;
    }

    @Override
    @Transactional
    public boolean updateResumeStatus(Long id, Integer status) {
        Resume resume = new Resume();
        resume.setId(id);
        resume.setStatus(status);
        return resumeMapper.updateById(resume) > 0;
    }

    // 面试管理
    @Override
    public Page<Interview> getInterviewPage(int pageNum, int pageSize, Long resumeId) {
        Page<Interview> page = new Page<>(pageNum, pageSize);
        LambdaQueryWrapper<Interview> wrapper = new LambdaQueryWrapper<>();
        if (resumeId != null) {
            wrapper.eq(Interview::getResumeId, resumeId);
        }
        wrapper.orderByDesc(Interview::getRound);
        return interviewMapper.selectPage(page, wrapper);
    }

    @Override
    public Interview getInterviewById(Long id) {
        return interviewMapper.selectById(id);
    }

    @Override
    @Transactional
    public boolean saveInterview(Interview interview) {
        return interviewMapper.insert(interview) > 0;
    }

    @Override
    @Transactional
    public boolean updateInterview(Interview interview) {
        return interviewMapper.updateById(interview) > 0;
    }

    @Override
    @Transactional
    public boolean removeInterview(Long id) {
        return interviewMapper.deleteById(id) > 0;
    }

    @Override
    @Transactional
    public boolean updateInterviewResult(Long id, Integer result, String evaluation) {
        Interview interview = new Interview();
        interview.setId(id);
        interview.setResult(result);
        interview.setEvaluation(evaluation);
        interview.setStatus(2); // 已完成
        return interviewMapper.updateById(interview) > 0;
    }

    // 入职管理
    @Override
    public Page<Onboarding> getOnboardingPage(int pageNum, int pageSize, Integer status) {
        Page<Onboarding> page = new Page<>(pageNum, pageSize);
        LambdaQueryWrapper<Onboarding> wrapper = new LambdaQueryWrapper<>();
        if (status != null) {
            wrapper.eq(Onboarding::getStatus, status);
        }
        wrapper.orderByDesc(Onboarding::getCreatedTime);
        return onboardingMapper.selectPage(page, wrapper);
    }

    @Override
    public Onboarding getOnboardingById(Long id) {
        return onboardingMapper.selectById(id);
    }

    @Override
    @Transactional
    public boolean saveOnboarding(Onboarding onboarding) {
        onboarding.setOfferTime(LocalDateTime.now());
        return onboardingMapper.insert(onboarding) > 0;
    }

    @Override
    @Transactional
    public boolean updateOnboarding(Onboarding onboarding) {
        return onboardingMapper.updateById(onboarding) > 0;
    }

    @Override
    @Transactional
    public boolean removeOnboarding(Long id) {
        return onboardingMapper.deleteById(id) > 0;
    }

    @Override
    @Transactional
    public boolean completeOnboarding(Long id, LocalDate actualEntryTime) {
        Onboarding onboarding = new Onboarding();
        onboarding.setId(id);
        onboarding.setActualEntryTime(actualEntryTime);
        onboarding.setStatus(2); // 已入职
        return onboardingMapper.updateById(onboarding) > 0;
    }
}

backend\src\main\resources\application.yml

server:
  port: 8080

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/hrm_system?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root
    password: root
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      logic-delete-field: deleted
      logic-delete-value: 1
      logic-not-delete-value: 0

backend\target\classes\application.yml

server:
  port: 8080

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/hrm_system?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root
    password: root
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      logic-delete-field: deleted
      logic-delete-value: 1
      logic-not-delete-value: 0

frontend\package.json

{
  "name": "hrm-system-frontend",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "vue": "^3.3.4",
    "vue-router": "^4.2.4",
    "pinia": "^2.1.6",
    "axios": "^1.4.0",
    "element-plus": "^2.3.9",
    "echarts": "^5.4.3"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^4.3.1",
    "vite": "^4.4.9"
  }
}

frontend\src\App.vue

<template>
  <el-container>
    <el-aside width="200px">
      <el-menu
        :router="true"
        background-color="#545c64"
        text-color="#fff"
        active-text-color="#ffd04b">
        <el-menu-item index="/employees">
          <el-icon><User /></el-icon>
          <span>员工管理</span>
        </el-menu-item>
        <el-menu-item index="/attendance">
          <el-icon><Calendar /></el-icon>
          <span>考勤管理</span>
        </el-menu-item>
        <el-menu-item index="/org-chart">
          <el-icon><Connection /></el-icon>
          <span>组织架构</span>
        </el-menu-item>
      </el-menu>
    </el-aside>
    <el-container>
      <el-header>
        <h2>企业人力资源管理系统</h2>
      </el-header>
      <el-main>
        <router-view></router-view>
      </el-main>
    </el-container>
  </el-container>
</template>

<script setup>
import { User, Calendar, Connection } from '@element-plus/icons-vue'
</script>

<style>
.el-header {
  background-color: #B3C0D1;
  color: #333;
  line-height: 60px;
}

.el-aside {
  background-color: #545c64;
  color: #333;
  height: 100vh;
}
</style>

frontend\src\main.js

import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia'

const app = createApp(App)
app.use(ElementPlus)
app.use(router)
app.use(createPinia())
app.mount('#app')

frontend\src\router\index.js

import { createRouter, createWebHistory } from 'vue-router'
import EmployeeList from '../views/EmployeeList.vue'
import OrgChart from '../views/OrgChart.vue'

const routes = [
  {
    path: '/',
    redirect: '/employees'
  },
  {
    path: '/employees',
    name: 'employees',
    component: EmployeeList
  },
  {
    path: '/org-chart',
    name: 'org-chart',
    component: OrgChart
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

frontend\src\views\EmployeeList.vue

<template>
  <div class="employee-list">
    <div class="search-bar">
      <el-input
        v-model="searchKeyword"
        placeholder="搜索员工姓名或工号"
        class="search-input"
        @keyup.enter="loadEmployees"
      >
        <template #append>
          <el-button @click="loadEmployees">
            <el-icon><Search /></el-icon>
          </el-button>
        </template>
      </el-input>
      <el-button type="primary" @click="showAddDialog">
        添加员工
      </el-button>
    </div>

    <el-table :data="employees" border style="width: 100%">
      <el-table-column prop="employeeNo" label="工号" width="120" />
      <el-table-column prop="name" label="姓名" width="120" />
      <el-table-column prop="gender" label="性别" width="80">
        <template #default="scope">
          {{ scope.row.gender === 'M' ? '男' : '女' }}
        </template>
      </el-table-column>
      <el-table-column prop="phone" label="手机号" width="150" />
      <el-table-column prop="email" label="邮箱" width="200" />
      <el-table-column prop="position" label="职位" width="150" />
      <el-table-column prop="entryDate" label="入职日期" width="120" />
      <el-table-column prop="status" label="状态" width="100">
        <template #default="scope">
          <el-tag :type="getStatusType(scope.row.status)">
            {{ getStatusText(scope.row.status) }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="200">
        <template #default="scope">
          <el-button size="small" @click="showEditDialog(scope.row)">
            编辑
          </el-button>
          <el-button
            size="small"
            type="danger"
            @click="handleDelete(scope.row)"
          >
            删除
          </el-button>
        </template>
      </el-table-column>
    </el-table>

    <el-pagination
      v-model:current-page="currentPage"
      v-model:page-size="pageSize"
      :total="total"
      :page-sizes="[10, 20, 50, 100]"
      layout="total, sizes, prev, pager, next"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
    />

    <!-- 添加/编辑员工对话框 -->
    <el-dialog
      v-model="dialogVisible"
      :title="isEdit ? '编辑员工' : '添加员工'"
      width="50%"
    >
      <el-form :model="employeeForm" label-width="120px">
        <el-form-item label="工号">
          <el-input v-model="employeeForm.employeeNo" />
        </el-form-item>
        <el-form-item label="姓名">
          <el-input v-model="employeeForm.name" />
        </el-form-item>
        <el-form-item label="性别">
          <el-select v-model="employeeForm.gender">
            <el-option label="男" value="M" />
            <el-option label="女" value="F" />
          </el-select>
        </el-form-item>
        <el-form-item label="手机号">
          <el-input v-model="employeeForm.phone" />
        </el-form-item>
        <el-form-item label="邮箱">
          <el-input v-model="employeeForm.email" />
        </el-form-item>
        <el-form-item label="职位">
          <el-input v-model="employeeForm.position" />
        </el-form-item>
        <el-form-item label="入职日期">
          <el-date-picker
            v-model="employeeForm.entryDate"
            type="date"
            placeholder="选择日期"
          />
        </el-form-item>
        <el-form-item label="状态">
          <el-select v-model="employeeForm.status">
            <el-option label="在职" :value="1" />
            <el-option label="离职" :value="2" />
            <el-option label="试用期" :value="3" />
          </el-select>
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="handleSave">
            确定
          </el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { Search } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import axios from 'axios'

const API_BASE_URL = 'http://localhost:8080/api'

const employees = ref([])
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)
const searchKeyword = ref('')
const dialogVisible = ref(false)
const isEdit = ref(false)
const employeeForm = ref({
  id: null,
  employeeNo: '',
  name: '',
  gender: 'M',
  phone: '',
  email: '',
  position: '',
  entryDate: '',
  status: 1
})

const loadEmployees = async () => {
  try {
    const response = await axios.get(`${API_BASE_URL}/employees`, {
      params: {
        pageNum: currentPage.value,
        pageSize: pageSize.value,
        keyword: searchKeyword.value
      }
    })
    employees.value = response.data.records
    total.value = response.data.total
  } catch (error) {
    ElMessage.error('加载员工列表失败')
  }
}

const showAddDialog = () => {
  isEdit.value = false
  employeeForm.value = {
    id: null,
    employeeNo: '',
    name: '',
    gender: 'M',
    phone: '',
    email: '',
    position: '',
    entryDate: '',
    status: 1
  }
  dialogVisible.value = true
}

const showEditDialog = (row) => {
  isEdit.value = true
  employeeForm.value = { ...row }
  dialogVisible.value = true
}

const handleSave = async () => {
  try {
    if (isEdit.value) {
      await axios.put(`${API_BASE_URL}/employees`, employeeForm.value)
    } else {
      await axios.post(`${API_BASE_URL}/employees`, employeeForm.value)
    }
    ElMessage.success(isEdit.value ? '更新成功' : '添加成功')
    dialogVisible.value = false
    loadEmployees()
  } catch (error) {
    ElMessage.error(isEdit.value ? '更新失败' : '添加失败')
  }
}

const handleDelete = (row) => {
  ElMessageBox.confirm('确定要删除该员工吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(async () => {
    try {
      await axios.delete(`${API_BASE_URL}/employees/${row.id}`)
      ElMessage.success('删除成功')
      loadEmployees()
    } catch (error) {
      ElMessage.error('删除失败')
    }
  })
}

const handleSizeChange = (val) => {
  pageSize.value = val
  loadEmployees()
}

const handleCurrentChange = (val) => {
  currentPage.value = val
  loadEmployees()
}

const getStatusType = (status) => {
  const types = {
    1: 'success',
    2: 'danger',
    3: 'warning'
  }
  return types[status] || 'info'
}

const getStatusText = (status) => {
  const texts = {
    1: '在职',
    2: '离职',
    3: '试用期'
  }
  return texts[status] || '未知'
}

onMounted(() => {
  loadEmployees()
})
</script>

<style scoped>
.employee-list {
  padding: 20px;
}

.search-bar {
  margin-bottom: 20px;
  display: flex;
  gap: 20px;
}

.search-input {
  width: 300px;
}

.dialog-footer {
  padding: 20px 0;
  text-align: right;
}
</style>

frontend\src\views\KpiIndicatorList.vue

<template>
    <div class="kpi-indicator-list">
      <div class="toolbar">
        <el-select v-model="queryParams.category" placeholder="指标类别" clearable>
          <el-option
            v-for="item in categoryOptions"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </el-select>
        <el-button type="primary" @click="showAddDialog">
          新建KPI指标
        </el-button>
      </div>
  
      <el-table :data="indicators" border style="width: 100%">
        <el-table-column prop="name" label="指标名称" />
        <el-table-column prop="category" label="指标类别" width="120">
          <template #default="scope">
            {{ getCategoryText(scope.row.category) }}
          </template>
        </el-table-column>
        <el-table-column prop="weight" label="权重" width="100">
          <template #default="scope">
            {{ scope.row.weight }}%
          </template>
        </el-table-column>
        <el-table-column prop="targetValue" label="目标值" width="120" />
        <el-table-column prop="unit" label="单位" width="100" />
        <el-table-column prop="evaluationCycle" label="考核周期" width="120">
          <template #default="scope">
            {{ getCycleText(scope.row.evaluationCycle) }}
          </template>
        </el-table-column>
        <el-table-column prop="status" label="状态" width="100">
          <template #default="scope">
            <el-tag :type="scope.row.status === 1 ? 'success' : 'info'">
              {{ scope.row.status === 1 ? '启用' : '禁用' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="200">
          <template #default="scope">
            <el-button
              size="small"
              @click="editIndicator(scope.row)"
            >
              编辑
            </el-button>
            <el-button
              size="small"
              type="danger"
              @click="deleteIndicator(scope.row)"
            >
              删除
            </el-button>
          </template>
        </el-table-column>
      </el-table>
  
      <el-pagination
        v-model:current-page="currentPage"
        v-model:page-size="pageSize"
        :total="total"
        :page-sizes="[10, 20, 50, 100]"
        layout="total, sizes, prev, pager, next"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
      />
  
      <!-- 新建/编辑对话框 -->
      <el-dialog
        v-model="dialogVisible"
        :title="isEdit ? '编辑KPI指标' : '新建KPI指标'"
        width="50%"
      >
        <el-form :model="indicatorForm" label-width="120px">
          <el-form-item label="指标名称">
            <el-input v-model="indicatorForm.name" />
          </el-form-item>
          <el-form-item label="指标类别">
            <el-select v-model="indicatorForm.category">
              <el-option
                v-for="item in categoryOptions"
                :key="item.value"
                :label="item.label"
                :value="item.value"
              />
            </el-select>
          </el-form-item>
          <el-form-item label="权重">
            <el-input-number
              v-model="indicatorForm.weight"
              :min="0"
              :max="100"
              :precision="2"
            />
          </el-form-item>
          <el-form-item label="目标值">
            <el-input v-model="indicatorForm.targetValue" />
          </el-form-item>
          <el-form-item label="单位">
            <el-input v-model="indicatorForm.unit" />
          </el-form-item>
          <el-form-item label="考核周期">
            <el-select v-model="indicatorForm.evaluationCycle">
              <el-option
                v-for="item in cycleOptions"
                :key="item.value"
                :label="item.label"
                :value="item.value"
              />
            </el-select>
          </el-form-item>
          <el-form-item label="计算方法">
            <el-input
              v-model="indicatorForm.calculationMethod"
              type="textarea"
              rows="3"
            />
          </el-form-item>
          <el-form-item label="数据来源">
            <el-input v-model="indicatorForm.dataSource" />
          </el-form-item>
          <el-form-item label="指标描述">
            <el-input
              v-model="indicatorForm.description"
              type="textarea"
              rows="3"
            />
          </el-form-item>
          <el-form-item label="状态">
            <el-switch
              v-model="indicatorForm.status"
              :active-value="1"
              :inactive-value="0"
            />
          </el-form-item>
        </el-form>
        <template #footer>
          <span class="dialog-footer">
            <el-button @click="dialogVisible = false">取消</el-button>
            <el-button type="primary" @click="saveIndicator">
              确定
            </el-button>
          </span>
        </template>
      </el-dialog>
    </div>
  </template>
  
  <script setup>
  import { ref, onMounted } from 'vue'
  import { ElMessage, ElMessageBox } from 'element-plus'
  import axios from 'axios'
  
  const API_BASE_URL = 'http://localhost:8080/api'
  
  const indicators = ref([])
  const total = ref(0)
  const currentPage = ref(1)
  const pageSize = ref(10)
  const dialogVisible = ref(false)
  const isEdit = ref(false)
  
  const queryParams = ref({
    category: null
  })
  
  const indicatorForm = ref({
    id: null,
    name: '',
    category: 1,
    weight: 0,
    targetValue: '',
    unit: '',
    evaluationCycle: 1,
    calculationMethod: '',
    dataSource: '',
    description: '',
    status: 1
  })
  
  const categoryOptions = [
    { value: 1, label: '公司级' },
    { value: 2, label: '部门级' },
    { value: 3, label: '岗位级' },
    { value: 4, label: '个人级' }
  ]
  
  const cycleOptions = [
    { value: 1, label: '月度' },
    { value: 2, label: '季度' },
    { value: 3, label: '半年度' },
    { value: 4, label: '年度' }
  ]
  
  const loadIndicators = async () => {
    try {
      const response = await axios.get(`${API_BASE_URL}/performance/kpi-indicators`, {
        params: {
          pageNum: currentPage.value,
          pageSize: pageSize.value,
          category: queryParams.value.category
        }
      })
      indicators.value = response.data.records
      total.value = response.data.total
    } catch (error) {
      ElMessage.error('加载KPI指标失败')
    }
  }
  
  const showAddDialog = () => {
    isEdit.value = false
    indicatorForm.value = {
      id: null,
      name: '',
      category: 1,
      weight: 0,
      targetValue: '',
      unit: '',
      evaluationCycle: 1,
      calculationMethod: '',
      dataSource: '',
      description: '',
      status: 1
    }
    dialogVisible.value = true
  }
  
  const editIndicator = (row) => {
    isEdit.value = true
    indicatorForm.value = { ...row }
    dialogVisible.value = true
  }
  
  const saveIndicator = async () => {
    try {
      if (isEdit.value) {
        await axios.put(`${API_BASE_URL}/performance/kpi-indicators`, indicatorForm.value)
      } else {
        await axios.post(`${API_BASE_URL}/performance/kpi-indicators`, indicatorForm.value)
      }
      ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
      dialogVisible.value = false
      loadIndicators()
    } catch (error) {
      ElMessage.error(isEdit.value ? '更新失败' : '创建失败')
    }
  }
  
  const deleteIndicator = (row) => {
    ElMessageBox.confirm('确定要删除该KPI指标吗?', '提示', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
    }).then(async () => {
      try {
        await axios.delete(`${API_BASE_URL}/performance/kpi-indicators/${row.id}`)
        ElMessage.success('删除成功')
        loadIndicators()
      } catch (error) {
        ElMessage.error('删除失败')
      }
    })
  }
  
  const getCategoryText = (category) => {
    const option = categoryOptions.find(item => item.value === category)
    return option ? option.label : '未知'
  }
  
  const getCycleText = (cycle) => {
    const option = cycleOptions.find(item => item.value === cycle)
    return option ? option.label : '未知'
  }
  
  const handleSizeChange = (val) => {
    pageSize.value = val
    loadIndicators()
  }
  
  const handleCurrentChange = (val) => {
    currentPage.value = val
    loadIndicators()
  }
  
  onMounted(() => {
    loadIndicators()
  })
  </script>
  
  <style scoped>
  .kpi-indicator-list {
    padding: 20px;
  }
  
  .toolbar {
    margin-bottom: 20px;
    display: flex;
    gap: 20px;
  }
  
  .dialog-footer {
    padding: 20px 0;
    text-align: right;
  }
  </style>

frontend\src\views\OrgChart.vue

<template>
  <div class="org-chart">
    <div ref="chartRef" class="chart-container"></div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import * as echarts from 'echarts'
import axios from 'axios'

const API_BASE_URL = 'http://localhost:8080/api'
const chartRef = ref(null)
let chart = null

const initChart = () => {
  chart = echarts.init(chartRef.value)
  loadOrgData()
}

const loadOrgData = async () => {
  try {
    // 获取部门数据
    const deptResponse = await axios.get(`${API_BASE_URL}/departments`)
    const departments = deptResponse.data

    // 获取员工数据
    const empResponse = await axios.get(`${API_BASE_URL}/employees`)
    const employees = empResponse.data.records

    const data = processOrgData(departments, employees)
    renderChart(data)
  } catch (error) {
    console.error('加载组织架构数据失败:', error)
  }
}

const processOrgData = (departments, employees) => {
  // 处理数据,构建组织架构树
  const deptMap = new Map()
  departments.forEach(dept => {
    deptMap.set(dept.id, {
      name: dept.name,
      value: dept.id,
      children: []
    })
  })

  // 构建部门层级关系
  departments.forEach(dept => {
    if (dept.parentId) {
      const parent = deptMap.get(dept.parentId)
      if (parent) {
        parent.children.push(deptMap.get(dept.id))
      }
    }
  })

  // 添加员工节点
  employees.forEach(emp => {
    const dept = deptMap.get(emp.departmentId)
    if (dept) {
      if (!dept.children) {
        dept.children = []
      }
      dept.children.push({
        name: emp.name,
        value: emp.id,
        position: emp.position
      })
    }
  })

  // 返回根节点
  return Array.from(deptMap.values()).filter(dept => !dept.parentId)
}

const renderChart = (data) => {
  const option = {
    tooltip: {
      trigger: 'item',
      triggerOn: 'mousemove'
    },
    series: [
      {
        type: 'tree',
        data: data,
        top: '10%',
        bottom: '10%',
        layout: 'orthogonal',
        symbol: 'emptyCircle',
        symbolSize: 7,
        initialTreeDepth: 3,
        lineStyle: {
          color: '#ccc',
          width: 1
        },
        label: {
          position: 'left',
          verticalAlign: 'middle',
          align: 'right',
          fontSize: 12
        },
        leaves: {
          label: {
            position: 'right',
            verticalAlign: 'middle',
            align: 'left'
          }
        },
        emphasis: {
          focus: 'descendant'
        },
        expandAndCollapse: true,
        animationDuration: 550,
        animationDurationUpdate: 750
      }
    ]
  }

  chart.setOption(option)
}

onMounted(() => {
  initChart()
  window.addEventListener('resize', () => {
    chart && chart.resize()
  })
})
</script>

<style scoped>
.org-chart {
  width: 100%;
  height: 100%;
  min-height: 600px;
}

.chart-container {
  width: 100%;
  height: 100%;
}
</style>

frontend\src\views\TrainingPlanList.vue

<template>
    <div class="training-plan-list">
      <div class="toolbar">
        <el-select v-model="queryParams.type" placeholder="培训类型" clearable>
          <el-option
            v-for="item in typeOptions"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </el-select>
        <el-select v-model="queryParams.status" placeholder="计划状态" clearable>
          <el-option
            v-for="item in statusOptions"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </el-select>
        <el-button type="primary" @click="showAddDialog">
          新建培训计划
        </el-button>
      </div>
  
      <el-table :data="plans" border style="width: 100%">
        <el-table-column prop="title" label="计划标题" />
        <el-table-column prop="type" label="培训类型" width="120">
          <template #default="scope">
            {{ getTypeText(scope.row.type) }}
          </template>
        </el-table-column>
        <el-table-column prop="startDate" label="开始日期" width="120" />
        <el-table-column prop="endDate" label="结束日期" width="120" />
        <el-table-column prop="budget" label="预算金额" width="120">
          <template #default="scope">
            {{ formatMoney(scope.row.budget) }}
          </template>
        </el-table-column>
        <el-table-column prop="status" label="状态" width="100">
          <template #default="scope">
            <el-tag :type="getStatusType(scope.row.status)">
              {{ getStatusText(scope.row.status) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="280">
          <template #default="scope">
            <el-button
              size="small"
              @click="viewDetail(scope.row)"
            >
              查看
            </el-button>
            <el-button
              size="small"
              type="primary"
              :disabled="!canEdit(scope.row)"
              @click="editPlan(scope.row)"
            >
              编辑
            </el-button>
            <el-button
              size="small"
              type="success"
              v-if="scope.row.status === 2"
              @click="approvePlan(scope.row)"
            >
              审批
            </el-button>
            <el-button
              size="small"
              type="danger"
              :disabled="!canCancel(scope.row)"
              @click="cancelPlan(scope.row)"
            >
              取消
            </el-button>
          </template>
        </el-table-column>
      </el-table>
  
      <el-pagination
        v-model:current-page="currentPage"
        v-model:page-size="pageSize"
        :total="total"
        :page-sizes="[10, 20, 50, 100]"
        layout="total, sizes, prev, pager, next"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
      />
  
      <!-- 新建/编辑对话框 -->
      <el-dialog
        v-model="dialogVisible"
        :title="isEdit ? '编辑培训计划' : '新建培训计划'"
        width="60%"
      >
        <el-form :model="planForm" label-width="120px">
          <el-form-item label="计划标题">
            <el-input v-model="planForm.title" />
          </el-form-item>
          <el-form-item label="培训类型">
            <el-select v-model="planForm.type">
              <el-option
                v-for="item in typeOptions"
                :key="item.value"
                :label="item.label"
                :value="item.value"
              />
            </el-select>
          </el-form-item>
          <el-form-item label="培训时间">
            <el-date-picker
              v-model="planForm.dateRange"
              type="daterange"
              range-separator="至"
              start-placeholder="开始日期"
              end-placeholder="结束日期"
            />
          </el-form-item>
          <el-form-item label="预算金额">
            <el-input-number v-model="planForm.budget" :precision="2" :step="1000" />
          </el-form-item>
          <el-form-item label="培训目标">
            <el-input
              v-model="planForm.objectives"
              type="textarea"
              rows="3"
            />
          </el-form-item>
          <el-form-item label="培训描述">
            <el-input
              v-model="planForm.description"
              type="textarea"
              rows="3"
            />
          </el-form-item>
        </el-form>
        <template #footer>
          <span class="dialog-footer">
            <el-button @click="dialogVisible = false">取消</el-button>
            <el-button type="primary" @click="savePlan">
              确定
            </el-button>
          </span>
        </template>
      </el-dialog>
    </div>
  </template>
  
  <script setup>
  import { ref, onMounted } from 'vue'
  import { ElMessage, ElMessageBox } from 'element-plus'
  import axios from 'axios'
  
  const API_BASE_URL = 'http://localhost:8080/api'
  
  const plans = ref([])
  const total = ref(0)
  const currentPage = ref(1)
  const pageSize = ref(10)
  const dialogVisible = ref(false)
  const isEdit = ref(false)
  
  const queryParams = ref({
    type: null,
    status: null
  })
  
  const planForm = ref({
    id: null,
    title: '',
    type: 1,
    dateRange: [],
    budget: 0,
    objectives: '',
    description: ''
  })
  
  const typeOptions = [
    { value: 1, label: '新员工培训' },
    { value: 2, label: '专业技能' },
    { value: 3, label: '管理能力' },
    { value: 4, label: '通用素质' }
  ]
  
  const statusOptions = [
    { value: 1, label: '草稿' },
    { value: 2, label: '待审批' },
    { value: 3, label: '已审批' },
    { value: 4, label: '进行中' },
    { value: 5, label: '已完成' },
    { value: 6, label: '已取消' }
  ]
  
  const loadPlans = async () => {
    try {
      const response = await axios.get(`${API_BASE_URL}/training/plans`, {
        params: {
          pageNum: currentPage.value,
          pageSize: pageSize.value,
          type: queryParams.value.type,
          status: queryParams.value.status
        }
      })
      plans.value = response.data.records
      total.value = response.data.total
    } catch (error) {
      ElMessage.error('加载培训计划失败')
    }
  }
  
  const showAddDialog = () => {
    isEdit.value = false
    planForm.value = {
      id: null,
      title: '',
      type: 1,
      dateRange: [],
      budget: 0,
      objectives: '',
      description: ''
    }
    dialogVisible.value = true
  }
  
  const editPlan = (row) => {
    isEdit.value = true
    planForm.value = {
      id: row.id,
      title: row.title,
      type: row.type,
      dateRange: [row.startDate, row.endDate],
      budget: row.budget,
      objectives: row.objectives,
      description: row.description
    }
    dialogVisible.value = true
  }
  
  const savePlan = async () => {
    try {
      const data = {
        ...planForm.value,
        startDate: planForm.value.dateRange[0],
        endDate: planForm.value.dateRange[1]
      }
      delete data.dateRange
  
      if (isEdit.value) {
        await axios.put(`${API_BASE_URL}/training/plans`, data)
      } else {
        await axios.post(`${API_BASE_URL}/training/plans`, data)
      }
      ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
      dialogVisible.value = false
      loadPlans()
    } catch (error) {
      ElMessage.error(isEdit.value ? '更新失败' : '创建失败')
    }
  }
  
  const approvePlan = (row) => {
    ElMessageBox.confirm('确定要审批通过该培训计划吗?', '提示', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
    }).then(async () => {
      try {
        await axios.post(`${API_BASE_URL}/training/plans/${row.id}/approve`)
        ElMessage.success('审批成功')
        loadPlans()
      } catch (error) {
        ElMessage.error('审批失败')
      }
    })
  }
  
  const cancelPlan = (row) => {
    ElMessageBox.confirm('确定要取消该培训计划吗?', '提示', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
    }).then(async () => {
      try {
        await axios.post(`${API_BASE_URL}/training/plans/${row.id}/cancel`)
        ElMessage.success('取消成功')
        loadPlans()
      } catch (error) {
        ElMessage.error('取消失败')
      }
    })
  }
  
  const getTypeText = (type) => {
    const option = typeOptions.find(item => item.value === type)
    return option ? option.label : '未知'
  }
  
  const getStatusText = (status) => {
    const option = statusOptions.find(item => item.value === status)
    return option ? option.label : '未知'
  }
  
  const getStatusType = (status) => {
    const types = {
      1: 'info',
      2: 'warning',
      3: 'success',
      4: 'primary',
      5: 'success',
      6: 'danger'
    }
    return types[status] || 'info'
  }
  
  const canEdit = (row) => {
    return row.status === 1 || row.status === 2
  }
  
  const canCancel = (row) => {
    return row.status !== 5 && row.status !== 6
  }
  
  const formatMoney = (value) => {
    return value ? `¥${value.toLocaleString()}` : '-'
  }
  
  const handleSizeChange = (val) => {
    pageSize.value = val
    loadPlans()
  }
  
  const handleCurrentChange = (val) => {
    currentPage.value = val
    loadPlans()
  }
  
  onMounted(() => {
    loadPlans()
  })
  </script>
  
  <style scoped>
  .training-plan-list {
    padding: 20px;
  }
  
  .toolbar {
    margin-bottom: 20px;
    display: flex;
    gap: 20px;
  }
  
  .dialog-footer {
    padding: 20px 0;
    text-align: right;
  }
  </style>

frontend\src\views\recruitment\DemandList.vue

<template>
  <div class="demand-list">
    <div class="toolbar">
      <el-input
        v-model="searchKeyword"
        placeholder="搜索职位名称"
        class="search-input"
        @keyup.enter="loadDemands"
      >
        <template #append>
          <el-button @click="loadDemands">
            <el-icon><Search /></el-icon>
          </el-button>
        </template>
      </el-input>
      <el-button type="primary" @click="showAddDialog">
        发布招聘需求
      </el-button>
    </div>

    <el-table :data="demands" border style="width: 100%">
      <el-table-column prop="title" label="职位名称" />
      <el-table-column prop="positionType" label="职位类型" width="120" />
      <el-table-column prop="headCount" label="招聘人数" width="100" />
      <el-table-column prop="salaryRange" label="薪资范围" width="120" />
      <el-table-column prop="status" label="状态" width="100">
        <template #default="scope">
          <el-tag :type="getDemandStatusType(scope.row.status)">
            {{ getDemandStatusText(scope.row.status) }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="publishTime" label="发布时间" width="180" />
      <el-table-column label="操作" width="250">
        <template #default="scope">
          <el-button size="small" @click="viewResumes(scope.row)">
            查看简历
          </el-button>
          <el-button size="small" @click="editDemand(scope.row)">
            编辑
          </el-button>
          <el-button
            size="small"
            type="danger"
            @click="deleteDemand(scope.row)"
          >
            删除
          </el-button>
        </template>
      </el-table-column>
    </el-table>

    <el-pagination
      v-model:current-page="currentPage"
      v-model:page-size="pageSize"
      :total="total"
      :page-sizes="[10, 20, 50, 100]"
      layout="total, sizes, prev, pager, next"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
    />

    <!-- 添加/编辑招聘需求对话框 -->
    <el-dialog
      v-model="dialogVisible"
      :title="isEdit ? '编辑招聘需求' : '发布招聘需求'"
      width="60%"
    >
      <el-form :model="demandForm" label-width="120px">
        <el-form-item label="职位名称">
          <el-input v-model="demandForm.title" />
        </el-form-item>
        <el-form-item label="职位类型">
          <el-input v-model="demandForm.positionType" />
        </el-form-item>
        <el-form-item label="招聘人数">
          <el-input-number v-model="demandForm.headCount" :min="1" />
        </el-form-item>
        <el-form-item label="薪资范围">
          <el-input v-model="demandForm.salaryRange" />
        </el-form-item>
        <el-form-item label="经验要求">
          <el-input v-model="demandForm.experienceRequirement" />
        </el-form-item>
        <el-form-item label="学历要求">
          <el-input v-model="demandForm.educationRequirement" />
        </el-form-item>
        <el-form-item label="技能要求">
          <el-input
            v-model="demandForm.skillsRequirement"
            type="textarea"
            rows="3"
          />
        </el-form-item>
        <el-form-item label="职位描述">
          <el-input
            v-model="demandForm.jobDescription"
            type="textarea"
            rows="5"
          />
        </el-form-item>
        <el-form-item label="截止时间">
          <el-date-picker
            v-model="demandForm.deadline"
            type="datetime"
            placeholder="选择截止时间"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="saveDemand">
            确定
          </el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { Search } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import axios from 'axios'
import { useRouter } from 'vue-router'

const API_BASE_URL = 'http://localhost:8080/api'
const router = useRouter()

const demands = ref([])
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)
const searchKeyword = ref('')
const dialogVisible = ref(false)
const isEdit = ref(false)
const demandForm = ref({
  title: '',
  positionType: '',
  headCount: 1,
  salaryRange: '',
  experienceRequirement: '',
  educationRequirement: '',
  skillsRequirement: '',
  jobDescription: '',
  deadline: null,
  status: 1
})

const loadDemands = async () => {
  try {
    const response = await axios.get(`${API_BASE_URL}/recruitment/demands`, {
      params: {
        pageNum: currentPage.value,
        pageSize: pageSize.value,
        keyword: searchKeyword.value
      }
    })
    demands.value = response.data.records
    total.value = response.data.total
  } catch (error) {
    ElMessage.error('加载招聘需求失败')
  }
}

const showAddDialog = () => {
  isEdit.value = false
  demandForm.value = {
    title: '',
    positionType: '',
    headCount: 1,
    salaryRange: '',
    experienceRequirement: '',
    educationRequirement: '',
    skillsRequirement: '',
    jobDescription: '',
    deadline: null,
    status: 1
  }
  dialogVisible.value = true
}

const editDemand = (row) => {
  isEdit.value = true
  demandForm.value = { ...row }
  dialogVisible.value = true
}

const saveDemand = async () => {
  try {
    if (isEdit.value) {
      await axios.put(`${API_BASE_URL}/recruitment/demands`, demandForm.value)
    } else {
      await axios.post(`${API_BASE_URL}/recruitment/demands`, demandForm.value)
    }
    ElMessage.success(isEdit.value ? '更新成功' : '发布成功')
    dialogVisible.value = false
    loadDemands()
  } catch (error) {
    ElMessage.error(isEdit.value ? '更新失败' : '发布失败')
  }
}

const deleteDemand = (row) => {
  ElMessageBox.confirm('确定要删除该招聘需求吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(async () => {
    try {
      await axios.delete(`${API_BASE_URL}/recruitment/demands/${row.id}`)
      ElMessage.success('删除成功')
      loadDemands()
    } catch (error) {
      ElMessage.error('删除失败')
    }
  })
}

const viewResumes = (row) => {
  router.push(`/recruitment/resumes?demandId=${row.id}`)
}

const handleSizeChange = (val) => {
  pageSize.value = val
  loadDemands()
}

const handleCurrentChange = (val) => {
  currentPage.value = val
  loadDemands()
}

const getDemandStatusType = (status) => {
  const types = {
    1: 'success',
    2: 'warning',
    3: 'info'
  }
  return types[status] || 'info'
}

const getDemandStatusText = (status) => {
  const texts = {
    1: '招聘中',
    2: '已暂停',
    3: '已结束'
  }
  return texts[status] || '未知'
}

onMounted(() => {
  loadDemands()
})
</script>

<style scoped>
.demand-list {
  padding: 20px;
}

.toolbar {
  margin-bottom: 20px;
  display: flex;
  gap: 20px;
}

.search-input {
  width: 300px;
}

.dialog-footer {
  padding: 20px 0;
  text-align: right;
}
</style>

frontend\src\views\recruitment\InterviewList.vue

<template>
  <div class="interview-list">
    <div class="toolbar">
      <el-select v-model="selectedStatus" placeholder="面试状态" clearable @change="loadInterviews">
        <el-option
          v-for="item in statusOptions"
          :key="item.value"
          :label="item.label"
          :value="item.value"
        />
      </el-select>
    </div>

    <el-table :data="interviews" border style="width: 100%">
      <el-table-column label="应聘者">
        <template #default="scope">
          <div>{{ scope.row.resumeName }}</div>
          <div class="text-gray">{{ scope.row.resumePhone }}</div>
        </template>
      </el-table-column>
      <el-table-column prop="round" label="面试轮次" width="100">
        <template #default="scope">
          第{{ scope.row.round }}轮
        </template>
      </el-table-column>
      <el-table-column prop="interviewTime" label="面试时间" width="180" />
      <el-table-column prop="interviewType" label="面试方式" width="120">
        <template #default="scope">
          {{ getInterviewTypeText(scope.row.interviewType) }}
        </template>
      </el-table-column>
      <el-table-column prop="location" label="面试地点" width="200" />
      <el-table-column prop="status" label="状态" width="100">
        <template #default="scope">
          <el-tag :type="getInterviewStatusType(scope.row.status)">
            {{ getInterviewStatusText(scope.row.status) }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="250">
        <template #default="scope">
          <el-button
            size="small"
            :disabled="scope.row.status !== 1"
            @click="showEvaluationDialog(scope.row)"
          >
            评价
          </el-button>
          <el-button
            size="small"
            type="danger"
            :disabled="scope.row.status !== 1"
            @click="cancelInterview(scope.row)"
          >
            取消
          </el-button>
        </template>
      </el-table-column>
    </el-table>

    <el-pagination
      v-model:current-page="currentPage"
      v-model:page-size="pageSize"
      :total="total"
      :page-sizes="[10, 20, 50, 100]"
      layout="total, sizes, prev, pager, next"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
    />

    <!-- 面试评价对话框 -->
    <el-dialog
      v-model="evaluationDialogVisible"
      title="面试评价"
      width="50%"
    >
      <el-form :model="evaluationForm" label-width="120px">
        <el-form-item label="面试评分">
          <el-rate
            v-model="evaluationForm.score"
            :max="5"
            :texts="['不合格', '待提高', '一般', '良好', '优秀']"
            show-text
          />
        </el-form-item>
        <el-form-item label="面试评价">
          <el-input
            v-model="evaluationForm.evaluation"
            type="textarea"
            rows="4"
            placeholder="请输入面试评价内容"
          />
        </el-form-item>
        <el-form-item label="面试结果">
          <el-select v-model="evaluationForm.result">
            <el-option label="通过" :value="1" />
            <el-option label="待定" :value="2" />
            <el-option label="不通过" :value="3" />
          </el-select>
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="evaluationDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="submitEvaluation">
            提交
          </el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import axios from 'axios'

const API_BASE_URL = 'http://localhost:8080/api'

const interviews = ref([])
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)
const selectedStatus = ref(null)
const evaluationDialogVisible = ref(false)

const statusOptions = [
  { value: 1, label: '待面试' },
  { value: 2, label: '已完成' },
  { value: 3, label: '已取消' }
]

const evaluationForm = ref({
  id: null,
  score: 3,
  evaluation: '',
  result: 1
})

const loadInterviews = async () => {
  try {
    const response = await axios.get(`${API_BASE_URL}/recruitment/interviews`, {
      params: {
        pageNum: currentPage.value,
        pageSize: pageSize.value,
        status: selectedStatus.value
      }
    })
    interviews.value = response.data.records
    total.value = response.data.total
  } catch (error) {
    ElMessage.error('加载面试列表失败')
  }
}

const showEvaluationDialog = (row) => {
  evaluationForm.value = {
    id: row.id,
    score: 3,
    evaluation: '',
    result: 1
  }
  evaluationDialogVisible.value = true
}

const submitEvaluation = async () => {
  try {
    await axios.put(`${API_BASE_URL}/recruitment/interviews/${evaluationForm.value.id}/result`, {
      result: evaluationForm.value.result,
      evaluation: evaluationForm.value.evaluation,
      score: evaluationForm.value.score
    })
    ElMessage.success('评价提交成功')
    evaluationDialogVisible.value = false
    loadInterviews()
  } catch (error) {
    ElMessage.error('评价提交失败')
  }
}

const cancelInterview = (row) => {
  ElMessageBox.confirm('确定要取消该面试吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(async () => {
    try {
      const interview = { ...row, status: 3 }
      await axios.put(`${API_BASE_URL}/recruitment/interviews`, interview)
      ElMessage.success('面试已取消')
      loadInterviews()
    } catch (error) {
      ElMessage.error('操作失败')
    }
  })
}

const getInterviewTypeText = (type) => {
  const types = {
    1: '现场面试',
    2: '视频面试',
    3: '电话面试'
  }
  return types[type] || '未知'
}

const getInterviewStatusType = (status) => {
  const types = {
    1: 'warning',
    2: 'success',
    3: 'info'
  }
  return types[status] || 'info'
}

const getInterviewStatusText = (status) => {
  const texts = {
    1: '待面试',
    2: '已完成',
    3: '已取消'
  }
  return texts[status] || '未知'
}

const handleSizeChange = (val) => {
  pageSize.value = val
  loadInterviews()
}

const handleCurrentChange = (val) => {
  currentPage.value = val
  loadInterviews()
}

onMounted(() => {
  loadInterviews()
})
</script>

<style scoped>
.interview-list {
  padding: 20px;
}

.toolbar {
  margin-bottom: 20px;
  display: flex;
  gap: 20px;
}

.text-gray {
  color: #909399;
  font-size: 12px;
}

.dialog-footer {
  padding: 20px 0;
  text-align: right;
}
</style>

frontend\src\views\recruitment\OnboardingList.vue

<template>
  <div class="onboarding-list">
    <div class="toolbar">
      <el-select v-model="selectedStatus" placeholder="入职状态" clearable @change="loadOnboardings">
        <el-option
          v-for="item in statusOptions"
          :key="item.value"
          :label="item.label"
          :value="item.value"
        />
      </el-select>
    </div>

    <el-table :data="onboardings" border style="width: 100%">
      <el-table-column label="应聘者">
        <template #default="scope">
          <div>{{ scope.row.resumeName }}</div>
          <div class="text-gray">{{ scope.row.resumePhone }}</div>
        </template>
      </el-table-column>
      <el-table-column prop="position" label="入职岗位" width="150" />
      <el-table-column prop="departmentName" label="入职部门" width="150" />
      <el-table-column prop="salary" label="薪资" width="120">
        <template #default="scope">
          {{ formatSalary(scope.row.salary) }}
        </template>
      </el-table-column>
      <el-table-column prop="offerTime" label="Offer时间" width="180" />
      <el-table-column prop="plannedEntryTime" label="预计入职时间" width="120" />
      <el-table-column prop="actualEntryTime" label="实际入职时间" width="120" />
      <el-table-column prop="status" label="状态" width="100">
        <template #default="scope">
          <el-tag :type="getOnboardingStatusType(scope.row.status)">
            {{ getOnboardingStatusText(scope.row.status) }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="250">
        <template #default="scope">
          <el-button
            size="small"
            :disabled="scope.row.status !== 1"
            @click="completeOnboarding(scope.row)"
          >
            确认入职
          </el-button>
          <el-button
            size="small"
            type="danger"
            :disabled="scope.row.status !== 1"
            @click="cancelOnboarding(scope.row)"
          >
            取消入职
          </el-button>
        </template>
      </el-table-column>
    </el-table>

    <el-pagination
      v-model:current-page="currentPage"
      v-model:page-size="pageSize"
      :total="total"
      :page-sizes="[10, 20, 50, 100]"
      layout="total, sizes, prev, pager, next"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
    />

    <!-- 确认入职对话框 -->
    <el-dialog
      v-model="confirmDialogVisible"
      title="确认入职"
      width="40%"
    >
      <el-form :model="confirmForm" label-width="120px">
        <el-form-item label="实际入职时间">
          <el-date-picker
            v-model="confirmForm.actualEntryTime"
            type="date"
            placeholder="选择实际入职时间"
          />
        </el-form-item>
        <el-form-item label="入职备注">
          <el-input
            v-model="confirmForm.notes"
            type="textarea"
            rows="3"
            placeholder="请输入入职备注信息"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="confirmDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="submitConfirmation">
            确定
          </el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import axios from 'axios'

const API_BASE_URL = 'http://localhost:8080/api'

const onboardings = ref([])
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)
const selectedStatus = ref(null)
const confirmDialogVisible = ref(false)

const statusOptions = [
  { value: 1, label: '待入职' },
  { value: 2, label: '已入职' },
  { value: 3, label: '已取消' }
]

const confirmForm = ref({
  id: null,
  actualEntryTime: null,
  notes: ''
})

const loadOnboardings = async () => {
  try {
    const response = await axios.get(`${API_BASE_URL}/recruitment/onboarding`, {
      params: {
        pageNum: currentPage.value,
        pageSize: pageSize.value,
        status: selectedStatus.value
      }
    })
    onboardings.value = response.data.records
    total.value = response.data.total
  } catch (error) {
    ElMessage.error('加载入职列表失败')
  }
}

const completeOnboarding = (row) => {
  confirmForm.value = {
    id: row.id,
    actualEntryTime: null,
    notes: ''
  }
  confirmDialogVisible.value = true
}

const submitConfirmation = async () => {
  if (!confirmForm.value.actualEntryTime) {
    ElMessage.warning('请选择实际入职时间')
    return
  }

  try {
    await axios.put(`${API_BASE_URL}/recruitment/onboarding/${confirmForm.value.id}/complete`, {
      actualEntryTime: confirmForm.value.actualEntryTime,
      notes: confirmForm.value.notes
    })
    ElMessage.success('确认入职成功')
    confirmDialogVisible.value = false
    loadOnboardings()
  } catch (error) {
    ElMessage.error('确认入职失败')
  }
}

const cancelOnboarding = (row) => {
  ElMessageBox.confirm('确定要取消该入职流程吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(async () => {
    try {
      const onboarding = { ...row, status: 3 }
      await axios.put(`${API_BASE_URL}/recruitment/onboarding`, onboarding)
      ElMessage.success('入职已取消')
      loadOnboardings()
    } catch (error) {
      ElMessage.error('操作失败')
    }
  })
}

const formatSalary = (salary) => {
  return salary ? `¥${salary.toLocaleString()}` : '-'
}

const getOnboardingStatusType = (status) => {
  const types = {
    1: 'warning',
    2: 'success',
    3: 'info'
  }
  return types[status] || 'info'
}

const getOnboardingStatusText = (status) => {
  const texts = {
    1: '待入职',
    2: '已入职',
    3: '已取消'
  }
  return texts[status] || '未知'
}

const handleSizeChange = (val) => {
  pageSize.value = val
  loadOnboardings()
}

const handleCurrentChange = (val) => {
  currentPage.value = val
  loadOnboardings()
}

onMounted(() => {
  loadOnboardings()
})
</script>

<style scoped>
.onboarding-list {
  padding: 20px;
}

.toolbar {
  margin-bottom: 20px;
  display: flex;
  gap: 20px;
}

.text-gray {
  color: #909399;
  font-size: 12px;
}

.dialog-footer {
  padding: 20px 0;
  text-align: right;
}
</style>

frontend\src\views\recruitment\ResumeList.vue

<template>
  <div class="resume-list">
    <div class="toolbar">
      <el-select v-model="selectedStatus" placeholder="简历状态" clearable @change="loadResumes">
        <el-option
          v-for="item in statusOptions"
          :key="item.value"
          :label="item.label"
          :value="item.value"
        />
      </el-select>
      <el-button type="primary" @click="showAddDialog">
        添加简历
      </el-button>
    </div>

    <el-table :data="resumes" border style="width: 100%">
      <el-table-column prop="name" label="姓名" width="120" />
      <el-table-column prop="gender" label="性别" width="80">
        <template #default="scope">
          {{ scope.row.gender === 'M' ? '男' : '女' }}
        </template>
      </el-table-column>
      <el-table-column prop="phone" label="手机号" width="150" />
      <el-table-column prop="email" label="邮箱" width="200" />
      <el-table-column prop="education" label="学历" width="120" />
      <el-table-column prop="school" label="毕业院校" width="200" />
      <el-table-column prop="status" label="状态" width="120">
        <template #default="scope">
          <el-tag :type="getResumeStatusType(scope.row.status)">
            {{ getResumeStatusText(scope.row.status) }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="380">
        <template #default="scope">
          <el-button size="small" @click="viewDetail(scope.row)">
            查看详情
          </el-button>
          <el-button size="small" @click="arrangeInterview(scope.row)">
            安排面试
          </el-button>
          <el-button size="small" type="success" @click="createOnboarding(scope.row)">
            发送Offer
          </el-button>
          <el-button size="small" type="danger" @click="rejectResume(scope.row)">
            拒绝
          </el-button>
        </template>
      </el-table-column>
    </el-table>

    <el-pagination
      v-model:current-page="currentPage"
      v-model:page-size="pageSize"
      :total="total"
      :page-sizes="[10, 20, 50, 100]"
      layout="total, sizes, prev, pager, next"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
    />

    <!-- 添加/编辑简历对话框 -->
    <el-dialog
      v-model="dialogVisible"
      :title="isEdit ? '编辑简历' : '添加简历'"
      width="60%"
    >
      <el-form :model="resumeForm" label-width="120px">
        <el-form-item label="姓名">
          <el-input v-model="resumeForm.name" />
        </el-form-item>
        <el-form-item label="性别">
          <el-select v-model="resumeForm.gender">
            <el-option label="男" value="M" />
            <el-option label="女" value="F" />
          </el-select>
        </el-form-item>
        <el-form-item label="出生日期">
          <el-date-picker v-model="resumeForm.birthDate" type="date" />
        </el-form-item>
        <el-form-item label="手机号">
          <el-input v-model="resumeForm.phone" />
        </el-form-item>
        <el-form-item label="邮箱">
          <el-input v-model="resumeForm.email" />
        </el-form-item>
        <el-form-item label="最高学历">
          <el-select v-model="resumeForm.education">
            <el-option label="高中" value="高中" />
            <el-option label="专科" value="专科" />
            <el-option label="本科" value="本科" />
            <el-option label="硕士" value="硕士" />
            <el-option label="博士" value="博士" />
          </el-select>
        </el-form-item>
        <el-form-item label="毕业院校">
          <el-input v-model="resumeForm.school" />
        </el-form-item>
        <el-form-item label="专业">
          <el-input v-model="resumeForm.major" />
        </el-form-item>
        <el-form-item label="工作经验">
          <el-input
            v-model="resumeForm.workExperience"
            type="textarea"
            rows="3"
          />
        </el-form-item>
        <el-form-item label="技能特长">
          <el-input
            v-model="resumeForm.skills"
            type="textarea"
            rows="3"
          />
        </el-form-item>
        <el-form-item label="自我评价">
          <el-input
            v-model="resumeForm.selfEvaluation"
            type="textarea"
            rows="3"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="saveResume">
            确定
          </el-button>
        </span>
      </template>
    </el-dialog>

    <!-- 安排面试对话框 -->
    <el-dialog
      v-model="interviewDialogVisible"
      title="安排面试"
      width="50%"
    >
      <el-form :model="interviewForm" label-width="120px">
        <el-form-item label="面试轮次">
          <el-input-number v-model="interviewForm.round" :min="1" />
        </el-form-item>
        <el-form-item label="面试时间">
          <el-date-picker
            v-model="interviewForm.interviewTime"
            type="datetime"
            placeholder="选择面试时间"
          />
        </el-form-item>
        <el-form-item label="面试方式">
          <el-select v-model="interviewForm.interviewType">
            <el-option label="现场面试" :value="1" />
            <el-option label="视频面试" :value="2" />
            <el-option label="电话面试" :value="3" />
          </el-select>
        </el-form-item>
        <el-form-item label="面试地点">
          <el-input v-model="interviewForm.location" />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="interviewDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="saveInterview">
            确定
          </el-button>
        </span>
      </template>
    </el-dialog>

    <!-- 发送Offer对话框 -->
    <el-dialog
      v-model="offerDialogVisible"
      title="发送Offer"
      width="50%"
    >
      <el-form :model="offerForm" label-width="120px">
        <el-form-item label="入职岗位">
          <el-input v-model="offerForm.position" />
        </el-form-item>
        <el-form-item label="入职部门">
          <el-select v-model="offerForm.departmentId">
            <el-option
              v-for="dept in departments"
              :key="dept.id"
              :label="dept.name"
              :value="dept.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="薪资">
          <el-input-number v-model="offerForm.salary" :precision="2" :step="1000" />
        </el-form-item>
        <el-form-item label="预计入职时间">
          <el-date-picker
            v-model="offerForm.plannedEntryTime"
            type="date"
            placeholder="选择预计入职时间"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="offerDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="saveOffer">
            确定
          </el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import axios from 'axios'
import { useRoute } from 'vue-router'

const API_BASE_URL = 'http://localhost:8080/api'
const route = useRoute()

const resumes = ref([])
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)
const selectedStatus = ref(null)
const dialogVisible = ref(false)
const interviewDialogVisible = ref(false)
const offerDialogVisible = ref(false)
const isEdit = ref(false)
const departments = ref([])

const statusOptions = [
  { value: 1, label: '待筛选' },
  { value: 2, label: '初筛通过' },
  { value: 3, label: '初筛未通过' },
  { value: 4, label: '面试中' },
  { value: 5, label: '已录用' },
  { value: 6, label: '已入职' },
  { value: 7, label: '已拒绝' }
]

const resumeForm = ref({
  name: '',
  gender: 'M',
  birthDate: null,
  phone: '',
  email: '',
  education: '',
  school: '',
  major: '',
  workExperience: '',
  skills: '',
  selfEvaluation: '',
  demandId: route.query.demandId
})

const interviewForm = ref({
  resumeId: null,
  round: 1,
  interviewTime: null,
  interviewType: 1,
  location: ''
})

const offerForm = ref({
  resumeId: null,
  position: '',
  departmentId: null,
  salary: 0,
  plannedEntryTime: null
})

const loadResumes = async () => {
  try {
    const response = await axios.get(`${API_BASE_URL}/recruitment/resumes`, {
      params: {
        pageNum: currentPage.value,
        pageSize: pageSize.value,
        demandId: route.query.demandId,
        status: selectedStatus.value
      }
    })
    resumes.value = response.data.records
    total.value = response.data.total
  } catch (error) {
    ElMessage.error('加载简历列表失败')
  }
}

const loadDepartments = async () => {
  try {
    const response = await axios.get(`${API_BASE_URL}/departments`)
    departments.value = response.data
  } catch (error) {
    ElMessage.error('加载部门列表失败')
  }
}

const showAddDialog = () => {
  isEdit.value = false
  resumeForm.value = {
    name: '',
    gender: 'M',
    birthDate: null,
    phone: '',
    email: '',
    education: '',
    school: '',
    major: '',
    workExperience: '',
    skills: '',
    selfEvaluation: '',
    demandId: route.query.demandId
  }
  dialogVisible.value = true
}

const saveResume = async () => {
  try {
    if (isEdit.value) {
      await axios.put(`${API_BASE_URL}/recruitment/resumes`, resumeForm.value)
    } else {
      await axios.post(`${API_BASE_URL}/recruitment/resumes`, resumeForm.value)
    }
    ElMessage.success(isEdit.value ? '更新成功' : '添加成功')
    dialogVisible.value = false
    loadResumes()
  } catch (error) {
    ElMessage.error(isEdit.value ? '更新失败' : '添加失败')
  }
}

const arrangeInterview = (row) => {
  interviewForm.value = {
    resumeId: row.id,
    round: 1,
    interviewTime: null,
    interviewType: 1,
    location: ''
  }
  interviewDialogVisible.value = true
}

const saveInterview = async () => {
  try {
    await axios.post(`${API_BASE_URL}/recruitment/interviews`, interviewForm.value)
    await axios.put(`${API_BASE_URL}/recruitment/resumes/${interviewForm.value.resumeId}/status`, { status: 4 })
    ElMessage.success('面试安排成功')
    interviewDialogVisible.value = false
    loadResumes()
  } catch (error) {
    ElMessage.error('面试安排失败')
  }
}

const createOnboarding = (row) => {
  offerForm.value = {
    resumeId: row.id,
    position: '',
    departmentId: null,
    salary: 0,
    plannedEntryTime: null
  }
  offerDialogVisible.value = true
}

const saveOffer = async () => {
  try {
    await axios.post(`${API_BASE_URL}/recruitment/onboarding`, offerForm.value)
    await axios.put(`${API_BASE_URL}/recruitment/resumes/${offerForm.value.resumeId}/status`, { status: 5 })
    ElMessage.success('Offer发送成功')
    offerDialogVisible.value = false
    loadResumes()
  } catch (error) {
    ElMessage.error('Offer发送失败')
  }
}

const rejectResume = (row) => {
  ElMessageBox.confirm('确定要拒绝该简历吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(async () => {
    try {
      await axios.put(`${API_BASE_URL}/recruitment/resumes/${row.id}/status`, { status: 7 })
      ElMessage.success('操作成功')
      loadResumes()
    } catch (error) {
      ElMessage.error('操作失败')
    }
  })
}

const getResumeStatusType = (status) => {
  const types = {
    1: 'info',
    2: 'success',
    3: 'danger',
    4: 'warning',
    5: 'success',
    6: 'success',
    7: 'danger'
  }
  return types[status] || 'info'
}

const getResumeStatusText = (status) => {
  const texts = {
    1: '待筛选',
    2: '初筛通过',
    3: '初筛未通过',
    4: '面试中',
    5: '已录用',
    6: '已入职',
    7: '已拒绝'
  }
  return texts[status] || '未知'
}

const handleSizeChange = (val) => {
  pageSize.value = val
  loadResumes()
}

const handleCurrentChange = (val) => {
  currentPage.value = val
  loadResumes()
}

onMounted(() => {
  loadResumes()
  loadDepartments()
})
</script>

<style scoped>
.resume-list {
  padding: 20px;
}

.toolbar {
  margin-bottom: 20px;
  display: flex;
  gap: 20px;
}

.dialog-footer {
  padding: 20px 0;
  text-align: right;
}
</style>

sql\hrm.sql

-- 创建数据库
CREATE DATABASE IF NOT EXISTS hrm_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

USE hrm_system;

-- 员工基本信息表
CREATE TABLE employee (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    employee_no VARCHAR(50) NOT NULL COMMENT '员工编号',
    name VARCHAR(50) NOT NULL COMMENT '姓名',
    gender CHAR(1) COMMENT '性别:M-男,F-女',
    birth_date DATE COMMENT '出生日期',
    id_card VARCHAR(18) COMMENT '身份证号',
    phone VARCHAR(20) COMMENT '手机号',
    email VARCHAR(50) COMMENT '邮箱',
    address VARCHAR(200) COMMENT '住址',
    department_id BIGINT COMMENT '部门ID',
    position VARCHAR(50) COMMENT '职位',
    entry_date DATE COMMENT '入职日期',
    status TINYINT DEFAULT 1 COMMENT '状态:1-在职,2-离职,3-试用期',
    created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_employee_no (employee_no),
    UNIQUE KEY uk_id_card (id_card)
) COMMENT '员工基本信息表';

-- 员工档案表
CREATE TABLE employee_archive (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    employee_id BIGINT NOT NULL COMMENT '员工ID',
    education VARCHAR(50) COMMENT '学历',
    school VARCHAR(100) COMMENT '毕业院校',
    major VARCHAR(100) COMMENT '专业',
    graduation_date DATE COMMENT '毕业时间',
    work_experience TEXT COMMENT '工作经历',
    skills TEXT COMMENT '技能特长',
    certificates TEXT COMMENT '证书',
    emergency_contact VARCHAR(50) COMMENT '紧急联系人',
    emergency_phone VARCHAR(20) COMMENT '紧急联系人电话',
    created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (employee_id) REFERENCES employee(id)
) COMMENT '员工档案表';

-- 部门表
CREATE TABLE department (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL COMMENT '部门名称',
    parent_id BIGINT COMMENT '父部门ID',
    level INT COMMENT '层级',
    sort INT COMMENT '排序号',
    leader_id BIGINT COMMENT '部门负责人ID',
    created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) COMMENT '部门表';

-- 考勤记录表
CREATE TABLE attendance (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    employee_id BIGINT NOT NULL COMMENT '员工ID',
    date DATE NOT NULL COMMENT '考勤日期',
    check_in DATETIME COMMENT '上班打卡时间',
    check_out DATETIME COMMENT '下班打卡时间',
    status TINYINT COMMENT '考勤状态:1-正常,2-迟到,3-早退,4-旷工,5-请假',
    remark VARCHAR(200) COMMENT '备注',
    created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (employee_id) REFERENCES employee(id)
) COMMENT '考勤记录表';

-- 插入测试数据
INSERT INTO department (id, name, parent_id, level, sort) VALUES
(1, '总裁办', NULL, 1, 1),
(2, '技术部', NULL, 1, 2),
(3, '人力资源部', NULL, 1, 3),
(4, '后端开发组', 2, 2, 1),
(5, '前端开发组', 2, 2, 2);

-- 插入测试员工数据
INSERT INTO employee (employee_no, name, gender, birth_date, department_id, position, entry_date) VALUES
('EMP001', '张三', 'M', '1990-01-01', 1, '总经理', '2020-01-01'),
('EMP002', '李四', 'M', '1991-02-02', 2, '技术总监', '2020-02-01'),
('EMP003', '王五', 'F', '1992-03-03', 3, 'HR经理', '2020-03-01');

-- 插入测试档案数据
INSERT INTO employee_archive (employee_id, education, school, major, graduation_date) VALUES
(1, '本科', '北京大学', '计算机科学', '2012-07-01'),
(2, '硕士', '清华大学', '软件工程', '2014-07-01'),
(3, '本科', '复旦大学', '人力资源管理', '2015-07-01');

sql\performance.sql

-- KPI指标表
CREATE TABLE kpi_indicator (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL COMMENT '指标名称',
    category INT NOT NULL COMMENT '类别:1-公司级,2-部门级,3-岗位级,4-个人级',
    parent_id BIGINT COMMENT '父指标ID',
    weight DECIMAL(5,2) NOT NULL COMMENT '权重',
    unit VARCHAR(20) COMMENT '计量单位',
    target_value VARCHAR(50) COMMENT '目标值',
    min_value VARCHAR(50) COMMENT '最小值',
    max_value VARCHAR(50) COMMENT '最大值',
    scoring_criteria TEXT COMMENT '评分标准',
    description TEXT COMMENT '指标描述',
    status INT NOT NULL DEFAULT 1 COMMENT '状态:1-启用,0-禁用',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) COMMENT 'KPI指标表';

-- 考核周期表
CREATE TABLE assessment_period (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL COMMENT '周期名称',
    type INT NOT NULL COMMENT '类型:1-月度,2-季度,3-半年度,4-年度',
    start_date DATE NOT NULL COMMENT '开始日期',
    end_date DATE NOT NULL COMMENT '结束日期',
    status INT NOT NULL DEFAULT 1 COMMENT '状态:1-未开始,2-进行中,3-已结束',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) COMMENT '考核周期表';

-- 考核计划表
CREATE TABLE assessment_plan (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    period_id BIGINT NOT NULL COMMENT '考核周期ID',
    title VARCHAR(100) NOT NULL COMMENT '计划标题',
    department_id BIGINT COMMENT '部门ID',
    target_employees JSON COMMENT '目标员工ID列表',
    indicator_settings JSON COMMENT '指标配置',
    self_assessment_start DATE COMMENT '自评开始日期',
    self_assessment_end DATE COMMENT '自评结束日期',
    manager_assessment_start DATE COMMENT '主管评价开始日期',
    manager_assessment_end DATE COMMENT '主管评价结束日期',
    peer_assessment_start DATE COMMENT '同事评价开始日期',
    peer_assessment_end DATE COMMENT '同事评价结束日期',
    status INT NOT NULL DEFAULT 1 COMMENT '状态:1-草稿,2-进行中,3-已完成',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) COMMENT '考核计划表';

-- 考核评分表
CREATE TABLE assessment_score (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    plan_id BIGINT NOT NULL COMMENT '考核计划ID',
    employee_id BIGINT NOT NULL COMMENT '被考核人ID',
    assessor_id BIGINT NOT NULL COMMENT '评分人ID',
    assessment_type INT NOT NULL COMMENT '评分类型:1-自评,2-上级评分,3-同事评分,4-下级评分',
    scores JSON NOT NULL COMMENT '评分详情',
    total_score DECIMAL(5,2) COMMENT '总分',
    comments TEXT COMMENT '评语',
    status INT NOT NULL DEFAULT 1 COMMENT '状态:1-待评分,2-已评分',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_plan_employee_assessor (plan_id, employee_id, assessor_id)
) COMMENT '考核评分表';

-- 360度评价关系表
CREATE TABLE assessment_360_relation (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    plan_id BIGINT NOT NULL COMMENT '考核计划ID',
    employee_id BIGINT NOT NULL COMMENT '被考核人ID',
    superior_ids JSON COMMENT '上级评价人ID列表',
    peer_ids JSON COMMENT '同事评价人ID列表',
    subordinate_ids JSON COMMENT '下级评价人ID列表',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_plan_employee (plan_id, employee_id)
) COMMENT '360度评价关系表';

-- 考核结果表
CREATE TABLE assessment_result (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    plan_id BIGINT NOT NULL COMMENT '考核计划ID',
    employee_id BIGINT NOT NULL COMMENT '员工ID',
    self_score DECIMAL(5,2) COMMENT '自评得分',
    superior_score DECIMAL(5,2) COMMENT '上级评分',
    peer_score DECIMAL(5,2) COMMENT '同事评分',
    subordinate_score DECIMAL(5,2) COMMENT '下级评分',
    final_score DECIMAL(5,2) COMMENT '最终得分',
    rank_level VARCHAR(20) COMMENT '等级:A+,A,B+,B,C,D',
    evaluation TEXT COMMENT '综合评价',
    improvement_plan TEXT COMMENT '改进计划',
    status INT NOT NULL DEFAULT 1 COMMENT '状态:1-待确认,2-已确认',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_plan_employee (plan_id, employee_id)
) COMMENT '考核结果表';

-- 插入示例数据
INSERT INTO kpi_indicator (name, category, weight, unit, target_value, scoring_criteria, description) VALUES
('销售业绩', 1, 30.00, '万元', '100', '90-100分:完成目标120%以上\n80-89分:完成目标100-120%\n60-79分:完成目标80-100%\n0-59分:完成目标80%以下', '年度销售目标达成情况'),
('客户满意度', 1, 20.00, '%', '95', '90-100分:满意度95%以上\n80-89分:满意度90-95%\n60-79分:满意度85-90%\n0-59分:满意度85%以下', '客户满意度调查结果'),
('项目交付及时率', 2, 25.00, '%', '98', '90-100分:及时率98%以上\n80-89分:及时率95-98%\n60-79分:及时率90-95%\n0-59分:及时率90%以下', '项目按期交付比率');

-- 插入考核周期示例数据
INSERT INTO assessment_period (name, type, start_date, end_date, status) VALUES
('2025年第一季度考核', 2, '2025-01-01', '2025-03-31', 1),
('2025年年度考核', 4, '2025-01-01', '2025-12-31', 1);

sql\recruitment.sql

-- 招聘需求表
CREATE TABLE recruitment_demand (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    title VARCHAR(100) NOT NULL COMMENT '职位标题',
    department_id BIGINT NOT NULL COMMENT '部门ID',
    position_type VARCHAR(50) NOT NULL COMMENT '职位类型',
    head_count INT NOT NULL COMMENT '招聘人数',
    salary_range VARCHAR(50) COMMENT '薪资范围',
    experience_requirement VARCHAR(50) COMMENT '经验要求',
    education_requirement VARCHAR(50) COMMENT '学历要求',
    skills_requirement TEXT COMMENT '技能要求',
    job_description TEXT COMMENT '职位描述',
    status TINYINT DEFAULT 1 COMMENT '状态:1-招聘中,2-已暂停,3-已结束',
    publisher_id BIGINT COMMENT '发布人ID',
    publish_time DATETIME COMMENT '发布时间',
    deadline DATETIME COMMENT '截止时间',
    created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (department_id) REFERENCES department(id),
    FOREIGN KEY (publisher_id) REFERENCES employee(id)
) COMMENT '招聘需求表';

-- 简历表
CREATE TABLE resume (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    demand_id BIGINT COMMENT '关联的招聘需求ID',
    name VARCHAR(50) NOT NULL COMMENT '姓名',
    gender CHAR(1) COMMENT '性别:M-男,F-女',
    birth_date DATE COMMENT '出生日期',
    phone VARCHAR(20) NOT NULL COMMENT '手机号',
    email VARCHAR(100) NOT NULL COMMENT '邮箱',
    education VARCHAR(50) COMMENT '最高学历',
    school VARCHAR(100) COMMENT '毕业院校',
    major VARCHAR(100) COMMENT '专业',
    work_experience TEXT COMMENT '工作经验',
    skills TEXT COMMENT '技能特长',
    self_evaluation TEXT COMMENT '自我评价',
    attachment_url VARCHAR(255) COMMENT '简历附件URL',
    status TINYINT DEFAULT 1 COMMENT '状态:1-待筛选,2-初筛通过,3-初筛未通过,4-面试中,5-已录用,6-已入职,7-已拒绝',
    created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (demand_id) REFERENCES recruitment_demand(id)
) COMMENT '简历表';

-- 面试流程表
CREATE TABLE interview (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    resume_id BIGINT NOT NULL COMMENT '简历ID',
    round INT NOT NULL COMMENT '面试轮次',
    interview_time DATETIME COMMENT '面试时间',
    interviewer_ids VARCHAR(255) COMMENT '面试官ID列表,逗号分隔',
    interview_type TINYINT COMMENT '面试方式:1-现场面试,2-视频面试,3-电话面试',
    location VARCHAR(200) COMMENT '面试地点',
    status TINYINT DEFAULT 1 COMMENT '状态:1-待面试,2-已完成,3-已取消',
    evaluation TEXT COMMENT '面试评价',
    score INT COMMENT '面试评分',
    result TINYINT COMMENT '面试结果:1-通过,2-待定,3-不通过',
    created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (resume_id) REFERENCES resume(id)
) COMMENT '面试流程表';

-- 入职管理表
CREATE TABLE onboarding (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    resume_id BIGINT NOT NULL COMMENT '简历ID',
    offer_time DATETIME COMMENT 'Offer发放时间',
    planned_entry_time DATE COMMENT '预计入职时间',
    actual_entry_time DATE COMMENT '实际入职时间',
    position VARCHAR(50) COMMENT '入职岗位',
    department_id BIGINT COMMENT '入职部门ID',
    salary DECIMAL(12,2) COMMENT '薪资',
    status TINYINT DEFAULT 1 COMMENT '状态:1-待入职,2-已入职,3-已取消',
    mentor_id BIGINT COMMENT '导师ID',
    checklist TEXT COMMENT '入职清单',
    notes TEXT COMMENT '备注',
    created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (resume_id) REFERENCES resume(id),
    FOREIGN KEY (department_id) REFERENCES department(id),
    FOREIGN KEY (mentor_id) REFERENCES employee(id)
) COMMENT '入职管理表';

-- 插入测试数据
INSERT INTO recruitment_demand (title, department_id, position_type, head_count, salary_range, experience_requirement, education_requirement, skills_requirement, job_description, status, publisher_id)
VALUES 
('Java高级开发工程师', 2, '技术', 2, '25k-35k', '3-5年', '本科及以上', 'Java, Spring Boot, MySQL', '负责公司核心业务系统的开发和维护...', 1, 1),
('前端开发工程师', 2, '技术', 1, '15k-25k', '1-3年', '本科及以上', 'Vue.js, React, JavaScript', '负责公司前端项目的开发和维护...', 1, 1);

-- 插入测试简历数据
INSERT INTO resume (demand_id, name, gender, phone, email, education, school, major, work_experience, skills, status)
VALUES 
(1, '张工', 'M', '13800138000', 'zhang@example.com', '本科', '北京理工大学', '计算机科学', '5年Java开发经验', 'Java, Spring Boot, MySQL', 1),
(1, '李工', 'F', '13900139000', 'li@example.com', '硕士', '清华大学', '软件工程', '3年Java开发经验', 'Java, Spring Cloud, Redis', 2);

sql\salary.sql

-- 薪资结构配置表
CREATE TABLE salary_structure (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL COMMENT '薪资项名称',
    type INT NOT NULL COMMENT '类型:1-基本工资,2-津贴,3-奖金,4-社保,5-公积金,6-个税,7-其他',
    calculation_type INT NOT NULL COMMENT '计算类型:1-固定金额,2-基本工资百分比,3-自定义公式',
    calculation_value VARCHAR(255) COMMENT '计算值:固定金额/百分比/公式',
    is_plus BOOLEAN NOT NULL COMMENT '是否加项',
    is_taxable BOOLEAN NOT NULL COMMENT '是否计税',
    status INT NOT NULL DEFAULT 1 COMMENT '状态:1-启用,0-禁用',
    description VARCHAR(255) COMMENT '描述',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) COMMENT '薪资结构配置表';

-- 员工薪资配置表
CREATE TABLE employee_salary_config (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    employee_id BIGINT NOT NULL COMMENT '员工ID',
    base_salary DECIMAL(12,2) NOT NULL COMMENT '基本工资',
    structure_ids JSON COMMENT '薪资结构项ID列表',
    effective_date DATE NOT NULL COMMENT '生效日期',
    status INT NOT NULL DEFAULT 1 COMMENT '状态:1-生效,0-失效',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_employee_date (employee_id, effective_date)
) COMMENT '员工薪资配置表';

-- 工资单表
CREATE TABLE salary_sheet (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    employee_id BIGINT NOT NULL COMMENT '员工ID',
    year INT NOT NULL COMMENT '年份',
    month INT NOT NULL COMMENT '月份',
    base_salary DECIMAL(12,2) NOT NULL COMMENT '基本工资',
    allowances DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '津贴合计',
    bonus DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '奖金合计',
    insurance DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '社保合计',
    housing_fund DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '公积金合计',
    tax DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '个人所得税',
    others DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '其他',
    total_salary DECIMAL(12,2) NOT NULL COMMENT '实发工资',
    details JSON COMMENT '明细项',
    status INT NOT NULL DEFAULT 1 COMMENT '状态:1-待发放,2-已发放,3-已撤回',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_employee_year_month (employee_id, year, month)
) COMMENT '工资单表';

-- 社保配置表
CREATE TABLE insurance_config (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    city_code VARCHAR(50) NOT NULL COMMENT '城市编码',
    insurance_type INT NOT NULL COMMENT '类型:1-养老,2-医疗,3-失业,4-工伤,5-生育',
    company_ratio DECIMAL(5,2) NOT NULL COMMENT '公司缴纳比例',
    personal_ratio DECIMAL(5,2) NOT NULL COMMENT '个人缴纳比例',
    min_base DECIMAL(12,2) NOT NULL COMMENT '最低基数',
    max_base DECIMAL(12,2) NOT NULL COMMENT '最高基数',
    effective_date DATE NOT NULL COMMENT '生效日期',
    status INT NOT NULL DEFAULT 1 COMMENT '状态:1-生效,0-失效',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_city_type_date (city_code, insurance_type, effective_date)
) COMMENT '社保配置表';

-- 公积金配置表
CREATE TABLE housing_fund_config (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    city_code VARCHAR(50) NOT NULL COMMENT '城市编码',
    company_ratio DECIMAL(5,2) NOT NULL COMMENT '公司缴纳比例',
    personal_ratio DECIMAL(5,2) NOT NULL COMMENT '个人缴纳比例',
    min_base DECIMAL(12,2) NOT NULL COMMENT '最低基数',
    max_base DECIMAL(12,2) NOT NULL COMMENT '最高基数',
    effective_date DATE NOT NULL COMMENT '生效日期',
    status INT NOT NULL DEFAULT 1 COMMENT '状态:1-生效,0-失效',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_city_date (city_code, effective_date)
) COMMENT '公积金配置表';

-- 社保公积金记录表
CREATE TABLE insurance_fund_record (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    employee_id BIGINT NOT NULL COMMENT '员工ID',
    year INT NOT NULL COMMENT '年份',
    month INT NOT NULL COMMENT '月份',
    city_code VARCHAR(50) NOT NULL COMMENT '城市编码',
    insurance_base DECIMAL(12,2) NOT NULL COMMENT '社保基数',
    housing_fund_base DECIMAL(12,2) NOT NULL COMMENT '公积金基数',
    pension_company DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '养老保险公司缴纳',
    pension_personal DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '养老保险个人缴纳',
    medical_company DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '医疗保险公司缴纳',
    medical_personal DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '医疗保险个人缴纳',
    unemployment_company DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '失业保险公司缴纳',
    unemployment_personal DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '失业保险个人缴纳',
    injury_company DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '工伤保险公司缴纳',
    maternity_company DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '生育保险公司缴纳',
    housing_fund_company DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '公积金公司缴纳',
    housing_fund_personal DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '公积金个人缴纳',
    status INT NOT NULL DEFAULT 1 COMMENT '状态:1-待缴纳,2-已缴纳',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_employee_year_month (employee_id, year, month)
) COMMENT '社保公积金记录表';

-- 插入示例数据
INSERT INTO salary_structure (name, type, calculation_type, calculation_value, is_plus, is_taxable, description) VALUES
('基本工资', 1, 1, NULL, true, true, '基本工资'),
('职务津贴', 2, 1, '1000', true, true, '职务津贴'),
('交通津贴', 2, 1, '500', true, true, '交通津贴'),
('餐补', 2, 1, '600', true, false, '餐补津贴'),
('绩效奖金', 3, 2, '20', true, true, '基本工资的20%'),
('年终奖', 3, 1, NULL, true, true, '年终奖金'),
('养老保险', 4, 2, '8', false, false, '基本工资的8%'),
('医疗保险', 4, 2, '2', false, false, '基本工资的2%'),
('失业保险', 4, 2, '0.5', false, false, '基本工资的0.5%'),
('住房公积金', 5, 2, '12', false, false, '基本工资的12%');

-- 插入社保配置示例数据
INSERT INTO insurance_config (city_code, insurance_type, company_ratio, personal_ratio, min_base, max_base, effective_date) VALUES
('110000', 1, 16, 8, 3613, 23565, '2025-01-01'),   -- 北京养老保险
('110000', 2, 10, 2, 3613, 23565, '2025-01-01'),   -- 北京医疗保险
('110000', 3, 0.8, 0.2, 3613, 23565, '2025-01-01'), -- 北京失业保险
('110000', 4, 0.4, 0, 3613, 23565, '2025-01-01'),   -- 北京工伤保险
('110000', 5, 0.8, 0, 3613, 23565, '2025-01-01');   -- 北京生育保险

-- 插入公积金配置示例数据
INSERT INTO housing_fund_config (city_code, company_ratio, personal_ratio, min_base, max_base, effective_date) VALUES
('110000', 12, 12, 3613, 23565, '2025-01-01');      -- 北京公积金

sql\training.sql

-- 培训计划表
CREATE TABLE training_plan (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    title VARCHAR(100) NOT NULL COMMENT '计划标题',
    type INT NOT NULL COMMENT '类型:1-新员工培训,2-专业技能,3-管理能力,4-通用素质',
    department_id BIGINT COMMENT '部门ID,空表示全公司',
    target_employees JSON COMMENT '目标员工ID列表',
    start_date DATE NOT NULL COMMENT '开始日期',
    end_date DATE NOT NULL COMMENT '结束日期',
    budget DECIMAL(12,2) COMMENT '预算金额',
    objectives TEXT COMMENT '培训目标',
    description TEXT COMMENT '培训描述',
    status INT NOT NULL DEFAULT 1 COMMENT '状态:1-草稿,2-待审批,3-已审批,4-进行中,5-已完成,6-已取消',
    creator_id BIGINT NOT NULL COMMENT '创建人ID',
    approver_id BIGINT COMMENT '审批人ID',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) COMMENT '培训计划表';

-- 课程资源表
CREATE TABLE course (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    title VARCHAR(100) NOT NULL COMMENT '课程标题',
    type INT NOT NULL COMMENT '类型:1-线上课程,2-线下课程,3-混合课程',
    category INT NOT NULL COMMENT '类别:1-专业技能,2-管理能力,3-通用素质',
    level INT NOT NULL COMMENT '难度等级:1-入门,2-初级,3-中级,4-高级',
    duration INT NOT NULL COMMENT '课程时长(分钟)',
    lecturer_id BIGINT COMMENT '讲师ID',
    max_participants INT COMMENT '最大参与人数',
    location VARCHAR(255) COMMENT '培训地点',
    materials JSON COMMENT '课程资料列表',
    objectives TEXT COMMENT '课程目标',
    description TEXT COMMENT '课程描述',
    status INT NOT NULL DEFAULT 1 COMMENT '状态:1-启用,0-禁用',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) COMMENT '课程资源表';

-- 培训班次表
CREATE TABLE training_class (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    plan_id BIGINT NOT NULL COMMENT '培训计划ID',
    course_id BIGINT NOT NULL COMMENT '课程ID',
    class_name VARCHAR(100) NOT NULL COMMENT '班次名称',
    start_time DATETIME NOT NULL COMMENT '开始时间',
    end_time DATETIME NOT NULL COMMENT '结束时间',
    participants JSON COMMENT '参训人员ID列表',
    actual_participants JSON COMMENT '实际参训人员ID列表',
    status INT NOT NULL DEFAULT 1 COMMENT '状态:1-未开始,2-进行中,3-已结束,4-已取消',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) COMMENT '培训班次表';

-- 培训评估表
CREATE TABLE training_evaluation (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    class_id BIGINT NOT NULL COMMENT '培训班次ID',
    employee_id BIGINT NOT NULL COMMENT '员工ID',
    course_score INT COMMENT '课程内容评分(1-5)',
    lecturer_score INT COMMENT '讲师评分(1-5)',
    material_score INT COMMENT '培训资料评分(1-5)',
    arrangement_score INT COMMENT '培训安排评分(1-5)',
    effectiveness_score INT COMMENT '培训效果评分(1-5)',
    knowledge_improvement TEXT COMMENT '知识技能提升',
    suggestions TEXT COMMENT '改进建议',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_class_employee (class_id, employee_id)
) COMMENT '培训评估表';

-- 职业发展规划表
CREATE TABLE career_plan (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    employee_id BIGINT NOT NULL COMMENT '员工ID',
    current_position VARCHAR(100) NOT NULL COMMENT '当前职位',
    target_position VARCHAR(100) NOT NULL COMMENT '目标职位',
    plan_period INT NOT NULL COMMENT '规划期限(月)',
    start_date DATE NOT NULL COMMENT '开始日期',
    end_date DATE NOT NULL COMMENT '结束日期',
    skill_requirements JSON COMMENT '技能要求',
    development_path JSON COMMENT '发展路径',
    training_needs JSON COMMENT '培训需求',
    milestones JSON COMMENT '关键里程碑',
    status INT NOT NULL DEFAULT 1 COMMENT '状态:1-制定中,2-执行中,3-已完成,4-已终止',
    mentor_id BIGINT COMMENT '导师ID',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) COMMENT '职业发展规划表';

-- 职业发展进度表
CREATE TABLE career_progress (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    plan_id BIGINT NOT NULL COMMENT '发展规划ID',
    milestone_id VARCHAR(50) NOT NULL COMMENT '里程碑ID',
    completion_date DATE COMMENT '完成日期',
    completion_status INT NOT NULL DEFAULT 1 COMMENT '状态:1-未开始,2-进行中,3-已完成,4-已延期',
    evaluation TEXT COMMENT '评估意见',
    next_steps TEXT COMMENT '下一步计划',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_plan_milestone (plan_id, milestone_id)
) COMMENT '职业发展进度表';

源码下载

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天天进步2015

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值