wuzhen update招生就业

This commit is contained in:
“wuzhen” committed 2026-04-02 10:49:59 +08:00
1 parent 7e122d76dd
commit e5c6fd7a24
55 files changed
+3563 -2397

No files matched your search

+14
View File
@@ -110,6 +110,20 @@
<artifactId>ruoyi-xg</artifactId>
</dependency>
<!-- iText PDF生成库(用于招聘简章PDF生成) -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13.3</version>
</dependency>
<!-- iText亚洲字体支持包 - 支持中文 -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
</dependencies>
</project>
@@ -1,14 +1,20 @@
package org.dromara.zsjy.controller;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Arrays;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import jakarta.servlet.http.HttpServletResponse;
import org.dromara.common.core.exception.ServiceException;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.multipart.MultipartFile;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.web.core.BaseController;
@@ -20,15 +26,21 @@ import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.zsjy.domain.vo.ZsjyEnterpriseVo;
import org.dromara.zsjy.domain.vo.ZsjyEnterpriseBrochureVo;
import org.dromara.zsjy.domain.vo.ZsjyEnterpriseImportVo;
import org.dromara.zsjy.domain.bo.ZsjyEnterpriseBo;
import org.dromara.zsjy.service.IZsjyEnterpriseService;
import org.dromara.zsjy.service.IZsjyEnterpriseBrochureService;
import org.dromara.zsjy.listener.ZsjyEnterpriseImportListener;
import lombok.extern.slf4j.Slf4j;
/**
* 企业信息
* 企业信息管理Controller
*
* @author Lion Li
* @author Lion Li (改造: Cursor AI 2026-03-30)
* @date 2023-07-05
*/
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
@@ -36,11 +48,12 @@ import org.dromara.zsjy.service.IZsjyEnterpriseService;
public class ZsjyEnterpriseController extends BaseController {
private final IZsjyEnterpriseService enterpriseService;
private final IZsjyEnterpriseBrochureService brochureService;
/**
* 查询企业信息列表
* 查询企业信息列表(包含岗位数量)
*/
// @SaCheckPermission("zsjy:enterprise:list")
@SaCheckPermission("zsjy:enterprise:list")
@GetMapping("/list")
public TableDataInfo<ZsjyEnterpriseVo> list(ZsjyEnterpriseBo bo, PageQuery pageQuery) {
return enterpriseService.queryPageList(bo, pageQuery);
@@ -49,8 +62,8 @@ public class ZsjyEnterpriseController extends BaseController {
/**
* 导出企业信息列表
*/
// @SaCheckPermission("zsjy:enterprise:export")
@Log(title = "企业信息", businessType = BusinessType.EXPORT)
@SaCheckPermission("zsjy:enterprise:export")
@PostMapping("/export")
public void export(ZsjyEnterpriseBo bo, HttpServletResponse response) {
List<ZsjyEnterpriseVo> list = enterpriseService.queryList(bo);
@@ -58,21 +71,19 @@ public class ZsjyEnterpriseController extends BaseController {
}
/**
* 获取企业信息详细信息
*
* @param id 主键
* 获取企业信息详细信息(包含岗位列表)
*/
// @SaCheckPermission("zsjy:enterprise:query")
@SaCheckPermission("zsjy:enterprise:query")
@GetMapping("/{id}")
public R<ZsjyEnterpriseVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
return R.ok(enterpriseService.queryById(id));
public R<ZsjyEnterpriseVo> getInfo(@PathVariable Long id) {
return R.ok(enterpriseService.queryWithJobs(id));
}
/**
* 新增企业信息
* 新增企业信息(同时新增岗位列表)
*/
// @SaCheckPermission("zsjy:enterprise:add")
@Log(title = "企业信息", businessType = BusinessType.INSERT)
@SaCheckPermission("zsjy:enterprise:add")
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody ZsjyEnterpriseBo bo) {
@@ -80,10 +91,10 @@ public class ZsjyEnterpriseController extends BaseController {
}
/**
* 修改企业信息
* 修改企业信息(同时更新岗位列表)
*/
// @SaCheckPermission("zsjy:enterprise:edit")
@Log(title = "企业信息", businessType = BusinessType.UPDATE)
@SaCheckPermission("zsjy:enterprise:edit")
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody ZsjyEnterpriseBo bo) {
@@ -92,22 +103,156 @@ public class ZsjyEnterpriseController extends BaseController {
/**
* 删除企业信息
*
* @param ids 主键串
*/
// @SaCheckPermission("zsjy:enterprise:remove")
@Log(title = "企业信息", businessType = BusinessType.DELETE)
@SaCheckPermission("zsjy:enterprise:remove")
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
public R<Void> remove(@PathVariable Long[] ids) {
return toAjax(enterpriseService.deleteWithValidByIds(Arrays.asList(ids), true));
}
/**
* 获取企业下拉选项
*/
// @SaCheckPermission("zsjy:enterprise:list")
@GetMapping("/options")
public R<List<ZsjyEnterpriseVo>> getOptions() {
return R.ok(enterpriseService.queryList(new ZsjyEnterpriseBo()));
}
/**
* 下载导入模板
*/
@GetMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
ExcelUtil.exportExcel(new ArrayList<>(List.of(new ZsjyEnterpriseImportVo())), "企业信息导入模板", ZsjyEnterpriseImportVo.class, response);
}
/**
* 导入企业信息(含岗位)
*
* @param file Excel文件
* @param updateSupport 是否支持更新(同名覆盖)
*/
@Log(title = "企业信息", businessType = BusinessType.IMPORT)
@SaCheckPermission("zsjy:enterprise:import")
@PostMapping("/importData")
@RepeatSubmit()
public R<String> importData(MultipartFile file, @RequestParam(value = "updateSupport", required = false, defaultValue = "false") Boolean updateSupport) throws Exception {
String resultMsg = ExcelUtil.importExcel(file.getInputStream(), ZsjyEnterpriseImportVo.class, new ZsjyEnterpriseImportListener(updateSupport)).getAnalysis();
return R.ok(resultMsg);
}
/**
* 下载企业招聘简章PDF(文件名使用企业名称)
*/
@SaCheckPermission("zsjy:enterprise:brochure")
@GetMapping("/brochure/download/{enterpriseId}")
public void downloadBrochure(@PathVariable Long enterpriseId, HttpServletResponse response) {
try {
// 优先从简章记录获取企业名称(更可靠)
ZsjyEnterpriseBrochureVo brochure = brochureService.getLatestBrochure(enterpriseId);
String fileName = null;
if (brochure != null && StringUtils.isNotBlank(brochure.getEnterpriseName())) {
fileName = brochure.getEnterpriseName();
}
// 如果简章记录中没有企业名称,再查询企业信息
if (fileName == null) {
ZsjyEnterpriseVo enterprise = enterpriseService.queryById(enterpriseId);
if (enterprise != null) {
fileName = enterprise.getEnterpriseName();
}
}
brochureService.downloadPdf(enterpriseId, fileName, response);
} catch (ServiceException e) {
writeErrorResponse(response, e.getMessage());
} catch (Exception e) {
log.error("下载PDF异常,企业ID: {}", enterpriseId, e);
writeErrorResponse(response, "下载PDF失败: " + e.getMessage());
}
}
/**
* 获取最新生成的简章信息
*/
@SaCheckPermission("zsjy:enterprise:query")
@GetMapping("/brochure/{enterpriseId}")
public R<ZsjyEnterpriseBrochureVo> getBrochure(@PathVariable Long enterpriseId) {
return R.ok(brochureService.getLatestBrochure(enterpriseId));
}
/**
* 生成招聘简章PDF(单个)
*/
@Log(title = "企业招聘简章", businessType = BusinessType.INSERT)
@SaCheckPermission("zsjy:enterprise:brochure")
@PostMapping("/brochure/{enterpriseId}")
public R<ZsjyEnterpriseBrochureVo> generateBrochure(@PathVariable Long enterpriseId) {
return R.ok(brochureService.generateBrochure(enterpriseId));
}
/**
* 批量生成招聘简章PDF
*
* @param enterpriseIds 企业ID列表(多个用逗号分隔)
*/
@Log(title = "批量生成企业招聘简章", businessType = BusinessType.INSERT)
@SaCheckPermission("zsjy:enterprise:brochure")
@PostMapping("/brochure/batch/{enterpriseIds}")
public R<List<ZsjyEnterpriseBrochureVo>> generateBrochureBatch(@PathVariable String enterpriseIds) {
List<Long> ids = Arrays.stream(enterpriseIds.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.map(Long::parseLong)
.collect(Collectors.toList());
return R.ok(brochureService.generateBrochureBatch(ids));
}
/**
* 批量下载多页PDF(每个企业一页)
*
* @param enterpriseIds 企业ID列表(多个用逗号分隔)
*/
@SaCheckPermission("zsjy:enterprise:brochure")
@PostMapping("/brochure/batch/download")
public void downloadBatchBrochure(@RequestBody java.util.List<Long> enterpriseIds, HttpServletResponse response) {
try {
brochureService.downloadBatchPdf(enterpriseIds, response);
} catch (ServiceException e) {
writeErrorResponse(response, e.getMessage());
} catch (Exception e) {
log.error("批量下载PDF异常", e);
writeErrorResponse(response, "批量下载PDF失败: " + e.getMessage());
}
}
/**
* 下载简章PDF(通过简章记录ID)
*/
@SaCheckPermission("zsjy:enterprise:brochure")
@GetMapping("/brochure/downloadById/{id}")
public void downloadBrochureById(@PathVariable Long id, HttpServletResponse response) {
try {
brochureService.downloadPdfById(id, response);
} catch (ServiceException e) {
writeErrorResponse(response, e.getMessage());
} catch (Exception e) {
log.error("下载PDF异常,简章ID: {}", id, e);
writeErrorResponse(response, "下载PDF失败: " + e.getMessage());
}
}
/**
* 直接写出错误响应(避免全局异常处理器返回JSON导致PDF损坏)
*/
private void writeErrorResponse(HttpServletResponse response, String message) {
try {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().write("{\"code\":500,\"msg\":\"" + message + "\",\"data\":null}");
response.getWriter().flush();
} catch (IOException e) {
log.error("写出错误响应失败", e);
}
}
}
@@ -46,6 +46,11 @@ public class ZsjyEnterprise extends TenantEntity implements Serializable {
*/
private String enterpriseScale;
/**
* 企业简介(≤600字,用于招聘简章)
*/
private String enterpriseIntro;
/**
* 企业联系人姓名
*/
@@ -56,11 +61,21 @@ public class ZsjyEnterprise extends TenantEntity implements Serializable {
*/
private String contactPhone;
/**
* 办公电话
*/
private String officePhone;
/**
* 企业邮箱
*/
private String email;
/**
* 企业官网
*/
private String website;
/**
* 企业地址
*/
@@ -0,0 +1,70 @@
package org.dromara.zsjy.domain;
import org.dromara.common.tenant.core.TenantEntity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* 简章生成记录对象 zsjy_enterprise_brochure
*
* @author Cursor AI
* @date 2026-03-30
*/
@Data
@TableName("zsjy_enterprise_brochure")
@EqualsAndHashCode(callSuper = true)
public class ZsjyEnterpriseBrochure extends TenantEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 记录ID
*/
@TableId(value = "id")
private Long id;
/**
* 企业ID
*/
private Long enterpriseId;
/**
* PDF存储路径(OSS)
*/
private String brochurePath;
/**
* 文件名称
*/
private String fileName;
/**
* 文件大小(字节)
*/
private Long fileSize;
/**
* 生成人
*/
private Long generateBy;
/**
* 生成人姓名
*/
private String generateByName;
/**
* 生成时间
*/
private java.util.Date generateTime;
/**
* 删除标志(0代表存在 1代表删除)
*/
@TableLogic
private Integer deleted;
}
@@ -0,0 +1,60 @@
package org.dromara.zsjy.domain;
import org.dromara.common.tenant.core.TenantEntity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* 企业招聘岗位对象 zsjy_enterprise_job
*
* @author Cursor AI
* @date 2026-03-30
*/
@Data
@TableName("zsjy_enterprise_job")
@EqualsAndHashCode(callSuper = true)
public class ZsjyEnterpriseJob extends TenantEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 岗位ID
*/
@TableId(value = "id")
private Long id;
/**
* 所属企业ID
*/
private Long enterpriseId;
/**
* 岗位名称
*/
private String positionName;
/**
* 岗位要求
*/
private String requirements;
/**
* 待遇
*/
private String benefits;
/**
* 状态(0正常 1停用)
*/
private String status;
/**
* 删除标志(0代表存在 1代表删除)
*/
@TableLogic
private Integer deleted;
}
@@ -7,12 +7,16 @@ import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import jakarta.validation.constraints.*;
import org.dromara.zsjy.domain.ZsjyEnterprise;
import org.dromara.zsjy.domain.ZsjyEnterpriseJob;
import java.util.List;
/**
* 企业信息业务对象 zsjy_enterprise
*
* @author Lion Li
* @author Lion Li (改造: Cursor AI 2026-03-30)
* @date 2023-07-05
*/
@Data
@@ -30,35 +34,35 @@ public class ZsjyEnterpriseBo extends BaseEntity {
* 企业名称
*/
@NotBlank(message = "企业名称不能为空", groups = { AddGroup.class, EditGroup.class })
@Size(min = 1, max = 100, message = "企业名称长度不能超过100个字符", groups = { AddGroup.class, EditGroup.class })
@Size(min = 1, max = 200, message = "企业名称长度不能超过200个字符", groups = { AddGroup.class, EditGroup.class })
private String enterpriseName;
/**
* 所属行业
*/
@NotBlank(message = "所属行业不能为空", groups = { AddGroup.class, EditGroup.class })
@Size(min = 1, max = 50, message = "所属行业长度不能超过50个字符", groups = { AddGroup.class, EditGroup.class })
private String industry;
/**
* 主营业务
* 主营业务(废弃字段,兼容保留)
*/
@NotBlank(message = "主营业务不能为空", groups = { AddGroup.class, EditGroup.class })
@Size(min = 1, max = 500, message = "主营业务长度不能超过500个字符", groups = { AddGroup.class, EditGroup.class })
private String mainBusiness;
/**
* 企业规模
*/
@NotBlank(message = "企业规模不能为空", groups = { AddGroup.class, EditGroup.class })
@Size(min = 1, max = 20, message = "企业规模长度不能超过20个字符", groups = { AddGroup.class, EditGroup.class })
private String enterpriseScale;
/**
* 企业简介(≤600字)
*/
@Size(max = 1500, message = "企业简介长度不能超过1500个字符", groups = { AddGroup.class, EditGroup.class })
private String enterpriseIntro;
/**
* 企业联系人姓名
*/
@NotBlank(message = "企业联系人姓名不能为空", groups = { AddGroup.class, EditGroup.class })
@Size(min = 1, max = 50, message = "企业联系人姓名长度不能超过50个字符", groups = { AddGroup.class, EditGroup.class })
@NotBlank(message = "联系人姓名不能为空", groups = { AddGroup.class, EditGroup.class })
@Size(min = 1, max = 50, message = "联系人姓名长度不能超过50个字符", groups = { AddGroup.class, EditGroup.class })
private String contactName;
/**
@@ -68,14 +72,24 @@ public class ZsjyEnterpriseBo extends BaseEntity {
@Size(min = 1, max = 20, message = "联系电话长度不能超过20个字符", groups = { AddGroup.class, EditGroup.class })
private String contactPhone;
/**
* 办公电话
*/
private String officePhone;
/**
* 企业邮箱
*/
@NotBlank(message = "企业邮箱不能为空", groups = { AddGroup.class, EditGroup.class })
@Email(message = "企业邮箱格式不正确", groups = { AddGroup.class, EditGroup.class })
@Size(min = 1, max = 100, message = "企业邮箱长度不能超过100个字符", groups = { AddGroup.class, EditGroup.class })
@NotBlank(message = "电子邮箱不能为空", groups = { AddGroup.class, EditGroup.class })
@Email(message = "电子邮箱格式不正确", groups = { AddGroup.class, EditGroup.class })
@Size(min = 1, max = 100, message = "电子邮箱长度不能超过100个字符", groups = { AddGroup.class, EditGroup.class })
private String email;
/**
* 企业官网
*/
private String website;
/**
* 企业地址
*/
@@ -89,7 +103,12 @@ public class ZsjyEnterpriseBo extends BaseEntity {
private String status;
/**
* 备注
* 备注(废弃字段,兼容保留)
*/
private String remark;
/**
* 岗位列表(用于新增/编辑时传递岗位信息)
*/
private List<ZsjyEnterpriseJobBo> jobList;
}
@@ -0,0 +1,57 @@
package org.dromara.zsjy.domain.bo;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import jakarta.validation.constraints.*;
import org.dromara.zsjy.domain.ZsjyEnterpriseJob;
/**
* 企业招聘岗位业务对象 zsjy_enterprise_job
*
* @author Cursor AI
* @date 2026-03-30
*/
@Data
@EqualsAndHashCode(callSuper = true)
@AutoMapper(target = ZsjyEnterpriseJob.class, reverseConvertGenerate = false)
public class ZsjyEnterpriseJobBo extends BaseEntity {
/**
* 岗位ID
*/
@NotNull(message = "岗位ID不能为空", groups = { EditGroup.class })
private Long id;
/**
* 所属企业ID
*/
@NotNull(message = "所属企业ID不能为空", groups = { AddGroup.class, EditGroup.class })
private Long enterpriseId;
/**
* 岗位名称
*/
@NotBlank(message = "岗位名称不能为空", groups = { AddGroup.class, EditGroup.class })
@Size(min = 1, max = 100, message = "岗位名称长度不能超过100个字符", groups = { AddGroup.class, EditGroup.class })
private String positionName;
/**
* 岗位要求
*/
private String requirements;
/**
* 待遇
*/
private String benefits;
/**
* 状态(0正常 1停用)
*/
private String status;
}
@@ -0,0 +1,81 @@
package org.dromara.zsjy.domain.vo;
import lombok.Data;
import io.github.linpeilie.annotations.AutoMapper;
import org.dromara.zsjy.domain.ZsjyEnterpriseBrochure;
import java.io.Serializable;
import java.util.Date;
/**
* 简章生成记录视图对象
*
* @author Cursor AI
* @date 2026-03-30
*/
@Data
@AutoMapper(target = ZsjyEnterpriseBrochure.class)
public class ZsjyEnterpriseBrochureVo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 记录ID
*/
private Long id;
/**
* 企业ID
*/
private Long enterpriseId;
/**
* 企业名称(关联字段)
*/
private String enterpriseName;
/**
* PDF存储路径(OSS)
*/
private String brochurePath;
/**
* 文件名称
*/
private String fileName;
/**
* 文件大小(字节)
*/
private Long fileSize;
/**
* 生成人
*/
private Long generateBy;
/**
* 生成人姓名
*/
private String generateByName;
/**
* 生成时间
*/
private Date generateTime;
/**
* 租户编号
*/
private String tenantId;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}
@@ -0,0 +1,161 @@
package org.dromara.zsjy.domain.vo;
import cn.idev.excel.annotation.ExcelProperty;
import jakarta.validation.constraints.*;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 企业信息导入VO(支持企业+最多6个岗位)
*
* @author Cursor AI
* @date 2026-03-30
*/
@Data
@cn.idev.excel.annotation.ExcelIgnoreUnannotated
public class ZsjyEnterpriseImportVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/* ==================== 企业基本信息 ==================== */
/**
* 企业名称(必填)
*/
@ExcelProperty("企业名称")
@NotBlank(message = "企业名称不能为空")
@Size(max = 200, message = "企业名称长度不能超过200")
private String enterpriseName;
/**
* 企业简介(选填,最多600字)
*/
@ExcelProperty("企业简介")
@Size(max = 1500, message = "企业简介长度不能超过1500字符")
private String enterpriseIntro;
/**
* 企业地址(必填)
*/
@ExcelProperty("企业地址")
@NotBlank(message = "企业地址不能为空")
@Size(max = 255, message = "企业地址长度不能超过255")
private String address;
/**
* 联系人(必填)
*/
@ExcelProperty("联系人")
@NotBlank(message = "联系人不能为空")
@Size(max = 50, message = "联系人姓名长度不能超过50")
private String contactName;
/**
* 联系电话(必填,手机号)
*/
@ExcelProperty("联系电话")
@NotBlank(message = "联系电话不能为空")
@Size(max = 20, message = "联系电话长度不能超过20")
private String contactPhone;
/**
* 办公电话(选填)
*/
@ExcelProperty("办公电话")
@Size(max = 20, message = "办公电话长度不能超过20")
private String officePhone;
/**
* 电子邮箱(必填)
*/
@ExcelProperty("电子邮箱")
@NotBlank(message = "电子邮箱不能为空")
@Email(message = "电子邮箱格式不正确")
@Size(max = 100, message = "电子邮箱长度不能超过100")
private String email;
/* ==================== 岗位1 ==================== */
/**
* 招聘岗位1-岗位名称
*/
@ExcelProperty("招聘岗位1")
@Size(max = 100, message = "岗位名称长度不能超过100")
private String positionName1;
/**
* 招聘岗位1-要求
*/
@ExcelProperty("要求1")
private String requirements1;
/**
* 招聘岗位1-待遇
*/
@ExcelProperty("待遇1")
private String benefits1;
/* ==================== 岗位2 ==================== */
@ExcelProperty("招聘岗位2")
@Size(max = 100, message = "岗位名称长度不能超过100")
private String positionName2;
@ExcelProperty("要求2")
private String requirements2;
@ExcelProperty("待遇2")
private String benefits2;
/* ==================== 岗位3 ==================== */
@ExcelProperty("招聘岗位3")
@Size(max = 100, message = "岗位名称长度不能超过100")
private String positionName3;
@ExcelProperty("要求3")
private String requirements3;
@ExcelProperty("待遇3")
private String benefits3;
/* ==================== 岗位4 ==================== */
@ExcelProperty("招聘岗位4")
@Size(max = 100, message = "岗位名称长度不能超过100")
private String positionName4;
@ExcelProperty("要求4")
private String requirements4;
@ExcelProperty("待遇4")
private String benefits4;
/* ==================== 岗位5 ==================== */
@ExcelProperty("招聘岗位5")
@Size(max = 100, message = "岗位名称长度不能超过100")
private String positionName5;
@ExcelProperty("要求5")
private String requirements5;
@ExcelProperty("待遇5")
private String benefits5;
/* ==================== 岗位6 ==================== */
@ExcelProperty("招聘岗位6")
@Size(max = 100, message = "岗位名称长度不能超过100")
private String positionName6;
@ExcelProperty("要求6")
private String requirements6;
@ExcelProperty("待遇6")
private String benefits6;
}
@@ -0,0 +1,86 @@
package org.dromara.zsjy.domain.vo;
import lombok.Data;
import io.github.linpeilie.annotations.AutoMapper;
import org.dromara.zsjy.domain.ZsjyEnterpriseJob;
import java.io.Serializable;
import java.util.Date;
/**
* 企业招聘岗位视图对象
*
* @author Cursor AI
* @date 2026-03-30
*/
@Data
@AutoMapper(target = ZsjyEnterpriseJob.class)
public class ZsjyEnterpriseJobVo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 岗位ID
*/
private Long id;
/**
* 所属企业ID
*/
private Long enterpriseId;
/**
* 岗位名称
*/
private String positionName;
/**
* 岗位要求
*/
private String requirements;
/**
* 待遇
*/
private String benefits;
/**
* 状态(0正常 1停用)
*/
private String status;
/**
* 删除标志(0代表存在 1代表删除)
*/
private Integer deleted;
/**
* 租户编号
*/
private String tenantId;
/**
* 创建部门
*/
private Long createDept;
/**
* 创建者
*/
private Long createBy;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新者
*/
private Long updateBy;
/**
* 更新时间
*/
private Date updateTime;
}
@@ -6,11 +6,12 @@ import org.dromara.zsjy.domain.ZsjyEnterprise;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
/**
* 企业信息视图对象
*
* @author Lion Li
* @author Lion Li (改造: Cursor AI 2026-03-30)
* @date 2023-07-05
*/
@Data
@@ -40,7 +41,7 @@ public class ZsjyEnterpriseVo implements Serializable {
private String industryName;
/**
* 主营业务
* 主营业务(废弃字段,仅保留兼容)
*/
private String mainBusiness;
@@ -54,6 +55,11 @@ public class ZsjyEnterpriseVo implements Serializable {
*/
private String enterpriseScaleName;
/**
* 企业简介(≤600字,用于招聘简章)
*/
private String enterpriseIntro;
/**
* 企业联系人姓名
*/
@@ -65,10 +71,20 @@ public class ZsjyEnterpriseVo implements Serializable {
private String contactPhone;
/**
* 企业邮箱
* 办公电话
*/
private String officePhone;
/**
* 电子邮箱
*/
private String email;
/**
* 企业官网
*/
private String website;
/**
* 企业地址
*/
@@ -80,7 +96,7 @@ public class ZsjyEnterpriseVo implements Serializable {
private String status;
/**
* 备注
* 备注(废弃字段,仅保留兼容)
*/
private String remark;
@@ -118,4 +134,14 @@ public class ZsjyEnterpriseVo implements Serializable {
* 更新时间
*/
private Date updateTime;
/**
* 岗位列表(用于招聘简章)
*/
private List<ZsjyEnterpriseJobVo> jobList;
/**
* 岗位数量(列表展示用)
*/
private Integer jobCount;
}
@@ -0,0 +1,148 @@
package org.dromara.zsjy.listener;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.http.HtmlUtil;
import cn.idev.excel.context.AnalysisContext;
import cn.idev.excel.event.AnalysisEventListener;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.excel.core.ExcelListener;
import org.dromara.common.excel.core.ExcelResult;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.zsjy.domain.bo.ZsjyEnterpriseBo;
import org.dromara.zsjy.domain.bo.ZsjyEnterpriseJobBo;
import org.dromara.zsjy.domain.vo.ZsjyEnterpriseImportVo;
import org.dromara.zsjy.service.IZsjyEnterpriseService;
import java.util.ArrayList;
import java.util.List;
/**
* 企业信息导入Listener
*
* @author Cursor AI
* @date 2026-03-30
*/
@Slf4j
public class ZsjyEnterpriseImportListener extends AnalysisEventListener<ZsjyEnterpriseImportVo>
implements ExcelListener<ZsjyEnterpriseImportVo> {
private final Boolean isUpdateSupport;
private final String operName;
private final Long operUserId;
private int successNum = 0;
private int failureNum = 0;
private final StringBuilder successMsg = new StringBuilder();
private final StringBuilder failureMsg = new StringBuilder();
public ZsjyEnterpriseImportListener(Boolean isUpdateSupport) {
this.isUpdateSupport = isUpdateSupport;
this.operName = LoginHelper.getUsername();
this.operUserId = LoginHelper.getUserId();
}
@Override
public void invoke(ZsjyEnterpriseImportVo importVo, AnalysisContext context) {
try {
if (importVo == null) {
failureNum++;
failureMsg.append("<br/>").append(failureNum).append("、第").append(context.readRowHolder().getRowIndex() + 1)
.append("行:数据为空");
return;
}
// 调试日志
log.info("导入第{}行数据: enterpriseName={}, address={}, contactName={}",
context.readRowHolder().getRowIndex() + 1,
importVo.getEnterpriseName(),
importVo.getAddress(),
importVo.getContactName());
ZsjyEnterpriseBo bo = convertToBo(importVo);
log.info("转换后BO: enterpriseName={}", bo.getEnterpriseName());
IZsjyEnterpriseService enterpriseService = SpringUtils.getBean(IZsjyEnterpriseService.class);
enterpriseService.importData(List.of(bo), isUpdateSupport, operName);
successNum++;
successMsg.append("<br/>").append(successNum).append("、企业 ").append(bo.getEnterpriseName())
.append(" 导入成功");
} catch (Exception e) {
failureNum++;
// 安全处理null情况
String enterpriseName = importVo.getEnterpriseName() != null ? importVo.getEnterpriseName() : "未知企业";
String msg = "<br/>" + failureNum + "、第" + (context.readRowHolder().getRowIndex() + 1)
+ "行 企业 " + HtmlUtil.cleanHtmlTag(enterpriseName) + " 导入失败:" + e.getMessage();
failureMsg.append(msg);
log.error(msg, e);
}
}
private ZsjyEnterpriseBo convertToBo(ZsjyEnterpriseImportVo vo) {
ZsjyEnterpriseBo bo = new ZsjyEnterpriseBo();
bo.setEnterpriseName(vo.getEnterpriseName());
bo.setEnterpriseIntro(vo.getEnterpriseIntro());
bo.setAddress(vo.getAddress());
bo.setContactName(vo.getContactName());
bo.setContactPhone(vo.getContactPhone());
bo.setOfficePhone(vo.getOfficePhone());
bo.setEmail(vo.getEmail());
bo.setStatus("0");
List<ZsjyEnterpriseJobBo> jobs = new ArrayList<>();
addJobIfPresent(jobs, vo.getPositionName1(), vo.getRequirements1(), vo.getBenefits1());
addJobIfPresent(jobs, vo.getPositionName2(), vo.getRequirements2(), vo.getBenefits2());
addJobIfPresent(jobs, vo.getPositionName3(), vo.getRequirements3(), vo.getBenefits3());
addJobIfPresent(jobs, vo.getPositionName4(), vo.getRequirements4(), vo.getBenefits4());
addJobIfPresent(jobs, vo.getPositionName5(), vo.getRequirements5(), vo.getBenefits5());
addJobIfPresent(jobs, vo.getPositionName6(), vo.getRequirements6(), vo.getBenefits6());
bo.setJobList(jobs);
return bo;
}
private void addJobIfPresent(List<ZsjyEnterpriseJobBo> jobs, String name, String requirements, String benefits) {
if (StringUtils.isNotBlank(name)) {
ZsjyEnterpriseJobBo job = new ZsjyEnterpriseJobBo();
job.setPositionName(name);
job.setRequirements(requirements);
job.setBenefits(benefits);
job.setStatus("0");
jobs.add(job);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
}
@Override
public ExcelResult<ZsjyEnterpriseImportVo> getExcelResult() {
return new ExcelResult<>() {
@Override
public String getAnalysis() {
if (failureNum > 0) {
failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
throw new ServiceException(failureMsg.toString());
} else {
successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
}
return successMsg.toString();
}
@Override
public List<ZsjyEnterpriseImportVo> getList() {
return null;
}
@Override
public List<String> getErrorList() {
return null;
}
};
}
}
@@ -0,0 +1,15 @@
package org.dromara.zsjy.mapper;
import org.dromara.zsjy.domain.ZsjyEnterpriseBrochure;
import org.dromara.zsjy.domain.vo.ZsjyEnterpriseBrochureVo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
/**
* 简章生成记录Mapper接口
*
* @author Cursor AI
* @date 2026-03-30
*/
public interface ZsjyEnterpriseBrochureMapper extends BaseMapperPlus<ZsjyEnterpriseBrochure, ZsjyEnterpriseBrochureVo> {
}
@@ -0,0 +1,22 @@
package org.dromara.zsjy.mapper;
import java.util.List;
import org.dromara.zsjy.domain.ZsjyEnterpriseJob;
import org.dromara.zsjy.domain.vo.ZsjyEnterpriseJobVo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
/**
* 企业招聘岗位Mapper接口
*
* @author Cursor AI
* @date 2026-03-30
*/
public interface ZsjyEnterpriseJobMapper extends BaseMapperPlus<ZsjyEnterpriseJob, ZsjyEnterpriseJobVo> {
/**
* 按企业ID查询岗位列表(用于简章展示)
*/
List<ZsjyEnterpriseJobVo> selectVoListByEnterpriseId(Long enterpriseId);
}
@@ -2,7 +2,13 @@ package org.dromara.zsjy.mapper;
import org.dromara.zsjy.domain.ZsjyEnterprise;
import org.dromara.zsjy.domain.vo.ZsjyEnterpriseVo;
import org.dromara.zsjy.domain.bo.ZsjyEnterpriseBo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Param;
/**
* 企业信息 Mapper 接口
@@ -12,4 +18,21 @@ import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
*/
public interface ZsjyEnterpriseMapper extends BaseMapperPlus<ZsjyEnterprise, ZsjyEnterpriseVo> {
/**
* 查询企业详情(含岗位列表,使用XML自定义resultMap)
*
* @param id 企业ID
* @return 企业信息(含岗位列表)
*/
ZsjyEnterpriseVo selectVoWithJobsById(Long id);
/**
* 分页查询企业列表(包含岗位数量,job_count通过子查询计算)
*
* @param page 分页参数
* @param bo 查询条件
* @return 企业分页列表
*/
Page<ZsjyEnterpriseVo> selectVoPageWithJobCount(IPage<ZsjyEnterpriseVo> page, @Param("bo") ZsjyEnterpriseBo bo);
}
@@ -0,0 +1,74 @@
package org.dromara.zsjy.service;
import org.dromara.zsjy.domain.vo.ZsjyEnterpriseVo;
import org.dromara.zsjy.domain.vo.ZsjyEnterpriseBrochureVo;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
/**
* 招聘简章生成Service接口
*
* @author Cursor AI
* @date 2026-03-30
*/
public interface IZsjyEnterpriseBrochureService {
/**
* 生成企业招聘简章PDF
*
* @param enterpriseId 企业ID
* @return 简章记录
*/
ZsjyEnterpriseBrochureVo generateBrochure(Long enterpriseId);
/**
* 批量生成企业招聘简章PDF
*
* @param enterpriseIds 企业ID列表
* @return 简章记录列表
*/
List<ZsjyEnterpriseBrochureVo> generateBrochureBatch(List<Long> enterpriseIds);
/**
* 下载PDF(通过response直接输出)
*
* @param enterpriseId 企业ID
* @param response HttpServletResponse
*/
void downloadPdf(Long enterpriseId, HttpServletResponse response);
/**
* 下载PDF(支持自定义文件名)
*
* @param enterpriseId 企业ID
* @param fileName 自定义文件名(不含扩展名)
* @param response HttpServletResponse
*/
void downloadPdf(Long enterpriseId, String fileName, HttpServletResponse response);
/**
* 获取简章记录
*
* @param enterpriseId 企业ID
* @return 简章记录
*/
ZsjyEnterpriseBrochureVo getLatestBrochure(Long enterpriseId);
/**
* 按ID下载PDF
*
* @param id 简章记录ID
* @param response HttpServletResponse
*/
void downloadPdfById(Long id, HttpServletResponse response);
/**
* 批量下载多页PDF(每个企业一页)
*
* @param enterpriseIds 企业ID列表
* @param response HttpServletResponse
*/
void downloadBatchPdf(List<Long> enterpriseIds, HttpServletResponse response);
}
@@ -0,0 +1,68 @@
package org.dromara.zsjy.service;
import org.dromara.zsjy.domain.bo.ZsjyEnterpriseJobBo;
import org.dromara.zsjy.domain.vo.ZsjyEnterpriseJobVo;
import org.dromara.common.mybatis.core.page.PageQuery;
import java.util.Collection;
import java.util.List;
/**
* 企业招聘岗位Service接口
*
* @author Cursor AI
* @date 2026-03-30
*/
public interface IZsjyEnterpriseJobService {
/**
* 查询岗位
*
* @param id 岗位主键
* @return 岗位
*/
ZsjyEnterpriseJobVo queryById(Long id);
/**
* 按企业ID查询岗位列表
*
* @param enterpriseId 企业ID
* @return 岗位列表
*/
List<ZsjyEnterpriseJobVo> queryListByEnterpriseId(Long enterpriseId);
/**
* 查询岗位列表
*/
List<ZsjyEnterpriseJobVo> queryList(ZsjyEnterpriseJobBo bo);
/**
* 新增岗位
*/
Boolean insertByBo(ZsjyEnterpriseJobBo bo);
/**
* 批量新增岗位(用于企业新增/编辑时批量插入岗位)
*
* @param enterpriseId 企业ID
* @param jobList 岗位列表
* @return 结果
*/
Boolean insertBatch(Long enterpriseId, List<ZsjyEnterpriseJobBo> jobList);
/**
* 修改岗位
*/
Boolean updateByBo(ZsjyEnterpriseJobBo bo);
/**
* 删除岗位
*/
Boolean deleteWithValidByIds(Collection<Long> ids);
/**
* 按企业ID删除岗位(用于企业删除时级联删除)
*/
Boolean deleteByEnterpriseIds(Collection<Long> enterpriseIds);
}
@@ -2,6 +2,7 @@ package org.dromara.zsjy.service;
import org.dromara.zsjy.domain.bo.ZsjyEnterpriseBo;
import org.dromara.zsjy.domain.vo.ZsjyEnterpriseVo;
import org.dromara.zsjy.domain.bo.ZsjyEnterpriseJobBo;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
@@ -11,7 +12,7 @@ import java.util.List;
/**
* 企业信息Service接口
*
* @author Lion Li
* @author Lion Li (改造: Cursor AI 2026-03-30)
* @date 2023-07-05
*/
public interface IZsjyEnterpriseService {
@@ -25,23 +26,25 @@ public interface IZsjyEnterpriseService {
ZsjyEnterpriseVo queryById(Long id);
/**
* 查询企业信息列表
*
* @param bo 企业信息
* @return 企业信息集合
* 查询企业信息列表(包含岗位数量)
*/
TableDataInfo<ZsjyEnterpriseVo> queryPageList(ZsjyEnterpriseBo bo, PageQuery pageQuery);
/**
* 查询企业信息列表
*
* @param bo 企业信息
* @return 企业信息集合
*/
List<ZsjyEnterpriseVo> queryList(ZsjyEnterpriseBo bo);
/**
* 新增企业信息
* 查询企业详情(包含岗位列表)
*
* @param id 企业ID
* @return 企业信息(含岗位列表)
*/
ZsjyEnterpriseVo queryWithJobs(Long id);
/**
* 新增企业信息(同时新增岗位列表)
*
* @param bo 企业信息
* @return 结果
@@ -49,7 +52,7 @@ public interface IZsjyEnterpriseService {
Boolean insertByBo(ZsjyEnterpriseBo bo);
/**
* 修改企业信息
* 修改企业信息(同时更新岗位列表)
*
* @param bo 企业信息
* @return 结果
@@ -57,7 +60,7 @@ public interface IZsjyEnterpriseService {
Boolean updateByBo(ZsjyEnterpriseBo bo);
/**
* 校验并批量删除企业信息信息
* 校验并批量删除企业信息
*
* @param ids 需要删除的企业信息主键集合
* @param isValid 是否校验,true-删除前校验,false-不校验
@@ -65,4 +68,19 @@ public interface IZsjyEnterpriseService {
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 根据企业名称查询(用于导入时唯一性校验)
*/
ZsjyEnterpriseVo queryByName(String enterpriseName);
/**
* 批量导入企业及岗位信息
*
* @param boList 企业及岗位列表
* @param isUpdateSupport 是否支持更新(同名覆盖)
* @param operName 操作人
* @return 导入结果
*/
String importData(List<ZsjyEnterpriseBo> boList, Boolean isUpdateSupport, String operName);
}
@@ -0,0 +1,545 @@
package org.dromara.zsjy.service.impl;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.zsjy.domain.ZsjyEnterpriseBrochure;
import org.dromara.zsjy.domain.vo.ZsjyEnterpriseVo;
import org.dromara.zsjy.domain.vo.ZsjyEnterpriseJobVo;
import org.dromara.zsjy.domain.vo.ZsjyEnterpriseBrochureVo;
import org.dromara.zsjy.mapper.ZsjyEnterpriseBrochureMapper;
import org.dromara.zsjy.service.IZsjyEnterpriseBrochureService;
import org.dromara.zsjy.service.IZsjyEnterpriseService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import jakarta.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import com.itextpdf.text.*;
import com.itextpdf.text.pdf.*;
import com.itextpdf.text.pdf.PdfCopy;
/**
* 招聘简章PDF生成Service实现类
*
* @author Cursor AI
* @date 2026-03-30
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class ZsjyEnterpriseBrochureServiceImpl implements IZsjyEnterpriseBrochureService {
private final IZsjyEnterpriseService enterpriseService;
private final ZsjyEnterpriseBrochureMapper brochureMapper;
@Value("${ruoyi.profile:./uploadPath}")
private String uploadPath;
private static final String BROCHURE_DIR = "enterprise-brochure";
private static final String PDF_EXTENSION = ".pdf";
private static final Map<Long, String> PDF_CACHE = new ConcurrentHashMap<>();
@Override
@Transactional(rollbackFor = Exception.class)
public ZsjyEnterpriseBrochureVo generateBrochure(Long enterpriseId) {
ZsjyEnterpriseVo enterprise = enterpriseService.queryWithJobs(enterpriseId);
if (enterprise == null) {
throw new ServiceException("企业信息不存在");
}
if (CollUtil.isEmpty(enterprise.getJobList())) {
throw new ServiceException("该企业下没有招聘岗位,无法生成简章");
}
try {
String dateDir = java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy/MM/dd"));
Path storageDir = Paths.get(uploadPath, BROCHURE_DIR, dateDir);
Files.createDirectories(storageDir);
String fileName = enterprise.getEnterpriseName() + "_" + System.currentTimeMillis() + PDF_EXTENSION;
Path filePath = storageDir.resolve(fileName);
generatePdfDocument(enterprise, filePath);
ZsjyEnterpriseBrochure brochure = new ZsjyEnterpriseBrochure();
brochure.setEnterpriseId(enterpriseId);
brochure.setBrochurePath(filePath.toString());
brochure.setFileName(fileName);
brochure.setFileSize(Files.size(filePath));
brochure.setGenerateBy(LoginHelper.getUserId());
brochure.setGenerateByName(LoginHelper.getUsername());
brochure.setGenerateTime(new Date());
brochureMapper.insert(brochure);
PDF_CACHE.put(enterpriseId, filePath.toString());
log.info("成功生成企业招聘简章PDF,企业ID: {}, 文件: {}", enterpriseId, filePath);
ZsjyEnterpriseBrochureVo vo = MapstructUtils.convert(brochure, ZsjyEnterpriseBrochureVo.class);
vo.setEnterpriseName(enterprise.getEnterpriseName());
return vo;
} catch (Exception e) {
log.error("生成招聘简章PDF失败,企业ID: {}", enterpriseId, e);
throw new ServiceException("生成招聘简章失败: " + e.getMessage());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public java.util.List<ZsjyEnterpriseBrochureVo> generateBrochureBatch(java.util.List<Long> enterpriseIds) {
java.util.List<ZsjyEnterpriseBrochureVo> results = new ArrayList<>();
for (Long id : enterpriseIds) {
try {
results.add(generateBrochure(id));
} catch (Exception e) {
log.error("批量生成简章失败,企业ID: {}", id, e);
}
}
return results;
}
@Override
public void downloadPdf(Long enterpriseId, HttpServletResponse response) {
downloadPdf(enterpriseId, null, response);
}
@Override
public void downloadPdf(Long enterpriseId, String fileName, HttpServletResponse response) {
String filePath = PDF_CACHE.get(enterpriseId);
if (StringUtils.isBlank(filePath)) {
log.debug("PDF缓存未命中,从数据库查询,企业ID: {}", enterpriseId);
ZsjyEnterpriseBrochureVo latest = getLatestBrochure(enterpriseId);
if (latest == null) {
log.error("未找到简章记录,企业ID: {}", enterpriseId);
throw new ServiceException("未找到该企业的简章记录,请先生成");
}
if (StringUtils.isBlank(latest.getBrochurePath())) {
log.error("简章记录存在但文件路径为空,简章ID: {}", latest.getId());
throw new ServiceException("简章文件路径不存在,请重新生成");
}
filePath = latest.getBrochurePath();
PDF_CACHE.put(enterpriseId, filePath);
// 如果没有指定文件名且查询到了企业名称,使用企业名称
if (StringUtils.isBlank(fileName) && StringUtils.isNotBlank(latest.getEnterpriseName())) {
fileName = latest.getEnterpriseName();
}
}
log.info("准备下载PDF,文件路径: {}", filePath);
outputFile(filePath, fileName, response);
}
@Override
public ZsjyEnterpriseBrochureVo getLatestBrochure(Long enterpriseId) {
LambdaQueryWrapper<ZsjyEnterpriseBrochure> lqw = Wrappers.lambdaQuery();
lqw.eq(ZsjyEnterpriseBrochure::getEnterpriseId, enterpriseId);
lqw.orderByDesc(ZsjyEnterpriseBrochure::getCreateTime);
lqw.last("LIMIT 1");
ZsjyEnterpriseBrochure brochure = brochureMapper.selectOne(lqw);
if (brochure == null) {
return null;
}
ZsjyEnterpriseBrochureVo vo = MapstructUtils.convert(brochure, ZsjyEnterpriseBrochureVo.class);
ZsjyEnterpriseVo enterprise = enterpriseService.queryById(enterpriseId);
if (enterprise != null) {
vo.setEnterpriseName(enterprise.getEnterpriseName());
}
return vo;
}
@Override
public void downloadPdfById(Long id, HttpServletResponse response) {
ZsjyEnterpriseBrochure brochure = brochureMapper.selectById(id);
if (brochure == null || StringUtils.isBlank(brochure.getBrochurePath())) {
throw new ServiceException("PDF文件不存在");
}
outputFile(brochure.getBrochurePath(), response);
}
@Override
public void downloadBatchPdf(java.util.List<Long> enterpriseIds, HttpServletResponse response) {
if (CollUtil.isEmpty(enterpriseIds)) {
throw new ServiceException("请选择要下载的企业");
}
try {
// 创建一个内存中的 PDF
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Rectangle pageSize = PageSize.A4;
Document document = new Document(pageSize, 0, 0, 0, 0);
PdfCopy copy = new PdfCopy(document, baos);
document.open();
BaseFont baseFont = resolveChineseBaseFont();
int pageNumber = 1;
for (Long enterpriseId : enterpriseIds) {
try {
ZsjyEnterpriseVo enterprise = enterpriseService.queryWithJobs(enterpriseId);
if (enterprise == null) {
log.warn("企业信息不存在,跳过,企业ID: {}", enterpriseId);
continue;
}
if (CollUtil.isEmpty(enterprise.getJobList())) {
log.warn("该企业下没有招聘岗位,跳过,企业ID: {}", enterpriseId);
continue;
}
// 为每个企业创建一页 PDF
PdfReader reader = generateSinglePagePdf(enterprise, baseFont);
for (int i = 1; i <= reader.getNumberOfPages(); i++) {
PdfImportedPage page = copy.getImportedPage(reader, i);
copy.addPage(page);
}
reader.close();
pageNumber++;
} catch (Exception e) {
log.error("处理企业PDF失败,企业ID: {}", enterpriseId, e);
}
}
document.close();
// 检查是否有内容
if (pageNumber == 1) {
throw new ServiceException("没有可生成的简章内容");
}
// 输出到响应
byte[] pdfBytes = baos.toByteArray();
response.setContentType("application/pdf");
String fileName = "企业招聘简章_" + System.currentTimeMillis() + ".pdf";
String encodedFileName = java.net.URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedFileName);
response.setContentLengthLong(pdfBytes.length);
response.getOutputStream().write(pdfBytes);
response.getOutputStream().flush();
log.info("批量下载PDF完成,共 {} 页", pageNumber - 1);
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
log.error("批量下载PDF失败", e);
throw new ServiceException("批量下载PDF失败: " + e.getMessage());
}
}
/**
* 为单个企业生成一页 PDF(返回 PdfReader)
*/
private PdfReader generateSinglePagePdf(ZsjyEnterpriseVo enterprise, BaseFont baseFont) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Rectangle pageSize = PageSize.A4;
Document document = new Document(pageSize, 0, 0, 0, 0);
PdfWriter writer = PdfWriter.getInstance(document, baos);
document.open();
float pageW = pageSize.getWidth();
float pageH = pageSize.getHeight();
Image bg = loadBrochureBackgroundImage();
boolean hasBg = bg != null;
if (hasBg) {
bg.scaleAbsolute(pageW, pageH);
bg.setAbsolutePosition(0, 0);
writer.getDirectContentUnder().addImage(bg);
}
PdfContentByte canvas = writer.getDirectContent();
BaseColor headerFooterColor = hasBg ? BaseColor.WHITE : new BaseColor(26, 82, 118);
Font headerNameFont = new Font(baseFont, 12, Font.BOLD, headerFooterColor);
Font footerFont = new Font(baseFont, 9.5f, Font.BOLD, headerFooterColor);
Font bodyHeadingFont = new Font(baseFont, 11, Font.BOLD, new BaseColor(0, 80, 140));
Font bodyFont = new Font(baseFont, 10, Font.NORMAL, new BaseColor(30, 30, 30));
Font pageNoFont = new Font(baseFont, 9, Font.NORMAL, new BaseColor(90, 90, 90));
// 左上蓝条:企业名称
String enterpriseName = StringUtils.defaultString(enterprise.getEnterpriseName(), "—");
ColumnText.showTextAligned(canvas, Element.ALIGN_LEFT, new Phrase(enterpriseName, headerNameFont),
70f, baselineFromTopPx(78f), 0);
// 中间内容区
ColumnText bodyColumn = new ColumnText(canvas);
float bodyLlx = 48f;
float bodyLly = baselineFromTopPx(706f);
float bodyUrx = 547f;
float bodyUry = baselineFromTopPx(120f);
bodyColumn.setSimpleColumn(bodyLlx, bodyLly, bodyUrx, bodyUry);
bodyColumn.setLeading(13f);
if (StringUtils.isNotBlank(enterprise.getEnterpriseIntro())) {
bodyColumn.addElement(new Paragraph("企业简介", bodyHeadingFont));
bodyColumn.addElement(new Paragraph(enterprise.getEnterpriseIntro(), bodyFont));
bodyColumn.addElement(new Paragraph(" ", bodyFont));
}
for (ZsjyEnterpriseJobVo job : enterprise.getJobList()) {
bodyColumn.addElement(new Paragraph("岗位名称:" + nvl(job.getPositionName()), bodyHeadingFont));
bodyColumn.addElement(new Paragraph("岗位要求:" + nvl(job.getRequirements()), bodyFont));
bodyColumn.addElement(new Paragraph("待遇:" + nvl(job.getBenefits()), bodyFont));
bodyColumn.addElement(new Paragraph(" ", bodyFont));
}
int flowStatus = bodyColumn.go();
if (ColumnText.hasMoreText(flowStatus)) {
log.warn("招聘简章正文超出模板区域,内容可能被截断,企业: {}", enterprise.getEnterpriseName());
}
// 底部蓝条
float footerX = 72f;
ColumnText.showTextAligned(canvas, Element.ALIGN_LEFT,
new Phrase("单位地址:" + nvl(enterprise.getAddress()), footerFont),
footerX, baselineFromTopPx(722f), 0);
String contactLine = "联系人:" + nvl(enterprise.getContactName());
if (StringUtils.isNotBlank(enterprise.getContactPhone())) {
contactLine += " 联系电话:" + enterprise.getContactPhone();
}
ColumnText.showTextAligned(canvas, Element.ALIGN_LEFT, new Phrase(contactLine, footerFont),
footerX, baselineFromTopPx(740f), 0);
ColumnText.showTextAligned(canvas, Element.ALIGN_LEFT,
new Phrase("电子邮箱:" + nvl(enterprise.getEmail()), footerFont),
footerX, baselineFromTopPx(758f), 0);
// 页码
ColumnText.showTextAligned(canvas, Element.ALIGN_CENTER, new Phrase("—1—", pageNoFont),
pageW / 2f, baselineFromTopPx(778f), 0);
document.close();
return new PdfReader(baos.toByteArray());
}
private void outputFile(String filePath, HttpServletResponse response) {
outputFile(filePath, null, response);
}
private void outputFile(String filePath, String customFileName, HttpServletResponse response) {
try {
Path path = Paths.get(filePath);
if (!Files.exists(path)) {
log.error("PDF文件不存在: {}", filePath);
throw new ServiceException("PDF文件不存在");
}
long fileSize = Files.size(path);
log.info("开始输出PDF文件: {}, 大小: {} bytes", filePath, fileSize);
response.setContentType("application/pdf");
String fileNameStr;
if (StringUtils.isNotBlank(customFileName)) {
// 使用自定义文件名
fileNameStr = customFileName + ".pdf";
} else {
// 使用原文件名
fileNameStr = path.getFileName().toString();
}
String encodedFileName = java.net.URLEncoder.encode(fileNameStr, "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedFileName);
response.setContentLengthLong(fileSize);
try (InputStream inputStream = Files.newInputStream(path);
OutputStream outputStream = response.getOutputStream()) {
byte[] buffer = new byte[8192];
int bytesRead;
long totalRead = 0;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
totalRead += bytesRead;
}
outputStream.flush();
log.info("PDF文件输出完成,已传输: {} bytes", totalRead);
}
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
log.error("输出PDF文件失败: {}", filePath, e);
throw new ServiceException("下载PDF文件失败: " + e.getMessage());
}
}
/**
* 校园招聘简章背景模板(PNG 与 A4 对齐,资源路径 classpath:/brochure/recruitment-brochure-bg.png)
*/
private static final String BROCHURE_BG_RESOURCE = "/brochure/recruitment-brochure-bg.png";
/**
* 模板图高度(像素与 PDF 点一致 595×841),用于将「距顶部像素」换算为 PDF 纵坐标
*/
private static final float TEMPLATE_H = 841f;
private Image loadBrochureBackgroundImage() {
try (InputStream in = getClass().getResourceAsStream(BROCHURE_BG_RESOURCE)) {
if (in == null) {
log.warn("未找到招聘简章背景图: {},将生成无背景 PDF", BROCHURE_BG_RESOURCE);
return null;
}
byte[] bytes = in.readAllBytes();
return Image.getInstance(bytes);
} catch (Exception e) {
log.warn("加载招聘简章背景图失败: {}", e.getMessage());
return null;
}
}
/** 模板图「距顶部」的像素行号 → PDF 基线纵坐标(原点在页面左下) */
private static float baselineFromTopPx(float yFromTopPx) {
return TEMPLATE_H - yFromTopPx;
}
private void generatePdfDocument(ZsjyEnterpriseVo enterprise, Path filePath) throws DocumentException, IOException {
Rectangle pageSize = PageSize.A4;
Document document = new Document(pageSize, 0, 0, 0, 0);
PdfWriter writer = PdfWriter.getInstance(document, Files.newOutputStream(filePath));
document.open();
BaseFont baseFont = resolveChineseBaseFont();
float pageW = pageSize.getWidth();
float pageH = pageSize.getHeight();
Image bg = loadBrochureBackgroundImage();
boolean hasBg = bg != null;
if (hasBg) {
bg.scaleAbsolute(pageW, pageH);
bg.setAbsolutePosition(0, 0);
writer.getDirectContentUnder().addImage(bg);
}
PdfContentByte canvas = writer.getDirectContent();
BaseColor headerFooterColor = hasBg ? BaseColor.WHITE : new BaseColor(26, 82, 118);
Font headerNameFont = new Font(baseFont, 12, Font.BOLD, headerFooterColor);
Font footerFont = new Font(baseFont, 9.5f, Font.BOLD, headerFooterColor);
Font bodyHeadingFont = new Font(baseFont, 11, Font.BOLD, new BaseColor(0, 80, 140));
Font bodyFont = new Font(baseFont, 10, Font.NORMAL, new BaseColor(30, 30, 30));
Font pageNoFont = new Font(baseFont, 9, Font.NORMAL, new BaseColor(90, 90, 90));
// 左上蓝条原占位「企业名称」→ 叠加真实企业名称(白字)
String enterpriseName = StringUtils.defaultString(enterprise.getEnterpriseName(), "—");
ColumnText.showTextAligned(canvas, Element.ALIGN_LEFT, new Phrase(enterpriseName, headerNameFont),
70f, baselineFromTopPx(78f), 0);
// 中间留白区:企业简介 + 各岗位(与模板图 595×841 标定:顶约 120px,底约 706px)
ColumnText bodyColumn = new ColumnText(canvas);
float bodyLlx = 48f;
float bodyLly = baselineFromTopPx(706f);
float bodyUrx = 547f;
// 顶部边界向下调整,避免遮盖右侧蓝色设计区域
float bodyUry = baselineFromTopPx(120f);
bodyColumn.setSimpleColumn(bodyLlx, bodyLly, bodyUrx, bodyUry);
bodyColumn.setLeading(13f);
if (StringUtils.isNotBlank(enterprise.getEnterpriseIntro())) {
bodyColumn.addElement(new Paragraph("企业简介", bodyHeadingFont));
bodyColumn.addElement(new Paragraph(enterprise.getEnterpriseIntro(), bodyFont));
bodyColumn.addElement(new Paragraph(" ", bodyFont));
}
for (ZsjyEnterpriseJobVo job : enterprise.getJobList()) {
bodyColumn.addElement(new Paragraph("岗位名称:" + nvl(job.getPositionName()), bodyHeadingFont));
bodyColumn.addElement(new Paragraph("岗位要求:" + nvl(job.getRequirements()), bodyFont));
bodyColumn.addElement(new Paragraph("待遇:" + nvl(job.getBenefits()), bodyFont));
bodyColumn.addElement(new Paragraph(" ", bodyFont));
}
int flowStatus = bodyColumn.go();
if (ColumnText.hasMoreText(flowStatus)) {
log.warn("招聘简章正文超出模板中间区域,内容可能被截断,企业: {}", enterprise.getEnterpriseName());
}
// 底部蓝条标签旁叠加字段值(白字):单位地址、联系人、电子邮箱
float footerX = 72f;
ColumnText.showTextAligned(canvas, Element.ALIGN_LEFT,
new Phrase("单位地址:" + nvl(enterprise.getAddress()), footerFont),
footerX, baselineFromTopPx(722f), 0);
String contactLine = "联系人:" + nvl(enterprise.getContactName());
if (StringUtils.isNotBlank(enterprise.getContactPhone())) {
contactLine += " 联系电话:" + enterprise.getContactPhone();
}
ColumnText.showTextAligned(canvas, Element.ALIGN_LEFT, new Phrase(contactLine, footerFont),
footerX, baselineFromTopPx(740f), 0);
ColumnText.showTextAligned(canvas, Element.ALIGN_LEFT,
new Phrase("电子邮箱:" + nvl(enterprise.getEmail()), footerFont),
footerX, baselineFromTopPx(758f), 0);
// 模板底部「—1—」页码区(单页简章固定为 1)
ColumnText.showTextAligned(canvas, Element.ALIGN_CENTER, new Phrase("—1—", pageNoFont),
pageW / 2f, baselineFromTopPx(778f), 0);
document.close();
}
private BaseFont resolveChineseBaseFont() {
BaseFont baseFont = null;
boolean fontLoaded = false;
try {
String[] fontPaths = {
"/System/Library/Fonts/STHeiti Light.ttc",
"/System/Library/Fonts/PingFang.ttc",
"/System/Library/Fonts/STSongti.ttf",
"/Library/Fonts/Arial Unicode.ttf",
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
"/usr/share/fonts/truetype/arphic/uming.ttc",
"STSong-Light",
"STSongStd-Light",
"SIMHEI",
"SIMSUN"
};
for (String fontPath : fontPaths) {
try {
if (fontPath.startsWith("/")) {
File fontFile = new File(fontPath);
if (fontFile.exists()) {
baseFont = BaseFont.createFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
fontLoaded = true;
log.info("成功加载中文字体: {}", fontPath);
break;
}
} else {
baseFont = BaseFont.createFont(fontPath, "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
fontLoaded = true;
log.info("成功加载中文字体: {}", fontPath);
break;
}
} catch (Exception e) {
log.debug("字体加载失败: {}, 尝试下一个", fontPath);
}
}
if (!fontLoaded) {
baseFont = BaseFont.createFont(BaseFont.HELVETICA, BaseFont.CP1252, BaseFont.NOT_EMBEDDED);
log.warn("无法加载中文字体,使用默认字体,中文可能无法正确显示");
}
} catch (DocumentException | IOException e) {
log.error("PDF字体初始化失败", e);
throw new ServiceException("PDF字体初始化失败,请检查字体配置");
}
if (baseFont == null) {
throw new ServiceException("PDF字体初始化失败");
}
return baseFont;
}
private String nvl(String str) {
return str == null ? "-" : str;
}
}
@@ -0,0 +1,119 @@
package org.dromara.zsjy.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.dromara.zsjy.domain.bo.ZsjyEnterpriseJobBo;
import org.dromara.zsjy.domain.ZsjyEnterpriseJob;
import org.dromara.zsjy.domain.vo.ZsjyEnterpriseJobVo;
import org.dromara.zsjy.mapper.ZsjyEnterpriseJobMapper;
import org.dromara.zsjy.service.IZsjyEnterpriseJobService;
import java.util.Collection;
import java.util.List;
/**
* 企业招聘岗位Service业务层处理
*
* @author Cursor AI
* @date 2026-03-30
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class ZsjyEnterpriseJobServiceImpl implements IZsjyEnterpriseJobService {
private final ZsjyEnterpriseJobMapper baseMapper;
@Override
public ZsjyEnterpriseJobVo queryById(Long id) {
return baseMapper.selectVoById(id);
}
@Override
public List<ZsjyEnterpriseJobVo> queryListByEnterpriseId(Long enterpriseId) {
LambdaQueryWrapper<ZsjyEnterpriseJob> lqw = Wrappers.lambdaQuery();
lqw.eq(ZsjyEnterpriseJob::getEnterpriseId, enterpriseId);
lqw.eq(ZsjyEnterpriseJob::getStatus, "0");
lqw.orderByAsc(ZsjyEnterpriseJob::getCreateTime);
return baseMapper.selectVoList(lqw);
}
@Override
public List<ZsjyEnterpriseJobVo> queryList(ZsjyEnterpriseJobBo bo) {
LambdaQueryWrapper<ZsjyEnterpriseJob> lqw = buildQueryWrapper(bo);
lqw.orderByDesc(ZsjyEnterpriseJob::getCreateTime);
return baseMapper.selectVoList(lqw);
}
private LambdaQueryWrapper<ZsjyEnterpriseJob> buildQueryWrapper(ZsjyEnterpriseJobBo bo) {
LambdaQueryWrapper<ZsjyEnterpriseJob> lqw = Wrappers.lambdaQuery();
lqw.eq(bo.getEnterpriseId() != null, ZsjyEnterpriseJob::getEnterpriseId, bo.getEnterpriseId());
lqw.like(StringUtils.isNotBlank(bo.getPositionName()), ZsjyEnterpriseJob::getPositionName, bo.getPositionName());
lqw.eq(StringUtils.isNotBlank(bo.getStatus()), ZsjyEnterpriseJob::getStatus, bo.getStatus());
return lqw;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean insertByBo(ZsjyEnterpriseJobBo bo) {
ZsjyEnterpriseJob add = MapstructUtils.convert(bo, ZsjyEnterpriseJob.class);
if (StringUtils.isEmpty(add.getStatus())) {
add.setStatus("0");
}
return baseMapper.insert(add) > 0;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean insertBatch(Long enterpriseId, List<ZsjyEnterpriseJobBo> jobList) {
if (jobList == null || jobList.isEmpty()) {
return true;
}
for (ZsjyEnterpriseJobBo bo : jobList) {
ZsjyEnterpriseJob add = MapstructUtils.convert(bo, ZsjyEnterpriseJob.class);
add.setId(null); // 清除旧ID,让MyBatis-Plus自动生成新的雪花ID
add.setEnterpriseId(enterpriseId);
if (StringUtils.isEmpty(add.getStatus())) {
add.setStatus("0");
}
baseMapper.insert(add);
}
return true;
}
@Override
public Boolean updateByBo(ZsjyEnterpriseJobBo bo) {
ZsjyEnterpriseJob update = MapstructUtils.convert(bo, ZsjyEnterpriseJob.class);
return baseMapper.updateById(update) > 0;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean deleteWithValidByIds(Collection<Long> ids) {
return baseMapper.deleteBatchIds(ids) > 0;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean deleteByEnterpriseIds(Collection<Long> enterpriseIds) {
if (enterpriseIds == null || enterpriseIds.isEmpty()) {
return true;
}
for (Long enterpriseId : enterpriseIds) {
LambdaQueryWrapper<ZsjyEnterpriseJob> wrapper = Wrappers.lambdaQuery();
wrapper.eq(ZsjyEnterpriseJob::getEnterpriseId, enterpriseId);
baseMapper.delete(wrapper);
}
return true;
}
}
@@ -1,144 +1,227 @@
package org.dromara.zsjy.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.dromara.zsjy.domain.bo.ZsjyEnterpriseBo;
import org.dromara.zsjy.domain.bo.ZsjyEnterpriseJobBo;
import org.dromara.zsjy.domain.ZsjyEnterprise;
import org.dromara.zsjy.domain.ZsjyJobPosition;
import org.dromara.zsjy.domain.ZsjyEnterpriseJob;
import org.dromara.zsjy.domain.vo.ZsjyEnterpriseVo;
import org.dromara.zsjy.domain.vo.ZsjyEnterpriseJobVo;
import org.dromara.zsjy.mapper.ZsjyEnterpriseMapper;
import org.dromara.zsjy.mapper.ZsjyJobPositionMapper;
import org.dromara.zsjy.mapper.ZsjyEnterpriseJobMapper;
import org.dromara.zsjy.service.IZsjyEnterpriseService;
import org.dromara.zsjy.service.IZsjyEnterpriseJobService;
import java.util.List;
import java.util.Map;
import java.util.Collection;
import java.util.List;
/**
* 企业信息Service业务层处理
*
* @author Lion Li
* @author Lion Li (改造: Cursor AI 2026-03-30)
* @date 2023-07-05
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class ZsjyEnterpriseServiceImpl implements IZsjyEnterpriseService {
private final ZsjyEnterpriseMapper baseMapper;
private final ZsjyJobPositionMapper jobPositionMapper;
private final ZsjyEnterpriseJobMapper jobMapper;
private final IZsjyEnterpriseJobService jobService;
/**
* 查询企业信息
*
* @param id 企业信息主键
* @return 企业信息
*/
@Override
public ZsjyEnterpriseVo queryById(Long id) {
return baseMapper.selectVoById(id);
}
/**
* 查询企业信息列表
*
* @param bo 企业信息
* @return 企业信息
*/
@Override
public TableDataInfo<ZsjyEnterpriseVo> queryPageList(ZsjyEnterpriseBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<ZsjyEnterprise> lqw = buildQueryWrapper(bo);
Page<ZsjyEnterpriseVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
Page<ZsjyEnterpriseVo> page = pageQuery.build();
Page<ZsjyEnterpriseVo> result = baseMapper.selectVoPageWithJobCount(page, bo);
return TableDataInfo.build(result);
}
/**
* 查询企业信息列表
*
* @param bo 企业信息
* @return 企业信息
*/
@Override
public List<ZsjyEnterpriseVo> queryList(ZsjyEnterpriseBo bo) {
LambdaQueryWrapper<ZsjyEnterprise> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
@Override
public ZsjyEnterpriseVo queryWithJobs(Long id) {
ZsjyEnterpriseVo vo = baseMapper.selectVoById(id);
if (vo != null) {
LambdaQueryWrapper<ZsjyEnterpriseJob> jobWrapper = Wrappers.lambdaQuery();
jobWrapper.eq(ZsjyEnterpriseJob::getEnterpriseId, id);
jobWrapper.eq(ZsjyEnterpriseJob::getStatus, "0");
jobWrapper.orderByAsc(ZsjyEnterpriseJob::getCreateTime);
List<ZsjyEnterpriseJobVo> jobs = jobMapper.selectVoList(jobWrapper);
vo.setJobList(jobs);
}
return vo;
}
private LambdaQueryWrapper<ZsjyEnterprise> buildQueryWrapper(ZsjyEnterpriseBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<ZsjyEnterprise> lqw = Wrappers.lambdaQuery();
lqw.like(StringUtils.isNotBlank(bo.getEnterpriseName()), ZsjyEnterprise::getEnterpriseName, bo.getEnterpriseName());
lqw.eq(StringUtils.isNotBlank(bo.getIndustry()), ZsjyEnterprise::getIndustry, bo.getIndustry());
lqw.like(StringUtils.isNotBlank(bo.getMainBusiness()), ZsjyEnterprise::getMainBusiness, bo.getMainBusiness());
lqw.eq(StringUtils.isNotBlank(bo.getEnterpriseScale()), ZsjyEnterprise::getEnterpriseScale, bo.getEnterpriseScale());
lqw.eq(StringUtils.isNotBlank(bo.getStatus()), ZsjyEnterprise::getStatus, bo.getStatus());
lqw.like(StringUtils.isNotBlank(bo.getContactName()), ZsjyEnterprise::getContactName, bo.getContactName());
lqw.like(StringUtils.isNotBlank(bo.getContactPhone()), ZsjyEnterprise::getContactPhone, bo.getContactPhone());
lqw.like(StringUtils.isNotBlank(bo.getEmail()), ZsjyEnterprise::getEmail, bo.getEmail());
lqw.like(StringUtils.isNotBlank(bo.getAddress()), ZsjyEnterprise::getAddress, bo.getAddress());
lqw.eq(StringUtils.isNotBlank(bo.getStatus()), ZsjyEnterprise::getStatus, bo.getStatus());
return lqw;
}
/**
* 新增企业信息
*
* @param bo 企业信息
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean insertByBo(ZsjyEnterpriseBo bo) {
ZsjyEnterprise add = MapstructUtils.convert(bo, ZsjyEnterprise.class);
// 默认状态为正常
if (StringUtils.isEmpty(add.getStatus())) {
add.setStatus("0");
}
log.info("开始插入企业信息: {}", bo.getEnterpriseName());
// 手动创建实体对象并复制字段,避免MapStruct转换问题
ZsjyEnterprise add = new ZsjyEnterprise();
add.setEnterpriseName(bo.getEnterpriseName());
// 处理数据库NOT NULL字段,确保不为null
add.setIndustry(safeString(bo.getIndustry()));
add.setMainBusiness(safeString(bo.getMainBusiness()));
add.setEnterpriseScale(safeString(bo.getEnterpriseScale()));
add.setEnterpriseIntro(bo.getEnterpriseIntro());
add.setContactName(safeString(bo.getContactName()));
add.setContactPhone(safeString(bo.getContactPhone()));
add.setOfficePhone(bo.getOfficePhone());
add.setEmail(safeString(bo.getEmail()));
add.setWebsite(bo.getWebsite());
add.setAddress(safeString(bo.getAddress()));
add.setRemark(bo.getRemark());
add.setStatus(StringUtils.isEmpty(bo.getStatus()) ? "0" : bo.getStatus());
boolean flag = baseMapper.insert(add) > 0;
log.info("企业信息插入结果: {}, 企业ID: {}", flag, add.getId());
if (flag) {
bo.setId(add.getId());
if (CollUtil.isNotEmpty(bo.getJobList())) {
log.info("开始插入岗位信息,企业ID: {}, 岗位数量: {}", add.getId(), bo.getJobList().size());
jobService.insertBatch(add.getId(), bo.getJobList());
}
}
return flag;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean updateByBo(ZsjyEnterpriseBo bo) {
// 手动创建实体对象并复制字段
ZsjyEnterprise update = new ZsjyEnterprise();
update.setId(bo.getId());
update.setEnterpriseName(bo.getEnterpriseName());
update.setIndustry(safeString(bo.getIndustry()));
update.setMainBusiness(safeString(bo.getMainBusiness()));
update.setEnterpriseScale(safeString(bo.getEnterpriseScale()));
update.setEnterpriseIntro(bo.getEnterpriseIntro());
update.setContactName(safeString(bo.getContactName()));
update.setContactPhone(safeString(bo.getContactPhone()));
update.setOfficePhone(bo.getOfficePhone());
update.setEmail(safeString(bo.getEmail()));
update.setWebsite(bo.getWebsite());
update.setAddress(safeString(bo.getAddress()));
update.setRemark(bo.getRemark());
update.setStatus(bo.getStatus());
boolean flag = baseMapper.updateById(update) > 0;
if (flag) {
// 先删除该企业下所有旧岗位,再统一插入新岗位
jobService.deleteByEnterpriseIds(List.of(bo.getId()));
if (CollUtil.isNotEmpty(bo.getJobList())) {
jobService.insertBatch(bo.getId(), bo.getJobList());
}
}
return flag;
}
/**
* 修改企业信息
*
* @param bo 企业信息
* @return 结果
* 安全字符串转换,确保不为null
*/
@Override
public Boolean updateByBo(ZsjyEnterpriseBo bo) {
ZsjyEnterprise update = MapstructUtils.convert(bo, ZsjyEnterprise.class);
return baseMapper.updateById(update) > 0;
private String safeString(String value) {
return value == null ? "" : value;
}
/**
* 批量删除企业信息
*
* @param ids 需要删除的企业信息主键
* @param isValid 是否校验,true-删除前校验,false-不校验
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if (isValid) {
// 校验是否存在关联的岗位
for (Long id : ids) {
LambdaQueryWrapper<ZsjyJobPosition> wrapper = Wrappers.lambdaQuery(ZsjyJobPosition.class)
.eq(ZsjyJobPosition::getEnterpriseId, id);
long count = jobPositionMapper.selectCount(wrapper);
if (count > 0) {
List<ZsjyEnterpriseJobVo> jobs = jobService.queryListByEnterpriseId(id);
if (CollUtil.isNotEmpty(jobs)) {
ZsjyEnterpriseVo enterprise = queryById(id);
throw new ServiceException(enterprise.getEnterpriseName() + "存在关联岗位,不能删除");
throw new ServiceException(enterprise.getEnterpriseName() + " 存在关联岗位,不能删除");
}
}
}
jobService.deleteByEnterpriseIds(ids);
return baseMapper.deleteBatchIds(ids) > 0;
}
@Override
public ZsjyEnterpriseVo queryByName(String enterpriseName) {
LambdaQueryWrapper<ZsjyEnterprise> lqw = Wrappers.lambdaQuery();
lqw.eq(ZsjyEnterprise::getEnterpriseName, enterpriseName);
return baseMapper.selectVoOne(lqw);
}
@Override
@Transactional(rollbackFor = Exception.class)
public String importData(List<ZsjyEnterpriseBo> boList, Boolean isUpdateSupport, String operName) {
if (boList == null || boList.isEmpty()) {
throw new ServiceException("导入企业数据不能为空");
}
int successNum = 0;
int failureNum = 0;
StringBuilder successMsg = new StringBuilder();
StringBuilder failureMsg = new StringBuilder();
for (ZsjyEnterpriseBo bo : boList) {
try {
ZsjyEnterpriseVo exist = queryByName(bo.getEnterpriseName());
if (exist == null) {
insertByBo(bo);
successNum++;
successMsg.append("<br/>").append(successNum).append("、企业 ").append(bo.getEnterpriseName()).append(" 导入成功");
} else if (isUpdateSupport) {
bo.setId(exist.getId());
updateByBo(bo);
successNum++;
successMsg.append("<br/>").append(successNum).append("、企业 ").append(bo.getEnterpriseName()).append(" 更新成功");
} else {
failureNum++;
failureMsg.append("<br/>").append(failureNum).append("、企业 ").append(bo.getEnterpriseName()).append(" 已存在");
}
} catch (Exception e) {
failureNum++;
String msg = "<br/>" + failureNum + "、企业 " + bo.getEnterpriseName() + " 导入失败:" + e.getMessage();
failureMsg.append(msg);
log.error(msg, e);
}
}
if (failureNum > 0) {
failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
throw new ServiceException(failureMsg.toString());
} else {
successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
}
return successMsg.toString();
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.zsjy.mapper.ZsjyEnterpriseBrochureMapper">
<resultMap type="org.dromara.zsjy.domain.vo.ZsjyEnterpriseBrochureVo" id="ZsjyEnterpriseBrochureResult">
<id property="id" column="id"/>
<result property="enterpriseId" column="enterprise_id"/>
<result property="enterpriseName" column="enterprise_name"/>
<result property="brochurePath" column="brochure_path"/>
<result property="fileName" column="file_name"/>
<result property="fileSize" column="file_size"/>
<result property="generateBy" column="generate_by"/>
<result property="generateByName" column="generate_by_name"/>
<result property="generateTime" column="generate_time"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<select id="selectVoByEnterpriseId" resultMap="ZsjyEnterpriseBrochureResult">
SELECT b.*, e.enterprise_name
FROM zsjy_enterprise_brochure b
LEFT JOIN zsjy_enterprise e ON b.enterprise_id = e.id
WHERE b.enterprise_id = #{enterpriseId}
AND b.deleted = 0
ORDER BY b.create_time DESC
LIMIT 1
</select>
</mapper>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.zsjy.mapper.ZsjyEnterpriseJobMapper">
<resultMap type="org.dromara.zsjy.domain.vo.ZsjyEnterpriseJobVo" id="ZsjyEnterpriseJobResult">
<id property="id" column="id"/>
<result property="enterpriseId" column="enterprise_id"/>
<result property="positionName" column="position_name"/>
<result property="requirements" column="requirements"/>
<result property="benefits" column="benefits"/>
<result property="status" column="status"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<!-- 按企业ID查询岗位列表(用于简章展示,仅查询正常状态的岗位) -->
<select id="selectVoListByEnterpriseId" resultMap="ZsjyEnterpriseJobResult">
select id, enterprise_id, position_name, requirements, benefits, status,
create_by, create_time, update_by, update_time
from zsjy_enterprise_job
where enterprise_id = #{enterpriseId}
and deleted = 0
and status = '0'
order by create_time asc
</select>
</mapper>
@@ -12,9 +12,12 @@
<result property="mainBusiness" column="main_business"/>
<result property="enterpriseScale" column="enterprise_scale"/>
<result property="enterpriseScaleName" column="enterprise_scale_name"/>
<result property="enterpriseIntro" column="enterprise_intro"/>
<result property="contactName" column="contact_name"/>
<result property="contactPhone" column="contact_phone"/>
<result property="officePhone" column="office_phone"/>
<result property="email" column="email"/>
<result property="website" column="website"/>
<result property="address" column="address"/>
<result property="status" column="status"/>
<result property="remark" column="remark"/>
@@ -22,6 +25,47 @@
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
<result property="jobCount" column="job_count"/>
<collection property="jobList" column="id"
select="org.dromara.zsjy.mapper.ZsjyEnterpriseJobMapper.selectVoListByEnterpriseId"/>
</resultMap>
<!-- 使用JOIN子查询的方式,避免嵌套select的列传递问题 -->
<resultMap type="org.dromara.zsjy.domain.vo.ZsjyEnterpriseVo" id="ZsjyEnterpriseWithJobsResult">
<id property="id" column="id"/>
<result property="enterpriseName" column="enterprise_name"/>
<result property="industry" column="industry"/>
<result property="industryName" column="industry_name"/>
<result property="mainBusiness" column="main_business"/>
<result property="enterpriseScale" column="enterprise_scale"/>
<result property="enterpriseScaleName" column="enterprise_scale_name"/>
<result property="enterpriseIntro" column="enterprise_intro"/>
<result property="contactName" column="contact_name"/>
<result property="contactPhone" column="contact_phone"/>
<result property="officePhone" column="office_phone"/>
<result property="email" column="email"/>
<result property="website" column="website"/>
<result property="address" column="address"/>
<result property="status" column="status"/>
<result property="remark" column="remark"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
<result property="jobCount" column="job_count"/>
<!-- 使用嵌套结果映射而非嵌套select,避免列值传递问题 -->
<collection property="jobList" ofType="org.dromara.zsjy.domain.vo.ZsjyEnterpriseJobVo">
<id property="id" column="job_id"/>
<result property="enterpriseId" column="job_enterprise_id"/>
<result property="positionName" column="job_position_name"/>
<result property="requirements" column="job_requirements"/>
<result property="benefits" column="job_benefits"/>
<result property="status" column="job_status"/>
<result property="createBy" column="job_create_by"/>
<result property="createTime" column="job_create_time"/>
<result property="updateBy" column="job_update_by"/>
<result property="updateTime" column="job_update_time"/>
</collection>
</resultMap>
<sql id="selectZsjyEnterpriseVo">
@@ -32,19 +76,101 @@
e.main_business,
e.enterprise_scale,
d2.dict_label as enterprise_scale_name,
e.enterprise_intro,
e.contact_name,
e.contact_phone,
e.office_phone,
e.email,
e.website,
e.address,
e.status,
e.remark,
e.create_by,
e.create_time,
e.update_by,
e.update_time
e.update_time,
(select count(*) from zsjy_enterprise_job j
where j.enterprise_id = e.id and j.deleted = 0 and j.status = '0') as job_count
from zsjy_enterprise e
left join sys_dict_data d1 on e.industry = d1.dict_value and d1.dict_type = 'zsjy_enterprise_industry'
left join sys_dict_data d2 on e.enterprise_scale = d2.dict_value and d2.dict_type = 'zsjy_enterprise_scale'
</sql>
<sql id="selectZsjyEnterpriseVoWithJobs">
select e.id,
e.enterprise_name,
e.industry,
d1.dict_label as industry_name,
e.main_business,
e.enterprise_scale,
d2.dict_label as enterprise_scale_name,
e.enterprise_intro,
e.contact_name,
e.contact_phone,
e.office_phone,
e.email,
e.website,
e.address,
e.status,
e.remark,
e.create_by,
e.create_time,
e.update_by,
e.update_time,
(select count(*) from zsjy_enterprise_job j
where j.enterprise_id = e.id and j.deleted = 0 and j.status = '0') as job_count,
j.id as job_id,
j.enterprise_id as job_enterprise_id,
j.position_name as job_position_name,
j.requirements as job_requirements,
j.benefits as job_benefits,
j.status as job_status,
j.create_by as job_create_by,
j.create_time as job_create_time,
j.update_by as job_update_by,
j.update_time as job_update_time
from zsjy_enterprise e
left join zsjy_enterprise_job j on e.id = j.enterprise_id and j.deleted = 0 and j.status = '0'
left join sys_dict_data d1 on e.industry = d1.dict_value and d1.dict_type = 'zsjy_enterprise_industry'
left join sys_dict_data d2 on e.enterprise_scale = d2.dict_value and d2.dict_type = 'zsjy_enterprise_scale'
</sql>
<!-- 查询企业详情(JOIN方式含岗位列表) -->
<select id="selectVoWithJobsById" resultMap="ZsjyEnterpriseWithJobsResult">
<include refid="selectZsjyEnterpriseVoWithJobs"/>
where e.id = #{id}
</select>
<!-- 分页查询企业列表(包含岗位数量,通过子查询计算) -->
<select id="selectVoPageWithJobCount" resultMap="ZsjyEnterpriseResult">
<include refid="selectZsjyEnterpriseVo"/>
<where>
<if test="bo.enterpriseName != null and bo.enterpriseName != ''">
and e.enterprise_name like concat('%', #{bo.enterpriseName}, '%')
</if>
<if test="bo.industry != null and bo.industry != ''">
and e.industry = #{bo.industry}
</if>
<if test="bo.enterpriseScale != null and bo.enterpriseScale != ''">
and e.enterprise_scale = #{bo.enterpriseScale}
</if>
<if test="bo.status != null and bo.status != ''">
and e.status = #{bo.status}
</if>
<if test="bo.contactName != null and bo.contactName != ''">
and e.contact_name like concat('%', #{bo.contactName}, '%')
</if>
<if test="bo.contactPhone != null and bo.contactPhone != ''">
and e.contact_phone like concat('%', #{bo.contactPhone}, '%')
</if>
<if test="bo.email != null and bo.email != ''">
and e.email like concat('%', #{bo.email}, '%')
</if>
<if test="bo.address != null and bo.address != ''">
and e.address like concat('%', #{bo.address}, '%')
</if>
</where>
order by e.create_time desc
</select>
</mapper>
+2
View File
@@ -73,6 +73,8 @@
## 企业信息管理API
[企业信息管理API](./企业信息管理API.md) - 包含企业基本信息管理、招聘岗位管理(含子表CRUD)、招聘简章PDF生成(单个/批量)、Excel导入功能(支持企业+岗位一体化导入)。
**重大更新(2026-03-30)**:企业信息新增企业简介、办公电话、官网字段;整合岗位管理;新增简章PDF生成和导入功能。
[统计报表API](./统计报表API.md) - 包含各类统计数据查询接口。
+502 -397
View File
@@ -1,454 +1,559 @@
# 企业信息管理API接口文档
> 文档创建人:admin
> 创建日期:2023-07-05
> 文档状态:创建中
> 文档创建人:admin
> 创建日期:2023-07-05
> 更新日期:2026-03-30
> 文档状态:已完成
## 1. 概述
本文档描述了企业信息管理模块的API接口设计。企业信息管理模块包含企业基本信息管理、招聘岗位管理以及招聘海报生成功能。
本文档描述了企业信息管理模块的API接口设计。企业信息管理模块包含企业基本信息管理、招聘岗位管理以及招聘简章PDF生成功能。
**核心变更(2026-03-30)**:
- 企业信息新增「企业简介」「办公电话」「官网」字段
- 企业信息与岗位信息在新增/编辑时一体化处理
- 新增招聘简章PDF生成功能(单个/批量)
- 新增Excel导入功能(支持企业+岗位一次性导入)
- 废弃原招聘海报管理、招聘岗位管理菜单(功能整合至本模块)
### 1.1 基础信息
- 接口基础路径:`/zsjy/enterprise`
- 请求方式:`GET`、`POST`、`PUT`、`DELETE`
- 认证方式:Bearer Token
- 响应格式:统一响应格式
| 项目 | 说明 |
|------|------|
| 接口基础路径 | `/zsjy/enterprise` |
| 请求方式 | `GET`、`POST`、`PUT`、`DELETE` |
| 认证方式 | Bearer Token |
| 响应格式 | 统一响应格式 `R<T>`,列表分页用 `TableDataInfo<T>` |
### 1.2 数据结构
#### 1.2.1 企业信息 (EnterpriseVO)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Long | 企业ID |
| enterpriseName | String | 企业名称 |
| industry | String | 所属行业(字典值) |
| industryName | String | 所属行业名称(字典标签) |
| mainBusiness | String | 主营业务(兼容保留) |
| enterpriseScale | String | 企业规模(字典值) |
| enterpriseScaleName | String | 企业规模名称(字典标签) |
| enterpriseIntro | String | **新增** 企业简介(≤1500字符,用于招聘简章) |
| contactName | String | 联系人姓名 |
| contactPhone | String | 联系电话 |
| officePhone | String | **新增** 办公电话 |
| email | String | 电子邮箱 |
| website | String | **新增** 企业官网 |
| address | String | 企业地址 |
| status | String | 状态(0正常 1停用) |
| remark | String | 备注(兼容保留) |
| jobList | List\<EnterpriseJobVO\> | **新增** 岗位列表(用于招聘简章) |
| jobCount | Integer | **新增** 岗位数量(列表展示用) |
| createBy | Long | 创建者 |
| createTime | Date | 创建时间 |
| updateBy | Long | 更新者 |
| updateTime | Date | 更新时间 |
#### 1.2.2 招聘岗位信息 (EnterpriseJobVO)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Long | 岗位ID |
| enterpriseId | Long | 所属企业ID |
| positionName | String | 岗位名称 |
| requirements | String | **岗位要求**(用于招聘简章) |
| benefits | String | **待遇**(用于招聘简章) |
| status | String | 状态(0正常 1停用) |
| createBy | Long | 创建者 |
| createTime | Date | 创建时间 |
| updateBy | Long | 更新者 |
| updateTime | Date | 更新时间 |
#### 1.2.3 简章生成记录 (EnterpriseBrochureVO)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Long | 记录ID |
| enterpriseId | Long | 企业ID |
| enterpriseName | String | 企业名称(关联) |
| brochurePath | String | PDF文件存储路径 |
| fileName | String | 文件名称 |
| fileSize | Long | 文件大小(字节) |
| generateBy | Long | 生成人ID |
| generateByName | String | 生成人姓名 |
| generateTime | Date | 生成时间 |
| createTime | Date | 创建时间 |
#### 1.2.4 企业导入模板 (ZsjyEnterpriseImportVo)
导入模板共19列(H~AC为岗位信息,最多支持6个岗位):
| 列 | 字段名 | 必填 | 说明 |
|----|--------|------|------|
| A | 企业名称 | 是 | 唯一性校验 |
| B | 企业简介 | 否 | ≤1500字符 |
| C | 企业地址 | 是 | — |
| D | 联系人 | 是 | — |
| E | 联系电话 | 是 | 手机号格式 |
| F | 办公电话 | 否 | — |
| G | 电子邮箱 | 是 | 邮箱格式 |
| H | 招聘岗位1 | 否 | 岗位名称 |
| I | 要求1 | 否 | 岗位要求 |
| J | 待遇1 | 否 | 待遇信息 |
| K | 招聘岗位2 | 否 | — |
| L | 要求2 | 否 | — |
| M | 待遇2 | 否 | — |
| N | 招聘岗位3 | 否 | — |
| O | 要求3 | 否 | — |
| P | 待遇3 | 否 | — |
| Q | 招聘岗位4 | 否 | — |
| R | 要求4 | 否 | — |
| S | 待遇4 | 否 | — |
| T | 招聘岗位5 | 否 | — |
| U | 要求5 | 否 | — |
| V | 待遇5 | 否 | — |
| W | 招聘岗位6 | 否 | — |
| X | 要求6 | 否 | — |
| Y | 待遇6 | 否 | — |
---
## 2. 接口详细说明
### 2.1 查询企业信息列表
**接口地址**:`GET /zsjy/enterprise/list`
**权限标识**:`zsjy:enterprise:list`
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| enterpriseName | String | 否 | 企业名称(模糊查询) |
| industry | String | 否 | 所属行业(字典值) |
| enterpriseScale | String | 否 | 企业规模(字典值) |
| status | String | 否 | 状态(0正常 1停用) |
| contactName | String | 否 | 联系人(模糊查询) |
| contactPhone | String | 否 | 联系电话(模糊查询) |
| email | String | 否 | 邮箱(模糊查询) |
| address | String | 否 | 地址(模糊查询) |
| pageNum | Integer | 否 | 页码(默认1) |
| pageSize | Integer | 否 | 每页条数(默认10) |
**响应示例**:
```json
{
"id": "企业ID,long型",
"enterpriseName": "企业名称,字符串",
"industry": "所属行业,字典值",
"mainBusiness": "主营业务,字符串",
"enterpriseScale": "企业规模,字典值",
"contactName": "企业联系人姓名,字符串",
"contactPhone": "联系电话,字符串",
"email": "企业邮箱,字符串",
"address": "企业地址,字符串",
"status": "状态(0正常 1停用),字符",
"remark": "备注,字符串",
"createBy": "创建者,long型",
"createTime": "创建时间,日期时间",
"updateBy": "更新者,long型",
"updateTime": "更新时间,日期时间"
"code": 200,
"msg": "操作成功",
"total": 10,
"rows": [
{
"id": 1,
"enterpriseName": "杭州某某科技有限公司",
"industry": "01",
"industryName": "互联网",
"enterpriseScale": "01",
"enterpriseScaleName": "小型企业",
"enterpriseIntro": "公司成立于2010年,专注于软件开发...",
"contactName": "张三",
"contactPhone": "13800138000",
"officePhone": "0571-88888888",
"email": "hr@example.com",
"website": "https://www.example.com",
"address": "浙江省杭州市西湖区xxx路xxx号",
"status": "0",
"jobCount": 3,
"createTime": "2026-03-30 10:00:00"
}
]
}
```
#### 1.2.2 招聘岗位信息 (JobPositionVO)
---
### 2.2 获取企业详细信息
**接口地址**:`GET /zsjy/enterprise/{id}`
**权限标识**:`zsjy:enterprise:query`
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| id | Long | 是 | 企业ID |
**响应示例**:
```json
{
"id": "岗位ID,long型",
"enterpriseId": "所属企业ID,long型",
"enterpriseName": "所属企业名称,字符串",
"positionName": "岗位名称,字符串",
"workAddress": "工作地址,字符串",
"workTime": "工作时间,字符串",
"salary": "薪资,字符串",
"requiredSkills": "所需技能,字符串",
"startDate": "有效期开始日期,日期时间",
"endDate": "有效期结束日期,日期时间",
"isPermanent": "是否永久有效(0否 1是),整型",
"status": "状态(0正常 1停用),字符",
"remark": "备注,字符串",
"createBy": "创建者,long型",
"createTime": "创建时间,日期时间",
"updateBy": "更新者,long型",
"updateTime": "更新时间,日期时间"
}
```
#### 1.2.3 招聘海报 (RecruitmentPosterVO)
```json
{
"id": "海报ID,long型",
"enterpriseId": "企业ID,long型",
"enterpriseName": "企业名称,字符串",
"positionId": "岗位ID,long型",
"positionName": "岗位名称,字符串",
"posterPath": "海报文件路径,字符串",
"posterType": "海报类型(1企业海报 2岗位海报),字符",
"createBy": "创建者,long型",
"createTime": "创建时间,日期时间",
"updateBy": "更新者,long型",
"updateTime": "更新时间,日期时间"
}
```
## 2. 接口定义
### 2.1 企业信息管理接口
#### 2.1.1 获取企业列表
- **接口URL**:`/zsjy/enterprise/list`
- **请求方式**:`GET`
- **接口权限**:`zsjy:enterprise:list`
- **请求参数**:
| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | 备注 |
|----------------|----------|----------|----------|----------|-------------------------|
| pageNum | 页码 | query | 是 | int | 默认为1 |
| pageSize | 每页数量 | query | 是 | int | 默认为10 |
| enterpriseName | 企业名称 | query | 否 | string | 模糊匹配 |
| industry | 所属行业 | query | 否 | string | 精确匹配,来自数据字典 |
| status | 状态 | query | 否 | string | 0正常 1停用 |
- **响应参数**:
```json
{
"total": "总记录数",
"rows": "企业列表数据,数组类型,每个元素为EnterpriseVO对象",
"code": "状态码,200表示成功",
"msg": "消息提示"
}
```
#### 2.1.2 获取企业详情
- **接口URL**:`/zsjy/enterprise/{id}`
- **请求方式**:`GET`
- **接口权限**:`zsjy:enterprise:query`
- **请求参数**:
| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | 备注 |
|----------|----------|----------|----------|----------|------------|
| id | 企业ID | path | 是 | long | 路径变量 |
- **响应参数**:
```json
{
"code": "状态码,200表示成功",
"data": "企业详情数据,EnterpriseVO对象",
"msg": "消息提示"
}
```
#### 2.1.3 新增企业信息
- **接口URL**:`/zsjy/enterprise`
- **请求方式**:`POST`
- **接口权限**:`zsjy:enterprise:add`
- **请求参数**:EnterpriseVO对象(不含id、createBy、createTime、updateBy、updateTime字段)
- **响应参数**:
```json
{
"code": "状态码,200表示成功",
"msg": "消息提示"
}
```
#### 2.1.4 修改企业信息
- **接口URL**:`/zsjy/enterprise`
- **请求方式**:`PUT`
- **接口权限**:`zsjy:enterprise:edit`
- **请求参数**:EnterpriseVO对象(必须包含id字段)
- **响应参数**:
```json
{
"code": "状态码,200表示成功",
"msg": "消息提示"
}
```
#### 2.1.5 删除企业信息
- **接口URL**:`/zsjy/enterprise/{ids}`
- **请求方式**:`DELETE`
- **接口权限**:`zsjy:enterprise:remove`
- **请求参数**:
| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | 备注 |
|----------|-----------------------------|----------|----------|----------|-----------------------|
| ids | 企业ID,多个以逗号分隔 | path | 是 | string | 例如:1 或 1,2,3 |
- **响应参数**:
```json
{
"code": "状态码,200表示成功",
"msg": "消息提示"
}
```
### 2.2 招聘岗位管理接口
#### 2.2.1 获取岗位列表
- **接口URL**:`/zsjy/position/list`
- **请求方式**:`GET`
- **接口权限**:`zsjy:position:list`
- **请求参数**:
| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | 备注 |
|--------------|--------------|----------|----------|----------|-------------------------|
| pageNum | 页码 | query | 是 | int | 默认为1 |
| pageSize | 每页数量 | query | 是 | int | 默认为10 |
| enterpriseId | 企业ID | query | 否 | long | 精确匹配 |
| positionName | 岗位名称 | query | 否 | string | 模糊匹配 |
| status | 状态 | query | 否 | string | 0正常 1停用 |
- **响应参数**:
```json
{
"total": "总记录数",
"rows": "岗位列表数据,数组类型,每个元素为JobPositionVO对象",
"code": "状态码,200表示成功",
"msg": "消息提示"
}
```
#### 2.2.2 获取岗位详情
- **接口URL**:`/zsjy/position/{id}`
- **请求方式**:`GET`
- **接口权限**:`zsjy:position:query`
- **请求参数**:
| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | 备注 |
|----------|----------|----------|----------|----------|------------|
| id | 岗位ID | path | 是 | long | 路径变量 |
- **响应参数**:
```json
{
"code": "状态码,200表示成功",
"data": "岗位详情数据,JobPositionVO对象",
"msg": "消息提示"
}
```
#### 2.2.3 新增招聘岗位
- **接口URL**:`/zsjy/position`
- **请求方式**:`POST`
- **接口权限**:`zsjy:position:add`
- **请求参数**:JobPositionVO对象(不含id、createBy、createTime、updateBy、updateTime字段)
- **响应参数**:
```json
{
"code": "状态码,200表示成功",
"msg": "消息提示"
}
```
#### 2.2.4 修改招聘岗位
- **接口URL**:`/zsjy/position`
- **请求方式**:`PUT`
- **接口权限**:`zsjy:position:edit`
- **请求参数**:JobPositionVO对象(必须包含id字段)
- **响应参数**:
```json
{
"code": "状态码,200表示成功",
"msg": "消息提示"
}
```
#### 2.2.5 删除招聘岗位
- **接口URL**:`/zsjy/position/{ids}`
- **请求方式**:`DELETE`
- **接口权限**:`zsjy:position:remove`
- **请求参数**:
| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | 备注 |
|----------|-----------------------------|----------|----------|----------|-----------------------|
| ids | 岗位ID,多个以逗号分隔 | path | 是 | string | 例如:1 或 1,2,3 |
- **响应参数**:
```json
{
"code": "状态码,200表示成功",
"msg": "消息提示"
}
```
#### 2.2.6 获取企业下的岗位列表
- **接口URL**:`/zsjy/position/enterprise/{enterpriseId}`
- **请求方式**:`GET`
- **接口权限**:`zsjy:position:list`
- **请求参数**:
| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | 备注 |
|--------------|--------------|----------|----------|----------|-------------------------|
| enterpriseId | 企业ID | path | 是 | long | 路径变量 |
- **响应参数**:
```json
{
"code": "状态码,200表示成功",
"data": "岗位列表数据,数组类型,每个元素为JobPositionVO对象",
"msg": "消息提示"
}
```
### 2.3 招聘海报管理接口
#### 2.3.1 生成企业招聘海报
- **接口URL**:`/zsjy/poster/enterprise/{enterpriseId}`
- **请求方式**:`POST`
- **接口权限**:`zsjy:poster:generate`
- **请求参数**:
| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | 备注 |
|--------------|----------|----------|----------|----------|------------|
| enterpriseId | 企业ID | path | 是 | long | 路径变量 |
- **响应参数**:
```json
{
"code": "状态码,200表示成功",
"code": 200,
"msg": "操作成功",
"data": {
"posterPath": "生成的海报文件路径",
"posterId": "海报记录ID"
},
"msg": "消息提示"
"id": 1,
"enterpriseName": "杭州某某科技有限公司",
"industry": "01",
"enterpriseIntro": "公司成立于2010年...",
"contactName": "张三",
"contactPhone": "13800138000",
"officePhone": "0571-88888888",
"email": "hr@example.com",
"website": "https://www.example.com",
"address": "浙江省杭州市xxx路xxx号",
"status": "0",
"jobList": [
{
"id": 1,
"enterpriseId": 1,
"positionName": "前端开发工程师",
"requirements": "1. 熟练掌握Vue3\n2. 熟悉TypeScript",
"benefits": "薪资范围:8000-15000元/月\n提供五险一金",
"status": "0"
},
{
"id": 2,
"enterpriseId": 1,
"positionName": "后端开发工程师",
"requirements": "1. 熟练掌握Java\n2. 熟悉Spring Boot",
"benefits": "薪资范围:10000-20000元/月",
"status": "0"
}
]
}
}
```
#### 2.3.2 生成岗位招聘海报
---
- **接口URL**:`/zsjy/poster/position/{positionId}`
- **请求方式**:`POST`
- **接口权限**:`zsjy:poster:generate`
- **请求参数**:
### 2.3 新增企业信息
| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | 备注 |
|------------|----------|----------|----------|----------|------------|
| positionId | 岗位ID | path | 是 | long | 路径变量 |
**接口地址**:`POST /zsjy/enterprise`
- **响应参数**:
**权限标识**:`zsjy:enterprise:add`
**请求体**:
```json
{
"code": "状态码,200表示成功",
"enterpriseName": "杭州某某科技有限公司",
"industry": "01",
"enterpriseIntro": "公司成立于2010年,专注于软件开发...",
"contactName": "张三",
"contactPhone": "13800138000",
"officePhone": "0571-88888888",
"email": "hr@example.com",
"website": "https://www.example.com",
"address": "浙江省杭州市西湖区xxx路xxx号",
"enterpriseScale": "01",
"status": "0",
"jobList": [
{
"positionName": "前端开发工程师",
"requirements": "1. 熟练掌握Vue3\n2. 熟悉TypeScript",
"benefits": "薪资范围:8000-15000元/月\n提供五险一金",
"status": "0"
},
{
"positionName": "后端开发工程师",
"requirements": "1. 熟练掌握Java\n2. 熟悉Spring Boot",
"benefits": "薪资范围:10000-20000元/月",
"status": "0"
}
]
}
```
**响应示例**:
```json
{
"code": 200,
"msg": "操作成功"
}
```
---
### 2.4 修改企业信息
**接口地址**:`PUT /zsjy/enterprise`
**权限标识**:`zsjy:enterprise:edit`
**说明**:编辑企业信息时,会先删除该企业下所有旧岗位,再批量插入新岗位列表。
**请求体**:同 2.3 新增
---
### 2.5 删除企业信息
**接口地址**:`DELETE /zsjy/enterprise/{ids}`
**权限标识**:`zsjy:enterprise:remove`
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| ids | Long[] | 是 | 企业ID数组 |
**说明**:删除企业时会同时删除关联的岗位信息。
---
### 2.6 导出企业信息
**接口地址**:`POST /zsjy/enterprise/export`
**权限标识**:`zsjy:enterprise:export`
**请求参数**:同列表查询参数(用于条件导出)
**响应**:Excel文件流下载
---
### 2.7 下载导入模板
**接口地址**:`GET /zsjy/enterprise/importTemplate`
**权限标识**:无需权限
**响应**:Excel模板文件流下载,文件名:`enterprise_import_template.xlsx`
---
### 2.8 导入企业信息
**接口地址**:`POST /zsjy/enterprise/importData`
**权限标识**:`zsjy:enterprise:import`
**请求方式**:`multipart/form-data`
**表单参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| file | File | 是 | Excel文件(.xlsx/.xls) |
| updateSupport | Boolean | 否 | 是否支持更新(同名覆盖,默认false) |
**响应示例**:
```json
{
"code": 200,
"msg": "恭喜您,数据已全部导入成功!共 5 条,数据如下:<br/>1、企业 xxx 导入成功"
}
```
**导入规则**:
- 企业名称唯一,重复时根据 `updateSupport` 决定是跳过还是覆盖
- 岗位按模板列顺序解析,岗位名称非空则创建一条岗位记录
---
### 2.9 生成招聘简章PDF(单个)
**接口地址**:`POST /zsjy/enterprise/brochure/{enterpriseId}`
**权限标识**:`zsjy:enterprise:brochure`
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| enterpriseId | Long | 是 | 企业ID |
**响应示例**:
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"posterPath": "生成的海报文件路径",
"posterId": "海报记录ID"
},
"msg": "消息提示"
"id": 1,
"enterpriseId": 1,
"enterpriseName": "杭州某某科技有限公司",
"brochurePath": "/app/uploadPath/enterprise-brochure/2026/03/30/xxx.pdf",
"fileName": "杭州某某科技有限公司_1743300000000.pdf",
"fileSize": 102400,
"generateBy": 1,
"generateByName": "admin",
"generateTime": "2026-03-30 10:00:00"
}
}
```
#### 2.3.3 获取海报列表
- **接口URL**:`/zsjy/poster/list`
- **请求方式**:`GET`
- **接口权限**:`zsjy:poster:list`
- **请求参数**:
| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | 备注 |
|--------------|--------------|----------|----------|----------|-----------------------------|
| pageNum | 页码 | query | 是 | int | 默认为1 |
| pageSize | 每页数量 | query | 是 | int | 默认为10 |
| enterpriseId | 企业ID | query | 否 | long | 精确匹配 |
| posterType | 海报类型 | query | 否 | string | 1企业海报 2岗位海报 |
- **响应参数**:
**错误示例**(无岗位时):
```json
{
"total": "总记录数",
"rows": "海报列表数据,数组类型,每个元素为RecruitmentPosterVO对象",
"code": "状态码,200表示成功",
"msg": "消息提示"
"code": 500,
"msg": "该企业下没有招聘岗位,无法生成简章"
}
```
#### 2.3.4 导出海报为图片
---
- **接口URL**:`/zsjy/poster/export/image/{id}`
- **请求方式**:`GET`
- **接口权限**:`zsjy:poster:export`
- **请求参数**:
### 2.10 批量生成招聘简章PDF
| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | 备注 |
|----------|----------|----------|----------|----------|------------|
| id | 海报ID | path | 是 | long | 路径变量 |
**接口地址**:`POST /zsjy/enterprise/brochure/batch/{enterpriseIds}`
- **响应参数**:图片文件流
**权限标识**:`zsjy:enterprise:brochure`
#### 2.3.5 导出海报为PDF
**路径参数**:
- **接口URL**:`/zsjy/poster/export/pdf/{id}`
- **请求方式**:`GET`
- **接口权限**:`zsjy:poster:export`
- **请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| enterpriseIds | String | 是 | 企业ID列表,逗号分隔,如:`1,2,3` |
| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | 备注 |
|----------|----------|----------|----------|----------|------------|
| id | 海报ID | path | 是 | long | 路径变量 |
**响应示例**:
- **响应参数**:PDF文件流
```json
{
"code": 200,
"msg": "操作成功",
"data": [
{
"id": 1,
"enterpriseId": 1,
"enterpriseName": "杭州某某科技有限公司",
"fileName": "杭州某某科技有限公司_xxx.pdf",
"generateTime": "2026-03-30 10:00:00"
}
]
}
```
## 3. 数据字典
---
### 3.1 企业规模 (zsjy_enterprise_scale)
### 2.11 获取最新简章信息
| 字典值 | 字典名称 | 备注 |
|--------|------------|------------|
| 1-49 | 1-49人 | 小型企业 |
| 50-99 | 50-99人 | |
| 100-499| 100-499人 | 中型企业 |
| 500-999| 500-999人 | |
| 1000-4999| 1000-4999人 | 大型企业 |
| 5000+ | 5000人以上 | 特大型企业 |
**接口地址**:`GET /zsjy/enterprise/brochure/{enterpriseId}`
### 3.2 企业行业 (zsjy_enterprise_industry)
**权限标识**:`zsjy:enterprise:query`
| 字典值 | 字典名称 |
|--------|---------------------|
| IT | 互联网/IT/电子/通信 |
| FINANCE| 金融/银行/保险 |
| EDUCATION | 教育/培训/科研 |
| ESTATE | 房地产/建筑 |
| MEDICAL| 医疗/健康 |
| TRADE | 贸易/零售/物流 |
| MANUFACTURING | 制造业 |
| MEDIA | 文化/媒体/娱乐 |
| ENERGY | 能源/环保 |
| GOVERNMENT | 政府/非营利组织 |
| OTHER | 其他行业 |
**路径参数**:
### 3.3 岗位有效期类型 (zsjy_position_validity_type)
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| enterpriseId | Long | 是 | 企业ID |
| 字典值 | 字典名称 | 备注 |
|--------|----------|----------------|
| 1 | 永久有效 | 岗位永久有效 |
| 0 | 日期区间 | 设置开始和结束日期|
**响应示例**:同 2.9
## 4. 错误码
---
| 错误码 | 描述 | 备注 |
|--------|------------------------|----------------------------|
| 200 | 操作成功 | |
| 500 | 系统内部错误 | |
| 401 | 未授权,请重新登录 | |
| 403 | 权限不足,禁止访问 | |
| 50001 | 企业信息不存在 | 查询或操作不存在的企业时 |
| 50002 | 岗位信息不存在 | 查询或操作不存在的岗位时 |
| 50003 | 海报生成失败 | 生成海报时发生错误 |
| 50004 | 导出失败 | 导出海报为图片或PDF时失败 |
| 50005 | 该企业下存在岗位,不能删除 | 删除有关联岗位的企业时 |
### 2.12 下载招聘简章PDF(通过企业ID)
**接口地址**:`GET /zsjy/enterprise/brochure/download/{enterpriseId}`
**权限标识**:`zsjy:enterprise:brochure`
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| enterpriseId | Long | 是 | 企业ID |
**响应**:PDF文件流,Content-Type: `application/pdf`
**文件名**:`招聘简章_{企业名称}.pdf`
---
### 2.13 下载招聘简章PDF(通过记录ID)
**接口地址**:`GET /zsjy/enterprise/brochure/downloadById/{id}`
**权限标识**:`zsjy:enterprise:brochure`
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| id | Long | 是 | 简章记录ID |
**响应**:PDF文件流
---
## 3. 招聘简章PDF版式说明
生成的招聘简章PDF每家企业一份,结构如下:
```
┌─────────────────────────────────────────┐
│ 企业名称(大标题) │ ← 蓝色背景标题区
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ 企业简介 │ ← 企业简介区块
│ 公司成立于2010年,专注于软件开发... │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ 联系人 | 联系电话 | 办公电话 | 电子邮箱 │ ← 联系信息表格
│ 企业地址:xxx路xxx号 │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ 招聘岗位 │ ← 岗位标题
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ 岗位1名称 │ ← 岗位1
│ 岗位要求:... │
│ 待遇:... │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ 岗位2名称 │ ← 岗位2
│ 岗位要求:... │
│ 待遇:... │
└─────────────────────────────────────────┘
... 更多岗位 ...
┌─────────────────────────────────────────┐
│ 联系人: 张三 | 电话: 13800138000 │ ← 页脚联系信息
│ 邮箱: hr@example.com | 地址: xxx │
└─────────────────────────────────────────┘
```
---
## 4. 权限标识清单
| 权限标识 | 说明 |
|---------|------|
| zsjy:enterprise:list | 列表查询 |
| zsjy:enterprise:query | 查看详情 |
| zsjy:enterprise:add | 新增 |
| zsjy:enterprise:edit | 修改(含岗位增删) |
| zsjy:enterprise:remove | 删除 |
| zsjy:enterprise:export | 导出 |
| zsjy:enterprise:import | 导入 |
| zsjy:enterprise:brochure | 生成简章 / 下载 |
---
## 5. 数据库表结构
| 表名 | 说明 |
|------|------|
| zsjy_enterprise | 企业信息表(改造:新增enterprise_intro、office_phone、website字段) |
| zsjy_enterprise_job | 企业招聘岗位表(新建) |
| zsjy_enterprise_brochure | 简章生成记录表(新建) |
---
## 6. 变更记录
| 日期 | 版本 | 变更内容 |
|------|------|----------|
| 2023-07-05 | v1.0 | 初始版本(企业基本信息CRUD) |
| 2026-03-30 | v2.0 | 重大改造:新增岗位子表、招聘简章PDF生成、Excel导入功能 |
+102 -11
View File
@@ -1,12 +1,12 @@
/**
* 企业信息管理 API
* @author Cursor AI
* @date 2025-01-25
* @date 2026-03-30
*/
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { EnterpriseVO, EnterpriseForm, EnterpriseQuery } from './types';
import { EnterpriseVO, EnterpriseForm, EnterpriseQuery, EnterpriseJobForm, EnterpriseBrochureVO } from './types';
// TableDataInfo 接口定义
export interface TableDataInfo<T> {
@@ -19,7 +19,6 @@ export interface TableDataInfo<T> {
/**
* 查询企业信息列表
* @param query 查询参数
* @returns Promise<TableDataInfo<EnterpriseVO>>
*/
export function listEnterprise(query?: EnterpriseQuery): AxiosPromise<TableDataInfo<EnterpriseVO>> {
return request({
@@ -32,9 +31,8 @@ export function listEnterprise(query?: EnterpriseQuery): AxiosPromise<TableDataI
/**
* 查询企业信息详细
* @param id 企业主键
* @returns Promise<EnterpriseVO>
*/
export function getEnterprise(id: string | number): AxiosPromise<EnterpriseVO> {
export function getEnterprise(id: string | number): AxiosPromise<any> {
return request({
url: `/zsjy/enterprise/${id}`,
method: 'get'
@@ -42,9 +40,8 @@ export function getEnterprise(id: string | number): AxiosPromise<EnterpriseVO> {
}
/**
* 新增企业信息
* 新增企业信息(含岗位)
* @param data 企业信息
* @returns Promise<any>
*/
export function addEnterprise(data: EnterpriseForm): AxiosPromise<any> {
return request({
@@ -55,9 +52,8 @@ export function addEnterprise(data: EnterpriseForm): AxiosPromise<any> {
}
/**
* 修改企业信息
* 修改企业信息(含岗位)
* @param data 企业信息
* @returns Promise<any>
*/
export function updateEnterprise(data: EnterpriseForm): AxiosPromise<any> {
return request({
@@ -70,7 +66,6 @@ export function updateEnterprise(data: EnterpriseForm): AxiosPromise<any> {
/**
* 删除企业信息
* @param id 企业主键
* @returns Promise<any>
*/
export function delEnterprise(id: string | number | Array<string | number>): AxiosPromise<any> {
return request({
@@ -82,7 +77,6 @@ export function delEnterprise(id: string | number | Array<string | number>): Axi
/**
* 导出企业信息
* @param query 查询参数
* @returns Promise<any>
*/
export function exportEnterprise(query: EnterpriseQuery): AxiosPromise<any> {
return request({
@@ -92,3 +86,100 @@ export function exportEnterprise(query: EnterpriseQuery): AxiosPromise<any> {
responseType: 'blob'
});
}
/**
* 获取企业下拉选项
*/
export function listEnterpriseOptions(): AxiosPromise<any> {
return request({
url: '/zsjy/enterprise/options',
method: 'get'
});
}
/**
* 下载导入模板
*/
export function getImportTemplate(): AxiosPromise<any> {
return request({
url: '/zsjy/enterprise/importTemplate',
method: 'get',
responseType: 'blob'
});
}
/**
* 导入企业信息
* @param file Excel文件
* @param updateSupport 是否支持更新(同名覆盖)
*/
export function importEnterprise(file: File, updateSupport: boolean = false): AxiosPromise<any> {
const formData = new FormData();
formData.append('file', file);
formData.append('updateSupport', String(updateSupport));
return request({
url: '/zsjy/enterprise/importData',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
});
}
/**
* 生成招聘简章PDF(单个)
* @param enterpriseId 企业ID
*/
export function generateBrochure(enterpriseId: number | string): AxiosPromise<EnterpriseBrochureVO> {
return request({
url: `/zsjy/enterprise/brochure/${enterpriseId}`,
method: 'post'
});
}
/**
* 批量生成招聘简章PDF
* @param enterpriseIds 企业ID列表(逗号分隔)
*/
export function generateBrochureBatch(enterpriseIds: string): AxiosPromise<any> {
return request({
url: `/zsjy/enterprise/brochure/batch/${enterpriseIds}`,
method: 'post'
});
}
/**
* 获取最新简章信息
* @param enterpriseId 企业ID
*/
export function getBrochure(enterpriseId: number | string): AxiosPromise<any> {
return request({
url: `/zsjy/enterprise/brochure/${enterpriseId}`,
method: 'get'
});
}
/**
* 下载招聘简章PDF(通过企业ID)
* @param enterpriseId 企业ID
*/
export function downloadBrochure(enterpriseId: number | string): AxiosPromise<any> {
return request({
url: `/zsjy/enterprise/brochure/download/${enterpriseId}`,
method: 'get',
responseType: 'blob'
});
}
/**
* 下载招聘简章PDF(通过简章记录ID)
* @param id 简章记录ID
*/
export function downloadBrochureById(id: number | string): AxiosPromise<any> {
return request({
url: `/zsjy/enterprise/brochure/downloadById/${id}`,
method: 'get',
responseType: 'blob'
});
}
+46 -1737
View File
File diff suppressed because it is too large. Load diff
+10 -5
View File
@@ -248,17 +248,22 @@ service.interceptors.response.use(
return Promise.reject(error);
}
);
// 通用下载方法
export function download(url: string, params: any, fileName: string) {
// 通用下载方法(支持GET和POST)
export function download(url: string, params: any, fileName: string, method: string = 'post') {
downloadLoadingInstance = ElLoading.service({ text: '正在下载数据,请稍候', background: 'rgba(0, 0, 0, 0.7)' });
const isGet = method.toLowerCase() === 'get';
// prettier-ignore
return service.post(url, params, {
transformRequest: [
return service({
url: url,
method: method,
params: isGet ? params : undefined,
data: isGet ? undefined : params,
transformRequest: isGet ? undefined : [
(params: any) => {
return tansParams(params);
}
],
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
headers: isGet ? undefined : { 'Content-Type': 'application/x-www-form-urlencoded' },
responseType: 'blob',
timeout: 1200000 // 导出接口单独设置20分钟超时
}).then(async (resp: any) => {
+657 -97
View File
@@ -63,6 +63,12 @@
<el-col :span="1.5">
<el-button icon="Download" @click="handleExport" v-hasPermi="['zsjy:enterprise:export']">导出</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="Upload" @click="showImportDialog = true" v-hasPermi="['zsjy:enterprise:import']">导入</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="Document" :disabled="multiple" type="success" @click="handleGenerateBrochureBatch" v-hasPermi="['zsjy:enterprise:brochure']">批量生成简章</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
@@ -83,15 +89,18 @@
</el-table-column>
<el-table-column label="联系人" align="center" prop="contactName" width="100" show-overflow-tooltip />
<el-table-column label="联系电话" align="center" prop="contactPhone" width="120" show-overflow-tooltip />
<el-table-column label="企业邮箱" align="center" prop="email" min-width="150" show-overflow-tooltip />
<el-table-column label="企业地址" align="center" prop="address" min-width="200" show-overflow-tooltip />
<el-table-column label="岗位数量" align="center" prop="jobCount" width="100">
<template #default="scope">
<el-tag type="primary" effect="plain">{{ scope.row.jobCount || 0 }}</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" width="80">
<template #default="scope">
<dict-tag :options="sys_normal_disable" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="160" show-overflow-tooltip />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="180" fixed="right">
<el-table-column label="创建时间" align="center" prop="createTime" width="160" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="320" fixed="right">
<template #default="scope">
<el-tooltip content="查看" placement="top">
<el-button
@@ -115,6 +124,17 @@
v-hasPermi="['zsjy:enterprise:edit']"
></el-button>
</el-tooltip>
<el-tooltip content="生成简章" placement="top">
<el-button
link
type="success"
icon="Document"
size="default"
style="font-size: 16px; margin-right: 8px"
@click="handleGenerateBrochure(scope.row)"
v-hasPermi="['zsjy:enterprise:brochure']"
></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button
link
@@ -137,52 +157,74 @@
</el-card>
<!-- 添加或修改企业信息对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="1000px" append-to-body>
<el-dialog :title="dialog.title" v-model="dialog.visible" width="1100px" append-to-body class="enterprise-dialog">
<el-form ref="enterpriseFormRef" :model="form" :rules="rules" label-width="120px">
<el-divider content-position="left">企业基本信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="企业名称" prop="enterpriseName">
<el-input v-model="form.enterpriseName" placeholder="请输入企业名称" />
<el-input v-model="form.enterpriseName" placeholder="请输入企业名称" maxlength="200" show-word-limit />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所属行业" prop="industry">
<el-select v-model="form.industry" placeholder="请选择所属行业" style="width: 100%">
<el-select v-model="form.industry" placeholder="请选择所属行业" clearable style="width: 100%">
<el-option v-for="dict in zsjy_enterprise_industry" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="联系人姓名" prop="contactName">
<el-input v-model="form.contactName" placeholder="请输入联系人姓名" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系电话" prop="contactPhone">
<el-input v-model="form.contactPhone" placeholder="请输入联系电话" maxlength="20" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="办公电话" prop="officePhone">
<el-input v-model="form.officePhone" placeholder="请输入办公电话" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="电子邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入电子邮箱" maxlength="100" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="企业地址" prop="address">
<el-input v-model="form.address" placeholder="请输入企业地址" maxlength="255" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="企业简介" prop="enterpriseIntro">
<el-input v-model="form.enterpriseIntro" type="textarea" placeholder="请输入企业简介(用于招聘简章展示)" :rows="4" maxlength="1500" show-word-limit />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="企业规模" prop="enterpriseScale">
<el-select v-model="form.enterpriseScale" placeholder="请选择企业规模" style="width: 100%">
<el-select v-model="form.enterpriseScale" placeholder="请选择企业规模" clearable style="width: 100%">
<el-option v-for="dict in zsjy_enterprise_scale" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系人姓名" prop="contactName">
<el-input v-model="form.contactName" placeholder="请输入联系人姓名" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="联系电话" prop="contactPhone">
<el-input v-model="form.contactPhone" placeholder="请输入联系电话" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="企业邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入企业邮箱" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
@@ -192,30 +234,42 @@
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="企业地址" prop="address">
<el-input v-model="form.address" placeholder="请输入企业地址" />
</el-form-item>
</el-col>
</el-row>
<!-- 岗位信息区域 -->
<el-divider content-position="left">
招聘岗位信息
<el-button type="primary" link icon="Plus" style="margin-left: 8px" @click="handleAddJob">添加岗位</el-button>
</el-divider>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="主营业务" prop="mainBusiness">
<el-input v-model="form.mainBusiness" type="textarea" placeholder="请输入主营业务" :autosize="{ minRows: 3, maxRows: 5 }" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注信息" :autosize="{ minRows: 2, maxRows: 4 }" />
</el-form-item>
</el-col>
</el-row>
<div class="job-table-container">
<el-table :data="form.jobList" border style="width: 100%" size="small">
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="岗位名称" min-width="150">
<template #default="scope">
<el-input v-model="scope.row.positionName" placeholder="请输入岗位名称" maxlength="100" />
</template>
</el-table-column>
<el-table-column label="岗位要求" min-width="200">
<template #default="scope">
<el-input v-model="scope.row.requirements" type="textarea" placeholder="请输入岗位要求" :rows="2" />
</template>
</el-table-column>
<el-table-column label="待遇" min-width="200">
<template #default="scope">
<el-input v-model="scope.row.benefits" type="textarea" placeholder="请输入待遇信息" :rows="2" />
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="scope">
<el-button link type="danger" icon="Delete" @click="handleRemoveJob(scope.$index)" :disabled="form.jobList.length <= 1">删除</el-button>
</template>
</el-table-column>
</el-table>
<div v-if="!form.jobList || form.jobList.length === 0" class="empty-job-tip">
暂无岗位信息,点击上方「添加岗位」按钮新增
</div>
</div>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
@@ -236,27 +290,135 @@
</el-descriptions-item>
<el-descriptions-item label="联系人姓名">{{ viewForm.contactName }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ viewForm.contactPhone }}</el-descriptions-item>
<el-descriptions-item label="企业邮箱">{{ viewForm.email }}</el-descriptions-item>
<el-descriptions-item label="办公电话">{{ viewForm.officePhone || '-' }}</el-descriptions-item>
<el-descriptions-item label="电子邮箱">{{ viewForm.email }}</el-descriptions-item>
<el-descriptions-item label="企业地址" span="2">{{ viewForm.address }}</el-descriptions-item>
<el-descriptions-item label="主营业务" span="2">{{ viewForm.mainBusiness }}</el-descriptions-item>
<el-descriptions-item label="企业简介" span="2">{{ viewForm.enterpriseIntro || '-' }}</el-descriptions-item>
<el-descriptions-item label="状态">
<dict-tag :options="sys_normal_disable" :value="viewForm.status" />
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ viewForm.createTime }}</el-descriptions-item>
<el-descriptions-item label="备注" span="2">{{ viewForm.remark || '-' }}</el-descriptions-item>
</el-descriptions>
<!-- 岗位信息展示 -->
<el-divider content-position="left">招聘岗位信息</el-divider>
<el-table v-if="viewForm.jobList && viewForm.jobList.length > 0" :data="viewForm.jobList" border size="small">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="positionName" label="岗位名称" min-width="150" />
<el-table-column prop="requirements" label="岗位要求" min-width="200" show-overflow-tooltip />
<el-table-column prop="benefits" label="待遇" min-width="200" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="scope">
<dict-tag :options="sys_normal_disable" :value="scope.row.status" />
</template>
</el-table-column>
</el-table>
<el-empty v-else description="暂无岗位信息" :image-size="60" />
<template #footer>
<div class="dialog-footer">
<el-button type="success" icon="Document" @click="handleViewGenerateBrochure(viewForm)" v-hasPermi="['zsjy:enterprise:brochure']">生成简章</el-button>
<el-button @click="viewDialog.visible = false">关 闭</el-button>
</div>
</template>
</el-dialog>
<!-- 导入对话框 -->
<el-dialog title="导入企业信息" v-model="showImportDialog" width="500px" append-to-body>
<el-upload
ref="uploadRef"
class="upload-demo"
drag
:limit="1"
accept=".xlsx, .xls"
:auto-upload="false"
:headers="uploadHeaders"
:action="importUrl"
:disabled="importLoading"
:on-progress="handleImportProgress"
:on-success="handleImportSuccess"
:on-error="handleImportError"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将Excel文件拖到此处,或<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip">
<el-checkbox v-model="importUpdateSupport" />是否更新已存在的企业数据
<br />
仅支持 .xlsx 和 .xls 格式,<el-link type="primary" :underline="false" @click="handleDownloadTemplate">下载模板</el-link>
</div>
</template>
</el-upload>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" :loading="importLoading" @click="submitImport">确 定</el-button>
<el-button @click="showImportDialog = false">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 简章生成/预览对话框 -->
<el-dialog
title="招聘简章"
v-model="brochureDialog.visible"
width="900px"
:close-on-click-modal="false"
append-to-body
destroy-on-close
>
<div class="brochure-actions">
<el-button type="success" icon="Download" @click="handleDownloadBrochure" :loading="brochureLoading">下载PDF</el-button>
<el-button icon="Refresh" @click="handleReGenerateBrochure" :loading="brochureLoading">重新生成</el-button>
</div>
<div v-if="brochureDialog.brochureInfo" class="brochure-info">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="企业名称">{{ brochureDialog.brochureInfo.enterpriseName }}</el-descriptions-item>
<el-descriptions-item label="生成人">{{ brochureDialog.brochureInfo.generateByName }}</el-descriptions-item>
<el-descriptions-item label="生成时间" :span="2">{{ brochureDialog.brochureInfo.generateTime }}</el-descriptions-item>
</el-descriptions>
</div>
<!-- PDF预览区 -->
<div class="pdf-preview-wrapper">
<div v-if="brochureLoading && !brochureDialog.previewUrl" class="pdf-loading">
<el-icon class="is-loading"><Loading /></el-icon>
<span>加载中...</span>
</div>
<iframe
v-else-if="brochureDialog.previewUrl"
:src="brochureDialog.previewUrl"
class="pdf-iframe"
title="招聘简章预览"
/>
<div v-else class="pdf-empty">暂无预览,请先生成简章</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="brochureDialog.visible = false">关 闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="Enterprise" lang="ts">
import { listEnterprise, getEnterprise, delEnterprise, addEnterprise, updateEnterprise } from '@/api/zsjy/enterprise';
import { EnterpriseVO, EnterpriseForm, EnterpriseQuery } from '@/api/zsjy/types';
import {
listEnterprise,
getEnterprise,
delEnterprise,
addEnterprise,
updateEnterprise,
exportEnterprise,
importEnterprise,
getImportTemplate,
generateBrochure,
getBrochure,
downloadBrochure
} from '@/api/zsjy/enterprise';
import { Loading } from '@element-plus/icons-vue';
import { EnterpriseVO, EnterpriseJobForm } from '@/api/zsjy/types';
import request from '@/utils/request';
import { globalHeaders } from '@/utils/request';
import { computed } from 'vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -268,6 +430,28 @@ const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const importLoading = ref(false);
const brochureLoading = ref(false);
// 导入相关
const showImportDialog = ref(false);
const importUpdateSupport = ref(false);
const uploadRef = ref();
const importUrl = computed(() => import.meta.env.VITE_APP_BASE_API + '/zsjy/enterprise/importData?updateSupport=' + importUpdateSupport.value);
const uploadHeaders = globalHeaders();
// 简章相关
const brochureDialog = reactive<{
visible: boolean;
enterpriseId: any;
brochureInfo: any;
previewUrl: string | null;
}>({
visible: false,
enterpriseId: null,
brochureInfo: null,
previewUrl: null
});
const queryFormRef = ref<ElFormInstance>();
const enterpriseFormRef = ref<ElFormInstance>();
@@ -277,21 +461,25 @@ const { sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_normal_disable'))
const { zsjy_enterprise_industry } = toRefs<any>(proxy?.useDict('zsjy_enterprise_industry'));
const { zsjy_enterprise_scale } = toRefs<any>(proxy?.useDict('zsjy_enterprise_scale'));
const initFormData: EnterpriseForm = {
const initFormData = {
id: undefined,
enterpriseName: '',
industry: '',
mainBusiness: '',
enterpriseScale: '',
enterpriseIntro: '',
contactName: '',
contactPhone: '',
officePhone: '',
email: '',
website: '',
address: '',
status: '0',
remark: ''
remark: '',
jobList: [] as EnterpriseJobForm[]
};
const data = reactive<PageData<EnterpriseForm, EnterpriseQuery>>({
const data = reactive<PageData<typeof initFormData, any>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
@@ -303,20 +491,16 @@ const data = reactive<PageData<EnterpriseForm, EnterpriseQuery>>({
},
rules: {
enterpriseName: [{ required: true, message: '企业名称不能为空', trigger: 'blur' }],
industry: [{ required: true, message: '所属行业不能为空', trigger: 'change' }],
mainBusiness: [{ required: true, message: '主营业务不能为空', trigger: 'blur' }],
enterpriseScale: [{ required: true, message: '企业规模不能为空', trigger: 'change' }],
contactName: [{ required: true, message: '联系人姓名不能为空', trigger: 'blur' }],
contactPhone: [
{ required: true, message: '联系电话不能为空', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
],
email: [
{ required: true, message: '企业邮箱不能为空', trigger: 'blur' },
{ required: true, message: '电子邮箱不能为空', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
],
address: [{ required: true, message: '企业地址不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
address: [{ required: true, message: '企业地址不能为空', trigger: 'blur' }]
}
});
@@ -338,10 +522,13 @@ const viewForm = ref<EnterpriseVO>({} as EnterpriseVO);
/** 查询企业信息列表 */
const getList = async () => {
loading.value = true;
const res = await listEnterprise(queryParams.value);
enterpriseList.value = res.rows;
total.value = res.total;
loading.value = false;
try {
const res = await listEnterprise(queryParams.value);
enterpriseList.value = res.rows;
total.value = res.total;
} finally {
loading.value = false;
}
};
/** 取消按钮 */
@@ -352,7 +539,7 @@ const cancel = () => {
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
form.value = { ...initFormData, jobList: [] };
enterpriseFormRef.value?.resetFields();
};
@@ -380,16 +567,28 @@ const handleAdd = () => {
reset();
dialog.visible = true;
dialog.title = '添加企业信息';
// 初始化至少一个空岗位行
form.value.jobList = [{ positionName: '', requirements: '', benefits: '', status: '0' }];
};
/** 修改按钮操作 */
const handleUpdate = async (row?: EnterpriseVO) => {
reset();
const id = row?.id || ids.value[0];
const res = await getEnterprise(id);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = '修改企业信息';
loading.value = true;
try {
reset();
const id = row?.id || ids.value[0];
const res = await getEnterprise(id);
// 显式设置 jobList,避免 Object.assign 后 jobList 被其他引用污染
Object.assign(form.value, res.data, { jobList: res.data.jobList || [] });
// 确保岗位列表不为空(至少有一行空岗位方便用户继续添加)
if (!form.value.jobList || form.value.jobList.length === 0) {
form.value.jobList = [{ positionName: '', requirements: '', benefits: '', status: '0' }];
}
dialog.visible = true;
dialog.title = '修改企业信息';
} finally {
loading.value = false;
}
};
/** 查看详情 */
@@ -399,27 +598,51 @@ const handleView = async (row: EnterpriseVO) => {
viewDialog.visible = true;
};
/** 添加岗位 */
const handleAddJob = () => {
if (!form.value.jobList) {
form.value.jobList = [];
}
form.value.jobList.push({ positionName: '', requirements: '', benefits: '', status: '0' });
};
/** 删除岗位 */
const handleRemoveJob = (index: number) => {
if (form.value.jobList && form.value.jobList.length > 1) {
form.value.jobList.splice(index, 1);
}
};
/** 提交按钮 */
const submitForm = () => {
enterpriseFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.id) {
await updateEnterprise(form.value).finally(() => (buttonLoading.value = false));
} else {
await addEnterprise(form.value).finally(() => (buttonLoading.value = false));
try {
// 过滤掉空岗位(岗位名称为空的不提交)
const validJobList = (form.value.jobList || []).filter((job) => job.positionName && job.positionName.trim() !== '');
const submitData = { ...form.value, jobList: validJobList };
if (form.value.id) {
await updateEnterprise(submitData);
proxy?.$modal.msgSuccess('修改成功');
} else {
await addEnterprise(submitData);
proxy?.$modal.msgSuccess('新增成功');
}
dialog.visible = false;
await getList();
} finally {
buttonLoading.value = false;
}
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
await getList();
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: EnterpriseVO) => {
const idList = row?.id || ids.value;
await proxy?.$modal.confirm('是否确认删除企业信息编号为"' + idList + '"的数据项?');
const idList = row?.id ? [row.id] : ids.value;
await proxy?.$modal.confirm('是否确认删除选中的企业信息?删除后将同时删除关联的岗位信息。');
await delEnterprise(idList);
proxy?.$modal.msgSuccess('删除成功');
await getList();
@@ -427,13 +650,271 @@ const handleDelete = async (row?: EnterpriseVO) => {
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'zsjy/enterprise/export',
{
...queryParams.value
},
`enterprise_${new Date().getTime()}.xlsx`
);
proxy?.download('zsjy/enterprise/export', { ...queryParams.value }, `enterprise_${new Date().getTime()}.xlsx`);
};
/** 下载导入模板 */
const handleDownloadTemplate = () => {
proxy?.download('zsjy/enterprise/importTemplate', {}, 'enterprise_import_template.xlsx', 'GET');
};
/** 提交导入 */
const submitImport = () => {
if (!uploadRef.value) {
proxy?.$modal.msgError('上传组件未初始化');
return;
}
importLoading.value = true;
uploadRef.value?.submit();
};
/** 导入文件上传中 */
const handleImportProgress = () => {
importLoading.value = true;
};
/** 导入文件上传成功 */
const handleImportSuccess = (response: any) => {
importLoading.value = false;
showImportDialog.value = false;
proxy?.$modal.msgSuccess(response.msg || '导入成功');
getList();
};
/** 导入文件上传失败 */
const handleImportError = (err: any) => {
importLoading.value = false;
console.error('导入失败:', err);
proxy?.$modal.msgError(err?.message || '导入失败');
};
/** 生成简章(单个) */
const handleGenerateBrochure = async (row: EnterpriseVO) => {
brochureLoading.value = true;
// 生成前清理旧预览
if (brochureDialog.previewUrl) {
window.URL.revokeObjectURL(brochureDialog.previewUrl);
brochureDialog.previewUrl = null;
}
try {
const res = await generateBrochure(row.id);
brochureDialog.enterpriseId = row.id;
brochureDialog.brochureInfo = res.data;
brochureDialog.visible = true;
// 生成成功后自动加载预览
await loadBrochurePreview(row.id);
proxy?.$modal.msgSuccess('简章生成成功');
} catch (err: any) {
proxy?.$modal.msgError(err?.msg || '生成失败');
} finally {
brochureLoading.value = false;
}
};
/** 批量生成简章 */
const handleGenerateBrochureBatch = async () => {
if (ids.value.length === 0) {
proxy?.$modal.msgWarning('请先选择要生成简章的企业');
return;
}
await proxy?.$modal.confirm(`确认要批量生成 ${ids.value.length} 家企业的招聘简章吗?\n生成后将下载一个包含 ${ids.value.length} 页的多页PDF文件。`);
brochureLoading.value = true;
proxy?.$modal.loading('正在生成多页PDF,请稍候...');
try {
const response: any = await request({
url: '/zsjy/enterprise/brochure/batch/download',
method: 'post',
data: ids.value,
responseType: 'blob',
timeout: 300000
});
proxy?.$modal.closeLoading();
// 处理响应
if (!response) {
throw new Error('下载响应为空');
}
// 检查是否是 Blob 对象
const blob = response instanceof Blob ? response : null;
if (!blob) {
throw new Error('下载响应格式错误');
}
// 检查文件大小
if (blob.size === 0) {
throw new Error('下载文件为空');
}
// 检查是否是 JSON 错误响应
if (blob.type === 'application/json' || blob.type === 'text/plain') {
const text = await blob.text();
try {
const json = JSON.parse(text);
throw new Error(json.msg || '下载失败');
} catch (e) {
if (e.message !== '下载失败') {
throw new Error('下载失败,请重试');
}
throw e;
}
}
// 下载文件
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `企业招聘简章_${new Date().getTime()}.pdf`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
proxy?.$modal.msgSuccess('批量生成成功,共下载 ' + ids.value.length + ' 页');
} catch (err: any) {
console.error('批量生成失败:', err);
proxy?.$modal.msgError(err?.message || err?.msg || '批量生成失败');
} finally {
brochureLoading.value = false;
}
};
/** 查看详情时生成简章 */
const handleViewGenerateBrochure = async (row: EnterpriseVO) => {
await handleGenerateBrochure(row);
viewDialog.visible = false;
};
/** 下载PDF */
const handleDownloadBrochure = async () => {
if (!brochureDialog.enterpriseId) return;
// 显示下载加载状态
const loadingInstance = ElLoading.service({ text: '正在下载,请稍候...', background: 'rgba(0, 0, 0, 0.7)' });
try {
// 使用API中已有的downloadBrochure函数,确保认证正确
const blob: any = await downloadBrochure(brochureDialog.enterpriseId);
// 检查响应
if (!blob) {
throw new Error('下载响应为空');
}
// 检查是否是 Blob 对象
if (!(blob instanceof Blob)) {
throw new Error('下载响应格式错误');
}
// 检查文件大小
if (blob.size === 0) {
throw new Error('下载文件为空');
}
// 检查是否是 JSON 错误响应
if (blob.type === 'application/json' || (blob.type === 'text/plain')) {
const text = await blob.text();
try {
const json = JSON.parse(text);
throw new Error(json.msg || '下载失败');
} catch {
throw new Error('下载失败,请重试');
}
}
// 下载文件
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `招聘简章_${brochureDialog.brochureInfo?.enterpriseName || '未知企业'}.pdf`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
proxy?.$modal.msgSuccess('下载成功');
} catch (err: any) {
console.error('下载PDF失败:', err);
proxy?.$modal.msgError(err?.message || '下载失败');
} finally {
loadingInstance.close();
}
};
/** 加载简章预览 */
const loadBrochurePreview = async (enterpriseId: any) => {
if (!enterpriseId) return;
brochureLoading.value = true;
try {
console.log('开始加载预览,企业ID:', enterpriseId);
const response = await downloadBrochure(enterpriseId);
console.log('预览响应类型:', typeof response, 'response:', response);
// 检查响应
if (!response) {
console.error('预览响应为空');
return;
}
// 检查是否是 Blob 对象
if (!(response instanceof Blob)) {
console.error('预览响应不是Blob对象:', typeof response);
return;
}
console.log('Blob大小:', response.size, '类型:', response.type);
// 检查文件大小
if (response.size === 0) {
console.error('预览文件为空');
return;
}
// 检查是否是 JSON 错误响应
if (response.type === 'application/json' || response.type === 'text/plain') {
const text = await response.text();
console.error('服务端返回了JSON错误:', text);
try {
const json = JSON.parse(text);
console.error('预览加载失败,服务端返回错误:', json.msg || json.message);
} catch {
console.error('预览加载失败,未知错误');
}
return;
}
// 清理旧的预览URL
if (brochureDialog.previewUrl) {
window.URL.revokeObjectURL(brochureDialog.previewUrl);
}
// 创建新的预览URL
brochureDialog.previewUrl = window.URL.createObjectURL(response);
console.log('预览加载成功');
} catch (err: any) {
console.error('预览加载失败:', err);
// 显示错误消息
proxy?.$modal.msgError('预览加载失败: ' + (err?.message || err?.msg || '未知错误'));
} finally {
brochureLoading.value = false;
}
};
/** 重新生成 */
const handleReGenerateBrochure = async () => {
if (!brochureDialog.enterpriseId) return;
brochureLoading.value = true;
try {
const res = await generateBrochure(brochureDialog.enterpriseId);
brochureDialog.brochureInfo = res.data;
await loadBrochurePreview(brochureDialog.enterpriseId);
proxy?.$modal.msgSuccess('重新生成成功');
} catch (err: any) {
proxy?.$modal.msgError(err?.msg || '重新生成失败');
} finally {
brochureLoading.value = false;
}
};
onMounted(() => {
@@ -442,7 +923,6 @@ onMounted(() => {
</script>
<style scoped lang="scss">
// 继承后勤管理的样式
.page-container {
.search-btn-container {
text-align: right;
@@ -473,26 +953,54 @@ onMounted(() => {
text-align: right;
}
// 表格适应性样式
.pdf-preview-wrapper {
margin-top: 15px;
border: 1px solid #dcdfe6;
border-radius: 4px;
background: #f5f7fa;
min-height: 400px;
display: flex;
align-items: center;
justify-content: center;
.pdf-loading,
.pdf-empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
color: #909399;
padding: 40px;
.el-icon {
font-size: 32px;
}
}
.pdf-iframe {
width: 100%;
height: 600px;
border: none;
border-radius: 4px;
}
}
:deep(.el-table) {
.el-table__cell {
padding: 8px 0;
}
}
// 响应式表格
:deep(.el-table__body-wrapper) {
overflow-x: auto;
}
// 表格列宽自适应
.el-table {
width: 100%;
table-layout: auto;
}
}
// 描述列表样式优化
:deep(.el-descriptions) {
.el-descriptions__label {
font-weight: 600;
@@ -504,6 +1012,7 @@ onMounted(() => {
color: #303133;
}
}
.search-btn-container {
display: flex;
justify-content: flex-end;
@@ -513,4 +1022,55 @@ onMounted(() => {
margin-bottom: 0 !important;
}
}
.enterprise-dialog {
:deep(.el-dialog__body) {
max-height: 70vh;
overflow-y: auto;
}
}
.job-table-container {
margin-top: 10px;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 10px;
.empty-job-tip {
text-align: center;
color: #909399;
padding: 20px;
font-size: 14px;
}
}
.brochure-actions {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.brochure-info {
margin-top: 10px;
}
.upload-demo {
text-align: center;
.el-icon-upload {
font-size: 67px;
color: #409eff;
margin: 20px 0 10px;
}
.el-upload__text {
color: #606266;
font-size: 14px;
em {
color: #409eff;
font-style: normal;
}
}
}
</style>
+91 -34
View File
@@ -175,7 +175,30 @@
<el-radio v-for="dict in zsjy_poster_type" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="关联岗位" prop="positionId" v-if="form.posterType === '2'">
<!-- 新增岗位海报时,支持岗位多选 -->
<el-form-item
v-if="form.posterType === '2' && !form.id"
label="关联岗位"
prop="positionIdList"
>
<el-select
v-model="form.positionIdList"
placeholder="请选择关联岗位(可多选)"
style="width: 100%"
:disabled="!form.enterpriseId"
multiple
collapse-tags
collapse-tags-tooltip
>
<el-option v-for="item in formPositionOptions" :key="item.id" :label="item.positionName" :value="item.id" />
</el-select>
</el-form-item>
<!-- 编辑岗位海报时,保持单选,避免修改已生成数据的关联关系过于复杂 -->
<el-form-item
v-else-if="form.posterType === '2' && !!form.id"
label="关联岗位"
prop="positionId"
>
<el-select v-model="form.positionId" placeholder="请选择关联岗位" style="width: 100%" :disabled="!form.enterpriseId">
<el-option v-for="item in formPositionOptions" :key="item.id" :label="item.positionName" :value="item.id" />
</el-select>
@@ -199,28 +222,17 @@
<!-- 预览对话框 -->
<el-dialog title="海报预览" v-model="previewOpen" width="900px" append-to-body>
<div v-if="previewOpen && previewData.posterPath" class="poster-preview-container">
<!-- 海报图片预览 -->
<div class="poster-image-section">
<image-preview :src="previewData.posterPath" :width="400" :height="600" :preview="true" style="margin: 0 auto" />
</div>
<!-- 海报详情信息 -->
<div class="poster-info-section">
<el-descriptions title="海报详情" :column="2" border>
<el-descriptions-item label="海报类型">
<dict-tag :options="zsjy_poster_type" :value="previewData.posterType" />
</el-descriptions-item>
<el-descriptions-item label="所属企业">
{{ enterpriseDetail.enterpriseName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="关联岗位" v-if="previewData.posterType === '2'">
{{ positionDetail.positionName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ previewData.createTime || '-' }}
</el-descriptions-item>
</el-descriptions>
</div>
<!-- 使用 PosterPreview 组件生成带企业/岗位信息的海报,并支持导出 -->
<PosterPreview
:posterData="{
posterPath: previewData.posterPath,
enterpriseName: enterpriseDetail.enterpriseName || previewData.enterpriseName || '',
positionName: positionDetail.positionName || previewData.positionName || '',
posterType: previewData.posterType
}"
:enterpriseDetail="enterpriseDetail"
:positionDetail="positionDetail"
/>
</div>
<!-- 无海报图片提示 -->
@@ -256,6 +268,7 @@ import { listEnterpriseOptions } from '@/api/zsjy/jobPosition';
import { getEnterprise } from '@/api/zsjy/enterprise';
import { getJobPosition } from '@/api/zsjy/jobPosition';
import { RecruitmentPosterVO, RecruitmentPosterForm, RecruitmentPosterQuery } from '@/api/zsjy/types';
import PosterPreview from '@/components/PosterPreview/index.vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -295,10 +308,11 @@ const queryParams = ref<RecruitmentPosterQuery>({
});
// 表单参数
const form = ref<RecruitmentPosterForm>({
const form = ref<RecruitmentPosterForm & { positionIdList?: Array<number | string> }>({
id: undefined,
enterpriseId: '',
positionId: undefined,
positionIdList: [],
posterPath: '',
posterType: '1'
});
@@ -307,8 +321,17 @@ const form = ref<RecruitmentPosterForm>({
const rules = {
enterpriseId: [{ required: true, message: '请选择所属企业', trigger: 'change' }],
posterType: [{ required: true, message: '请选择海报类型', trigger: 'change' }],
posterPath: [{ required: true, message: '请上传海报图片', trigger: 'change' }]
};
posterPath: [{ required: true, message: '请上传海报图片', trigger: 'change' }],
// 仅在新增岗位海报且岗位多选时生效
positionIdList: [
{
type: 'array',
required: true,
message: '请选择至少一个关联岗位',
trigger: 'change'
}
]
} as const;
// 预览数据
const previewOpen = ref(false);
@@ -378,6 +401,7 @@ const handleEnterpriseChange = (val: number | string) => {
/** 表单企业选择改变 */
const handleFormEnterpriseChange = (val: number | string) => {
form.value.positionId = undefined;
form.value.positionIdList = [];
if (val) {
getFormPositionOptions(val);
} else {
@@ -389,6 +413,7 @@ const handleFormEnterpriseChange = (val: number | string) => {
const handlePosterTypeChange = (val: string) => {
if (val === '1') {
form.value.positionId = undefined;
form.value.positionIdList = [];
}
};
@@ -460,17 +485,48 @@ const handlePreview = async (row: RecruitmentPosterVO) => {
/** 提交按钮 */
const submitForm = () => {
posterFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
if (form.value.id) {
await updateRecruitmentPoster(form.value);
proxy?.$modal.msgSuccess('修改成功');
} else {
await addRecruitmentPoster(form.value);
proxy?.$modal.msgSuccess('新增成功');
}
if (!valid) {
return;
}
// 编辑模式:保持原有单条更新逻辑
if (form.value.id) {
await updateRecruitmentPoster(form.value);
proxy?.$modal.msgSuccess('修改成功');
open.value = false;
getList();
return;
}
// 新增岗位海报且选择了多个岗位时,批量生成多条记录
if (
form.value.posterType === '2' &&
Array.isArray(form.value.positionIdList) &&
form.value.positionIdList.length > 0
) {
const { positionIdList, ...baseForm } = form.value as RecruitmentPosterForm & {
positionIdList: Array<number | string>;
};
const requests = positionIdList.map((pid) =>
addRecruitmentPoster({
...baseForm,
positionId: pid
})
);
await Promise.all(requests);
proxy?.$modal.msgSuccess(`新增成功(共生成 ${positionIdList.length} 条岗位海报)`);
open.value = false;
getList();
return;
}
// 其他情况(企业海报或单个岗位海报),按原有逻辑新增一条记录
await addRecruitmentPoster(form.value);
proxy?.$modal.msgSuccess('新增成功');
open.value = false;
getList();
});
};
@@ -502,6 +558,7 @@ const resetForm = () => {
id: undefined,
enterpriseId: '',
positionId: undefined,
positionIdList: [],
posterPath: '',
posterType: '1'
};
+1 -1
View File
@@ -13,7 +13,7 @@ import path from 'path';
export default (viteEnv: any, isBuild = false): [] => {
const vitePlugins: any = [];
vitePlugins.push(vue());
vitePlugins.push(vueDevTools());
// vitePlugins.push(vueDevTools()); // 禁用以解决localStorage兼容性问题
vitePlugins.push(createUnoCss());
vitePlugins.push(createAutoImport(path));
vitePlugins.push(createComponents(path));