diff --git a/.codegraph/.gitignore b/.codegraph/.gitignore new file mode 100644 index 0000000..9de0f16 --- /dev/null +++ b/.codegraph/.gitignore @@ -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 diff --git a/CreateTableHelper.class b/CreateTableHelper.class new file mode 100644 index 0000000..6fa2f3f Binary files /dev/null and b/CreateTableHelper.class differ diff --git a/CreateTableHelper.java b/CreateTableHelper.java new file mode 100644 index 0000000..1ca0c70 --- /dev/null +++ b/CreateTableHelper.java @@ -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(); + } + } +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/app/AppInterviewController.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/app/AppInterviewController.java index cc30d35..1fc40dd 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/app/AppInterviewController.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/app/AppInterviewController.java @@ -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(); - query.setUserId(SiteSecurityUtils.getUserId()); + // 优先用传入的userId,否则取当前登录用户 + if (userId != null) { + query.setUserId(userId); + } else { + query.setUserId(SiteSecurityUtils.getUserId()); + } + if (status != null && !status.isEmpty()) { + query.setStatus(status); + } startPage(); - List list = interviewInvitationService.getInterviewInvitationList(query); + List 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); } } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/InterviewInvitation.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/InterviewInvitation.java index 7504d52..af095fb 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/InterviewInvitation.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/InterviewInvitation.java @@ -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) */ diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/InterviewInvitationVO.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/InterviewInvitationVO.java new file mode 100644 index 0000000..aaa8356 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/InterviewInvitationVO.java @@ -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; +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/InterviewInvitationMapper.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/InterviewInvitationMapper.java index 2bbe6d1..1f71192 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/InterviewInvitationMapper.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/InterviewInvitationMapper.java @@ -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 { List getInterviewInvitationList(InterviewInvitation interviewInvitation); + + List getInterviewInvitationVOList(InterviewInvitation interviewInvitation); } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/InterviewInvitationService.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/InterviewInvitationService.java index 1d15580..4ac77a8 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/InterviewInvitationService.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/InterviewInvitationService.java @@ -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 getInterviewInvitationList(InterviewInvitation interviewInvitation); + List 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); } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/InterviewInvitationServiceImpl.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/InterviewInvitationServiceImpl.java index 4f28c50..8d334f6 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/InterviewInvitationServiceImpl.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/InterviewInvitationServiceImpl.java @@ -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 getInterviewInvitationList(InterviewInvitation interviewInvitation) { return interviewInvitationMapper.getInterviewInvitationList(interviewInvitation); } + @Override + public List getInterviewInvitationVOList(InterviewInvitation interviewInvitation) { + return interviewInvitationMapper.getInterviewInvitationVOList(interviewInvitation); + } + + @Override + public InterviewInvitationVO getInterviewInvitationVOById(Long id) { + InterviewInvitation query = new InterviewInvitation(); + query.setId(id); + List list = interviewInvitationMapper.getInterviewInvitationVOList(query); + return list.isEmpty() ? null : list.get(0); + } + @Override public int insertInterviewInvitation(InterviewInvitation interviewInvitation) { + // 检查是否已存在有效的面试邀约(排除已拒绝的) + QueryWrapper 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("】岗位收到了面试邀约。"); diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/JobServiceImpl.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/JobServiceImpl.java index dd07b8e..7399e2e 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/JobServiceImpl.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/JobServiceImpl.java @@ -1060,16 +1060,32 @@ public class JobServiceImpl extends ServiceImpl implements IJobSe private List userCollection(List 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 collectionIds = jobCollectionMapper.selectList(Wrappers.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 applyJobIds = new HashSet<>(); + List 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; diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/notice/NoticeUtils.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/notice/NoticeUtils.java index 7692a89..d1974ba 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/notice/NoticeUtils.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/notice/NoticeUtils.java @@ -34,6 +34,8 @@ public class NoticeUtils { public static final String NOTICE_TYPE_SXLX="2"; + public static final String NOTICE_TYPE_MSLX="4"; + /** * 拼装岗位 */ diff --git a/ruoyi-bussiness/src/main/resources/mapper/app/InterviewInvitationMapper.xml b/ruoyi-bussiness/src/main/resources/mapper/app/InterviewInvitationMapper.xml index d745909..59e7c90 100644 --- a/ruoyi-bussiness/src/main/resources/mapper/app/InterviewInvitationMapper.xml +++ b/ruoyi-bussiness/src/main/resources/mapper/app/InterviewInvitationMapper.xml @@ -16,6 +16,8 @@ + + @@ -25,11 +27,16 @@ + + + + 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 @@ -45,4 +52,22 @@ order by create_time desc + + + diff --git a/ruoyi-bussiness/src/main/resources/mapper/app/JobMapper.xml b/ruoyi-bussiness/src/main/resources/mapper/app/JobMapper.xml index 1fcf3d4..020f993 100644 --- a/ruoyi-bussiness/src/main/resources/mapper/app/JobMapper.xml +++ b/ruoyi-bussiness/src/main/resources/mapper/app/JobMapper.xml @@ -336,8 +336,9 @@