feat: 新增招聘会企业签到与未参会统计

This commit is contained in:
2026-07-24 12:35:25 +08:00
parent 357a861338
commit 0dc26a9e2e
18 changed files with 935 additions and 8 deletions

View File

@@ -8,6 +8,7 @@ import com.ruoyi.cms.domain.OutdoorFair;
import com.ruoyi.cms.domain.OutdoorFairAttendee;
import com.ruoyi.cms.domain.OutdoorFairBooth;
import com.ruoyi.cms.domain.OutdoorFairBoothBooking;
import com.ruoyi.cms.domain.OutdoorFairCompanyAttendance;
import com.ruoyi.cms.domain.OutdoorFairDevice;
import com.ruoyi.cms.mapper.CompanyMapper;
import com.ruoyi.cms.mapper.FairCompanyMapper;
@@ -15,12 +16,14 @@ import com.ruoyi.cms.mapper.OutdoorFairBoothMapper;
import com.ruoyi.cms.mapper.OutdoorFairBoothBookingMapper;
import com.ruoyi.cms.mapper.OutdoorFairDeviceMapper;
import com.ruoyi.cms.mapper.OutdoorFairJobMapper;
import com.ruoyi.cms.mapper.OutdoorFairCompanyAttendanceMapper;
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.RoleUtils;
import com.ruoyi.cms.util.StringUtil;
import com.ruoyi.common.annotation.BussinessLog;
import com.ruoyi.common.core.controller.BaseController;
@@ -40,6 +43,7 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.HttpServletRequest;
import java.text.ParseException;
@@ -83,6 +87,9 @@ public class AppOutdoorFairController extends BaseController
@Autowired
private OutdoorFairJobMapper outdoorFairJobMapper;
@Autowired
private OutdoorFairCompanyAttendanceMapper outdoorFairCompanyAttendanceMapper;
@Autowired
private IOutdoorFairAttendeeService outdoorFairAttendeeService;
@@ -221,6 +228,113 @@ public class AppOutdoorFairController extends BaseController
return checkIn(fairId, SiteSecurityUtils.getUserId());
}
/**
* 当前登录企业在招聘会举办期间签到。小程序暂不接入页面,但保留独立的企业接口供后续使用。
*/
@BussinessLog(title = "户外招聘会企业签到")
@ApiOperation("移动端企业签到户外招聘会")
@PostMapping("/company/check-in")
@Transactional(rollbackFor = Exception.class)
public AjaxResult companyCheckIn(@RequestParam Long fairId)
{
if (!SiteSecurityUtils.isLogin()) {
return AjaxResult.error("请先登录");
}
AppUser appUser = RoleUtils.getAppUser();
return companyCheckIn(fairId, appUser);
}
/** 签到业务实现单独保留 AppUser 入参,便于单元测试覆盖企业权限和业务规则。 */
AjaxResult companyCheckIn(Long fairId, AppUser appUser)
{
if (fairId == null) {
return AjaxResult.error("缺少招聘会参数");
}
if (appUser == null) {
return AjaxResult.error("用户信息不存在,请重新登录");
}
if (!StringUtil.IS_COMPANY_USER.equals(appUser.getIsCompanyUser())) {
return AjaxResult.error("仅移动端企业可以签到");
}
Company company = null;
if (StringUtils.isNotBlank(appUser.getIdCard())) {
company = companyMapper.selectOne(new LambdaQueryWrapper<Company>()
.eq(Company::getCode, appUser.getIdCard())
.eq(Company::getDelFlag, "0")
.last("LIMIT 1"));
}
if (company == null && appUser.getUserId() != null) {
company = companyMapper.selectOne(new LambdaQueryWrapper<Company>()
.eq(Company::getUserId, appUser.getUserId())
.eq(Company::getDelFlag, "0")
.last("LIMIT 1"));
}
if (company == null) {
return AjaxResult.error("未找到当前企业信息");
}
OutdoorFair fair = outdoorFairService.getById(fairId);
if (fair == null) {
return AjaxResult.error("招聘会不存在");
}
String timeError = validateCompanyCheckInTime(fair, new Date());
if (timeError != null) {
return AjaxResult.error(timeError);
}
FairCompany relation = fairCompanyMapper.selectOne(new LambdaQueryWrapper<FairCompany>()
.eq(FairCompany::getJobFairId, fairId)
.eq(FairCompany::getCompanyId, company.getCompanyId())
.eq(FairCompany::getDelFlag, "0")
.last("LIMIT 1"));
if (relation == null) {
return AjaxResult.error("该企业未报名当前招聘会");
}
if (!"1".equals(relation.getReviewStatus())) {
return AjaxResult.error("报名审核通过后才能签到");
}
OutdoorFairCompanyAttendance attendance = outdoorFairCompanyAttendanceMapper.selectOne(
new LambdaQueryWrapper<OutdoorFairCompanyAttendance>()
.eq(OutdoorFairCompanyAttendance::getFairId, fairId)
.eq(OutdoorFairCompanyAttendance::getCompanyId, company.getCompanyId())
.eq(OutdoorFairCompanyAttendance::getDelFlag, "0")
.last("LIMIT 1"));
if (attendance != null && "1".equals(attendance.getAttendanceStatus())) {
return AjaxResult.error("该企业已签到,无需重复签到");
}
Date checkInTime = new Date();
if (attendance == null) {
attendance = new OutdoorFairCompanyAttendance();
attendance.setFairId(fairId);
attendance.setCompanyId(company.getCompanyId());
attendance.setCreateBy(String.valueOf(appUser.getUserId()));
attendance.setCreateTime(checkInTime);
}
attendance.setCompanyName(company.getName());
attendance.setContactPerson(company.getContactPerson());
attendance.setContactPhone(company.getContactPersonPhone());
attendance.setAttendanceStatus("1");
attendance.setCheckInTime(checkInTime);
attendance.setUpdateBy(String.valueOf(appUser.getUserId()));
attendance.setUpdateTime(checkInTime);
boolean saved = attendance.getId() == null
? outdoorFairCompanyAttendanceMapper.insert(attendance) > 0
: outdoorFairCompanyAttendanceMapper.updateById(attendance) > 0;
if (!saved) {
return AjaxResult.error("签到失败,请稍后重试");
}
Map<String, Object> data = new HashMap<>();
data.put("fairId", fairId);
data.put("companyId", company.getCompanyId());
data.put("checkInTime", checkInTime);
data.put("attendanceStatus", "checked_in");
return success(data);
}
/**
* 签到业务实现单独保留 userId 入参,便于无 Web 容器的单元测试覆盖业务规则。
*/
@@ -276,6 +390,20 @@ public class AppOutdoorFairController extends BaseController
&& !currentTime.before(fair.getHoldTime());
}
private static String validateCompanyCheckInTime(OutdoorFair fair, Date currentTime)
{
if (fair == null || fair.getHoldTime() == null || fair.getEndTime() == null || currentTime == null) {
return "招聘会举办时间不完整,暂不能签到";
}
if (currentTime.before(fair.getHoldTime())) {
return "招聘会还没有开始,暂不能签到";
}
if (currentTime.after(fair.getEndTime())) {
return "招聘会已结束,不能签到";
}
return null;
}
private boolean hasCheckedIn(Long fairId)
{
if (!SiteSecurityUtils.isLogin()) {

View File

@@ -2,8 +2,10 @@ package com.ruoyi.cms.controller.cms;
import com.ruoyi.cms.domain.query.CompanySearch;
import com.ruoyi.cms.domain.vo.CompanyMissedOutdoorFairVO;
import com.ruoyi.cms.domain.query.JobSearch;
import com.ruoyi.cms.mapper.AppUserMapper;
import com.ruoyi.cms.mapper.CompanyMapper;
import com.ruoyi.cms.util.RoleUtils;
import com.ruoyi.cms.util.sensitiveWord.SensitiveWordChecker;
import com.ruoyi.common.core.domain.entity.AppUser;
@@ -46,6 +48,9 @@ public class CompanyController extends BaseController
@Autowired
private AppUserMapper appUserMapper;
@Autowired
private CompanyMapper companyMapper;
@Autowired
SensitiveWordChecker sensitiveWordChecker;
@@ -70,6 +75,26 @@ public class CompanyController extends BaseController
return getDataTable(list);
}
/**
* 查询企业未参加的已结束户外招聘会明细。
* 失信口径为:报名审核通过、招聘会已结束且企业签到状态不是已签到。
*/
@ApiOperation("查询企业未参加招聘会明细")
@PreAuthorize("@ss.hasPermi('cms:company:list')")
@GetMapping("/{companyId}/missed-outdoor-fairs")
public TableDataInfo missedOutdoorFairs(@PathVariable Long companyId)
{
if (RoleUtils.isCompanyAdmin()) {
Company currentCompany = getCurrentCompany();
if (currentCompany == null || !companyId.equals(currentCompany.getCompanyId())) {
return getDataTable(new ArrayList<>());
}
}
startPage();
List<CompanyMissedOutdoorFairVO> list = companyMapper.selectMissedOutdoorFairList(companyId);
return getDataTable(list);
}
/**
* 导出公司列表
*/

View File

@@ -24,15 +24,18 @@ import com.ruoyi.cms.domain.Job;
import com.ruoyi.cms.domain.OutdoorFair;
import com.ruoyi.cms.domain.OutdoorFairBooth;
import com.ruoyi.cms.domain.OutdoorFairBoothBooking;
import com.ruoyi.cms.domain.OutdoorFairCompanyAttendance;
import com.ruoyi.cms.domain.OutdoorFairCompanyUnsubscribe;
import com.ruoyi.cms.domain.OutdoorFairDevice;
import com.ruoyi.cms.domain.OutdoorFairDictDataRequest;
import com.ruoyi.cms.domain.OutdoorFairJob;
import com.ruoyi.cms.domain.VenueBooth;
import com.ruoyi.cms.domain.VenueInfo;
import com.ruoyi.cms.domain.vo.OutdoorFairCompanyAttendanceVO;
import com.ruoyi.cms.mapper.FairCompanyMapper;
import com.ruoyi.cms.mapper.OutdoorFairBoothMapper;
import com.ruoyi.cms.mapper.OutdoorFairBoothBookingMapper;
import com.ruoyi.cms.mapper.OutdoorFairCompanyAttendanceMapper;
import com.ruoyi.cms.mapper.OutdoorFairDeviceMapper;
import com.ruoyi.cms.mapper.OutdoorFairCompanyUnsubscribeMapper;
import com.ruoyi.cms.mapper.OutdoorFairJobMapper;
@@ -97,6 +100,9 @@ public class OutdoorFairController extends BaseController
@Autowired
private OutdoorFairCompanyUnsubscribeMapper outdoorFairCompanyUnsubscribeMapper;
@Autowired
private OutdoorFairCompanyAttendanceMapper outdoorFairCompanyAttendanceMapper;
@Autowired
private CompanyMapper companyMapper;
@@ -604,6 +610,7 @@ public class OutdoorFairController extends BaseController
.set("company_name", null)
.set("bind_time", null));
fairCompanyMapper.deleteById(relation.getId());
invalidateCompanyAttendanceRecord(fairId, company.getCompanyId());
return success();
}
@@ -618,6 +625,116 @@ public class OutdoorFairController extends BaseController
return success(buildCompanyQrCodeData(request, fairId, company.getCompanyId()));
}
/**
* 当前企业在招聘会举办期间签到。
* 只有企业账号、当前企业已报名且审核通过,并且当前时间处于举办时间闭区间内时才允许签到。
*/
@ApiOperation("企业签到户外招聘会")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:companyCheckIn')")
@Log(title = "户外招聘会企业签到", businessType = BusinessType.UPDATE)
@PostMapping("/{fairId}/company/check-in")
@Transactional(rollbackFor = Exception.class)
public AjaxResult checkInCompany(@PathVariable Long fairId)
{
if (!RoleUtils.isCompanyAdmin()) {
return AjaxResult.error("仅企业用户可以签到");
}
Company company = getCurrentCompanyOrThrow();
OutdoorFair fair = outdoorFairService.getById(fairId);
if (fair == null) {
return AjaxResult.error("户外招聘会不存在");
}
String timeError = validateCompanyCheckInTime(fair, new Date());
if (timeError != null) {
return AjaxResult.error(timeError);
}
FairCompany relation = fairCompanyMapper.selectOne(new LambdaQueryWrapper<FairCompany>()
.eq(FairCompany::getJobFairId, fairId)
.eq(FairCompany::getCompanyId, company.getCompanyId())
.eq(FairCompany::getDelFlag, "0")
.last("LIMIT 1"));
if (relation == null) {
return AjaxResult.error("该企业未报名当前招聘会");
}
if (!"1".equals(relation.getReviewStatus())) {
return AjaxResult.error("报名审核通过后才能签到");
}
OutdoorFairCompanyAttendance attendance = outdoorFairCompanyAttendanceMapper.selectOne(
new LambdaQueryWrapper<OutdoorFairCompanyAttendance>()
.eq(OutdoorFairCompanyAttendance::getFairId, fairId)
.eq(OutdoorFairCompanyAttendance::getCompanyId, company.getCompanyId())
.eq(OutdoorFairCompanyAttendance::getDelFlag, "0")
.last("LIMIT 1"));
if (attendance != null && "1".equals(attendance.getAttendanceStatus())) {
return AjaxResult.error("该企业已签到,无需重复签到");
}
Date checkInTime = new Date();
if (attendance == null) {
attendance = new OutdoorFairCompanyAttendance();
attendance.setFairId(fairId);
attendance.setCompanyId(company.getCompanyId());
attendance.setCreateBy(getUsername());
attendance.setCreateTime(checkInTime);
}
attendance.setCompanyName(company.getName());
attendance.setContactPerson(company.getContactPerson());
attendance.setContactPhone(company.getContactPersonPhone());
attendance.setAttendanceStatus("1");
attendance.setCheckInTime(checkInTime);
attendance.setUpdateBy(getUsername());
attendance.setUpdateTime(checkInTime);
boolean saved = attendance.getId() == null
? outdoorFairCompanyAttendanceMapper.insert(attendance) > 0
: outdoorFairCompanyAttendanceMapper.updateById(attendance) > 0;
if (!saved) {
return AjaxResult.error("签到失败,请稍后重试");
}
Map<String, Object> data = new HashMap<>();
data.put("fairId", fairId);
data.put("companyId", company.getCompanyId());
data.put("checkInTime", checkInTime);
data.put("attendanceStatus", "checked_in");
return success(data);
}
/**
* 查询单场招聘会已签到/未签到企业。checkedIn=true 为已签到false 为未签到。
*/
@ApiOperation("查询招聘会企业签到情况")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:query')")
@GetMapping("/{fairId}/company-attendance")
public AjaxResult listCompanyAttendance(@PathVariable Long fairId,
String companyName,
Boolean checkedIn,
Integer current,
Integer pageSize)
{
ensureFairExists(fairId);
Long companyId = null;
if (RoleUtils.isCompanyAdmin()) {
Company company = getCurrentCompanyOrNull();
if (company == null) {
return emptyAjaxPage();
}
companyId = company.getCompanyId();
}
List<OutdoorFairCompanyAttendanceVO> rows = outdoorFairCompanyAttendanceMapper
.selectCompanyAttendanceList(fairId, companyId, companyName, checkedIn);
int total = rows.size();
int page = current == null || current < 1 ? 1 : current;
int size = pageSize == null || pageSize < 1 ? 10 : Math.min(pageSize, 100);
int fromIndex = Math.min((page - 1) * size, total);
int toIndex = Math.min(fromIndex + size, total);
AjaxResult result = AjaxResult.success();
result.put("rows", rows.subList(fromIndex, toIndex));
result.put("total", total);
return result;
}
@ApiOperation("查询户外招聘会参会企业")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:query')")
@GetMapping("/{fairId}/companies")
@@ -665,6 +782,13 @@ public class OutdoorFairController extends BaseController
.isNotNull(OutdoorFairDevice::getCompanyId))
.stream()
.collect(Collectors.toMap(OutdoorFairDevice::getCompanyId, item -> item, (a, b) -> a));
Map<Long, OutdoorFairCompanyAttendance> attendanceMap = outdoorFairCompanyAttendanceMapper.selectList(
new LambdaQueryWrapper<OutdoorFairCompanyAttendance>()
.eq(OutdoorFairCompanyAttendance::getFairId, fairId)
.eq(OutdoorFairCompanyAttendance::getDelFlag, "0"))
.stream()
.filter(item -> item.getCompanyId() != null)
.collect(Collectors.toMap(OutdoorFairCompanyAttendance::getCompanyId, item -> item, (a, b) -> a));
List<Map<String, Object>> rows = new ArrayList<>();
for (FairCompany relation : relations) {
@@ -693,6 +817,10 @@ public class OutdoorFairController extends BaseController
row.put("reviewTime", relation.getReviewTime());
row.put("reviewRemark", relation.getReviewRemark());
row.put("registrationTime", relation.getCreateTime());
OutdoorFairCompanyAttendance attendance = attendanceMap.get(company.getCompanyId());
row.put("companyCheckInStatus", attendance != null && "1".equals(attendance.getAttendanceStatus())
? "checked_in" : "not_checked_in");
row.put("companyCheckInTime", attendance == null ? null : attendance.getCheckInTime());
OutdoorFairDevice device = deviceMap.get(company.getCompanyId());
String h5Url = buildOutdoorFairCompanyH5Url(request, fairId, company.getCompanyId());
row.put("deviceId", device == null ? null : device.getId());
@@ -810,12 +938,16 @@ public class OutdoorFairController extends BaseController
if (company == null || company.getStatus() == null || company.getStatus() != 1) {
return AjaxResult.error("只能添加审核通过的企业");
}
ensureFairCompany(fairId, companyId);
FairCompany relation = ensureFairCompany(fairId, companyId);
if ("1".equals(relation.getReviewStatus())) {
ensureCompanyAttendanceRecord(fairId, company);
}
return success();
}
@ApiOperation("审核户外招聘会参会企业")
@PreAuthorize("@ss.hasPermi('cms:outdoorFair:edit')")
@Transactional(rollbackFor = Exception.class)
@PutMapping("/{fairId}/companies/{id}/review")
public AjaxResult reviewCompany(@PathVariable Long fairId, @PathVariable Long id,
@RequestBody Map<String, Object> request)
@@ -828,11 +960,19 @@ public class OutdoorFairController extends BaseController
}
String reviewRemark = request.get("reviewRemark") == null ? null : String.valueOf(request.get("reviewRemark"));
validateRejectRemark(reviewStatus, reviewRemark);
String previousReviewStatus = relation.getReviewStatus();
relation.setReviewStatus(reviewStatus);
relation.setReviewBy(getUsername());
relation.setReviewTime(new Date());
relation.setReviewRemark(reviewRemark);
return toAjax(fairCompanyMapper.updateById(relation));
int updated = fairCompanyMapper.updateById(relation);
if (updated > 0 && "1".equals(reviewStatus) && !"1".equals(previousReviewStatus)) {
Company company = companyMapper.selectById(relation.getCompanyId());
if (company != null) {
ensureCompanyAttendanceRecord(fairId, company);
}
}
return toAjax(updated);
}
@ApiOperation("移除户外招聘会参会企业")
@@ -851,7 +991,11 @@ public class OutdoorFairController extends BaseController
outdoorFairJobMapper.delete(new LambdaQueryWrapper<OutdoorFairJob>()
.eq(OutdoorFairJob::getFairId, fairId)
.eq(OutdoorFairJob::getCompanyId, relation.getCompanyId()));
return toAjax(fairCompanyMapper.deleteById(id));
int deleted = fairCompanyMapper.deleteById(id);
if (deleted > 0) {
invalidateCompanyAttendanceRecord(fairId, relation.getCompanyId());
}
return toAjax(deleted);
}
@ApiOperation("新增户外招聘会岗位")
@@ -1088,9 +1232,17 @@ public class OutdoorFairController extends BaseController
List<Long> fairIds = list.stream().map(OutdoorFair::getId).collect(Collectors.toList());
Map<Long, FairCompany> relationMap = fairCompanyMapper.selectList(new LambdaQueryWrapper<FairCompany>()
.in(FairCompany::getJobFairId, fairIds)
.eq(FairCompany::getCompanyId, company.getCompanyId()))
.eq(FairCompany::getCompanyId, company.getCompanyId())
.eq(FairCompany::getDelFlag, "0"))
.stream()
.collect(Collectors.toMap(FairCompany::getJobFairId, item -> item, (a, b) -> a));
Map<Long, OutdoorFairCompanyAttendance> attendanceMap = outdoorFairCompanyAttendanceMapper.selectList(
new LambdaQueryWrapper<OutdoorFairCompanyAttendance>()
.in(OutdoorFairCompanyAttendance::getFairId, fairIds)
.eq(OutdoorFairCompanyAttendance::getCompanyId, company.getCompanyId())
.eq(OutdoorFairCompanyAttendance::getDelFlag, "0"))
.stream()
.collect(Collectors.toMap(OutdoorFairCompanyAttendance::getFairId, item -> item, (a, b) -> a));
for (OutdoorFair fair : list) {
FairCompany relation = relationMap.get(fair.getId());
boolean signedUp = relation != null;
@@ -1099,6 +1251,12 @@ public class OutdoorFairController extends BaseController
fair.setSignedUp(signedUp);
fair.setReviewStatus(relation == null ? null : relation.getReviewStatus());
fair.setReviewRemark(relation == null ? null : relation.getReviewRemark());
OutdoorFairCompanyAttendance attendance = attendanceMap.get(fair.getId());
fair.setCompanyCheckInStatus(attendance != null && "1".equals(attendance.getAttendanceStatus())
? "checked_in" : "not_checked_in");
fair.setCompanyCheckInTime(attendance == null ? null : attendance.getCheckInTime());
fair.setCompanyCheckInAllowed(signedUp && "1".equals(fair.getReviewStatus())
&& isCompanyCheckInAllowed(fair, new Date()));
if (signedUp) {
String h5Url = buildOutdoorFairCompanyH5Url(request, fair.getId(), company.getCompanyId());
fair.setH5Url(h5Url);
@@ -1141,6 +1299,48 @@ public class OutdoorFairController extends BaseController
return fairCompany;
}
/** 为审核通过的报名关系建立企业签到状态记录。 */
private void ensureCompanyAttendanceRecord(Long fairId, Company company)
{
if (fairId == null || company == null || company.getCompanyId() == null) {
return;
}
OutdoorFairCompanyAttendance existing = outdoorFairCompanyAttendanceMapper.selectOne(
new LambdaQueryWrapper<OutdoorFairCompanyAttendance>()
.eq(OutdoorFairCompanyAttendance::getFairId, fairId)
.eq(OutdoorFairCompanyAttendance::getCompanyId, company.getCompanyId())
.eq(OutdoorFairCompanyAttendance::getDelFlag, "0")
.last("LIMIT 1"));
if (existing != null) {
return;
}
OutdoorFairCompanyAttendance attendance = new OutdoorFairCompanyAttendance();
attendance.setFairId(fairId);
attendance.setCompanyId(company.getCompanyId());
attendance.setCompanyName(company.getName());
attendance.setContactPerson(company.getContactPerson());
attendance.setContactPhone(company.getContactPersonPhone());
attendance.setAttendanceStatus("0");
attendance.setCreateBy(getUsername());
attendance.setCreateTime(new Date());
outdoorFairCompanyAttendanceMapper.insert(attendance);
}
/** 报名被移除或退订后作废旧签到记录,避免重新报名沿用上一次签到状态。 */
private void invalidateCompanyAttendanceRecord(Long fairId, Long companyId)
{
if (fairId == null || companyId == null) {
return;
}
outdoorFairCompanyAttendanceMapper.update(null, new UpdateWrapper<OutdoorFairCompanyAttendance>()
.eq("fair_id", fairId)
.eq("company_id", companyId)
.eq("del_flag", "0")
.set("del_flag", "2")
.set("update_by", getUsername())
.set("update_time", new Date()));
}
private void validateReviewStatus(String reviewStatus)
{
if (!"0".equals(reviewStatus) && !"1".equals(reviewStatus) && !"2".equals(reviewStatus)) {
@@ -1203,6 +1403,25 @@ public class OutdoorFairController extends BaseController
return null;
}
static String validateCompanyCheckInTime(OutdoorFair fair, Date currentTime)
{
if (fair == null || fair.getHoldTime() == null || fair.getEndTime() == null || currentTime == null) {
return "招聘会举办时间不完整,暂不能签到";
}
if (currentTime.before(fair.getHoldTime())) {
return "招聘会还没有开始,暂不能签到";
}
if (currentTime.after(fair.getEndTime())) {
return "招聘会已结束,不能签到";
}
return null;
}
private boolean isCompanyCheckInAllowed(OutdoorFair fair, Date currentTime)
{
return validateCompanyCheckInTime(fair, currentTime) == null;
}
/**
* 将当前场地模板克隆为招聘会自己的展位图。模板与副本只在这里发生单向复制,之后互不影响。
*/

View File

@@ -148,4 +148,17 @@ public class OutdoorFair extends BaseEntity
@TableField(exist = false)
@ApiModelProperty("当前企业报名审核备注")
private String reviewRemark;
@TableField(exist = false)
@ApiModelProperty("当前企业签到状态not_checked_in/checked_in")
private String companyCheckInStatus;
@TableField(exist = false)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty("当前企业签到时间")
private Date companyCheckInTime;
@TableField(exist = false)
@ApiModelProperty("当前企业当前是否允许签到")
private Boolean companyCheckInAllowed;
}

View File

@@ -0,0 +1,52 @@
package com.ruoyi.cms.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.domain.BaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
/**
* 户外招聘会企业签到记录。
*
* <p>每个审核通过的企业报名对应一条记录0 表示尚未签到1 表示已签到。
* 招聘会结束后仍为 0 的记录即为企业未参会记录。</p>
*/
@Data
@ApiModel("户外招聘会企业签到记录")
@TableName("shz.cms_outdoor_fair_company_attendance")
public class OutdoorFairCompanyAttendance extends BaseEntity
{
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
@ApiModelProperty("主键ID")
private Long id;
@ApiModelProperty("招聘会ID")
private Long fairId;
@ApiModelProperty("企业ID")
private Long companyId;
@ApiModelProperty("企业名称快照")
private String companyName;
@ApiModelProperty("联系人快照")
private String contactPerson;
@ApiModelProperty("联系电话快照")
private String contactPhone;
@ApiModelProperty("签到状态0未签到1已签到")
private String attendanceStatus;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty("签到时间")
private Date checkInTime;
}

View File

@@ -15,4 +15,7 @@ public class CompanySearch extends Company {
private String startDate;
//查询结束时间
private String endDate;
/** 只查询存在未参加已结束户外招聘会记录的企业。 */
private Boolean missedOutdoorFairOnly;
}

View File

@@ -0,0 +1,38 @@
package com.ruoyi.cms.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
/** 企业未参加户外招聘会明细。 */
@Data
@ApiModel("企业未参加户外招聘会明细")
public class CompanyMissedOutdoorFairVO
{
@ApiModelProperty("招聘会ID")
private Long fairId;
@ApiModelProperty("招聘会标题")
private String title;
@ApiModelProperty("举办单位")
private String hostUnit;
@ApiModelProperty("举办地址")
private String address;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty("举办开始时间")
private Date holdTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty("举办结束时间")
private Date endTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty("企业报名时间")
private Date registrationTime;
}

View File

@@ -0,0 +1,53 @@
package com.ruoyi.cms.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
/** 招聘会企业签到列表项。 */
@Data
@ApiModel("招聘会企业签到列表项")
public class OutdoorFairCompanyAttendanceVO
{
@ApiModelProperty("报名关系ID")
private Long id;
@ApiModelProperty("招聘会ID")
private Long fairId;
@ApiModelProperty("企业ID")
private Long companyId;
@ApiModelProperty("企业名称")
private String companyName;
@ApiModelProperty("企业行业")
private String industry;
@ApiModelProperty("企业规模")
private String scale;
@ApiModelProperty("联系人")
private String contactPerson;
@ApiModelProperty("联系电话")
private String contactPhone;
@ApiModelProperty("报名审核时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date reviewTime;
@ApiModelProperty("报名时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date registrationTime;
@ApiModelProperty("签到状态0未签到1已签到")
private String attendanceStatus;
@ApiModelProperty("签到时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date checkInTime;
}

View File

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import java.util.List;
import com.ruoyi.cms.domain.query.CompanySearch;
import com.ruoyi.cms.domain.vo.CompanyMissedOutdoorFairVO;
import com.ruoyi.common.core.domain.entity.Company;
import org.apache.ibatis.annotations.Param;
@@ -36,5 +37,8 @@ public interface CompanyMapper extends BaseMapper<Company>
int updateLcToLocalStatus(Company company);
List<Company> selectAdminCompanyList(Company company);
List<Company> selectAdminCompanyList(CompanySearch company);
/** 查询企业未参加的已结束户外招聘会。 */
List<CompanyMissedOutdoorFairVO> selectMissedOutdoorFairList(@Param("companyId") Long companyId);
}

View File

@@ -0,0 +1,19 @@
package com.ruoyi.cms.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.cms.domain.OutdoorFairCompanyAttendance;
import com.ruoyi.cms.domain.vo.OutdoorFairCompanyAttendanceVO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/** 户外招聘会企业签到记录 Mapper。 */
public interface OutdoorFairCompanyAttendanceMapper extends BaseMapper<OutdoorFairCompanyAttendance>
{
/** 查询一场招聘会的企业签到列表。 */
List<OutdoorFairCompanyAttendanceVO> selectCompanyAttendanceList(
@Param("fairId") Long fairId,
@Param("companyId") Long companyId,
@Param("companyName") String companyName,
@Param("checkedIn") Boolean checkedIn);
}

View File

@@ -86,5 +86,5 @@ public interface ICompanyService
int companyUp(Long companyId);
List<Company> selectAdminCompanyList(Company company);
List<Company> selectAdminCompanyList(CompanySearch company);
}

View File

@@ -408,7 +408,7 @@ public class CompanyServiceImpl extends ServiceImpl<CompanyMapper, Company> impl
}
@Override
public List<Company> selectAdminCompanyList(Company company) {
public List<Company> selectAdminCompanyList(CompanySearch company) {
return companyMapper.selectAdminCompanyList(company);
}
}

View File

@@ -41,6 +41,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="companyStatus" column="company_status" />
<result property="isHrs" column="is_hrs" />
<result property="lcUserid" column="lc_userid" />
<result property="missedOutdoorFairCount" column="missed_outdoor_fair_count" />
<collection property="companyContactList" ofType="com.ruoyi.common.core.domain.entity.CompanyContact"
resultMap="CompanyContactResult"/>
@@ -53,7 +54,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap>
<sql id="selectCompanyVo">
select company_id, name, location, industry, scale, del_flag, create_by, create_time, update_by, update_time, remark,code,description,nature,company_nature,total_recruitment,registered_address,contact_person,contact_person_phone,is_abnormal,not_pass_reason,status,case when status='2' then update_time else null end reject_time,is_imp_company,imp_company_type,enterprise_type,legal_person,legal_id_card,legal_phone,is_hrs from company
select company_id, name, location, industry, scale, del_flag, create_by, create_time, update_by, update_time, remark,code,description,nature,company_nature,total_recruitment,registered_address,contact_person,contact_person_phone,is_abnormal,not_pass_reason,status,case when status='2' then update_time else null end reject_time,is_imp_company,imp_company_type,enterprise_type,legal_person,legal_id_card,legal_phone,is_hrs,
(select count(1)
from shz.fair_company fc
join shz.cms_outdoor_fair f on f.id = fc.job_fair_id and f.del_flag = '0'
left join shz.cms_outdoor_fair_company_attendance a
on a.fair_id = fc.job_fair_id and a.company_id = fc.company_id and a.del_flag = '0'
where fc.company_id = c.company_id
and fc.del_flag = '0'
and fc.review_status = '1'
and f.end_time &lt; CURRENT_TIMESTAMP
and coalesce(a.attendance_status, '0') &lt;&gt; '1') as missed_outdoor_fair_count
from company c
</sql>
<sql id="companyIndustryFilter">
@@ -148,6 +160,20 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="endDate != null and endDate != ''">
and create_time &lt;= #{endDate}
</if>
<if test="missedOutdoorFairOnly == true">
and exists (
select 1
from shz.fair_company fc
join shz.cms_outdoor_fair f on f.id = fc.job_fair_id and f.del_flag = '0'
left join shz.cms_outdoor_fair_company_attendance a
on a.fair_id = fc.job_fair_id and a.company_id = fc.company_id and a.del_flag = '0'
where fc.company_id = c.company_id
and fc.del_flag = '0'
and fc.review_status = '1'
and f.end_time &lt; CURRENT_TIMESTAMP
and coalesce(a.attendance_status, '0') &lt;&gt; '1'
)
</if>
</where>
</select>
<select id="label" resultMap="CompanyResult">
@@ -197,6 +223,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
select t.company_id, t.name, t.location, t.industry, t.scale, t.del_flag, t.create_by, t.create_time, t.update_by, t.update_time, t.remark,t.code,t.description,
t.nature,t.company_nature,t.total_recruitment,t.registered_address,t.contact_person,t.contact_person_phone,t.is_abnormal,t.not_pass_reason,t.status,case when t.status='2'
then t.update_time else null end reject_time,t.is_imp_company,t.imp_company_type,t.enterprise_type,t.legal_person,t.legal_id_card,t.legal_phone,t.is_hrs,
(select count(1)
from shz.fair_company fc
join shz.cms_outdoor_fair f on f.id = fc.job_fair_id and f.del_flag = '0'
left join shz.cms_outdoor_fair_company_attendance a
on a.fair_id = fc.job_fair_id and a.company_id = fc.company_id and a.del_flag = '0'
where fc.company_id = t.company_id
and fc.del_flag = '0'
and fc.review_status = '1'
and f.end_time &lt; CURRENT_TIMESTAMP
and coalesce(a.attendance_status, '0') &lt;&gt; '1') as missed_outdoor_fair_count,
su.lc_userid,su.user_id,cct.contact_person as con_contact_person,cct.contact_person_phone as con_contact_person_phone,cct.company_id as con_company_id from company t
left join sys_user su on t.code=su.id_card and su.del_flag='0'
left join company_contact cct on t.company_id=cct.company_id and cct.del_flag='0'
@@ -232,7 +268,47 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="endDate != null and endDate != ''">
and t.create_time &lt;= #{endDate}
</if>
<if test="missedOutdoorFairOnly == true">
and exists (
select 1
from shz.fair_company fc
join shz.cms_outdoor_fair f on f.id = fc.job_fair_id and f.del_flag = '0'
left join shz.cms_outdoor_fair_company_attendance a
on a.fair_id = fc.job_fair_id and a.company_id = fc.company_id and a.del_flag = '0'
where fc.company_id = t.company_id
and fc.del_flag = '0'
and fc.review_status = '1'
and f.end_time &lt; CURRENT_TIMESTAMP
and coalesce(a.attendance_status, '0') &lt;&gt; '1'
)
</if>
</where>
</select>
<select id="selectMissedOutdoorFairList"
parameterType="long"
resultType="com.ruoyi.cms.domain.vo.CompanyMissedOutdoorFairVO">
SELECT f.id AS fair_id,
f.title,
f.host_unit,
f.address,
f.hold_time,
f.end_time,
fc.create_time AS registration_time
FROM shz.fair_company fc
JOIN shz.cms_outdoor_fair f
ON f.id = fc.job_fair_id
AND f.del_flag = '0'
LEFT JOIN shz.cms_outdoor_fair_company_attendance a
ON a.fair_id = fc.job_fair_id
AND a.company_id = fc.company_id
AND a.del_flag = '0'
WHERE fc.company_id = #{companyId}
AND fc.del_flag = '0'
AND fc.review_status = '1'
AND f.end_time &lt; CURRENT_TIMESTAMP
AND COALESCE(a.attendance_status, '0') &lt;&gt; '1'
ORDER BY f.end_time DESC, f.id DESC
</select>
</mapper>

View File

@@ -0,0 +1,64 @@
<?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.OutdoorFairCompanyAttendanceMapper">
<resultMap id="OutdoorFairCompanyAttendanceResult"
type="com.ruoyi.cms.domain.vo.OutdoorFairCompanyAttendanceVO">
<id property="id" column="registration_id"/>
<result property="fairId" column="fair_id"/>
<result property="companyId" column="company_id"/>
<result property="companyName" column="company_name"/>
<result property="industry" column="industry"/>
<result property="scale" column="scale"/>
<result property="contactPerson" column="contact_person"/>
<result property="contactPhone" column="contact_phone"/>
<result property="reviewTime" column="review_time"/>
<result property="registrationTime" column="registration_time"/>
<result property="attendanceStatus" column="attendance_status"/>
<result property="checkInTime" column="check_in_time"/>
</resultMap>
<select id="selectCompanyAttendanceList"
resultMap="OutdoorFairCompanyAttendanceResult">
SELECT fc.id AS registration_id,
fc.job_fair_id AS fair_id,
fc.company_id,
c.name AS company_name,
c.industry,
c.scale,
c.contact_person,
c.contact_person_phone AS contact_phone,
fc.review_time,
fc.create_time AS registration_time,
COALESCE(a.attendance_status, '0') AS attendance_status,
a.check_in_time
FROM shz.fair_company fc
JOIN shz.company c ON c.company_id = fc.company_id AND c.del_flag = '0'
LEFT JOIN shz.cms_outdoor_fair_company_attendance a
ON a.fair_id = fc.job_fair_id
AND a.company_id = fc.company_id
AND a.del_flag = '0'
WHERE fc.del_flag = '0'
AND fc.job_fair_id = #{fairId}
AND fc.review_status = '1'
<if test="companyId != null">
AND fc.company_id = #{companyId}
</if>
<if test="companyName != null and companyName != ''">
AND c.name LIKE CONCAT('%', CAST(#{companyName} AS VARCHAR), '%')
</if>
<if test="checkedIn != null">
<choose>
<when test="checkedIn">
AND COALESCE(a.attendance_status, '0') = '1'
</when>
<otherwise>
AND COALESCE(a.attendance_status, '0') &lt;&gt; '1'
</otherwise>
</choose>
</if>
ORDER BY a.check_in_time DESC NULLS LAST, fc.id DESC
</select>
</mapper>

View File

@@ -3,10 +3,12 @@ package com.ruoyi.cms.controller.app;
import com.ruoyi.cms.domain.FairCompany;
import com.ruoyi.cms.domain.OutdoorFair;
import com.ruoyi.cms.domain.OutdoorFairAttendee;
import com.ruoyi.cms.domain.OutdoorFairCompanyAttendance;
import com.ruoyi.cms.mapper.CompanyMapper;
import com.ruoyi.cms.mapper.FairCompanyMapper;
import com.ruoyi.cms.mapper.OutdoorFairBoothBookingMapper;
import com.ruoyi.cms.mapper.OutdoorFairBoothMapper;
import com.ruoyi.cms.mapper.OutdoorFairCompanyAttendanceMapper;
import com.ruoyi.cms.mapper.OutdoorFairJobMapper;
import com.ruoyi.cms.service.IOutdoorFairAttendeeService;
import com.ruoyi.cms.service.IAppUserService;
@@ -158,6 +160,70 @@ class AppOutdoorFairControllerTest
verify(attendeeService, never()).save(any(OutdoorFairAttendee.class));
}
@Test
void appCompanyCheckInRequiresCompanyUser() throws Exception
{
IOutdoorFairService fairService = mock(IOutdoorFairService.class);
IOutdoorFairAttendeeService attendeeService = mock(IOutdoorFairAttendeeService.class);
AppOutdoorFairController controller = controllerWith(fairService, attendeeService);
AppUser appUser = appUser();
AjaxResult result = controller.companyCheckIn(12L, appUser);
assertEquals(500, result.get(AjaxResult.CODE_TAG));
assertEquals("仅移动端企业可以签到", result.get(AjaxResult.MSG_TAG));
verify(fairService, never()).getById(12L);
}
@Test
void appCompanyCheckInCreatesAttendanceForApprovedCompany() throws Exception
{
IOutdoorFairService fairService = mock(IOutdoorFairService.class);
IOutdoorFairAttendeeService attendeeService = mock(IOutdoorFairAttendeeService.class);
CompanyMapper companyMapper = mock(CompanyMapper.class);
FairCompanyMapper fairCompanyMapper = mock(FairCompanyMapper.class);
OutdoorFairCompanyAttendanceMapper attendanceMapper = mock(OutdoorFairCompanyAttendanceMapper.class);
OutdoorFair fair = fairWithTimes(
new Date(System.currentTimeMillis() - 1_000L),
new Date(System.currentTimeMillis() + 60_000L));
Company company = new Company();
company.setCompanyId(31L);
company.setCode("CODE-31");
company.setName("测试企业");
company.setContactPerson("联系人");
company.setContactPersonPhone("13800000000");
FairCompany relation = new FairCompany();
relation.setJobFairId(12L);
relation.setCompanyId(31L);
relation.setReviewStatus("1");
AppUser appUser = appUser();
appUser.setIsCompanyUser("0");
appUser.setIdCard("CODE-31");
when(companyMapper.selectOne(any())).thenReturn(company);
when(fairService.getById(12L)).thenReturn(fair);
when(fairCompanyMapper.selectOne(any())).thenReturn(relation);
when(attendanceMapper.selectOne(any())).thenReturn(null);
when(attendanceMapper.insert(any(OutdoorFairCompanyAttendance.class))).thenReturn(1);
AppOutdoorFairController controller = controllerWith(fairService, attendeeService);
setField(controller, "companyMapper", companyMapper);
setField(controller, "fairCompanyMapper", fairCompanyMapper);
setField(controller, "outdoorFairCompanyAttendanceMapper", attendanceMapper);
AjaxResult result = controller.companyCheckIn(12L, appUser);
assertEquals(200, result.get(AjaxResult.CODE_TAG));
ArgumentCaptor<OutdoorFairCompanyAttendance> captor =
ArgumentCaptor.forClass(OutdoorFairCompanyAttendance.class);
verify(attendanceMapper).insert(captor.capture());
OutdoorFairCompanyAttendance attendance = captor.getValue();
assertEquals(12L, attendance.getFairId());
assertEquals(31L, attendance.getCompanyId());
assertEquals("1", attendance.getAttendanceStatus());
assertTrue(attendance.getCheckInTime() != null);
}
@SuppressWarnings("unchecked")
@Test
void publicDetailIncludesOnlySafeCompanyCardFields() throws Exception
@@ -257,4 +323,11 @@ class AppOutdoorFairControllerTest
fair.setHoldTime(holdTime);
return fair;
}
private OutdoorFair fairWithTimes(Date holdTime, Date endTime)
{
OutdoorFair fair = fairAt(holdTime);
fair.setEndTime(endTime);
return fair;
}
}

