This commit is contained in:
chenyanchang
2026-06-25 15:09:25 +08:00
38 changed files with 2392 additions and 15 deletions

10
.gitignore vendored
View File

@@ -45,4 +45,12 @@ nbdist/
!*/build/*.java
!*/build/*.html
!*/build/*.xml
.DS_Store
.DS_Store
# Local credentials for project-scoped Codex skills
.codex/skills/*/.password
# Local application runtime files
.local/
.codex/
local.sh

10
pom.xml
View File

@@ -33,6 +33,7 @@
<mybatis-plus.version>3.5.1</mybatis-plus.version>
<qiniu.version>7.19.0</qiniu.version>
<aliyun.oss.version>2.5.0</aliyun.oss.version>
<lombok.version>1.18.34</lombok.version>
<qcloud.cos.version>5.5.9</qcloud.cos.version>
</properties>
@@ -129,6 +130,13 @@
<version>${velocity.version}</version>
</dependency>
<!-- Lombok (强制升级以兼容 Java 21) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<!-- 阿里JSON解析器 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
@@ -228,7 +236,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<version>3.8.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>

View File

@@ -0,0 +1,140 @@
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.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
/**
* 面试菜单权限自动迁移
* 应用启动后自动检查并添加面试列表菜单及按钮权限到 sys_menu 和 sys_role_menu 表
*/
@Slf4j
@Component
public class InterviewMenuMigration {
@Autowired
private DataSource dataSource;
@EventListener(ApplicationReadyEvent.class)
public void migrate() {
log.info("开始检查面试菜单权限...");
try (Connection conn = dataSource.getConnection()) {
// Step 1: 查找岗位管理目录的 menu_id
Long parentId = null;
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(
"SELECT menu_id FROM sys_menu WHERE path = 'management' AND menu_type = 'M' LIMIT 1")) {
if (rs.next()) {
parentId = rs.getLong("menu_id");
}
}
if (parentId == null) {
log.warn("未找到 management 目录菜单,跳过面试菜单迁移");
return;
}
log.info("找到 management 目录菜单parent_id={}", parentId);
// Step 2: 插入面试列表菜单(幂等)
Long menuId = insertMenuIfNotExists(conn, 2100L, "面试列表", parentId, 10,
"interview-list", "Management/InterviewList", "InterviewList",
"C", "cms:interview:list", "message");
if (menuId == null) {
log.warn("面试列表菜单插入失败(可能已存在但 parent_id 不同)");
return;
}
// Step 3: 插入按钮权限
insertMenuIfNotExists(conn, 2101L, "面试查询", menuId, 1,
"", "", "", "F", "cms:interview:query", "#");
insertMenuIfNotExists(conn, 2102L, "面试新增", menuId, 2,
"", "", "", "F", "cms:interview:add", "#");
insertMenuIfNotExists(conn, 2103L, "面试修改", menuId, 3,
"", "", "", "F", "cms:interview:edit", "#");
insertMenuIfNotExists(conn, 2104L, "面试删除", menuId, 4,
"", "", "", "F", "cms:interview:remove", "#");
// Step 4: 将新菜单赋给所有非管理员角色
try (Statement stmt = conn.createStatement()) {
stmt.execute(
"INSERT INTO sys_role_menu (role_id, menu_id) " +
"SELECT r.role_id, " + menuId + " FROM sys_role r " +
"WHERE r.role_key != 'admin' " +
"AND NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = r.role_id AND rm.menu_id = " + menuId + ")");
} catch (Exception e) {
log.debug("角色菜单关联已存在或插入失败: {}", e.getMessage());
}
// 按钮也赋给角色
for (long btnId : new long[]{2101L, 2102L, 2103L, 2104L}) {
try (Statement stmt = conn.createStatement()) {
stmt.execute(
"INSERT INTO sys_role_menu (role_id, menu_id) " +
"SELECT r.role_id, " + btnId + " FROM sys_role r " +
"WHERE r.role_key != 'admin' " +
"AND NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = r.role_id AND rm.menu_id = " + btnId + ")");
} catch (Exception e) {
log.debug("按钮角色关联已存在: {}", e.getMessage());
}
}
log.info("面试菜单权限迁移完成");
} catch (Exception e) {
log.error("面试菜单迁移失败: {}", e.getMessage(), e);
}
}
/**
* 幂等插入菜单记录
*/
private Long insertMenuIfNotExists(Connection conn, Long menuId, String menuName,
Long parentId, int orderNum, String path,
String component, String routeName,
String menuType, String perms, String icon) {
try {
// 检查是否已存在
try (PreparedStatement ps = conn.prepareStatement(
"SELECT 1 FROM sys_menu WHERE menu_id = ?")) {
ps.setLong(1, menuId);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
log.debug("菜单已存在: {} (id={})", menuName, menuId);
return menuId;
}
}
}
// 插入新菜单
try (PreparedStatement ps = conn.prepareStatement(
"INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) " +
"VALUES (?, ?, ?, ?, ?, ?, '', ?, 1, 0, ?, '0', '0', ?, ?, 'admin', CURRENT_TIMESTAMP)")) {
ps.setLong(1, menuId);
ps.setString(2, menuName);
ps.setLong(3, parentId);
ps.setInt(4, orderNum);
ps.setString(5, path);
ps.setString(6, component);
ps.setString(7, routeName);
ps.setString(8, menuType);
ps.setString(9, perms);
ps.setString(10, icon);
ps.executeUpdate();
log.info("菜单创建成功: {} (id={}, perms={})", menuName, menuId, perms);
return menuId;
}
} catch (Exception e) {
log.warn("菜单创建失败: {} (id={}), error={}", menuName, menuId, e.getMessage());
return null;
}
}
}

View File

