diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/RecruitmentFairExportController.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/RecruitmentFairExportController.java
new file mode 100644
index 0000000..2a8b873
--- /dev/null
+++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/RecruitmentFairExportController.java
@@ -0,0 +1,47 @@
+package com.ruoyi.cms.controller.cms;
+
+import com.ruoyi.cms.domain.export.RecruitmentFairExportQuery;
+import com.ruoyi.cms.service.RecruitmentFairExportService;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.enums.BusinessType;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/** 线上/户外招聘会参会企业与岗位导出接口。 */
+@RestController
+@RequestMapping("/cms")
+@Api(tags = "后台:招聘会参会数据导出")
+public class RecruitmentFairExportController extends BaseController
+{
+ @Autowired
+ private RecruitmentFairExportService exportService;
+
+ @ApiOperation("导出线上招聘会参会企业或岗位")
+ @PreAuthorize("@ss.hasPermi('cms:publicJobFair:export')")
+ @Log(title = "线上招聘会参会数据", businessType = BusinessType.EXPORT)
+ @PostMapping("/publicJobFair/export")
+ public void exportPublicJobFair(RecruitmentFairExportQuery query, HttpServletResponse response)
+ throws IOException
+ {
+ exportService.export(response, "online", query);
+ }
+
+ @ApiOperation("导出户外招聘会参会企业或岗位")
+ @PreAuthorize("@ss.hasPermi('cms:outdoorFair:export')")
+ @Log(title = "户外招聘会参会数据", businessType = BusinessType.EXPORT)
+ @PostMapping("/outdoor-fair/export")
+ public void exportOutdoorFair(RecruitmentFairExportQuery query, HttpServletResponse response)
+ throws IOException
+ {
+ exportService.export(response, "outdoor", query);
+ }
+}
diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/export/RecruitmentFairExportQuery.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/export/RecruitmentFairExportQuery.java
new file mode 100644
index 0000000..f2ca679
--- /dev/null
+++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/export/RecruitmentFairExportQuery.java
@@ -0,0 +1,89 @@
+package com.ruoyi.cms.domain.export;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 招聘会参会企业/岗位导出条件。
+ *
+ *
线上招聘会和户外招聘会共用这组条件,未被某一类招聘会使用的字段会被忽略。
+ * 时间条件使用闭区间,便于页面按“开始时间/结束时间”筛选整个招聘会。
+ */
+@Data
+@ApiModel("招聘会参会数据导出条件")
+public class RecruitmentFairExportQuery implements Serializable
+{
+ private static final long serialVersionUID = 1L;
+
+ @ApiModelProperty("导出类型:companies 企业、jobs 岗位、companyJobs 企业及岗位")
+ private String exportType = "companies";
+
+ @ApiModelProperty("招聘会标题")
+ private String title;
+
+ @ApiModelProperty("举办单位")
+ private String hostUnit;
+
+ @ApiModelProperty("招聘会类型")
+ private String fairType;
+
+ @ApiModelProperty("举办区域/区域类型")
+ private String region;
+
+ @ApiModelProperty("场地ID")
+ private Long venueId;
+
+ @ApiModelProperty("场地名称")
+ private String venueName;
+
+ @ApiModelProperty("举办地址")
+ private String address;
+
+ @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @ApiModelProperty("招聘会开始时间下限")
+ private Date startTime;
+
+ @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @ApiModelProperty("招聘会结束时间上限")
+ private Date endTime;
+
+ @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @ApiModelProperty("报名开始时间下限")
+ private Date applyStartTime;
+
+ @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @ApiModelProperty("报名截止时间上限")
+ private Date applyEndTime;
+
+ @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @ApiModelProperty("创建时间下限")
+ private Date createStartTime;
+
+ @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @ApiModelProperty("创建时间上限")
+ private Date createEndTime;
+
+ @ApiModelProperty("举办状态:NOT_STARTED 未举办,ONGOING 正在举办,ENDED 已举办")
+ private String fairStatus;
+
+ @ApiModelProperty("是否跨域:Y/N;仅线上招聘会使用")
+ private String isCrossDomain;
+
+ @ApiModelProperty("是否开启线上申请:true/false;仅户外招聘会使用")
+ private Boolean onlineApply;
+
+ @ApiModelProperty("参会企业或岗位审核状态:0待审核,1通过,2驳回")
+ private String reviewStatus;
+}
diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/export/RecruitmentFairExportRow.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/export/RecruitmentFairExportRow.java
new file mode 100644
index 0000000..d6946c6
--- /dev/null
+++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/export/RecruitmentFairExportRow.java
@@ -0,0 +1,66 @@
+package com.ruoyi.cms.domain.export;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 招聘会导出行。线上和户外查询统一投影到该结构,Excel 层负责字典显示值转换。
+ */
+@Data
+public class RecruitmentFairExportRow implements Serializable
+{
+ private static final long serialVersionUID = 1L;
+
+ private String fairId;
+ private String channel;
+ private String fairTitle;
+ private String fairAddress;
+ private String fairType;
+ private String fairRegion;
+ private String fairVenueName;
+ private String isCrossDomain;
+ private Boolean onlineApply;
+ private Date fairStartTime;
+ private Date fairEndTime;
+ private Date applyStartTime;
+ private Date applyEndTime;
+ private Date fairCreateTime;
+ private String fairHostUnit;
+ private String fairHelpUnit;
+ private String fairOrganizeUnit;
+ private String fairPhone;
+ private String fairCategory;
+
+ private Long companyId;
+ private String companyName;
+ private String companyType;
+ private String companyIndustry;
+ private String companyScale;
+ private String companyLocation;
+ private String companyCode;
+ private String contactPerson;
+ private String contactPhone;
+ private Integer companyStatus;
+ private String companyReviewStatus;
+ private String companyReviewRemark;
+
+ private Long jobId;
+ private String jobTitle;
+ private Integer minSalary;
+ private Integer maxSalary;
+ private String salaryComposition;
+ private String welfareBenefits;
+ private String workSchedule;
+ private String education;
+ private String experience;
+ private String jobType;
+ private String jobCategory;
+ private String jobLocation;
+ private String jobAddress;
+ private Integer vacancies;
+ private Date postingDate;
+ private String jobReviewStatus;
+ private String jobReviewRemark;
+}
diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/RecruitmentFairExportMapper.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/RecruitmentFairExportMapper.java
new file mode 100644
index 0000000..013dbac
--- /dev/null
+++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/RecruitmentFairExportMapper.java
@@ -0,0 +1,23 @@
+package com.ruoyi.cms.mapper;
+
+import com.ruoyi.cms.domain.export.RecruitmentFairExportQuery;
+import com.ruoyi.cms.domain.export.RecruitmentFairExportRow;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/** 招聘会参会企业/岗位导出查询。 */
+public interface RecruitmentFairExportMapper
+{
+ List selectPublicCompanyRows(
+ @Param("query") RecruitmentFairExportQuery query);
+
+ List selectPublicJobRows(
+ @Param("query") RecruitmentFairExportQuery query);
+
+ List selectOutdoorCompanyRows(
+ @Param("query") RecruitmentFairExportQuery query);
+
+ List selectOutdoorJobRows(
+ @Param("query") RecruitmentFairExportQuery query);
+}
diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/RecruitmentFairExportService.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/RecruitmentFairExportService.java
new file mode 100644
index 0000000..52800e6
--- /dev/null
+++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/RecruitmentFairExportService.java
@@ -0,0 +1,402 @@
+package com.ruoyi.cms.service;
+
+import com.ruoyi.cms.domain.export.RecruitmentFairExportQuery;
+import com.ruoyi.cms.domain.export.RecruitmentFairExportRow;
+import com.ruoyi.cms.mapper.JobTitleMapper;
+import com.ruoyi.cms.mapper.RecruitmentFairExportMapper;
+import com.ruoyi.common.core.domain.entity.JobTitle;
+import org.apache.poi.ss.usermodel.BorderStyle;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.CellStyle;
+import org.apache.poi.ss.usermodel.FillPatternType;
+import org.apache.poi.ss.usermodel.Font;
+import org.apache.poi.ss.usermodel.HorizontalAlignment;
+import org.apache.poi.ss.usermodel.IndexedColors;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** 招聘会参会企业和岗位 Excel 导出服务。 */
+@Service
+public class RecruitmentFairExportService
+{
+ private static final String ONLINE = "online";
+ private static final String OUTDOOR = "outdoor";
+ private static final String COMPANIES = "companies";
+ private static final String JOBS = "jobs";
+ private static final String COMPANY_JOBS = "companyJobs";
+ private static final String DATE_PATTERN = "yyyy-MM-dd HH:mm:ss";
+
+ private static final List COMPANY_HEADERS = Arrays.asList(
+ "招聘会标题", "招聘会类型", "区域/举办区域", "是否跨域", "是否线上申请", "举办状态",
+ "开始时间", "结束时间", "报名开始时间", "报名截止时间", "创建时间", "招聘会地址", "举办单位", "协办单位",
+ "组织单位", "招聘会联系电话", "招聘会分类", "场地", "参会企业", "企业ID", "企业类型", "所属行业", "企业规模", "企业地址", "统一社会信用代码",
+ "联系人", "联系电话", "企业审核状态", "企业审核意见");
+
+ private static final List JOB_HEADERS = Arrays.asList(
+ "招聘会标题", "招聘会类型", "区域/举办区域", "是否跨域", "是否线上申请", "举办状态",
+ "开始时间", "结束时间", "报名开始时间", "报名截止时间", "创建时间", "招聘会地址", "举办单位", "协办单位",
+ "组织单位", "招聘会联系电话", "招聘会分类", "场地", "参会企业", "企业ID", "企业类型", "所属行业", "企业规模", "联系人", "联系电话", "企业审核状态",
+ "企业审核意见", "岗位名称", "岗位ID", "最低薪资(元)", "最高薪资(元)", "薪资构成",
+ "福利待遇", "工作时间安排", "学历要求", "工作经验", "岗位类型", "岗位分类", "岗位区划",
+ "工作地址", "招聘人数", "发布时间", "岗位审核状态", "岗位审核意见");
+
+ @Autowired
+ private RecruitmentFairExportMapper exportMapper;
+
+ @Autowired
+ private JobTitleMapper jobTitleMapper;
+
+ /**
+ * 导出指定渠道的数据。导出类型由 query.exportType 决定。
+ */
+ public void export(HttpServletResponse response, String channel, RecruitmentFairExportQuery query)
+ throws IOException
+ {
+ RecruitmentFairExportQuery condition = query == null ? new RecruitmentFairExportQuery() : query;
+ String exportType = normalizeExportType(condition.getExportType());
+ List rows = queryRows(channel, exportType, condition);
+
+ try (Workbook workbook = new XSSFWorkbook()) {
+ CellStyle headerStyle = createHeaderStyle(workbook);
+ if (COMPANIES.equals(exportType)) {
+ writeRows(workbook.createSheet("参会企业"), COMPANY_HEADERS, rows, headerStyle,
+ this::companyValues);
+ } else if (JOBS.equals(exportType)) {
+ writeRows(workbook.createSheet("参加岗位"), JOB_HEADERS, rows, headerStyle,
+ this::jobValues);
+ } else {
+ writeCompanySheets(workbook, rows, headerStyle);
+ }
+
+ response.reset();
+ response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+ response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+ String channelName = ONLINE.equals(channel) ? "线上招聘会" : "户外招聘会";
+ String fileName = channelName + "参会数据_" + System.currentTimeMillis() + ".xlsx";
+ String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name())
+ .replace("+", "%20");
+ response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedFileName);
+ workbook.write(response.getOutputStream());
+ response.flushBuffer();
+ }
+ }
+
+ private List queryRows(String channel, String exportType,
+ RecruitmentFairExportQuery query)
+ {
+ boolean online = ONLINE.equals(channel);
+ if (COMPANIES.equals(exportType)) {
+ return online ? exportMapper.selectPublicCompanyRows(query)
+ : exportMapper.selectOutdoorCompanyRows(query);
+ }
+ if (JOBS.equals(exportType)) {
+ return online ? exportMapper.selectPublicJobRows(query)
+ : exportMapper.selectOutdoorJobRows(query);
+ }
+
+ // 组合导出需要同时保留“没有岗位的参会企业”,因此分别查询企业和岗位。
+ List companies = online
+ ? exportMapper.selectPublicCompanyRows(query)
+ : exportMapper.selectOutdoorCompanyRows(query);
+ List jobs = online
+ ? exportMapper.selectPublicJobRows(query)
+ : exportMapper.selectOutdoorJobRows(query);
+ List result = new ArrayList<>(companies);
+ result.addAll(jobs);
+ return result;
+ }
+
+ private String normalizeExportType(String exportType)
+ {
+ if (JOBS.equals(exportType) || COMPANY_JOBS.equals(exportType)) {
+ return exportType;
+ }
+ return COMPANIES;
+ }
+
+ private CellStyle createHeaderStyle(Workbook workbook)
+ {
+ CellStyle style = workbook.createCellStyle();
+ style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
+ style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+ style.setAlignment(HorizontalAlignment.CENTER);
+ style.setBorderBottom(BorderStyle.THIN);
+ style.setBorderTop(BorderStyle.THIN);
+ style.setBorderLeft(BorderStyle.THIN);
+ style.setBorderRight(BorderStyle.THIN);
+ Font font = workbook.createFont();
+ font.setBold(true);
+ style.setFont(font);
+ return style;
+ }
+
+ private void writeRows(Sheet sheet, List headers, List rows,
+ CellStyle headerStyle, RowValues values)
+ {
+ writeHeader(sheet, headers, headerStyle);
+ int rowIndex = 1;
+ for (RecruitmentFairExportRow rowData : rows == null ? Collections.emptyList() : rows) {
+ Row row = sheet.createRow(rowIndex++);
+ List