预约面试功能开发

This commit is contained in:
francis-fh
2026-06-24 18:18:55 +08:00
parent a3cd9b0277
commit f51b1a3ce1
14 changed files with 354 additions and 23 deletions

16
.codegraph/.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
# CodeGraph data files
# These are local to each machine and should not be committed
# Database
*.db
*.db-wal
*.db-shm
# Cache
cache/
# Logs
*.log
# Hook markers
.dirty

BIN
CreateTableHelper.class Normal file

Binary file not shown.

45
CreateTableHelper.java Normal file
View File

@@ -0,0 +1,45 @@
import java.sql.*;
public class CreateTableHelper {
public static void main(String[] args) throws Exception {
String url = "jdbc:highgo://39.98.44.136:6023/highgo?currentSchema=shz";
String user = "sysdba";
String password = "Hello@2026";
Class.forName("com.highgo.jdbc.Driver");
try (Connection conn = DriverManager.getConnection(url, user, password);
Statement stmt = conn.createStatement()) {
String sql = "CREATE TABLE interview_invitation ("
+ "id bigint NOT NULL,"
+ "company_id bigint NOT NULL,"
+ "job_id bigint NOT NULL,"
+ "user_id bigint NOT NULL,"
+ "apply_id bigint NULL,"
+ "interview_time varchar(100) NULL,"
+ "interview_method varchar(20) NULL,"
+ "meeting_link varchar(500) NULL,"
+ "meeting_password varchar(100) NULL,"
+ "interview_location varchar(500) NULL,"
+ "contact_phone varchar(50) NULL,"
+ "status varchar(20) DEFAULT 'pending' NULL,"
+ "del_flag char(1) DEFAULT '0' NULL,"
+ "create_by varchar(64) DEFAULT '' NULL,"
+ "create_time timestamp NULL,"
+ "update_by varchar(64) DEFAULT '' NULL,"
+ "update_time timestamp NULL,"
+ "remark varchar(500) NULL"
+ ")";
stmt.execute(sql);
System.out.println("Table interview_invitation created successfully!");
// Check table exists
ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM interview_invitation");
rs.next();
System.out.println("Table verified, row count: " + rs.getInt(1));
rs.close();
}
}
}

View File

