소스 검색

加入easyExcel

wangliang 1 년 전
부모
커밋
907274cdf5
20개의 변경된 파일818개의 추가작업 그리고 50개의 파일을 삭제
  1. 11 0
      pom.xml
  2. 83 0
      sop-business/src/main/java/com/qmth/sop/business/bean/dto/SysUserImportDto.java
  3. 42 3
      sop-business/src/main/java/com/qmth/sop/business/entity/TBTask.java
  4. 12 0
      sop-business/src/main/java/com/qmth/sop/business/service/TBTaskService.java
  5. 3 2
      sop-business/src/main/java/com/qmth/sop/business/service/impl/BasicAttachmentServiceImpl.java
  6. 53 0
      sop-business/src/main/java/com/qmth/sop/business/service/impl/TBTaskServiceImpl.java
  7. 80 0
      sop-business/src/main/java/com/qmth/sop/business/templete/execute/AsyncSysUserDataImportService.java
  8. 44 0
      sop-business/src/main/java/com/qmth/sop/business/templete/importData/AsyncImportTaskTemplete.java
  9. 8 1
      sop-business/src/main/java/com/qmth/sop/business/templete/service/TaskLogicService.java
  10. 53 0
      sop-business/src/main/java/com/qmth/sop/business/templete/service/impl/TaskLogicServiceImpl.java
  11. 89 0
      sop-business/src/main/java/com/qmth/sop/business/util/ImportExportUtil.java
  12. 227 0
      sop-business/src/main/java/com/qmth/sop/business/util/excel/BasicExcelListener.java
  13. 53 0
      sop-business/src/main/java/com/qmth/sop/business/util/excel/BasicExcelRow.java
  14. 8 0
      sop-common/pom.xml
  15. 6 0
      sop-common/src/main/java/com/qmth/sop/common/contant/SystemConstant.java
  16. 0 33
      sop-common/src/main/java/com/qmth/sop/common/enums/UploadFileEnum.java
  17. 12 0
      sop-common/src/main/java/com/qmth/sop/common/util/FileStoreUtil.java
  18. 1 9
      sop-server/src/main/java/com/qmth/sop/server/api/SysController.java
  19. 33 2
      sop-server/src/main/java/com/qmth/sop/server/api/SysUserController.java
  20. BIN
      sop-server/src/main/resources/static/user.xlsx

+ 11 - 0
pom.xml

@@ -46,6 +46,7 @@
         <jasypt.version>3.0.3</jasypt.version>
         <httpmime.version>4.5.13</httpmime.version>
         <dom4j.version>2.1.4</dom4j.version>
+        <easyexcel.version>3.0.5</easyexcel.version>
     </properties>
 
     <dependencyManagement>
@@ -110,6 +111,16 @@
                 <artifactId>tools-device</artifactId>
                 <version>${qmth.boot.version}</version>
             </dependency>
+            <dependency>
+                <groupId>com.alibaba</groupId>
+                <artifactId>easyexcel</artifactId>
+                <version>${easyexcel.version}</version>
+            </dependency>
+<!--            <dependency>-->
+<!--                <groupId>com.qmth.boot</groupId>-->
+<!--                <artifactId>tools-poi</artifactId>-->
+<!--                <version>${qmth.boot.version}</version>-->
+<!--            </dependency>-->
 <!--            <dependency>-->
 <!--                <groupId>com.qmth.boot</groupId>-->
 <!--                <artifactId>core-solar</artifactId>-->

+ 83 - 0
sop-business/src/main/java/com/qmth/sop/business/bean/dto/SysUserImportDto.java

@@ -0,0 +1,83 @@
+package com.qmth.sop.business.bean.dto;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.alibaba.excel.annotation.write.style.ColumnWidth;
+import com.alibaba.excel.annotation.write.style.HeadFontStyle;
+import com.alibaba.excel.annotation.write.style.HeadStyle;
+import com.qmth.sop.business.util.excel.BasicExcelRow;
+
+import javax.validation.constraints.NotBlank;
+import java.io.Serializable;
+
+/**
+ * @Description: 用户导入Dto
+ * @Param:
+ * @return:
+ * @Author: wangliang
+ * @Date: 2023/7/31
+ */
+@ColumnWidth(value = 30)
+@HeadStyle(fillForegroundColor = 11)
+@HeadFontStyle(color = 1)
+public class SysUserImportDto extends BasicExcelRow implements Serializable {
+
+    //    @ExcelColumn(name = "姓名", index = 0, nullable = true)
+    @ExcelProperty(value = "姓名")
+    @NotBlank(message = "姓名不能为空")
+    private String name;
+
+    @ExcelProperty(value = "工号")
+    @NotBlank(message = "工号不能为空")
+    private String code;
+
+    @ExcelProperty(value = "手机号")
+    private String phoneNumber;
+
+    @ExcelProperty(value = "组织架构")
+    @NotBlank(message = "组织架构不能为空")
+    private String orgName;
+
+    @ExcelProperty(value = "角色")
+    @NotBlank(message = "角色不能为空")
+    private String roleName;
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public void setCode(String code) {
+        this.code = code;
+    }
+
+    public String getPhoneNumber() {
+        return phoneNumber;
+    }
+
+    public void setPhoneNumber(String phoneNumber) {
+        this.phoneNumber = phoneNumber;
+    }
+
+    public String getOrgName() {
+        return orgName;
+    }
+
+    public void setOrgName(String orgName) {
+        this.orgName = orgName;
+    }
+
+    public String getRoleName() {
+        return roleName;
+    }
+
+    public void setRoleName(String roleName) {
+        this.roleName = roleName;
+    }
+}

+ 42 - 3
sop-business/src/main/java/com/qmth/sop/business/entity/TBTask.java

@@ -3,6 +3,8 @@ package com.qmth.sop.business.entity;
 import com.fasterxml.jackson.databind.annotation.JsonSerialize;
 import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
 import com.qmth.sop.common.base.BaseEntity;
