luohairen
2024-11-29 40d262e091d43b15c082260b7279daf4e89b2799
Merge remote-tracking branch 'origin/master'
10个文件已修改
23个文件已添加
3437 ■■■■■ 已修改文件
business/src/main/java/com/ycl/controller/ProjectInfoController.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
business/src/main/java/com/ycl/domain/entity/ProjectInfo.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
business/src/main/java/com/ycl/domain/excel/ProjectExcelTemplate.java 655 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
business/src/main/java/com/ycl/domain/query/ProjectInfoQuery.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
business/src/main/java/com/ycl/domain/vo/ProjectInfoVO.java 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
business/src/main/java/com/ycl/domain/vo/ProjectVO.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
business/src/main/java/com/ycl/service/impl/ProjectInfoServiceImpl.java 84 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
business/src/main/java/com/ycl/service/impl/ProjectInvestmentPolicyComplianceServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
business/src/main/resources/mapper/ProjectInfoMapper.xml 170 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/pom.xml 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/ycl/common/enums/business/ProjectCategoryEnum.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/ycl/common/utils/CopyUtils.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/ycl/common/utils/DateUtils.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/ycl/common/utils/excel/OutputExcelUtils.java 217 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/ycl/common/utils/excel/annotation/CellMerge.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/ycl/common/utils/excel/annotation/ExcelDictFormat.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/ycl/common/utils/excel/annotation/ExcelEnumFormat.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/ycl/common/utils/excel/convert/ExcelBigNumberConvert.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/ycl/common/utils/excel/convert/ExcelEnumConvert.java 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/ycl/common/utils/excel/core/CellMergeStrategy.java 157 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/ycl/common/utils/excel/core/DefaultExcelListener.java 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/ycl/common/utils/excel/core/DefaultExcelResult.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/ycl/common/utils/excel/core/DropDownOptions.java 149 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/ycl/common/utils/excel/core/ExcelDownHandler.java 373 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/ycl/common/utils/excel/core/ExcelListener.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/ycl/common/utils/excel/core/ExcelResult.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/ycl/common/utils/excel/service/DictService.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/ycl/common/utils/excel/utils/JsonUtils.java 169 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/ycl/common/utils/excel/utils/ReflectUtils.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/ycl/common/utils/excel/utils/StreamUtils.java 282 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/ycl/common/utils/excel/utils/StringUtils.java 323 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
common/src/main/java/com/ycl/common/utils/excel/utils/ValidatorUtils.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
system/pom.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
business/src/main/java/com/ycl/controller/ProjectInfoController.java
@@ -4,6 +4,8 @@
import com.ycl.common.group.Add;
import com.ycl.common.group.Update;
import com.ycl.common.utils.ProjectCodeGenerator;
import com.ycl.common.utils.excel.OutputExcelUtils;
import com.ycl.domain.excel.ProjectExcelTemplate;
import com.ycl.domain.form.DocumentInfoForm;
import com.ycl.domain.form.ProjectInfoForm;
import com.ycl.domain.query.ProjectInfoQuery;
@@ -14,7 +16,9 @@
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotEmpty;
import java.io.IOException;
import java.util.List;
/**
@@ -104,4 +108,17 @@
    public Result getManagerFlag(@PathVariable("recordId") Integer recordId) {
        return projectInfoService.getManagerFlag(recordId);
    }
    /**
     * 导出模板
     * @param response
     * @return
     */
    @PostMapping("/export/template")
    public void exportTemplate(HttpServletResponse response,
                               @RequestBody List<String> fieldList
    ) throws IOException {
        OutputExcelUtils.export(response, "导入模板", "项目信息", null, ProjectExcelTemplate.class ,fieldList);
    }
}
business/src/main/java/com/ycl/domain/entity/ProjectInfo.java
@@ -49,9 +49,9 @@
    /** 投资类别(0企业投资,1政府投资,2外商投资,3境外投资) */
    private String investType;
    @TableField("project_phase")
    /** 项目阶段(0储备规划阶段,  1项目前期阶段,  2实施阶段,  3竣工投用阶段) */
    private String projectPhase;
//    @TableField("project_phase")
//    /** 项目阶段(0储备规划阶段,  1项目前期阶段,  2实施阶段,  3竣工投用阶段) */
//    private String projectPhase;
    @TableField("tag")
    /** 标签 */
business/src/main/java/com/ycl/domain/excel/ProjectExcelTemplate.java
New file
@@ -0,0 +1,655 @@
package com.ycl.domain.excel;
import com.alibaba.excel.annotation.ExcelProperty;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.util.Date;
/**
 * 导出模板类
 *
 * @Author: ljx
 * @CreateTime: 2024-10-18 10:19
 */