@@ -12,6 +12,8 @@ import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.util.List;
@@ -67,7 +69,32 @@ public class AppInterviewController extends BaseController {
*/
@ApiOperation("更新面试邀约状态")
@PutMapping("/status/{id}")
public AjaxResult updateStatus(@PathVariable Long id, @RequestParam String status){
public AjaxResult updateStatus(@PathVariable Long id, HttpServletRequest request) {
String status = request.getParameter("status");
// 如果 URL 参数没有,尝试从 JSON body 中读取
if (status == null || status.isEmpty()) {
try {
BufferedReader reader = request.getReader();
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
String body = sb.toString();
if (body != null && !body.isEmpty()) {
// 简单解析 JSON: {"status": "accepted"}
status = body.replaceAll(".*\"status\"\\s*:\\s*\"([^\"]+)\".*", "$1");
if (status.equals(body)) {
status = null;
}
}
} catch (Exception e) {
// ignore body read errors
}
}
if (status == null || status.isEmpty()) {
return AjaxResult.error("状态参数不能为空");
}
int rows = interviewInvitationService.updateInterviewStatus(id, status);
if (rows > 0 && "accepted".equals(status)) {
// 接受时返回面试官联系方式,供前端弹窗展示

View File

@@ -1,6 +1,7 @@
package com.ruoyi.cms.controller.cms;
import com.ruoyi.cms.domain.InterviewInvitation;
import com.ruoyi.cms.domain.vo.InterviewInvitationVO;
import com.ruoyi.cms.service.InterviewInvitationService;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
@@ -38,7 +39,7 @@ public class CmsInterviewController extends BaseController {
@GetMapping("/list")
public TableDataInfo list(InterviewInvitation interviewInvitation){
startPage();
List<InterviewInvitation> list = interviewInvitationService.getInterviewInvitationList(interviewInvitation);
List<InterviewInvitationVO> list = interviewInvitationService.getInterviewInvitationVOList(interviewInvitation);
return getDataTable(list);
}

View File

@@ -0,0 +1,134 @@
package com.ruoyi.cms.controller.cms;
import java.util.Arrays;
import java.util.List;
import com.ruoyi.cms.domain.OutdoorFair;
import com.ruoyi.cms.domain.OutdoorFairDictDataRequest;
import com.ruoyi.cms.domain.VenueInfo;
import com.ruoyi.cms.service.IOutdoorFairService;
import com.ruoyi.cms.service.IVenueInfoService;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.exception.ServiceException;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 户外招聘会Controller
*
* @author ruoyi
* @date 2026-06-25
*/
@RestController
@RequestMapping("/cms/outdoor-fair")
@Api(tags = "后台:户外招聘会")
public class OutdoorFairController extends BaseController
{
@Autowired
private IOutdoorFairService outdoorFairService;
@Autowired
private IVenueInfoService venueInfoService;
/**
* 查询户外招聘会列表
*/
@ApiOperation("查询户外招聘会列表")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:list')")
@GetMapping("/list")
public TableDataInfo list(OutdoorFair outdoorFair)
{
startPage();
List<OutdoorFair> list = outdoorFairService.selectOutdoorFairList(outdoorFair);
return getDataTable(list);
}
/**
* 获取户外招聘会详细信息
*/
@ApiOperation("获取户外招聘会详细信息")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") Long id)
{
return success(outdoorFairService.getById(id));
}
/**
* 新增户外招聘会
*/
@ApiOperation("新增户外招聘会")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:add')")
@Log(title = "户外招聘会", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody OutdoorFair outdoorFair)
{
fillVenueFields(outdoorFair);
return toAjax(outdoorFairService.save(outdoorFair));
}
/**
* 修改户外招聘会
*/
@ApiOperation("修改户外招聘会")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:edit')")
@Log(title = "户外招聘会", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody OutdoorFair outdoorFair)
{
fillVenueFields(outdoorFair);
return toAjax(outdoorFairService.updateById(outdoorFair));
}
/**
* 删除户外招聘会
*/
@ApiOperation("删除户外招聘会")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:remove')")
@Log(title = "户外招聘会", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids)
{
return toAjax(outdoorFairService.removeByIds(Arrays.asList(ids)));
}
/**
* 新增户外招聘会页面使用的字典项
*/
@ApiOperation("新增户外招聘会字典项")
@PreAuthorize("@ss.hasAnyPermi('cms:outdoorFair:add,cms:outdoorFair:edit')")
@Log(title = "户外招聘会字典项", businessType = BusinessType.INSERT)
@PostMapping("/dict-data")
public AjaxResult addDictData(@Validated @RequestBody OutdoorFairDictDataRequest request)
{
return toAjax(outdoorFairService.insertOutdoorFairDictData(request.getDictType(), request.getDictLabel(), getUsername()));
}
private void fillVenueFields(OutdoorFair outdoorFair)
{
if (outdoorFair.getVenueId() == null) {
return;
}
VenueInfo venueInfo = venueInfoService.getById(outdoorFair.getVenueId());
if (venueInfo == null) {
throw new ServiceException("绑定场地不存在");
}
outdoorFair.setVenueName(venueInfo.getVenueName());
outdoorFair.setBoothCount(venueInfo.getFloorCount() == null ? 0 : venueInfo.getFloorCount());
}
}

View File

@@ -0,0 +1,325 @@
package com.ruoyi.cms.controller.cms;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.cms.domain.VenueInfo;
import com.ruoyi.cms.domain.OutdoorFair;
import com.ruoyi.cms.domain.OutdoorFairBoothBooking;
import com.ruoyi.cms.domain.VenueBooth;
import com.ruoyi.cms.domain.VenueInfoDictDataRequest;
import com.ruoyi.cms.domain.VenueBoothSwapRequest;
import com.ruoyi.cms.mapper.CompanyMapper;
import com.ruoyi.cms.mapper.OutdoorFairBoothBookingMapper;
import com.ruoyi.cms.mapper.OutdoorFairMapper;
import com.ruoyi.cms.mapper.VenueBoothMapper;
import com.ruoyi.cms.service.IVenueInfoService;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.Company;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.StringUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 场地信息Controller
*
* @author ruoyi
* @date 2026-06-25
*/
@RestController
@RequestMapping("/cms/venue-info")
@Api(tags = "后台:场地信息维护")
public class VenueInfoController extends BaseController
{
@Autowired
private IVenueInfoService venueInfoService;
@Autowired
private VenueBoothMapper venueBoothMapper;
@Autowired
private OutdoorFairMapper outdoorFairMapper;
@Autowired
private OutdoorFairBoothBookingMapper outdoorFairBoothBookingMapper;
@Autowired
private CompanyMapper companyMapper;
/**
* 查询场地信息列表
*/
@ApiOperation("查询场地信息列表")
@PreAuthorize("@ss.hasPermi('cms:venueInfo:list')")
@GetMapping("/list")
public TableDataInfo list(VenueInfo venueInfo)
{
startPage();
List<VenueInfo> list = venueInfoService.selectVenueInfoList(venueInfo);
return getDataTable(list);
}
/**
* 获取场地信息详细信息
*/
@ApiOperation("获取场地信息详细信息")
@PreAuthorize("@ss.hasPermi('cms:venueInfo:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") Long id)
{
return success(venueInfoService.getById(id));
}
/**
* 新增场地信息
*/
@ApiOperation("新增场地信息")
@PreAuthorize("@ss.hasPermi('cms:venueInfo:add')")
@Log(title = "场地信息", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody VenueInfo venueInfo)
{
fillHandleFields(venueInfo);
return toAjax(venueInfoService.save(venueInfo));
}
/**
* 修改场地信息
*/
@ApiOperation("修改场地信息")
@PreAuthorize("@ss.hasPermi('cms:venueInfo:edit')")
@Log(title = "场地信息", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody VenueInfo venueInfo)
{
fillHandleFields(venueInfo);
return toAjax(venueInfoService.updateById(venueInfo));
}
/**
* 删除场地信息
*/
@ApiOperation("删除场地信息")
@PreAuthorize("@ss.hasPermi('cms:venueInfo:remove')")
@Log(title = "场地信息", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids)
{
return toAjax(venueInfoService.removeByIds(Arrays.asList(ids)));
}
/**
* 新增场地信息维护页面使用的字典项
*/
@ApiOperation("新增场地信息字典项")
@PreAuthorize("@ss.hasAnyPermi('cms:venueInfo:add,cms:venueInfo:edit')")
@Log(title = "场地信息字典项", businessType = BusinessType.INSERT)
@PostMapping("/dict-data")
public AjaxResult addDictData(@Validated @RequestBody VenueInfoDictDataRequest request)
{
return toAjax(venueInfoService.insertVenueInfoDictData(request.getDictType(), request.getDictLabel(), getUsername()));
}
@ApiOperation("查询场地展位图")
@PreAuthorize("@ss.hasAnyPermi('cms:venueInfo:query,cms:outdoorFair:query')")
@GetMapping("/{venueId}/booths")
public AjaxResult listBooths(@PathVariable Long venueId)
{
List<VenueBooth> list = venueBoothMapper.selectList(new LambdaQueryWrapper<VenueBooth>()
.eq(VenueBooth::getVenueId, venueId)
.orderByAsc(VenueBooth::getPositionY)
.orderByAsc(VenueBooth::getPositionX)
.orderByAsc(VenueBooth::getBoothNumber));
return success(list);
}
@ApiOperation("保存场地展位图")
@PreAuthorize("@ss.hasPermi('cms:venueInfo:edit')")
@Transactional(rollbackFor = Exception.class)
@PostMapping("/{venueId}/booths")
public AjaxResult saveBooths(@PathVariable Long venueId, @RequestBody List<VenueBooth> booths)
{
venueBoothMapper.physicalDeleteDeletedByVenueId(venueId);
List<VenueBooth> existingBooths = venueBoothMapper.selectList(new LambdaQueryWrapper<VenueBooth>()
.eq(VenueBooth::getVenueId, venueId));
Map<Long, VenueBooth> existingById = existingBooths.stream()
.filter(item -> item.getId() != null)
.collect(Collectors.toMap(VenueBooth::getId, item -> item, (a, b) -> a));
Map<String, VenueBooth> existingByNumber = existingBooths.stream()
.filter(item -> StringUtils.isNotBlank(item.getBoothNumber()))
.collect(Collectors.toMap(VenueBooth::getBoothNumber, item -> item, (a, b) -> a));
Set<String> boothNumbers = new HashSet<>();
Set<Long> savedIds = new HashSet<>();
if (booths != null) {
for (VenueBooth booth : booths) {
if (StringUtils.isBlank(booth.getBoothNumber())) {
return AjaxResult.error("展位号不能为空");
}
if (!boothNumbers.add(booth.getBoothNumber())) {
return AjaxResult.error("展位号不能重复:" + booth.getBoothNumber());
}
}
for (VenueBooth booth : booths) {
VenueBooth existing = booth.getId() == null ? null : existingById.get(booth.getId());
if (existing == null) {
existing = existingByNumber.get(booth.getBoothNumber());
}
booth.setVenueId(venueId);
if (existing == null) {
booth.setId(null);
venueBoothMapper.insert(booth);
} else {
booth.setId(existing.getId());
venueBoothMapper.updateById(booth);
}
if (booth.getId() != null) {
savedIds.add(booth.getId());
}
}
VenueInfo venueInfo = venueInfoService.getById(venueId);
if (venueInfo != null) {
venueInfo.setFloorCount(booths.size());
venueInfoService.updateById(venueInfo);
}
}
List<Long> removedIds = existingBooths.stream()
.map(VenueBooth::getId)
.filter(id -> id != null && !savedIds.contains(id))
.collect(Collectors.toList());
if (!removedIds.isEmpty()) {
venueBoothMapper.physicalDeleteByVenueIdAndIds(venueId, removedIds);
}
return success();
}
@ApiOperation("查询户外招聘会展位图")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:query')")
@GetMapping("/fair/{fairId}/booth-map")
public AjaxResult fairBoothMap(@PathVariable Long fairId)
{
OutdoorFair fair = outdoorFairMapper.selectById(fairId);
if (fair == null || fair.getVenueId() == null) {
return success(new ArrayList<>());
}
List<VenueBooth> booths = venueBoothMapper.selectList(new LambdaQueryWrapper<VenueBooth>()
.eq(VenueBooth::getVenueId, fair.getVenueId())
.orderByAsc(VenueBooth::getPositionY)
.orderByAsc(VenueBooth::getPositionX)
.orderByAsc(VenueBooth::getBoothNumber));
List<OutdoorFairBoothBooking> bookings = outdoorFairBoothBookingMapper.selectList(new LambdaQueryWrapper<OutdoorFairBoothBooking>()
.eq(OutdoorFairBoothBooking::getFairId, fairId));
Map<Long, OutdoorFairBoothBooking> bookingMap = bookings.stream().collect(Collectors.toMap(OutdoorFairBoothBooking::getBoothId, item -> item, (a, b) -> a));
List<Map<String, Object>> result = new ArrayList<>();
for (VenueBooth booth : booths) {
OutdoorFairBoothBooking booking = bookingMap.get(booth.getId());
Map<String, Object> row = new java.util.HashMap<>();
row.put("id", booth.getId());
row.put("boothId", booth.getId());
row.put("boothNumber", booth.getBoothNumber());
row.put("positionX", booth.getPositionX());
row.put("positionY", booth.getPositionY());
row.put("bookingId", booking == null ? null : booking.getId());
row.put("companyId", booking == null ? null : booking.getCompanyId());
row.put("companyName", booking == null ? null : booking.getCompanyName());
row.put("status", booking == null ? "available" : booking.getStatus());
result.add(row);
}
return success(result);
}
@ApiOperation("预定户外招聘会展位")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:edit')")
@PostMapping("/fair/{fairId}/booth-booking")
public AjaxResult bookBooth(@PathVariable Long fairId, @RequestBody OutdoorFairBoothBooking booking)
{
if (booking.getCompanyId() == null) {
return AjaxResult.error("请选择预定单位");
}
Company company = companyMapper.selectById(booking.getCompanyId());
if (company == null || company.getStatus() == null || company.getStatus() != 1) {
return AjaxResult.error("只能预定审核通过的企业");
}
VenueBooth booth = venueBoothMapper.selectById(booking.getBoothId());
booking.setFairId(fairId);
booking.setBoothNumber(booth == null ? booking.getBoothNumber() : booth.getBoothNumber());
booking.setCompanyName(company.getName());
booking.setStatus(StringUtils.isBlank(booking.getStatus()) ? "reserved" : booking.getStatus());
OutdoorFairBoothBooking existing = outdoorFairBoothBookingMapper.selectOne(new LambdaQueryWrapper<OutdoorFairBoothBooking>()
.eq(OutdoorFairBoothBooking::getFairId, fairId)
.eq(OutdoorFairBoothBooking::getBoothId, booking.getBoothId())
.last("limit 1"));
if (existing == null) {
return toAjax(outdoorFairBoothBookingMapper.insert(booking));
}
booking.setId(existing.getId());
return toAjax(outdoorFairBoothBookingMapper.updateById(booking));
}
@ApiOperation("取消户外招聘会展位预定")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:edit')")
@DeleteMapping("/fair/{fairId}/booth-booking/{boothId}")
public AjaxResult cancelBooth(@PathVariable Long fairId, @PathVariable Long boothId)
{
return toAjax(outdoorFairBoothBookingMapper.delete(new LambdaQueryWrapper<OutdoorFairBoothBooking>()
.eq(OutdoorFairBoothBooking::getFairId, fairId)
.eq(OutdoorFairBoothBooking::getBoothId, boothId)));
}
@ApiOperation("调换户外招聘会展位")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:edit')")
@PostMapping("/fair/{fairId}/booth-swap")
public AjaxResult swapBooth(@PathVariable Long fairId, @RequestBody VenueBoothSwapRequest request)
{
OutdoorFairBoothBooking first = outdoorFairBoothBookingMapper.selectOne(new LambdaQueryWrapper<OutdoorFairBoothBooking>()
.eq(OutdoorFairBoothBooking::getFairId, fairId)
.eq(OutdoorFairBoothBooking::getBoothId, request.getFirstBoothId()).last("limit 1"));
OutdoorFairBoothBooking second = outdoorFairBoothBookingMapper.selectOne(new LambdaQueryWrapper<OutdoorFairBoothBooking>()
.eq(OutdoorFairBoothBooking::getFairId, fairId)
.eq(OutdoorFairBoothBooking::getBoothId, request.getSecondBoothId()).last("limit 1"));
if (first == null || second == null) {
return AjaxResult.error("请选择两个已预定展位");
}
Long companyId = first.getCompanyId();
String companyName = first.getCompanyName();
first.setCompanyId(second.getCompanyId());
first.setCompanyName(second.getCompanyName());
second.setCompanyId(companyId);
second.setCompanyName(companyName);
outdoorFairBoothBookingMapper.updateById(first);
outdoorFairBoothBookingMapper.updateById(second);
return success();
}
private void fillHandleFields(VenueInfo venueInfo)
{
if (venueInfo.getHandleDate() == null) {
venueInfo.setHandleDate(new Date());
}
if (StringUtils.isBlank(venueInfo.getHandler())) {
venueInfo.setHandler(getUsername());
}
}
}

View File

@@ -77,6 +77,11 @@ public class InterviewInvitation extends BaseEntity {
*/
@ApiModelProperty("面试官联系方式")
private String contactPhone;
/**
* 面试官姓名
*/
@ApiModelProperty("面试官姓名")
private String interviewerName;
/**
* 公司名称冗余存储创建时从company表获取
*/

View File

@@ -0,0 +1,91 @@
package com.ruoyi.cms.domain;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 户外招聘会对象 cms_outdoor_fair
*
* @author ruoyi
* @date 2026-06-25
*/
@Data
@ApiModel("户外招聘会")
@TableName("cms_outdoor_fair")
public class OutdoorFair extends BaseEntity
{
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
@ApiModelProperty("主键ID")
private Long id;
@Excel(name = "招聘会标题")
@ApiModelProperty("招聘会标题")
private String title;
@Excel(name = "举办单位")
@ApiModelProperty("举办单位")
private String hostUnit;
@Excel(name = "招聘会类型")
@ApiModelProperty("招聘会类型")
private String fairType;
@Excel(name = "举办区域")
@ApiModelProperty("举办区域")
private String region;
@Excel(name = "绑定场地ID")
@ApiModelProperty("绑定场地ID")
private Long venueId;
@Excel(name = "绑定场地")
@ApiModelProperty("绑定场地")
private String venueName;
@Excel(name = "举办地址")
@ApiModelProperty("举办地址")
private String address;
@Excel(name = "展位数量")
@ApiModelProperty("展位数量")
private Integer boothCount;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "举办时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty("举办时间")
private Date holdTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "截止时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty("截止时间")
private Date endTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "开放申请时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty("开放申请时间")
private Date applyStartTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "截止申请时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty("截止申请时间")
private Date applyEndTime;
@Excel(name = "是否开启线上申请")
@ApiModelProperty("是否开启线上申请")
private Boolean onlineApply;
@Excel(name = "招聘会照片")
@ApiModelProperty("招聘会照片")
private String photoUrl;
}

View File

@@ -0,0 +1,38 @@
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 io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("户外招聘会展位预定")
@TableName("cms_outdoor_fair_booth_booking")
public class OutdoorFairBoothBooking extends BaseEntity
{
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@ApiModelProperty("招聘会ID")
private Long fairId;
@ApiModelProperty("场地展位ID")
private Long boothId;
@ApiModelProperty("展位号")
private String boothNumber;
@ApiModelProperty("企业ID")
private Long companyId;
@ApiModelProperty("企业名称")
private String companyName;
@ApiModelProperty("预定状态")
private String status;
}

View File

@@ -0,0 +1,26 @@
package com.ruoyi.cms.domain;
import javax.validation.constraints.NotBlank;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 户外招聘会字典新增请求
*
* @author ruoyi
* @date 2026-06-25
*/
@Data
@ApiModel("户外招聘会字典新增请求")
public class OutdoorFairDictDataRequest
{
@NotBlank(message = "字典类型不能为空")
@ApiModelProperty(value = "字典类型", required = true)
private String dictType;
@NotBlank(message = "字典标签不能为空")
@ApiModelProperty(value = "字典标签", required = true)
private String dictLabel;
}

View File

@@ -0,0 +1,32 @@
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 io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("场地展位")
@TableName("cms_venue_booth")
public class VenueBooth extends BaseEntity
{
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@ApiModelProperty("场地ID")
private Long venueId;
@ApiModelProperty("展位号")
private String boothNumber;
@ApiModelProperty("X坐标")
private Integer positionX;
@ApiModelProperty("Y坐标")
private Integer positionY;
}

View File

@@ -0,0 +1,10 @@
package com.ruoyi.cms.domain;
import lombok.Data;
@Data
public class VenueBoothSwapRequest
{
private Long firstBoothId;
private Long secondBoothId;
}

View File

@@ -0,0 +1,77 @@
package com.ruoyi.cms.domain;
import java.math.BigDecimal;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 场地信息对象 cms_venue_info
*
* @author ruoyi
* @date 2026-06-25
*/
@Data
@ApiModel("场地信息")
@TableName("cms_venue_info")
public class VenueInfo extends BaseEntity
{
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
@ApiModelProperty("主键ID")
private Long id;
@Excel(name = "场地类型")
@ApiModelProperty("场地类型")
private String venueType;
@Excel(name = "场地名称")
@ApiModelProperty("场地名称")
private String venueName;
@Excel(name = "场地面积")
@ApiModelProperty("场地面积")
private BigDecimal venueArea;
@Excel(name = "展位数量")
@ApiModelProperty("展位数量")
private Integer floorCount;
@Excel(name = "场地地址")
@ApiModelProperty("场地地址")
private String venueAddress;
@Excel(name = "联系电话")
@ApiModelProperty("联系电话")
private String contactPhone;
@Excel(name = "乘坐线路ID")
@ApiModelProperty("乘坐线路ID多个用英文逗号分隔")
private String routeLineIds;
@Excel(name = "乘坐线路")
@ApiModelProperty("乘坐线路名称,多个用英文逗号分隔")
private String routeLineNames;
@JsonFormat(pattern = "yyyy-MM-dd")
@Excel(name = "经办日期", width = 30, dateFormat = "yyyy-MM-dd")
@ApiModelProperty("经办日期")
private Date handleDate;
@Excel(name = "经办机构")
@ApiModelProperty("经办机构")
private String handleOrg;
@Excel(name = "经办人")
@ApiModelProperty("经办人")
private String handler;
}

View File

@@ -0,0 +1,26 @@
package com.ruoyi.cms.domain;
import javax.validation.constraints.NotBlank;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 场地信息字典新增请求
*
* @author ruoyi
* @date 2026-06-25
*/
@Data
@ApiModel("场地信息字典新增请求")
public class VenueInfoDictDataRequest
{
@NotBlank(message = "字典类型不能为空")
@ApiModelProperty(value = "字典类型", required = true)
private String dictType;
@NotBlank(message = "字典标签不能为空")
@ApiModelProperty(value = "字典标签", required = true)
private String dictLabel;
}

View File

@@ -18,4 +18,6 @@ public class CandidateVO extends AppUser {
private String companyName;
@Excel(name = "岗位名称", sort = 1)
private String jobName;
private String interviewStatus;
private Long interviewId;
}

View File

@@ -11,4 +11,10 @@ public class InterviewInvitationVO extends InterviewInvitation {
@ApiModelProperty("岗位名称(来自关联查询的实时数据)")
private String jobTitle;
@ApiModelProperty("求职者姓名(来自关联查询的实时数据)")
private String applicantName;
@ApiModelProperty("求职者联系方式(来自关联查询的实时数据)")
private String applicantPhone;
}

View File

@@ -0,0 +1,8 @@
package com.ruoyi.cms.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.cms.domain.OutdoorFairBoothBooking;
public interface OutdoorFairBoothBookingMapper extends BaseMapper<OutdoorFairBoothBooking>
{
}

View File

@@ -0,0 +1,53 @@
package com.ruoyi.cms.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.cms.domain.OutdoorFair;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/**
* 户外招聘会Mapper接口
*
* @author ruoyi
* @date 2026-06-25
*/
public interface OutdoorFairMapper extends BaseMapper<OutdoorFair>
{
/**
* 查询字典项是否存在
*
* @param dictType 字典类型
* @param dictValue 字典值
* @return 存在数量
*/
@Select("select count(1) from sys_dict_data where dict_type = #{dictType} and dict_value = #{dictValue}")
int countDictDataByTypeAndValue(@Param("dictType") String dictType, @Param("dictValue") String dictValue);
/**
* 获取字典下一个排序
*
* @param dictType 字典类型
* @return 下一个排序值
*/
@Select("select coalesce(max(dict_sort), 0) + 1 from sys_dict_data where dict_type = #{dictType}")
Long selectNextDictSort(@Param("dictType") String dictType);
/**
* 新增系统字典项
*
* @param dictSort 字典排序
* @param dictLabel 字典标签
* @param dictValue 字典值
* @param dictType 字典类型
* @param createBy 创建人
* @return 影响行数
*/
@Insert("insert into sys_dict_data (dict_code, dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) " +
"values ((select coalesce(max(dict_code), 0) + 1 from sys_dict_data), #{dictSort}, #{dictLabel}, #{dictValue}, #{dictType}, 'default', 'N', '0', #{createBy}, sysdate(), '户外招聘会页面新增')")
int insertDictData(@Param("dictSort") Long dictSort,
@Param("dictLabel") String dictLabel,
@Param("dictValue") String dictValue,
@Param("dictType") String dictType,
@Param("createBy") String createBy);
}

View File

@@ -0,0 +1,24 @@
package com.ruoyi.cms.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.cms.domain.VenueBooth;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface VenueBoothMapper extends BaseMapper<VenueBooth>
{
@Delete("DELETE FROM cms_venue_booth WHERE venue_id = #{venueId} AND del_flag <> '0'")
int physicalDeleteDeletedByVenueId(@Param("venueId") Long venueId);
@Delete({
"<script>",
"DELETE FROM cms_venue_booth WHERE venue_id = #{venueId} AND id IN",
"<foreach collection='ids' item='id' open='(' separator=',' close=')'>",
"#{id}",
"</foreach>",
"</script>"
})
int physicalDeleteByVenueIdAndIds(@Param("venueId") Long venueId, @Param("ids") List<Long> ids);
}

View File

@@ -0,0 +1,53 @@
package com.ruoyi.cms.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.cms.domain.VenueInfo;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/**
* 场地信息Mapper接口
*
* @author ruoyi
* @date 2026-06-25
*/
public interface VenueInfoMapper extends BaseMapper<VenueInfo>
{
/**
* 查询字典项是否存在
*
* @param dictType 字典类型
* @param dictValue 字典值
* @return 存在数量
*/
@Select("select count(1) from sys_dict_data where dict_type = #{dictType} and dict_value = #{dictValue}")
int countDictDataByTypeAndValue(@Param("dictType") String dictType, @Param("dictValue") String dictValue);
/**
* 获取字典下一个排序
*
* @param dictType 字典类型
* @return 下一个排序值
*/
@Select("select coalesce(max(dict_sort), 0) + 1 from sys_dict_data where dict_type = #{dictType}")
Long selectNextDictSort(@Param("dictType") String dictType);
/**
* 新增系统字典项
*
* @param dictSort 字典排序
* @param dictLabel 字典标签
* @param dictValue 字典值
* @param dictType 字典类型
* @param createBy 创建人
* @return 影响行数
*/
@Insert("insert into sys_dict_data (dict_code, dict_sort, dict_label, dict_value, dict_type, list_class, is_default, status, create_by, create_time, remark) " +
"values ((select coalesce(max(dict_code), 0) + 1 from sys_dict_data), #{dictSort}, #{dictLabel}, #{dictValue}, #{dictType}, 'default', 'N', '0', #{createBy}, sysdate(), '场地信息维护页面新增')")
int insertDictData(@Param("dictSort") Long dictSort,
@Param("dictLabel") String dictLabel,
@Param("dictValue") String dictValue,
@Param("dictType") String dictType,
@Param("createBy") String createBy);
}

View File

@@ -0,0 +1,33 @@
package com.ruoyi.cms.service;
import java.util.List;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.cms.domain.OutdoorFair;
/**
* 户外招聘会Service接口
*
* @author ruoyi
* @date 2026-06-25
*/
public interface IOutdoorFairService extends IService<OutdoorFair>
{
/**
* 查询户外招聘会列表
*
* @param outdoorFair 户外招聘会
* @return 户外招聘会集合
*/
List<OutdoorFair> selectOutdoorFairList(OutdoorFair outdoorFair);
/**
* 新增户外招聘会相关字典项
*
* @param dictType 字典类型
* @param dictLabel 字典标签
* @param createBy 创建人
* @return 影响行数
*/
int insertOutdoorFairDictData(String dictType, String dictLabel, String createBy);
}

View File

@@ -0,0 +1,33 @@
package com.ruoyi.cms.service;
import java.util.List;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.cms.domain.VenueInfo;
/**
* 场地信息Service接口
*
* @author ruoyi
* @date 2026-06-25
*/
public interface IVenueInfoService extends IService<VenueInfo>
{
/**
* 查询场地信息列表
*
* @param venueInfo 场地信息
* @return 场地信息集合
*/
List<VenueInfo> selectVenueInfoList(VenueInfo venueInfo);
/**
* 新增场地信息相关字典项
*
* @param dictType 字典类型
* @param dictLabel 字典标签
* @param createBy 创建人
* @return 影响行数
*/
int insertVenueInfoDictData(String dictType, String dictLabel, String createBy);
}

View File

@@ -72,7 +72,7 @@ public class ESJobSearchImpl implements IESJobSearchService
/**
* 项目启动时,初始化索引及数据
*/
@PostConstruct
// @PostConstruct
public void init()
{
boolean isLockAcquired = false;

View File

@@ -17,12 +17,14 @@ 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 org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import java.util.List;
@@ -43,8 +45,9 @@ public class InterviewInvitationServiceImpl implements InterviewInvitationServic
@Autowired
private DataSource dataSource;
@PostConstruct
@EventListener(ApplicationReadyEvent.class)
public void initTable() {
log.info("开始检查并初始化 interview_invitation 表...");
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement()) {
// Check if id column has auto-increment (BIGSERIAL or sequence default)
@@ -80,6 +83,7 @@ public class InterviewInvitationServiceImpl implements InterviewInvitationServic
+ "meeting_password varchar(100) NULL,"
+ "interview_location varchar(500) NULL,"
+ "contact_phone varchar(50) NULL,"
+ "interviewer_name varchar(100) NULL,"
+ "company_name varchar(200) NULL,"
+ "job_name varchar(200) NULL,"
+ "status varchar(20) DEFAULT 'pending' NULL,"
@@ -96,9 +100,16 @@ public class InterviewInvitationServiceImpl implements InterviewInvitationServic
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) {}
// 为已有数据库添加新列(幂等操作,使用 DO 块兼容不支持 IF NOT EXISTS 的数据库版本
try {
stmt.execute("DO $$ BEGIN ALTER TABLE interview_invitation ADD COLUMN company_name varchar(200) NULL; EXCEPTION WHEN duplicate_column THEN END; $$;");
} catch (Exception e) { log.warn("添加 company_name 列失败: {}", e.getMessage()); }
try {
stmt.execute("DO $$ BEGIN ALTER TABLE interview_invitation ADD COLUMN job_name varchar(200) NULL; EXCEPTION WHEN duplicate_column THEN END; $$;");
} catch (Exception e) { log.warn("添加 job_name 列失败: {}", e.getMessage()); }
try {
stmt.execute("DO $$ BEGIN ALTER TABLE interview_invitation ADD COLUMN interviewer_name varchar(100) NULL; EXCEPTION WHEN duplicate_column THEN END; $$;");
} catch (Exception e) { log.warn("添加 interviewer_name 列失败: {}", e.getMessage()); }
}
} catch (Exception e) {
log.error("初始化表失败: {}", e.getMessage());

View File

@@ -0,0 +1,79 @@
package com.ruoyi.cms.service.impl;
import java.util.List;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.cms.domain.OutdoorFair;
import com.ruoyi.cms.mapper.OutdoorFairMapper;
import com.ruoyi.cms.service.IOutdoorFairService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DictUtils;
import com.ruoyi.common.utils.StringUtils;
import org.springframework.stereotype.Service;
/**
* 户外招聘会Service业务层处理
*
* @author ruoyi
* @date 2026-06-25
*/
@Service
public class OutdoorFairServiceImpl extends ServiceImpl<OutdoorFairMapper, OutdoorFair> implements IOutdoorFairService
{
private static final String OUTDOOR_FAIR_TYPE_DICT = "outdoor_fair_type";
private static final String OUTDOOR_FAIR_REGION_DICT = "outdoor_fair_region";
/**
* 查询户外招聘会列表
*
* @param outdoorFair 户外招聘会
* @return 户外招聘会集合
*/
@Override
public List<OutdoorFair> selectOutdoorFairList(OutdoorFair outdoorFair)
{
LambdaQueryWrapper<OutdoorFair> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(StringUtils.isNotBlank(outdoorFair.getTitle()), OutdoorFair::getTitle, outdoorFair.getTitle());
queryWrapper.like(StringUtils.isNotBlank(outdoorFair.getHostUnit()), OutdoorFair::getHostUnit, outdoorFair.getHostUnit());
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.orderByDesc(OutdoorFair::getHoldTime);
queryWrapper.orderByDesc(OutdoorFair::getCreateTime);
return list(queryWrapper);
}
/**
* 新增户外招聘会相关字典项
*
* @param dictType 字典类型
* @param dictLabel 字典标签
* @param createBy 创建人
* @return 影响行数
*/
@Override
public int insertOutdoorFairDictData(String dictType, String dictLabel, String createBy)
{
if (!OUTDOOR_FAIR_TYPE_DICT.equals(dictType) && !OUTDOOR_FAIR_REGION_DICT.equals(dictType)) {
throw new ServiceException("只允许维护户外招聘会类型和举办区域字典");
}
String trimmedLabel = StringUtils.trim(dictLabel);
if (StringUtils.isBlank(trimmedLabel)) {
throw new ServiceException("字典名称不能为空");
}
if (trimmedLabel.length() > 100) {
throw new ServiceException("字典名称长度不能超过100个字符");
}
if (baseMapper.countDictDataByTypeAndValue(dictType, trimmedLabel) > 0) {
return 1;
}
Long dictSort = baseMapper.selectNextDictSort(dictType);
int rows = baseMapper.insertDictData(dictSort, trimmedLabel, trimmedLabel, dictType, createBy);
if (rows > 0) {
DictUtils.removeDictCache(dictType);
}
return rows;
}
}

View File

@@ -0,0 +1,74 @@
package com.ruoyi.cms.service.impl;
import java.util.List;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.cms.domain.VenueInfo;
import com.ruoyi.cms.mapper.VenueInfoMapper;
import com.ruoyi.cms.service.IVenueInfoService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DictUtils;
import com.ruoyi.common.utils.StringUtils;
import org.springframework.stereotype.Service;
/**
* 场地信息Service业务层处理
*
* @author ruoyi
* @date 2026-06-25
*/
@Service
public class VenueInfoServiceImpl extends ServiceImpl<VenueInfoMapper, VenueInfo> implements IVenueInfoService
{
private static final String VENUE_TYPE_DICT = "venue_info_type";
/**
* 查询场地信息列表
*
* @param venueInfo 场地信息
* @return 场地信息集合
*/
@Override
public List<VenueInfo> selectVenueInfoList(VenueInfo venueInfo)
{
LambdaQueryWrapper<VenueInfo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(StringUtils.isNotBlank(venueInfo.getVenueType()), VenueInfo::getVenueType, venueInfo.getVenueType());
queryWrapper.like(StringUtils.isNotBlank(venueInfo.getVenueName()), VenueInfo::getVenueName, venueInfo.getVenueName());
queryWrapper.like(StringUtils.isNotBlank(venueInfo.getVenueAddress()), VenueInfo::getVenueAddress, venueInfo.getVenueAddress());
queryWrapper.orderByDesc(VenueInfo::getCreateTime);
return list(queryWrapper);
}
/**
* 新增场地信息相关字典项
*
* @param dictType 字典类型
* @param dictLabel 字典标签
* @param createBy 创建人
* @return 影响行数
*/
@Override
public int insertVenueInfoDictData(String dictType, String dictLabel, String createBy)
{
if (!VENUE_TYPE_DICT.equals(dictType)) {
throw new ServiceException("只允许维护场地类型字典");
}
String trimmedLabel = StringUtils.trim(dictLabel);
if (StringUtils.isBlank(trimmedLabel)) {
throw new ServiceException("字典名称不能为空");
}
if (trimmedLabel.length() > 100) {
throw new ServiceException("字典名称长度不能超过100个字符");
}
if (baseMapper.countDictDataByTypeAndValue(dictType, trimmedLabel) > 0) {
return 1;
}
Long dictSort = baseMapper.selectNextDictSort(dictType);
int rows = baseMapper.insertDictData(dictSort, trimmedLabel, trimmedLabel, dictType, createBy);
if (rows > 0) {
DictUtils.removeDictCache(dictType);
}
return rows;
}
}

View File

@@ -16,6 +16,7 @@
<result property="meetingPassword" column="meeting_password"/>
<result property="interviewLocation" column="interview_location"/>
<result property="contactPhone" column="contact_phone"/>
<result property="interviewerName" column="interviewer_name"/>
<result property="companyName" column="company_name"/>
<result property="jobName" column="job_name"/>
<result property="status" column="status"/>
@@ -30,12 +31,14 @@
<!-- VO 带公司名和岗位名继承基础映射jobTitle 来自关联查询的实时数据) -->
<resultMap type="com.ruoyi.cms.domain.vo.InterviewInvitationVO" id="InterviewInvitationVOResult" extends="InterviewInvitationResult">
<result property="jobTitle" column="job_title"/>
<result property="applicantName" column="applicant_name"/>
<result property="applicantPhone" column="applicant_phone"/>
</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, company_name, job_name, status, del_flag,
contact_phone, interviewer_name, company_name, job_name, status, del_flag,
create_by, create_time, update_by, update_time, remark
from interview_invitation
</sql>
@@ -53,13 +56,22 @@
</select>
<!-- 带公司名和岗位名的列表查询COALESCE 优先用关联表实时数据,兜底用冗余存储 -->
<!-- 注意:不用 t.* 避免 company_name 列名冲突,显式列出除 company_name 外的所有列 -->
<select id="getInterviewInvitationVOList" resultMap="InterviewInvitationVOResult" parameterType="InterviewInvitation">
select t.*,
select t.id, t.company_id, t.job_id, t.user_id, t.apply_id,
t.interview_time, t.interview_method, t.meeting_link,
t.meeting_password, t.interview_location,
t.contact_phone, t.interviewer_name,
t.job_name, t.status, t.del_flag,
t.create_by, t.create_time, t.update_by, t.update_time, t.remark,
COALESCE(c.name, t.company_name) as company_name,
COALESCE(j.job_title, t.job_name) as job_title
COALESCE(j.job_title, t.job_name) as job_title,
u.name as applicant_name,
u.phone as applicant_phone
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'
left join app_user u on u.user_id = t.user_id and u.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>

View File

@@ -42,10 +42,26 @@
ORDER BY ja.create_time DESC
</select>
<select id="candidates" resultType="com.ruoyi.cms.domain.vo.CandidateVO">
SELECT au.*,jc.create_time as apply_date,jc.matching_degree,jc.id as applyId
SELECT au.*, jc.create_time as apply_date, jc.matching_degree, jc.id as applyId,
latest_ii.status as interviewStatus, latest_ii.id as interviewId
from job_apply as jc
inner join app_user as au on jc.user_id = au.user_id
left join (
select ii1.*
from interview_invitation ii1
inner join (
select user_id, job_id, max(create_time) as max_ct
from interview_invitation
where del_flag = '0'
group by user_id, job_id
) ii2 on ii1.user_id = ii2.user_id
and ii1.job_id = ii2.job_id
and ii1.create_time = ii2.max_ct
) latest_ii on latest_ii.user_id = jc.user_id
and latest_ii.job_id = jc.job_id
and latest_ii.del_flag = '0'
where jc.job_id = #{jobId} and jc.del_flag = '0' and au.del_flag ='0'
order by jc.create_time desc
</select>
<select id="trendChart" resultType="java.util.HashMap" parameterType="JobApply">
@@ -104,10 +120,26 @@
</select>
<select id="selectApplyJobUserList" parameterType="com.ruoyi.common.core.domain.entity.AppUser" resultType="com.ruoyi.cms.domain.vo.CandidateVO">
select a.id applyId, a.hire, b.job_title jobName,l.name companyName,e.* from job_apply a
select a.id applyId, a.hire, b.job_title jobName, l.name companyName, e.*,
latest_ii.status as interviewStatus, latest_ii.id as interviewId
from job_apply a
INNER join job b on a.job_id=b.job_id and b.del_flag='0'
INNER join app_user e on a.user_id =e.user_id and e.del_flag='0'
INNER join company l on b.company_id =l.company_id and l.del_flag='0'
left join (
select ii1.*
from interview_invitation ii1
inner join (
select user_id, job_id, max(create_time) as max_ct
from interview_invitation
where del_flag = '0'
group by user_id, job_id
) ii2 on ii1.user_id = ii2.user_id
and ii1.job_id = ii2.job_id
and ii1.create_time = ii2.max_ct
) latest_ii on latest_ii.user_id = a.user_id
and latest_ii.job_id = a.job_id
and latest_ii.del_flag = '0'
where a.del_flag='0'
<if test="company != null and company.jobTitle != null and company.jobTitle != ''"> and b.job_title like concat('%', cast(#{company.jobTitle, jdbcType=VARCHAR} as varchar), '%')</if>
<if test="company != null and company.education != null and company.education != ''"> and b.education = #{company.education}</if>

58
sql/cms_outdoor_fair.sql Normal file
View File

@@ -0,0 +1,58 @@
-- 户外招聘会表
-- 瀚高数据库 / PostgreSQL 兼容
CREATE SEQUENCE IF NOT EXISTS "cms_outdoor_fair_id_seq"
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
CREATE TABLE IF NOT EXISTS "cms_outdoor_fair" (
"id" BIGINT NOT NULL DEFAULT nextval('"cms_outdoor_fair_id_seq"') PRIMARY KEY,
"title" VARCHAR(200) NOT NULL,
"host_unit" VARCHAR(200) NOT NULL,
"fair_type" VARCHAR(50) NOT NULL,
"region" VARCHAR(100) NOT NULL,
"venue_id" BIGINT,
"venue_name" VARCHAR(200),
"address" VARCHAR(500) NOT NULL,
"booth_count" INTEGER NOT NULL DEFAULT 0,
"hold_time" TIMESTAMP(0) NOT NULL,
"end_time" TIMESTAMP(0) NOT NULL,
"apply_start_time" TIMESTAMP(0) NOT NULL,
"apply_end_time" TIMESTAMP(0) NOT NULL,
"online_apply" BOOLEAN NOT NULL DEFAULT FALSE,
"photo_url" VARCHAR(500),
"create_by" VARCHAR(64) DEFAULT '',
"create_time" TIMESTAMP(0),
"update_by" VARCHAR(64) DEFAULT '',
"update_time" TIMESTAMP(0),
"remark" VARCHAR(500),
"del_flag" CHAR(1) DEFAULT '0'
);
COMMENT ON TABLE "cms_outdoor_fair" IS '户外招聘会表';
COMMENT ON COLUMN "cms_outdoor_fair"."id" IS '主键ID';
COMMENT ON COLUMN "cms_outdoor_fair"."title" IS '招聘会标题';
COMMENT ON COLUMN "cms_outdoor_fair"."host_unit" IS '举办单位';
COMMENT ON COLUMN "cms_outdoor_fair"."fair_type" IS '招聘会类型';
COMMENT ON COLUMN "cms_outdoor_fair"."region" IS '举办区域';
COMMENT ON COLUMN "cms_outdoor_fair"."venue_id" IS '绑定场地ID';
COMMENT ON COLUMN "cms_outdoor_fair"."venue_name" IS '绑定场地';
COMMENT ON COLUMN "cms_outdoor_fair"."address" IS '举办地址';
COMMENT ON COLUMN "cms_outdoor_fair"."booth_count" IS '展位数量';
COMMENT ON COLUMN "cms_outdoor_fair"."hold_time" IS '举办时间';
COMMENT ON COLUMN "cms_outdoor_fair"."end_time" IS '截止时间';
COMMENT ON COLUMN "cms_outdoor_fair"."apply_start_time" IS '开放申请时间';
COMMENT ON COLUMN "cms_outdoor_fair"."apply_end_time" IS '截止申请时间';
COMMENT ON COLUMN "cms_outdoor_fair"."online_apply" IS '是否开启线上申请';
COMMENT ON COLUMN "cms_outdoor_fair"."photo_url" IS '招聘会照片';
COMMENT ON COLUMN "cms_outdoor_fair"."del_flag" IS '删除标志0代表存在 2代表删除';
CREATE INDEX IF NOT EXISTS "idx_cms_outdoor_fair_title" ON "cms_outdoor_fair" ("title");
CREATE INDEX IF NOT EXISTS "idx_cms_outdoor_fair_host_unit" ON "cms_outdoor_fair" ("host_unit");
CREATE INDEX IF NOT EXISTS "idx_cms_outdoor_fair_type" ON "cms_outdoor_fair" ("fair_type");
CREATE INDEX IF NOT EXISTS "idx_cms_outdoor_fair_venue_id" ON "cms_outdoor_fair" ("venue_id");
CREATE INDEX IF NOT EXISTS "idx_cms_outdoor_fair_hold_time" ON "cms_outdoor_fair" ("hold_time");
CREATE INDEX IF NOT EXISTS "idx_cms_outdoor_fair_del_flag" ON "cms_outdoor_fair" ("del_flag");

View File

@@ -0,0 +1,20 @@
-- 户外招聘会绑定场地字段
-- 瀚高数据库 / PostgreSQL 兼容
-- 可重复执行。
ALTER TABLE "cms_outdoor_fair"
ADD COLUMN IF NOT EXISTS "venue_id" BIGINT;
ALTER TABLE "cms_outdoor_fair"
ADD COLUMN IF NOT EXISTS "venue_name" VARCHAR(200);
COMMENT ON COLUMN "cms_outdoor_fair"."venue_id" IS '绑定场地ID';
COMMENT ON COLUMN "cms_outdoor_fair"."venue_name" IS '绑定场地';
CREATE INDEX IF NOT EXISTS "idx_cms_outdoor_fair_venue_id" ON "cms_outdoor_fair" ("venue_id");
UPDATE "cms_outdoor_fair" fair
SET "venue_name" = venue."venue_name",
"booth_count" = COALESCE(venue."floor_count", 0)
FROM "cms_venue_info" venue
WHERE fair."venue_id" = venue."id";

View File

@@ -0,0 +1,202 @@
-- 户外招聘会石河子 mock 数据
-- 可重复执行:按 title 去重,已存在同名招聘会时不会重复插入。
INSERT INTO "cms_outdoor_fair" (
"title", "host_unit", "fair_type", "region", "address", "booth_count",
"hold_time", "end_time", "apply_start_time", "apply_end_time",
"online_apply", "photo_url", "create_by", "create_time", "update_by", "update_time", "del_flag"
)
SELECT
'2026年石河子市春季户外综合招聘会',
'第八师石河子市人力资源和社会保障局',
'综合类',
'第八师(石河子市)',
'石河子市北三路石河子大学中区图书馆前广场',
80,
TIMESTAMP '2026-03-15 09:00:00',
TIMESTAMP '2026-03-15 17:00:00',
TIMESTAMP '2026-03-01 00:00:00',
TIMESTAMP '2026-03-10 23:59:59',
TRUE,
'https://picsum.photos/seed/shihezi-outdoor-1/400/300',
'system',
NOW(),
'system',
NOW(),
'0'
WHERE NOT EXISTS (SELECT 1 FROM "cms_outdoor_fair" WHERE "title" = '2026年石河子市春季户外综合招聘会');
INSERT INTO "cms_outdoor_fair" (
"title", "host_unit", "fair_type", "region", "address", "booth_count",
"hold_time", "end_time", "apply_start_time", "apply_end_time",
"online_apply", "photo_url", "create_by", "create_time", "update_by", "update_time", "del_flag"
)
SELECT
'2026年石河子大学毕业生户外专场招聘会',
'石河子大学就业指导中心',
'校园招聘',
'第八师(石河子市)',
'石河子大学中区主楼前广场',
60,
TIMESTAMP '2026-04-18 09:30:00',
TIMESTAMP '2026-04-18 16:30:00',
TIMESTAMP '2026-04-01 00:00:00',
TIMESTAMP '2026-04-12 23:59:59',
TRUE,
'https://picsum.photos/seed/shihezi-outdoor-2/400/300',
'system',
NOW(),
'system',
NOW(),
'0'
WHERE NOT EXISTS (SELECT 1 FROM "cms_outdoor_fair" WHERE "title" = '2026年石河子大学毕业生户外专场招聘会');
INSERT INTO "cms_outdoor_fair" (
"title", "host_unit", "fair_type", "region", "address", "booth_count",
"hold_time", "end_time", "apply_start_time", "apply_end_time",
"online_apply", "photo_url", "create_by", "create_time", "update_by", "update_time", "del_flag"
)
SELECT
'2026年石河子经开区制造业户外招聘会',
'石河子经济技术开发区管委会',
'行业专场',
'第八师(石河子市)',
'石河子经济技术开发区企业服务中心广场',
100,
TIMESTAMP '2026-05-16 09:00:00',
TIMESTAMP '2026-05-16 17:30:00',
TIMESTAMP '2026-04-25 00:00:00',
TIMESTAMP '2026-05-10 23:59:59',
TRUE,
'https://picsum.photos/seed/shihezi-outdoor-3/400/300',
'system',
NOW(),
'system',
NOW(),
'0'
WHERE NOT EXISTS (SELECT 1 FROM "cms_outdoor_fair" WHERE "title" = '2026年石河子经开区制造业户外招聘会');
INSERT INTO "cms_outdoor_fair" (
"title", "host_unit", "fair_type", "region", "address", "booth_count",
"hold_time", "end_time", "apply_start_time", "apply_end_time",
"online_apply", "photo_url", "create_by", "create_time", "update_by", "update_time", "del_flag"
)
SELECT
'2026年石河子市高新区数字经济户外招聘会',
'石河子高新技术产业开发区管理委员会',
'高新技术专场',
'第八师(石河子市)',
'石河子高新区科技企业孵化器广场',
55,
TIMESTAMP '2026-06-20 09:00:00',
TIMESTAMP '2026-06-20 16:30:00',
TIMESTAMP '2026-06-01 00:00:00',
TIMESTAMP '2026-06-14 23:59:59',
TRUE,
'https://picsum.photos/seed/shihezi-outdoor-4/400/300',
'system',
NOW(),
'system',
NOW(),
'0'
WHERE NOT EXISTS (SELECT 1 FROM "cms_outdoor_fair" WHERE "title" = '2026年石河子市高新区数字经济户外招聘会');
INSERT INTO "cms_outdoor_fair" (
"title", "host_unit", "fair_type", "region", "address", "booth_count",
"hold_time", "end_time", "apply_start_time", "apply_end_time",
"online_apply", "photo_url", "create_by", "create_time", "update_by", "update_time", "del_flag"
)
SELECT
'2026年石河子军垦文化广场服务业招聘会',
'石河子市就业服务中心',
'服务业专场',
'第八师(石河子市)',
'石河子市军垦文化广场',
45,
TIMESTAMP '2026-07-11 10:00:00',
TIMESTAMP '2026-07-11 17:00:00',
TIMESTAMP '2026-06-20 00:00:00',
TIMESTAMP '2026-07-05 23:59:59',
FALSE,
'https://picsum.photos/seed/shihezi-outdoor-5/400/300',
'system',
NOW(),
'system',
NOW(),
'0'
WHERE NOT EXISTS (SELECT 1 FROM "cms_outdoor_fair" WHERE "title" = '2026年石河子军垦文化广场服务业招聘会');
INSERT INTO "cms_outdoor_fair" (
"title", "host_unit", "fair_type", "region", "address", "booth_count",
"hold_time", "end_time", "apply_start_time", "apply_end_time",
"online_apply", "photo_url", "create_by", "create_time", "update_by", "update_time", "del_flag"
)
SELECT
'2026年第八师团场农业技术人才户外招聘会',
'第八师农业农村局',
'行业专场',
'第八师(石河子市)',
'石河子市人民广场东侧',
50,
TIMESTAMP '2026-08-08 09:00:00',
TIMESTAMP '2026-08-08 17:00:00',
TIMESTAMP '2026-07-20 00:00:00',
TIMESTAMP '2026-08-02 23:59:59',
FALSE,
'https://picsum.photos/seed/shihezi-outdoor-6/400/300',
'system',
NOW(),
'system',
NOW(),
'0'
WHERE NOT EXISTS (SELECT 1 FROM "cms_outdoor_fair" WHERE "title" = '2026年第八师团场农业技术人才户外招聘会');
INSERT INTO "cms_outdoor_fair" (
"title", "host_unit", "fair_type", "region", "address", "booth_count",
"hold_time", "end_time", "apply_start_time", "apply_end_time",
"online_apply", "photo_url", "create_by", "create_time", "update_by", "update_time", "del_flag"
)
SELECT
'2026年石河子市金秋就业援助户外招聘会',
'石河子市公共就业和人才服务中心',
'就业援助专场',
'第八师(石河子市)',
'石河子市幸福路步行街广场',
70,
TIMESTAMP '2026-09-19 09:30:00',
TIMESTAMP '2026-09-19 17:30:00',
TIMESTAMP '2026-09-01 00:00:00',
TIMESTAMP '2026-09-13 23:59:59',
TRUE,
'https://picsum.photos/seed/shihezi-outdoor-7/400/300',
'system',
NOW(),
'system',
NOW(),
'0'
WHERE NOT EXISTS (SELECT 1 FROM "cms_outdoor_fair" WHERE "title" = '2026年石河子市金秋就业援助户外招聘会');
INSERT INTO "cms_outdoor_fair" (
"title", "host_unit", "fair_type", "region", "address", "booth_count",
"hold_time", "end_time", "apply_start_time", "apply_end_time",
"online_apply", "photo_url", "create_by", "create_time", "update_by", "update_time", "del_flag"
)
SELECT
'2026年石河子市文旅商贸户外招聘会',
'石河子市文化体育广电和旅游局',
'文旅专场',
'第八师(石河子市)',
'石河子市北湖公园游客服务中心广场',
40,
TIMESTAMP '2026-10-24 10:00:00',
TIMESTAMP '2026-10-24 17:00:00',
TIMESTAMP '2026-10-01 00:00:00',
TIMESTAMP '2026-10-18 23:59:59',
FALSE,
'https://picsum.photos/seed/shihezi-outdoor-8/400/300',
'system',
NOW(),
'system',
NOW(),
'0'
WHERE NOT EXISTS (SELECT 1 FROM "cms_outdoor_fair" WHERE "title" = '2026年石河子市文旅商贸户外招聘会');

View File

@@ -0,0 +1,86 @@
-- 场地展位图与户外招聘会展位预定
-- 瀚高数据库 / PostgreSQL 兼容
CREATE SEQUENCE IF NOT EXISTS "cms_venue_booth_id_seq" START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1;
CREATE TABLE IF NOT EXISTS "cms_venue_booth" (
"id" BIGINT NOT NULL DEFAULT nextval('"cms_venue_booth_id_seq"') PRIMARY KEY,
"venue_id" BIGINT NOT NULL,
"booth_number" VARCHAR(50) NOT NULL,
"position_x" INTEGER NOT NULL DEFAULT 0,
"position_y" INTEGER NOT NULL DEFAULT 0,
"create_by" VARCHAR(64) DEFAULT '',
"create_time" TIMESTAMP(0),
"update_by" VARCHAR(64) DEFAULT '',
"update_time" TIMESTAMP(0),
"remark" VARCHAR(500),
"del_flag" CHAR(1) DEFAULT '0'
);
COMMENT ON TABLE "cms_venue_booth" IS '场地展位图';
COMMENT ON COLUMN "cms_venue_booth"."venue_id" IS '场地ID';
COMMENT ON COLUMN "cms_venue_booth"."booth_number" IS '展位号';
COMMENT ON COLUMN "cms_venue_booth"."position_x" IS 'X坐标';
COMMENT ON COLUMN "cms_venue_booth"."position_y" IS 'Y坐标';
CREATE UNIQUE INDEX IF NOT EXISTS "uk_cms_venue_booth_number" ON "cms_venue_booth" ("venue_id", "booth_number");
CREATE INDEX IF NOT EXISTS "idx_cms_venue_booth_venue" ON "cms_venue_booth" ("venue_id");
CREATE SEQUENCE IF NOT EXISTS "cms_outdoor_fair_booth_booking_id_seq" START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1;
CREATE TABLE IF NOT EXISTS "cms_outdoor_fair_booth_booking" (
"id" BIGINT NOT NULL DEFAULT nextval('"cms_outdoor_fair_booth_booking_id_seq"') PRIMARY KEY,
"fair_id" BIGINT NOT NULL,
"booth_id" BIGINT NOT NULL,
"booth_number" VARCHAR(50) NOT NULL,
"company_id" BIGINT,
"company_name" VARCHAR(200),
"status" VARCHAR(30) DEFAULT 'reserved',
"create_by" VARCHAR(64) DEFAULT '',
"create_time" TIMESTAMP(0),
"update_by" VARCHAR(64) DEFAULT '',
"update_time" TIMESTAMP(0),
"remark" VARCHAR(500),
"del_flag" CHAR(1) DEFAULT '0'
);
COMMENT ON TABLE "cms_outdoor_fair_booth_booking" IS '户外招聘会展位预定';
CREATE UNIQUE INDEX IF NOT EXISTS "uk_cms_outdoor_fair_booth_booking" ON "cms_outdoor_fair_booth_booking" ("fair_id", "booth_id");
CREATE INDEX IF NOT EXISTS "idx_cms_outdoor_fair_booth_booking_fair" ON "cms_outdoor_fair_booth_booking" ("fair_id");
WITH target_venue AS (
SELECT "id" AS venue_id
FROM "cms_venue_info"
WHERE "venue_name" = '石河子市人民广场户外招聘区'
LIMIT 1
),
seed AS (
SELECT
target_venue.venue_id,
'KJ' || LPAD(gs::text, 2, '0') AS booth_number,
40 + ((gs - 1) % 20) * 54 AS position_x,
40 + ((gs - 1) / 20) * 54 AS position_y
FROM target_venue
CROSS JOIN generate_series(1, 80) gs
)
INSERT INTO "cms_venue_booth" ("venue_id", "booth_number", "position_x", "position_y", "create_by", "create_time", "del_flag")
SELECT seed.venue_id, seed.booth_number, seed.position_x, seed.position_y, 'system', NOW(), '0'
FROM seed
WHERE NOT EXISTS (
SELECT 1
FROM "cms_venue_booth" existing
WHERE existing."venue_id" = seed.venue_id
AND existing."booth_number" = seed.booth_number
);
UPDATE "cms_venue_info" venue
SET "floor_count" = booth_counts.total
FROM (
SELECT "venue_id", COUNT(*) AS total
FROM "cms_venue_booth"
GROUP BY "venue_id"
) booth_counts
WHERE venue."id" = booth_counts."venue_id";
UPDATE "cms_outdoor_fair" fair
SET "booth_count" = venue."floor_count"
FROM "cms_venue_info" venue
WHERE fair."venue_id" = venue."id";

215
sql/cms_venue_info.sql Normal file
View File

@@ -0,0 +1,215 @@
-- 场地信息维护表、字典、菜单
-- 瀚高数据库 / PostgreSQL 兼容
-- 可重复执行。
CREATE SEQUENCE IF NOT EXISTS "cms_venue_info_id_seq"
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
CREATE TABLE IF NOT EXISTS "cms_venue_info" (
"id" BIGINT NOT NULL DEFAULT nextval('"cms_venue_info_id_seq"') PRIMARY KEY,
"venue_type" VARCHAR(100) NOT NULL,
"venue_name" VARCHAR(200) NOT NULL,
"venue_area" NUMERIC(12, 2),
"floor_count" INTEGER,
"venue_address" VARCHAR(500) NOT NULL,
"contact_phone" VARCHAR(50),
"route_line_ids" VARCHAR(500),
"route_line_names" VARCHAR(1000),
"handle_date" DATE,
"handle_org" VARCHAR(200),
"handler" VARCHAR(100),
"create_by" VARCHAR(64) DEFAULT '',
"create_time" TIMESTAMP(0),
"update_by" VARCHAR(64) DEFAULT '',
"update_time" TIMESTAMP(0),
"remark" VARCHAR(100),
"del_flag" CHAR(1) DEFAULT '0'
);
COMMENT ON TABLE "cms_venue_info" IS '场地信息维护表';
COMMENT ON COLUMN "cms_venue_info"."id" IS '主键ID';
COMMENT ON COLUMN "cms_venue_info"."venue_type" IS '场地类型';
COMMENT ON COLUMN "cms_venue_info"."venue_name" IS '场地名称';
COMMENT ON COLUMN "cms_venue_info"."venue_area" IS '场地面积';
COMMENT ON COLUMN "cms_venue_info"."floor_count" IS '展位数量';
COMMENT ON COLUMN "cms_venue_info"."venue_address" IS '场地地址';
COMMENT ON COLUMN "cms_venue_info"."contact_phone" IS '联系电话';
COMMENT ON COLUMN "cms_venue_info"."route_line_ids" IS '乘坐线路ID多个用英文逗号分隔';
COMMENT ON COLUMN "cms_venue_info"."route_line_names" IS '乘坐线路名称,多个用英文逗号分隔';
COMMENT ON COLUMN "cms_venue_info"."handle_date" IS '经办日期';
COMMENT ON COLUMN "cms_venue_info"."handle_org" IS '经办机构';
COMMENT ON COLUMN "cms_venue_info"."handler" IS '经办人';
COMMENT ON COLUMN "cms_venue_info"."remark" IS '备注';
COMMENT ON COLUMN "cms_venue_info"."del_flag" IS '删除标志0代表存在 2代表删除';
CREATE INDEX IF NOT EXISTS "idx_cms_venue_info_type" ON "cms_venue_info" ("venue_type");
CREATE INDEX IF NOT EXISTS "idx_cms_venue_info_name" ON "cms_venue_info" ("venue_name");
CREATE INDEX IF NOT EXISTS "idx_cms_venue_info_del_flag" ON "cms_venue_info" ("del_flag");
INSERT INTO "sys_dict_type" (
"dict_id", "dict_name", "dict_type", "status", "create_by", "create_time", "remark"
)
SELECT
(SELECT COALESCE(MAX("dict_id"), 0) + 1 FROM "sys_dict_type"),
'场地类型',
'venue_info_type',
'0',
'system',
NOW(),
'场地信息维护使用'
WHERE NOT EXISTS (
SELECT 1 FROM "sys_dict_type" WHERE "dict_type" = 'venue_info_type'
);
WITH seed("dict_sort", "dict_label", "dict_value", "list_class", "is_default") AS (
VALUES
(1, '智能化招聘场馆', '智能化招聘场馆', 'primary', 'Y'),
(2, '户外招聘广场', '户外招聘广场', 'success', 'N'),
(3, '高校招聘场地', '高校招聘场地', 'processing', 'N'),
(4, '园区招聘场地', '园区招聘场地', 'cyan', 'N'),
(5, '临时招聘场地', '临时招聘场地', 'warning', 'N'),
(6, '其他', '其他', 'default', 'N')
),
new_rows AS (
SELECT
ROW_NUMBER() OVER (ORDER BY seed."dict_sort") AS rn,
seed.*
FROM seed
WHERE NOT EXISTS (
SELECT 1
FROM "sys_dict_data" data
WHERE data."dict_type" = 'venue_info_type'
AND data."dict_value" = seed."dict_value"
)
),
base AS (
SELECT COALESCE(MAX("dict_code"), 0) AS max_code FROM "sys_dict_data"
)
INSERT INTO "sys_dict_data" (
"dict_code", "dict_sort", "dict_label", "dict_value", "dict_type",
"css_class", "list_class", "is_default", "status", "create_by", "create_time", "remark"
)
SELECT
base.max_code + new_rows.rn,
new_rows."dict_sort",
new_rows."dict_label",
new_rows."dict_value",
'venue_info_type',
NULL,
new_rows."list_class",
new_rows."is_default",
'0',
'system',
NOW(),
'场地类型'
FROM new_rows
CROSS JOIN base;
WITH parent_menu AS (
SELECT "menu_id" FROM "sys_menu" WHERE "menu_id" = 2055
),
inserted_page AS (
INSERT INTO "sys_menu" (
"menu_name", "parent_id", "order_num", "path", "component", "query", "route_name",
"is_frame", "is_cache", "menu_type", "visible", "status", "perms", "icon",
"create_by", "create_time", "remark"
)
SELECT
'场地信息维护',
parent_menu."menu_id",
2,
'jobfair/venueinfo',
'jobfair/venueinfo/index',
NULL,
'',
1,
0,
'C',
'0',
'0',
'cms:venueInfo:list',
'#',
'system',
NOW(),
'场地信息维护菜单'
FROM parent_menu
WHERE NOT EXISTS (
SELECT 1 FROM "sys_menu" WHERE "parent_id" = parent_menu."menu_id" AND "menu_name" = '场地信息维护'
)
RETURNING "menu_id"
),
page_menu AS (
SELECT "menu_id" FROM inserted_page
UNION
SELECT "menu_id" FROM "sys_menu" WHERE "parent_id" = 2055 AND "menu_name" = '场地信息维护'
),
button_seed("menu_name", "order_num", "perms") AS (
VALUES
('场地信息查询', 1, 'cms:venueInfo:query'),
('场地信息新增', 2, 'cms:venueInfo:add'),
('场地信息修改', 3, 'cms:venueInfo:edit'),
('场地信息删除', 4, 'cms:venueInfo:remove')
)
INSERT INTO "sys_menu" (
"menu_name", "parent_id", "order_num", "path", "component", "query", "route_name",
"is_frame", "is_cache", "menu_type", "visible", "status", "perms", "icon",
"create_by", "create_time", "remark"
)
SELECT
button_seed."menu_name",
page_menu."menu_id",
button_seed."order_num",
'',
NULL,
NULL,
'',
1,
0,
'F',
'0',
'0',
button_seed."perms",
'#',
'system',
NOW(),
'场地信息维护按钮'
FROM page_menu
CROSS JOIN button_seed
WHERE NOT EXISTS (
SELECT 1 FROM "sys_menu" WHERE "perms" = button_seed."perms"
);
WITH outdoor_roles AS (
SELECT DISTINCT "role_id"
FROM "sys_role_menu"
WHERE "menu_id" = 2111
),
venue_menus AS (
SELECT "menu_id"
FROM "sys_menu"
WHERE ("parent_id" = 2055 AND "menu_name" = '场地信息维护')
OR "perms" IN (
'cms:venueInfo:query',
'cms:venueInfo:add',
'cms:venueInfo:edit',
'cms:venueInfo:remove'
)
),
role_menu_rows AS (
SELECT outdoor_roles."role_id", venue_menus."menu_id"
FROM outdoor_roles
CROSS JOIN venue_menus
)
INSERT INTO "sys_role_menu" ("role_id", "menu_id")
SELECT role_menu_rows."role_id", role_menu_rows."menu_id"
FROM role_menu_rows
WHERE NOT EXISTS (
SELECT 1
FROM "sys_role_menu" existing
WHERE existing."role_id" = role_menu_rows."role_id"
AND existing."menu_id" = role_menu_rows."menu_id"
);

View File

@@ -0,0 +1,90 @@
-- =====================================================
-- 面试列表 - 菜单权限配置 SQL
-- 在 岗位管理 菜单下增加"面试列表"子菜单及按钮权限
--
-- 执行前请确认:
-- 1. 确认 parent_id面试列表将放在 "岗位管理" 目录下
-- 如果数据库中岗位管理目录的 menu_id 不同,请修改下面子查询中的条件
-- 2. sys_menu 表使用自增主键auto_increment=2000
-- 本脚本使用 2100+ 的固定ID避免与已有数据冲突
-- 3. 执行后需要在"角色管理"中将新菜单分配给对应角色
-- =====================================================
-- Step 1: 新增菜单项C=菜单页面)
-- parent_id 动态查找path='management' 的目录菜单
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
SELECT 2100, '面试列表',
(SELECT menu_id FROM sys_menu WHERE path = 'management' AND menu_type = 'M' LIMIT 1),
10, 'interview-list', 'Management/InterviewList', '', 'InterviewList', 1, 0, 'C', '0', '0',
'cms:interview:list', 'message', 'admin', CURRENT_TIMESTAMP, '', null, '面试列表菜单'
WHERE EXISTS (SELECT 1 FROM sys_menu WHERE path = 'management' AND menu_type = 'M');
-- Step 2: 新增按钮权限F=按钮)
-- 面试查询
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
SELECT 2101, '面试查询', 2100, 1, '', '', '', '', 1, 0, 'F', '0', '0',
'cms:interview:query', '#', 'admin', CURRENT_TIMESTAMP, '', null, ''
WHERE EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2100);
-- 面试新增
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
SELECT 2102, '面试新增', 2100, 2, '', '', '', '', 1, 0, 'F', '0', '0',
'cms:interview:add', '#', 'admin', CURRENT_TIMESTAMP, '', null, ''
WHERE EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2100);
-- 面试修改
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
SELECT 2103, '面试修改', 2100, 3, '', '', '', '', 1, 0, 'F', '0', '0',
'cms:interview:edit', '#', 'admin', CURRENT_TIMESTAMP, '', null, ''
WHERE EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2100);
-- 面试删除
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
SELECT 2104, '面试删除', 2100, 4, '', '', '', '', 1, 0, 'F', '0', '0',
'cms:interview:remove', '#', 'admin', CURRENT_TIMESTAMP, '', null, ''
WHERE EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2100);
-- Step 3: 将新菜单分配给角色
-- 为所有非管理员角色分配面试列表权限(管理员角色无需分配,通配符 *:*:* 自动拥有全部权限)
-- 如需仅限特定角色,请修改 WHERE 条件
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT r.role_id, 2100
FROM sys_role r
WHERE r.role_key != 'admin'
AND EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2100);
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT r.role_id, 2101
FROM sys_role r
WHERE r.role_key != 'admin'
AND EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2101);
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT r.role_id, 2102
FROM sys_role r
WHERE r.role_key != 'admin'
AND EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2102);
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT r.role_id, 2103
FROM sys_role r
WHERE r.role_key != 'admin'
AND EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2103);
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT r.role_id, 2104
FROM sys_role r
WHERE r.role_key != 'admin'
AND EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2104);
-- =====================================================
-- 验证 SQL可选执行
-- =====================================================
-- 检查菜单是否创建成功
-- SELECT * FROM sys_menu WHERE menu_id BETWEEN 2100 AND 2104;
-- 检查角色菜单关联
-- SELECT r.role_name, m.menu_name FROM sys_role_menu rm
-- JOIN sys_role r ON r.role_id = rm.role_id
-- JOIN sys_menu m ON m.menu_id = rm.menu_id
-- WHERE rm.menu_id BETWEEN 2100 AND 2104;