+import com.qmth.sop.common.contant.SystemConstant;
+import com.qmth.sop.common.enums.TaskResultEnum;
 import com.qmth.sop.common.enums.TaskStatusEnum;
 import com.qmth.sop.common.enums.TaskTypeEnum;
 import io.swagger.annotations.ApiModel;
@@ -40,7 +42,7 @@ public class TBTask extends BaseEntity implements Serializable {
     private Double progress;
 
     @ApiModelProperty(value = "数据结果,SUCCESS:成功,ERROR:失败")
-    private TaskTypeEnum result;
+    private TaskResultEnum result;
 
     @ApiModelProperty(value = "是否启用,false:停用,true:启用")
     private Boolean enable;
@@ -79,6 +81,43 @@ public class TBTask extends BaseEntity implements Serializable {
     @ApiModelProperty(value = "人工错误原因")
     private String errorMessage;
 
+    public TBTask() {
+
+    }
+
+    public TBTask(TaskTypeEnum type, Long entityId, TaskStatusEnum status, String summary, String importFileName, String importFilePath, Long createId) {
+        setId(SystemConstant.getDbUuid());
+        this.type = type;
+        this.entityId = entityId;
+        this.status = status;
+        this.summary = summary;
+        this.importFileName = importFileName;
+        this.importFilePath = importFilePath;
+        setCreateId(createId);
+        setCreateTime(System.currentTimeMillis());
+    }
+
+    public TBTask(TaskTypeEnum type, TaskStatusEnum status, String summary, String importFileName, String importFilePath, Long createId) {
+        setId(SystemConstant.getDbUuid());
+        this.type = type;
+        this.status = status;
+        this.summary = summary;
+        this.importFileName = importFileName;
+        this.importFilePath = importFilePath;
+        setCreateId(createId);
+        setCreateTime(System.currentTimeMillis());
+    }
+
+    public TBTask(TaskTypeEnum type, TaskStatusEnum status, BasicAttachment basicAttachment, Long createId) {
+        setId(SystemConstant.getDbUuid());
+        this.type = type;
+        this.status = status;
+        this.importFileName = basicAttachment.getName();
+        this.importFilePath = basicAttachment.getPath();
+        setCreateId(createId);
+        setCreateTime(System.currentTimeMillis());
+    }
+
     public TaskTypeEnum getType() {
         return type;
     }
@@ -119,11 +158,11 @@ public class TBTask extends BaseEntity implements Serializable {
         this.progress = progress;
     }
 
-    public TaskTypeEnum getResult() {
+    public TaskResultEnum getResult() {
         return result;
     }
 
-    public void setResult(TaskTypeEnum result) {
+    public void setResult(TaskResultEnum result) {
         this.result = result;
     }
 

+ 12 - 0
sop-business/src/main/java/com/qmth/sop/business/service/TBTaskService.java

@@ -2,6 +2,10 @@ package com.qmth.sop.business.service;
 
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.qmth.sop.business.entity.TBTask;
+import com.qmth.sop.common.enums.TaskTypeEnum;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.Map;
 
 /**
  * <p>
@@ -13,4 +17,12 @@ import com.qmth.sop.business.entity.TBTask;
  */
 public interface TBTaskService extends IService<TBTask> {
 
+    /**
+     * 保存任务
+     *
+     * @param file
+     * @param taskTypeEnum
+     * @return
+     */
+    Map<String, Object> saveTask(MultipartFile file, TaskTypeEnum taskTypeEnum) throws Exception;
 }

+ 3 - 2
sop-business/src/main/java/com/qmth/sop/business/service/impl/BasicAttachmentServiceImpl.java

@@ -104,7 +104,7 @@ public class BasicAttachmentServiceImpl extends ServiceImpl<BasicAttachmentMappe
         BasicAttachment basicAttachment = null;
         AttachmentInfoDto attachmentInfoDto = this.validateAttachment(file);
         if (Objects.nonNull(attachmentInfoDto)) {
-            StringJoiner stringJoiner = SystemConstant.getDirName(type, true).add(file.getOriginalFilename());
+            StringJoiner stringJoiner = SystemConstant.getDirName(type, true).add(SystemConstant.getNanoId()).add(attachmentInfoDto.getFormat());
             JSONObject jsonObject = new JSONObject();
             jsonObject.put(SystemConstant.TYPE, fileStoreUtil.uploadFileEnumIsOss(type) ? SystemConstant.OSS : SystemConstant.LOCAL);
             fileStoreUtil.ossUpload(stringJoiner.toString(), file.getInputStream(), attachmentInfoDto.getMd5(), type.getFssType());
@@ -114,6 +114,7 @@ public class BasicAttachmentServiceImpl extends ServiceImpl<BasicAttachmentMappe
             basicAttachment = new BasicAttachment(jsonObject.toJSONString(), attachmentInfoDto, sysUser.getId());
             this.save(basicAttachment);
         }
+        Optional.ofNullable(basicAttachment).orElseThrow(() -> ExceptionResultEnum.ATTACHMENT_ERROR.exception());
         return basicAttachment;
     }
 
@@ -140,7 +141,7 @@ public class BasicAttachmentServiceImpl extends ServiceImpl<BasicAttachmentMappe
     public String filePreview(String path) throws Exception {
         String url = null;
         JSONObject jsonObject = JSONObject.parseObject(path);
-        UploadFileEnum uploadFileEnum = UploadFileEnum.valueOf((String) jsonObject.get(SystemConstant.UPLOAD_TYPE));
+        UploadFileEnum uploadFileEnum = Enum.valueOf(UploadFileEnum.class, (String) jsonObject.get(SystemConstant.UPLOAD_TYPE));
         String attachmentType = (String) jsonObject.get(SystemConstant.TYPE);
         String filePath = (String) jsonObject.get(SystemConstant.PATH);
         if (Objects.equals(attachmentType, SystemConstant.LOCAL)) {

+ 53 - 0
sop-business/src/main/java/com/qmth/sop/business/service/impl/TBTaskServiceImpl.java

@@ -1,10 +1,27 @@
 package com.qmth.sop.business.service.impl;
 
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.qmth.boot.api.exception.ApiException;
+import com.qmth.sop.business.entity.BasicAttachment;
+import com.qmth.sop.business.entity.SysUser;
 import com.qmth.sop.business.entity.TBTask;
 import com.qmth.sop.business.mapper.TBTaskMapper;
+import com.qmth.sop.business.service.BasicAttachmentService;
 import com.qmth.sop.business.service.TBTaskService;
+import com.qmth.sop.common.contant.SystemConstant;
+import com.qmth.sop.common.enums.TaskStatusEnum;
+import com.qmth.sop.common.enums.TaskTypeEnum;
+import com.qmth.sop.common.enums.UploadFileEnum;
+import com.qmth.sop.common.util.ResultUtil;
+import com.qmth.sop.common.util.ServletUtil;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
 
 /**
  * <p>
@@ -17,4 +34,40 @@ import org.springframework.stereotype.Service;
 @Service
 public class TBTaskServiceImpl extends ServiceImpl<TBTaskMapper, TBTask> implements TBTaskService {
 
+    @Resource
+    BasicAttachmentService basicAttachmentService;
+
+    /**
+     * 保存任务
+     *
+     * @param file
+     * @param taskTypeEnum
+     * @return
+     */
+    @Override
+    @Transactional
+    public Map<String, Object> saveTask(MultipartFile file, TaskTypeEnum taskTypeEnum) throws Exception {
+        BasicAttachment basicAttachment = null;
+        Map<String, Object> map = new HashMap<>();
+        try {
+            basicAttachment = basicAttachmentService.saveAttachment(file, UploadFileEnum.FILE);
+            SysUser sysUser = (SysUser) ServletUtil.getRequestUser();
+            TBTask tbTask = new TBTask(taskTypeEnum,
+                    TaskStatusEnum.INIT,
+                    basicAttachment,
+                    Objects.nonNull(sysUser) ? sysUser.getId() : null);
+            this.save(tbTask);
+            map.computeIfAbsent(SystemConstant.USER, v -> sysUser);
+            map.computeIfAbsent(SystemConstant.TASK, v -> tbTask);
+        } catch (Exception e) {
+            log.error(SystemConstant.LOG_ERROR, e);
+            basicAttachmentService.deleteAttachment(basicAttachment);
+            if (e instanceof ApiException) {
+                ResultUtil.error((ApiException) e, e.getMessage());
+            } else {
+                ResultUtil.error(e.getMessage());
+            }
+        }
+        return map;
+    }
 }

+ 80 - 0
sop-business/src/main/java/com/qmth/sop/business/templete/execute/AsyncSysUserDataImportService.java

@@ -0,0 +1,80 @@
+package com.qmth.sop.business.templete.execute;
+
+import cn.hutool.core.date.DateUtil;
+import com.qmth.boot.api.exception.ApiException;
+import com.qmth.sop.business.bean.dto.SysUserImportDto;
+import com.qmth.sop.business.entity.TBTask;
+import com.qmth.sop.business.service.TBTaskService;
+import com.qmth.sop.business.templete.importData.AsyncImportTaskTemplete;
+import com.qmth.sop.business.templete.service.TaskLogicService;
+import com.qmth.sop.business.util.excel.BasicExcelListener;
+import com.qmth.sop.common.contant.SystemConstant;
+import com.qmth.sop.common.enums.TaskResultEnum;
+import com.qmth.sop.common.enums.TaskStatusEnum;
+import com.qmth.sop.common.util.Result;
+import com.qmth.sop.common.util.ResultUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.util.LinkedMultiValueMap;
+
+import javax.annotation.Resource;
+import java.text.MessageFormat;
+import java.util.Date;
+import java.util.Map;
+import java.util.Objects;
+import java.util.StringJoiner;
+
+/**
+ * @Description: 系统用户批量导入
+ * @Author: CaoZixuan
+ * @Date: 2021-08-12
+ */
+@Service
+public class AsyncSysUserDataImportService extends AsyncImportTaskTemplete {
+    private final static Logger log = LoggerFactory.getLogger(AsyncSysUserDataImportService.class);
+    public static final String OBJ_TITLE = "用户基本信息";
+
+    @Resource
+    TaskLogicService taskLogicService;
+
+    @Resource
+    TBTaskService tbTaskService;
+
+    @Override
+    public Result importTask(Map<String, Object> map) throws Exception {
+        TBTask tbTask = (TBTask) map.get(SystemConstant.TASK);
+        StringJoiner stringJoinerSummary = new StringJoiner("\n").add(MessageFormat.format("{0}{1}{2}", DateUtil.format(new Date(), SystemConstant.DEFAULT_DATE_PATTERN), BEGIN_TITLE, OBJ_TITLE));
+        tbTask.setStatus(TaskStatusEnum.RUNNING);
+        tbTaskService.updateById(tbTask);
+        try {
+            tbTask.setSummary(stringJoinerSummary.toString());
+            // 执行导入基础用户数据
+            Map<String, Object> result = taskLogicService.executeImportSysUserLogic(map);
+            LinkedMultiValueMap<String, SysUserImportDto> sysUserImportDtoLinkedMultiValueMap = (LinkedMultiValueMap<String, SysUserImportDto>) result.get(SystemConstant.EXCEL_DATA);
+            String errorList = (String) result.get(SystemConstant.EXCEL_DATA_ERROR);
+            int successSize = Objects.nonNull(sysUserImportDtoLinkedMultiValueMap.get(BasicExcelListener.SUCCESS)) ? sysUserImportDtoLinkedMultiValueMap.get(BasicExcelListener.SUCCESS).size() : 0;
+            int errorSize = Objects.nonNull(sysUserImportDtoLinkedMultiValueMap.get(BasicExcelListener.ERROR)) ? sysUserImportDtoLinkedMultiValueMap.get(BasicExcelListener.ERROR).size() : 0;
+            stringJoinerSummary.add(MessageFormat.format("{0}{1}{2}{3}{4}{5}{6}{7}", DateUtil.format(new Date(), SystemConstant.DEFAULT_DATE_PATTERN), FINISH_TITLE, (successSize + errorSize), FINISH_TOTAL_SIZE, successSize, FINISH_SUCCESS_SIZE, errorSize, FINISH_ERROR_SIZE));
+            if (Objects.nonNull(errorList) && !Objects.equals(errorList, "")) {
+                tbTask.setResult(TaskResultEnum.ERROR);
+                stringJoinerSummary.add(MessageFormat.format("{0}{1}", ERROR_DATA, errorList));
+            } else {
+                tbTask.setResult(TaskResultEnum.SUCCESS);
+            }
+        } catch (Exception e) {
+            log.error(SystemConstant.LOG_ERROR, e);
+            stringJoinerSummary.add(MessageFormat.format("{0}{1}{2}{3}", DateUtil.format(new Date(), SystemConstant.DEFAULT_DATE_PATTERN), EXCEPTION_TITLE, EXCEPTION_DATA, e.getMessage()));
+            tbTask.setResult(TaskResultEnum.ERROR);
+            if (e instanceof ApiException) {
+                ResultUtil.error((ApiException) e, e.getMessage());
+            } else {
+                ResultUtil.error(e.getMessage());
+            }
+        } finally {//生成txt文件
+            tbTask.setSummary(stringJoinerSummary.toString());
+            super.createTxt(tbTask);
+        }
+        return ResultUtil.ok(map);
+    }
+}

+ 44 - 0
sop-business/src/main/java/com/qmth/sop/business/templete/importData/AsyncImportTaskTemplete.java

@@ -1,12 +1,26 @@
 package com.qmth.sop.business.templete.importData;
 
+import cn.hutool.core.date.DateUtil;
+import com.alibaba.fastjson.JSONObject;
+import com.qmth.boot.api.exception.ApiException;
+import com.qmth.sop.business.entity.TBTask;
+import com.qmth.sop.business.service.TBTaskService;
+import com.qmth.sop.business.util.ImportExportUtil;
+import com.qmth.sop.common.contant.SpringContextHolder;
+import com.qmth.sop.common.contant.SystemConstant;
+import com.qmth.sop.common.enums.TaskResultEnum;
+import com.qmth.sop.common.enums.TaskStatusEnum;
 import com.qmth.sop.common.util.Result;
+import com.qmth.sop.common.util.ResultUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.scheduling.annotation.Async;
 
 import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.Date;
 import java.util.Map;
+import java.util.StringJoiner;
 
 /**
  * @Description: 异步导入模版
@@ -25,6 +39,7 @@ public abstract class AsyncImportTaskTemplete {
     public static final String FINISH_SUCCESS_SIZE = "条数据,失败";
     public static final String FINISH_ERROR_SIZE = "条数据";
     public static final String EXCEPTION_CREATE_TXT_TITLE = "->创建导出日志时发生异常!";
+    public static final String ERROR_DATA = "->失败数据如下:";
 
     /**
      * 异步导入任务
@@ -35,4 +50,33 @@ public abstract class AsyncImportTaskTemplete {
      */
     @Async
     public abstract Result importTask(Map<String, Object> map) throws IOException, Exception;
+
+    /**
+     * 创建txt文件
+     *
+     * @param tbTask
+     */
+    public void createTxt(TBTask tbTask) throws IOException {
+        try {
+            ImportExportUtil importExportUtil = SpringContextHolder.getBean(ImportExportUtil.class);
+            JSONObject json = importExportUtil.createTxt(tbTask.getSummary());
+            tbTask.setReportFilePath(json.toJSONString());
+        } catch (Exception e) {
+            log.error(SystemConstant.LOG_ERROR, e);
+            StringJoiner stringJoinerSummary = new StringJoiner("").add(tbTask.getSummary()).add("\n");
+            stringJoinerSummary.add(MessageFormat.format("{0}{1}{2}{3}", DateUtil.format(new Date(), SystemConstant.DEFAULT_DATE_PATTERN), EXCEPTION_CREATE_TXT_TITLE, EXCEPTION_DATA, e.getMessage()));
+
+            tbTask.setSummary(stringJoinerSummary.toString());
+            tbTask.setResult(TaskResultEnum.ERROR);
+            if (e instanceof ApiException) {
+                ResultUtil.error((ApiException) e, e.getMessage());
+            } else {
+                ResultUtil.error(e.getMessage());
+            }
+        } finally {
+            TBTaskService tbTaskService = SpringContextHolder.getBean(TBTaskService.class);
+            tbTask.setStatus(TaskStatusEnum.FINISH);
+            tbTaskService.updateById(tbTask);
+        }
+    }
 }

+ 8 - 1
sop-business/src/main/java/com/qmth/sop/business/templete/service/TaskLogicService.java

@@ -1,6 +1,5 @@
 package com.qmth.sop.business.templete.service;
 
-import java.io.IOException;
 import java.util.Map;
 
 /**
@@ -12,4 +11,12 @@ import java.util.Map;
  */
 public interface TaskLogicService {
 
+    /**
+     * 处理导入用户数据
+     *
+     * @param map 数据源
+     * @return 结果
+     * @throws Exception 异常
+     */
+    Map<String, Object> executeImportSysUserLogic(Map<String, Object> map) throws Exception;
 }

+ 53 - 0
sop-business/src/main/java/com/qmth/sop/business/templete/service/impl/TaskLogicServiceImpl.java

@@ -1,9 +1,23 @@
 package com.qmth.sop.business.templete.service.impl;
 
+import com.alibaba.excel.EasyExcel;
+import com.qmth.sop.business.bean.dto.SysUserImportDto;
+import com.qmth.sop.business.entity.TBTask;
 import com.qmth.sop.business.templete.service.TaskLogicService;
+import com.qmth.sop.business.util.ImportExportUtil;
+import com.qmth.sop.business.util.excel.BasicExcelListener;
+import com.qmth.sop.common.contant.SystemConstant;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.LinkedMultiValueMap;
+
+import javax.annotation.Resource;
+import java.io.InputStream;
+import java.util.Map;
+import java.util.Objects;
+import java.util.StringJoiner;
 
 /**
  * @Description: 任务处理逻辑impl
@@ -16,4 +30,43 @@ import org.springframework.stereotype.Service;
 public class TaskLogicServiceImpl implements TaskLogicService {
     private final static Logger log = LoggerFactory.getLogger(TaskLogicServiceImpl.class);
 
+    @Resource
+    ImportExportUtil importExportUtil;
+
+    /**
+     * 处理导入用户数据
+     *
+     * @param map 数据源
+     * @return
+     * @throws Exception
+     */
+    @Override
+    @Transactional
+    public Map<String, Object> executeImportSysUserLogic(Map<String, Object> map) throws Exception {
+        InputStream inputStream = null;
+        try {
+            TBTask tbTask = (TBTask) map.get(SystemConstant.TASK);
+            inputStream = importExportUtil.getUploadFileInputStream(tbTask);
+            LinkedMultiValueMap<String, SysUserImportDto> sysUserImportDtoLinkedMultiValueMap = new LinkedMultiValueMap<>();
+            StringJoiner errorData = new StringJoiner("");
+            EasyExcel.read(inputStream, SysUserImportDto.class, new BasicExcelListener<SysUserImportDto>() {
+
+                @Override
+                public void handle(LinkedMultiValueMap<String, SysUserImportDto> dataList, StringJoiner errorDataSj) {
+                    sysUserImportDtoLinkedMultiValueMap.addAll(dataList);
+                    errorData.add(errorDataSj.toString());
+                }
+            }).headRowNumber(2).sheet(0).doRead();
+
+            map.computeIfAbsent(SystemConstant.EXCEL_DATA, v -> sysUserImportDtoLinkedMultiValueMap);
+            map.computeIfAbsent(SystemConstant.EXCEL_DATA_ERROR, v -> errorData.toString());
+
+            //处理数据逻辑
+        } finally {
+            if (Objects.nonNull(inputStream)) {
+                inputStream.close();
+            }
+        }
+        return map;
+    }
 }

+ 89 - 0
sop-business/src/main/java/com/qmth/sop/business/util/ImportExportUtil.java

@@ -0,0 +1,89 @@
+package com.qmth.sop.business.util;
+
+import com.alibaba.fastjson.JSONObject;
+import com.qmth.sop.business.cache.CommonCacheService;
+import com.qmth.sop.business.entity.SysConfig;
+import com.qmth.sop.business.entity.TBTask;
+import com.qmth.sop.common.contant.SystemConstant;
+import com.qmth.sop.common.enums.UploadFileEnum;
+import com.qmth.sop.common.util.FileStoreUtil;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.util.Objects;
+import java.util.StringJoiner;
+
+/**
+ * @Description: 导入导出util
+ * @Param:
+ * @return:
+ * @Author: wangliang
+ * @Date: 2023/7/31
+ */
+@Component
+public class ImportExportUtil {
+
+    @Resource
+    FileStoreUtil fileStoreUtil;
+
+    @Resource
+    CommonCacheService commonCacheService;
+
+    /**
+     * 创建txt文件
+     *
+     * @param message
+     * @return
+     * @throws Exception
+     */
+    public JSONObject createTxt(String message) throws Exception {
+        File txtFileTemp = null;
+        try {
+            StringJoiner stringJoiner = SystemConstant.getDirName(UploadFileEnum.FILE, true);
+            stringJoiner.add(SystemConstant.getNanoId()).add(SystemConstant.TXT_PREFIX);
+
+            String txtDirName = stringJoiner.toString();
+            txtFileTemp = SystemConstant.getFileTempVar(SystemConstant.TXT_PREFIX);
+
+            String charset = SystemConstant.CHARSET_NAME;
+            SysConfig sysConfig = commonCacheService.addSysConfigCache(SystemConstant.SYS_CONFIG_KEY_CHARSETS);
+            if (Objects.nonNull(sysConfig) && StringUtils.isNotBlank(sysConfig.getConfigValue())) {
+                charset = sysConfig.getConfigValue();
+            }
+            IOUtils.write(message.getBytes(charset), new FileOutputStream(txtFileTemp));
+
+            String txtFileMd5 = DigestUtils.md5Hex(new FileInputStream(txtFileTemp));
+            fileStoreUtil.ossUpload(txtDirName, txtFileTemp, txtFileMd5, UploadFileEnum.FILE.getFssType());
+            JSONObject json = new JSONObject();
+            json.put(SystemConstant.PATH, stringJoiner.toString());
+            json.put(SystemConstant.TYPE, fileStoreUtil.uploadFileEnumIsOss(UploadFileEnum.FILE) ? SystemConstant.OSS : SystemConstant.LOCAL);
+            json.put(SystemConstant.UPLOAD_TYPE, UploadFileEnum.FILE);
+            return json;
+        } finally {
+            if (Objects.nonNull(txtFileTemp)) {
+                txtFileTemp.delete();
+            }
+        }
+    }
+
+    /**
+     * 获取上传的文件
+     *
+     * @param tbTask
+     * @return
+     * @throws Exception
+     */
+    public InputStream getUploadFileInputStream(TBTask tbTask) throws Exception {
+        JSONObject jsonObject = JSONObject.parseObject(tbTask.getImportFilePath());
+        String path = (String) jsonObject.get(SystemConstant.PATH);
+        UploadFileEnum uploadType = Enum.valueOf(UploadFileEnum.class, (String) jsonObject.get(SystemConstant.UPLOAD_TYPE));
+        return fileStoreUtil.ossDownloadIs(path, uploadType.getFssType());
+    }
+}

+ 227 - 0
sop-business/src/main/java/com/qmth/sop/business/util/excel/BasicExcelListener.java

@@ -0,0 +1,227 @@
+package com.qmth.sop.business.util.excel;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.alibaba.excel.context.AnalysisContext;
+import com.alibaba.excel.event.AnalysisEventListener;
+import com.alibaba.excel.metadata.CellExtra;
+import com.alibaba.fastjson.JSONObject;
+import org.hibernate.validator.constraints.Length;
+import org.hibernate.validator.constraints.Range;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.util.LinkedMultiValueMap;
+
+import javax.validation.constraints.*;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.math.BigDecimal;
+import java.util.*;
+
+/**
+ * @Description: easyexcel监听
+ * @Param:
+ * @return:
+ * @Author: wangliang
+ * @Date: 2022/5/14
+ */
+public abstract class BasicExcelListener<T> extends AnalysisEventListener<T> {
+    private final static Logger log = LoggerFactory.getLogger(BasicExcelListener.class);
+
+    /**
+     * 批处理阈值2000
+     */
+    private static int BATCH_COUNT = 2000;
+    public static final String SUCCESS = "success";
+    public static final String ERROR = "error";
+    private LinkedMultiValueMap<String, T> list;
+    private StringJoiner errorDataSj = new StringJoiner("\n");
+
+    public BasicExcelListener() {
+        this.list = new LinkedMultiValueMap<>(BATCH_COUNT);
+    }
+
+    public BasicExcelListener(int batchCount) {
+        BATCH_COUNT = batchCount;
+        this.list = new LinkedMultiValueMap<>(BATCH_COUNT);
+    }
+
+    public abstract void handle(LinkedMultiValueMap<String, T> dataList, StringJoiner errorData);
+
+//    public abstract void errorData(List<String> errorDataList);
+
+    @Override
+    public void invoke(T o, AnalysisContext analysisContext) {
+//        if (validData(o, analysisContext) || (analysisContext.readRowHolder().getRowIndex() == 2001 || analysisContext.readRowHolder().getRowIndex() == 7001)) {
+        if (validData(o, analysisContext)) {
+            list.add(ERROR, o);
+        } else {
+            list.add(SUCCESS, o);
+        }
+        if (list.size() >= BATCH_COUNT) {
+            handle(list, errorDataSj);
+            list.clear();
+        }
+    }
+
+//    @Override
+//    public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
+//        log.info("表头:{}", JSONObject.toJSONString(headMap));
+//    }
+
+    @Override
+    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
+        log.info("表头:{}", JSONObject.toJSONString(headMap));
+    }
+
+    @Override
+    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
+        log.info("所有数据解析完成!");
+        if (Objects.nonNull(this.list.get(ERROR)) && this.list.get(ERROR).size() > 0) {
+            List<BasicExcelRow> basicExcelRowList = (List<BasicExcelRow>) this.list.get(ERROR);
+            for (BasicExcelRow basicExcelRow : basicExcelRowList) {
+                errorDataSj.add("第" + basicExcelRow.getSheet() + "个sheet第" + (basicExcelRow.getRow() - 1) + "行" + basicExcelRow.getErrorMessage().toString());
+            }
+        }
+        handle(list, errorDataSj);
+    }
+
+    @Override
+    public void onException(Exception exception, AnalysisContext context) {
+        log.info("onException is come in!");
+    }
+
+    @Override
+    public void extra(CellExtra extra, AnalysisContext context) {
+        log.info("extra is come in!");
+    }
+
+    private void extendBasicExcelField(T o, AnalysisContext analysisContext, List<String> errorMessage) {
+        try {
+            Field rowField = o.getClass().getField("row");
+            rowField.setAccessible(true);
+            rowField.set(o, analysisContext.readRowHolder().getRowIndex());
+
+            Field sheetField = o.getClass().getField("sheet");
+            sheetField.setAccessible(true);
+            sheetField.set(o, analysisContext.readSheetHolder().getSheetNo() + 1);
+
+            Field errorMessageMapField = o.getClass().getField("errorMessage");
+            errorMessageMapField.setAccessible(true);
+            errorMessageMapField.set(o, errorMessage);
+        } catch (NoSuchFieldException | IllegalAccessException e) {
+            log.error("请求出错:", e);
+        }
+    }
+
+    private boolean validData(T o, AnalysisContext analysisContext) {
+        List<String> errorMessage = new ArrayList<>();
+        Field[] fields = o.getClass().getDeclaredFields();
+        Object object = null;
+        for (int i = 0; i < fields.length; i++) {
+            fields[i].setAccessible(true);
+            try {
+                object = fields[i].get(o);
+            } catch (IllegalAccessException e) {
+                e.printStackTrace();
+            }
+            ExcelProperty excelProperty = fields[i].getDeclaredAnnotation(ExcelProperty.class);
+            if (Objects.isNull(excelProperty)) {
+                continue;
+            }
+            Annotation[] annotations = fields[i].getDeclaredAnnotations();
+            NotBlank notBlank = null;
+            Length length = null;
+            Min min = null;
+            Max max = null;
+            DecimalMin decimalMin = null;
+            DecimalMax decimalMax = null;
+            NotNull notNull = null;
+            Null isnull = null;
+            NotEmpty notEmpty = null;
+            Size size = null;
+            Range range = null;
+            AssertTrue assertTrue = null;
+            AssertFalse assertFalse = null;
+            for (Annotation annotation : annotations) {
+                if (annotation instanceof NotBlank) {
+                    notBlank = (NotBlank) annotation;
+                    if (Objects.isNull(object) || Objects.equals(object, " ")) {
+                        errorMessage.add("列名[" + excelProperty.value()[0] + "]:" + notBlank.message());
+                    }
+                } else if (annotation instanceof Length) {
+                    length = (Length) annotation;
+                    if (Objects.nonNull(object) && (object.toString().length() < length.min() || object.toString().length() > length.max())) {
+                        errorMessage.add("列名[" + excelProperty.value()[0] + "]:" + length.message());
+                    }
+                } else if (annotation instanceof Min) {
+                    min = (Min) annotation;
+                    if (Objects.nonNull(object) && Long.parseLong(object.toString()) < min.value()) {
+                        errorMessage.add("列名[" + excelProperty.value()[0] + "]:" + min.message());
+                    }
+                } else if (annotation instanceof Max) {
+                    max = (Max) annotation;
+                    if (Objects.nonNull(object) && Long.parseLong(object.toString()) > max.value()) {
+                        errorMessage.add("列名[" + excelProperty.value()[0] + "]:" + max.message());
+                    }
+                } else if (annotation instanceof DecimalMin) {
+                    decimalMin = (DecimalMin) annotation;
+                    if (Objects.nonNull(object) && new BigDecimal(object.toString()).compareTo(new BigDecimal(decimalMin.value())) == -1) {
+                        errorMessage.add("列名[" + excelProperty.value()[0] + "]:" + decimalMin.message());
+                    }
+                } else if (annotation instanceof DecimalMax) {
+                    decimalMax = (DecimalMax) annotation;
+                    if (Objects.nonNull(object) && new BigDecimal(object.toString()).compareTo(new BigDecimal(decimalMax.value())) == 1) {
+                        errorMessage.add("列名[" + excelProperty.value()[0] + "]:" + decimalMax.message());
+                    }
+                } else if (annotation instanceof NotNull) {
+                    notNull = (NotNull) annotation;
+                    if (Objects.isNull(object)) {
+                        errorMessage.add("列名[" + excelProperty.value()[0] + "]:" + notNull.message());
+                    }
+                } else if (annotation instanceof Null) {
+                    isnull = (Null) annotation;
+                    if (Objects.nonNull(object)) {
+                        errorMessage.add("列名[" + excelProperty.value()[0] + "]:" + isnull.message());
+                    }
+                } else if (annotation instanceof NotEmpty) {
+                    notEmpty = (NotEmpty) annotation;
+                    if (Objects.isNull(object)) {
+                        errorMessage.add("列名[" + excelProperty.value()[0] + "]:" + notEmpty.message());
+                    }
+                } else if (annotation instanceof Size) {
+                    size = (Size) annotation;
+                    if (Objects.nonNull(object) && (object.toString().length() < size.min() || object.toString().length() > size.max())) {
+                        errorMessage.add("列名[" + excelProperty.value()[0] + "]:" + size.message());
+                    }
+                } else if (annotation instanceof Range) {
+                    range = (Range) annotation;
+                    if (Objects.nonNull(object) && (Long.parseLong(object.toString()) < range.min() || Long.parseLong(object.toString()) > range.max())) {
+                        errorMessage.add("列名[" + excelProperty.value()[0] + "]:" + range.message());
+                    }
+                } else if (annotation instanceof AssertTrue) {
+                    assertTrue = (AssertTrue) annotation;
+                    if (Objects.nonNull(object) && Boolean.valueOf(object.toString())) {
+                        errorMessage.add("列名[" + excelProperty.value()[0] + "]:" + assertTrue.message());
+                    }
+                } else if (annotation instanceof AssertFalse) {
+                    assertFalse = (AssertFalse) annotation;
+                    if (Objects.nonNull(object) && !Boolean.valueOf(object.toString())) {
+                        errorMessage.add("列名[" + excelProperty.value()[0] + "]:" + assertFalse.message());
+                    }
+                }
+            }
+            if (errorMessage.size() > 0) {
+                extendBasicExcelField(o, analysisContext, errorMessage);
+            }
+        }
+        return errorMessage.size() > 0 ? true : false;
+    }
+
+    //可重写的方法:
+//	void invoke(T data, AnalysisContext context); //处理一行数据
+//	void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) //处理表头的一行数据
+//	void extra(CellExtra extra, AnalysisContext context); //获取单元格的额外信息
+//	void doAfterAllAnalysed(AnalysisContext context) //全部读取结束后的操作
+//	boolean hasNext(AnalysisContext context); //是否读取下一行
+//	void onException(Exception exception, AnalysisContext context) //发生异常时调用
+}

