POI报表的入门
理解员工管理的的业务逻辑
能够说出Eureka和Feign的作用
理解报表的两种形式和POI的基本操作熟练使用POI完成Excel的导入导出操作
员工管理
需求分析
企业员工管理是人事资源管理系统中最重要的一个环节,分为对员工入职,转正,离职,调岗,员工报表导入导出 等业务逻辑。需求看似复杂,实际上都是对数据库表的基本操作。
数据库表概述
对于员工操作而言,涉及到的数据库表如下表格说明:
数据库表名称 | 说明 |
em_archive | 月度员工归档表 |
em_positive | 转正申请表 |
em_resignation | 离职申请表 |
em_transferposition | 员工调岗申请表 |
em_user_company_jobs | 员工岗位信息表 |
em_user_company_personal | 员工详细信息表 |
代码实现
服务端实现
- 创建员工微服务
ihrm_employee
- 配置文件 application.yml
- 配置Shiro核心配置类ShiroConfiguration
- 配置启动类EmployeeApplication
- 导入资源中提供的基本Controller,Service,Dao,Domain代码
前端实现
导入资源中提供的前端代码。
服务发现组件 Eureka
Eureka是Netflflix开发的服务发现框架,SpringCloud将它集成在自己的子项目spring-cloud-Netflflix中,实现SpringCloud的服务发现功能。Eureka包含两个组件:Eureka Server和Eureka Client。
Eureka Server提供服务注册服务,各个节点启动后,会在Eureka Server中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观的看到。
Eureka Client是一个java客户端,用于简化与Eureka Server的交互,客户端同时也就别一个内置的、使用轮 询(round-robin)负载算法的负载均衡器。在应用启动后,将会向Eureka Server发送心跳,默认周期为30秒, 如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,Eureka Server将会从服务注册表中把这个服务节点移除(默认90秒)。
Eureka服务端开发
- 创建ihrm_eureka模块
- 引入依赖 父工程pom.xml定义SpringCloud版本
<dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>Finchley.M9</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement>
ihrm_eureka模块pom.xml引入eureka-server
<dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-server</artifactId></dependency></dependencies>
- 添加application.yml
server:port: 6868 #服务端口
eureka:client:registerWithEureka: false #是否将自己注册到Eureka服务中,本身就是所有无需
注册fetchRegistry: false #是否从Eureka中获取注册信息serviceUrl: #Eureka客户端与Eureka服务端进行交互的地址defaultZone: http://127.0.0.1:${server.port}/eureka/
- 配置启动类
@SpringBootApplication
@EnableEurekaServer
public class EurekaServer {public static void main(String[] args) {SpringApplication.run(EurekaServer.class, args);}
}
微服务注册
我们现在就将所有的微服务都注册到Eureka中,这样所有的微服务之间都可以互相调用了。
- 将其他微服务模块添加依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 修改每个微服务的application.yml,添加注册eureka服务的配置
eureka:client:service-url:defaultZone: http://localhost:6868/eurekainstance:prefer-ip-address: true
- 修改每个服务类的启动类,添加注解@EnableEurekaClient
骚戴理解:把微服务都注册到Eureka后可以通过访问:http://localhost:6868进入Eureka网站看微服务的存活情况
微服务调用组件Feign
简介
Feign是简化Java HTTP客户端开发的工具(java-to-httpclient-binder),它的灵感来自于Retrofit、JAXRS-2.0和WebSocket。Feign的初衷是降低统一绑定Denominator到HTTP API的复杂度,不区分是否为restful
快速体验
我们现在在系统微服务调用企业微服务的方法(根据ID查询部门)
- 在ihrm_system模块添加依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 修改ihrm_system模块的启动类,添加注解
@EnableDiscoveryClient
@EnableFeignClients
- 在Ihrm_system模块创建com.ihrm.system.client包,包下创建接口
//@FeignClient注解用于指定从哪个服务中调用功能 ,注意里面的名称与被调用的服务名保持一致
@FeignClient(value = "ihrm-company")
public interface DepartmentFeignClient {//@RequestMapping注解用于对被调用的微服务进行地址映射@RequestMapping(value = "/company/departments/{id}/", method = RequestMethod.GET)public Department findById(@PathVariable("id") String id) throws Exception;
}
- 修改Ihrm_system模块的 UserController
@Autowired private DepartmentFeignClient departmentFeignClient; //测试通过系统微服务调用企业微服务方法@RequestMapping(value = "/test/{id}") public void findDeptById(@PathVariable String id){ Department dept = departmentFeignClient.findById(id); System.out.println(dept); }
骚戴理解:Feign组件是谁调用就写在谁那里,例如微服务A远程调用微服务B的接口,那么上面的这些步骤都写在微服务A里面
- 配置Feign拦截器添加请求头
package com.ihrm.common.feign;import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;@Configuration
public class FeignConfiguration {//配置feign拦截器,解决请求头问题@Beanpublic RequestInterceptor requestInterceptor() {return new RequestInterceptor() {//获取所有浏览器发送的请求属性,请求头赋值到feignpublic void apply(RequestTemplate requestTemplate) {//请求属性ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();if(attributes != null) {HttpServletRequest request = attributes.getRequest();//获取浏览器发起的请求头Enumeration<String> headerNames = request.getHeaderNames();if (headerNames != null) {while (headerNames.hasMoreElements()) {String name = headerNames.nextElement(); //请求头名称 AuthorizationString value = request.getHeader(name);//请求头数据 "Bearer b1dbb4cf-7de6-41e5-99e2-0e8b7e8fe6ee"requestTemplate.header(name,value);}}}}};}
}
骚戴理解:在使用Feign组件远程调用其他微服务的时候会丢失请求头,所以要写一个上面的配置类来解决这个问题,其实就是把请求头给配置好,在每次调用其他微服务的时候都把这个请求头设置进去,这样就不缺请求头了
POI报表的概述
需求说明
在企业级应用开发中,Excel报表是一种最常见的报表需求。Excel报表开发一般分为两种形式:
- 为了方便操作,基于Excel的报表批量上传数据
- 通过java代码生成Excel报表。
在Saas-HRM系统中,也有大量的报表操作,那么接下来的课程就是一起来学习企业级的报表开发。
Excel的两种形式
目前世面上的Excel分为两个大的版本Excel2003和Excel2007及以上两个版本,两者之间的区别如下:
Excel2003是一个特有的二进制格式,其核心结构是复合文档类型的结构,存储数据量较小;Excel2007 的核心结构是 XML 类型的结构,采用的是基于 XML 的压缩方式,使其占用的空间更小,操作效率更高
常见excel操作工具
Java中常见的用来操作Excl的方式一般有2种:JXL和POI。
JXL只能对Excel进行操作,属于比较老的框架,它只支持到Excel 95-2000的版本。现在已经停止更新和维护。POI是apache的项目,可对微软的Word,Excel,Ppt进行操作,包括office2003和2007,Excl2003和2007。poi现在一直有更新。所以现在主流使用POI。
POI的概述
Apache POI是Apache软件基金会的开源项目,由Java编写的免费开源的跨平台的 Java API,Apache POI提供API
给Java语言操作Microsoft Office的功能。
POI的应用场景
- 数据报表生成
- 数据备份
- 数据批量上传
POI的入门操作
搭建环境
<dependencies><dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>4.0.1</version></dependency><dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>4.0.1</version></dependency><dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml-schemas</artifactId><version>4.0.1</version></dependency>
</dependencies>
POI结构说明
- HSSF提供读写Microsoft Excel XLS格式档案的功能。
- XSSF提供读写Microsoft Excel OOXML XLSX格式档案的功能。
- HWPF提供读写Microsoft Word DOC格式档案的功能。
- HSLF提供读写Microsoft PowerPoint格式档案的功能。
- HDGF提供读Microsoft Visio格式档案的功能。
- HPBF提供读Microsoft Publisher格式档案的功能。
- HSMF提供读Microsoft Outlook格式档案的功能。
API介绍
API名称 | |
Workbook | Excel的文档对象,针对不同的Excel类型分为:HSSFWorkbook【2003版本后缀为.xls】和XSSFWorkbool【2007版本后缀为.xlsx】 |
Sheet | Excel的表单 |
Row | Excel的行 |
Cell | Excel的格子单元 |
Font | Excel字体 |
CellStyle | 格子单元样式 |
基本操作
创建Excel
public class PoiTest01 {//测试创建excel文件public static void main(String[] args) throws Exception {//1.创建workbook工作簿Workbook wb = new XSSFWorkbook();//2.创建表单SheetSheet sheet = wb.createSheet("test");//3.文件流FileOutputStream fos = new FileOutputStream("E:\\test.xlsx");//4.写入文件wb.write(fos);fos.close();}
}
骚戴理解:FileOutputStream fos = new FileOutputStream("E:\\test.xlsx");表示把创建的excel文件输出到E:\\test.xlsx路径下
创建单元格
//测试创建单元格public static void main(String[] args) throws Exception {//1.创建workbook工作簿Workbook wb = new XSSFWorkbook();//2.创建表单SheetSheet sheet = wb.createSheet("test");//3.创建行对象,从0开始Row row = sheet.createRow(3);//4.创建单元格,从0开始Cell cell = row.createCell(0);//5.单元格写入数据cell.setCellValue("骚戴");//6.文件流FileOutputStream fos = new FileOutputStream("E:\\test.xlsx");//7.写入文件wb.write(fos);fos.close();}
骚戴理解: Row row = sheet.createRow(3);是指定第四行,Cell cell = row.createCell(0);是指定第1列,如图所示:
设置格式
package cn.itcast.poi.test;import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;import java.io.FileOutputStream;/*** 单元格样式处理*/
public class PoiTest03 {public static void main(String[] args) throws Exception {//创建工作簿 HSSFWorkbook -- 2003Workbook wb = new XSSFWorkbook(); //2007版本//创建表单sheetSheet sheet = wb.createSheet("test");//创建行对象 参数:索引(从0开始)Row row = sheet.createRow(2);//创建单元格对象 参数:索引(从0开始)Cell cell = row.createCell(2);//向单元格中写入内容cell.setCellValue("传智播客");//样式处理//创建样式对象CellStyle style = wb.createCellStyle();style.setBorderTop(BorderStyle.THIN);//上边框style.setBorderBottom(BorderStyle.THIN);//下边框style.setBorderLeft(BorderStyle.THIN);//左边框style.setBorderRight(BorderStyle.THIN);//右边框//创建字体对象Font font = wb.createFont();font.setFontName("华文行楷"); //字体font.setFontHeightInPoints((short)28);//字号style.setFont(font);//行高和列宽row.setHeightInPoints(50);//行高//列宽的宽度 字符宽度sheet.setColumnWidth(2,31 * 256);//列宽//剧中显示style.setAlignment(HorizontalAlignment.CENTER);//水平居中style.setVerticalAlignment(VerticalAlignment.CENTER);//垂直居中//向单元格设置样式cell.setCellStyle(style);//文件流FileOutputStream pis = new FileOutputStream("E:\\excel\\poi\\test2.xlsx");//写入文件wb.write(pis);pis.close();}
}
骚戴理解: sheet.setColumnWidth(0, 31 * 256);//设置第一列的宽度是31个字符宽度,注意这里要乘以256是因为源码里面设计的时候是会除以256
绘制图形
package cn.itcast.poi.test;import org.apache.poi.ss.usermodel.*;
import org.apache.poi.util.IOUtils;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;import java.io.FileInputStream;
import java.io.FileOutputStream;/*** 插入图片*/
public class PoiTest04 {public static void main(String[] args) throws Exception {//创建工作簿 HSSFWorkbook -- 2003Workbook wb = new XSSFWorkbook(); //2007版本//创建表单sheetSheet sheet = wb.createSheet("test");//读取图片流FileInputStream stream = new FileInputStream("E:\\excel\\poi\\logo.jpg");//转化二进制数组byte[] bytes = IOUtils.toByteArray(stream);stream.read(bytes);//向POI内存中添加一张图片,返回图片在图片集合中的索引int index = wb.addPicture(bytes, Workbook.PICTURE_TYPE_JPEG);//参数一:图片的二进制数据,参数二:图片类型//绘制图片工具类CreationHelper helper = wb.getCreationHelper();//创建一个绘图对象Drawing<?> patriarch = sheet.createDrawingPatriarch();//创建锚点,设置图片坐标ClientAnchor anchor = helper.createClientAnchor();anchor.setRow1(0);anchor.setCol1(0);//绘制图片Picture picture = patriarch.createPicture(anchor, index);//图片位置,图片的索引picture.resize();//自适应渲染图片//文件流FileOutputStream pis = new FileOutputStream("E:\\excel\\poi\\test3.xlsx");//写入文件wb.write(pis);pis.close();}
}
骚戴理解:下面的所谓的锚点其实就是图片的起始下标,例如下面设置就是在第一行第一列插入一张图片
//创建锚点,设置图片坐标
ClientAnchor anchor = helper.createClientAnchor();
anchor.setRow1(0);
anchor.setCol1(0);
加载Excel
package cn.itcast.poi.test;import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;import java.io.IOException;/*** 读取excel并解析* sheet.getLastRowNum() : 最后一行的索引* row.getLastCellNum() : 最后一个单元格的号码*/
public class PoiTest05 {public static void main(String[] args) throws Exception {//1.根据Excel文件创建工作簿Workbook wb = new XSSFWorkbook("E:\\excel\\poi\\demo.xlsx");//2.获取SheetSheet sheet = wb.getSheetAt(0);//参数:索引//3.获取Sheet中的每一行,和每一个单元格for (int rowNum = 0; rowNum<= sheet.getLastRowNum() ;rowNum ++) {Row row = sheet.getRow(rowNum);//根据索引获取每一个行StringBuilder sb = new StringBuilder();for(int cellNum=2;cellNum< row.getLastCellNum(); cellNum ++) {//根据索引获取每一个单元格Cell cell = row.getCell(cellNum);//获取每一个单元格的内容Object value = getCellValue(cell);sb.append(value).append("-");}System.out.println(sb.toString());}}//根据单元格数据类型获取数据public static Object getCellValue(Cell cell) {//1.获取到单元格的属性类型CellType cellType = cell.getCellType();//2.根据单元格数据类型获取数据Object value = null;switch (cellType) {case STRING:value = cell.getStringCellValue();break;case BOOLEAN:value = cell.getBooleanCellValue();break;case NUMERIC:if(DateUtil.isCellDateFormatted(cell)) {//日期格式value = cell.getDateCellValue();}else{//数字value = cell.getNumericCellValue();}break;case FORMULA: //公式value = cell.getCellFormula();break;default:break;}return value;}
}
骚戴理解:这里要注意Excel里面单元格有很多类型,所以需要判断是什么类型然后对应Java代码,其中日期和数字类型都是“NUMERIC”类型,所以需要调用DateUtil.isCellDateFormatted()去判断这个单元格的类型是不是日期类型,如果不是那就是数字类型,上面的getCellValue方法是可以提取处理作为一个工具类的
POI报表导入
需求分析
实现批量导入员工功能,页面端上传excel表格,服务端解析表格获取数据,批量新增用户
员工导入
搭建环境
父模块pom文件添加依赖
<dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>4.0.1</version>
</dependency>
<dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>4.0.1</version>
</dependency>
<dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml-schemas</artifactId><version>4.0.1</version>
</dependency>
实现Excel上传
- 用户实体类配置构造方法
//objs数据位置和excel上传位置一致。public User(Object []objs,String companyId,String companyName) {//用户名 手机号 工号 聘用 形式 入职 时间 部门编码this.username = values[1].toString();this.mobile = values[2].toString();this.createTime = new Date();//默认手机号excel读取为字符串会存在科学记数法问题,转化处理this.workNumber = new DecimalFormat("#").format(values[3]).toString();this.formOfEmployment =((Double) values[4]).intValue();this.timeOfEntry = (Date) values[5];this.departmentId = values[6].toString(); //部门编码 != 部门id}
骚戴理解:这里代码设计的就有问题,这个Excel里面搞了一个部门编码,然后在User里面压根就没有部门编码这个属性,所以就把部门编码的属性放到了departmentId里面,才会有部门编码 != 部门id,我一开始都被绕晕了,这样搞应该是为了用到远程调用,为了调用ihrm_company微服务来根据部门编码和公司id去查询部门id,这样设计太牵强了!
this.workNumber = new DecimalFormat("#").format(values[3]).toString(); //默认手机号excel读取为字符串会存在科学记数法问题,转化处理。
DecimalFormat("#") 在这里创建了一个 DecimalFormat 的对象,其中 # 是特殊字符,表示数字的占位符。
.format(values[3]) 表示将values[3]这个数字转换为字符串,并按照构造函数中制定的格式进行格式化。
举个例子:如果 objs[2]=1234.5678 ,那么 new DecimalFormat("#").format(objs[2]) 的返回结果就是一个字符串 "1235",因为格式中只有整数部分的占位符 #
- 在系统微服务UserController中添加上传方法
/*** 导入Excel,添加用户* 文件上传:springboot*/@RequestMapping(value="/user/import",method = RequestMethod.POST)public Result importUser(@RequestParam(name="file") MultipartFile file) throws Exception {//1.解析Excel//1.1.根据Excel文件创建工作簿Workbook wb = new XSSFWorkbook(file.getInputStream());//1.2.获取SheetSheet sheet = wb.getSheetAt(0);//参数:索引//1.3.获取Sheet中的每一行,和每一个单元格//2.获取用户数据列表List<User> list = new ArrayList<>();System.out.println(sheet.getLastRowNum());for (int rowNum = 1; rowNum<= sheet.getLastRowNum() ;rowNum ++) {Row row = sheet.getRow(rowNum);//根据索引获取每一个行Object [] values = new Object[row.getLastCellNum()];for(int cellNum=1;cellNum< row.getLastCellNum(); cellNum ++) {Cell cell = row.getCell(cellNum);Object value = getCellValue(cell);values[cellNum] = value;}User user = new User(values);list.add(user);}//3.批量保存用户userService.saveAll(list,companyId,companyName);return new Result(ResultCode.SUCCESS);}//根据单元格数据类型获取数据public static Object getCellValue(Cell cell) {//1.获取到单元格的属性类型CellType cellType = cell.getCellType();//2.根据单元格数据类型获取数据Object value = null;switch (cellType) {case STRING:value = cell.getStringCellValue();break;case BOOLEAN:value = cell.getBooleanCellValue();break;case NUMERIC:if(DateUtil.isCellDateFormatted(cell)) {//日期格式value = cell.getDateCellValue();}else{//数字value = cell.getNumericCellValue();}break;case FORMULA: //公式value = cell.getCellFormula();break;default:break;}return value;}
调用企业微服务获取部门数据
- 在Ihrm_system模块创建com.ihrm.system.client包,包下创建接口
@FeignClient(value = "ihrm-company")
package com.ihrm.system.client;import com.ihrm.common.entity.Result;
import com.ihrm.domain.company.Department;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;/*** 声明接口,通过feign调用其他微服务*/
//声明调用的微服务名称
@FeignClient("ihrm-company")
public interface DepartmentFeignClient {/*** 调用微服务的接口*/@RequestMapping(value="/company/department/{id}",method = RequestMethod.GET)public Result findById(@PathVariable(value="id") String id);//远程调用企业微服务,根据企业编码code和企业名称获取企业信息@RequestMapping(value="/company/department/search",method = RequestMethod.POST)public Department findByCode(@RequestParam(value="code") String code, @RequestParam(value="companyId") String companyId);
}
- 修改UserService,注入DepartmentFeignClient
@Autowired
private DepartmentFeignClient departmentFeignClient;
保存全部用户
UserService中添加保存全部的方法
@Transactionalpublic void save(List<User> users) throws Exception {for (User user : users) {//配置密码user.setPassword(new Md5Hash("123456",user.getMobile(),3).toString());//配置iduser.setId(idWorker.nextId()+"");//其他基本属性user.setInServiceStatus(1);user.setEnableState(1);user.setLevel("user");//获取部门信息Department dept = departmentFeignClient.findById(user.getDepartmentId(),user.getCompanyId());if(dept != null) {user.setDepartmentId(dept.getId());user.setDepartmentName(dept.getName());}userDao.save(user);}}
POI报表导出
需求分析
完成当月人事报表的导出:包含当月入职员工信息,离职员工信息
骚戴理解:也就是要把em_user_company_personal(员工详细信息表)和em_resignation(离职申请表)进行左外连接
人事报表导出
步骤分析
- 构造Excel表格数据创建工作簿
- 创建sheet 创建行对象
- 创建单元格对象
- 填充数据,设置样式下载
代码实现
- EmployeeReportResult
package com.ihrm.domain.employee.response;import com.ihrm.domain.employee.EmployeeResignation;
import com.ihrm.domain.employee.UserCompanyPersonal;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.springframework.beans.BeanUtils;/*** 导出报表构造的人事报表对象*/
@Getter
@Setter
@NoArgsConstructor
@ToString
public class EmployeeReportResult {private String userId;private String username;private String departmentName;private String mobile;private String timeOfEntry;private String companyId;private String sex;/*** 出生日期*/private String dateOfBirth;/*** 最高学历*/private String theHighestDegreeOfEducation;/*** 国家地区*/private String nationalArea;/*** 护照号*/private String passportNo;/*** 身份证号*/private String idNumber;/*** 身份证照片-正面*/private String idCardPhotoPositive;/*** 身份证照片-背面*/private String idCardPhotoBack;/*** 籍贯*/private String nativePlace;/*** 民族*/private String nation;/*** 英文名*/private String englishName;/*** 婚姻状况*/private String maritalStatus;/*** 员工照片*/private String staffPhoto;/*** 生日*/private String birthday;/*** 属相*/private String zodiac;/*** 年龄*/private String age;/*** 星座*/private String constellation;/*** 血型*/private String bloodType;/*** 户籍所在地*/private String domicile;/*** 政治面貌*/private String politicalOutlook;/*** 入党时间*/private String timeToJoinTheParty;/*** 存档机构*/private String archivingOrganization;/*** 子女状态*/private String stateOfChildren;/*** 子女有无商业保险*/private String doChildrenHaveCommercialInsurance;/*** 有无违法违纪行为*/private String isThereAnyViolationOfLawOrDiscipline;/*** 有无重大病史*/private String areThereAnyMajorMedicalHistories;/*** QQ*/private String qq;/*** 微信*/private String wechat;/*** 居住证城市*/private String residenceCardCity;/*** 居住证办理日期*/private String dateOfResidencePermit;/*** 居住证截止日期*/private String residencePermitDeadline;/*** 现居住地*/private String placeOfResidence;/*** 通讯地址*/private String postalAddress;/*** 联系手机*/private String contactTheMobilePhone;/*** 个人邮箱*/private String personalMailbox;/*** 紧急联系人*/private String emergencyContact;/*** 紧急联系电话*/private String emergencyContactNumber;/*** 社保电脑号*/private String socialSecurityComputerNumber;/*** 公积金账号*/private String providentFundAccount;/*** 银行卡号*/private String bankCardNumber;/*** 开户行*/private String openingBank;/*** 学历类型*/private String educationalType;/*** 毕业学校*/private String graduateSchool;/*** 入学时间*/private String enrolmentTime;/*** 毕业时间*/private String graduationTime;/*** 专业*/private String major;/*** 毕业证书*/private String graduationCertificate;/*** 学位证书*/private String certificateOfAcademicDegree;/*** 上家公司*/private String homeCompany;/*** 职称*/private String title;/*** 简历*/private String resume;/*** 有无竞业限制*/private String isThereAnyCompetitionRestriction;/*** 前公司离职证明*/private String proofOfDepartureOfFormerCompany;/*** 备注*/private String remarks;/*** 离职时间*/private String resignationTime;/*** 离职类型*/private String typeOfTurnover;/*** 申请离职原因*/private String reasonsForLeaving;public EmployeeReportResult(UserCompanyPersonal personal, EmployeeResignation resignation) {BeanUtils.copyProperties(personal,this);if(resignation != null) {BeanUtils.copyProperties(resignation,this);}}
}
- 配置EmployeeController
/*** 导出人事报表* @param month* @throws Exception*/
@RequestMapping(value = "/export/{month}", method = RequestMethod.GET)public void export(@PathVariable(name = "month") String month) throws Exception {//1.构造数据List<EmployeeReportResult> list = userCompanyPersonalService.findByReport(companyId,month+"%");//2.创建工作簿XSSFWorkbook workbook = new XSSFWorkbook();//3.构造sheetString[] titles = {"编号", "姓名", "手机","最高学历", "国家地区", "护照号", "籍贯", "生日", "属相","入职时间","离职类型","离职原因","离职时间"};Sheet sheet = workbook.createSheet(); Row row = sheet.createRow(0);AtomicInteger headersAi = new AtomicInteger();for (String title : titles) {Cell cell = row.createCell(headersAi.getAndIncrement());cell.setCellValue(title);}AtomicInteger datasAi = new AtomicInteger(1);Cell cell = null;for (EmployeeReportResult report : list) {Row dataRow = sheet.createRow(datasAi.getAndIncrement());//编号cell = dataRow.createCell(0);cell.setCellValue(report.getUserId());//姓名cell = dataRow.createCell(1);cell.setCellValue(report.getUsername());//手机cell = dataRow.createCell(2);cell.setCellValue(report.getMobile());//最高学历cell = dataRow.createCell(3);cell.setCellValue(report.getTheHighestDegreeOfEducation());//国家地区cell = dataRow.createCell(4);cell.setCellValue(report.getNationalArea());//护照号cell = dataRow.createCell(5);cell.setCellValue(report.getPassportNo());//籍贯cell = dataRow.createCell(6);cell.setCellValue(report.getNativePlace());//生日cell = dataRow.createCell(7);cell.setCellValue(report.getBirthday());//属相cell = dataRow.createCell(8);cell.setCellValue(report.getZodiac());//入职时间cell = dataRow.createCell(9);cell.setCellValue(report.getTimeOfEntry());//离职类型cell = dataRow.createCell(10);cell.setCellValue(report.getTypeOfTurnover());//离职原因cell = dataRow.createCell(11);cell.setCellValue(report.getReasonsForLeaving());//离职时间cell = dataRow.createCell(12);cell.setCellValue(report.getResignationTime());}String fileName = URLEncoder.encode(month+"人员信息.xlsx", "UTF-8");response.setContentType("application/octet-stream");response.setHeader("content-disposition", "attachment;filename=" + new String(fileName.getBytes("ISO8859-1")));response.setHeader("filename", fileName);workbook.write(response.getOutputStream());}
骚戴理解:AtomicInteger datasAi = new AtomicInteger(1);和datasAi.getAndIncrement()这是Java中使用AtomicInteger类来实现原子操作的例子。
- AtomicInteger datasAi = new AtomicInteger(1);创建了一个对象datasAi,它是AtomicInteger类型的并初始化为1。因此,如果多个线程同时访问这个对象进行操作,则可以保证对其进行操作时是线程安全的。
- datasAi.getAndIncrement() 可以理解为"获取并增加",它的作用是先获取当前值(即值为1),然后将值自增1,返回的结果是二者相加后的值。这个操作是原子性的,也就是说,在同时有多个线程操作该对象时,只有一个线程能够成功执行它,其他线程则被阻塞等待。
export方法里的response是父控制器BaseController里的定义的
package com.ihrm.common.controller;import com.ihrm.domain.system.response.ProfileResult;
import io.jsonwebtoken.Claims;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.ModelAttribute;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;public class BaseController {protected HttpServletRequest request;protected HttpServletResponse response;protected String companyId;private String userId;protected String companyName;protected Claims claims;//使用jwt方式获取
// @ModelAttribute
// public void setResAnReq(HttpServletRequest request,HttpServletResponse response) {
// this.request = request;
// this.response = response;
//
// Object obj = request.getAttribute("user_claims");
//
// if(obj != null) {
// this.claims = (Claims) obj;
// this.companyId = (String)claims.get("companyId");
// this.companyName = (String)claims.get("companyName");
// }
// }//使用shiro获取@ModelAttributepublic void setResAnReq(HttpServletRequest request,HttpServletResponse response) {this.request = request;this.response = response;//获取session中的安全数据Subject subject = SecurityUtils.getSubject();//1.subject获取所有的安全数据集合PrincipalCollection principals = subject.getPrincipals();if(principals != null && !principals.isEmpty()){//2.获取安全数据ProfileResult result = (ProfileResult)principals.getPrimaryPrincipal();this.companyId = result.getCompanyId();this.companyName = result.getCompany();this.userId = result.getUserId();System.out.println(companyId);}}}
- 添加service
//根据企业id和年月查询public List<EmployeeReportResult> findByReport(String companyId, String month) {return userCompanyPersonalDao.findByReport(companyId,month);}
- dao层实现
@Query(value = "select new com.ihrm.domain.employee.response.EmployeeReportResult(a,b) " +"FROM UserCompanyPersonal a LEFT JOIN EmployeeResignation b ON a.userId=b.userId WHERE a.companyId = ?1 AND a.timeOfEntry LIKE ?2 OR (b.resignationTime LIKE ?2)")List<EmployeeReportResult> findByReport(String companyId, String month);
骚戴理解:这是一个使用JPA自定义查询语句的例子,可以通过该语句查询符合条件的员工信息。
该查询语句包含了两个数据表 UserCompanyPersonal 和 EmployeeResignation,并依据它们中的特定字段获取数据。其中:
- new com.ihrm.domain.employee.response.EmployeeReportResult() 指定查询结果返回的对象为 EmployeeReportResult 对象。
- LEFT JOIN 表示使用左连接查询,根据a.userId=b.userId实现两张表的关联查询。
- ON a.userId=b.userId 为JOIN语句中的ON条件,表示UserCompanyPersonal和EmployeeResignation表中userId字段相等时关联查询两个表的员工信息。
- WHERE 子句中的 a.companyId = ?1 条件表示筛选companyId等于第一个参数的员工信息。
- AND 连接符号指定 后续筛选条件需要满足该条件,后面跟着的 a.timeOfEntry LIKE ?2 OR (b.resignationTime LIKE ?2) 是一个复杂的逻辑运算符,经过或与运算判断timeOfEntry或resignationTime是否匹配,具体如下:
- a.timeOfEntry LIKE ?2 表示筛选timeOfEntry字段类似于第二个参数的查询值(即时间模糊匹配),满足该条件之一即可。
- OR 表示或逻辑运算符。
- (b.resignationTime LIKE ?2) 表示筛选resignationTime字段类似于第二个参数的查询值。
因此,该自定义查询语句最终返回符合条件的实体集合,并且每个EmployeeReportResult对象包含UserCompanyPersonal和EmployeeResignation表中领取的一行数据。
SELECT*
FROMem_user_company_personal pLEFT JOIN em_resignation r ON p.user_id = r.user_id
WHEREp.company_id = 1 AND p.time_of_entry LIKE '2023-01%' OR r.resignation_time LIKE '2023-01%'
骚戴理解:这里我更喜欢转成sql语句更好理解,看着更舒服,需要注意的是2023-01%能够查出90多条数据,但是2023-1%只能查出3条数据,我一开始以为这两种写法查出来的结果是一样的!