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) + '
' +
'' +
- '' +
- '' +
- '
' +
'' +
'招聘会信息
' +
'' +
(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';