+ 53 - 0
sop-business/src/main/java/com/qmth/sop/business/util/excel/BasicExcelRow.java

@@ -0,0 +1,53 @@
+package com.qmth.sop.business.util.excel;
+
+import com.alibaba.excel.annotation.ExcelIgnore;
+import io.swagger.annotations.ApiModelProperty;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * @Description: easyexcel基础列
+ * @Param:
+ * @return:
+ * @Author: wangliang
+ * @Date: 2022/5/14
+ */
+public class BasicExcelRow implements Serializable {
+
+    @ApiModelProperty(value = "sheet页")
+    @ExcelIgnore
+    public Integer sheet;
+
+    @ApiModelProperty(value = "行号")
+    @ExcelIgnore
+    public Integer row;
+
+    @ApiModelProperty(value = "列名和错误原因")
+    @ExcelIgnore
+    public List<String> errorMessage;
+
+    public Integer getSheet() {
+        return sheet;
+    }
+
+    public void setSheet(Integer sheet) {
+        this.sheet = sheet;
+    }
+
+    public Integer getRow() {
+        return row;
+    }
+
+    public void setRow(Integer row) {
+        this.row = row;
+    }
+
+    public List<String> getErrorMessage() {
+        return errorMessage;
+    }
+
+    public void setErrorMessage(List<String> errorMessage) {
+        this.errorMessage = errorMessage;
+    }
+}

