diff --git a/docs/app-outdoor-fair-h5-api.md b/docs/app-outdoor-fair-h5-api.md index cb00790..318d195 100644 --- a/docs/app-outdoor-fair-h5-api.md +++ b/docs/app-outdoor-fair-h5-api.md @@ -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 | 页面 | 说明 | | --- | --- | diff --git a/ruoyi-admin/src/main/resources/static/h5/outdoor-fair/company.html b/ruoyi-admin/src/main/resources/static/h5/outdoor-fair/company.html index a933458..ae3bfcf 100644 --- a/ruoyi-admin/src/main/resources/static/h5/outdoor-fair/company.html +++ b/ruoyi-admin/src/main/resources/static/h5/outdoor-fair/company.html @@ -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; } @@ -169,7 +140,6 @@ '

' + escapeHtml(fair.address) + '

' + '' + '
' + - '' + '查看招聘会' + '
' + '
' + @@ -193,87 +163,7 @@ (jobs.length ? '
' + jobs.map(function (job) { return renderJob(job, educationDict, experienceDict); }).join('') + '
' : '
暂无岗位
') + - '
' + - '
' + - '
' + - '

签到

' + - '

请填写您的姓名和手机号完成签到

' + - '
' + - '' + - '' + - '
' + - '
' + - '' + - '' + - '
' + - '
' + - '' + - '' + - '
' + - '
' + - '
'; - 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); - }); + ''; } function renderJob(job, educationDict, experienceDict) { diff --git a/ruoyi-admin/src/main/resources/static/h5/outdoor-fair/detail.html b/ruoyi-admin/src/main/resources/static/h5/outdoor-fair/detail.html index 5c30463..9138323 100644 --- a/ruoyi-admin/src/main/resources/static/h5/outdoor-fair/detail.html +++ b/ruoyi-admin/src/main/resources/static/h5/outdoor-fair/detail.html @@ -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; } @@ -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 = '
' + @@ -111,9 +79,6 @@ '

' + escapeHtml(fair.hostUnit) + '

' + '

' + escapeHtml(fair.address) + '

' + '
' + - '
' + - '' + - '
' + '
' + '

招聘会信息

' + '
' + @@ -125,80 +90,7 @@ '
展位数量' + escapeHtml(fair.boothCount) + '
' + '
' + (fair.photoUrl ? '招聘会照片' : '') + - '
' + - '
' + - '
' + - '

签到

' + - '

请填写您的姓名和手机号完成签到

' + - '
' + - '' + - '' + - '
' + - '
' + - '' + - '' + - '
' + - '
' + - '' + - '' + - '
' + - '
' + - '
'; - 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); - }); + ''; } if (!fairId) { diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/app/AppOutdoorFairController.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/app/AppOutdoorFairController.java index d476594..a8b1ffe 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/app/AppOutdoorFairController.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/app/AppOutdoorFairController.java @@ -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 data = new HashMap<>(); - data.put("fair", buildPublicFairDetail(fair)); + Map 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() - .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() + .eq(OutdoorFairAttendee::getFairId, fairId) + .eq(OutdoorFairAttendee::getUserId, userId)); + return count != null && count > 0; + } + private Map buildCompanyPage(Long fairId, Long companyId, OutdoorFairDevice device, HttpServletRequest request) { diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairAttendeeController.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairAttendeeController.java index eb34f61..2ed10c4 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairAttendeeController.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairAttendeeController.java @@ -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 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() + .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)); } /** diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/OutdoorFairAttendee.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/OutdoorFairAttendee.java index 0c37448..e2853d5 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/OutdoorFairAttendee.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/OutdoorFairAttendee.java @@ -22,6 +22,9 @@ public class OutdoorFairAttendee extends BaseEntity /** 招聘会ID */ private Long fairId; + /** 移动端用户ID */ + private Long userId; + /** 人员姓名 */ private String name; diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/OutdoorFairAttendeeUserOption.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/OutdoorFairAttendeeUserOption.java new file mode 100644 index 0000000..f60ea8a --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/OutdoorFairAttendeeUserOption.java @@ -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; +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairAttendeeMapper.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairAttendeeMapper.java index 98e7c1c..ec20f77 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairAttendeeMapper.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairAttendeeMapper.java @@ -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 { + /** + * 查询签到人员列表。关联移动端用户后,优先返回用户当前信息。 + */ + List selectAttendeeList(@Param("query") OutdoorFairAttendee query); + + /** + * 查询当前招聘会尚未签到的移动端求职者。 + */ + List selectAvailableUserOptions(@Param("fairId") Long fairId, + @Param("keyword") String keyword); } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/IOutdoorFairAttendeeService.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/IOutdoorFairAttendeeService.java index 3281453..aaae905 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/IOutdoorFairAttendeeService.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/IOutdoorFairAttendeeService.java @@ -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 selectAttendeeList(OutdoorFairAttendee query); + + /** + * 查询当前招聘会尚未签到的移动端求职者。 + */ + List selectAvailableUserOptions(Long fairId, String keyword); } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/OutdoorFairAttendeeServiceImpl.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/OutdoorFairAttendeeServiceImpl.java index 3231aaa..559e477 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/OutdoorFairAttendeeServiceImpl.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/OutdoorFairAttendeeServiceImpl.java @@ -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 selectAttendeeList(OutdoorFairAttendee query) { - LambdaQueryWrapper 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 selectAvailableUserOptions(Long fairId, String keyword) + { + return baseMapper.selectAvailableUserOptions(fairId, keyword); } } diff --git a/ruoyi-bussiness/src/main/resources/mapper/cms/OutdoorFairAttendeeMapper.xml b/ruoyi-bussiness/src/main/resources/mapper/cms/OutdoorFairAttendeeMapper.xml new file mode 100644 index 0000000..c81ef0b --- /dev/null +++ b/ruoyi-bussiness/src/main/resources/mapper/cms/OutdoorFairAttendeeMapper.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ruoyi-bussiness/src/test/java/com/ruoyi/cms/controller/app/AppOutdoorFairControllerTest.java b/ruoyi-bussiness/src/test/java/com/ruoyi/cms/controller/app/AppOutdoorFairControllerTest.java index 0d67ddd..965afe7 100644 --- a/ruoyi-bussiness/src/test/java/com/ruoyi/cms/controller/app/AppOutdoorFairControllerTest.java +++ b/ruoyi-bussiness/src/test/java/com/ruoyi/cms/controller/app/AppOutdoorFairControllerTest.java @@ -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 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) diff --git a/sql/cms_outdoor_fair_attendee.sql b/sql/cms_outdoor_fair_attendee.sql index 2a21bbd..2b605e3 100644 --- a/sql/cms_outdoor_fair_attendee.sql +++ b/sql/cms_outdoor_fair_attendee.sql @@ -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 '住址'; diff --git a/sql/migration_outdoor_fair_attendee_user.test-executed.sql b/sql/migration_outdoor_fair_attendee_user.test-executed.sql new file mode 100644 index 0000000..cb152ac --- /dev/null +++ b/sql/migration_outdoor_fair_attendee_user.test-executed.sql @@ -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';