@Data
public class ProjectExcelTemplate {
    private static final long serialVersionUID = 1L;
                                                /* 项目管理基础信息表 */
    /**
     * 项目名称
     */
    @ExcelProperty(value = "项目名称")
    private String projectName;
    /**
     * 项目代码
     */
    @ExcelProperty(value = "项目代码")
    private String projectCode;
    /**
     * 项目类型
     */
    @ExcelProperty(value = "项目类型")
    private String projectType;
    /**
     * 项目状态
     */
    @ExcelProperty(value = "项目状态")
    private String projectStatus;
    /**
     * 资金类型
     */
    @ExcelProperty(value = "资金类型")
    private String fundType;
    /**
     * 投资类别
     */
    @ExcelProperty(value = "投资类别")
    private String investType;
    /**
     * 重点分类
     */
    @ExcelProperty(value = "重点分类")
    private String importanceType;
    /**
     * 项目阶段
     */
    @ExcelProperty(value = "项目阶段")
    private String projectPhase;
    /**
     * 标签
     */
    @ExcelProperty(value = "标签")
    private String tag;
    /**
     * 主管部门
     */
    @ExcelProperty(value = "主管部门")
    private String competentDepartment;
    /**
     * 项目归属地
     */
    @ExcelProperty(value = "项目归属地")
    private String projectLocation;
    /**
     * 经度
     */
    @ExcelProperty(value = "经度")
    private String longitude;
    /**
     * 纬度
     */
    @ExcelProperty(value = "纬度")
    private String latitude;
    /**
     * 管理归口
     */
    @ExcelProperty(value = "管理归口")
    private String managementCentralization;
    /**
     * 项目申报阶段
     */
    @ExcelProperty(value = "项目申报阶段")
    private String projectApplicationPhase;
    /**
     * 项目审批类型
     */
    @ExcelProperty(value = "项目审批类型")
    private String projectApprovalType;
    /**
     * 投资目录
     */
    @ExcelProperty(value = "投资目录")
    private String investmentCatalogue;
    /**
     * 审批计划书
     */
    @ExcelProperty(value = "审批计划书(附件名)")
    private String approvalPlan;
    /**
     * 是否立项
     */
    @ExcelProperty(value = "是否立项")
    private String isSetProject;
    /**
     * 成立时间
     */
    @ExcelProperty(value = "成立时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date setTime;
    /**
     * 赋码状态
     */
    @ExcelProperty(value = "赋码状态")
    private String assignmentStatus;
    /**
     * 行政区划
     */
    @ExcelProperty(value = "行政区划")
    private String area;
    /**
     * 中标时间
     */
    @ExcelProperty(value = "中标时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date winTime;
    /**
     * 中标单位
     */
    @ExcelProperty(value = "中标单位")
    private String winUnit;
    /**
     * 中标金额
     */
    @ExcelProperty(value = "中标金额")
    private String winAmount;
    /**
     * 详细地址
     */
    @ExcelProperty(value = "详细地址")
    private String address;
    /**
     * 建设内容
     */
    @ExcelProperty(value = "建设内容")
    private String content;
    /**
     * 联系方式
     */
    @ExcelProperty(value = "联系方式")
    private String contact;
    /**
     * 项目业主单位
     */
    @ExcelProperty(value = "项目业主单位")
    private String projectOwnerUnit;
    /**
     * 计划开工时间
     */
    @ExcelProperty(value = "计划开工时间")
    private Date planStartTime;
    /**
     * 计划竣工时间
     */
    @ExcelProperty(value = "计划竣工时间")
    private Date planCompleteTime;
    /**
     * 项目联系人
     */
    @ExcelProperty(value = "项目联系人")
    private String projectContactPerson;
    /**
     * 本年计划投资
     */
    @ExcelProperty(value = "本年计划投资")
    private BigDecimal yearInvestAmount;
                                        /* 投资项目基础信息表 */
    /**
     * 建设地点是否跨域
     */
    @ExcelProperty(value = "建设地点是否跨域")
    private String beCrossRegion;
    /**
     * 项目建设地点
     */
    @ExcelProperty(value = "建设地点")
    private String constructionLocation;
    /**
     * 建设详细地址
     */
    @ExcelProperty(value = "建设详细地址")
    private String detailedAddress;
    /**
     * 是否是补码项目
     */
    @ExcelProperty(value = "是否是补码项目")
    private String beCompensationProject;
    /**
     * 补码原因
     */
    @ExcelProperty(value = "补码原因")
    private String compensationReason;
    /**
     * 计划开工时间
     */
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @ExcelProperty(value = "计划开工时间")
    private Date plannedStartDate;
    /**
     * 拟建成时间
     */
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @ExcelProperty(value = "拟建成时间")
    private Date expectedCompletionDate;
    /**
     * 国标行业分类
     */
    @ExcelProperty(value = "国标行业分类")
    private String nationalIndustryClassification;
    /**
     * 所属行业分类
     */
    @ExcelProperty(value = "所属行业分类")
    private String industryClassification;
    /**
     * 项目建设性质
     */
    @ExcelProperty(value = "项目建设性质")
    private String projectNature;
    /**
     * 项目属性
     */
    @ExcelProperty(value = "项目属性")
    private String projectAttribute;
    /**
     * 是否使用土地
     */
    @ExcelProperty(value = "是否使用土地")
    private String useEarth;
    /**
     * 主要建设内容及规模
     */
    @ExcelProperty(value = "主要建设内容及规模")
    private String contentScale;
    /**
     * 建管平台代码
     */
    @ExcelProperty(value = "建管平台代码")
    private String code;
                                            /* 项目投资及资金来源 */
    /**
     * 项目总投资额
     */
    @ExcelProperty(value = "项目总投资额")
    private String totalInvestment;
    /**
     * 项目本金
     */
    @ExcelProperty(value = "项目本金")
    private String principal;
    /**
     * 政府投资总额
     */
    @ExcelProperty(value = "政府投资总额")
    private String governmentInvestmentTotal;
    /**
     * 中央投资总额
     */
    @ExcelProperty(value = "中央投资总额")
    private String centralInvestmentTotal;
    /**
     * 中央预算投资
     */
    @ExcelProperty(value = "中央预算投资")
    private String centralBudgetInvestment;
    /**
     * 中央财政
     */
    @ExcelProperty(value = "中央财政")
    private String centralFiscalInvestment;
    /**
     * 中央专项债券筹集的专项建设资金
     */
    @ExcelProperty(value = "中央专项债券筹集的专项建设资金")
    private String centralSpecialBondInvestment;
    /**
     * 中央专项建设基金
     */
    @ExcelProperty(value = "中央专项建设基金")
    private String centralSpecialFundInvestment;
    /**
     * 省级投资总额
     */
    @ExcelProperty(value = "省级投资总额")
    private String provincialInvestmentTotal;
    /**
     * 省预算内投资
     */
    @ExcelProperty(value = "省预算内投资")
    private String provincialBudgetInvestment;
    /**
     * 省财政性建设投资
     */
    @ExcelProperty(value = "省财政性建设投资")
    private String provincialFiscalInvestment;
    /**
     * 省专项建设资金
     */
    @ExcelProperty(value = "省专项建设资金")
    private String provincialSpecialFundInvestment;
    /**
     * 市(州)投资总额
     */
    @ExcelProperty(value = "市(州)投资总额")
    private String cityInvestmentTotal;
    /**
     * 市(州)预算内投资
     */
    @ExcelProperty(value = "市(州)预算内投资")
    private String cityBudgetInvestment;
    /**
     * 市(州)财政性投资
     */
    @ExcelProperty(value = "市(州)财政性投资")
    private String cityFiscalInvestment;
    /**
     * 市(州)专项资金
     */
    @ExcelProperty(value = "市(州)专项资金")
    private String citySpecialFundInvestment;
    /**
     * 县(市、区)投资总额
     */
    @ExcelProperty(value = "县(市、区)投资总额")
    private String countyInvestmentTotal;
    /**
     * 县(市、区)预算内投资
     */
    @ExcelProperty(value = "县(市、区)预算内投资")
    private String countyBudgetInvestment;
    /**
     * 县(市、区)财政性建设资金
     */
    @ExcelProperty(value = "县(市、区)财政性建设资金")
    private String countyFiscalInvestment;
    /**
     * 县(市、区)专项资金
     */
    @ExcelProperty(value = "县(市、区)专项资金")
    private String countySpecialFundInvestment;
    /**
     * 国内贷款总额
     */
    @ExcelProperty(value = "国内贷款总额")
    private String domesticLoanTotal;
    /**
     * 银行贷款
     */
    @ExcelProperty(value = "银行贷款")
    private String bankLoan;
    /**
     * 外商投资总额
     */
    @ExcelProperty(value = "外商投资总额")
    private String foreignInvestmentTotal;
    /**
     * 企业自筹总额
     */
    @ExcelProperty(value = "企业自筹总额")
    private String enterpriseSelfRaisedTotal;
    /**
     * 其他投资总额
     */
    @ExcelProperty(value = "其他投资总额")
    private String otherInvestmentTotal;
                                        /* 项目(法人)单位登记信息表 */
//    /**
//     * 项目总投资额(根据前面的数据填充)
//     */
//    private BigDecimal totalInvestment;
    /**
     * 项目单位
     */
    @ExcelProperty(value = "项目单位")
    private String projectUnit;
    /**
     * 项目单位类型
     */
    @ExcelProperty(value = "项目单位类型")
    private String projectUnitType;
    /**
     * 登记注册类型
     */
    @ExcelProperty(value = "登记注册类型")
    private String registrationType;
    /**
     * 控股情况
     */
    @ExcelProperty(value = "控股情况")
    private String holdingSituation;
    /**
     * 证照类型
     */
    @ExcelProperty(value = "证照类型")
    private String certificateType;
    /**
     * 证照号码
     */
    @ExcelProperty(value = "证照号码")
    private String certificateNumber;
    /**
     * 注册地址
     */
    @ExcelProperty(value = "注册地址")
    private String registeredAddress;
    /**
     * 注册资金
     */
    @ExcelProperty(value = "注册资金")
    private BigDecimal registeredCapital;
    /**
     * 法人代表
     */
    @ExcelProperty(value = "法人代表")
    private String legal_representative;
    /**
     * 固定电话
     */
    @ExcelProperty(value = "固定电话")
    private String fixedPhone;
    /**
     * 法人身份证
     */
    @ExcelProperty(value = "法人身份证")
    private String legalPersonIdcard;
//    /**
//     * 项目联系人(根据前面的数据填充)
//     */
//    private String projectContactPerson;
    /**
     * 移动电话
     */
    @ExcelProperty(value = "移动电话")
    private String phone;
    /**
     * 联系人身份证
     */
    @ExcelProperty(value = "联系人身份证")
    private String contactIdcard;
    /**
     * 微信号
     */
    @ExcelProperty(value = "微信号")
    private String wechat;
    /**
     * 联系人通讯地址
     */
    @ExcelProperty(value = "联系人通讯地址")
    private String contactAddress;
    /**
     * 邮政编码
     */
    @ExcelProperty(value = "邮政编码")
    private String postCode;
    /**
     * 电子邮箱
     */
    @ExcelProperty(value = "电子邮箱")
    private String email;
                                    /* 投资项目产业政策符合情况表 */
    /**
     * 符合产业政策附件
     */
    @ExcelProperty(value = "符合产业政策(附件名)")
    private String policyComplianceAttachment;
    /**
     * 是否属于《产业结构调整指导目录》下的项目
     */
    @ExcelProperty(value = "属于《产业结构调整指导目录》下的项目")
    private String belongsToIndustryAdjustmentDirectory;
    /**
     * 是否属于未列入《产业结构调整指导目录》的允许类项目
     */
    @ExcelProperty(value = "属于未列入《产业结构调整指导目录》的允许类项目")
    private String belongsToAllowedProjects;
    /**
     * 是否属于《西部地区鼓励类产业目录》的项目
     */
    @ExcelProperty(value = "属于《西部地区鼓励类产业目录》的项目")
    private String belongsToWesternEncouragedDirectory;
    /**
     * 是否不属于产业政策禁止投资建设或实行核准、审批管理的项目
     */
    @ExcelProperty(value = "不属于产业政策禁止投资建设或实行核准、审批管理的项目")
    private String notBannedOrControlledProject;
    /**
     * 填报信息是否真实
     */
    @ExcelProperty(value = "填报信息是否真实")
    private String informationIsTrue;
    /**
     * 专项规划复合情况
     */
    @ExcelProperty(value = "专项规划复合情况")
    private String specialPlanningCompliance;
    /**
     * 项目能耗情况
     */
    @ExcelProperty(value = "项目能耗情况")
    private String energyConsumption;
    /**
     * 项目年综合能源消费量(标准煤当量值)
     */
    @ExcelProperty(value = "项目年综合能源消费量(标准煤当量值)")
    private BigDecimal annualEnergyConsumption;
    /**
     * 项目年电力消耗量(标准煤当量值)
     */
    @ExcelProperty(value = "项目年电力消耗量(标准煤当量值)")
    private BigDecimal annualElectricityConsumption;
    /* 相关文书 */
    @ExcelProperty(value = "相关文书(附件名)")
    private String documents;
}
business/src/main/java/com/ycl/domain/query/ProjectInfoQuery.java
@@ -1,8 +1,12 @@
package com.ycl.domain.query;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ycl.system.domain.base.AbsQuery;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
/**
 * 项目管理基础信息表查询
@@ -13,5 +17,37 @@
@Data
@ApiModel(value = "ProjectInfo查询参数", description = "项目管理基础信息表查询参数")
public class ProjectInfoQuery extends AbsQuery {
    //项目类别
    private String projectCategory;
    //项目名称
    private String projectName;
    //项目代码
    private String projectCode;
    //项目类型
    private String projectType;
    //重点分类
    private String importanceType;
    //项目标签
    private String tag;
    //项目状态
    private String projectStatus;
    //项目码
    private String projectColorCode;
    //关联状态
    private String assignmentStatus;
    //资金类型
    private String fundType;
    //项目阶段
    private String projectPhase;
    //投资类别
    private String investType;
    //行政区划
    private String area;
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date projectStartTime;
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date projectEndTime;
    //从有没有流程判断是存储还是早期
    private String reserveOrPrevious;
}
business/src/main/java/com/ycl/domain/vo/ProjectInfoVO.java
@@ -67,7 +67,7 @@
    /** 主管部门(对应审批部门id) */
    @ApiModelProperty("主管部门(对应审批部门id)")
    private List<Long> competentDepartmentList;
    private String competentDepartment;
    /** 行政区域 */
    @ApiModelProperty("行政区域")
    private String area;
@@ -75,7 +75,7 @@
    /** 管理归口  (0基本建设(发改),  1更新改造(经信),  2单纯购置(发改),  3信息化(发改),  4其他投资) */
    @ApiModelProperty("管理归口  (0基本建设(发改),  1更新改造(经信),  2单纯购置(发改),  3信息化(发改),  4其他投资)")
    private List<String> managementCentralizationList;
    private String managementCentralization;
    /** 项目审批类型 */
    @ApiModelProperty("项目审批类型")
    private String projectApprovalType;
@@ -159,6 +159,11 @@
    @ApiModelProperty("文件")
    private List<File> fileList;
    private Long processId;
    private ProjectInvestmentInfoVO projectInvestmentInfo;
    private ProjectInvestmentFundingVO projectInvestmentFunding;
    private ProjectUnitRegistrationInfoVO projectUnitRegistrationInfo;
    private ProjectInvestmentPolicyComplianceVO projectInvestmentPolicyCompliance;
    public static ProjectInfoVO getVoByEntity(@NonNull ProjectInfo entity, ProjectInfoVO vo) {
        if(vo == null) {
@@ -180,5 +185,20 @@
        }
        return vo;
    }
    //转换字符串集合字段
    public static void transform(@NonNull ProjectInfoVO vo) {
        //主管部门转成list
        String competentDepartment = vo.getCompetentDepartment();
        if(!StringUtils.isBlank(competentDepartment)){
            List<Long> list = Arrays.stream(competentDepartment.split(","))
                    .map(Long::parseLong)
                    .collect(Collectors.toList());
            vo.setCompetentDepartmentList(list);
        }
        //管理归口转换
        String managementCentralization = vo.getManagementCentralization();
        if(!StringUtils.isBlank(managementCentralization)){
            vo.setManagementCentralizationList(Arrays.asList(managementCentralization.split(",")));
        }
    }
}
business/src/main/java/com/ycl/domain/vo/ProjectVO.java
New file
@@ -0,0 +1,17 @@
package com.ycl.domain.vo;
import com.ycl.domain.excel.ProjectExcelTemplate;
import lombok.Data;
import java.util.List;
@Data
public class ProjectVO extends ProjectExcelTemplate {
    private Long id;
    /** 状态码 */
    private String projectColorCode;
    private List<Long> competentDepartmentList;
    private List<String> managementCentralizationList;
    private Long processId;
}
business/src/main/java/com/ycl/service/impl/ProjectInfoServiceImpl.java
@@ -1,10 +1,14 @@
package com.ycl.service.impl;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ycl.common.base.Result;
import com.ycl.common.enums.business.FileTypeEnum;
import com.ycl.common.enums.business.ProjectCategoryEnum;
import com.ycl.common.utils.CopyUtils;
import com.ycl.common.utils.DateUtils;
import com.ycl.common.utils.SecurityUtils;
import com.ycl.domain.entity.File;
import com.ycl.domain.entity.ProjectInfo;
@@ -39,6 +43,7 @@
    private final ProjectInfoMapper projectInfoMapper;
    private final FileService fileService;
    private final FileMapper fileMapper;
    /**
     * 添加
     *
@@ -56,7 +61,7 @@
        baseMapper.insert(entity);
        //添加文件
        List<File> fileList = form.getFileList();
        fileList.forEach(item->{
        fileList.forEach(item -> {
            item.setBusId(entity.getId());
            item.setType(FileTypeEnum.PROJECT_INFO);
        });
@@ -76,20 +81,21 @@
        ProjectInfo entity = baseMapper.selectById(form.getId());
        // 为空抛IllegalArgumentException,做全局异常处理
        Assert.notNull(entity, "记录不存在");
        ProjectInfoForm.getEntityByForm(form,entity);
        ProjectInfoForm.getEntityByForm(form, entity);
        Long userId = SecurityUtils.getUserId();
        entity.setUpdateBy(userId);
        //更新项目信息
        baseMapper.updateById(entity);
        List<File> fileList = form.getFileList();
        fileList.forEach(item->{
        fileList.forEach(item -> {
            item.setId(null);
            item.setBusId(entity.getId());
            item.setType(FileTypeEnum.PROJECT_INFO);
        });
        //删除原有文件
        QueryWrapper<File> fileQueryWrapper = new QueryWrapper<>();
        fileQueryWrapper.eq("type",FileTypeEnum.PROJECT_INFO.getType());
        fileQueryWrapper.eq("bus_id",entity.getId());
        fileQueryWrapper.eq("type", FileTypeEnum.PROJECT_INFO.getType());
        fileQueryWrapper.eq("bus_id", entity.getId());
        fileMapper.delete(fileQueryWrapper);
        //替换成现有
        fileService.saveBatch(fileList);
@@ -130,17 +136,54 @@
     */
    @Override
    public Result page(ProjectInfoQuery query) {
        IPage<ProjectInfo> page = PageUtil.getPage(query, ProjectInfo.class);
        if (query.getProjectStartTime() != null) {
            query.setProjectStartTime(DateUtils.getDayStart(query.getProjectStartTime()));
        }
        if (query.getProjectEndTime() != null) {
            query.setProjectEndTime(DateUtils.getDayEnd(query.getProjectEndTime()));
        }
        String projectCategory = query.getProjectCategory();
        if (ProjectCategoryEnum.RESERVE.getType().equals(projectCategory)) {
            query.setProjectStatus(ProjectCategoryEnum.RESERVE.getStatus());
            query.setReserveOrPrevious(ProjectCategoryEnum.RESERVE.getCode());
        } else if (ProjectCategoryEnum.PREVIOUS.getType().equals(projectCategory)) {
            query.setProjectStatus(ProjectCategoryEnum.PREVIOUS.getStatus());
            query.setReserveOrPrevious(ProjectCategoryEnum.PREVIOUS.getCode());
        } else if (ProjectCategoryEnum.FINISH.getType().equals(projectCategory)) {
            query.setProjectStatus(ProjectCategoryEnum.FINISH.getStatus());
        } else if (ProjectCategoryEnum.EXCEPTION.getType().equals(projectCategory)) {
            //TODO
            //先查出异常流程或者异常进度或者异常计划的projectId和异常种类
            //通过projectId查出项目数据
            //补充相应的异常数据(异常种类、异常流程节点等)
            List<ProjectVO> list = new ArrayList<>();
            return Result.ok().data(list).total(0);
        }
        IPage<ProjectInfoVO> page = PageUtil.getPage(query, ProjectInfoVO.class);
        baseMapper.getPage(page, query);
        List<ProjectInfo> records = page.getRecords();
        List<ProjectInfoVO> list = records.stream()
                .map(entity -> {
                    ProjectInfoVO vo = ProjectInfoVO.getVoByEntity(entity, null);
        List<ProjectInfoVO> records = page.getRecords();
        List<ProjectVO> list = new ArrayList<>();
        records.forEach(vo -> {
                    ProjectInfoVO.transform(vo);
                    vo.setProjectColorCode("green");
                    return vo;
                })
                .collect(Collectors.toList());
            ProjectVO projectVO = new ProjectVO();
            copyToProjectVO(vo,projectVO);
            //翻译项目阶段
            String phase = ProjectCategoryEnum.getPhaseByProjectStatus(projectVO.getProjectStatus(), projectVO.getProcessId() != null);
            projectVO.setProjectPhase(phase);
            list.add(projectVO);
        });
        return Result.ok().data(list).total(page.getTotal());
    }
    private void copyToProjectVO(ProjectInfoVO vo,ProjectVO projectVO) {
        //忽略null值的复制
        CopyUtils.copyNoNullProperties(vo, projectVO);
        if(vo.getProjectInvestmentFunding()!=null) CopyUtils.copyNoNullProperties(vo.getProjectInvestmentFunding(),projectVO);
        if(vo.getProjectInvestmentInfo()!=null)  CopyUtils.copyNoNullProperties(vo.getProjectInvestmentInfo(),projectVO);
        if(vo.getProjectUnitRegistrationInfo()!=null)  CopyUtils.copyNoNullProperties(vo.getProjectUnitRegistrationInfo(),projectVO);
        if(vo.getProjectInvestmentPolicyCompliance()!=null)  CopyUtils.copyNoNullProperties(vo.getProjectInvestmentPolicyCompliance(),projectVO);
    }
    /**
@@ -155,8 +198,8 @@
        Assert.notNull(entity, "记录不存在");
        ProjectInfoVO vo = ProjectInfoVO.getVoByEntity(entity, null);
        QueryWrapper<File> fileQueryWrapper = new QueryWrapper<>();
        fileQueryWrapper.eq("type",FileTypeEnum.PROJECT_INFO.getType());
        fileQueryWrapper.eq("bus_id",vo.getId());
        fileQueryWrapper.eq("type", FileTypeEnum.PROJECT_INFO.getType());
        fileQueryWrapper.eq("bus_id", vo.getId());
        List<File> files = fileMapper.selectList(fileQueryWrapper);
        vo.setFileList(files);
        return Result.ok().data(vo);
@@ -213,8 +256,8 @@
    public Result docDetail(Integer id) {
        DocumentInfoForm documentInfoForm = new DocumentInfoForm();
        QueryWrapper<File> fileQueryWrapper = new QueryWrapper<>();
        fileQueryWrapper.eq("type",FileTypeEnum.DOCUMENT_INFO.getType());
        fileQueryWrapper.eq("bus_id",id);
        fileQueryWrapper.eq("type", FileTypeEnum.DOCUMENT_INFO.getType());
        fileQueryWrapper.eq("bus_id", id);
        List<File> files = fileMapper.selectList(fileQueryWrapper);
        documentInfoForm.setFileList(files);
        return Result.ok().data(documentInfoForm);
@@ -223,14 +266,15 @@
    @Override
    public Result addDoc(DocumentInfoForm form) {
        List<File> fileList = form.getFileList();
        fileList.forEach(item->{
        fileList.forEach(item -> {
            item.setId(null);
            item.setBusId(form.getProjectId());
            item.setType(FileTypeEnum.DOCUMENT_INFO);
        });
        //删除原有文件
        QueryWrapper<File> fileQueryWrapper = new QueryWrapper<>();
        fileQueryWrapper.eq("type",FileTypeEnum.DOCUMENT_INFO.getType());
        fileQueryWrapper.eq("bus_id",form.getProjectId());
        fileQueryWrapper.eq("type", FileTypeEnum.DOCUMENT_INFO.getType());
        fileQueryWrapper.eq("bus_id", form.getProjectId());
        fileMapper.delete(fileQueryWrapper);
        //替换成现有
        fileService.saveBatch(fileList);
business/src/main/java/com/ycl/service/impl/ProjectInvestmentPolicyComplianceServiceImpl.java
@@ -56,6 +56,7 @@
        //添加文件
        List<File> fileList = form.getFileList();
        fileList.forEach(item->{
            item.setId(null);
            item.setBusId(entity.getId());
            item.setType(FileTypeEnum.INVEST_POLICY);
        });
@@ -80,6 +81,7 @@
        baseMapper.updateById(entity);
        List<File> fileList = form.getFileList();
        fileList.forEach(item->{
            item.setId(null);
            item.setBusId(entity.getId());
            item.setType(FileTypeEnum.INVEST_POLICY);
        });
business/src/main/resources/mapper/ProjectInfoMapper.xml
@@ -3,44 +3,15 @@
<mapper namespace="com.ycl.mapper.ProjectInfoMapper">
    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.ycl.domain.entity.ProjectInfo">
        <id column="id" property="id"/>
        <result column="project_name" property="projectName" />
        <result column="project_code" property="projectCode" />
        <result column="content" property="content" />
        <result column="project_type" property="projectType" />
        <result column="project_status" property="projectStatus" />
        <result column="fund_type" property="fundType" />
        <result column="invest_type" property="investType" />
        <result column="project_phase" property="projectPhase" />
        <result column="tag" property="tag" />
        <result column="competent_department" property="competentDepartment" />
        <result column="area" property="area" />
        <result column="management_centralization" property="managementCentralization" />
        <result column="project_approval_type" property="projectApprovalType" />
        <result column="importance_type" property="importanceType" />
        <result column="year" property="year" />
        <result column="year_invest_amount" property="yearInvestAmount" />
        <result column="create_project_time" property="createProjectTime" />
        <result column="plan_start_time" property="planStartTime" />
        <result column="plan_complete_time" property="planCompleteTime" />
        <result column="win_unit" property="winUnit" />
        <result column="win_amount" property="winAmount" />
        <result column="win_time" property="winTime" />
        <result column="project_address" property="projectAddress" />
        <result column="longitude" property="longitude" />
        <result column="latitude" property="latitude" />
        <result column="project_owner_unit" property="projectOwnerUnit" />
        <result column="project_contact_person" property="projectContactPerson" />
        <result column="contact" property="contact" />
        <result column="gmt_create" property="gmtCreate" />
        <result column="gmt_update" property="gmtUpdate" />
        <result column="update_by" property="updateBy" />
        <result column="create_by" property="createBy" />
    <resultMap id="resultMap" type="com.ycl.domain.vo.ProjectInfoVO" autoMapping="true">
        <association property="projectInvestmentInfo" javaType="com.ycl.domain.vo.ProjectInvestmentInfoVO" autoMapping="true" columnPrefix="TPII_"/>
        <association property="projectInvestmentFunding" javaType="com.ycl.domain.vo.ProjectInvestmentFundingVO" autoMapping="true" columnPrefix="TPIF_"/>
        <association property="projectUnitRegistrationInfo" javaType="com.ycl.domain.vo.ProjectUnitRegistrationInfoVO" autoMapping="true" columnPrefix="TPURI_"/>
        <association property="projectInvestmentPolicyCompliance" javaType="com.ycl.domain.vo.ProjectInvestmentPolicyComplianceVO" autoMapping="true" columnPrefix="TPIPC_"/>
    </resultMap>
    <select id="getById" resultMap="BaseResultMap">
    <select id="getById" resultType="com.ycl.domain.entity.ProjectInfo">
        SELECT
            TPI.project_name,
            TPI.project_code,
@@ -82,45 +53,104 @@
    </select>
    <select id="getPage" resultMap="BaseResultMap">
    <select id="getPage" resultMap="resultMap">
        SELECT
            TPI.project_name,
            TPI.project_code,
            TPI.content,
            TPI.project_type,
            TPI.project_status,
            TPI.fund_type,
            TPI.invest_type,
            TPI.project_phase,
            TPI.tag,
            TPI.competent_department,
            TPI.area,
            TPI.management_centralization,
            TPI.project_approval_type,
            TPI.importance_type,
            TPI.year,
            TPI.year_invest_amount,
            TPI.create_project_time,
            TPI.plan_start_time,
            TPI.plan_complete_time,
            TPI.win_unit,
            TPI.win_amount,
            TPI.win_time,
            TPI.project_address,
            TPI.longitude,
            TPI.latitude,
            TPI.project_owner_unit,
            TPI.project_contact_person,
            TPI.contact,
            TPI.gmt_create,
            TPI.gmt_update,
            TPI.update_by,
            TPI.create_by,
            TPI.id
            TPI.*,TPP.process_instance_id as processId,
            TPIF.total_investment  as TPIF_totalInvestment,
            TPIF.principal as TPIF_principal,TPIF.government_investment_total as TPIF_government_investment_total,TPIF.central_investment_total as TPIF_central_investment_total,
            TPIF.central_budget_investment as TPIF_central_budget_investment,TPIF.central_fiscal_investment as TPIF_central_fiscal_investment,
            TPIF.central_special_bond_investment as TPIF_central_special_bond_investment,TPIF.central_special_fund_investment as TPIF_central_special_fund_investment,
            TPIF.provincial_investment_total as TPIF_provincial_investment_total,TPIF.provincial_budget_investment as TPIF_provincial_budget_investment,
            TPIF.provincial_fiscal_investment as TPIF_provincial_fiscal_investment,TPIF.provincial_special_fund_investment as TPIF_provincial_special_fund_investment,
            TPIF.city_investment_total as TPIF_city_investment_total,TPIF.city_budget_investment as TPIF_city_budget_investment,TPIF.city_fiscal_investment as TPIF_city_fiscal_investment,
            TPIF.city_special_fund_investment as TPIF_city_special_fund_investment,TPIF.county_investment_total as TPIF_county_investment_total,TPIF.county_budget_investment as TPIF_county_budget_investment,
            TPIF.county_fiscal_investment as TPIF_county_fiscal_investment,TPIF.county_special_fund_investment as TPIF_county_special_fund_investment,
            TPIF.domestic_loan_total as TPIF_domestic_loan_total,TPIF.bank_loan as TPIF_bank_loan,TPIF.foreign_investment_total as TPIF_foreign_investment_total,
            TPIF.enterprise_self_raised_total as TPIF_enterprise_self_raised_total,TPIF.other_investment_total as TPIF_other_investment_total,
            TPII.be_cross_region as TPII_be_cross_region,TPII.construction_location as TPII_construction_location,
            TPII.detailed_address as TPII_detailed_address,TPII.be_compensation_project as TPII_be_compensation_project,TPII.compensation_reason as TPII_compensation_reason,
            TPII.planned_start_date as TPII_planned_start_date,TPII.expected_completion_date as TPII_expected_completion_date,
            TPII.national_industry_classification as TPII_national_industry_classification,TPII.industry_classification as TPII_industry_classification,TPII.project_nature as TPII_project_nature,
            TPII.project_attribute as TPII_project_attribute,TPII.use_earth as TPII_use_earth,TPII.content_scale as TPII_content_scale,TPII.code as TPII_code,
            TPIPC.belongs_to_industry_adjustment_directory as TPIPC_belongs_to_industry_adjustment_directory,TPIPC.belongs_to_western_encouraged_directory as TPIPC_belongs_to_western_encouraged_directory,
            TPIPC.not_banned_or_controlled_project as TPIPC_not_banned_or_controlled_project,TPIPC.information_is_true as TPIPC_information_is_true,
            TPIPC.special_planning_compliance as TPIPC_special_planning_compliance,TPIPC.annual_energy_consumption as TPIPC_annual_energy_consumption,TPIPC.annual_electricity_consumption as TPIPC_annual_electricity_consumption,
            TPIPC.energy_check as TPIPC_energy_check,TPIPC.no_only_check_type as TPIPC_no_only_check_type,TPIPC.remarks as TPIPC_remarks,
            TPURI.total_investment as TPURI_total_investment,
            TPURI.project_unit as TPURI_project_unit,
            TPURI.project_unit_type as TPURI_project_unit_type,
            TPURI.registration_type as TPURI_registration_type,
            TPURI.holding_situation as TPURI_holding_situation,
            TPURI.certificate_type  as TPURI_certificate_type,
            TPURI.certificate_number as TPURI_certificate_number,
            TPURI.registered_address as TPURI_registered_address,
            TPURI.registered_capital as TPURI_registered_capital,
            TPURI.legal_representative as TPURI_legal_representative,
            TPURI.fixed_phone as TPURI_fixed_phone,
            TPURI.legal_person_idcard as TPURI_legal_person_idcard,
            TPURI.project_contact_person as TPURI_project_contact_person,
            TPURI.phone as TPURI_phone,
            TPURI.contact_idcard as TPURI_contact_idcard,
            TPURI.wechat as TPURI_wechat,
            TPURI.contact_address as TPURI_contact_address,
            TPURI.post_code as TPURI_post_code,
            TPURI.email as TPURI_email
        FROM
            t_project_info TPI
        WHERE
        LEFT JOIN t_project_investment_funding TPIF ON TPI.id = TPIF.project_id and TPIF.deleted = 0
        LEFT JOIN t_project_investment_info TPII ON TPI.id = TPII.project_id and TPII.deleted = 0
        LEFT JOIN t_project_investment_policy_compliance TPIPC ON TPI.id = TPIPC.project_id and TPIPC.deleted = 0
        LEFT JOIN t_project_unit_registration_info TPURI ON TPI.id = TPURI.project_id and TPURI.deleted = 0
        LEFT JOIN t_project_process TPP ON TPI.id = TPP.project_id and TPP.deleted = 0
        <where>
            TPI.deleted = 0
            <if test="query.projectName !=null and query.projectName!=''">
                and TPI.project_name like concat('%',#{query.projectName},'%')
            </if>
            <if test="query.projectCode !=null and query.projectCode!=''">
                and TPI.project_code like concat('%',#{query.projectCode},'%')
            </if>
            <if test="query.projectType !=null and query.projectType!=''">
                and TPI.project_type = #{query.projectType}
            </if>
            <if test="query.importanceType !=null and query.importanceType!=''">
                and TPI.importance_type = #{query.importanceType}
            </if>
            <if test="query.tag !=null and query.tag!=''">
                and TPI.tag like concat('%',#{query.tag},'%')
            </if>
            <if test="query.projectStatus !=null and query.projectStatus!=''">
                and TPI.project_status = #{query.projectStatus}
            </if>
            <if test="query.projectPhase !=null and query.projectPhase!=''">
                and TPI.project_phase = #{query.projectPhase}
            </if>
<!--            <if test=" assignmentStatus !=null and assignmentStatus!=''">-->
<!--                and TPI.project_phase = #{projectPhase}-->
<!--            </if>-->
            <if test="query.fundType !=null and query.fundType!=''">
                and TPI.fund_type = #{query.fundType}
            </if>
            <if test="query.projectPhase !=null and query.projectPhase!=''">
                and TPI.project_phase = #{query.projectPhase}
            </if>
            <if test="query.investType !=null and query.investType!=''">
                and TPI.invest_type = #{query.investType}
            </if>
            <if test="query.area !=null and query.area!=''">
                and TPI.area = #{query.area}
            </if>
            <if test="query.projectStartTime !=null and query.projectEndTime !=null">
                and TPI.create_project_time between #{query.projectStartTime} and #{query.projectEndTime}
            </if>
            <if test="query.reserveOrPrevious != null and query.reserveOrPrevious == 'reserve'">
                and TPP.process_instance_id is null
            </if>
            <if test="query.reserveOrPrevious != null and query.reserveOrPrevious == 'previous'">
                and TPP.process_instance_id is not null
            </if>
        </where>
        order by TPI.gmt_create
    </select>
</mapper>
common/pom.xml
@@ -128,7 +128,16 @@
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
        </dependency>
        <!-- easy excel -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-crypto</artifactId>
        </dependency>
    </dependencies>
</project>
common/src/main/java/com/ycl/common/enums/business/ProjectCategoryEnum.java
New file
@@ -0,0 +1,51 @@
package com.ycl.common.enums.business;
import cn.hutool.core.util.ObjectUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum ProjectCategoryEnum {
    RESERVE("1", "pendding", "储备项目","reserve","储备规划阶段"),
    PREVIOUS("2", "pendding","前期项目","previous","项目前期阶段"),
    IMPLEMENT("3", "working,stop","实施项目","implement","实施阶段"),
    FINISH("4", "finish","竣工项目","finish","竣工投用阶段"),
    EXCEPTION("5", "","异常项目","exception","");
    private final String type;
    private final String status;
    private final String name;
    private final String code;
    private  final String desc;
    //判断传入数据是否存在四种项目推进状态
    public static boolean isValidType(String type) {
        for (ProjectCategoryEnum status : ProjectCategoryEnum.values()) {
            if (status.getDesc().equals(type) && ObjectUtil.notEqual(type,ProjectCategoryEnum.EXCEPTION.getName())) {
                return true;
            }
        }
        return false;
    }
    /**
     * 获取项目阶段
     * @param projectStatus
     * @param hasProcess 是否启动流程
     * @return
     */
    public static String getPhaseByProjectStatus(String projectStatus, boolean hasProcess) {
        for (ProjectCategoryEnum projectCategoryEnum : ProjectCategoryEnum.values()) {
            if (hasProcess && PREVIOUS.status.contains(projectStatus)) {
                return PREVIOUS.desc;
            }
            if (projectCategoryEnum.status.contains(projectStatus)) {
                return projectCategoryEnum.desc;
            }
        }
        return null;
    }
}
common/src/main/java/com/ycl/common/utils/CopyUtils.java
New file
@@ -0,0 +1,48 @@
package com.ycl.common.utils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import java.beans.PropertyDescriptor;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
/**
 * 自定义复制工具类
 */
