Compare commits

..

3 Commits

17 changed files with 599 additions and 262 deletions

View File

@@ -1,4 +1,4 @@
# 户外招聘会企业 H5 公共接口
# 户外招聘会公共接口
基础路径:`/app/outdoor-fair`
@@ -42,9 +42,25 @@
**请求路径:** `/{fairId}`
返回 `fair``fairH5Url`
返回 `fair`、参会企业 `companies``fairH5Url`。登录用户对应的 `fair.checkedIn` 表示当前用户是否已在本场招聘会签到
## 4. 后台参会企业二维码入口
## 4. 移动端用户一键签到
**请求方式:** `POST`
**请求路径:** `/check-in`
**请求头:** 需要移动端登录态 `Authorization`
**请求参数:**
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| fairId | Long | 是 | 招聘会 ID |
接口从当前登录用户读取姓名、手机号和地址,签到记录关联 `app_user.user_id`,不再接受前端传入的姓名和手机号。招聘会开始后,同一用户在同一场招聘会只能签到一次。
## 5. 后台参会企业二维码入口
后台接口 `GET /cms/outdoor-fair/{fairId}/companies` 的每行企业现在额外返回:
@@ -57,7 +73,7 @@
也可以调用 `GET /cms/outdoor-fair/{fairId}/companies/{companyId}/qrcode` 单独获取某个参会企业的二维码内容。
## 5. 已提供的静态 H5
## 6. 已提供的静态 H5
| 页面 | 说明 |
| --- | --- |

View File

