From 0dc26a9e2e9acab9c2020a3af7ae54679c929d68 Mon Sep 17 00:00:00 2001 From: lapuda <577732344@qq.com> Date: Fri, 24 Jul 2026 12:35:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=8B=9B=E8=81=98?= =?UTF-8?q?=E4=BC=9A=E4=BC=81=E4=B8=9A=E7=AD=BE=E5=88=B0=E4=B8=8E=E6=9C=AA?= =?UTF-8?q?=E5=8F=82=E4=BC=9A=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/AppOutdoorFairController.java | 128 ++++++++++ .../cms/controller/cms/CompanyController.java | 25 ++ .../controller/cms/OutdoorFairController.java | 227 +++++++++++++++++- .../com/ruoyi/cms/domain/OutdoorFair.java | 13 + .../domain/OutdoorFairCompanyAttendance.java | 52 ++++ .../ruoyi/cms/domain/query/CompanySearch.java | 3 + .../domain/vo/CompanyMissedOutdoorFairVO.java | 38 +++ .../vo/OutdoorFairCompanyAttendanceVO.java | 53 ++++ .../com/ruoyi/cms/mapper/CompanyMapper.java | 6 +- .../OutdoorFairCompanyAttendanceMapper.java | 19 ++ .../ruoyi/cms/service/ICompanyService.java | 2 +- .../cms/service/impl/CompanyServiceImpl.java | 2 +- .../resources/mapper/app/CompanyMapper.xml | 78 +++++- .../OutdoorFairCompanyAttendanceMapper.xml | 64 +++++ .../app/AppOutdoorFairControllerTest.java | 73 ++++++ .../cms/OutdoorFairControllerTest.java | 26 ++ .../common/core/domain/entity/Company.java | 9 + ..._fair_company_attendance.test-executed.sql | 125 ++++++++++ 18 files changed, 935 insertions(+), 8 deletions(-) create mode 100644 ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/OutdoorFairCompanyAttendance.java create mode 100644 ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/CompanyMissedOutdoorFairVO.java create mode 100644 ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/OutdoorFairCompanyAttendanceVO.java create mode 100644 ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairCompanyAttendanceMapper.java create mode 100644 ruoyi-bussiness/src/main/resources/mapper/cms/OutdoorFairCompanyAttendanceMapper.xml create mode 100644 sql/migration_outdoor_fair_company_attendance.test-executed.sql 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 a8b1ffe..cad40ec 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 @@ -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() + .eq(Company::getCode, appUser.getIdCard()) + .eq(Company::getDelFlag, "0") + .last("LIMIT 1")); + } + if (company == null && appUser.getUserId() != null) { + company = companyMapper.selectOne(new LambdaQueryWrapper() + .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() + .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() + .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 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()) { diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/CompanyController.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/CompanyController.java index c8f2b7c..50a7963 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/CompanyController.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/CompanyController.java @@ -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 list = companyMapper.selectMissedOutdoorFairList(companyId); + return getDataTable(list); + } + /** * 导出公司列表 */ diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairController.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairController.java index 04a12ec..abc9b9d 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairController.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/controller/cms/OutdoorFairController.java @@ -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() + .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() + .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 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 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 attendanceMap = outdoorFairCompanyAttendanceMapper.selectList( + new LambdaQueryWrapper() + .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> 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 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() .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 fairIds = list.stream().map(OutdoorFair::getId).collect(Collectors.toList()); Map relationMap = fairCompanyMapper.selectList(new LambdaQueryWrapper() .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 attendanceMap = outdoorFairCompanyAttendanceMapper.selectList( + new LambdaQueryWrapper() + .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() + .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() + .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; + } + /** * 将当前场地模板克隆为招聘会自己的展位图。模板与副本只在这里发生单向复制,之后互不影响。 */ diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/OutdoorFair.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/OutdoorFair.java index 807210f..8585209 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/OutdoorFair.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/OutdoorFair.java @@ -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; } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/OutdoorFairCompanyAttendance.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/OutdoorFairCompanyAttendance.java new file mode 100644 index 0000000..298f5d7 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/OutdoorFairCompanyAttendance.java @@ -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; + +/** + * 户外招聘会企业签到记录。 + * + *

每个审核通过的企业报名对应一条记录:0 表示尚未签到,1 表示已签到。 + * 招聘会结束后仍为 0 的记录即为企业未参会记录。

+ */ +@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; +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/query/CompanySearch.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/query/CompanySearch.java index 9702ab2..51a34d7 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/query/CompanySearch.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/query/CompanySearch.java @@ -15,4 +15,7 @@ public class CompanySearch extends Company { private String startDate; //查询结束时间 private String endDate; + + /** 只查询存在未参加已结束户外招聘会记录的企业。 */ + private Boolean missedOutdoorFairOnly; } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/CompanyMissedOutdoorFairVO.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/CompanyMissedOutdoorFairVO.java new file mode 100644 index 0000000..fa3c578 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/CompanyMissedOutdoorFairVO.java @@ -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; +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/OutdoorFairCompanyAttendanceVO.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/OutdoorFairCompanyAttendanceVO.java new file mode 100644 index 0000000..5869cdc --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/vo/OutdoorFairCompanyAttendanceVO.java @@ -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; +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/CompanyMapper.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/CompanyMapper.java index ff8e2a4..76f2eb2 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/CompanyMapper.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/CompanyMapper.java @@ -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 int updateLcToLocalStatus(Company company); - List selectAdminCompanyList(Company company); + List selectAdminCompanyList(CompanySearch company); + + /** 查询企业未参加的已结束户外招聘会。 */ + List selectMissedOutdoorFairList(@Param("companyId") Long companyId); } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairCompanyAttendanceMapper.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairCompanyAttendanceMapper.java new file mode 100644 index 0000000..4de8066 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/OutdoorFairCompanyAttendanceMapper.java @@ -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 +{ + /** 查询一场招聘会的企业签到列表。 */ + List selectCompanyAttendanceList( + @Param("fairId") Long fairId, + @Param("companyId") Long companyId, + @Param("companyName") String companyName, + @Param("checkedIn") Boolean checkedIn); +} diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/ICompanyService.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/ICompanyService.java index b32a01c..66fba37 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/ICompanyService.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/ICompanyService.java @@ -86,5 +86,5 @@ public interface ICompanyService int companyUp(Long companyId); - List selectAdminCompanyList(Company company); + List selectAdminCompanyList(CompanySearch company); } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/CompanyServiceImpl.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/CompanyServiceImpl.java index 02b0e17..9e0393a 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/CompanyServiceImpl.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/CompanyServiceImpl.java @@ -408,7 +408,7 @@ public class CompanyServiceImpl extends ServiceImpl impl } @Override - public List selectAdminCompanyList(Company company) { + public List selectAdminCompanyList(CompanySearch company) { return companyMapper.selectAdminCompanyList(company); } } diff --git a/ruoyi-bussiness/src/main/resources/mapper/app/CompanyMapper.xml b/ruoyi-bussiness/src/main/resources/mapper/app/CompanyMapper.xml index dcced9a..20f6089 100644 --- a/ruoyi-bussiness/src/main/resources/mapper/app/CompanyMapper.xml +++ b/ruoyi-bussiness/src/main/resources/mapper/app/CompanyMapper.xml @@ -41,6 +41,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -53,7 +54,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - 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 < CURRENT_TIMESTAMP + and coalesce(a.attendance_status, '0') <> '1') as missed_outdoor_fair_count + from company c @@ -148,6 +160,20 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and create_time <= #{endDate} + + 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 < CURRENT_TIMESTAMP + and coalesce(a.attendance_status, '0') <> '1' + ) + + + diff --git a/ruoyi-bussiness/src/main/resources/mapper/cms/OutdoorFairCompanyAttendanceMapper.xml b/ruoyi-bussiness/src/main/resources/mapper/cms/OutdoorFairCompanyAttendanceMapper.xml new file mode 100644 index 0000000..7f90496 --- /dev/null +++ b/ruoyi-bussiness/src/main/resources/mapper/cms/OutdoorFairCompanyAttendanceMapper.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + 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 965afe7..dec2771 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 @@ -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 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; + } } diff --git a/ruoyi-bussiness/src/test/java/com/ruoyi/cms/controller/cms/OutdoorFairControllerTest.java b/ruoyi-bussiness/src/test/java/com/ruoyi/cms/controller/cms/OutdoorFairControllerTest.java index 031700b..25494a3 100644 --- a/ruoyi-bussiness/src/test/java/com/ruoyi/cms/controller/cms/OutdoorFairControllerTest.java +++ b/ruoyi-bussiness/src/test/java/com/ruoyi/cms/controller/cms/OutdoorFairControllerTest.java @@ -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; } diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/Company.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/Company.java index 74d4d2d..192a017 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/Company.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/Company.java @@ -119,6 +119,15 @@ public class Company extends BaseEntity @ApiModelProperty("是否是就业见习基地 0是 1否") private String enterpriseType; + /** + * 已结束且审核通过报名但未签到的户外招聘会数量。 + * 仅由企业列表查询填充,不对应 company 表字段。 + */ + @TableField(exist = false) + @Excel(name = "未参加招聘会数量") + @ApiModelProperty("未参加招聘会数量") + private Integer missedOutdoorFairCount; + @ApiModelProperty("法人姓名") private String legalPerson; diff --git a/sql/migration_outdoor_fair_company_attendance.test-executed.sql b/sql/migration_outdoor_fair_company_attendance.test-executed.sql new file mode 100644 index 0000000..5bd4f8b --- /dev/null +++ b/sql/migration_outdoor_fair_company_attendance.test-executed.sql @@ -0,0 +1,125 @@ +-- 户外招聘会企业签到与未参会失信统计 +-- 瀚高数据库 / PostgreSQL 兼容;可重复执行。 +-- 执行状态:测试环境已执行 +-- 测试环境:test +-- 测试执行时间:2026-07-24 12:23:44 +0800 +-- 正式环境:未执行 + +BEGIN; + +SET LOCAL search_path = shz, public; + +-- 兼容尚未执行招聘会报名审核迁移的旧库;已存在字段不会被覆盖。 +ALTER TABLE "shz"."fair_company" + ADD COLUMN IF NOT EXISTS "review_status" CHAR(1) DEFAULT '1'; +ALTER TABLE "shz"."fair_company" + ADD COLUMN IF NOT EXISTS "review_by" VARCHAR(64); +ALTER TABLE "shz"."fair_company" + ADD COLUMN IF NOT EXISTS "review_time" TIMESTAMP; +ALTER TABLE "shz"."fair_company" + ADD COLUMN IF NOT EXISTS "review_remark" VARCHAR(500); +UPDATE "shz"."fair_company" + SET "review_status" = '1' + WHERE "review_status" IS NULL; + +CREATE SEQUENCE IF NOT EXISTS "shz"."cms_outdoor_fair_company_attendance_id_seq" + START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; + +CREATE TABLE IF NOT EXISTS "shz"."cms_outdoor_fair_company_attendance" ( + "id" BIGINT NOT NULL DEFAULT nextval('"shz"."cms_outdoor_fair_company_attendance_id_seq"'::regclass) PRIMARY KEY, + "fair_id" BIGINT NOT NULL, + "company_id" BIGINT NOT NULL, + "company_name" VARCHAR(255), + "contact_person" VARCHAR(64), + "contact_phone" VARCHAR(64), + "attendance_status" CHAR(1) NOT NULL DEFAULT '0', + "check_in_time" TIMESTAMP, + "create_by" VARCHAR(64) DEFAULT '', + "create_time" TIMESTAMP, + "update_by" VARCHAR(64) DEFAULT '', + "update_time" TIMESTAMP, + "remark" VARCHAR(500), + "del_flag" CHAR(1) DEFAULT '0' +); + +COMMENT ON TABLE "shz"."cms_outdoor_fair_company_attendance" IS '户外招聘会企业签到记录表'; +COMMENT ON COLUMN "shz"."cms_outdoor_fair_company_attendance"."fair_id" IS '招聘会ID'; +COMMENT ON COLUMN "shz"."cms_outdoor_fair_company_attendance"."company_id" IS '企业ID'; +COMMENT ON COLUMN "shz"."cms_outdoor_fair_company_attendance"."company_name" IS '企业名称快照'; +COMMENT ON COLUMN "shz"."cms_outdoor_fair_company_attendance"."contact_person" IS '联系人快照'; +COMMENT ON COLUMN "shz"."cms_outdoor_fair_company_attendance"."contact_phone" IS '联系电话快照'; +COMMENT ON COLUMN "shz"."cms_outdoor_fair_company_attendance"."attendance_status" IS '签到状态:0未签到,1已签到'; +COMMENT ON COLUMN "shz"."cms_outdoor_fair_company_attendance"."check_in_time" IS '签到时间'; +COMMENT ON COLUMN "shz"."cms_outdoor_fair_company_attendance"."del_flag" IS '删除标志(0代表存在 2代表删除)'; + +CREATE UNIQUE INDEX IF NOT EXISTS "uk_cms_outdoor_fair_company_attendance" + ON "shz"."cms_outdoor_fair_company_attendance" ("fair_id", "company_id") + WHERE "del_flag" = '0'; +CREATE INDEX IF NOT EXISTS "idx_cms_outdoor_fair_company_attendance_fair_status" + ON "shz"."cms_outdoor_fair_company_attendance" ("fair_id", "attendance_status"); +CREATE INDEX IF NOT EXISTS "idx_cms_outdoor_fair_company_attendance_company" + ON "shz"."cms_outdoor_fair_company_attendance" ("company_id"); + +-- 为历史审核通过的户外招聘会报名补齐“未签到”初始记录。 +INSERT INTO "shz"."cms_outdoor_fair_company_attendance" ( + "fair_id", "company_id", "company_name", "contact_person", "contact_phone", + "attendance_status", "create_by", "create_time", "del_flag" +) +SELECT fc."job_fair_id", + fc."company_id", + c."name", + c."contact_person", + c."contact_person_phone", + '0', + 'system', + CURRENT_TIMESTAMP, + '0' + FROM "shz"."fair_company" fc + JOIN "shz"."company" c ON c."company_id" = fc."company_id" AND c."del_flag" = '0' + WHERE fc."del_flag" = '0' + AND fc."review_status" = '1' + AND NOT EXISTS ( + SELECT 1 + FROM "shz"."cms_outdoor_fair_company_attendance" existing + WHERE existing."fair_id" = fc."job_fair_id" + AND existing."company_id" = fc."company_id" + AND existing."del_flag" = '0' + ); + +-- 企业签到接口使用独立按钮权限;把权限授给已有户外招聘会列表权限的角色。 +INSERT INTO "shz"."sys_menu" ( + "menu_id", "menu_name", "parent_id", "order_num", "path", "component", + "query", "route_name", "is_frame", "is_cache", "menu_type", "visible", + "status", "perms", "icon", "create_by", "create_time", "update_by", + "update_time", "remark" +) +SELECT + (SELECT COALESCE(MAX("menu_id"), 0) + 1 FROM "shz"."sys_menu"), + '企业签到', list_menu."menu_id", 6, '', '', '', '', + 1, 0, 'F', '0', '0', 'cms:outdoorFair:companyCheckIn', '#', 'system', CURRENT_TIMESTAMP, + '', NULL, '户外招聘会企业端签到按钮权限' +FROM "shz"."sys_menu" list_menu +WHERE list_menu."perms" = 'cms:outdoorFair:list' + AND NOT EXISTS ( + SELECT 1 FROM "shz"."sys_menu" + WHERE "perms" = 'cms:outdoorFair:companyCheckIn' + ) +ORDER BY list_menu."menu_id" +LIMIT 1; + +INSERT INTO "shz"."sys_role_menu" ("role_id", "menu_id") +SELECT DISTINCT role_menu."role_id", check_in_menu."menu_id" +FROM "shz"."sys_role_menu" role_menu +JOIN "shz"."sys_menu" list_menu + ON list_menu."menu_id" = role_menu."menu_id" +JOIN "shz"."sys_menu" check_in_menu + ON check_in_menu."perms" = 'cms:outdoorFair:companyCheckIn' +WHERE list_menu."perms" = 'cms:outdoorFair:list' + AND NOT EXISTS ( + SELECT 1 + FROM "shz"."sys_role_menu" existing + WHERE existing."role_id" = role_menu."role_id" + AND existing."menu_id" = check_in_menu."menu_id" + ); + +COMMIT;