public class CopyUtils {
        /**
         * 所有为空值的属性都不copy
         *
         * @param source
         * @param target
         */
        public static void copyNoNullProperties(Object source, Object target) {
            BeanUtils.copyProperties(source, target, getNullField(source));
        }
        /**
         * 获取属性中为空的字段
         *
         * @param target
         * @return
         */
        private static String[] getNullField(Object target) {
            BeanWrapper beanWrapper = new BeanWrapperImpl(target);
            PropertyDescriptor[] propertyDescriptors = beanWrapper.getPropertyDescriptors();
            Set<String> notNullFieldSet = new HashSet<>();
            if (propertyDescriptors.length > 0) {
                for (PropertyDescriptor p : propertyDescriptors) {
                    String name = p.getName();
                    Object value = beanWrapper.getPropertyValue(name);
                    if (Objects.isNull(value)) {
                        notNullFieldSet.add(name);
                    }
                }
            }
            String[] notNullField = new String[notNullFieldSet.size()];
            return notNullFieldSet.toArray(notNullField);
        }
}
common/src/main/java/com/ycl/common/utils/DateUtils.java
@@ -1,6 +1,7 @@
package com.ycl.common.utils;
import java.lang.management.ManagementFactory;
import java.sql.Timestamp;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
@@ -9,7 +10,10 @@
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.Objects;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.springframework.lang.Nullable;
/**
 * 时间工具类
@@ -188,4 +192,35 @@
        ZonedDateTime zdt = localDateTime.atZone(ZoneId.systemDefault());
        return Date.from(zdt.toInstant());
    }
    /**
     * 获取某天的开始时间
     *
     * @param date
     * @return 2023-01-01 00:00:00
     */
    public static Date getDayStart(@Nullable Date date) {
        if (Objects.isNull(date)) {
            date = new Date();
        }
        LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.of("GMT+8"));
        LocalDateTime of = LocalDateTime.of(localDateTime.getYear(), localDateTime.getMonth(), localDateTime.getDayOfMonth(), 0, 0, 0);
        return Timestamp.valueOf(of);
    }
    /**
     * 获取某天的结束时间
     *
     * @param date
     * @return 2023-01-01 23:59:59
     */
    public static Date getDayEnd(@Nullable Date date) {
        if (Objects.isNull(date)) {
            date = new Date();
        }
        LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
        LocalDateTime of = LocalDateTime.of(localDateTime.getYear(), localDateTime.getMonth(), localDateTime.getDayOfMonth(), 23, 59, 59);
        return Timestamp.valueOf(of);
    }
}
common/src/main/java/com/ycl/common/utils/excel/OutputExcelUtils.java
New file
@@ -0,0 +1,217 @@
package com.ycl.common.utils.excel;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ZipUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.ycl.common.utils.excel.convert.ExcelBigNumberConvert;
import com.ycl.common.utils.excel.core.CellMergeStrategy;
import com.ycl.common.utils.excel.core.DropDownOptions;
import com.ycl.common.utils.excel.core.ExcelDownHandler;
import com.ycl.common.utils.file.FileUtils;
import org.apache.commons.codec.Charsets;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotNull;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
 * @Author: ljx
 * @CreateTime: 2024-10-18 10:13
 */
