feat: Add SHZ log inspection and HighGo database querying capabilities

- Introduced `inspect-shz-logs` skill for inspecting local and production logs with bounded read-only commands.
- Added `view.sh` script for log viewing with options for following logs and filtering by keywords.
- Created `query-shz-highgo` skill for querying the SHZ HighGo database through an SSH gateway.
- Implemented read-only SQL execution with strict controls on allowed statements and query structure.
- Added new domain and mapper classes for managing outdoor fair job associations.
- Enhanced `OutdoorFairController` to support job listing and management for outdoor fairs.
- Created SQL scripts for initializing outdoor fair job data and managing menu visibility.
This commit is contained in:
2026-06-25 23:13:35 +08:00
parent c24f5e6621
commit 82d3264c72
18 changed files with 781 additions and 4 deletions

View File

@@ -0,0 +1,65 @@
package com.ruoyi.cms.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
/**
* 户外招聘会岗位关联表自动迁移。
*/
@Slf4j
@Component
public class OutdoorFairJobMigration
{
@Autowired
private DataSource dataSource;
@EventListener(ApplicationReadyEvent.class)
public void migrate()
{
try (Connection connection = dataSource.getConnection();
ResultSet tables = connection.getMetaData().getTables(
connection.getCatalog(), "shz", "cms_outdoor_fair_job", new String[]{"TABLE"}))
{
if (tables.next())
{
log.info("户外招聘会岗位关联表已存在");
return;
}
}
catch (Exception e)
{
log.error("检查户外招聘会岗位关联表失败: {}", e.getMessage(), e);
return;
}
try (Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement())
{
statement.execute("CREATE SEQUENCE shz.cms_outdoor_fair_job_id_seq " +
"START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1");
statement.execute("CREATE TABLE shz.cms_outdoor_fair_job (" +
"id BIGINT NOT NULL DEFAULT nextval('shz.cms_outdoor_fair_job_id_seq') PRIMARY KEY, " +
"fair_id BIGINT NOT NULL, company_id BIGINT NOT NULL, " +
"job_id BIGINT NOT NULL, create_by VARCHAR(64), create_time TIMESTAMP, " +
"update_by VARCHAR(64), update_time TIMESTAMP, remark VARCHAR(500), " +
"del_flag CHAR(1) DEFAULT '0')");
statement.execute("CREATE UNIQUE INDEX uk_cms_outdoor_fair_job " +
"ON shz.cms_outdoor_fair_job (fair_id, job_id)");
statement.execute("CREATE INDEX idx_cms_outdoor_fair_job_company " +
"ON shz.cms_outdoor_fair_job (fair_id, company_id)");
log.info("户外招聘会岗位关联表检查完成");
}
catch (Exception e)
{
log.error("户外招聘会岗位关联表迁移失败: {}", e.getMessage(), e);
}
}
}

View File