@@ -48,37 +48,8 @@
text-decoration: none;
}
.btn.secondary { color: #1f6feb; background: #eaf2ff; }
.btn.checkin { background: #14866d; }
.empty, .loading, .error { padding: 28px 16px; color: #667085; text-align: center; font-size: 14px; line-height: 1.7; }
.error { color: #b42318; }
/* 签到弹窗 */
.mask { position: fixed; inset: 0; background: rgba(23,32,51,.45); display: none; z-index: 50; }
.mask.show { display: block; }
.dialog {
position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%);
width: calc(100vw - 56px); max-width: 380px; background: #fff;
border-radius: 10px; padding: 18px 16px; z-index: 51; display: none;
box-shadow: 0 18px 48px rgba(23,32,51,.22);
}
.dialog.show { display: block; }
.dialog h3 { margin: 0 0 4px; font-size: 17px; }
.dialog .sub { margin: 0 0 14px; font-size: 13px; color: #667085; }
.field { margin-bottom: 12px; }
.field label { display: block; margin-bottom: 6px; font-size: 14px; color: #344054; }
.field input {
width: 100%; min-height: 42px; padding: 8px 12px; border: 1px solid #d0d5dd;
border-radius: 8px; font-size: 15px; background: #fff; box-sizing: border-box;
}
.field input:focus { outline: none; border-color: #1f6feb; }
.dialog-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 4px; }
.btn.ghost { color: #475467; background: #f2f4f7; }
.toast {
position: fixed; left: 50%; top: 24px; transform: translateX(-50%);
padding: 10px 16px; border-radius: 8px; font-size: 14px; color: #fff;
background: #14866d; z-index: 60; display: none; max-width: 80vw; text-align: center;
}
.toast.error { background: #b42318; }
.toast.show { display: block; }
</style>
</head>
<body>
@@ -169,7 +140,6 @@
'<p>' + escapeHtml(fair.address) + '</p>' +
'</section>' +
'<div class="actions">' +
'<button type="button" class="btn checkin" id="checkinBtn">签到</button>' +
'<a class="btn" href="' + escapeHtml(publicBase() + '/h5/outdoor-fair/detail.html?fairId=' + pageFairId) + '">查看招聘会</a>' +
'</div>' +
'<section class="section">' +
@@ -193,87 +163,7 @@
(jobs.length ? '<div class="jobs">' + jobs.map(function (job) {
return renderJob(job, educationDict, experienceDict);
}).join('') + '</div>' : '<div class="empty">暂无岗位</div>') +
'</section>' +
'<div class="mask" id="checkinMask"></div>' +
'<div class="dialog" id="checkinDialog">' +
'<h3>签到</h3>' +
'<p class="sub">请填写您的姓名和手机号完成签到</p>' +
'<div class="field">' +
'<label for="checkinName">姓名</label>' +
'<input type="text" id="checkinName" placeholder="请输入姓名" autocomplete="name">' +
'</div>' +
'<div class="field">' +
'<label for="checkinPhone">手机号</label>' +
'<input type="tel" id="checkinPhone" placeholder="请输入手机号" autocomplete="tel" maxlength="11">' +
'</div>' +
'<div class="dialog-actions">' +
'<button type="button" class="btn ghost" id="checkinCancel">取消</button>' +
'<button type="button" class="btn checkin" id="checkinSubmit">提交签到</button>' +
'</div>' +
'</div>' +
'<div class="toast" id="checkinToast"></div>';
bindCheckIn();
}
function showToast(msg, isError) {
var toast = document.getElementById('checkinToast');
if (!toast) return;
toast.textContent = msg;
toast.className = 'toast show' + (isError ? ' error' : '');
setTimeout(function () { toast.className = 'toast' + (isError ? ' error' : ''); }, 2400);
}
function openCheckIn() {
var mask = document.getElementById('checkinMask');
var dialog = document.getElementById('checkinDialog');
if (mask && dialog) {
mask.className = 'mask show';
dialog.className = 'dialog show';
}
}
function closeCheckIn() {
var mask = document.getElementById('checkinMask');
var dialog = document.getElementById('checkinDialog');
if (mask) mask.className = 'mask';
if (dialog) dialog.className = 'dialog';
}
function bindCheckIn() {
var openBtn = document.getElementById('checkinBtn');
var cancelBtn = document.getElementById('checkinCancel');
var mask = document.getElementById('checkinMask');
var submitBtn = document.getElementById('checkinSubmit');
if (openBtn) openBtn.onclick = openCheckIn;
if (cancelBtn) cancelBtn.onclick = closeCheckIn;
if (mask) mask.onclick = closeCheckIn;
if (submitBtn) submitBtn.onclick = submitCheckIn;
}
function submitCheckIn() {
var name = (document.getElementById('checkinName') || {}).value;
var phone = (document.getElementById('checkinPhone') || {}).value;
if (!name || !name.trim()) { showToast('请输入姓名', true); return; }
if (!phone || !/^\d{11}$/.test(phone.trim())) { showToast('请输入正确的11位手机号', true); return; }
if (!pageFairId) { showToast('缺少招聘会信息', true); return; }
var submitBtn = document.getElementById('checkinSubmit');
if (submitBtn) submitBtn.disabled = true;
fetch(apiBase() + '/app/outdoor-fair/check-in', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fairId: Number(pageFairId), name: name.trim(), phone: phone.trim() })
}).then(function (res) { return res.json(); }).then(function (body) {
if (submitBtn) submitBtn.disabled = false;
if (body && body.code === 200) {
closeCheckIn();
showToast('签到成功');
} else {
showToast((body && body.msg) || '签到失败', true);
}
}).catch(function () {
if (submitBtn) submitBtn.disabled = false;
showToast('网络异常,请重试', true);
});
'</section>';
}
function renderJob(job, educationDict, experienceDict) {

View File

@@ -31,41 +31,11 @@
.photo { width: 100%; margin-top: 14px; border-radius: 8px; object-fit: cover; }
.loading, .error { padding: 28px 16px; color: #667085; text-align: center; font-size: 14px; line-height: 1.7; }
.error { color: #b42318; }
.actions { display: grid; grid-template-columns: 1fr; gap: 10px; margin-top: 14px; }
.btn {
display: flex; align-items: center; justify-content: center;
min-height: 44px; padding: 10px 14px; color: #fff; background: #1f6feb;
border: 0; border-radius: 8px; font-size: 15px; text-decoration: none; width: 100%;
}
.btn.checkin { background: #14866d; }
/* 签到弹窗 */
.mask { position: fixed; inset: 0; background: rgba(23,32,51,.45); display: none; z-index: 50; }
.mask.show { display: block; }
.dialog {
position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%);
width: calc(100vw - 56px); max-width: 380px; background: #fff;
border-radius: 10px; padding: 18px 16px; z-index: 51; display: none;
box-shadow: 0 18px 48px rgba(23,32,51,.22);
}
.dialog.show { display: block; }
.dialog h3 { margin: 0 0 4px; font-size: 17px; }
.dialog .sub { margin: 0 0 14px; font-size: 13px; color: #667085; }
.field { margin-bottom: 12px; }
.field label { display: block; margin-bottom: 6px; font-size: 14px; color: #344054; }
.field input {
width: 100%; min-height: 42px; padding: 8px 12px; border: 1px solid #d0d5dd;
border-radius: 8px; font-size: 15px; background: #fff; box-sizing: border-box;
}
.field input:focus { outline: none; border-color: #1f6feb; }
.dialog-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 4px; }
.btn.ghost { color: #475467; background: #f2f4f7; }
.toast {
position: fixed; left: 50%; top: 24px; transform: translateX(-50%);
padding: 10px 16px; border-radius: 8px; font-size: 14px; color: #fff;
background: #14866d; z-index: 60; display: none; max-width: 80vw; text-align: center;
}
.toast.error { background: #b42318; }
.toast.show { display: block; }
</style>
</head>
<body>
@@ -76,7 +46,6 @@
(function () {
var app = document.getElementById('app');
var fairId = new URLSearchParams(window.location.search).get('fairId');
var pageFairId = null;
function text(value) {
return value === null || value === undefined || value === '' ? '暂无' : String(value);
@@ -103,7 +72,6 @@
function render(data) {
var fair = data.fair || {};
pageFairId = (fair.id !== null && fair.id !== undefined) ? fair.id : fairId;
document.title = text(fair.title);
app.innerHTML =
'<section class="hero">' +
@@ -111,9 +79,6 @@
'<p>' + escapeHtml(fair.hostUnit) + '</p>' +
'<p>' + escapeHtml(fair.address) + '</p>' +
'</section>' +
'<div class="actions">' +
'<button type="button" class="btn checkin" id="checkinBtn">签到</button>' +
'</div>' +
'<section class="section">' +
'<h2>招聘会信息</h2>' +
'<div class="meta">' +
@@ -125,80 +90,7 @@
'<div><span>展位数量</span><span>' + escapeHtml(fair.boothCount) + '</span></div>' +
'</div>' +
(fair.photoUrl ? '<img class="photo" src="' + escapeHtml(fair.photoUrl) + '" alt="招聘会照片">' : '') +
'</section>' +
'<div class="mask" id="checkinMask"></div>' +
'<div class="dialog" id="checkinDialog">' +
'<h3>签到</h3>' +
'<p class="sub">请填写您的姓名和手机号完成签到</p>' +
'<div class="field">' +
'<label for="checkinName">姓名</label>' +
'<input type="text" id="checkinName" placeholder="请输入姓名" autocomplete="name">' +
'</div>' +
'<div class="field">' +
'<label for="checkinPhone">手机号</label>' +
'<input type="tel" id="checkinPhone" placeholder="请输入手机号" autocomplete="tel" maxlength="11">' +
'</div>' +
'<div class="dialog-actions">' +
'<button type="button" class="btn ghost" id="checkinCancel">取消</button>' +
'<button type="button" class="btn checkin" id="checkinSubmit">提交签到</button>' +
'</div>' +
'</div>' +
'<div class="toast" id="checkinToast"></div>';
bindCheckIn();
}
function showToast(msg, isError) {
var toast = document.getElementById('checkinToast');
if (!toast) return;
toast.textContent = msg;
toast.className = 'toast show' + (isError ? ' error' : '');
setTimeout(function () { toast.className = 'toast' + (isError ? ' error' : ''); }, 2400);
}
function openCheckIn() {
var mask = document.getElementById('checkinMask');
var dialog = document.getElementById('checkinDialog');
if (mask && dialog) { mask.className = 'mask show'; dialog.className = 'dialog show'; }
}
function closeCheckIn() {
var mask = document.getElementById('checkinMask');
var dialog = document.getElementById('checkinDialog');
if (mask) mask.className = 'mask';
if (dialog) dialog.className = 'dialog';
}
function bindCheckIn() {
var openBtn = document.getElementById('checkinBtn');
var cancelBtn = document.getElementById('checkinCancel');
var mask = document.getElementById('checkinMask');
var submitBtn = document.getElementById('checkinSubmit');
if (openBtn) openBtn.onclick = openCheckIn;
if (cancelBtn) cancelBtn.onclick = closeCheckIn;
if (mask) mask.onclick = closeCheckIn;
if (submitBtn) submitBtn.onclick = submitCheckIn;
}
function submitCheckIn() {
var name = (document.getElementById('checkinName') || {}).value;
var phone = (document.getElementById('checkinPhone') || {}).value;
if (!name || !name.trim()) { showToast('请输入姓名', true); return; }
if (!phone || !/^\d{11}$/.test(phone.trim())) { showToast('请输入正确的11位手机号', true); return; }
if (!pageFairId) { showToast('缺少招聘会信息', true); return; }
var submitBtn = document.getElementById('checkinSubmit');
if (submitBtn) submitBtn.disabled = true;
fetch(apiBase() + '/app/outdoor-fair/check-in', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fairId: Number(pageFairId), name: name.trim(), phone: phone.trim() })
}).then(function (res) { return res.json(); }).then(function (body) {
if (submitBtn) submitBtn.disabled = false;
if (body && body.code === 200) { closeCheckIn(); showToast('签到成功'); }
else { showToast((body && body.msg) || '签到失败', true); }
}).catch(function () {
if (submitBtn) submitBtn.disabled = false;
showToast('网络异常,请重试', true);
});
'</section>';
}
if (!fairId) {

View File

@@ -18,14 +18,18 @@ import com.ruoyi.cms.mapper.OutdoorFairJobMapper;
import com.ruoyi.cms.domain.rc.PublicJobFair;
import com.ruoyi.cms.domain.rc.PublicJobFairQuery;
import com.ruoyi.cms.domain.rc.PublicJobFairResponse;
import com.ruoyi.cms.service.IAppUserService;
import com.ruoyi.cms.service.IOutdoorFairAttendeeService;
import com.ruoyi.cms.service.IOutdoorFairService;
import com.ruoyi.cms.util.StringUtil;
import com.ruoyi.common.annotation.BussinessLog;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.AppUser;
import com.ruoyi.common.core.domain.entity.Company;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.RequestUrlUtils;
import com.ruoyi.common.utils.SiteSecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@@ -33,7 +37,6 @@ import org.springframework.beans.factory.annotation.Autowired;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@@ -83,6 +86,9 @@ public class AppOutdoorFairController extends BaseController
@Autowired
private IOutdoorFairAttendeeService outdoorFairAttendeeService;
@Autowired
private IAppUserService appUserService;
@BussinessLog(title = "户外招聘会列表")
@ApiOperation("分页查询户外招聘会列表")
@GetMapping("/page")
@@ -193,30 +199,50 @@ public class AppOutdoorFairController extends BaseController
return AjaxResult.error("招聘会不存在");
}
Map<String, Object> data = new HashMap<>();
data.put("fair", buildPublicFairDetail(fair));
Map<String, Object> publicFair = buildPublicFairDetail(fair);
publicFair.put("checkedIn", hasCheckedIn(fairId));
data.put("fair", publicFair);
data.put("companies", buildPublicParticipatingCompanies(fairId));
data.put("fairH5Url", buildFairH5Url(request, fairId));
return success(data);
}
/**
* 匿名签到(姓名+手机号。H5 企业页调用,无需登录
* 当前登录移动端用户一键签到
*/
@BussinessLog(title = "户外招聘会企业H5签到")
@ApiOperation("匿名签到(姓名+手机号)")
@BussinessLog(title = "户外招聘会移动端签到")
@ApiOperation("移动端用户一键签到")
@PostMapping("/check-in")
public AjaxResult checkIn(@RequestBody OutdoorFairAttendee attendee)
public AjaxResult checkIn(@RequestParam Long fairId)
{
if (attendee.getFairId() == null) {
if (!SiteSecurityUtils.isLogin()) {
return AjaxResult.error("请先登录");
}
return checkIn(fairId, SiteSecurityUtils.getUserId());
}
/**
* 签到业务实现单独保留 userId 入参,便于无 Web 容器的单元测试覆盖业务规则。
*/
AjaxResult checkIn(Long fairId, Long userId)
{
if (fairId == null) {
return AjaxResult.error("缺少招聘会参数");
}
if (StringUtils.isBlank(attendee.getName())) {
return AjaxResult.error("姓名不能为空");
if (userId == null) {
return AjaxResult.error("用户信息不存在,请重新登录");
}
if (StringUtils.isBlank(attendee.getPhone())) {
return AjaxResult.error("手机号不能为空");
AppUser appUser = appUserService.selectAppUserByUserId(userId);
if (appUser == null || !"0".equals(appUser.getDelFlag())) {
return AjaxResult.error("用户信息不存在,请重新登录");
}
OutdoorFair fair = outdoorFairService.getById(attendee.getFairId());
if (!StringUtil.IS_JOB_REQUEST_USER.equals(appUser.getIsCompanyUser())) {
return AjaxResult.error("仅移动端求职者可以签到");
}
if (StringUtils.isBlank(appUser.getName())) {
return AjaxResult.error("请先完善移动端用户姓名");
}
OutdoorFair fair = outdoorFairService.getById(fairId);
if (fair == null) {
return AjaxResult.error("招聘会不存在");
}
@@ -226,13 +252,20 @@ public class AppOutdoorFairController extends BaseController
if (!hasFairStarted(fair, new Date())) {
return AjaxResult.error("招聘会还没有开始,暂不能签到");
}
// 同一招聘会下同手机号已签到则不重复记录
// 同一招聘会下同一移动端用户已签到则不重复记录
Long exists = outdoorFairAttendeeService.count(new LambdaQueryWrapper<OutdoorFairAttendee>()
.eq(OutdoorFairAttendee::getFairId, attendee.getFairId())
.eq(OutdoorFairAttendee::getPhone, attendee.getPhone()));
.eq(OutdoorFairAttendee::getFairId, fairId)
.eq(OutdoorFairAttendee::getUserId, userId));
if (exists != null && exists > 0) {
return AjaxResult.error("该手机号已签到");
return AjaxResult.error("您已签到,无需重复签到");
}
OutdoorFairAttendee attendee = new OutdoorFairAttendee();
attendee.setFairId(fairId);
attendee.setUserId(userId);
attendee.setName(appUser.getName());
attendee.setPhone(appUser.getPhone());
attendee.setAddress(StringUtils.isNotBlank(appUser.getAddress())
? appUser.getAddress() : appUser.getDomicileAddress());
attendee.setCheckInTime(new Date());
return toAjax(outdoorFairAttendeeService.save(attendee));
}
@@ -243,6 +276,23 @@ public class AppOutdoorFairController extends BaseController
&& !currentTime.before(fair.getHoldTime());
}
private boolean hasCheckedIn(Long fairId)
{
if (!SiteSecurityUtils.isLogin()) {
return false;
}
Long userId;
try {
userId = SiteSecurityUtils.getUserId();
} catch (Exception e) {
return false;
}
Long count = outdoorFairAttendeeService.count(new LambdaQueryWrapper<OutdoorFairAttendee>()
.eq(OutdoorFairAttendee::getFairId, fairId)
.eq(OutdoorFairAttendee::getUserId, userId));
return count != null && count > 0;
}
private Map<String, Object> buildCompanyPage(Long fairId, Long companyId, OutdoorFairDevice device,
HttpServletRequest request)
{

View File

@@ -3,13 +3,21 @@ package com.ruoyi.cms.controller.cms;
import java.util.Arrays;
import java.util.List;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.cms.domain.OutdoorFair;
import com.ruoyi.cms.domain.OutdoorFairAttendee;
import com.ruoyi.cms.domain.vo.OutdoorFairAttendeeUserOption;
import com.ruoyi.cms.service.IAppUserService;
import com.ruoyi.cms.service.IOutdoorFairAttendeeService;
import com.ruoyi.cms.service.IOutdoorFairService;
import com.ruoyi.cms.util.StringUtil;
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.AppUser;
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;
@@ -37,6 +45,12 @@ public class OutdoorFairAttendeeController extends BaseController
@Autowired
private IOutdoorFairAttendeeService outdoorFairAttendeeService;
@Autowired
private IOutdoorFairService outdoorFairService;
@Autowired
private IAppUserService appUserService;
/**
* 查询签到人员列表(支持按人员姓名、手机号模糊查询)
*/
@@ -50,6 +64,22 @@ public class OutdoorFairAttendeeController extends BaseController
return getDataTable(list);
}
/**
* 查询当前招聘会尚未签到的移动端求职者。
*/
@ApiOperation("查询可录入签到的移动端用户")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:add')")
@GetMapping("/available-users")
public AjaxResult availableUsers(Long fairId, String keyword)
{
if (fairId == null) {
return AjaxResult.error("缺少招聘会参数");
}
List<OutdoorFairAttendeeUserOption> list = outdoorFairAttendeeService
.selectAvailableUserOptions(fairId, keyword);
return success(list);
}
/**
* 获取签到人员详细信息
*/
@@ -70,7 +100,42 @@ public class OutdoorFairAttendeeController extends BaseController
@PostMapping
public AjaxResult add(@RequestBody OutdoorFairAttendee attendee)
{
return toAjax(outdoorFairAttendeeService.save(attendee));
if (attendee == null || attendee.getFairId() == null) {
return AjaxResult.error("缺少招聘会参数");
}
if (attendee.getUserId() == null) {
return AjaxResult.error("请选择移动端用户");
}
OutdoorFair fair = outdoorFairService.getById(attendee.getFairId());
if (fair == null) {
return AjaxResult.error("招聘会不存在");
}
AppUser appUser = appUserService.selectAppUserByUserId(attendee.getUserId());
if (appUser == null || !"0".equals(appUser.getDelFlag())) {
return AjaxResult.error("移动端用户不存在或已删除");
}
if (!StringUtil.IS_JOB_REQUEST_USER.equals(appUser.getIsCompanyUser())) {
return AjaxResult.error("只能录入移动端求职者");
}
if (StringUtils.isBlank(appUser.getName())) {
return AjaxResult.error("移动端用户姓名为空,无法签到");
}
Long exists = outdoorFairAttendeeService.count(new LambdaQueryWrapper<OutdoorFairAttendee>()
.eq(OutdoorFairAttendee::getFairId, attendee.getFairId())
.eq(OutdoorFairAttendee::getUserId, attendee.getUserId()));
if (exists != null && exists > 0) {
return AjaxResult.error("该用户已签到");
}
OutdoorFairAttendee record = new OutdoorFairAttendee();
record.setFairId(attendee.getFairId());
record.setUserId(appUser.getUserId());
record.setName(appUser.getName());
record.setPhone(appUser.getPhone());
record.setAddress(StringUtils.isNotBlank(appUser.getAddress())
? appUser.getAddress() : appUser.getDomicileAddress());
record.setCheckInTime(new java.util.Date());
return toAjax(outdoorFairAttendeeService.save(record));
}
/**

View File

@@ -22,6 +22,9 @@ public class OutdoorFairAttendee extends BaseEntity
/** 招聘会ID */
private Long fairId;
/** 移动端用户ID */
private Long userId;
/** 人员姓名 */
private String name;

View File

@@ -0,0 +1,22 @@
package com.ruoyi.cms.domain.vo;
import lombok.Data;
/**
* 户外招聘会后台录入签到人员时使用的移动端用户选项。
*/
@Data
public class OutdoorFairAttendeeUserOption
{
/** 移动端用户ID */
private Long userId;
/** 用户名称 */
private String name;
/** 手机号 */
private String phone;
/** 用户地址 */
private String address;
}

View File

@@ -2,10 +2,24 @@ package com.ruoyi.cms.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.cms.domain.OutdoorFairAttendee;
import com.ruoyi.cms.domain.vo.OutdoorFairAttendeeUserOption;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 户外招聘会签到人员 Mapper。
*/
public interface OutdoorFairAttendeeMapper extends BaseMapper<OutdoorFairAttendee>
{
/**
* 查询签到人员列表。关联移动端用户后,优先返回用户当前信息。
*/
List<OutdoorFairAttendee> selectAttendeeList(@Param("query") OutdoorFairAttendee query);
/**
* 查询当前招聘会尚未签到的移动端求职者。
*/
List<OutdoorFairAttendeeUserOption> selectAvailableUserOptions(@Param("fairId") Long fairId,
@Param("keyword") String keyword);
}

View File

@@ -2,6 +2,7 @@ package com.ruoyi.cms.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.cms.domain.OutdoorFairAttendee;
import com.ruoyi.cms.domain.vo.OutdoorFairAttendeeUserOption;
import java.util.List;
@@ -14,4 +15,9 @@ public interface IOutdoorFairAttendeeService extends IService<OutdoorFairAttende
* 查询签到人员列表(支持按人员姓名、手机号模糊查询)。
*/
List<OutdoorFairAttendee> selectAttendeeList(OutdoorFairAttendee query);
/**
* 查询当前招聘会尚未签到的移动端求职者。
*/
List<OutdoorFairAttendeeUserOption> selectAvailableUserOptions(Long fairId, String keyword);
}

View File

@@ -1,11 +1,10 @@
package com.ruoyi.cms.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.cms.domain.OutdoorFairAttendee;
import com.ruoyi.cms.domain.vo.OutdoorFairAttendeeUserOption;
import com.ruoyi.cms.mapper.OutdoorFairAttendeeMapper;
import com.ruoyi.cms.service.IOutdoorFairAttendeeService;
import com.ruoyi.common.utils.StringUtils;
import org.springframework.stereotype.Service;
import java.util.List;
@@ -21,12 +20,12 @@ public class OutdoorFairAttendeeServiceImpl
@Override
public List<OutdoorFairAttendee> selectAttendeeList(OutdoorFairAttendee query)
{
LambdaQueryWrapper<OutdoorFairAttendee> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(query.getFairId() != null, OutdoorFairAttendee::getFairId, query.getFairId());
wrapper.like(StringUtils.isNotBlank(query.getName()), OutdoorFairAttendee::getName, query.getName());
wrapper.like(StringUtils.isNotBlank(query.getPhone()), OutdoorFairAttendee::getPhone, query.getPhone());
wrapper.orderByDesc(OutdoorFairAttendee::getCheckInTime);
wrapper.orderByDesc(OutdoorFairAttendee::getId);
return list(wrapper);
return baseMapper.selectAttendeeList(query);
}
@Override
public List<OutdoorFairAttendeeUserOption> selectAvailableUserOptions(Long fairId, String keyword)
{
return baseMapper.selectAvailableUserOptions(fairId, keyword);
}
}

View File

@@ -59,6 +59,7 @@ public class PublicJobFairServiceImpl implements IPublicJobFairService {
@Override
public PublicJobFairResponse getJobFairPage(PublicJobFairQuery query) {
query.setJobFairTitle(normalizeJobFairTitle(query.getJobFairTitle()));
PageHelper.startPage(query.getPageNum(), query.getPageSize());
List<PublicJobFair> list = publicJobFairMapper.selectPublicJobFairList(query);
fillJobFairStatus(list);
@@ -180,12 +181,28 @@ public class PublicJobFairServiceImpl implements IPublicJobFairService {
@Override
public List<PublicJobFair> selectList(PublicJobFairQuery query) {
query.setJobFairTitle(normalizeJobFairTitle(query.getJobFairTitle()));
List<PublicJobFair> list = publicJobFairMapper.selectPublicJobFairList(query);
fillJobFairStatus(list);
fillCompanyApplyState(list);
return list;
}
/**
* 将标题查询条件转换为 LIKE 的字面量片段。
*
* <p>标题搜索是普通的包含匹配,用户输入的通配符不应改变 SQL 的匹配范围。</p>
*/
static String normalizeJobFairTitle(String title) {
if (StringUtils.isBlank(title)) {
return null;
}
return title.trim()
.replace("\\", "\\\\")
.replace("%", "\\%")
.replace("_", "\\_");
}
@Override
public PublicJobFair selectById(String jobFairId) {
PublicJobFair jobFair = publicJobFairMapper.selectPublicJobFairById(jobFairId);

View File

@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.cms.mapper.OutdoorFairAttendeeMapper">
<resultMap id="OutdoorFairAttendeeResult"
type="com.ruoyi.cms.domain.OutdoorFairAttendee">
<id property="id" column="id"/>
<result property="fairId" column="fair_id"/>
<result property="userId" column="user_id"/>
<result property="name" column="name"/>
<result property="phone" column="phone"/>
<result property="address" column="address"/>
<result property="checkInTime" column="check_in_time"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
<result property="remark" column="remark"/>
<result property="delFlag" column="del_flag"/>
</resultMap>
<resultMap id="OutdoorFairAttendeeUserOptionResult"
type="com.ruoyi.cms.domain.vo.OutdoorFairAttendeeUserOption">
<result property="userId" column="user_id"/>
<result property="name" column="name"/>
<result property="phone" column="phone"/>
<result property="address" column="address"/>
</resultMap>
<select id="selectAttendeeList"
parameterType="com.ruoyi.cms.domain.OutdoorFairAttendee"
resultMap="OutdoorFairAttendeeResult">
SELECT a.id,
a.fair_id,
a.user_id,
COALESCE(NULLIF(u.name, ''), a.name) AS name,
COALESCE(NULLIF(u.phone, ''), a.phone) AS phone,
COALESCE(NULLIF(u.address, ''), NULLIF(u.domicile_address, ''), a.address) AS address,
a.check_in_time,
a.create_by,
a.create_time,
a.update_by,
a.update_time,
a.remark,
a.del_flag
FROM shz.cms_outdoor_fair_attendee a
LEFT JOIN app_user u ON u.user_id = a.user_id AND u.del_flag = '0'
<where>
a.del_flag = '0'
<if test="query != null and query.fairId != null">
AND a.fair_id = #{query.fairId}
</if>
<if test="query != null and query.name != null and query.name != ''">
AND COALESCE(NULLIF(u.name, ''), a.name) LIKE CONCAT('%', CAST(#{query.name} AS VARCHAR), '%')
</if>
<if test="query != null and query.phone != null and query.phone != ''">
AND COALESCE(NULLIF(u.phone, ''), a.phone) LIKE CONCAT('%', CAST(#{query.phone} AS VARCHAR), '%')
</if>
</where>
ORDER BY a.check_in_time DESC, a.id DESC
</select>
<select id="selectAvailableUserOptions"
resultMap="OutdoorFairAttendeeUserOptionResult">
SELECT u.user_id,
u.name,
u.phone,
COALESCE(u.address, u.domicile_address) AS address
FROM app_user u
WHERE u.del_flag = '0'
AND u.is_company_user = '1'
AND NULLIF(u.name, '') IS NOT NULL
AND NOT EXISTS (
SELECT 1
FROM shz.cms_outdoor_fair_attendee a
WHERE a.fair_id = #{fairId}
AND a.user_id = u.user_id
AND a.del_flag = '0'
)
<if test="keyword != null and keyword != ''">
AND (
u.name LIKE CONCAT('%', CAST(#{keyword} AS VARCHAR), '%')
OR u.phone LIKE CONCAT('%', CAST(#{keyword} AS VARCHAR), '%')
)
</if>
ORDER BY u.name ASC NULLS LAST, u.user_id ASC
</select>
</mapper>

View File

@@ -9,10 +9,13 @@ import com.ruoyi.cms.mapper.OutdoorFairBoothBookingMapper;
import com.ruoyi.cms.mapper.OutdoorFairBoothMapper;
import com.ruoyi.cms.mapper.OutdoorFairJobMapper;
import com.ruoyi.cms.service.IOutdoorFairAttendeeService;
import com.ruoyi.cms.service.IAppUserService;
import com.ruoyi.cms.service.IOutdoorFairService;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.AppUser;
import com.ruoyi.common.core.domain.entity.Company;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.mock.web.MockHttpServletRequest;
import java.lang.reflect.Field;
@@ -37,11 +40,13 @@ class AppOutdoorFairControllerTest
{
IOutdoorFairService fairService = mock(IOutdoorFairService.class);
IOutdoorFairAttendeeService attendeeService = mock(IOutdoorFairAttendeeService.class);
IAppUserService appUserService = mock(IAppUserService.class);
OutdoorFair fair = fairAt(new Date(System.currentTimeMillis() + 60_000L));
when(fairService.getById(12L)).thenReturn(fair);
AppOutdoorFairController controller = controllerWith(fairService, attendeeService);
when(appUserService.selectAppUserByUserId(99L)).thenReturn(appUser());
AppOutdoorFairController controller = controllerWith(fairService, attendeeService, appUserService);
AjaxResult result = controller.checkIn(attendee(12L));
AjaxResult result = controller.checkIn(12L, 99L);
assertEquals(500, result.get(AjaxResult.CODE_TAG));
assertEquals("招聘会还没有开始,暂不能签到", result.get(AjaxResult.MSG_TAG));
@@ -53,10 +58,12 @@ class AppOutdoorFairControllerTest
{
IOutdoorFairService fairService = mock(IOutdoorFairService.class);
IOutdoorFairAttendeeService attendeeService = mock(IOutdoorFairAttendeeService.class);
IAppUserService appUserService = mock(IAppUserService.class);
when(fairService.getById(12L)).thenReturn(fairAt(null));
AppOutdoorFairController controller = controllerWith(fairService, attendeeService);
when(appUserService.selectAppUserByUserId(99L)).thenReturn(appUser());
AppOutdoorFairController controller = controllerWith(fairService, attendeeService, appUserService);
AjaxResult result = controller.checkIn(attendee(12L));
AjaxResult result = controller.checkIn(12L, 99L);
assertEquals(500, result.get(AjaxResult.CODE_TAG));
assertEquals("招聘会还没有开始,暂不能签到", result.get(AjaxResult.MSG_TAG));
@@ -68,13 +75,15 @@ class AppOutdoorFairControllerTest
{
IOutdoorFairService fairService = mock(IOutdoorFairService.class);
IOutdoorFairAttendeeService attendeeService = mock(IOutdoorFairAttendeeService.class);
IAppUserService appUserService = mock(IAppUserService.class);
OutdoorFair fair = fairAt(new Date(System.currentTimeMillis() - 1_000L));
when(fairService.getById(12L)).thenReturn(fair);
when(appUserService.selectAppUserByUserId(99L)).thenReturn(appUser());
when(attendeeService.count(any())).thenReturn(0L);
when(attendeeService.save(any(OutdoorFairAttendee.class))).thenReturn(true);
AppOutdoorFairController controller = controllerWith(fairService, attendeeService);
AppOutdoorFairController controller = controllerWith(fairService, attendeeService, appUserService);
AjaxResult result = controller.checkIn(attendee(12L));
AjaxResult result = controller.checkIn(12L, 99L);
assertEquals(200, result.get(AjaxResult.CODE_TAG));
verify(attendeeService).save(any(OutdoorFairAttendee.class));
@@ -83,6 +92,72 @@ class AppOutdoorFairControllerTest
fair, new Date(fair.getHoldTime().getTime() - 1L)));
}
@Test
void checkInUsesCurrentMobileUserProfile() throws Exception
{
IOutdoorFairService fairService = mock(IOutdoorFairService.class);
IOutdoorFairAttendeeService attendeeService = mock(IOutdoorFairAttendeeService.class);
IAppUserService appUserService = mock(IAppUserService.class);
OutdoorFair fair = fairAt(new Date(System.currentTimeMillis() - 1_000L));
AppUser appUser = appUser();
appUser.setAddress("石河子市测试地址");
when(fairService.getById(12L)).thenReturn(fair);
when(appUserService.selectAppUserByUserId(99L)).thenReturn(appUser);
when(attendeeService.count(any())).thenReturn(0L);
when(attendeeService.save(any(OutdoorFairAttendee.class))).thenReturn(true);
AppOutdoorFairController controller = controllerWith(fairService, attendeeService, appUserService);
AjaxResult result = controller.checkIn(12L, 99L);
assertEquals(200, result.get(AjaxResult.CODE_TAG));
ArgumentCaptor<OutdoorFairAttendee> captor = ArgumentCaptor.forClass(OutdoorFairAttendee.class);
verify(attendeeService).save(captor.capture());
OutdoorFairAttendee record = captor.getValue();
assertEquals(12L, record.getFairId());
assertEquals(99L, record.getUserId());
assertEquals("测试人员", record.getName());
assertEquals("13800000000", record.getPhone());
assertEquals("石河子市测试地址", record.getAddress());
assertTrue(record.getCheckInTime() != null);
}
@Test
void duplicateUserCannotCheckInAgain() throws Exception
{
IOutdoorFairService fairService = mock(IOutdoorFairService.class);
IOutdoorFairAttendeeService attendeeService = mock(IOutdoorFairAttendeeService.class);
IAppUserService appUserService = mock(IAppUserService.class);
when(fairService.getById(12L)).thenReturn(fairAt(new Date(System.currentTimeMillis() - 1_000L)));
when(appUserService.selectAppUserByUserId(99L)).thenReturn(appUser());
when(attendeeService.count(any())).thenReturn(1L);
AppOutdoorFairController controller = controllerWith(fairService, attendeeService, appUserService);
AjaxResult result = controller.checkIn(12L, 99L);
assertEquals(500, result.get(AjaxResult.CODE_TAG));
assertEquals("您已签到,无需重复签到", result.get(AjaxResult.MSG_TAG));
verify(attendeeService, never()).save(any(OutdoorFairAttendee.class));
}
@Test
void nonJobSeekerCannotCheckIn() throws Exception
{
IOutdoorFairService fairService = mock(IOutdoorFairService.class);
IOutdoorFairAttendeeService attendeeService = mock(IOutdoorFairAttendeeService.class);
IAppUserService appUserService = mock(IAppUserService.class);
AppUser appUser = appUser();
appUser.setIsCompanyUser("0");
when(appUserService.selectAppUserByUserId(99L)).thenReturn(appUser);
AppOutdoorFairController controller = controllerWith(fairService, attendeeService, appUserService);
AjaxResult result = controller.checkIn(12L, 99L);
assertEquals(500, result.get(AjaxResult.CODE_TAG));
assertEquals("仅移动端求职者可以签到", result.get(AjaxResult.MSG_TAG));
verify(fairService, never()).getById(12L);
verify(attendeeService, never()).save(any(OutdoorFairAttendee.class));
}
@SuppressWarnings("unchecked")
@Test
void publicDetailIncludesOnlySafeCompanyCardFields() throws Exception
@@ -144,6 +219,17 @@ class AppOutdoorFairControllerTest
AppOutdoorFairController controller = new AppOutdoorFairController();
setField(controller, "outdoorFairService", fairService);
setField(controller, "outdoorFairAttendeeService", attendeeService);
setField(controller, "appUserService", mock(IAppUserService.class));
return controller;
}
private AppOutdoorFairController controllerWith(IOutdoorFairService fairService,
IOutdoorFairAttendeeService attendeeService,
IAppUserService appUserService)
throws Exception
{
AppOutdoorFairController controller = controllerWith(fairService, attendeeService);
setField(controller, "appUserService", appUserService);
return controller;
}
@@ -154,13 +240,15 @@ class AppOutdoorFairControllerTest
field.set(target, value);
}
private OutdoorFairAttendee attendee(Long fairId)
private AppUser appUser()
{
OutdoorFairAttendee attendee = new OutdoorFairAttendee();
attendee.setFairId(fairId);
attendee.setName("测试人员");
attendee.setPhone("13800000000");
return attendee;
AppUser appUser = new AppUser();
appUser.setUserId(99L);
appUser.setName("测试人员");
appUser.setPhone("13800000000");
appUser.setIsCompanyUser("1");
appUser.setDelFlag("0");
return appUser;
}
private OutdoorFair fairAt(Date holdTime)

View File

@@ -39,6 +39,18 @@ class PublicJobFairServiceImplTest
assertNull(PublicJobFairServiceImpl.resolveJobFairStatus(START_TIME, null, END_TIME));
}
@Test
void titleSearchEscapesLikeWildcardsAndTrimsWhitespace()
{
assertEquals("100\\%\\_\\\\", PublicJobFairServiceImpl.normalizeJobFairTitle(" 100%_\\ "));
}
@Test
void blankTitleSearchIsIgnored()
{
assertNull(PublicJobFairServiceImpl.normalizeJobFairTitle(" \t"));
}
private String resolveStatus(Date now)
{
return PublicJobFairServiceImpl.resolveJobFairStatus(START_TIME, END_TIME, now);

View File

@@ -7,6 +7,7 @@ CREATE SEQUENCE IF NOT EXISTS shz.cms_outdoor_fair_attendee_id_seq
CREATE TABLE IF NOT EXISTS shz.cms_outdoor_fair_attendee (
id BIGINT NOT NULL DEFAULT nextval('shz.cms_outdoor_fair_attendee_id_seq'::regclass) PRIMARY KEY,
fair_id BIGINT NOT NULL,
user_id BIGINT,
name VARCHAR(64) NOT NULL,
phone VARCHAR(20),
address VARCHAR(255),
@@ -22,9 +23,14 @@ CREATE TABLE IF NOT EXISTS shz.cms_outdoor_fair_attendee (
CREATE INDEX IF NOT EXISTS idx_cms_outdoor_fair_attendee_fair
ON shz.cms_outdoor_fair_attendee (fair_id);
CREATE UNIQUE INDEX IF NOT EXISTS uk_cms_outdoor_fair_attendee_user
ON shz.cms_outdoor_fair_attendee (fair_id, user_id)
WHERE user_id IS NOT NULL AND del_flag = '0';
COMMENT ON TABLE shz.cms_outdoor_fair_attendee IS '户外招聘会签到人员表';
COMMENT ON COLUMN shz.cms_outdoor_fair_attendee.id IS '主键';
COMMENT ON COLUMN shz.cms_outdoor_fair_attendee.fair_id IS '招聘会ID';
COMMENT ON COLUMN shz.cms_outdoor_fair_attendee.user_id IS '移动端用户ID';
COMMENT ON COLUMN shz.cms_outdoor_fair_attendee.name IS '人员姓名';
COMMENT ON COLUMN shz.cms_outdoor_fair_attendee.phone IS '手机号';
COMMENT ON COLUMN shz.cms_outdoor_fair_attendee.address IS '住址';

View File

@@ -0,0 +1,149 @@
-- 执行状态:测试环境已执行,功能验证已通过,正式环境待执行
-- 测试环境test
-- 测试执行时间2026-07-24 09:39:31 +0800
-- 功能验证时间2026-07-24 09:56:15 +0800
-- 正式环境:待明确授权后执行
-- 招聘会机构管理及场地所属机构多选关联。
-- 依赖招聘会管理目录menu_name='招聘会管理')和场地信息维护权限已存在。
-- 可重复执行;正式环境仅在功能验收通过并获单独授权后执行。
BEGIN;
SET LOCAL search_path = shz, public;
CREATE SEQUENCE IF NOT EXISTS cms_job_fair_institution_id_seq
START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1;
CREATE SEQUENCE IF NOT EXISTS cms_venue_institution_id_seq
START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1;
CREATE TABLE IF NOT EXISTS cms_job_fair_institution (
id BIGINT NOT NULL DEFAULT nextval('cms_job_fair_institution_id_seq') PRIMARY KEY,
institution_name VARCHAR(200) NOT NULL,
institution_intro TEXT,
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_job_fair_institution IS '招聘会机构管理表';
COMMENT ON COLUMN cms_job_fair_institution.institution_name IS '机构名称';
COMMENT ON COLUMN cms_job_fair_institution.institution_intro IS '机构简介';
COMMENT ON COLUMN cms_job_fair_institution.del_flag IS '删除标志0代表存在 2代表删除';
CREATE UNIQUE INDEX IF NOT EXISTS uk_cms_job_fair_institution_name
ON cms_job_fair_institution (institution_name) WHERE del_flag = '0';
CREATE TABLE IF NOT EXISTS cms_venue_institution (
id BIGINT NOT NULL DEFAULT nextval('cms_venue_institution_id_seq') PRIMARY KEY,
venue_id BIGINT NOT NULL,
institution_id BIGINT NOT NULL
);
COMMENT ON TABLE cms_venue_institution IS '场地与招聘会机构关联表';
COMMENT ON COLUMN cms_venue_institution.venue_id IS '场地ID';
COMMENT ON COLUMN cms_venue_institution.institution_id IS '机构ID';
CREATE UNIQUE INDEX IF NOT EXISTS uk_cms_venue_institution
ON cms_venue_institution (venue_id, institution_id);
CREATE INDEX IF NOT EXISTS idx_cms_venue_institution_institution
ON cms_venue_institution (institution_id);
WITH parent_menu AS (
SELECT menu_id
FROM sys_menu
WHERE menu_name = '招聘会管理' AND parent_id = 0
ORDER BY menu_id
LIMIT 1
), inserted_page AS (
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, remark
)
SELECT
(SELECT COALESCE(MAX(menu_id), 0) + 1 FROM sys_menu),
'机构管理', parent_menu.menu_id, 3, 'institution', 'jobfair/institution/index', NULL, '',
1, 0, 'C', '0', '0', 'cms:jobFairInstitution:list', '#',
'system', NOW(), '招聘会机构管理菜单'
FROM parent_menu
WHERE NOT EXISTS (
SELECT 1
FROM sys_menu existing
WHERE existing.parent_id = parent_menu.menu_id
AND existing.menu_name = '机构管理'
)
RETURNING menu_id
), institution_page AS (
SELECT menu_id FROM inserted_page
UNION
SELECT menu_id
FROM sys_menu
WHERE menu_name = '机构管理'
AND parent_id = (SELECT menu_id FROM parent_menu)
), button_seed(menu_name, order_num, perms) AS (
VALUES
('机构查询', 1, 'cms:jobFairInstitution:query'),
('机构新增', 2, 'cms:jobFairInstitution:add'),
('机构修改', 3, 'cms:jobFairInstitution:edit'),
('机构删除', 4, 'cms:jobFairInstitution:remove')
), max_menu AS (
SELECT COALESCE(MAX(menu_id), 0) AS menu_id FROM sys_menu
)
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, remark
)
SELECT
max_menu.menu_id + 1 + ROW_NUMBER() OVER (ORDER BY button_seed.order_num),
button_seed.menu_name, institution_page.menu_id, button_seed.order_num, '', NULL, NULL, '',
1, 0, 'F', '0', '0', button_seed.perms, '#',
'system', NOW(), '招聘会机构管理按钮权限'
FROM institution_page
CROSS JOIN button_seed
CROSS JOIN max_menu
WHERE NOT EXISTS (
SELECT 1 FROM sys_menu existing WHERE existing.perms = button_seed.perms
);
WITH institution_menus AS (
SELECT menu_id
FROM sys_menu
WHERE perms IN (
'cms:jobFairInstitution:list',
'cms:jobFairInstitution:query',
'cms:jobFairInstitution:add',
'cms:jobFairInstitution:edit',
'cms:jobFairInstitution:remove'
)
UNION
SELECT menu_id
FROM sys_menu
WHERE menu_name = '机构管理'
AND parent_id = (SELECT menu_id FROM sys_menu WHERE menu_name = '招聘会管理' AND parent_id = 0 ORDER BY menu_id LIMIT 1)
), venue_roles AS (
SELECT DISTINCT role_id
FROM sys_role_menu
WHERE menu_id IN (
SELECT menu_id FROM sys_menu WHERE perms = 'cms:venueInfo:list'
)
), role_menu_rows AS (
SELECT venue_roles.role_id, institution_menus.menu_id
FROM venue_roles
CROSS JOIN institution_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
);
COMMIT;

View File

@@ -0,0 +1,18 @@
-- 执行状态:测试环境已执行
-- 测试环境test
-- 测试执行时间2026-07-24 10:16:38 +0800
-- 正式环境:未执行
-- 户外招聘会签到记录关联移动端用户
-- 历史签到记录 user_id 保持为空,仍使用原有快照字段展示。
ALTER TABLE shz.cms_outdoor_fair_attendee
ADD COLUMN IF NOT EXISTS user_id BIGINT;
COMMENT ON COLUMN shz.cms_outdoor_fair_attendee.user_id IS '移动端用户ID';
DROP INDEX IF EXISTS shz.uk_cms_outdoor_fair_attendee_user;
CREATE UNIQUE INDEX uk_cms_outdoor_fair_attendee_user
ON shz.cms_outdoor_fair_attendee (fair_id, user_id)
WHERE user_id IS NOT NULL AND del_flag = '0';