public class OutputExcelUtils {
    /**
     * 忽略部分导出字段方法
     * @param response
     * @param fileName 文件名称
     * @param sheetName sheet名称
     * @param dataList 需要导出的数据
     * @param clazz 类
     * @param
     * @param <T>
     * @throws IOException
     */
    public static <T> void export(HttpServletResponse response, String fileName, String sheetName, List<T> dataList, Class<T> clazz, List<String> fieldNames) throws IOException {
        response.setContentType("application/zip");
        response.setCharacterEncoding(Charsets.UTF_8.name());
        fileName = URLEncoder.encode(fileName, Charsets.UTF_8);
        response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".zip");
        Set<Integer> selectedIndexes = getSelectFields(fieldNames, clazz);
        //下载模板
        downloadTemplate(response, sheetName, dataList, clazz, selectedIndexes);
        // 临时向资源文件夹写 名为template的文件夹
//        ClassPathResource classPathResource = new ClassPathResource("/template/test.xls");
//        File file = classPathResource.getFile();
//        try(FileOutputStream fileOutputStream = new FileOutputStream(file,false)) {
//            EasyExcel.write(fileOutputStream, clazz).sheet(sheetName).doWrite(dataList);
//        }
//
//        ClassPathResource classPathResource1 = new ClassPathResource("/template");
//        File zip = ZipUtil.zip(classPathResource1.getFile());
//        byte[] bytes;
//        try (FileInputStream fileInputStream = new FileInputStream(zip)) {
//            bytes = fileInputStream.readAllBytes();
//        }
//        response.getOutputStream().write(bytes);
//        if (selectedIndexes.size() > 0) {
//            EasyExcel.write(response.getOutputStream(), clazz).excludeColumnIndexes(selectedIndexes).sheet(sheetName).doWrite(dataList);
//        } else {
//            EasyExcel.write(response.getOutputStream(), clazz).sheet(sheetName).doWrite(dataList);
//        }
    }
    private synchronized static <T> void downloadTemplate(HttpServletResponse response, String sheetName, List<T> dataList, Class<T> clazz, Set<Integer> columnIndex) throws IOException {
        File tempDir = null;
        try {
            // 创建临时目录
            tempDir = Files.createTempDirectory("temp").toFile();
            File templateDir = new File(tempDir, "template");
            if (!templateDir.exists()) {
                templateDir.mkdirs();
            }
            // 创建 Excel 文件
            File excelFile = new File(templateDir, "excel.xlsx");
            if (!excelFile.exists()) {
                excelFile.createNewFile();
            }
            // 写入 Excel 模板数据
            try (FileOutputStream fileOutputStream = new FileOutputStream(excelFile, false)) {
                EasyExcel.write(fileOutputStream, clazz).includeColumnIndexes(columnIndex).sheet(sheetName).doWrite(dataList);
            }
            // 创建附件目录
            File attachmentDir = new File(templateDir, "attachment");
            if (!attachmentDir.exists()) {
                attachmentDir.mkdirs();
            }
            // 打包 ZIP 文件
            File zipFile = ZipUtil.zip(templateDir);
            byte[] zipBytes = Files.readAllBytes(zipFile.toPath());
            // 将 ZIP 文件写入响应
            try(ServletOutputStream outputStream = response.getOutputStream()) {
                outputStream.write(zipBytes);
            }
        } finally {
            deleteDirectoryOrFile(tempDir);
        }
    }
    private static void deleteDirectoryOrFile(File file) {
        if (ObjectUtil.isNull(file)) {
            return;
        }
        if (file.isDirectory()) {
            File[] files = file.listFiles();
            if (files != null) {
                for (File f : files) {
                    deleteDirectoryOrFile(f);
                }
            }
        }
        file.delete();
    }
    /**
     * 导出模板
     * @param response
     * @param fileName
     * @param sheetName
     * @param dataList
     * @param clazz
     * @param  fieldNames
     */
    public static <T> void exportTemplate(HttpServletResponse response,String fileName, String sheetName, List<T> dataList, Class<T> clazz, List<String> fieldNames) throws IOException {
        Set<Integer> selectedIndexes = getSelectFields(fieldNames, clazz);
        resetResponse(fileName, response);
        exportExcel(dataList, sheetName, clazz, false, response.getOutputStream(), null, selectedIndexes);
    }
    public static <T> @NotNull Set<Integer> getSelectFields(List<String> fieldNames, Class<T> clazz) {
        Set<Integer> selectedIndexes = new HashSet<>();
        if (CollUtil.isNotEmpty(fieldNames)) {
            // 反射获取字段属性
            Field[] declaredFields = clazz.getDeclaredFields();
//            // 匹配需要导入的字段
            for (int i = 0; i < declaredFields.length; i++) {
                if (fieldNames.contains(declaredFields[i].getName())) {
                    // 获取需要导入的字段下标
                    selectedIndexes.add(i);
                }
            }
        }
        return selectedIndexes;
    }
    /**
     * 导出excel
     *
     * @param list      导出数据集合
     * @param sheetName 工作表的名称
     * @param clazz     实体类
     * @param merge     是否合并单元格
     * @param os        输出流
     */
    public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge,
                                       OutputStream os, List<DropDownOptions> options, Set<Integer> selectedIndexes) {
        ExcelWriterSheetBuilder builder = EasyExcel.write(os, clazz)
            .autoCloseStream(false)
            .includeColumnIndexes(selectedIndexes)
            // 自动适配
            .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
            // 大数值自动转换 防止失真
            .registerConverter(new ExcelBigNumberConvert())
            .sheet(sheetName);
        if (merge) {
            // 合并处理器
            builder.registerWriteHandler(new CellMergeStrategy(list, true));
        }
        // 添加下拉框操作
        builder.registerWriteHandler(new ExcelDownHandler(options));
        builder.doWrite(list);
    }
    /**
     * 重置响应体
     */
    private static void resetResponse(String sheetName, HttpServletResponse response) throws UnsupportedEncodingException {
        String filename = encodingFilename(sheetName);
        FileUtils.setAttachmentResponseHeader(response, filename);
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");
    }
    /**
     * 编码文件名
     */
    public static String encodingFilename(String filename) {
        return IdUtil.fastSimpleUUID() + "_" + filename + ".xlsx";
    }
}
common/src/main/java/com/ycl/common/utils/excel/annotation/CellMerge.java
New file
@@ -0,0 +1,29 @@
package com.ycl.common.utils.excel.annotation;
import com.ycl.common.utils.excel.core.CellMergeStrategy;
import java.lang.annotation.*;
/**
 * excel 列单元格合并(合并列相同项)
 * <p>
 * 需搭配 {@link CellMergeStrategy} 策略使用
 *
 * @author Lion Li
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface CellMerge {
    /**
     * col index
     */
    int index() default -1;
    /**
     * 合并需要依赖的其他字段名称
     */
    String[] mergeBy() default {};
}
common/src/main/java/com/ycl/common/utils/excel/annotation/ExcelDictFormat.java
New file
@@ -0,0 +1,32 @@
package com.ycl.common.utils.excel.annotation;
import com.ycl.common.utils.excel.utils.StringUtils;
import java.lang.annotation.*;
/**
 * 字典格式化
 *
 * @author Lion Li
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface ExcelDictFormat {
    /**
     * 如果是字典类型,请设置字典的type值 (如: sys_user_sex)
     */
    String dictType() default "";
    /**
     * 读取内容转表达式 (如: 0=男,1=女,2=未知)
     */
    String readConverterExp() default "";
    /**
     * 分隔符,读取字符串组内容
     */
    String separator() default StringUtils.SEPARATOR;
}
common/src/main/java/com/ycl/common/utils/excel/annotation/ExcelEnumFormat.java
New file
@@ -0,0 +1,30 @@
package com.ycl.common.utils.excel.annotation;
import java.lang.annotation.*;
/**
 * 枚举格式化
 *
 * @author Liang
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface ExcelEnumFormat {
    /**
     * 字典枚举类型
     */
    Class<? extends Enum<?>> enumClass();
    /**
     * 字典枚举类中对应的code属性名称,默认为code
     */
    String codeField() default "code";
    /**
     * 字典枚举类中对应的text属性名称,默认为text
     */
    String textField() default "text";
}
common/src/main/java/com/ycl/common/utils/excel/convert/ExcelBigNumberConvert.java
New file
@@ -0,0 +1,52 @@
package com.ycl.common.utils.excel.convert;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
/**
 * 大数值转换
 * Excel 数值长度位15位 大于15位的数值转换位字符串
 *
 * @author Lion Li
 */
@Slf4j
public class ExcelBigNumberConvert implements Converter<Long> {
    @Override
    public Class<Long> supportJavaTypeKey() {
        return Long.class;
    }
    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.STRING;
    }
    @Override
    public Long convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
        return Convert.toLong(cellData.getData());
    }
    @Override
    public WriteCellData<Object> convertToExcelData(Long object, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
        if (ObjectUtil.isNotNull(object)) {
            String str = Convert.toStr(object);
            if (str.length() > 15) {
                return new WriteCellData<>(str);
            }
        }
        WriteCellData<Object> cellData = new WriteCellData<>(new BigDecimal(object));
        cellData.setType(CellDataTypeEnum.NUMBER);
        return cellData;
    }
}
common/src/main/java/com/ycl/common/utils/excel/convert/ExcelEnumConvert.java
New file
@@ -0,0 +1,87 @@
package com.ycl.common.utils.excel.convert;
import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import com.ycl.common.utils.excel.annotation.ExcelEnumFormat;
import com.ycl.common.utils.reflect.ReflectUtils;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
/**
 * 枚举格式化转换处理
 *
 * @author Liang
 */
@Slf4j
public class ExcelEnumConvert implements Converter<Object> {
    @Override
    public Class<Object> supportJavaTypeKey() {
        return Object.class;
    }
    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return null;
    }
    @Override
    public Object convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
        cellData.checkEmpty();
        // Excel中填入的是枚举中指定的描述
        Object textValue = switch (cellData.getType()) {
            case STRING, DIRECT_STRING, RICH_TEXT_STRING -> cellData.getStringValue();
            case NUMBER -> cellData.getNumberValue();
            case BOOLEAN -> cellData.getBooleanValue();
            default -> throw new IllegalArgumentException("单元格类型异常!");
        };
        // 如果是空值
        if (ObjectUtil.isNull(textValue)) {
            return null;
        }
        Map<Object, String> enumCodeToTextMap = beforeConvert(contentProperty);
        // 从Java输出至Excel是code转text
        // 因此从Excel转Java应该将text与code对调
        Map<Object, Object> enumTextToCodeMap = new HashMap<>();
        enumCodeToTextMap.forEach((key, value) -> enumTextToCodeMap.put(value, key));
        // 应该从text -> code中查找
        Object codeValue = enumTextToCodeMap.get(textValue);
        return Convert.convert(contentProperty.getField().getType(), codeValue);
    }
    @Override
    public WriteCellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
        if (ObjectUtil.isNull(object)) {
            return new WriteCellData<>("");
        }
        Map<Object, String> enumValueMap = beforeConvert(contentProperty);
        String value = Convert.toStr(enumValueMap.get(object), "");
        return new WriteCellData<>(value);
    }
    private Map<Object, String> beforeConvert(ExcelContentProperty contentProperty) {
        ExcelEnumFormat anno = getAnnotation(contentProperty.getField());
        Map<Object, String> enumValueMap = new HashMap<>();
        Enum<?>[] enumConstants = anno.enumClass().getEnumConstants();
        for (Enum<?> enumConstant : enumConstants) {
            Object codeValue = ReflectUtils.invokeGetter(enumConstant, anno.codeField());
            String textValue = ReflectUtils.invokeGetter(enumConstant, anno.textField());
            enumValueMap.put(codeValue, textValue);
        }
        return enumValueMap;
    }
    private ExcelEnumFormat getAnnotation(Field field) {
        return AnnotationUtil.getAnnotation(field, ExcelEnumFormat.class);
    }
}
common/src/main/java/com/ycl/common/utils/excel/core/CellMergeStrategy.java
New file
@@ -0,0 +1,157 @@
package com.ycl.common.utils.excel.core;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.handler.WorkbookWriteHandler;
import com.alibaba.excel.write.handler.context.WorkbookWriteHandlerContext;
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
import com.ycl.common.utils.excel.annotation.CellMerge;
import com.ycl.common.utils.excel.utils.ReflectUtils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;
import java.lang.reflect.Field;
import java.util.*;
/**
 * 列值重复合并策略
 *
 * @author Lion Li
 */