+ 8 - 0
sop-common/pom.xml

@@ -40,8 +40,16 @@
         </dependency>
 <!--        <dependency>-->
 <!--            <groupId>com.qmth.boot</groupId>-->
+<!--            <artifactId>tools-poi</artifactId>-->
+<!--        </dependency>-->
+<!--        <dependency>-->
+<!--            <groupId>com.qmth.boot</groupId>-->
 <!--            <artifactId>core-solar</artifactId>-->
 <!--        </dependency>-->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>easyexcel</artifactId>
+        </dependency>
         <dependency>
             <groupId>com.qmth.boot</groupId>
             <artifactId>core-logging</artifactId>

+ 6 - 0
sop-common/src/main/java/com/qmth/sop/common/contant/SystemConstant.java

@@ -37,6 +37,7 @@ public class SystemConstant {
     public static final String UPDATE_TIME = "updateTime";
     public static final String SESSION = "session:";
     public static final String USER = "account";
+    public static final String TASK = "task";
     public static final String PARAMS = "params";
     public static final String ORG = "org";
     public static final String MD5 = "MD5";
@@ -67,6 +68,10 @@ public class SystemConstant {
     public static final String OSS = "oss";
     public static final String UPLOAD_TYPE = "uploadType";
     public static final String TEMP = "temp";
+    public static final String DEFAULT_DATE_PATTERN = "yyyy-MM-dd HH:mm:ss";
+    public static final String TXT_PREFIX = ".txt";
+    public static final String EXCEL_DATA = "excelData";
+    public static final String EXCEL_DATA_ERROR = "excelDataError";
 
     /**
      * 系统配置
@@ -93,6 +98,7 @@ public class SystemConstant {
     public static final String PREFIX_URL_WIDGET = "/admin/widget";
     public static final String PREFIX_TEST_FLOW = "/test/flow";
     public static final String PREFIX_URL_COMMON = "/admin/common";
+    public static final String PREFIX_URL_USER = "/admin/user";
 
     /**
      * 缓存配置

+ 0 - 33
sop-common/src/main/java/com/qmth/sop/common/enums/UploadFileEnum.java

@@ -1,7 +1,5 @@
 package com.qmth.sop.common.enums;
 
-import java.util.Objects;
-
 /**
  * @Description: 上传文件类型
  * @Param:
@@ -34,35 +32,4 @@ public enum UploadFileEnum {
     public String getFssType() {
         return fssType;
     }
-
-
-    /**
-     * 状态转换 toName
-     *
-     * @param title
-     * @return
-     */
-    public static String convertToName(String title) {
-        for (UploadFileEnum e : UploadFileEnum.values()) {
-            if (Objects.equals(title, e.getTitle())) {
-                return e.name();
-            }
-        }
-        return null;
-    }
-
-    /**
-     * 状态转换 toName
-     *
-     * @param title
-     * @return
-     */
-    public static String convertToFssType(String title) {
-        for (UploadFileEnum e : UploadFileEnum.values()) {
-            if (Objects.equals(title.toLowerCase(), e.getTitle())) {
-                return e.getFssType();
-            }
-        }
-        return null;
-    }
 }

+ 12 - 0
sop-common/src/main/java/com/qmth/sop/common/util/FileStoreUtil.java

@@ -145,6 +145,18 @@ public class FileStoreUtil {
         return fileService.getFileStore(type).delete(objectName);
     }
 
+    /**
+     * 从文件存储上下载文件到InputStream
+     *
+     * @param objectName 文件地址
+     * @param type       fileStore类型
+     * @throws Exception 异常
+     */
+    public InputStream ossDownloadIs(String objectName, String type) throws Exception {
+        log.info("oss Download is come in");
+        return fileService.getFileStore(type).read(objectName);
+    }
+
     /**
      * 获取文件访问url
      *

+ 1 - 9
sop-server/src/main/java/com/qmth/sop/server/api/SysController.java

@@ -110,11 +110,6 @@ public class SysController {
         return ResultUtil.ok(sysUserService.login(login.getPassword(), sysUser, AppSourceEnum.SYSTEM));
     }
 
-    /**
-     * 登出
-     *
-     * @return
-     */
     @ApiOperation(value = "登出")
     @RequestMapping(value = "/logout", method = RequestMethod.POST)
     @ApiResponses({@ApiResponse(code = 200, message = "返回信息", response = Object.class)})
@@ -132,12 +127,9 @@ public class SysController {
         BasicAttachment basicAttachment = null;
         try {
             basicAttachment = basicAttachmentService.saveAttachment(file, type);
-            Optional.ofNullable(basicAttachment).orElseThrow(() -> ExceptionResultEnum.ATTACHMENT_ERROR.exception());
         } catch (Exception e) {
             log.error(SystemConstant.LOG_ERROR, e);
-            if (Objects.nonNull(basicAttachment)) {
-                basicAttachmentService.deleteAttachment(basicAttachment);
-            }
+            basicAttachmentService.deleteAttachment(basicAttachment);
             if (e instanceof ApiException) {
                 ResultUtil.error((ApiException) e, e.getMessage());
             } else {

+ 33 - 2
sop-server/src/main/java/com/qmth/sop/server/api/SysUserController.java

@@ -1,8 +1,23 @@
 package com.qmth.sop.server.api;
 
-
+import com.qmth.boot.api.constant.ApiConstant;
+import com.qmth.sop.business.bean.result.EditResult;
+import com.qmth.sop.business.entity.TBTask;
+import com.qmth.sop.business.service.TBTaskService;
+import com.qmth.sop.business.templete.execute.AsyncSysUserDataImportService;
+import com.qmth.sop.common.contant.SystemConstant;
+import com.qmth.sop.common.enums.TaskTypeEnum;
+import com.qmth.sop.common.util.Result;
+import com.qmth.sop.common.util.ResultUtil;
+import io.swagger.annotations.*;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import java.util.Map;
 
 /**
  * <p>
@@ -12,8 +27,24 @@ import org.springframework.web.bind.annotation.RestController;
  * @author wangliang
  * @since 2023-07-17
  */
+@Api(tags = "用户Controller")
 @RestController
-@RequestMapping("/sys-user")
+@RequestMapping(ApiConstant.DEFAULT_URI_PREFIX + SystemConstant.PREFIX_URL_USER)
 public class SysUserController {
 
+    @Resource
+    TBTaskService tbTaskService;
+
+    @Resource
+    AsyncSysUserDataImportService asyncSysUserDataImportService;
+
+    @ApiOperation(value = "用户导入")
+    @RequestMapping(value = "/import", method = RequestMethod.POST)
+    @ApiResponses({@ApiResponse(code = 200, message = "返回信息", response = EditResult.class)})
+    public Result importUser(@ApiParam(value = "上传文件", required = true) @RequestParam MultipartFile file) throws Exception {
+        Map<String, Object> map = tbTaskService.saveTask(file, TaskTypeEnum.USER_IMPORT);
+        asyncSysUserDataImportService.importTask(map);
+        TBTask tbTask = (TBTask) map.get(SystemConstant.TASK);
+        return ResultUtil.ok(tbTask.getId());
+    }
 }

BIN
sop-server/src/main/resources/static/user.xlsx