diff --git a/src/main/java/com/budwk/app/sys/controller/SysHomeController.java b/src/main/java/com/budwk/app/sys/controller/SysHomeController.java index 91e35037..0c154eff 100644 --- a/src/main/java/com/budwk/app/sys/controller/SysHomeController.java +++ b/src/main/java/com/budwk/app/sys/controller/SysHomeController.java @@ -12,9 +12,11 @@ import com.budwk.app.sys.models.Sys_home_template; import com.budwk.app.sys.models.Sys_menu; import com.budwk.app.sys.services.SysMenuService; import com.budwk.app.sys.services.SysUserService; +import com.budwk.app.web.commons.base.Globals; import com.budwk.app.web.commons.auth.utils.AuthUtil; import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.zhgh.activity.basic.models.ActivityUserScope; +import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting; import io.swagger.annotations.ApiOperation; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; @@ -40,6 +42,7 @@ import org.nutz.mvc.annotation.Param; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; +import java.time.LocalDate; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -278,6 +281,40 @@ public class SysHomeController { return Result.success(list); } + /** + * 查询PC首页疗休养报名浮动入口。 + * 只返回本年度启用、开启首页入口且已上传图片的疗休养配置,避免首页出现空图片入口。 + * + * @return 当前年度可展示的疗休养报名入口 + */ + @At + @SaCheckLogin + @ApiOperation("PC首页疗休养报名浮动入口") + @Ok("json") + public Result listTourHomeSignupEntry() { + int currentYear = LocalDate.now().getYear(); + Cnd cnd = Cnd.where(TourSetting::getDelFlag, "=", false) + .and(TourSetting::getEnabled, "=", true) + .and(TourSetting::getYear, "=", currentYear) + .and(TourSetting::getHomeSignupEntryEnabled, "=", true); + cnd.asc(TourSetting::getSortNo); + cnd.desc(TourSetting::getUpdatedAt); + cnd.desc(TourSetting::getCreatedAt); + List settings = dao.query(TourSetting.class, cnd); + List rows = new ArrayList<>(); + for (TourSetting setting : settings) { + // 优先使用配置上传图片,未上传时使用系统默认疗休养图片,避免首页浮动入口空图。 + String imageUrl = StrUtil.blankToDefault(setting.getHomeSignupEntryImage(), Globals.AppDomain + "/assets/platform/images/tour/疗休养图片.jpeg"); + rows.add(NutMap.NEW() + .addv("id", setting.getId()) + .addv("year", setting.getYear()) + .addv("title", setting.getYear() + "暑期教职工疗休养") + .addv("imageUrl", imageUrl) + .addv("href", "/platform/tour/signup")); + } + return Result.success(rows); + } + @At @SaCheckLogin diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourBranchUserAssignmentController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourBranchUserAssignmentController.java index 2d1cdbf1..6944e60c 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourBranchUserAssignmentController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourBranchUserAssignmentController.java @@ -48,12 +48,12 @@ public class TourBranchUserAssignmentController { } /** - * 分页查询候选人员,候选范围由疗休养配置可参加人员范围和当前登录人所在分工会共同决定。 + * 分页查询候选人员,候选范围由疗休养配置可参加人员范围和当前登录人所在分工会共同决定;userIds 仅作为人员选择器筛选条件。 */ @At @SaCheckPermission("tour.branchUserAssignment") - public Result candidatePageData(PageForm pageForm, String settingId, String keyword) { - return Result.success(tourUserAssignmentService.branchCandidatePage(pageForm, settingId, keyword)); + public Result candidatePageData(PageForm pageForm, String settingId, String keyword, @Param("userIds") String userIds) { + return Result.success(tourUserAssignmentService.branchCandidatePage(pageForm, settingId, keyword, parseUserIds(userIds))); } /** @@ -71,7 +71,7 @@ public class TourBranchUserAssignmentController { @At @SaCheckPermission("tour.branchUserAssignment") public Result matterOptions(String settingId) { - return Result.success(tourUserAssignmentService.listMatterOptions(settingId)); + return Result.success(tourUserAssignmentService.listSignupOpenMatterOptions(settingId)); } /** diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLedgerController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLedgerController.java index fc184703..103bc4a1 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLedgerController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLedgerController.java @@ -132,7 +132,7 @@ public class TourLedgerController { dr.id AS directRelativeId, COALESCE(NULLIF(l.lineName, ''), t.lineName) AS currentLineName, COALESCE(NULLIF(vu.sex, ''), t.gender, '') AS currentGender, - vu.age AS age, + COALESCE(t.age, vu.age) AS age, COALESCE(NULLIF(t.idCard, ''), NULLIF(vu.idCard, ''), '') AS currentIdCard, COALESCE(NULLIF(t.mobile, ''), NULLIF(vu.mobile, ''), '') AS currentMobile, IFNULL(f.familyCount, 0) AS familyCount, @@ -393,7 +393,7 @@ public class TourLedgerController { t.jobNo, t.userName, COALESCE(NULLIF(vu.sex, ''), t.gender, '') AS gender, - vu.age AS age, + COALESCE(t.age, vu.age) AS age, COALESCE(NULLIF(t.idCard, ''), NULLIF(vu.idCard, ''), '') AS idCard, COALESCE(NULLIF(t.mobile, ''), NULLIF(vu.mobile, ''), '') AS mobile, t.unitName, diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMatterController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMatterController.java index 45f227b8..11354a72 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMatterController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMatterController.java @@ -37,8 +37,6 @@ import java.util.stream.Collectors; @At("/platform/tour/matter") public class TourMatterController { - private static final String MOBILE_PATTERN = "^1[3-9]\\d{9}$"; - @Inject private TourMatterService tourMatterService; @@ -353,8 +351,9 @@ public class TourMatterController { if (StrUtil.isBlank(matter.getContactName())) { return Result.error("联系人不能为空"); } - if (StrUtil.isBlank(matter.getContactPhone()) || !matter.getContactPhone().matches(MOBILE_PATTERN)) { - return Result.error("联系方式格式不正确"); + // 线路联系人电话允许填写座机、分机或其他联系说明,后端只保留必填校验。 + if (StrUtil.isBlank(matter.getContactPhone())) { + return Result.error("联系方式不能为空"); } if (matter.getMinGroupPeople() == null || matter.getMinGroupPeople() <= 0) { return Result.error("最少成团人数必须大于0"); diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMySignupController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMySignupController.java index f49d64e5..97e3cf89 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMySignupController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMySignupController.java @@ -154,6 +154,7 @@ public class TourMySignupController { IFNULL(f.familyCount, 0) AS familyCount, a.id AS assignmentId, IFNULL(a.cancelled, 0) AS assignmentCancelled, + m.signupEndTime AS actionSignupEndTime, m.travelStartTime AS actionTravelStartTime, COALESCE(m.travelEndTime, tp.travelEndTime) AS travelEndTime, CASE @@ -309,6 +310,7 @@ public class TourMySignupController { m.matterName, m.unionId AS matterUnionId, m.defaultBoardingPlace, + m.signupEndTime, m.travelStartTime, m.travelEndTime, CASE @@ -345,6 +347,8 @@ public class TourMySignupController { if (isTravelEnded(matter.getString("travelEndTime"))) { return Result.error("线路出行已结束,不能修改"); } + // 修改弹窗内的取消按钮也由后端返回的报名截止状态控制,避免报名结束后仍可操作。 + matter.put("canCancelSignup", isWithinSignupCancelTime(matter.getString("signupEndTime", ""), LocalDateTime.now())); Cnd familyCnd = Cnd.where(TourLedgerFamily::getDelFlag, "=", false) .and(TourLedgerFamily::getLedgerId, "=", ledger.getId()); familyCnd.asc(TourLedgerFamily::getCreatedAt); @@ -549,6 +553,10 @@ public class TourMySignupController { if (ledger == null) { return Result.error("报名记录不存在"); } + TourMatter matter = StrUtil.isBlank(ledger.getMatterId()) ? null : tourMatterService.fetch(ledger.getMatterId()); + if (!isWithinSignupCancelTime(matter)) { + return Result.error("报名已结束,不能取消报名"); + } tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId())); tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId())); tourLedgerService.clear(Cnd.where(TourLedger::getId, "=", ledger.getId())); @@ -676,22 +684,51 @@ public class TourMySignupController { item.put("cancelDeadlineDays", cancelDeadlineDays); item.put("canLeaveTour", StrUtil.isNotBlank(assignmentId) && !assignmentCancelled - && isWithinLeaveTimeRange(item.getString("actionTravelStartTime", ""), cancelDeadlineDays, now)); + && isWithinLeaveTimeRange(item.getString("actionSignupEndTime", ""), item.getString("actionTravelStartTime", ""), cancelDeadlineDays, now)); + item.put("canCancelSignup", isWithinSignupCancelTime(item.getString("actionSignupEndTime", ""), now)); } } /** - * 判断当前时间是否处于“出行开始前N天”到“出行开始前”的可退出范围内。 + * 判断当前报名是否还处于允许取消的报名截止时间内,报名结束后不再显示或执行取消报名。 */ - private boolean isWithinLeaveTimeRange(String travelStartTime, int cancelDeadlineDays, LocalDateTime now) { + private boolean isWithinSignupCancelTime(TourMatter matter) { + if (matter == null) { + return false; + } + return isWithinSignupCancelTime(matter.getSignupEndTime(), LocalDateTime.now()); + } + + /** + * 报名结束时间为空时不允许取消;当前时间小于等于报名结束时间才允许取消。 + */ + private boolean isWithinSignupCancelTime(String signupEndTime, LocalDateTime now) { try { - String normalized = normalizeDateTime(travelStartTime, false); - if (StrUtil.isBlank(normalized)) { + String normalizedSignupEnd = normalizeDateTime(signupEndTime, true); + if (StrUtil.isBlank(normalizedSignupEnd)) { return false; } - LocalDateTime startTime = LocalDateTime.parse(normalized, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + LocalDateTime endTime = LocalDateTime.parse(normalizedSignupEnd, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + return !now.isAfter(endTime); + } catch (Exception e) { + return false; + } + } + + /** + * 判断当前时间是否处于“报名截止时间 < 当前时间 < 出行开始前N天”的可退出范围内。 + */ + private boolean isWithinLeaveTimeRange(String signupEndTime, String travelStartTime, int cancelDeadlineDays, LocalDateTime now) { + try { + String normalizedSignupEnd = normalizeDateTime(signupEndTime, true); + String normalizedTravelStart = normalizeDateTime(travelStartTime, false); + if (StrUtil.isBlank(normalizedSignupEnd) || StrUtil.isBlank(normalizedTravelStart)) { + return false; + } + LocalDateTime signupEnd = LocalDateTime.parse(normalizedSignupEnd, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + LocalDateTime startTime = LocalDateTime.parse(normalizedTravelStart, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); LocalDateTime deadline = startTime.minusDays(cancelDeadlineDays); - return !now.isBefore(deadline) && now.isBefore(startTime); + return now.isAfter(signupEnd) && now.isBefore(deadline); } catch (Exception e) { return false; } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSchoolUserAssignmentController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSchoolUserAssignmentController.java index 129afd82..bfd9d91f 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSchoolUserAssignmentController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSchoolUserAssignmentController.java @@ -50,12 +50,12 @@ public class TourSchoolUserAssignmentController { } /** - * 分页查询候选人员,候选范围由疗休养配置的可参加人员范围决定。 + * 分页查询候选人员,候选范围由疗休养配置的可参加人员范围决定;userIds 仅作为人员选择器筛选条件。 */ @At @SaCheckPermission("tour.schoolUserAssignment") - public Result candidatePageData(PageForm pageForm, String settingId, String unionId, String keyword) { - return Result.success(tourUserAssignmentService.schoolCandidatePage(pageForm, settingId, unionId, keyword)); + public Result candidatePageData(PageForm pageForm, String settingId, String unionId, String keyword, @Param("userIds") String userIds) { + return Result.success(tourUserAssignmentService.schoolCandidatePage(pageForm, settingId, unionId, keyword, parseUserIds(userIds))); } /** diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSettingController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSettingController.java index a58966a8..9c8e650d 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSettingController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSettingController.java @@ -155,6 +155,14 @@ public class TourSettingController { if (tourSetting.getFillBedInfo() == null) { tourSetting.setFillBedInfo(true); } + if (tourSetting.getHomeSignupEntryEnabled() == null) { + tourSetting.setHomeSignupEntryEnabled(false); + } + if (!Boolean.TRUE.equals(tourSetting.getHomeSignupEntryEnabled())) { + tourSetting.setHomeSignupEntryImage(""); + } else { + tourSetting.setHomeSignupEntryImage(normalizeSingleHomeSignupEntryImage(tourSetting.getHomeSignupEntryImage())); + } // 报名资格校验方式默认按人员分配表校验,后续报名校验切换会读取该配置。 if (StrUtil.isBlank(tourSetting.getSignupEligibilityMode())) { tourSetting.setSignupEligibilityMode(SIGNUP_ELIGIBILITY_MODE_ASSIGNED_USER); @@ -226,6 +234,15 @@ public class TourSettingController { .collect(Collectors.toList()); } + private String normalizeSingleHomeSignupEntryImage(String image) { + // 首页报名入口图片仅允许保存一张,兼容逗号分隔的历史多图路径时只保留第一张。 + if (StrUtil.isBlank(image)) { + return ""; + } + List images = StrUtil.splitTrim(image, ","); + return Lang.isEmpty(images) ? "" : images.get(0); + } + private void saveLots(TourSetting tourSetting) { List lots = tourSetting.getLots(); if (Lang.isEmpty(lots)) { diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSignupController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSignupController.java index a4c412c5..ccf70882 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSignupController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSignupController.java @@ -93,11 +93,76 @@ public class TourSignupController { } @At("/h5") - @Ok("beetl:/platform/zhghh5/dayofficework/tour/signup/index.html") + @Ok("beetl:/platform/zhghh5/dayofficework/tour/signup/entry.html") @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) public void h5() { } + /** + * 移动端疗休养报名入口的信息确认页。 + * 用户先确认基础信息并保存到人员分配表,再进入线路选择页面。 + */ + @At("/h5/confirm") + @Ok("beetl:/platform/zhghh5/dayofficework/tour/signup/confirm.html") + @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) + public void h5Confirm() { + } + + /** + * 移动端疗休养报名范围外提示页。 + * 当前登录人不存在人员分配记录时,从报名入口跳转到此页并引导返回首页。 + */ + @At("/h5/noAssignment") + @Ok("beetl:/platform/zhghh5/dayofficework/tour/signup/noAssignment.html") + @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) + public void h5NoAssignment() { + } + + /** + * 移动端报名成功后展示服务须知页。 + * 用户点击“我已阅读”后返回线路选择页面。 + */ + @At("/h5/notice") + @Ok("beetl:/platform/zhghh5/dayofficework/tour/signup/index.html") + @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) + public void h5Notice() { + } + + /** + * 查询移动端信息确认页数据。 + * 数据优先来自人员分配表;人员分配表不存在时返回 vw_user 基础信息供展示。 + * + * @return 信息确认表单数据和乘车地点选项 + */ + @At("/h5/confirmInfo") + @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) + public Result h5ConfirmInfo() { + return Result.success(tourUserAssignmentService.currentH5ConfirmInfo()); + } + + /** + * 保存移动端信息确认页数据。 + * 保存范围限制为当前年度既有人员分配记录,避免新增分配资格影响报名校验。 + * + * @param userName 姓名 + * @param gender 性别 + * @param age 年龄 + * @param idCard 身份证号 + * @param mobile 手机号 + * @param boardingPlace 乘车地点 + * @return 保存结果 + */ + @At("/h5/saveConfirmInfo") + @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) + public Result h5SaveConfirmInfo(String userName, String gender, Integer age, String idCard, String mobile, String boardingPlace) { + try { + tourUserAssignmentService.saveCurrentH5ConfirmInfo(userName, gender, age, idCard, mobile, boardingPlace); + return Result.success(); + } catch (IllegalArgumentException e) { + return Result.error(e.getMessage()); + } + } + @At("/h5/serviceNotice") @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) public Result h5ServiceNotice() { @@ -333,6 +398,7 @@ public class TourSignupController { SELECT m.id AS matterId, m.`year`, + m.settingId, m.matterName, m.minGroupPeople, m.maxGroupPeople, @@ -502,12 +568,14 @@ public class TourSignupController { SELECT m.id AS matterId, m.`year`, + m.settingId, m.matterName, m.unionId AS matterUnionId, m.defaultBoardingPlace, m.travelStartTime, m.travelEndTime, m.contactName, + m.contactPhone, CASE WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) @@ -569,6 +637,7 @@ public class TourSignupController { .addv("unitName", user == null ? "" : defaultIfBlank(user.getUnitName(), "")) .addv("unionId", user == null ? SecurityUtil.getUnionId() : defaultIfBlank(user.getUnionId(), SecurityUtil.getUnionId())) .addv("unionName", user == null ? "" : defaultIfBlank(user.getUnionName(), "")); + NutMap assignment = tourUserAssignmentService.currentUserAssignmentInfo(matter.getString("settingId", "")); Cnd ledgerCnd = Cnd.where(TourLedger::getDelFlag, "=", false) .and(TourLedger::getMatterId, "=", matter.getString("matterId")) .and(TourLedger::getJobNo, "=", staff.getString("jobNo")); @@ -589,6 +658,7 @@ public class TourSignupController { return Result.success(NutMap.NEW() .addv("matter", matter) .addv("staff", staff) + .addv("assignment", assignment) .addv("ledger", ledger) .addv("process", getSignupProcessInfo(ledger)) .addv("families", families) @@ -803,6 +873,7 @@ public class TourSignupController { TourLine line = tourLedgerService.dao().fetch(TourLine.class, matter.getLineId()); boolean directFamilyLine = line != null && Boolean.TRUE.equals(line.getDirectFamilyUnitLine()); TourSetting setting = tourLedgerService.dao().fetch(TourSetting.class, matter.getSettingId()); + fillLedgerFromAssignmentConfirmInfo(ledger, matter.getSettingId()); Result boardingPlaceResult = normalizeBoardingPlace(ledger, matter, setting); if (boardingPlaceResult != null) { return boardingPlaceResult; @@ -838,6 +909,7 @@ public class TourSignupController { } ledger.setOverCostReimbursed(Boolean.TRUE.equals(ledger.getOverCostReimbursed())); fillStaffInfo(ledger); + fillLedgerFromAssignmentConfirmInfo(ledger, matter.getSettingId()); if (update) { tourLedgerService.updateIgnoreNull(ledger); @@ -1288,19 +1360,21 @@ public class TourSignupController { // Button visibility is calculated on the server so H5 follows the same time rules as backend submit actions. item.put("cancelDeadlineDays", cancelDeadlineDays); item.put("canLeaveTour", signed && StrUtil.isNotBlank(assignmentId) && !assignmentCancelled - && isWithinLeaveTimeRange(item.getString("travelStartTime", ""), cancelDeadlineDays, now)); + && isWithinLeaveTimeRange(item.getString("signupEndTime", ""), item.getString("travelStartTime", ""), cancelDeadlineDays, now)); item.put("canCancelLine", signed && StrUtil.isNotBlank(ledgerId) && isWithinSignupRange(item.getString("signupStartTime", ""), item.getString("signupEndTime", ""), now)); } } - private boolean isWithinLeaveTimeRange(String travelStartTime, int cancelDeadlineDays, LocalDateTime now) { + private boolean isWithinLeaveTimeRange(String signupEndTime, String travelStartTime, int cancelDeadlineDays, LocalDateTime now) { + LocalDateTime signupEnd = parseDateTimeValue(signupEndTime, true); LocalDateTime startTime = parseDateTimeValue(travelStartTime, false); - if (startTime == null) { + if (signupEnd == null || startTime == null) { return false; } LocalDateTime deadline = startTime.minusDays(cancelDeadlineDays); - return !now.isBefore(deadline) && now.isBefore(startTime); + // 退出疗休养按钮严格限定在“报名截止时间 < 当前时间 < 出行开始前N天”范围内。 + return now.isAfter(signupEnd) && now.isBefore(deadline); } private boolean isWithinSignupRange(String signupStartTime, String signupEndTime, LocalDateTime now) { @@ -1638,6 +1712,30 @@ public class TourSignupController { ledger.setUnionName(user == null ? ledger.getUnionName() : defaultIfBlank(user.getUnionName(), ledger.getUnionName())); } + private void fillLedgerFromAssignmentConfirmInfo(TourLedger ledger, String settingId) { + if (ledger == null || StrUtil.isBlank(settingId)) { + return; + } + NutMap assignment = tourUserAssignmentService.currentUserAssignmentInfo(settingId); + if (assignment == null || assignment.isEmpty()) { + return; + } + // 移动端信息确认页保存到人员分配表,最终报名时以该确认信息回写台账。 + ledger.setUserName(defaultIfBlank(assignment.getString("userName", ""), ledger.getUserName())); + ledger.setGender(defaultIfBlank(assignment.getString("gender", ""), ledger.getGender())); + Integer assignmentAge = assignment.getInt("age"); + if (assignmentAge != null) { + ledger.setAge(assignmentAge); + } + ledger.setIdCard(defaultIfBlank(assignment.getString("idCard", ""), ledger.getIdCard())); + ledger.setMobile(defaultIfBlank(assignment.getString("mobile", ""), ledger.getMobile())); + ledger.setBoardingPlace(defaultIfBlank(assignment.getString("boardingPlace", ""), ledger.getBoardingPlace())); + ledger.setUnitId(defaultIfBlank(assignment.getString("unitId", ""), ledger.getUnitId())); + ledger.setUnitName(defaultIfBlank(assignment.getString("unitName", ""), ledger.getUnitName())); + ledger.setUnionId(defaultIfBlank(assignment.getString("unionId", ""), ledger.getUnionId())); + ledger.setUnionName(defaultIfBlank(assignment.getString("unionName", ""), ledger.getUnionName())); + } + private void fillLedgerContactFallback(TourLedger ledger, NutMap staff) { if (ledger == null || staff == null) { return; diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedger.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedger.java index bd7f2597..34e63be8 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedger.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedger.java @@ -46,6 +46,11 @@ public class TourLedger extends BaseModel implements Serializable { @ColDefine(type = ColType.VARCHAR, width = 20) private String gender; + @Column + @Comment("年龄") + @ColDefine(type = ColType.INT, width = 3) + private Integer age; + @Column @Comment("身份证号") @ColDefine(type = ColType.VARCHAR, width = 30) diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSetting.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSetting.java index 289882ed..8bd064ee 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSetting.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSetting.java @@ -138,6 +138,17 @@ public class TourSetting extends BaseModel implements Serializable { @Default("1") private Boolean enabled; + @Column + @Comment("是否展示PC首页报名入口") + @ColDefine(type = ColType.BOOLEAN) + @Default("0") + private Boolean homeSignupEntryEnabled; + + @Column + @Comment("PC首页报名入口图片") + @ColDefine(type = ColType.VARCHAR, width = 500) + private String homeSignupEntryImage; + /** * 标段管理沿用老疗休养配置的子表设计,后续线路、旅行社等模块可通过标段ID继续关联。 */ diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourUserAssignment.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourUserAssignment.java index 465a5398..5074ef6e 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourUserAssignment.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourUserAssignment.java @@ -97,6 +97,16 @@ public class TourUserAssignment extends BaseModel implements Serializable { @ColDefine(type = ColType.VARCHAR, width = 20) private String gender; + @Column + @Comment("年龄") + @ColDefine(type = ColType.INT, width = 3) + private Integer age; + + @Column + @Comment("身份证号") + @ColDefine(type = ColType.VARCHAR, width = 30) + private String idCard; + @Column @Comment("手机号") @ColDefine(type = ColType.VARCHAR, width = 30) diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourUserAssignmentService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourUserAssignmentService.java index cf2c9dc2..65efb16e 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourUserAssignmentService.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourUserAssignmentService.java @@ -33,9 +33,10 @@ public interface TourUserAssignmentService extends BaseService schoolCandidatePage(PageForm pageForm, String settingId, String unionId, String keyword); + Pagination schoolCandidatePage(PageForm pageForm, String settingId, String unionId, String keyword, List userIds); /** * 分页查询当前登录人所在分工会的人员分配记录,列表只读取人员分配表,不读取报名台账。 @@ -57,9 +58,10 @@ public interface TourUserAssignmentService extends BaseService branchCandidatePage(PageForm pageForm, String settingId, String keyword); + Pagination branchCandidatePage(PageForm pageForm, String settingId, String keyword, List userIds); /** * 查询启用的疗休养配置选项,供人员分配列表筛选和分配弹窗复用。 @@ -77,6 +79,14 @@ public interface TourUserAssignmentService extends BaseService listMatterOptions(String settingId); + /** + * 查询指定配置下当前处于报名时间内的分配线路选项,供分工会人员选择线路时使用。 + * + * @param settingId 疗休养配置ID + * @return 当前报名时间内的事项选项列表 + */ + List listSignupOpenMatterOptions(String settingId); + /** * 查询分工会选项,供校工会分配候选人和列表筛选使用。 * @@ -148,6 +158,36 @@ public interface TourUserAssignmentService extends BaseService schoolCandidatePage(PageForm pageForm, String settingId, String unionId, String keyword) { + public Pagination schoolCandidatePage(PageForm pageForm, String settingId, String unionId, String keyword, List userIds) { TourSetting setting = fetchSettingForAssignment(settingId); if (setting == null || StrUtil.isBlank(setting.getActivityGroupId())) { return emptyPagination(pageForm); } Sql sql = buildCandidateSql(settingId); Cnd cnd = buildCandidateCnd(settingId, setting.getActivityGroupId(), unionId, keyword); + List normalizedUserIds = normalizeUserIds(userIds); + // 人员选择器多选查询时,候选范围仍受活动组、分工会、已分配排除规则约束,再按指定人员ID精确过滤。 + if (!Lang.isEmpty(normalizedUserIds)) { + cnd.and("u.id", "in", normalizedUserIds); + } cnd.asc("u.unionname"); cnd.asc("u.unitname"); cnd.asc("u.loginname"); @@ -108,12 +113,12 @@ public class TourUserAssignmentServiceImpl extends BaseServiceImpl branchCandidatePage(PageForm pageForm, String settingId, String keyword) { + public Pagination branchCandidatePage(PageForm pageForm, String settingId, String keyword, List userIds) { String unionId = SecurityUtil.getUnionId(); if (StrUtil.isBlank(unionId)) { return emptyPagination(pageForm); } - return schoolCandidatePage(pageForm, settingId, unionId, keyword); + return schoolCandidatePage(pageForm, settingId, unionId, keyword, userIds); } @Override @@ -134,6 +139,18 @@ public class TourUserAssignmentServiceImpl extends BaseServiceImpl listMatterOptions(String settingId) { + return listMatterOptions(settingId, false); + } + + @Override + public List listSignupOpenMatterOptions(String settingId) { + return listMatterOptions(settingId, true); + } + + /** + * 统一查询人员分配可选线路;分工会选择线路时只返回当前报名期内的事项。 + */ + private List listMatterOptions(String settingId, boolean onlySignupOpen) { if (StrUtil.isBlank(settingId)) { return Collections.emptyList(); } @@ -143,6 +160,8 @@ public class TourUserAssignmentServiceImpl extends BaseServiceImpl=", now); + } sql.setCondition(cnd); return listMap(sql); } @@ -336,6 +360,9 @@ public class TourUserAssignmentServiceImpl extends BaseServiceImpl users = Collections.singletonList(fetchAssignmentUserSnapshot(assignment)); checkProxySignupMaxGroupPeople(matterInfo, users); updateAssignmentMatterSnapshot(id, matterInfo, TourUserAssignment.ASSIGN_SOURCE_BRANCH_UNION); @@ -487,6 +514,103 @@ public class TourUserAssignmentServiceImpl extends BaseServiceImpl= 0; + } + private TourUserAssignment buildSchoolAssignment(String settingId, NutMap matterInfo, String personType, NutMap user) { return buildAssignment(settingId, matterInfo, personType, user, TourUserAssignment.ASSIGN_SOURCE_SCHOOL_UNION); } @@ -973,6 +1171,9 @@ public class TourUserAssignmentServiceImpl extends BaseServiceImpl= 14) { + String normalizedIdCard = idCard.trim(); + birthDate = LocalDate.parse(normalizedIdCard.substring(6, 14), DateTimeFormatter.ofPattern("yyyyMMdd")); + } else if (StrUtil.isNotBlank(birthday) && birthday.length() >= 10) { + birthDate = LocalDate.parse(birthday.substring(0, 10), DateTimeFormatter.ofPattern("yyyy-MM-dd")); + } + } catch (Exception e) { + birthDate = null; + } + if (birthDate == null) { + return null; + } + LocalDate today = LocalDate.now(); + int age = today.getYear() - birthDate.getYear(); + if (today.getMonthValue() < birthDate.getMonthValue() + || (today.getMonthValue() == birthDate.getMonthValue() && today.getDayOfMonth() < birthDate.getDayOfMonth())) { + age--; + } + return Math.max(age, 0); + } + + private boolean isValidIdCard(String idCard) { + return StrUtil.isNotBlank(idCard) + && idCard.matches("^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dX]$"); + } } diff --git a/src/main/resources/db/tour_ledger_add_age.sql b/src/main/resources/db/tour_ledger_add_age.sql new file mode 100644 index 00000000..f3913e0f --- /dev/null +++ b/src/main/resources/db/tour_ledger_add_age.sql @@ -0,0 +1,3 @@ +-- Add signup ledger age field for mobile confirmation snapshot. +ALTER TABLE `tour_ledger` + ADD COLUMN `age` int DEFAULT NULL COMMENT '年龄' AFTER `gender`; diff --git a/src/main/resources/db/tour_setting_add_home_signup_entry.sql b/src/main/resources/db/tour_setting_add_home_signup_entry.sql new file mode 100644 index 00000000..78385433 --- /dev/null +++ b/src/main/resources/db/tour_setting_add_home_signup_entry.sql @@ -0,0 +1,4 @@ +-- 疗休养配置增加PC首页报名浮动入口控制字段。 +ALTER TABLE `tour_setting` + ADD COLUMN `homeSignupEntryEnabled` tinyint(1) DEFAULT 0 COMMENT '是否展示PC首页报名入口' AFTER `enabled`, + ADD COLUMN `homeSignupEntryImage` varchar(500) DEFAULT NULL COMMENT 'PC首页报名入口图片' AFTER `homeSignupEntryEnabled`; diff --git a/src/main/resources/db/tour_setting_create.sql b/src/main/resources/db/tour_setting_create.sql index 05caa494..6a6daed4 100644 --- a/src/main/resources/db/tour_setting_create.sql +++ b/src/main/resources/db/tour_setting_create.sql @@ -23,6 +23,8 @@ CREATE TABLE IF NOT EXISTS `tour_setting` ( `allowFamily` tinyint(1) DEFAULT 0 COMMENT '是否允许携带家属', `fillBedInfo` tinyint(1) DEFAULT 1 COMMENT '是否填报床位信息', `enabled` tinyint(1) DEFAULT 1 COMMENT '是否启用', + `homeSignupEntryEnabled` tinyint(1) DEFAULT 0 COMMENT '是否展示PC首页报名入口', + `homeSignupEntryImage` varchar(500) DEFAULT NULL COMMENT 'PC首页报名入口图片', `serviceNotice` text COMMENT '服务须知', `createdBy` varchar(32) DEFAULT NULL COMMENT '创建人', `createdAt` bigint DEFAULT NULL COMMENT '创建时间', @@ -87,6 +89,8 @@ CREATE TABLE IF NOT EXISTS `tour_user_assignment` ( `loginName` varchar(50) DEFAULT NULL COMMENT '工号', `userName` varchar(100) DEFAULT NULL COMMENT '姓名', `gender` varchar(20) DEFAULT NULL COMMENT '性别', + `age` int DEFAULT NULL COMMENT '年龄', + `idCard` varchar(30) DEFAULT NULL COMMENT '身份证号', `mobile` varchar(30) DEFAULT NULL COMMENT '手机号', `unitId` varchar(32) DEFAULT NULL COMMENT '所在单位ID', `unitName` varchar(100) DEFAULT NULL COMMENT '所在单位', diff --git a/src/main/resources/db/tour_user_assignment_add_confirm_info.sql b/src/main/resources/db/tour_user_assignment_add_confirm_info.sql new file mode 100644 index 00000000..852793e7 --- /dev/null +++ b/src/main/resources/db/tour_user_assignment_add_confirm_info.sql @@ -0,0 +1,4 @@ +-- Add mobile H5 signup confirmation fields to tour user assignment. +ALTER TABLE `tour_user_assignment` + ADD COLUMN `age` int DEFAULT NULL COMMENT '年龄' AFTER `gender`, + ADD COLUMN `idCard` varchar(30) DEFAULT NULL COMMENT '身份证号' AFTER `age`; diff --git a/src/main/resources/db/tour_user_assignment_create.sql b/src/main/resources/db/tour_user_assignment_create.sql index 0beb81ff..e91cdc87 100644 --- a/src/main/resources/db/tour_user_assignment_create.sql +++ b/src/main/resources/db/tour_user_assignment_create.sql @@ -13,6 +13,8 @@ CREATE TABLE IF NOT EXISTS `tour_user_assignment` ( `loginName` varchar(50) DEFAULT NULL COMMENT '工号', `userName` varchar(100) DEFAULT NULL COMMENT '姓名', `gender` varchar(20) DEFAULT NULL COMMENT '性别', + `age` int DEFAULT NULL COMMENT '年龄', + `idCard` varchar(30) DEFAULT NULL COMMENT '身份证号', `mobile` varchar(30) DEFAULT NULL COMMENT '手机号', `unitId` varchar(32) DEFAULT NULL COMMENT '所在单位ID', `unitName` varchar(100) DEFAULT NULL COMMENT '所在单位', diff --git a/src/main/resources/static/assets/platform/images/tour/疗休养图片.jpeg b/src/main/resources/static/assets/platform/images/tour/疗休养图片.jpeg new file mode 100644 index 00000000..9df824ed Binary files /dev/null and b/src/main/resources/static/assets/platform/images/tour/疗休养图片.jpeg differ diff --git a/src/main/resources/views/layouts/v4/home/index.html b/src/main/resources/views/layouts/v4/home/index.html index a39f2a2d..bfbb20a2 100644 --- a/src/main/resources/views/layouts/v4/home/index.html +++ b/src/main/resources/views/layouts/v4/home/index.html @@ -93,6 +93,41 @@ layout("/layouts/v4/baseLayout.html"){ font-weight: 500; } + .tour-home-float { + position: fixed; + left: 0; + top: 0; + z-index: 99999; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 8px 24px rgba(15, 23, 42, 0.22); + cursor: pointer; + background: #ffffff; + will-change: transform; + } + + .tour-home-float img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; + } + + .tour-home-float__title { + position: absolute; + left: 0; + right: 0; + bottom: 0; + padding: 8px 10px; + color: #ffffff; + font-size: 15px; + font-weight: 700; + line-height: 1.2; + background: linear-gradient(180deg, rgba(15, 23, 42, 0), rgba(15, 23, 42, 0.75)); + text-shadow: 0 1px 3px rgba(15, 23, 42, 0.45); + box-sizing: border-box; + } +
@@ -133,6 +168,16 @@ layout("/layouts/v4/baseLayout.html"){
+
+ +
{{ item.title }}
+
+ + + diff --git a/src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/entry.html b/src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/entry.html new file mode 100644 index 00000000..2c0c2b15 --- /dev/null +++ b/src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/entry.html @@ -0,0 +1,42 @@ + + + + +
+ 加载中... +
+ + + + diff --git a/src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/index.html b/src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/index.html index 43194769..477d1132 100644 --- a/src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/index.html +++ b/src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/index.html @@ -6,83 +6,32 @@ layout("/layouts/platform_h5.html"){ .tour-signup-h5 { min-height: 100vh; background: #f5f7fb; - padding-bottom: 80px; + padding: 10px 10px 80px; box-sizing: border-box; } - .tour-signup-banner { - position: relative; - height: 193px; - overflow: hidden; - background: #0f74bc; - color: #ffffff; - } - - .tour-signup-swipe { - width: 100%; - height: 100%; - } - - .tour-signup-swipe img { - display: block; - width: 100%; - height: 193px; - object-fit: cover; - } - - .tour-signup-banner::after { - content: ""; - position: absolute; - inset: 0; - background: linear-gradient(90deg, rgba(5, 83, 143, 0.72) 0%, rgba(5, 83, 143, 0.32) 48%, rgba(5, 83, 143, 0.08) 100%); - pointer-events: none; - } - - .tour-signup-banner__text { - position: absolute; - left: 16px; - right: 16px; - bottom: 20px; - z-index: 1; - } - - .tour-signup-title { - font-size: 22px; - font-weight: 700; - line-height: 1.35; - } - - .tour-signup-subtitle { - margin-top: 8px; - color: rgba(255, 255, 255, 0.86); - font-size: 13px; - line-height: 1.5; - } - .tour-signup-card { - margin: 10px 12px 0; + display: flex; + flex-direction: column; + min-height: calc(100vh - 160px); + max-height: calc(100vh - 160px); + margin: 0; padding: 14px 14px 16px; border-radius: 8px; background: #ffffff; box-shadow: 0 8px 20px rgba(15, 23, 42, 0.08); + box-sizing: border-box; } .tour-signup-card__header { display: flex; align-items: center; - justify-content: space-between; + justify-content: flex-end; gap: 10px; padding-bottom: 12px; border-bottom: 1px solid #eef2f7; } - .tour-signup-card__title { - color: #111827; - font-size: 17px; - font-weight: 700; - line-height: 1.4; - } - .tour-signup-card__year { flex-shrink: 0; color: #0f74bc; @@ -91,13 +40,19 @@ layout("/layouts/platform_h5.html"){ } .tour-signup-notice { - margin-top: 14px; color: #334155; font-size: 15px; line-height: 1.8; word-break: break-word; } + .tour-signup-card__body { + flex: 1; + min-height: 0; + margin-top: 14px; + overflow-y: auto; + } + .tour-signup-notice /deep/ img, .tour-signup-notice /deep/ video { max-width: 100%; @@ -140,7 +95,10 @@ layout("/layouts/platform_h5.html"){ } .tour-signup-empty { - padding: 34px 0 26px; + min-height: 100%; + display: flex; + align-items: center; + justify-content: center; } .tour-signup-footer { @@ -156,48 +114,38 @@ layout("/layouts/platform_h5.html"){ @@ -306,7 +306,7 @@ layout("/layouts/platform_h5.html"){ } return this.canModifySignup() ? "修改报名" : "审核中" } - return "我要核对信息" + return "我要报名" }, detailActionType() { return this.canRevokeSignup() ? "danger" : "info" diff --git a/src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/list.html b/src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/list.html index e8ab274d..2e75e526 100644 --- a/src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/list.html +++ b/src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/list.html @@ -462,7 +462,7 @@ layout("/layouts/platform_h5.html"){
- +
@@ -668,6 +668,9 @@ layout("/layouts/platform_h5.html"){ } }, methods: { + goHome() { + window.location.replace("/platform/h5/home") + }, thumbUrl(row) { const thumb = this.resolveThumbPath(row && (row.lineMobileThumb || row.agencyMobileThumb)) if (!thumb) { @@ -817,7 +820,7 @@ layout("/layouts/platform_h5.html"){ } vant.Dialog.confirm({ title: "提示", - message: "是否确认退出本次疗休养,退出后将取消报名资格!", + message: "是否确认退出本次疗休,退出后将取消报名资格!", confirmButtonColor: "#ee2f2f" }).then(() => { this.$axios.post("/platform/tour/signup/h5/doLeaveTour", { assignmentId: row.assignmentId }).then((res) => { @@ -837,7 +840,7 @@ layout("/layouts/platform_h5.html"){ } vant.Dialog.confirm({ title: "提示", - message: "是否确认取消本线路?", + message: "是否确认取消本线路报名?", confirmButtonColor: "#ee2f2f" }).then(() => { this.$axios.post("/platform/tour/signup/h5/doCancelLine", { ledgerId: row.ledgerId }).then((res) => { diff --git a/src/main/resources/views/platform/zhghh5/sys/home/home.js b/src/main/resources/views/platform/zhghh5/sys/home/home.js index 8d757f57..4efec188 100644 --- a/src/main/resources/views/platform/zhghh5/sys/home/home.js +++ b/src/main/resources/views/platform/zhghh5/sys/home/home.js @@ -73,7 +73,7 @@ const home = {
-
+
政策文件
更多 @@ -120,6 +120,7 @@ const home = { return { activityOptions: [], quickEntries: [], + disabledHomeModules: ["劳模先进", "普惠信息", "我的课堂", "政策文件"], classroomCourses: [], classroomCourseIndex: 0, classroomCourseTimer: null, @@ -374,16 +375,40 @@ const home = { }, 3000) }, menuClick(item) { + if (this.isHomeModuleDisabled(item && item.name)) { + this.showDisabledHomeModuleNotice() + return + } this.$pjaxReplace(item.href) }, featureClick(item) { + if (this.isHomeModuleDisabled(item && item.name)) { + this.showDisabledHomeModuleNotice() + return + } if (item.href) { this.$pjaxReplace(item.href) } }, enterClassroom() { + if (this.isHomeModuleDisabled("我的课堂")) { + this.showDisabledHomeModuleNotice() + return + } this.$pjaxReplace("/platform/learning/course/h5") }, + enterPolicy() { + if (this.isHomeModuleDisabled("政策文件")) { + this.showDisabledHomeModuleNotice() + } + }, + isHomeModuleDisabled(moduleName) { + // 首页临时停用模块统一在点击入口拦截,保留原模块展示和后台数据配置。 + return moduleName && this.disabledHomeModules.indexOf(moduleName) !== -1 + }, + showDisabledHomeModuleNotice() { + this.$toast("暂未启用") + }, bannerChange(index) { this.activeBannerIndex = index },