@Slf4j
public class CellMergeStrategy extends AbstractMergeStrategy implements WorkbookWriteHandler {
    private final List<CellRangeAddress> cellList;
    private final boolean hasTitle;
    private int rowIndex;
    public CellMergeStrategy(List<?> list, boolean hasTitle) {
        this.hasTitle = hasTitle;
        // 行合并开始下标
        this.rowIndex = hasTitle ? 1 : 0;
        this.cellList = handle(list, hasTitle);
    }
    @Override
    protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
        //单元格写入了,遍历合并区域,如果该Cell在区域内,但非首行,则清空
        final int rowIndex = cell.getRowIndex();
        if (CollUtil.isNotEmpty(cellList)){
            for (CellRangeAddress cellAddresses : cellList) {
                final int firstRow = cellAddresses.getFirstRow();
                if (cellAddresses.isInRange(cell) && rowIndex != firstRow){
                    cell.setBlank();
                }
            }
        }
    }
    @Override
    public void afterWorkbookDispose(final WorkbookWriteHandlerContext context) {
        //当前表格写完后,统一写入
        if (CollUtil.isNotEmpty(cellList)){
            for (CellRangeAddress item : cellList) {
                context.getWriteContext().writeSheetHolder().getSheet().addMergedRegion(item);
            }
        }
    }
    @SneakyThrows
    private List<CellRangeAddress> handle(List<?> list, boolean hasTitle) {
        List<CellRangeAddress> cellList = new ArrayList<>();
        if (CollUtil.isEmpty(list)) {
            return cellList;
        }
        Field[] fields = ReflectUtils.getFields(list.get(0).getClass(), field -> !"serialVersionUID".equals(field.getName()));
        // 有注解的字段
        List<Field> mergeFields = new ArrayList<>();
        List<Integer> mergeFieldsIndex = new ArrayList<>();
        for (int i = 0; i < fields.length; i++) {
            Field field = fields[i];
            if (field.isAnnotationPresent(CellMerge.class)) {
                CellMerge cm = field.getAnnotation(CellMerge.class);
                mergeFields.add(field);
                mergeFieldsIndex.add(cm.index() == -1 ? i : cm.index());
                if (hasTitle) {
                    ExcelProperty property = field.getAnnotation(ExcelProperty.class);
                    rowIndex = Math.max(rowIndex, property.value().length);
                }
            }
        }
        Map<Field, RepeatCell> map = new HashMap<>();
        // 生成两两合并单元格
        for (int i = 0; i < list.size(); i++) {
            for (int j = 0; j < mergeFields.size(); j++) {
                Field field = mergeFields.get(j);
                Object val = ReflectUtils.invokeGetter(list.get(i), field.getName());
                int colNum = mergeFieldsIndex.get(j);
                if (!map.containsKey(field)) {
                    map.put(field, new RepeatCell(val, i));
                } else {
                    RepeatCell repeatCell = map.get(field);
                    Object cellValue = repeatCell.getValue();
                    if (cellValue == null || "".equals(cellValue)) {
                        // 空值跳过不合并
                        continue;
                    }
                    if (!cellValue.equals(val)) {
                        if ((i - repeatCell.getCurrent() > 1)) {
                            cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
                        }
                        map.put(field, new RepeatCell(val, i));
                    } else if (i == list.size() - 1) {
                        if (i > repeatCell.getCurrent() && isMerge(list, i, field)) {
                            cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum));
                        }
                    } else if (!isMerge(list, i, field)) {
                        if ((i - repeatCell.getCurrent() > 1)) {
                            cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
                        }
                        map.put(field, new RepeatCell(val, i));
                    }
                }
            }
        }
        return cellList;
    }
    private boolean isMerge(List<?> list, int i, Field field) {
        boolean isMerge = true;
        CellMerge cm = field.getAnnotation(CellMerge.class);
        final String[] mergeBy = cm.mergeBy();
        if (StrUtil.isAllNotBlank(mergeBy)) {
            //比对当前list(i)和list(i - 1)的各个属性值一一比对 如果全为真 则为真
            for (String fieldName : mergeBy) {
                final Object valCurrent = ReflectUtil.getFieldValue(list.get(i), fieldName);
                final Object valPre = ReflectUtil.getFieldValue(list.get(i - 1), fieldName);
                if (!Objects.equals(valPre, valCurrent)) {
                    //依赖字段如有任一不等值,则标记为不可合并
                    isMerge = false;
                }
            }
        }
        return isMerge;
    }
    @Data
    @AllArgsConstructor
    static class RepeatCell {
        private Object value;
        private int current;
    }
}
common/src/main/java/com/ycl/common/utils/excel/core/DefaultExcelListener.java
New file
@@ -0,0 +1,104 @@
package com.ycl.common.utils.excel.core;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.exception.ExcelAnalysisException;
import com.alibaba.excel.exception.ExcelDataConvertException;
import com.ycl.common.utils.excel.utils.JsonUtils;
import com.ycl.common.utils.excel.utils.StreamUtils;
import com.ycl.common.utils.excel.utils.ValidatorUtils;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.Map;
import java.util.Set;
/**
 * Excel 导入监听
 *
 * @author Yjoioooo
 * @author Lion Li
 */
@Slf4j
@NoArgsConstructor
public class DefaultExcelListener<T> extends AnalysisEventListener<T> implements ExcelListener<T> {
    /**
     * 是否Validator检验,默认为是
     */
    private Boolean isValidate = Boolean.TRUE;
    /**
     * excel 表头数据
     */
    private Map<Integer, String> headMap;
    /**
     * 导入回执
     */
    private ExcelResult<T> excelResult;
    public DefaultExcelListener(boolean isValidate) {
        this.excelResult = new DefaultExcelResult<>();
        this.isValidate = isValidate;
    }
    /**
     * 处理异常
     *
     * @param exception ExcelDataConvertException
     * @param context   Excel 上下文
     */
    @Override
    public void onException(Exception exception, AnalysisContext context) throws Exception {
        String errMsg = null;
        if (exception instanceof ExcelDataConvertException excelDataConvertException) {
            // 如果是某一个单元格的转换异常 能获取到具体行号
            Integer rowIndex = excelDataConvertException.getRowIndex();
            Integer columnIndex = excelDataConvertException.getColumnIndex();
            errMsg = StrUtil.format("第{}行-第{}列-表头{}: 解析异常<br/>",
                rowIndex + 1, columnIndex + 1, headMap.get(columnIndex));
            if (log.isDebugEnabled()) {
                log.error(errMsg);
            }
        }
        if (exception instanceof ConstraintViolationException constraintViolationException) {
            Set<ConstraintViolation<?>> constraintViolations = constraintViolationException.getConstraintViolations();
            String constraintViolationsMsg = StreamUtils.join(constraintViolations, ConstraintViolation::getMessage, ", ");
            errMsg = StrUtil.format("第{}行数据校验异常: {}", context.readRowHolder().getRowIndex() + 1, constraintViolationsMsg);
            if (log.isDebugEnabled()) {
                log.error(errMsg);
            }
        }
        excelResult.getErrorList().add(errMsg);
        throw new ExcelAnalysisException(errMsg);
    }
    @Override
    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
        this.headMap = headMap;
        log.debug("解析到一条表头数据: {}", JsonUtils.toJsonString(headMap));
    }
    @Override
    public void invoke(T data, AnalysisContext context) {
        if (isValidate) {
            ValidatorUtils.validate(data);
        }
        excelResult.getList().add(data);
    }
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        log.debug("所有数据解析完成!");
    }
    @Override
    public ExcelResult<T> getExcelResult() {
        return excelResult;
    }
}
common/src/main/java/com/ycl/common/utils/excel/core/DefaultExcelResult.java
New file
@@ -0,0 +1,73 @@
package com.ycl.common.utils.excel.core;
import cn.hutool.core.util.StrUtil;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
/**
 * 默认excel返回对象
 *
 * @author Yjoioooo
 * @author Lion Li
 */
public class DefaultExcelResult<T> implements ExcelResult<T> {
    /**
     * 数据对象list
     */
    @Setter
    private List<T> list;
    /**
     * 错误信息列表
     */
    @Setter
    private List<String> errorList;
    public DefaultExcelResult() {
        this.list = new ArrayList<>();
        this.errorList = new ArrayList<>();
    }
    public DefaultExcelResult(List<T> list, List<String> errorList) {
        this.list = list;
        this.errorList = errorList;
    }
    public DefaultExcelResult(ExcelResult<T> excelResult) {
        this.list = excelResult.getList();
        this.errorList = excelResult.getErrorList();
    }
    @Override
    public List<T> getList() {
        return list;
    }
    @Override
    public List<String> getErrorList() {
        return errorList;
    }
    /**
     * 获取导入回执
     *
     * @return 导入回执
     */
    @Override
    public String getAnalysis() {
        int successCount = list.size();
        int errorCount = errorList.size();
        if (successCount == 0) {
            return "读取失败,未解析到数据";
        } else {
            if (errorCount == 0) {
                return StrUtil.format("恭喜您,全部读取成功!共{}条", successCount);
            } else {
                return "";
            }
        }
    }
}
common/src/main/java/com/ycl/common/utils/excel/core/DropDownOptions.java
New file
@@ -0,0 +1,149 @@
package com.ycl.common.utils.excel.core;
import cn.hutool.core.util.StrUtil;
import com.ycl.common.exception.ServiceException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
 * <h1>Excel下拉可选项</h1>
 * 注意:为确保下拉框解析正确,传值务必使用createOptionValue()做为值的拼接
 *
 * @author Emil.Zhang
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@SuppressWarnings("unused")
public class DropDownOptions {
    /**
     * 一级下拉所在列index,从0开始算
     */
    private int index = 0;
    /**
     * 二级下拉所在的index,从0开始算,不能与一级相同
     */
    private int nextIndex = 0;
    /**
     * 一级下拉所包含的数据
     */
    private List<String> options = new ArrayList<>();
    /**
     * 二级下拉所包含的数据Map
     * <p>以每一个一级选项值为Key,每个一级选项对应的二级数据为Value</p>
     */
    private Map<String, List<String>> nextOptions = new HashMap<>();
    /**
     * 分隔符
     */
    private static final String DELIMITER = "_";
    /**
     * 创建只有一级的下拉选
     */
    public DropDownOptions(int index, List<String> options) {
        this.index = index;
        this.options = options;
    }
    /**
     * <h2>创建每个选项可选值</h2>
     * <p>注意:不能以数字,特殊符号开头,选项中不可以包含任何运算符号</p>
     *
     * @param vars 可选值内包含的参数
     * @return 合规的可选值
     */
    public static String createOptionValue(Object... vars) {
        StringBuilder stringBuffer = new StringBuilder();
        String regex = "^[\\S\\d\\u4e00-\\u9fa5]+$";
        for (int i = 0; i < vars.length; i++) {
            String var = StrUtil.trimToEmpty(String.valueOf(vars[i]));
            if (!var.matches(regex)) {
                throw new ServiceException("选项数据不符合规则,仅允许使用中英文字符以及数字");
            }
            stringBuffer.append(var);
            if (i < vars.length - 1) {
                // 直至最后一个前,都以_作为切割线
                stringBuffer.append(DELIMITER);
            }
        }
        if (stringBuffer.toString().matches("^\\d_*$")) {
            throw new ServiceException("禁止以数字开头");
        }
        return stringBuffer.toString();
    }
    /**
     * 将处理后合理的可选值解析为原始的参数
     *
     * @param option 经过处理后的合理的可选项
     * @return 原始的参数
     */
    public static List<String> analyzeOptionValue(String option) {
        return StrUtil.split(option, DELIMITER, true, true);
    }
    /**
     * 创建级联下拉选项
     *
     * @param parentList                  父实体可选项原始数据
     * @param parentIndex                 父下拉选位置
     * @param sonList                     子实体可选项原始数据
     * @param sonIndex                    子下拉选位置
     * @param parentHowToGetIdFunction    父类如何获取唯一标识
     * @param sonHowToGetParentIdFunction 子类如何获取父类的唯一标识
     * @param howToBuildEveryOption       如何生成下拉选内容
     * @return 级联下拉选项
     */
    public static <T> DropDownOptions buildLinkedOptions(List<T> parentList,
                                                         int parentIndex,
                                                         List<T> sonList,
                                                         int sonIndex,
                                                         Function<T, Number> parentHowToGetIdFunction,
                                                         Function<T, Number> sonHowToGetParentIdFunction,
                                                         Function<T, String> howToBuildEveryOption) {
        DropDownOptions parentLinkSonOptions = new DropDownOptions();
        // 先创建父类的下拉
        parentLinkSonOptions.setIndex(parentIndex);
        parentLinkSonOptions.setOptions(
            parentList.stream()
                .map(howToBuildEveryOption)
                .collect(Collectors.toList())
        );
        // 提取父-子级联下拉
        Map<String, List<String>> sonOptions = new HashMap<>();
        // 父级依据自己的ID分组
        Map<Number, List<T>> parentGroupByIdMap =
            parentList.stream().collect(Collectors.groupingBy(parentHowToGetIdFunction));
        // 遍历每个子集,提取到Map中
        sonList.forEach(everySon -> {
            if (parentGroupByIdMap.containsKey(sonHowToGetParentIdFunction.apply(everySon))) {
                // 找到对应的上级
                T parentObj = parentGroupByIdMap.get(sonHowToGetParentIdFunction.apply(everySon)).get(0);
                // 提取名称和ID作为Key
                String key = howToBuildEveryOption.apply(parentObj);
                // Key对应的Value
                List<String> thisParentSonOptionList;
                if (sonOptions.containsKey(key)) {
                    thisParentSonOptionList = sonOptions.get(key);
                } else {
                    thisParentSonOptionList = new ArrayList<>();
                    sonOptions.put(key, thisParentSonOptionList);
                }
                // 往Value中添加当前子集选项
                thisParentSonOptionList.add(howToBuildEveryOption.apply(everySon));
            }
        });
        parentLinkSonOptions.setNextIndex(sonIndex);
        parentLinkSonOptions.setNextOptions(sonOptions);
        return parentLinkSonOptions;
    }
}
common/src/main/java/com/ycl/common/utils/excel/core/ExcelDownHandler.java
New file
@@ -0,0 +1,373 @@
package com.ycl.common.utils.excel.core;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.EnumUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.metadata.FieldCache;
import com.alibaba.excel.metadata.FieldWrapper;
import com.alibaba.excel.util.ClassUtils;
import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
import com.ycl.common.exception.ServiceException;
import com.ycl.common.utils.excel.annotation.ExcelDictFormat;
import com.ycl.common.utils.excel.annotation.ExcelEnumFormat;
import com.ycl.common.utils.excel.service.DictService;
import com.ycl.common.utils.excel.utils.StreamUtils;
import com.ycl.common.utils.excel.utils.StringUtils;
import com.ycl.common.utils.spring.SpringUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.apache.poi.ss.util.WorkbookUtil;
import org.apache.poi.xssf.usermodel.XSSFDataValidation;
import java.lang.reflect.Field;
import java.util.*;
/**
 * <h1>Excel表格下拉选操作</h1>
 * 考虑到下拉选过多可能导致Excel打开缓慢的问题,只校验前1000行
 * <p>
 * 即只有前1000行的数据可以用下拉框,超出的自行通过限制数据量的形式,第二次输出
 *
 * @author Emil.Zhang
 */