View File

@@ -0,0 +1,70 @@
-- 户外招聘会举办区域系统字典
-- 可重复执行:按 dict_type / dict_value 去重。
INSERT INTO "sys_dict_type" (
"dict_id", "dict_name", "dict_type", "status", "create_by", "create_time", "remark"
)
SELECT
(SELECT COALESCE(MAX("dict_id"), 0) + 1 FROM "sys_dict_type"),
'户外招聘会举办区域',
'outdoor_fair_region',
'0',
'system',
NOW(),
'户外招聘会管理使用'
WHERE NOT EXISTS (
SELECT 1 FROM "sys_dict_type" WHERE "dict_type" = 'outdoor_fair_region'
);
WITH seed("dict_sort", "dict_label", "dict_value", "list_class", "is_default") AS (
VALUES
(1, '石河子市', '石河子市', 'primary', 'N'),
(2, '第一师(阿拉尔市)', '第一师(阿拉尔市)', 'default', 'N'),
(3, '第二师(铁门关市)', '第二师(铁门关市)', 'default', 'N'),
(4, '第三师(图木舒克市)', '第三师(图木舒克市)', 'default', 'N'),
(5, '第四师(可克达拉市)', '第四师(可克达拉市)', 'default', 'N'),
(6, '第五师(双河市)', '第五师(双河市)', 'default', 'N'),
(7, '第六师(五家渠市)', '第六师(五家渠市)', 'default', 'N'),
(8, '第七师(胡杨河市)', '第七师(胡杨河市)', 'default', 'N'),
(9, '第八师(石河子市)', '第八师(石河子市)', 'success', 'Y'),
(10, '第九师(白杨市)', '第九师(白杨市)', 'default', 'N'),
(11, '第十师(北屯市)', '第十师(北屯市)', 'default', 'N'),
(12, '第十一师(建工师)', '第十一师(建工师)', 'default', 'N'),
(13, '第十二师', '第十二师', 'default', 'N'),
(14, '第十三师(新星市)', '第十三师(新星市)', 'default', 'N'),
(15, '第十四师(昆玉市)', '第十四师(昆玉市)', 'default', 'N')
),
new_rows AS (
SELECT
ROW_NUMBER() OVER (ORDER BY seed."dict_sort") AS rn,
seed.*
FROM seed
WHERE NOT EXISTS (
SELECT 1
FROM "sys_dict_data" data
WHERE data."dict_type" = 'outdoor_fair_region'
AND data."dict_value" = seed."dict_value"
)
),
base AS (
SELECT COALESCE(MAX("dict_code"), 0) AS max_code FROM "sys_dict_data"
)
INSERT INTO "sys_dict_data" (
"dict_code", "dict_sort", "dict_label", "dict_value", "dict_type",
"css_class", "list_class", "is_default", "status", "create_by", "create_time", "remark"
)
SELECT
base.max_code + new_rows.rn,
new_rows."dict_sort",
new_rows."dict_label",
new_rows."dict_value",
'outdoor_fair_region',
NULL,
new_rows."list_class",
new_rows."is_default",
'0',
'system',
NOW(),
'户外招聘会举办区域'
FROM new_rows
CROSS JOIN base;