View File

@@ -36,6 +36,31 @@ class OutdoorFairControllerTest
assertEquals("@ss.hasPermi('cms:outdoorFair:unsubscribe')", annotation.value());
}
@Test
void companyCheckInEndpointRequiresDedicatedPermission() throws Exception
{
Method method = OutdoorFairController.class.getDeclaredMethod("checkInCompany", Long.class);
org.springframework.security.access.prepost.PreAuthorize annotation =
method.getAnnotation(org.springframework.security.access.prepost.PreAuthorize.class);
assertNotNull(annotation);
assertEquals("@ss.hasPermi('cms:outdoorFair:companyCheckIn')", annotation.value());
}
@Test
void companyCheckInIsAllowedOnlyWithinFairTime()
{
OutdoorFair fair = fairWithTimes(2_000L, 3_000L);
fair.setEndTime(new Date(3_000L));
assertEquals("招聘会还没有开始,暂不能签到",
OutdoorFairController.validateCompanyCheckInTime(fair, new Date(1_999L)));
assertNull(OutdoorFairController.validateCompanyCheckInTime(fair, new Date(2_000L)));
assertNull(OutdoorFairController.validateCompanyCheckInTime(fair, new Date(3_000L)));
assertEquals("招聘会已结束,不能签到",
OutdoorFairController.validateCompanyCheckInTime(fair, new Date(3_001L)));
}
@Test
void companyQrCodeUrlUsesPublicForwardedHost() throws Exception
{
@@ -115,6 +140,7 @@ class OutdoorFairControllerTest
OutdoorFair fair = new OutdoorFair();
fair.setHoldTime(new Date(holdTime));
fair.setApplyEndTime(new Date(applyEndTime));
fair.setEndTime(new Date(applyEndTime + 1_000L));
return fair;
}