@Slf4j
public class ExcelDownHandler implements SheetWriteHandler {
    /**
     * Excel表格中的列名英文
     * 仅为了解析列英文,禁止修改
     */
    private static final String EXCEL_COLUMN_NAME = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    /**
     * 单选数据Sheet名
     */
    private static final String OPTIONS_SHEET_NAME = "options";
    /**
     * 联动选择数据Sheet名的头
     */
    private static final String LINKED_OPTIONS_SHEET_NAME = "linkedOptions";
    /**
     * 下拉可选项
     */
    private final List<DropDownOptions> dropDownOptions;
    /**
     * 当前单选进度
     */
    private int currentOptionsColumnIndex;
    /**
     * 当前联动选择进度
     */
    private int currentLinkedOptionsSheetIndex;
    private final DictService dictService;
    public ExcelDownHandler(List<DropDownOptions> options) {
        this.dropDownOptions = options;
        this.currentOptionsColumnIndex = 0;
        this.currentLinkedOptionsSheetIndex = 0;
        this.dictService = SpringUtils.getBean(DictService.class);
    }
    /**
     * <h2>开始创建下拉数据</h2>
     * 1.通过解析传入的@ExcelProperty同级是否标注有@DropDown选项
     * 如果有且设置了value值,则将其直接置为下拉可选项
     * <p>
     * 2.或者在调用ExcelUtil时指定了可选项,将依据传入的可选项做下拉
     * <p>
     * 3.二者并存,注意调用方式
     */
    @Override
    public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
        Sheet sheet = writeSheetHolder.getSheet();
        // 开始设置下拉框 HSSFWorkbook
        DataValidationHelper helper = sheet.getDataValidationHelper();
        Workbook workbook = writeWorkbookHolder.getWorkbook();
        FieldCache fieldCache = ClassUtils.declaredFields(writeWorkbookHolder.getClazz(), writeWorkbookHolder);
        for (Map.Entry<Integer, FieldWrapper> entry : fieldCache.getSortedFieldMap().entrySet()) {
            Integer index = entry.getKey();
            FieldWrapper wrapper = entry.getValue();
            Field field = wrapper.getField();
            // 循环实体中的每个属性
            // 可选的下拉值
            List<String> options = new ArrayList<>();
            if (field.isAnnotationPresent(ExcelDictFormat.class)) {
                // 如果指定了@ExcelDictFormat,则使用字典的逻辑
                ExcelDictFormat format = field.getDeclaredAnnotation(ExcelDictFormat.class);
                String dictType = format.dictType();
                String converterExp = format.readConverterExp();
                if (StringUtils.isNotBlank(dictType)) {
                    // 如果传递了字典名,则依据字典建立下拉
                    Collection<String> values = Optional.ofNullable(dictService.getAllDictByDictType(dictType))
                        .orElseThrow(() -> new ServiceException(String.format("字典 %s 不存在", dictType)))
                        .values();
                    options = new ArrayList<>(values);
                } else if (StringUtils.isNotBlank(converterExp)) {
                    // 如果指定了确切的值,则直接解析确切的值
                    List<String> strList = StringUtils.splitList(converterExp, format.separator());
                    options = StreamUtils.toList(strList, s -> StringUtils.split(s, "=")[1]);
                }
            } else if (field.isAnnotationPresent(ExcelEnumFormat.class)) {
                // 否则如果指定了@ExcelEnumFormat,则使用枚举的逻辑
                ExcelEnumFormat format = field.getDeclaredAnnotation(ExcelEnumFormat.class);
                List<Object> values = EnumUtil.getFieldValues(format.enumClass(), format.textField());
                options = StreamUtils.toList(values, String::valueOf);
            }
            if (ObjectUtil.isNotEmpty(options)) {
                // 仅当下拉可选项不为空时执行
                if (options.size() > 20) {
                    // 这里限制如果可选项大于20,则使用额外表形式
                    dropDownWithSheet(helper, workbook, sheet, index, options);
                } else {
                    // 否则使用固定值形式
                    dropDownWithSimple(helper, sheet, index, options);
                }
            }
        }
        if (CollUtil.isEmpty(dropDownOptions)) {
            return;
        }
        dropDownOptions.forEach(everyOptions -> {
            // 如果传递了下拉框选择器参数
            if (!everyOptions.getNextOptions().isEmpty()) {
                // 当二级选项不为空时,使用额外关联表的形式
                dropDownLinkedOptions(helper, workbook, sheet, everyOptions);
            } else if (everyOptions.getOptions().size() > 10) {
                // 当一级选项参数个数大于10,使用额外表的形式
                dropDownWithSheet(helper, workbook, sheet, everyOptions.getIndex(), everyOptions.getOptions());
            } else if (everyOptions.getOptions().size() != 0) {
                // 当一级选项个数不为空,使用默认形式
                dropDownWithSimple(helper, sheet, everyOptions.getIndex(), everyOptions.getOptions());
            }
        });
    }
    /**
     * <h2>简单下拉框</h2>
     * 直接将可选项拼接为指定列的数据校验值
     *
     * @param celIndex 列index
     * @param value    下拉选可选值
     */
    private void dropDownWithSimple(DataValidationHelper helper, Sheet sheet, Integer celIndex, List<String> value) {
        if (ObjectUtil.isEmpty(value)) {
            return;
        }
        this.markOptionsToSheet(helper, sheet, celIndex, helper.createExplicitListConstraint(ArrayUtil.toArray(value, String.class)));
    }
    /**
     * <h2>额外表格形式的级联下拉框</h2>
     *
     * @param options 额外表格形式存储的下拉可选项
     */
    private void dropDownLinkedOptions(DataValidationHelper helper, Workbook workbook, Sheet sheet, DropDownOptions options) {
        String linkedOptionsSheetName = String.format("%s_%d", LINKED_OPTIONS_SHEET_NAME, currentLinkedOptionsSheetIndex);
        // 创建联动下拉数据表
        Sheet linkedOptionsDataSheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(linkedOptionsSheetName));
        // 将下拉表隐藏
        workbook.setSheetHidden(workbook.getSheetIndex(linkedOptionsDataSheet), true);
        // 完善横向的一级选项数据表
        List<String> firstOptions = options.getOptions();
        Map<String, List<String>> secoundOptionsMap = options.getNextOptions();
        // 创建名称管理器
        Name name = workbook.createName();
        // 设置名称管理器的别名
        name.setNameName(linkedOptionsSheetName);
        // 以横向第一行创建一级下拉拼接引用位置
        String firstOptionsFunction = String.format("%s!$%s$1:$%s$1",
            linkedOptionsSheetName,
            getExcelColumnName(0),
            getExcelColumnName(firstOptions.size())
        );
        // 设置名称管理器的引用位置
        name.setRefersToFormula(firstOptionsFunction);
        // 设置数据校验为序列模式,引用的是名称管理器中的别名
        this.markOptionsToSheet(helper, sheet, options.getIndex(), helper.createFormulaListConstraint(linkedOptionsSheetName));
        for (int columIndex = 0; columIndex < firstOptions.size(); columIndex++) {
            // 先提取主表中一级下拉的列名
            String firstOptionsColumnName = getExcelColumnName(columIndex);
            // 一次循环是每一个一级选项
            int finalI = columIndex;
            // 本次循环的一级选项值
            String thisFirstOptionsValue = firstOptions.get(columIndex);
            // 创建第一行的数据
            Optional.ofNullable(linkedOptionsDataSheet.getRow(0))
                // 如果不存在则创建第一行
                .orElseGet(() -> linkedOptionsDataSheet.createRow(finalI))
                // 第一行当前列
                .createCell(columIndex)
                // 设置值为当前一级选项值
                .setCellValue(thisFirstOptionsValue);
            // 第二行开始,设置第二级别选项参数
            List<String> secondOptions = secoundOptionsMap.get(thisFirstOptionsValue);
            if (CollUtil.isEmpty(secondOptions)) {
                // 必须保证至少有一个关联选项,否则将导致Excel解析错误
                secondOptions = Collections.singletonList("暂无_0");
            }
            // 以该一级选项值创建子名称管理器
            Name sonName = workbook.createName();
            // 设置名称管理器的别名
            sonName.setNameName(thisFirstOptionsValue);
            // 以第二行该列数据拼接引用位置
            String sonFunction = String.format("%s!$%s$2:$%s$%d",
                linkedOptionsSheetName,
                firstOptionsColumnName,
                firstOptionsColumnName,
                secondOptions.size() + 1
            );
            // 设置名称管理器的引用位置
            sonName.setRefersToFormula(sonFunction);
            // 数据验证为序列模式,引用到每一个主表中的二级选项位置
            // 创建子项的名称管理器,只是为了使得Excel可以识别到数据
            String mainSheetFirstOptionsColumnName = getExcelColumnName(options.getIndex());
            for (int i = 0; i < 100; i++) {
                // 以一级选项对应的主体所在位置创建二级下拉
                String secondOptionsFunction = String.format("=INDIRECT(%s%d)", mainSheetFirstOptionsColumnName, i + 1);
                // 二级只能主表每一行的每一列添加二级校验
                markLinkedOptionsToSheet(helper, sheet, i, options.getNextIndex(), helper.createFormulaListConstraint(secondOptionsFunction));
            }
            for (int rowIndex = 0; rowIndex < secondOptions.size(); rowIndex++) {
                // 从第二行开始填充二级选项
                int finalRowIndex = rowIndex + 1;
                int finalColumIndex = columIndex;
                Row row = Optional.ofNullable(linkedOptionsDataSheet.getRow(finalRowIndex))
                    // 没有则创建
                    .orElseGet(() -> linkedOptionsDataSheet.createRow(finalRowIndex));
                Optional
                    // 在本级一级选项所在的列
                    .ofNullable(row.getCell(finalColumIndex))
                    // 不存在则创建
                    .orElseGet(() -> row.createCell(finalColumIndex))
                    // 设置二级选项值
                    .setCellValue(secondOptions.get(rowIndex));
            }
        }
        currentLinkedOptionsSheetIndex++;
    }
    /**
     * <h2>额外表格形式的普通下拉框</h2>
     * 由于下拉框可选值数量过多,为提升Excel打开效率,使用额外表格形式做下拉
     *
     * @param celIndex 下拉选
     * @param value    下拉选可选值
     */
    private void dropDownWithSheet(DataValidationHelper helper, Workbook workbook, Sheet sheet, Integer celIndex, List<String> value) {
        // 创建下拉数据表
        Sheet simpleDataSheet = Optional.ofNullable(workbook.getSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME)))
            .orElseGet(() -> workbook.createSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME)));
        // 将下拉表隐藏
        workbook.setSheetHidden(workbook.getSheetIndex(simpleDataSheet), true);
        // 完善纵向的一级选项数据表
        for (int i = 0; i < value.size(); i++) {
            int finalI = i;
            // 获取每一选项行,如果没有则创建
            Row row = Optional.ofNullable(simpleDataSheet.getRow(i))
                .orElseGet(() -> simpleDataSheet.createRow(finalI));
            // 获取本级选项对应的选项列,如果没有则创建
            Cell cell = Optional.ofNullable(row.getCell(currentOptionsColumnIndex))
                .orElseGet(() -> row.createCell(currentOptionsColumnIndex));
            // 设置值
            cell.setCellValue(value.get(i));
        }
        // 创建名称管理器
        Name name = workbook.createName();
        // 设置名称管理器的别名
        String nameName = String.format("%s_%d", OPTIONS_SHEET_NAME, celIndex);
        name.setNameName(nameName);
        // 以纵向第一列创建一级下拉拼接引用位置
        String function = String.format("%s!$%s$1:$%s$%d",
            OPTIONS_SHEET_NAME,
            getExcelColumnName(currentOptionsColumnIndex),
            getExcelColumnName(currentOptionsColumnIndex),
            value.size());
        // 设置名称管理器的引用位置
        name.setRefersToFormula(function);
        // 设置数据校验为序列模式,引用的是名称管理器中的别名
        this.markOptionsToSheet(helper, sheet, celIndex, helper.createFormulaListConstraint(nameName));
        currentOptionsColumnIndex++;
    }
    /**
     * 挂载下拉的列,仅限一级选项
     */
    private void markOptionsToSheet(DataValidationHelper helper, Sheet sheet, Integer celIndex,
                                    DataValidationConstraint constraint) {
        // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
        CellRangeAddressList addressList = new CellRangeAddressList(1, 1000, celIndex, celIndex);
        markDataValidationToSheet(helper, sheet, constraint, addressList);
    }
    /**
     * 挂载下拉的列,仅限二级选项
     */
    private void markLinkedOptionsToSheet(DataValidationHelper helper, Sheet sheet, Integer rowIndex,
                                          Integer celIndex, DataValidationConstraint constraint) {
        // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
        CellRangeAddressList addressList = new CellRangeAddressList(rowIndex, rowIndex, celIndex, celIndex);
        markDataValidationToSheet(helper, sheet, constraint, addressList);
    }
    /**
     * 应用数据校验
     */
    private void markDataValidationToSheet(DataValidationHelper helper, Sheet sheet,
                                           DataValidationConstraint constraint, CellRangeAddressList addressList) {
        // 数据有效性对象
        DataValidation dataValidation = helper.createValidation(constraint, addressList);
        // 处理Excel兼容性问题
        if (dataValidation instanceof XSSFDataValidation) {
            //数据校验
            dataValidation.setSuppressDropDownArrow(true);
            //错误提示
            dataValidation.setErrorStyle(DataValidation.ErrorStyle.STOP);
            dataValidation.createErrorBox("提示", "此值与单元格定义数据不一致");
            dataValidation.setShowErrorBox(true);
            //选定提示
            dataValidation.createPromptBox("填写说明:", "填写内容只能为下拉中数据,其他数据将导致导入失败");
            dataValidation.setShowPromptBox(true);
            sheet.addValidationData(dataValidation);
        } else {
            dataValidation.setSuppressDropDownArrow(false);
        }
        sheet.addValidationData(dataValidation);
    }
    /**
     * <h2>依据列index获取列名英文</h2>
     * 依据列index转换为Excel中的列名英文
     * <p>例如第1列,index为0,解析出来为A列</p>
     * 第27列,index为26,解析为AA列
     * <p>第28列,index为27,解析为AB列</p>
     *
     * @param columnIndex 列index
     * @return 列index所在得英文名
     */
    private String getExcelColumnName(int columnIndex) {
        // 26一循环的次数
        int columnCircleCount = columnIndex / 26;
        // 26一循环内的位置
        int thisCircleColumnIndex = columnIndex % 26;
        // 26一循环的次数大于0,则视为栏名至少两位
        String columnPrefix = columnCircleCount == 0
            ? StrUtil.EMPTY
            : StrUtil.subWithLength(EXCEL_COLUMN_NAME, columnCircleCount - 1, 1);
        // 从26一循环内取对应的栏位名
        String columnNext = StrUtil.subWithLength(EXCEL_COLUMN_NAME, thisCircleColumnIndex, 1);
        // 将二者拼接即为最终的栏位名
        return columnPrefix + columnNext;
    }
}
common/src/main/java/com/ycl/common/utils/excel/core/ExcelListener.java
New file
@@ -0,0 +1,14 @@
package com.ycl.common.utils.excel.core;
import com.alibaba.excel.read.listener.ReadListener;
/**
 * Excel 导入监听
 *
 * @author Lion Li
 */