View File

@@ -0,0 +1,64 @@
-- 户外招聘会类型系统字典
-- 可重复执行:按 dict_type / dict_value 去重。
INSERT INTO "sys_dict_type" (
"dict_id", "dict_name", "dict_type", "status", "create_by", "create_time", "remark"
)
SELECT
(SELECT COALESCE(MAX("dict_id"), 0) + 1 FROM "sys_dict_type"),
'户外招聘会类型',
'outdoor_fair_type',
'0',
'system',
NOW(),
'户外招聘会管理使用'
WHERE NOT EXISTS (
SELECT 1 FROM "sys_dict_type" WHERE "dict_type" = 'outdoor_fair_type'
);
WITH seed("dict_sort", "dict_label", "dict_value", "list_class", "is_default") AS (
VALUES
(1, '综合类', '综合类', 'primary', 'Y'),
(2, '校园招聘', '校园招聘', 'success', 'N'),
(3, '行业专场', '行业专场', 'processing', 'N'),
(4, '高新技术专场', '高新技术专场', 'purple', 'N'),
(5, '智能制造专场', '智能制造专场', 'cyan', 'N'),
(6, '服务业专场', '服务业专场', 'blue', 'N'),
(7, '就业援助专场', '就业援助专场', 'warning', 'N'),
(8, '文旅专场', '文旅专场', 'magenta', 'N'),
(9, '其他', '其他', 'default', 'N')
),
new_rows AS (
SELECT
ROW_NUMBER() OVER (ORDER BY seed."dict_sort") AS rn,
seed.*
FROM seed
WHERE NOT EXISTS (
SELECT 1
FROM "sys_dict_data" data
WHERE data."dict_type" = 'outdoor_fair_type'
AND data."dict_value" = seed."dict_value"
)
),
base AS (
SELECT COALESCE(MAX("dict_code"), 0) AS max_code FROM "sys_dict_data"
)
INSERT INTO "sys_dict_data" (
"dict_code", "dict_sort", "dict_label", "dict_value", "dict_type",
"css_class", "list_class", "is_default", "status", "create_by", "create_time", "remark"
)
SELECT
base.max_code + new_rows.rn,
new_rows."dict_sort",
new_rows."dict_label",
new_rows."dict_value",
'outdoor_fair_type',
NULL,
new_rows."list_class",
new_rows."is_default",
'0',
'system',
NOW(),
'户外招聘会类型'
FROM new_rows
CROSS JOIN base;