@@ -1,6 +1,7 @@
package com.ruoyi.cms.controller.app;
import com.ruoyi.cms.domain.InterviewInvitation;
import com.ruoyi.cms.domain.vo.InterviewInvitationVO;
import com.ruoyi.cms.service.InterviewInvitationService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
@@ -30,25 +31,35 @@ public class AppInterviewController extends BaseController {
private InterviewInvitationService interviewInvitationService;
/**
* 我的面试邀约列表
* 我的面试邀约列表(带公司名和岗位名)
*/
@ApiOperation("我的面试邀约列表")
@GetMapping("/list")
public TableDataInfo list(){
public TableDataInfo list(@RequestParam(required = false) Long userId,
@RequestParam(required = false) String status){
InterviewInvitation query = new InterviewInvitation();
// 优先用传入的userId否则取当前登录用户
if (userId != null) {
query.setUserId(userId);
} else {
query.setUserId(SiteSecurityUtils.getUserId());
}
if (status != null && !status.isEmpty()) {
query.setStatus(status);
}
startPage();
List<InterviewInvitation> list = interviewInvitationService.getInterviewInvitationList(query);
List<InterviewInvitationVO> list = interviewInvitationService.getInterviewInvitationVOList(query);
return getDataTable(list);
}
/**
* 获取详细信息
* 获取详细信息(带公司名和岗位名)
*/
@ApiOperation("面试邀约详情")
@GetMapping("/{id}")
public AjaxResult getInfo(@PathVariable Long id){
return AjaxResult.success(interviewInvitationService.getInterviewInvitationById(id));
InterviewInvitationVO vo = interviewInvitationService.getInterviewInvitationVOById(id);
return AjaxResult.success(vo);
}
/**
@@ -57,6 +68,13 @@ public class AppInterviewController extends BaseController {
@ApiOperation("更新面试邀约状态")
@PutMapping("/status/{id}")
public AjaxResult updateStatus(@PathVariable Long id, @RequestParam String status){
return toAjax(interviewInvitationService.updateInterviewStatus(id, status));
int rows = interviewInvitationService.updateInterviewStatus(id, status);
if (rows > 0 && "accepted".equals(status)) {
// 接受时返回面试官联系方式,供前端弹窗展示
InterviewInvitation invitation = interviewInvitationService.getInterviewInvitationById(id);
return AjaxResult.success("操作成功",
invitation != null ? invitation.getContactPhone() : null);
}
return toAjax(rows);
}
}

View File

@@ -77,6 +77,16 @@ public class InterviewInvitation extends BaseEntity {
*/
@ApiModelProperty("面试官联系方式")
private String contactPhone;
/**
* 公司名称冗余存储创建时从company表获取
*/
@ApiModelProperty("公司名称")
private String companyName;
/**
* 岗位名称冗余存储创建时从job表获取
*/
@ApiModelProperty("岗位名称")
private String jobName;
/**
* 状态(pending/accepted/rejected/completed)
*/

View File

@@ -0,0 +1,14 @@
package com.ruoyi.cms.domain.vo;
import com.ruoyi.cms.domain.InterviewInvitation;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
public class InterviewInvitationVO extends InterviewInvitation {
// companyName 已从父类 InterviewInvitation 继承
@ApiModelProperty("岗位名称(来自关联查询的实时数据)")
private String jobTitle;
}

View File

@@ -2,8 +2,11 @@ package com.ruoyi.cms.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.cms.domain.InterviewInvitation;
import com.ruoyi.cms.domain.vo.InterviewInvitationVO;
import java.util.List;
public interface InterviewInvitationMapper extends BaseMapper<InterviewInvitation> {
List<InterviewInvitation> getInterviewInvitationList(InterviewInvitation interviewInvitation);
List<InterviewInvitationVO> getInterviewInvitationVOList(InterviewInvitation interviewInvitation);
}

View File

@@ -1,6 +1,7 @@
package com.ruoyi.cms.service;
import com.ruoyi.cms.domain.InterviewInvitation;
import com.ruoyi.cms.domain.vo.InterviewInvitationVO;
import java.util.List;
@@ -15,6 +16,8 @@ public interface InterviewInvitationService {
List<InterviewInvitation> getInterviewInvitationList(InterviewInvitation interviewInvitation);
List<InterviewInvitationVO> getInterviewInvitationVOList(InterviewInvitation interviewInvitation);
int insertInterviewInvitation(InterviewInvitation interviewInvitation);
int updateInterviewInvitation(InterviewInvitation interviewInvitation);
@@ -23,5 +26,7 @@ public interface InterviewInvitationService {
InterviewInvitation getInterviewInvitationById(Long id);
InterviewInvitationVO getInterviewInvitationVOById(Long id);
int updateInterviewStatus(Long id, String status);
}

View File

@@ -1,22 +1,32 @@
package com.ruoyi.cms.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ruoyi.cms.domain.InterviewInvitation;
import com.ruoyi.cms.domain.Job;
import com.ruoyi.cms.domain.JobApply;
import com.ruoyi.cms.domain.Notice;
import com.ruoyi.cms.domain.vo.InterviewInvitationVO;
import com.ruoyi.cms.mapper.CompanyMapper;
import com.ruoyi.cms.mapper.InterviewInvitationMapper;
import com.ruoyi.cms.mapper.JobApplyMapper;
import com.ruoyi.cms.mapper.JobMapper;
import com.ruoyi.cms.mapper.NoticeMapper;
import com.ruoyi.cms.service.InterviewInvitationService;
import com.ruoyi.cms.util.notice.NoticeUtils;
import com.ruoyi.common.core.domain.entity.Company;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.Statement;
import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;
import java.util.List;
@Slf4j
@Service
public class InterviewInvitationServiceImpl implements InterviewInvitationService {
@@ -28,21 +38,123 @@ public class InterviewInvitationServiceImpl implements InterviewInvitationServic
private NoticeMapper noticeMapper;
@Autowired
private JobMapper jobMapper;
@Autowired
private CompanyMapper companyMapper;
@Autowired
private DataSource dataSource;
@PostConstruct
public void initTable() {
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement()) {
// Check if id column has auto-increment (BIGSERIAL or sequence default)
boolean needRecreate = false;
try {
// Check column default
java.sql.ResultSet rs = stmt.executeQuery("SELECT column_default FROM information_schema.columns WHERE table_name='interview_invitation' AND column_name='id'");
if (rs.next()) {
String def = rs.getString(1);
if (def == null || !def.contains("nextval")) {
needRecreate = true;
}
} else {
needRecreate = true;
}
rs.close();
} catch (Exception e) {
needRecreate = true;
}
if (needRecreate) {
log.info("需要重建 interview_invitation 表...");
try { stmt.execute("DROP TABLE IF EXISTS interview_invitation"); } catch (Exception ignored) {}
try { stmt.execute("DROP SEQUENCE IF EXISTS interview_invitation_id_seq"); } catch (Exception ignored) {}
String sql = "CREATE TABLE interview_invitation ("
+ "id bigint NOT NULL,"
+ "company_id bigint NOT NULL,"
+ "job_id bigint NOT NULL,"
+ "user_id bigint NOT NULL,"
+ "apply_id bigint NULL,"
+ "interview_time varchar(100) NULL,"
+ "interview_method varchar(20) NULL,"
+ "meeting_link varchar(500) NULL,"
+ "meeting_password varchar(100) NULL,"
+ "interview_location varchar(500) NULL,"
+ "contact_phone varchar(50) NULL,"
+ "company_name varchar(200) NULL,"
+ "job_name varchar(200) NULL,"
+ "status varchar(20) DEFAULT 'pending' NULL,"
+ "del_flag char(1) DEFAULT '0' NULL,"
+ "create_by varchar(64) DEFAULT '' NULL,"
+ "create_time timestamp NULL,"
+ "update_by varchar(64) DEFAULT '' NULL,"
+ "update_time timestamp NULL,"
+ "remark varchar(500) NULL"
+ ")";
stmt.execute(sql);
stmt.execute("CREATE SEQUENCE interview_invitation_id_seq START 1");
stmt.execute("ALTER TABLE interview_invitation ALTER COLUMN id SET DEFAULT nextval('interview_invitation_id_seq')");
log.info("interview_invitation 表创建成功(序列自增)!");
} else {
log.info("interview_invitation 表结构正常,检查新增列...");
// 为已有数据库添加新列(幂等操作)
try { stmt.execute("ALTER TABLE interview_invitation ADD COLUMN IF NOT EXISTS company_name varchar(200) NULL"); } catch (Exception ignored) {}
try { stmt.execute("ALTER TABLE interview_invitation ADD COLUMN IF NOT EXISTS job_name varchar(200) NULL"); } catch (Exception ignored) {}
}
} catch (Exception e) {
log.error("初始化表失败: {}", e.getMessage());
}
}
@Override
public List<InterviewInvitation> getInterviewInvitationList(InterviewInvitation interviewInvitation) {
return interviewInvitationMapper.getInterviewInvitationList(interviewInvitation);
}
@Override
public List<InterviewInvitationVO> getInterviewInvitationVOList(InterviewInvitation interviewInvitation) {
return interviewInvitationMapper.getInterviewInvitationVOList(interviewInvitation);
}
@Override
public InterviewInvitationVO getInterviewInvitationVOById(Long id) {
InterviewInvitation query = new InterviewInvitation();
query.setId(id);
List<InterviewInvitationVO> list = interviewInvitationMapper.getInterviewInvitationVOList(query);
return list.isEmpty() ? null : list.get(0);
}
@Override
public int insertInterviewInvitation(InterviewInvitation interviewInvitation) {
// 检查是否已存在有效的面试邀约(排除已拒绝的)
QueryWrapper<InterviewInvitation> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", interviewInvitation.getUserId())
.eq("job_id", interviewInvitation.getJobId())
.eq("del_flag", "0")
.ne("status", "rejected");
Long existingCount = interviewInvitationMapper.selectCount(queryWrapper);
if (existingCount != null && existingCount > 0) {
throw new RuntimeException("该求职者已存在有效的面试邀约,无法重复邀约");
}
// 填充公司名称和岗位名称(冗余存储,保证记录自包含)
// 如果前端已传岗位名称则优先使用,否则通过 jobId 查询
if (interviewInvitation.getJobName() == null || interviewInvitation.getJobName().isEmpty()) {
Job job = jobMapper.getJobInfo(interviewInvitation.getJobId());
if (job != null) {
interviewInvitation.setJobName(job.getJobTitle());
}
}
Company company = companyMapper.selectById(interviewInvitation.getCompanyId());
if (company != null) {
interviewInvitation.setCompanyName(company.getName());
}
int t = interviewInvitationMapper.insert(interviewInvitation);
if (t > 0) {
// 获取岗位和申请信息
Job job = jobMapper.getJobInfo(interviewInvitation.getJobId());
// 获取申请信息
JobApply jobApply = jobApplyMapper.selectById(interviewInvitation.getApplyId());
// 创建面试邀约通知
Notice notice = createInterviewNotice(interviewInvitation, job, jobApply);
// 创建面试邀约通知(给求职者)
Notice notice = createInterviewNotice(interviewInvitation, jobApply);
if (notice != null) {
noticeMapper.insert(notice);
}
@@ -70,13 +182,64 @@ public class InterviewInvitationServiceImpl implements InterviewInvitationServic
InterviewInvitation invitation = new InterviewInvitation();
invitation.setId(id);
invitation.setStatus(status);
return interviewInvitationMapper.updateById(invitation);
int rows = interviewInvitationMapper.updateById(invitation);
// 如果是接受或拒绝,通知公司方
if (rows > 0 && ("accepted".equals(status) || "rejected".equals(status))) {
InterviewInvitation fullInvitation = interviewInvitationMapper.selectById(id);
if (fullInvitation != null) {
notifyCompanyStatusChange(fullInvitation, status);
}
}
return rows;
}
/**
* 通知公司方面试邀约状态变更
*/
private void notifyCompanyStatusChange(InterviewInvitation invitation, String newStatus) {
Company company = companyMapper.selectById(invitation.getCompanyId());
if (company == null || company.getUserId() == null) {
log.warn("无法通知公司companyId={} 不存在或无关联用户", invitation.getCompanyId());
return;
}
String statusText = "accepted".equals(newStatus) ? "已接受" : "已拒绝";
String jobName = invitation.getJobName();
// 如果冗余字段为空,尝试从关联表获取
if (jobName == null || jobName.isEmpty()) {
Job job = jobMapper.getJobInfo(invitation.getJobId());
if (job != null) {
jobName = job.getJobTitle();
}
}
Notice notice = new Notice();
notice.setUserId(company.getUserId());
notice.setBussinessId(invitation.getJobId());
notice.setIsRead(NoticeUtils.NOTICE_WD);
notice.setTitle("面试邀约状态更新");
notice.setSubTitle("面试邀约");
notice.setNoticeType(NoticeUtils.NOTICE_TYPE_XTLX);
notice.setRemark(NoticeUtils.NOTICE_REMARK);
StringBuilder content = new StringBuilder();
content.append("您发出的面试邀约(岗位:").append(jobName != null ? jobName : "").append("");
content.append("已被求职者").append(statusText).append("");
content.append("面试时间:").append(invitation.getInterviewTime() != null ? invitation.getInterviewTime() : "待定").append("");
notice.setNoticeContent(content.toString());
noticeMapper.insert(notice);
log.info("已通知公司方userId={}面试邀约状态变更id={}, status={}",
company.getUserId(), invitation.getId(), newStatus);
}
/**
* 创建面试邀约通知
*/
private Notice createInterviewNotice(InterviewInvitation invitation, Job job, JobApply jobApply) {
private Notice createInterviewNotice(InterviewInvitation invitation, JobApply jobApply) {
Notice notice = new Notice();
Long userId = invitation.getUserId();
if (userId == null && jobApply != null) {
@@ -85,13 +248,16 @@ public class InterviewInvitationServiceImpl implements InterviewInvitationServic
notice.setUserId(userId);
notice.setBussinessId(invitation.getJobId());
notice.setIsRead(NoticeUtils.NOTICE_WD);
notice.setTitle("面试邀约通知");
notice.setSubTitle("面试邀约");
notice.setNoticeType(NoticeUtils.NOTICE_TYPE_XTLX);
notice.setTitle("面试通知");
notice.setSubTitle("面试通知");
notice.setNoticeType(NoticeUtils.NOTICE_TYPE_MSLX);
notice.setRemark(NoticeUtils.NOTICE_REMARK);
// 拼装通知内容
String jobTitle = job != null ? job.getJobTitle() : "";
// 拼装通知内容:优先使用前端传来的岗位名称
String jobTitle = invitation.getJobName();
if (jobTitle == null || jobTitle.isEmpty()) {
jobTitle = "";
}
String methodText = "online".equals(invitation.getInterviewMethod()) ? "线上" : "线下";
StringBuilder content = new StringBuilder();
content.append("您好,您申请的【").append(jobTitle).append("】岗位收到了面试邀约。");

View File

@@ -1060,16 +1060,32 @@ public class JobServiceImpl extends ServiceImpl<JobMapper,Job> implements IJobSe
private List<ESJobDocument> userCollection(List<ESJobDocument> jobs){
if(jobs.isEmpty()){return new ArrayList<>();}
// 优先使用 App 端用户,兜底使用 PC 端用户
Long userId = null;
if(SiteSecurityUtils.isLogin()){
userId = SiteSecurityUtils.getUserId();
} else if(SecurityUtils.isLogin()){
userId = SecurityUtils.getUserId();
}
if(userId != null){
//收藏
Set<Long> collectionIds = jobCollectionMapper.selectList(Wrappers.<JobCollection>lambdaQuery()
.eq(JobCollection::getUserId, SiteSecurityUtils.getUserId())
.eq(JobCollection::getUserId, userId)
.in(JobCollection::getJobId, jobs.stream().map(ESJobDocument::getJobId).collect(Collectors.toList())))
.stream().map(JobCollection::getJobId).collect(Collectors.toSet());
//申请
Set<Long> applyJobIds = new HashSet<>();
List<Job> jobList = jobApplyMapper.applyJob(userId);
if (CollectionUtils.isNotEmpty(jobList)) {
applyJobIds = jobList.stream().map(Job::getJobId).collect(Collectors.toSet());
}
for (ESJobDocument j : jobs) {
if (collectionIds.contains(j.getJobId())) {
j.setIsCollection(1);
}
if (applyJobIds.contains(j.getJobId())) {
j.setIsApply(1);
}
}
}
return jobs;

View File

@@ -34,6 +34,8 @@ public class NoticeUtils {
public static final String NOTICE_TYPE_SXLX="2";
public static final String NOTICE_TYPE_MSLX="4";
/**
* 拼装岗位
*/

View File

@@ -16,6 +16,8 @@
<result property="meetingPassword" column="meeting_password"/>
<result property="interviewLocation" column="interview_location"/>
<result property="contactPhone" column="contact_phone"/>
<result property="companyName" column="company_name"/>
<result property="jobName" column="job_name"/>
<result property="status" column="status"/>
<result property="delFlag" column="del_flag"/>
<result property="createBy" column="create_by"/>
@@ -25,11 +27,16 @@
<result property="remark" column="remark"/>
</resultMap>
<!-- VO 带公司名和岗位名继承基础映射jobTitle 来自关联查询的实时数据) -->
<resultMap type="com.ruoyi.cms.domain.vo.InterviewInvitationVO" id="InterviewInvitationVOResult" extends="InterviewInvitationResult">
<result property="jobTitle" column="job_title"/>
</resultMap>
<sql id="selectInterviewInvitationVo">
select id, company_id, job_id, user_id, apply_id, interview_time,
interview_method, meeting_link, meeting_password, interview_location,
contact_phone, status, del_flag, create_by, create_time, update_by, update_time, remark
contact_phone, company_name, job_name, status, del_flag,
create_by, create_time, update_by, update_time, remark
from interview_invitation
</sql>
@@ -45,4 +52,22 @@
order by create_time desc
</select>
<!-- 带公司名和岗位名的列表查询COALESCE 优先用关联表实时数据,兜底用冗余存储 -->
<select id="getInterviewInvitationVOList" resultMap="InterviewInvitationVOResult" parameterType="InterviewInvitation">
select t.*,
COALESCE(c.name, t.company_name) as company_name,
COALESCE(j.job_title, t.job_name) as job_title
from interview_invitation t
left join company c on c.company_id = t.company_id and c.del_flag = '0'
left join job j on j.job_id = t.job_id and j.del_flag = '0'
<where> t.del_flag = '0'
<if test="companyId != null and companyId != ''"> and t.company_id = #{companyId}</if>
<if test="jobId != null and jobId != ''"> and t.job_id = #{jobId}</if>
<if test="userId != null and userId != ''"> and t.user_id = #{userId}</if>
<if test="applyId != null and applyId != ''"> and t.apply_id = #{applyId}</if>
<if test="status != null and status != ''"> and t.status = #{status}</if>
</where>
order by t.create_time desc
</select>
</mapper>

View File

@@ -336,8 +336,9 @@
<select id="getJobInfo" resultType="com.ruoyi.cms.domain.Job">
SELECT j.*,c.code,c.name as companyName,c.company_id FROM job as j
left join company as c on c.company_id = j.company_id and j.del_flag='0' and j.job_id=#{jobId}
and c.del_flag='0' limit 1
left join company as c on c.company_id = j.company_id and c.del_flag='0'
WHERE j.del_flag='0' AND j.job_id=#{jobId}
limit 1
</select>
<select id="getTotals" parameterType="Job" resultType="java.lang.Integer">

View File

@@ -15,6 +15,11 @@ public class TableSupport
*/
public static final String PAGE_NUM = "current";
/**
* 兼容移动端使用的分页参数名
*/
public static final String PAGE_NUM_APP = "pageNum";
/**
* 每页显示记录数
*/
@@ -41,7 +46,12 @@ public class TableSupport
public static PageDomain getPageDomain()
{
PageDomain pageDomain = new PageDomain();
pageDomain.setPageNum(Convert.toInt(ServletUtils.getParameter(PAGE_NUM), 1));
// 兼容移动端 pageNum 和后台管理 current 两种分页参数名
Integer pageNum = Convert.toInt(ServletUtils.getParameter(PAGE_NUM));
if (pageNum == null) {
pageNum = Convert.toInt(ServletUtils.getParameter(PAGE_NUM_APP), 1);
}
pageDomain.setPageNum(pageNum);
pageDomain.setPageSize(Convert.toInt(ServletUtils.getParameter(PAGE_SIZE), 10));
pageDomain.setOrderByColumn(ServletUtils.getParameter(ORDER_BY_COLUMN));
pageDomain.setIsAsc(ServletUtils.getParameter(IS_ASC));