public interface ExcelListener<T> extends ReadListener<T> {
    ExcelResult<T> getExcelResult();
}
common/src/main/java/com/ycl/common/utils/excel/core/ExcelResult.java
New file
@@ -0,0 +1,26 @@
package com.ycl.common.utils.excel.core;
import java.util.List;
/**
 * excel返回对象
 *
 * @author Lion Li
 */
public interface ExcelResult<T> {
    /**
     * 对象列表
     */
    List<T> getList();
    /**
     * 错误列表
     */
    List<String> getErrorList();
    /**
     * 导入回执
     */
    String getAnalysis();
}
common/src/main/java/com/ycl/common/utils/excel/service/DictService.java
New file
@@ -0,0 +1,67 @@
package com.ycl.common.utils.excel.service;
import java.util.Map;
/**
 * 通用 字典服务
 *
 * @author Lion Li
 */
public interface DictService {
    /**
     * 分隔符
     */
    String SEPARATOR = ",";
    /**
     * 根据字典类型和字典值获取字典标签
     *
     * @param dictType  字典类型
     * @param dictValue 字典值
     * @return 字典标签
     */
    default String getDictLabel(String dictType, String dictValue) {
        return getDictLabel(dictType, dictValue, SEPARATOR);
    }
    /**
     * 根据字典类型和字典标签获取字典值
     *
     * @param dictType  字典类型
     * @param dictLabel 字典标签
     * @return 字典值
     */
    default String getDictValue(String dictType, String dictLabel) {
        return getDictValue(dictType, dictLabel, SEPARATOR);
    }
    /**
     * 根据字典类型和字典值获取字典标签
     *
     * @param dictType  字典类型
     * @param dictValue 字典值
     * @param separator 分隔符
     * @return 字典标签
     */
    String getDictLabel(String dictType, String dictValue, String separator);
    /**
     * 根据字典类型和字典标签获取字典值
     *
     * @param dictType  字典类型
     * @param dictLabel 字典标签
     * @param separator 分隔符
     * @return 字典值
     */
    String getDictValue(String dictType, String dictLabel, String separator);
    /**
     * 获取字典下所有的字典值与标签
     *
     * @param dictType 字典类型
     * @return dictValue为key,dictLabel为值组成的Map
     */
    Map<String, String> getAllDictByDictType(String dictType);
}
common/src/main/java/com/ycl/common/utils/excel/utils/JsonUtils.java
New file
@@ -0,0 +1,169 @@
package com.ycl.common.utils.excel.utils;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.ycl.common.utils.spring.SpringUtils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
 * JSON 工具类
 *
 * @author 芋道源码
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class JsonUtils {
    private static final ObjectMapper OBJECT_MAPPER = SpringUtils.getBean(ObjectMapper.class);
    public static ObjectMapper getObjectMapper() {
        return OBJECT_MAPPER;
    }
    /**
     * 将对象转换为JSON格式的字符串
     *
     * @param object 要转换的对象
     * @return JSON格式的字符串,如果对象为null,则返回null
     * @throws RuntimeException 如果转换过程中发生JSON处理异常,则抛出运行时异常
     */
    public static String toJsonString(Object object) {
        if (ObjectUtil.isNull(object)) {
            return null;
        }
        try {
            return OBJECT_MAPPER.writeValueAsString(object);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }
    /**
     * 将JSON格式的字符串转换为指定类型的对象
     *
     * @param text  JSON格式的字符串
     * @param clazz 要转换的目标对象类型
     * @param <T>   目标对象的泛型类型
     * @return 转换后的对象,如果字符串为空则返回null
     * @throws RuntimeException 如果转换过程中发生IO异常,则抛出运行时异常
     */
    public static <T> T parseObject(String text, Class<T> clazz) {
        if (StringUtils.isEmpty(text)) {
            return null;
        }
        try {
            return OBJECT_MAPPER.readValue(text, clazz);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    /**
     * 将字节数组转换为指定类型的对象
     *
     * @param bytes 字节数组
     * @param clazz 要转换的目标对象类型
     * @param <T>   目标对象的泛型类型
     * @return 转换后的对象,如果字节数组为空则返回null
     * @throws RuntimeException 如果转换过程中发生IO异常,则抛出运行时异常
     */
    public static <T> T parseObject(byte[] bytes, Class<T> clazz) {
        if (ArrayUtil.isEmpty(bytes)) {
            return null;
        }
        try {
            return OBJECT_MAPPER.readValue(bytes, clazz);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    /**
     * 将JSON格式的字符串转换为指定类型的对象,支持复杂类型
     *
     * @param text          JSON格式的字符串
     * @param typeReference 指定类型的TypeReference对象
     * @param <T>           目标对象的泛型类型
     * @return 转换后的对象,如果字符串为空则返回null
     * @throws RuntimeException 如果转换过程中发生IO异常,则抛出运行时异常
     */
    public static <T> T parseObject(String text, TypeReference<T> typeReference) {
        if (StringUtils.isBlank(text)) {
            return null;
        }
        try {
            return OBJECT_MAPPER.readValue(text, typeReference);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    /**
     * 将JSON格式的字符串转换为Dict对象
     *
     * @param text JSON格式的字符串
     * @return 转换后的Dict对象,如果字符串为空或者不是JSON格式则返回null
     * @throws RuntimeException 如果转换过程中发生IO异常,则抛出运行时异常
     */
    public static Dict parseMap(String text) {
        if (StringUtils.isBlank(text)) {
            return null;
        }
        try {
            return OBJECT_MAPPER.readValue(text, OBJECT_MAPPER.getTypeFactory().constructType(Dict.class));
        } catch (MismatchedInputException e) {
            // 类型不匹配说明不是json
            return null;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    /**
     * 将JSON格式的字符串转换为Dict对象的列表
     *
     * @param text JSON格式的字符串
     * @return 转换后的Dict对象的列表,如果字符串为空则返回null
     * @throws RuntimeException 如果转换过程中发生IO异常,则抛出运行时异常
     */
    public static List<Dict> parseArrayMap(String text) {
        if (StringUtils.isBlank(text)) {
            return null;
        }
        try {
            return OBJECT_MAPPER.readValue(text, OBJECT_MAPPER.getTypeFactory().constructCollectionType(List.class, Dict.class));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    /**
     * 将JSON格式的字符串转换为指定类型对象的列表
     *
     * @param text  JSON格式的字符串
     * @param clazz 要转换的目标对象类型
     * @param <T>   目标对象的泛型类型
     * @return 转换后的对象的列表,如果字符串为空则返回空列表
     * @throws RuntimeException 如果转换过程中发生IO异常,则抛出运行时异常
     */
    public static <T> List<T> parseArray(String text, Class<T> clazz) {
        if (StringUtils.isEmpty(text)) {
            return new ArrayList<>();
        }
        try {
            return OBJECT_MAPPER.readValue(text, OBJECT_MAPPER.getTypeFactory().constructCollectionType(List.class, clazz));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
common/src/main/java/com/ycl/common/utils/excel/utils/ReflectUtils.java
New file
@@ -0,0 +1,55 @@
package com.ycl.common.utils.excel.utils;
import cn.hutool.core.util.ReflectUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import java.lang.reflect.Method;
/**
 * 反射工具类. 提供调用getter/setter方法, 访问私有变量, 调用私有方法, 获取泛型类型Class, 被AOP过的真实类等工具函数.
 *
 * @author Lion Li
 */
@SuppressWarnings("rawtypes")
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ReflectUtils extends ReflectUtil {
    private static final String SETTER_PREFIX = "set";
    private static final String GETTER_PREFIX = "get";
    /**
     * 调用Getter方法.
     * 支持多级,如:对象名.对象名.方法
     */
    @SuppressWarnings("unchecked")
    public static <E> E invokeGetter(Object obj, String propertyName) {
        Object object = obj;
        for (String name : StringUtils.split(propertyName, ".")) {
            String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(name);
            object = invoke(object, getterMethodName);
        }
        return (E) object;
    }
    /**
     * 调用Setter方法, 仅匹配方法名。
     * 支持多级,如:对象名.对象名.方法
     */
    public static <E> void invokeSetter(Object obj, String propertyName, E value) {
        Object object = obj;
        String[] names = StringUtils.split(propertyName, ".");
        for (int i = 0; i < names.length; i++) {
            if (i < names.length - 1) {
                String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(names[i]);
                object = invoke(object, getterMethodName);
            } else {
                String setterMethodName = SETTER_PREFIX + StringUtils.capitalize(names[i]);
                Method method = getMethodByName(object.getClass(), setterMethodName);
                invoke(object, method, value);
            }
        }
    }
}
common/src/main/java/com/ycl/common/utils/excel/utils/StreamUtils.java
New file
@@ -0,0 +1,282 @@
package com.ycl.common.utils.excel.utils;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
 * stream 流工具类
 *
 * @author Lion Li
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class StreamUtils {
    /**
     * 将collection过滤
     *
     * @param collection 需要转化的集合
     * @param function   过滤方法
     * @return 过滤后的list
     */
    public static <E> List<E> filter(Collection<E> collection, Predicate<E> function) {
        if (CollUtil.isEmpty(collection)) {
            return CollUtil.newArrayList();
        }
        // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
        return collection.stream().filter(function).collect(Collectors.toList());
    }
    /**
     * 找到流中满足条件的第一个元素
     *
     * @param collection 需要查询的集合
     * @param function   过滤方法
     * @return 找到符合条件的第一个元素,没有则返回null
     */
    public static <E> E findFirst(Collection<E> collection, Predicate<E> function) {
        if (CollUtil.isEmpty(collection)) {
            return null;
        }
        return collection.stream().filter(function).findFirst().orElse(null);
    }
    /**
     * 找到流中任意一个满足条件的元素
     *
     * @param collection 需要查询的集合
     * @param function   过滤方法
     * @return 找到符合条件的任意一个元素,没有则返回null
     */
    public static <E> Optional<E> findAny(Collection<E> collection, Predicate<E> function) {
        if (CollUtil.isEmpty(collection)) {
            return Optional.empty();
        }
        return collection.stream().filter(function).findAny();
    }
    /**
     * 将collection拼接
     *
     * @param collection 需要转化的集合
     * @param function   拼接方法
     * @return 拼接后的list
     */
    public static <E> String join(Collection<E> collection, Function<E, String> function) {
        return join(collection, function, StringUtils.SEPARATOR);
    }
    /**
     * 将collection拼接
     *
     * @param collection 需要转化的集合
     * @param function   拼接方法
     * @param delimiter  拼接符
     * @return 拼接后的list
     */
    public static <E> String join(Collection<E> collection, Function<E, String> function, CharSequence delimiter) {
        if (CollUtil.isEmpty(collection)) {
            return StringUtils.EMPTY;
        }
        return collection.stream().map(function).filter(Objects::nonNull).collect(Collectors.joining(delimiter));
    }
    /**
     * 将collection排序
     *
     * @param collection 需要转化的集合
     * @param comparing  排序方法
     * @return 排序后的list
     */
    public static <E> List<E> sorted(Collection<E> collection, Comparator<E> comparing) {
        if (CollUtil.isEmpty(collection)) {
            return CollUtil.newArrayList();
        }
        // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
        return collection.stream().filter(Objects::nonNull).sorted(comparing).collect(Collectors.toList());
    }
    /**
     * 将collection转化为类型不变的map<br>
     * <B>{@code Collection<V>  ---->  Map<K,V>}</B>
     *
     * @param collection 需要转化的集合
     * @param key        V类型转化为K类型的lambda方法
     * @param <V>        collection中的泛型
     * @param <K>        map中的key类型
     * @return 转化后的map
     */
    public static <V, K> Map<K, V> toIdentityMap(Collection<V> collection, Function<V, K> key) {
        if (CollUtil.isEmpty(collection)) {
            return MapUtil.newHashMap();
        }
        return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
    }
    /**
     * 将Collection转化为map(value类型与collection的泛型不同)<br>
     * <B>{@code Collection<E> -----> Map<K,V>  }</B>
     *
     * @param collection 需要转化的集合
     * @param key        E类型转化为K类型的lambda方法
     * @param value      E类型转化为V类型的lambda方法
     * @param <E>        collection中的泛型
     * @param <K>        map中的key类型
     * @param <V>        map中的value类型
     * @return 转化后的map
     */
    public static <E, K, V> Map<K, V> toMap(Collection<E> collection, Function<E, K> key, Function<E, V> value) {
        if (CollUtil.isEmpty(collection)) {
            return MapUtil.newHashMap();
        }
        return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, value, (l, r) -> l));
    }
    /**
     * 将collection按照规则(比如有相同的班级id)分类成map<br>
     * <B>{@code Collection<E> -------> Map<K,List<E>> } </B>
     *
     * @param collection 需要分类的集合
     * @param key        分类的规则
     * @param <E>        collection中的泛型
     * @param <K>        map中的key类型
     * @return 分类后的map
     */
    public static <E, K> Map<K, List<E>> groupByKey(Collection<E> collection, Function<E, K> key) {
        if (CollUtil.isEmpty(collection)) {
            return MapUtil.newHashMap();
        }
        return collection
            .stream().filter(Objects::nonNull)
            .collect(Collectors.groupingBy(key, LinkedHashMap::new, Collectors.toList()));
    }
    /**
     * 将collection按照两个规则(比如有相同的年级id,班级id)分类成双层map<br>
     * <B>{@code Collection<E>  --->  Map<T,Map<U,List<E>>> } </B>
     *
     * @param collection 需要分类的集合
     * @param key1       第一个分类的规则
     * @param key2       第二个分类的规则
     * @param <E>        集合元素类型
     * @param <K>        第一个map中的key类型
     * @param <U>        第二个map中的key类型
     * @return 分类后的map
     */
    public static <E, K, U> Map<K, Map<U, List<E>>> groupBy2Key(Collection<E> collection, Function<E, K> key1, Function<E, U> key2) {
        if (CollUtil.isEmpty(collection)) {
            return MapUtil.newHashMap();
        }
        return collection
            .stream().filter(Objects::nonNull)
            .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.groupingBy(key2, LinkedHashMap::new, Collectors.toList())));
    }
    /**
     * 将collection按照两个规则(比如有相同的年级id,班级id)分类成双层map<br>
     * <B>{@code Collection<E>  --->  Map<T,Map<U,E>> } </B>
     *
     * @param collection 需要分类的集合
     * @param key1       第一个分类的规则
     * @param key2       第二个分类的规则
     * @param <T>        第一个map中的key类型
     * @param <U>        第二个map中的key类型
     * @param <E>        collection中的泛型
     * @return 分类后的map
     */
    public static <E, T, U> Map<T, Map<U, E>> group2Map(Collection<E> collection, Function<E, T> key1, Function<E, U> key2) {
        if (CollUtil.isEmpty(collection) || key1 == null || key2 == null) {
            return MapUtil.newHashMap();
        }
        return collection
            .stream().filter(Objects::nonNull)
            .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.toMap(key2, Function.identity(), (l, r) -> l)));
    }
    /**
     * 将collection转化为List集合,但是两者的泛型不同<br>
     * <B>{@code Collection<E>  ------>  List<T> } </B>
     *
     * @param collection 需要转化的集合
     * @param function   collection中的泛型转化为list泛型的lambda表达式
     * @param <E>        collection中的泛型
     * @param <T>        List中的泛型
     * @return 转化后的list
     */
    public static <E, T> List<T> toList(Collection<E> collection, Function<E, T> function) {
        if (CollUtil.isEmpty(collection)) {
            return CollUtil.newArrayList();
        }
        return collection
            .stream()
            .map(function)
            .filter(Objects::nonNull)
            // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
            .collect(Collectors.toList());
    }
    /**
     * 将collection转化为Set集合,但是两者的泛型不同<br>
     * <B>{@code Collection<E>  ------>  Set<T> } </B>
     *
     * @param collection 需要转化的集合
     * @param function   collection中的泛型转化为set泛型的lambda表达式
     * @param <E>        collection中的泛型
     * @param <T>        Set中的泛型
     * @return 转化后的Set
     */
    public static <E, T> Set<T> toSet(Collection<E> collection, Function<E, T> function) {
        if (CollUtil.isEmpty(collection) || function == null) {
            return CollUtil.newHashSet();
        }
        return collection
            .stream()
            .map(function)
            .filter(Objects::nonNull)
            .collect(Collectors.toSet());
    }
    /**
     * 合并两个相同key类型的map
     *
     * @param map1  第一个需要合并的 map
     * @param map2  第二个需要合并的 map
     * @param merge 合并的lambda,将key  value1 value2合并成最终的类型,注意value可能为空的情况
     * @param <K>   map中的key类型
     * @param <X>   第一个 map的value类型
     * @param <Y>   第二个 map的value类型
     * @param <V>   最终map的value类型
     * @return 合并后的map
     */
    public static <K, X, Y, V> Map<K, V> merge(Map<K, X> map1, Map<K, Y> map2, BiFunction<X, Y, V> merge) {
        if (MapUtil.isEmpty(map1) && MapUtil.isEmpty(map2)) {
            return MapUtil.newHashMap();
        } else if (MapUtil.isEmpty(map1)) {
            map1 = MapUtil.newHashMap();
        } else if (MapUtil.isEmpty(map2)) {
            map2 = MapUtil.newHashMap();
        }
        Set<K> key = new HashSet<>();
        key.addAll(map1.keySet());
        key.addAll(map2.keySet());
        Map<K, V> map = new HashMap<>();
        for (K t : key) {
            X x = map1.get(t);
            Y y = map2.get(t);
            V z = merge.apply(x, y);
            if (z != null) {
                map.put(t, z);
            }
        }
        return map;
    }
}
common/src/main/java/com/ycl/common/utils/excel/utils/StringUtils.java
New file
@@ -0,0 +1,323 @@
package com.ycl.common.utils.excel.utils;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.StrUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.util.AntPathMatcher;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
 * 字符串工具类
 *
 * @author Lion Li
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class StringUtils extends org.apache.commons.lang3.StringUtils {
    public static final String SEPARATOR = ",";
    public static final String SLASH = "/";
    /**
     * 获取参数不为空值
     *
     * @param str defaultValue 要判断的value
     * @return value 返回值
     */
    public static String blankToDefault(String str, String defaultValue) {
        return StrUtil.blankToDefault(str, defaultValue);
    }
    /**
     * * 判断一个字符串是否为空串
     *
     * @param str String
     * @return true:为空 false:非空
     */
    public static boolean isEmpty(String str) {
        return StrUtil.isEmpty(str);
    }
    /**
     * * 判断一个字符串是否为非空串
     *
     * @param str String
     * @return true:非空串 false:空串
     */
    public static boolean isNotEmpty(String str) {
        return !isEmpty(str);
    }
    /**
     * 去空格
     */
    public static String trim(String str) {
        return StrUtil.trim(str);
    }
    /**
     * 截取字符串
     *
     * @param str   字符串
     * @param start 开始
     * @return 结果
     */
    public static String substring(final String str, int start) {
        return substring(str, start, str.length());
    }
    /**
     * 截取字符串
     *
     * @param str   字符串
     * @param start 开始
     * @param end   结束
     * @return 结果
     */
    public static String substring(final String str, int start, int end) {
        return StrUtil.sub(str, start, end);
    }
    /**
     * 格式化文本, {} 表示占位符<br>
     * 此方法只是简单将占位符 {} 按照顺序替换为参数<br>
     * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可<br>
     * 例:<br>
     * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b<br>
     * 转义{}: format("this is \\{} for {}", "a", "b") -> this is {} for a<br>
     * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b<br>
     *
     * @param template 文本模板,被替换的部分用 {} 表示
     * @param params   参数值
     * @return 格式化后的文本
     */
    public static String format(String template, Object... params) {
        return StrUtil.format(template, params);
    }
    /**
     * 是否为http(s)://开头
     *
     * @param link 链接
     * @return 结果
     */
    public static boolean ishttp(String link) {
        return Validator.isUrl(link);
    }
    /**
     * 字符串转set
     *
     * @param str 字符串
     * @param sep 分隔符
     * @return set集合
     */
    public static Set<String> str2Set(String str, String sep) {
        return new HashSet<>(str2List(str, sep, true, false));
    }
    /**
     * 字符串转list
     *
     * @param str         字符串
     * @param sep         分隔符
     * @param filterBlank 过滤纯空白
     * @param trim        去掉首尾空白
     * @return list集合
     */
    public static List<String> str2List(String str, String sep, boolean filterBlank, boolean trim) {
        List<String> list = new ArrayList<>();
        if (isEmpty(str)) {
            return list;
        }
        // 过滤空白字符串
        if (filterBlank && isBlank(str)) {
            return list;
        }
        String[] split = str.split(sep);
        for (String string : split) {
            if (filterBlank && isBlank(string)) {
                continue;
            }
            if (trim) {
                string = trim(string);
            }
            list.add(string);
        }
        return list;
    }
    /**
     * 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写
     *
     * @param cs                  指定字符串
     * @param searchCharSequences 需要检查的字符串数组
     * @return 是否包含任意一个字符串
     */
    public static boolean containsAnyIgnoreCase(CharSequence cs, CharSequence... searchCharSequences) {
        return StrUtil.containsAnyIgnoreCase(cs, searchCharSequences);
    }
    /**
     * 驼峰转下划线命名
     */
    public static String toUnderScoreCase(String str) {
        return StrUtil.toUnderlineCase(str);
    }
    /**
     * 是否包含字符串
     *
     * @param str  验证字符串
     * @param strs 字符串组
     * @return 包含返回true
     */
    public static boolean inStringIgnoreCase(String str, String... strs) {
        return StrUtil.equalsAnyIgnoreCase(str, strs);
    }
    /**
     * 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld
     *
     * @param name 转换前的下划线大写方式命名的字符串
     * @return 转换后的驼峰式命名的字符串
     */
    public static String convertToCamelCase(String name) {
        return StrUtil.upperFirst(StrUtil.toCamelCase(name));
    }
    /**
     * 驼峰式命名法 例如:user_name->userName
     */
    public static String toCamelCase(String s) {
        return StrUtil.toCamelCase(s);
    }
    /**
     * 查找指定字符串是否匹配指定字符串列表中的任意一个字符串
     *
     * @param str  指定字符串
     * @param strs 需要检查的字符串数组
     * @return 是否匹配
     */
    public static boolean matches(String str, List<String> strs) {
        if (isEmpty(str) || CollUtil.isEmpty(strs)) {
            return false;
        }
        for (String pattern : strs) {
            if (isMatch(pattern, str)) {
                return true;
            }
        }
        return false;
    }
    /**
     * 判断url是否与规则配置:
     * ? 表示单个字符;
     * * 表示一层路径内的任意字符串,不可跨层级;
     * ** 表示任意层路径;
     *
     * @param pattern 匹配规则
     * @param url     需要匹配的url
     */
    public static boolean isMatch(String pattern, String url) {
        AntPathMatcher matcher = new AntPathMatcher();
        return matcher.match(pattern, url);
    }
    /**
     * 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。
     *
     * @param num  数字对象
     * @param size 字符串指定长度
     * @return 返回数字的字符串格式,该字符串为指定长度。
     */
    public static String padl(final Number num, final int size) {
        return padl(num.toString(), size, '0');
    }
    /**
     * 字符串左补齐。如果原始字符串s长度大于size,则只保留最后size个字符。
     *
     * @param s    原始字符串
     * @param size 字符串指定长度
     * @param c    用于补齐的字符
     * @return 返回指定长度的字符串,由原字符串左补齐或截取得到。
     */
    public static String padl(final String s, final int size, final char c) {
        final StringBuilder sb = new StringBuilder(size);
        if (s != null) {
            final int len = s.length();
            if (s.length() <= size) {
                sb.append(String.valueOf(c).repeat(size - len));
                sb.append(s);
            } else {
                return s.substring(len - size, len);
            }
        } else {
            sb.append(String.valueOf(c).repeat(Math.max(0, size)));
        }
        return sb.toString();
    }
    /**
     * 切分字符串(分隔符默认逗号)
     *
     * @param str 被切分的字符串
     * @return 分割后的数据列表
     */
    public static List<String> splitList(String str) {
        return splitTo(str, Convert::toStr);
    }
    /**
     * 切分字符串
     *
     * @param str       被切分的字符串
     * @param separator 分隔符
     * @return 分割后的数据列表
     */
    public static List<String> splitList(String str, String separator) {
        return splitTo(str, separator, Convert::toStr);
    }
    /**
     * 切分字符串自定义转换(分隔符默认逗号)
     *
     * @param str    被切分的字符串
     * @param mapper 自定义转换
     * @return 分割后的数据列表
     */
    public static <T> List<T> splitTo(String str, Function<? super Object, T> mapper) {
        return splitTo(str, SEPARATOR, mapper);
    }
    /**
     * 切分字符串自定义转换
     *
     * @param str       被切分的字符串
     * @param separator 分隔符
     * @param mapper    自定义转换
     * @return 分割后的数据列表
     */
    public static <T> List<T> splitTo(String str, String separator, Function<? super Object, T> mapper) {
        if (isBlank(str)) {
            return new ArrayList<>(0);
        }
        return StrUtil.split(str, separator)
            .stream()
            .filter(Objects::nonNull)
            .map(mapper)
            .collect(Collectors.toList());
    }
}
common/src/main/java/com/ycl/common/utils/excel/utils/ValidatorUtils.java
New file
@@ -0,0 +1,36 @@
package com.ycl.common.utils.excel.utils;
import com.ycl.common.utils.spring.SpringUtils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validator;
import java.util.Set;
/**
 * Validator 校验框架工具
 *
 * @author Lion Li
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ValidatorUtils {
    private static final Validator VALID = SpringUtils.getBean(Validator.class);
    /**
     * 对给定对象进行参数校验,并根据指定的校验组进行校验
     *
     * @param object 要进行校验的对象
     * @param groups 校验组
     * @throws ConstraintViolationException 如果校验不通过,则抛出参数校验异常
     */
    public static <T> void validate(T object, Class<?>... groups) {
        Set<ConstraintViolation<T>> validate = VALID.validate(object, groups);
        if (!validate.isEmpty()) {
            throw new ConstraintViolationException("参数校验异常", validate);
        }
    }
}
system/pom.xml
@@ -22,12 +22,6 @@
            <artifactId>knife4j-spring-boot-starter</artifactId>
        </dependency>
        <!-- easy excel -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
        </dependency>
        <!-- 验证码 -->
        <dependency>