204
启动指南.md Normal file
View File

@@ -0,0 +1,204 @@
# 石河子后端服务 - 手动启动指南
## 项目概述
- **框架**: RuoYi (若依) v3.8.8 / Spring Boot 2.5.15
- **语言**: Java 1.8 (兼容 Java 21)
- **构建工具**: Maven 3.9+
- **默认端口**: `9091`
- **主启动类**: `com.ruoyi.RuoYiApplication`
---
## 1. 环境要求
| 依赖 | 当前环境 | 备注 |
|------|----------|------|
| **JDK** | 21.0.11 (Temurin) | 路径: `C:\Program Files\Eclipse Adoptium\jdk-21.0.11.10-hotspot` |
| **Maven** | 3.9.9 | 路径: `C:\Users\franc\apache-maven-3.9.9` |
| **数据库** | Highgo DB (远程) | `39.98.44.136:6023` (local 配置) |
| **Redis** | 远程 | `124.243.245.42:5379` (local 配置) |
| **Elasticsearch** | 远程 | `124.243.245.42:9200` (local 配置) |
---
## 2. 环境变量设置
### Git Bash / WSL
```bash
export PATH="/c/Users/franc/apache-maven-3.9.9/bin:$PATH"
```
### 永久生效 (可选)
将上述 export 命令添加到 `~/.bashrc` 末尾:
```bash
echo 'export PATH="/c/Users/franc/apache-maven-3.9.9/bin:$PATH"' >> ~/.bashrc
```
### 验证安装
```bash
java -version # 应显示 OpenJDK 21
mvn -version # 应显示 Apache Maven 3.9.9
```
---
## 3. 构建项目
```bash
# 进入后端项目目录
cd shz-backend
# 清理并构建跳过测试4线程并行编译
mvn clean install -DskipTests -T 4
```
> **构建产物**: `ruoyi-admin/target/ruoyi-admin.jar`
---
## 4. 启动服务
### 前台运行用于调试Ctrl+C 停止)
```bash
cd shz-backend
java -jar ruoyi-admin/target/ruoyi-admin.jar --spring.profiles.active=local
```
### 后台运行
```bash
cd shz-backend
# 创建日志目录
mkdir -p logs
# 后台启动
nohup java -jar ruoyi-admin/target/ruoyi-admin.jar \
--spring.profiles.active=local \
>> logs/backend.log 2>&1 &
# 跟踪日志
tail -f logs/backend.log
```
### JVM 参数调优(可选)
```bash
java -Xms2048M -Xmx2048M -jar ruoyi-admin/target/ruoyi-admin.jar \
--spring.profiles.active=local
```
---
## 5. 验证服务
```bash
# 检查服务首页
curl http://localhost:9091/
# 返回: 欢迎使用RuoYi后台管理框架当前版本v3.8.8
# 检查 Swagger API 文档
curl -s -o /dev/null -w "HTTP: %{http_code}" http://localhost:9091/swagger-ui/index.html
# 返回: HTTP: 200
# 检查 Druid 监控面板
# 浏览器访问: http://localhost:9091/druid/
# 账号: ruoyi / 123456
# 查看运行中的进程
jps -l | grep ruoyi
```
---
## 6. 停止服务
```bash
# 方法一: 查找并终止(推荐)
jps -l | grep ruoyi # 查找 PID
kill <PID> # 优雅关闭
# 方法二: 按端口查找
netstat -ano | findstr 9091 # Windows
lsof -i :9091 # Linux/Mac
```
---
## 7. 可用配置 Profile
| Profile | 配置文件 | 数据库 | 端口 |
|---------|----------|--------|------|
| `dev` | [`application-dev.yml`](ruoyi-admin/src/main/resources/application-dev.yml) | 内网 `172.31.9.107:8765` | `9091` |
| `local` | [`application-local.yml`](ruoyi-admin/src/main/resources/application-local.yml) | 外网 `39.98.44.136:6023` | `9091` |
默认激活 Profile 在 [`application.yml`](ruoyi-admin/src/main/resources/application.yml) 的 `spring.profiles.active` 中配置。
---
## 8. 构建常见问题
### 编译错误: `NoSuchFieldError: JCTree$JCImport qualid`
Lombok 版本与 JDK 21 不兼容。确保 `pom.xml` 中 Lombok 版本 ≥ 1.18.32
```xml
<lombok.version>1.18.34</lombok.version>
```
### 编译错误: `maven-compiler-plugin` 版本过低
需要 `maven-compiler-plugin` ≥ 3.11.0 才支持 JDK 21
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
```
### Maven 下载依赖缓慢
项目已配置阿里云 Maven 镜像(`maven.aliyun.com`),无需额外配置。
---
## 9. 项目结构
```
shz-backend/
├── pom.xml # 根 POM依赖管理
├── ruoyi-admin/ # Web 入口模块Spring Boot 启动类)
│ └── src/main/resources/
│ ├── application.yml # 主配置
│ ├── application-dev.yml # 开发环境
│ └── application-local.yml # 本地环境
├── ruoyi-common/ # 通用工具模块
├── ruoyi-framework/ # 核心框架模块
├── ruoyi-system/ # 系统管理模块
├── ruoyi-bussiness/ # 业务模块
├── ruoyi-quartz/ # 定时任务模块
├── ruoyi-generator/ # 代码生成模块
├── sql/ # 数据库脚本
├── docs/ # 项目文档
└── buildAndStart.sh # Linux 部署脚本
```
---
## 10. 相关服务地址
| 服务 | 地址 |
|------|------|
| 后端 API | `http://localhost:9091/` |
| Swagger 文档 | `http://localhost:9091/swagger-ui/index.html` |
| Druid 监控 | `http://localhost:9091/druid/` |
| 管理后台前端 | `shz-admin/` 目录(需单独启动) |
| 微信小程序 | `shzApp/` 目录uniapp 项目) |