@@ -9,15 +9,19 @@ import java.util.stream.Collectors;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.cms.domain.FairCompany;
import com.ruoyi.cms.domain.Job;
import com.ruoyi.cms.domain.OutdoorFair;
import com.ruoyi.cms.domain.OutdoorFairBooth;
import com.ruoyi.cms.domain.OutdoorFairBoothBooking;
import com.ruoyi.cms.domain.OutdoorFairDictDataRequest;
import com.ruoyi.cms.domain.OutdoorFairJob;
import com.ruoyi.cms.domain.VenueInfo;
import com.ruoyi.cms.mapper.FairCompanyMapper;
import com.ruoyi.cms.mapper.OutdoorFairBoothMapper;
import com.ruoyi.cms.mapper.OutdoorFairBoothBookingMapper;
import com.ruoyi.cms.mapper.OutdoorFairJobMapper;
import com.ruoyi.cms.mapper.CompanyMapper;
import com.ruoyi.cms.service.IJobService;
import com.ruoyi.cms.service.IOutdoorFairService;
import com.ruoyi.cms.service.IVenueInfoService;
import com.ruoyi.common.annotation.Log;
@@ -72,6 +76,12 @@ public class OutdoorFairController extends BaseController
@Autowired
private OutdoorFairBoothMapper outdoorFairBoothMapper;
@Autowired
private OutdoorFairJobMapper outdoorFairJobMapper;
@Autowired
private IJobService jobService;
/**
* 查询户外招聘会列表
*/
@@ -161,7 +171,8 @@ public class OutdoorFairController extends BaseController
@ApiOperation("查询户外招聘会参会企业")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:query')")
@GetMapping("/{fairId}/companies")
public AjaxResult listCompanies(@PathVariable Long fairId, String companyName, Integer current, Integer pageSize)
public AjaxResult listCompanies(@PathVariable Long fairId, String companyName,
Integer current, Integer pageSize)
{
List<FairCompany> relations = fairCompanyMapper.selectList(new LambdaQueryWrapper<FairCompany>()
.eq(FairCompany::getJobFairId, fairId)
@@ -182,6 +193,11 @@ public class OutdoorFairController extends BaseController
.stream()
.filter(item -> item.getCompanyId() != null)
.collect(Collectors.toMap(OutdoorFairBoothBooking::getCompanyId, item -> item, (a, b) -> a));
Map<Long, Long> jobCountMap = outdoorFairJobMapper.countJobsByCompany(fairId).stream()
.collect(Collectors.toMap(
item -> Long.valueOf(String.valueOf(item.get("companyId"))),
item -> Long.valueOf(String.valueOf(item.get("jobCount"))),
(a, b) -> a));
List<Map<String, Object>> rows = new ArrayList<>();
for (FairCompany relation : relations) {
@@ -204,7 +220,7 @@ public class OutdoorFairController extends BaseController
row.put("contactPhone", company.getContactPersonPhone());
row.put("boothId", booking == null ? null : booking.getBoothId());
row.put("boothNumber", booking == null ? null : booking.getBoothNumber());
row.put("jobList", new ArrayList<>());
row.put("jobCount", jobCountMap.getOrDefault(company.getCompanyId(), 0L));
rows.add(row);
}
@@ -219,6 +235,35 @@ public class OutdoorFairController extends BaseController
return result;
}
@ApiOperation("分页查询户外招聘会参会岗位")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:query')")
@GetMapping("/{fairId}/jobs")
public AjaxResult listJobs(@PathVariable Long fairId, Long companyId, String companyName,
String jobTitle, String education, String experience,
Integer current, Integer pageSize)
{
int page = current == null || current < 1 ? 1 : current;
int size = pageSize == null || pageSize < 1 ? 10 : Math.min(pageSize, 100);
int offset = (page - 1) * size;
List<Job> rows = outdoorFairJobMapper.selectFairJobPage(
fairId, companyId, companyName, jobTitle, education, experience, offset, size);
Long total = outdoorFairJobMapper.countFairJobs(
fairId, companyId, companyName, jobTitle, education, experience);
AjaxResult result = AjaxResult.success();
result.put("rows", rows);
result.put("total", total == null ? 0 : total);
return result;
}
@ApiOperation("获取户外招聘会岗位详情")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:query')")
@GetMapping("/{fairId}/jobs/{jobId}")
public AjaxResult getCompanyJob(@PathVariable Long fairId, @PathVariable Long jobId)
{
ensureFairJobExists(fairId, jobId);
return success(jobService.selectJobByJobId(jobId));
}
@ApiOperation("新增户外招聘会参会企业")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:edit')")
@PostMapping("/{fairId}/companies")
@@ -236,11 +281,90 @@ public class OutdoorFairController extends BaseController
@ApiOperation("移除户外招聘会参会企业")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:edit')")
@DeleteMapping("/{fairId}/companies/{id}")
@Transactional(rollbackFor = Exception.class)
public AjaxResult removeCompany(@PathVariable Long fairId, @PathVariable Long id)
{
return toAjax(fairCompanyMapper.delete(new LambdaQueryWrapper<FairCompany>()
FairCompany relation = fairCompanyMapper.selectById(id);
if (relation == null || !fairId.equals(relation.getJobFairId())) {
return AjaxResult.error("参会企业不存在");
}
outdoorFairJobMapper.delete(new LambdaQueryWrapper<OutdoorFairJob>()
.eq(OutdoorFairJob::getFairId, fairId)
.eq(OutdoorFairJob::getCompanyId, relation.getCompanyId()));
return toAjax(fairCompanyMapper.deleteById(id));
}
@ApiOperation("新增户外招聘会岗位")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:edit')")
@Transactional(rollbackFor = Exception.class)
@PostMapping("/{fairId}/companies/{companyId}/jobs")
public AjaxResult addCompanyJob(@PathVariable Long fairId, @PathVariable Long companyId,
@RequestBody Job job)
{
ensureFairCompanyExists(fairId, companyId);
Company company = companyMapper.selectById(companyId);
job.setCompanyId(companyId);
job.setCompanyName(company.getName());
if (job.getIsPublish() == null) {
job.setIsPublish(1);
}
int inserted = jobService.insertJob(job);
if (inserted <= 0) {
return AjaxResult.error("岗位新增失败");
}
OutdoorFairJob relation = new OutdoorFairJob();
relation.setFairId(fairId);
relation.setCompanyId(companyId);
relation.setJobId(job.getJobId());
relation.setCreateBy(getUsername());
outdoorFairJobMapper.insert(relation);
return success(job);
}
@ApiOperation("修改户外招聘会岗位")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:edit')")
@PutMapping("/{fairId}/jobs/{jobId}")
public AjaxResult updateCompanyJob(@PathVariable Long fairId, @PathVariable Long jobId,
@RequestBody Job job)
{
ensureFairJobExists(fairId, jobId);
job.setJobId(jobId);
// 当前弹窗不修改发布状态,避免重复触发岗位发布流程。
job.setIsPublish(null);
return toAjax(jobService.updateJob(job));
}
@ApiOperation("删除户外招聘会岗位")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:edit')")
@Transactional(rollbackFor = Exception.class)
@DeleteMapping("/{fairId}/jobs/{jobId}")
public AjaxResult removeCompanyJob(@PathVariable Long fairId, @PathVariable Long jobId)
{
ensureFairJobExists(fairId, jobId);
outdoorFairJobMapper.delete(new LambdaQueryWrapper<OutdoorFairJob>()
.eq(OutdoorFairJob::getFairId, fairId)
.eq(OutdoorFairJob::getJobId, jobId));
return toAjax(jobService.deleteJobByJobIds(new Long[]{jobId}));
}
private void ensureFairCompanyExists(Long fairId, Long companyId)
{
Long count = fairCompanyMapper.selectCount(new LambdaQueryWrapper<FairCompany>()
.eq(FairCompany::getJobFairId, fairId)
.eq(FairCompany::getId, id)));
.eq(FairCompany::getCompanyId, companyId));
if (count == null || count == 0) {
throw new ServiceException("该企业未参加当前招聘会");
}
}
private void ensureFairJobExists(Long fairId, Long jobId)
{
Long count = outdoorFairJobMapper.selectCount(new LambdaQueryWrapper<OutdoorFairJob>()
.eq(OutdoorFairJob::getFairId, fairId)
.eq(OutdoorFairJob::getJobId, jobId));
if (count == null || count == 0) {
throw new ServiceException("当前招聘会不存在该岗位");
}
}
private void ensureFairCompany(Long fairId, Long companyId)

View File

@@ -0,0 +1,24 @@
package com.ruoyi.cms.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
/**
* 户外招聘会岗位关联。
*/
@Data
@TableName("shz.cms_outdoor_fair_job")
public class OutdoorFairJob extends BaseEntity
{
@TableId(value = "id", type = IdType.AUTO)
private Long id;
private Long fairId;
private Long companyId;
private Long jobId;
}

View File

@@ -0,0 +1,33 @@
package com.ruoyi.cms.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.cms.domain.Job;
import com.ruoyi.cms.domain.OutdoorFairJob;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
/**
* 户外招聘会岗位关联 Mapper。
*/
public interface OutdoorFairJobMapper extends BaseMapper<OutdoorFairJob>
{
List<Job> selectFairJobPage(@Param("fairId") Long fairId,
@Param("companyId") Long companyId,
@Param("companyName") String companyName,
@Param("jobTitle") String jobTitle,
@Param("education") String education,
@Param("experience") String experience,
@Param("offset") int offset,
@Param("pageSize") int pageSize);
Long countFairJobs(@Param("fairId") Long fairId,
@Param("companyId") Long companyId,
@Param("companyName") String companyName,
@Param("jobTitle") String jobTitle,
@Param("education") String education,
@Param("experience") String experience);
List<Map<String, Object>> countJobsByCompany(@Param("fairId") Long fairId);
}

View File

@@ -806,6 +806,7 @@ public class ESJobSearchImpl implements IESJobSearchService
@Override
public void deleteJob(Long jobId) {
ensureJobDocumentIndexExists();
LambdaEsQueryWrapper<ESJobDocument> lambdaEsQueryWrapper = new LambdaEsQueryWrapper();
lambdaEsQueryWrapper.eq(ESJobDocument::getJobId,jobId);
esJobDocumentMapper.delete(lambdaEsQueryWrapper);
@@ -847,12 +848,31 @@ public class ESJobSearchImpl implements IESJobSearchService
if(StringUtil.isEmptyOrNull(job.getCompanyNature())){
esJobDocument.setCompanyNature("6");
}
ensureJobDocumentIndexExists();
LambdaEsQueryWrapper<ESJobDocument> lambdaEsQueryWrapper = new LambdaEsQueryWrapper();
lambdaEsQueryWrapper.eq(ESJobDocument::getJobId,job.getJobId());
esJobDocumentMapper.delete(lambdaEsQueryWrapper);
esJobDocumentMapper.insert(esJobDocument);
}
/**
* 确保job_document索引存在缺失时懒创建仅建空索引与映射不重建全量数据
* process-index-mode: manual 下easy-es不会自动建索引缺失时delete/update等写操作
* 会抛 index_not_found_exception 导致业务接口500并回滚事务故在写操作前调用以自愈。
*/
private void ensureJobDocumentIndexExists() {
try {
if (!esJobDocumentMapper.existsIndex("job_document")) {
esJobDocumentMapper.createIndex();
logger.info("job_document索引不存在已自动创建");
}
} catch (Exception e) {
// 并发场景下其他请求可能已创建索引,忽略“索引已存在”类异常
logger.warn("自动创建job_document索引异常可能已被其他请求创建忽略", e);
}
}
public List<ESJobDocument> selectByIds(Long[] jobIds) {
LambdaEsQueryWrapper<ESJobDocument> wrapper = new LambdaEsQueryWrapper<>();
wrapper.in(ESJobDocument::getJobId,jobIds);

View File

@@ -40,6 +40,7 @@ public class OutdoorFairServiceImpl extends ServiceImpl<OutdoorFairMapper, Outdo
queryWrapper.eq(StringUtils.isNotBlank(outdoorFair.getFairType()), OutdoorFair::getFairType, outdoorFair.getFairType());
queryWrapper.eq(StringUtils.isNotBlank(outdoorFair.getRegion()), OutdoorFair::getRegion, outdoorFair.getRegion());
queryWrapper.eq(outdoorFair.getVenueId() != null, OutdoorFair::getVenueId, outdoorFair.getVenueId());
queryWrapper.eq(outdoorFair.getOnlineApply() != null, OutdoorFair::getOnlineApply, outdoorFair.getOnlineApply());
queryWrapper.orderByDesc(OutdoorFair::getHoldTime);
queryWrapper.orderByDesc(OutdoorFair::getCreateTime);
return list(queryWrapper);