From f4f8387c1ee356aac741503021623d036f559ee7 Mon Sep 17 00:00:00 2001 From: zhangrui Date: Sun, 14 Jun 2026 17:21:05 +0800 Subject: [PATCH] =?UTF-8?q?=E7=96=97=E4=BC=91=E5=85=BB=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/sys/controller/SysHomeController.java | 120 +++++----- .../tour/controller/TourGroupController.java | 39 +--- .../tour/controller/TourLedgerController.java | 31 ++- .../controller/TourMySignupController.java | 113 ++++++++- .../TourSchoolUnionApprovalController.java | 19 ++ .../tour/controller/TourSignupController.java | 162 +++++++++++-- .../TourUnionApprovalController.java | 19 ++ .../controller/TourUnionLedgerController.java | 4 +- .../dayofficework/tour/models/TourLedger.java | 5 + .../service/TourUserAssignmentService.java | 7 + .../impl/TourUserAssignmentServiceImpl.java | 9 +- .../resources/db/tour_ledger_add_mobile.sql | 3 + .../views/layouts/v4/home/index.html | 2 +- .../Tour/branchUserAssignment/index.html | 2 +- .../zhgh/dayofficework/Tour/signup/index.html | 131 ++++++++++- .../dayofficework/tour/mysignup/index.html | 33 +++ .../dayofficework/tour/signup/apply.html | 41 +++- .../dayofficework/tour/signup/lineInfo.html | 43 +--- .../dayofficework/tour/signup/list.html | 218 +++++++++++------- .../views/platform/zhghh5/sys/home/home.js | 13 +- 20 files changed, 733 insertions(+), 281 deletions(-) create mode 100644 src/main/resources/db/tour_ledger_add_mobile.sql 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 60d71311..91e35037 100644 --- a/src/main/java/com/budwk/app/sys/controller/SysHomeController.java +++ b/src/main/java/com/budwk/app/sys/controller/SysHomeController.java @@ -5,7 +5,6 @@ import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.http.useragent.UserAgent; import cn.hutool.http.useragent.UserAgentUtil; -import cn.hutool.json.JSONObject; import com.budwk.app.base.constant.RoleConstant; import com.budwk.app.base.result.Result; import com.budwk.app.sys.models.Sys_home_activity; @@ -287,76 +286,67 @@ public class SysHomeController { // @CacheDefaults(cacheName = RedisConstant.PLATFORM_REDIS_WKCACHE_PREFIX + "web_news", isHash = true) // @CacheResult(cacheKey = "news", ignoreNull = true, cacheLiveTime = 60 * 60 * 24) public Result getNews() { - // 定义要爬取的URL String domain = "https://cdgh.ncu.edu.cn/"; try { - // 使用Jsoup连接到URL并获取页面内容 Document doc = Jsoup.connect(domain).get(); - JSONObject root = new JSONObject(); - - // 1. 新闻快讯(来自 .list1) - List newsArray = new ArrayList<>(); - Elements newsItems = doc.select(".group1 .list1 li"); - for (Element item : newsItems) { - String date = item.select(".date").text().trim(); - Element link = item.select("a.text").first(); - String title = link.select(".title").text().trim(); - String url = link.attr("href").trim(); - - JSONObject node = new JSONObject(); - node.set("title", title); - node.set("date", date); - node.set("url", domain + url); - newsArray.add(node); - } - root.set("news", newsArray); - - // 2. 通知公告(来自 .list2) - List noticesArray = new ArrayList<>(); - Elements noticeItems = doc.select(".group2 .list2 li"); - for (Element item : noticeItems) { - String date = item.select(".date").text().trim(); - Element link = item.select("a.title").first(); - String title = link.text().trim(); - String url = link.attr("href").trim(); - JSONObject node = new JSONObject(); - node.set("title", title); - node.set("date", date); - node.set("url", domain + url); - noticesArray.add(node); - } - root.set("notices", noticesArray); - - // 3. 基层风采(来自 .pic-list1) - List grassrootsArray = new ArrayList<>(); - Elements grassrootsItems = doc.select(".group3 .pic-list1 li"); - for (Element item : grassrootsItems) { - Element link = item.select("a.img-scale").first(); - String date = link.select(".date").text().trim(); - String title = link.select(".title").text().trim(); - String url = link.attr("href").trim(); - String summary = link.select(".info").text().trim(); - - String imageUrl = ""; - Element img = link.select("img").first(); - if (img != null) { - imageUrl = img.attr("src").trim(); - } - - JSONObject node = new JSONObject(); - node.set("title", title); - node.set("date", date); - node.set("url", domain + url); - node.set("summary", summary); - node.set("image", domain + imageUrl); - grassrootsArray.add(node); - } - root.set("grassroots", grassrootsArray); - return Result.success(root); + List news = new ArrayList<>(); + news.add(NutMap.NEW().setv("label", "新闻快讯") + .setv("value", parseHomeNewsItems(domain, doc.select(".group1 .list1 li"), "a.text", ".title"))); + news.add(NutMap.NEW().setv("label", "通知公告") + .setv("value", parseHomeNewsItems(domain, doc.select(".group2 .list2 li"), "a.title", ""))); + news.add(NutMap.NEW().setv("label", "基层风采") + .setv("value", parseHomeNewsItems(domain, doc.select(".group3 .pic-list1 li"), "a.img-scale", ".title"))); + return Result.success(news); } catch (Exception e) { log.error(e); - return Result.success(); + return Result.success(Collections.emptyList()); } } + + /** + * Converts the NCU union website list nodes to the PC v4 home carousel structure. + * The migrated jcdt component renders label/value and item href/time/text fields. + * + * @param domain base website domain used to complete relative links + * @param items news list nodes parsed from the source page + * @param linkSelector selector for the clickable element inside each list node + * @param titleSelector selector for the title inside the clickable element, blank means using link text + * @return normalized news items for the PC v4 home page + */ + private List parseHomeNewsItems(String domain, Elements items, String linkSelector, String titleSelector) { + List rows = new ArrayList<>(); + for (Element item : items) { + Element link = item.select(linkSelector).first(); + if (link == null) { + continue; + } + String title = StrUtil.isBlank(titleSelector) ? link.text().trim() : link.select(titleSelector).text().trim(); + String href = link.attr("href").trim(); + String time = item.select(".date").text().trim(); + rows.add(NutMap.NEW() + .setv("href", completeNewsUrl(domain, href)) + .setv("time", time) + .setv("text", title)); + } + return rows; + } + + /** + * Completes relative news links while preserving absolute website links. + * + * @param domain base website domain + * @param href original href from the source page + * @return absolute link for opening the news detail page + */ + private String completeNewsUrl(String domain, String href) { + if (StrUtil.isBlank(href) || StrUtil.startWithIgnoreCase(href, "http://") + || StrUtil.startWithIgnoreCase(href, "https://")) { + return href; + } + if (href.startsWith("/")) { + return domain + href.substring(1); + } + return domain + href; + } } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourGroupController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourGroupController.java index 9c3c3ab9..a0c5d547 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourGroupController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourGroupController.java @@ -193,14 +193,15 @@ public class TourGroupController { t.id, t.jobNo, t.userName, - '' AS mobile, - t.idCard, + COALESCE(NULLIF(t.mobile, ''), NULLIF(vu.mobile, ''), '') AS mobile, + COALESCE(NULLIF(t.idCard, ''), NULLIF(vu.idCard, ''), '') AS idCard, t.unionId, t.unionName, t.signupTime FROM tour_ledger t INNER JOIN tour_matter m ON m.id = t.matterId INNER JOIN tour_line l ON l.id = m.lineId + LEFT JOIN vw_user vu ON vu.loginname = t.jobNo $condition ORDER BY $orderColumn $orderBy, t.signupTime DESC, t.createdAt DESC """); @@ -212,7 +213,6 @@ public class TourGroupController { tourLedgerService.dao().execute(listSql); List list = listSql.getList(NutMap.class); - fillSignupMobile(list); fillSignupFamilies(list); Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list); return Result.success(pagination); @@ -300,8 +300,8 @@ public class TourGroupController { t.id AS ledgerId, t.jobNo, t.userName, - t.idCard, - vu.mobile, + COALESCE(NULLIF(t.idCard, ''), NULLIF(vu.idCard, ''), '') AS idCard, + COALESCE(NULLIF(t.mobile, ''), NULLIF(vu.mobile, ''), '') AS mobile, t.unionName, l.lineName, CASE @@ -526,35 +526,6 @@ public class TourGroupController { } } - private void fillSignupMobile(List list) { - if (list == null || list.isEmpty()) { - return; - } - List jobNos = new ArrayList<>(); - for (NutMap item : list) { - String jobNo = item.getString("jobNo", ""); - if (StrUtil.isNotBlank(jobNo) && !jobNos.contains(jobNo)) { - jobNos.add(jobNo); - } - } - if (jobNos.isEmpty()) { - return; - } - - Sql sql = Sqls.create("SELECT loginname, mobile FROM vw_user WHERE loginname IN (@jobNos)"); - sql.setParam("jobNos", jobNos.toArray(new String[0])); - sql.setCallback(Sqls.callback.maps()); - tourLedgerService.dao().execute(sql); - - Map mobileMap = new HashMap<>(); - for (NutMap user : sql.getList(NutMap.class)) { - mobileMap.put(user.getString("loginname", ""), user.getString("mobile", "")); - } - for (NutMap item : list) { - item.put("mobile", mobileMap.getOrDefault(item.getString("jobNo", ""), "")); - } - } - private Cnd buildQueryCnd(Integer year, String lineName, String lineType, String unionId) { Cnd cnd = Cnd.NEW(); cnd.and("m.delFlag", "=", false); 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 3e97679d..fc184703 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 @@ -133,8 +133,8 @@ public class TourLedgerController { COALESCE(NULLIF(l.lineName, ''), t.lineName) AS currentLineName, COALESCE(NULLIF(vu.sex, ''), t.gender, '') AS currentGender, vu.age AS age, - COALESCE(NULLIF(vu.idCard, ''), t.idCard, '') AS currentIdCard, - COALESCE(NULLIF(vu.mobile, ''), '') AS mobile, + COALESCE(NULLIF(t.idCard, ''), NULLIF(vu.idCard, ''), '') AS currentIdCard, + COALESCE(NULLIF(t.mobile, ''), NULLIF(vu.mobile, ''), '') AS currentMobile, IFNULL(f.familyCount, 0) AS familyCount, CASE WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' @@ -188,6 +188,7 @@ public class TourLedgerController { item.put("lineName", item.getString("currentLineName")); item.put("gender", item.getString("currentGender")); item.put("idCard", item.getString("currentIdCard")); + item.put("mobile", item.getString("currentMobile")); }); Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list); return Result.success(pagination); @@ -206,6 +207,7 @@ public class TourLedgerController { if (!canAccessLedgerUnion(ledger.getUnionId())) { return Result.error("无权查看该台账记录"); } + fillLedgerContactFallback(ledger); ledger.setLineName(getCurrentLineName(ledger)); Cnd familyCnd = Cnd.NEW(); familyCnd.and(TourLedgerFamily::getLedgerId, "=", id); @@ -392,8 +394,8 @@ public class TourLedgerController { t.userName, COALESCE(NULLIF(vu.sex, ''), t.gender, '') AS gender, vu.age AS age, - COALESCE(NULLIF(vu.idCard, ''), t.idCard, '') AS idCard, - COALESCE(NULLIF(vu.mobile, ''), '') AS mobile, + COALESCE(NULLIF(t.idCard, ''), NULLIF(vu.idCard, ''), '') AS idCard, + COALESCE(NULLIF(t.mobile, ''), NULLIF(vu.mobile, ''), '') AS mobile, t.unitName, t.boardingPlace, COALESCE(NULLIF(l.lineName, ''), t.lineName, '') AS lineName, @@ -491,11 +493,11 @@ public class TourLedgerController { t.id, t.jobNo, t.userName, - t.idCard, + COALESCE(NULLIF(t.idCard, ''), NULLIF(vu.idCard, ''), '') AS idCard, t.unionId, COALESCE(NULLIF(t.unionName, ''), su.name, vu.unionName, '') AS unionName, COALESCE(NULLIF(su.unionCode, ''), vu.unionCode, '') AS unionCode, - COALESCE(NULLIF(vu.mobile, ''), '') AS mobile, + COALESCE(NULLIF(t.mobile, ''), NULLIF(vu.mobile, ''), '') AS mobile, COALESCE(NULLIF(l.lineName, ''), t.lineName, '') AS lineName, CASE WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' @@ -948,6 +950,23 @@ public class TourLedgerController { return StrUtil.isNotBlank(currentUnionId) && StrUtil.equals(currentUnionId, unionId); } + private void fillLedgerContactFallback(TourLedger ledger) { + if (ledger == null || (StrUtil.isNotBlank(ledger.getIdCard()) && StrUtil.isNotBlank(ledger.getMobile()))) { + return; + } + // Detail views prefer ledger contact data; blank ledger fields fall back to the user profile snapshot for display only. + View_user user = tourLedgerService.dao().fetch(View_user.class, Cnd.where(View_user::getLoginname, "=", ledger.getJobNo())); + if (user == null) { + return; + } + if (StrUtil.isBlank(ledger.getIdCard())) { + ledger.setIdCard(user.getIdCard()); + } + if (StrUtil.isBlank(ledger.getMobile())) { + ledger.setMobile(user.getMobile()); + } + } + private void appendApprovedWorkflowFilter(Cnd cnd) { SqlExpressionGroup group = new SqlExpressionGroup(); group.or("ins.id", "IS", null); 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 5d6e5e0b..f49d64e5 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 @@ -27,6 +27,7 @@ import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerFamily; import com.budwk.app.zhgh.dayofficework.tour.models.TourLine; import com.budwk.app.zhgh.dayofficework.tour.models.TourMatter; import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting; +import com.budwk.app.zhgh.dayofficework.tour.models.TourUserAssignment; import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeService; import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService; import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService; @@ -147,8 +148,13 @@ public class TourMySignupController { (SELECT MAX(id) FROM wf_process_task WHERE processInstanceId = ins.id AND taskName = 'startTask' AND taskState IN (10, 20)) AS startTaskId, IF(IFNULL(l.directFamilyUnitLine, 0) = 1 OR dr.id IS NOT NULL, 1, 0) AS directFamilyUnitLine, dr.id AS directRelativeId, + COALESCE(NULLIF(t.idCard, ''), NULLIF(vu.idCard, ''), '') AS currentIdCard, + COALESCE(NULLIF(t.mobile, ''), NULLIF(vu.mobile, ''), '') AS currentMobile, IFNULL(GROUP_CONCAT(DISTINCT task.displayName), IF(ins.id IS NULL, '', '结束')) AS curTaskName, IFNULL(f.familyCount, 0) AS familyCount, + a.id AS assignmentId, + IFNULL(a.cancelled, 0) AS assignmentCancelled, + m.travelStartTime AS actionTravelStartTime, COALESCE(m.travelEndTime, tp.travelEndTime) AS travelEndTime, CASE WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' @@ -160,6 +166,7 @@ public class TourMySignupController { LEFT JOIN wf_process_task task ON task.processInstanceId = ins.id AND task.taskState = 10 LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 LEFT JOIN tour_ledger_direct_relative dr ON dr.ledgerId = t.id AND dr.delFlag = 0 + LEFT JOIN vw_user vu ON vu.loginname = t.jobNo LEFT JOIN ( SELECT ledgerId, COUNT(1) AS familyCount FROM tour_ledger_family @@ -167,6 +174,10 @@ public class TourMySignupController { GROUP BY ledgerId ) f ON f.ledgerId = t.id LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN tour_user_assignment a ON a.matterId = t.matterId + AND a.userId = @currentUserId + AND a.loginName = t.jobNo + AND a.delFlag = 0 LEFT JOIN ( SELECT `year`, @@ -192,13 +203,20 @@ public class TourMySignupController { ORDER BY $orderColumn $orderBy, t.`year` DESC, t.signupTime DESC, t.createdAt DESC """); listSql.setCondition(cnd); + listSql.setParam("currentUserId", SecurityUtil.getUserId()); listSql.setVar("orderColumn", getOrderColumn(pageForm.getPageOrderName())); listSql.setVar("orderBy", "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc"); listSql.setPager(tourLedgerService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize())); listSql.setCallback(Sqls.callback.maps()); tourLedgerService.dao().execute(listSql); - Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, listSql.getList(NutMap.class)); + List list = listSql.getList(NutMap.class); + list.forEach(item -> { + item.put("idCard", item.getString("currentIdCard")); + item.put("mobile", item.getString("currentMobile")); + }); + appendMySignupActionFlags(list); + Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list); return Result.success(pagination); } @@ -209,6 +227,7 @@ public class TourMySignupController { if (ledger == null) { return Result.error("报名记录不存在"); } + fillLedgerContactFallback(ledger); Cnd familyCnd = Cnd.where(TourLedgerFamily::getDelFlag, "=", false) .and(TourLedgerFamily::getLedgerId, "=", ledger.getId()); familyCnd.asc(TourLedgerFamily::getCreatedAt); @@ -279,6 +298,7 @@ public class TourMySignupController { if (ledger == null) { return Result.error("报名记录不存在"); } + fillLedgerContactFallback(ledger); if (StrUtil.isBlank(ledger.getMatterId())) { return Result.error("历史报名记录缺少事项信息,不能修改"); } @@ -536,6 +556,29 @@ public class TourMySignupController { return Result.success().addMsg("取消报名成功"); } + /** + * 移动端我的疗休养退出报名入口。 + * 仅允许当前登录人操作自己的人员分配记录,具体退出截止天数和台账清理逻辑复用 service 层。 + * + * @param assignmentId 人员分配记录ID + * @return 退出结果 + */ + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission(value = {"tour.mysignup", "h5.tour.mysignup"}, mode = SaMode.OR) + public Result doLeaveTour(String assignmentId) { + TourUserAssignment assignment = fetchOwnAssignment(assignmentId); + if (assignment == null) { + return Result.error("人员分配记录不存在或无权操作"); + } + try { + tourUserAssignmentService.cancelAssignment(assignment.getId()); + return Result.success().addMsg("退出疗休养报名成功"); + } catch (IllegalArgumentException e) { + return Result.error(e.getMessage()); + } + } + @At @Aop(TransAop.READ_COMMITTED) @SaCheckPermission(value = {"tour.mysignup", "h5.tour.mysignup"}, mode = SaMode.OR) @@ -605,6 +648,55 @@ public class TourMySignupController { .and(TourLedger::getJobNo, "=", currentJobNo())); } + /** + * 查询当前登录人的人员分配记录,避免移动端传入他人分配ID进行退出。 + */ + private TourUserAssignment fetchOwnAssignment(String assignmentId) { + if (StrUtil.isBlank(assignmentId)) { + return null; + } + return tourUserAssignmentService.fetch(Cnd.where(TourUserAssignment::getId, "=", assignmentId) + .and(TourUserAssignment::getUserId, "=", SecurityUtil.getUserId()) + .and(TourUserAssignment::getLoginName, "=", currentJobNo()) + .and(TourUserAssignment::getDelFlag, "=", false)); + } + + /** + * 给移动端我的疗休养列表追加操作按钮显示标识,退出截止时间与 service 层最终校验保持一致。 + */ + private void appendMySignupActionFlags(List list) { + if (Lang.isEmpty(list)) { + return; + } + int cancelDeadlineDays = tourUserAssignmentService.getCancelDeadlineDays(); + LocalDateTime now = LocalDateTime.now(); + for (NutMap item : list) { + boolean assignmentCancelled = item.getBoolean("assignmentCancelled", false); + String assignmentId = item.getString("assignmentId", ""); + item.put("cancelDeadlineDays", cancelDeadlineDays); + item.put("canLeaveTour", StrUtil.isNotBlank(assignmentId) + && !assignmentCancelled + && isWithinLeaveTimeRange(item.getString("actionTravelStartTime", ""), cancelDeadlineDays, now)); + } + } + + /** + * 判断当前时间是否处于“出行开始前N天”到“出行开始前”的可退出范围内。 + */ + private boolean isWithinLeaveTimeRange(String travelStartTime, int cancelDeadlineDays, LocalDateTime now) { + try { + String normalized = normalizeDateTime(travelStartTime, false); + if (StrUtil.isBlank(normalized)) { + return false; + } + LocalDateTime startTime = LocalDateTime.parse(normalized, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + LocalDateTime deadline = startTime.minusDays(cancelDeadlineDays); + return !now.isBefore(deadline) && now.isBefore(startTime); + } catch (Exception e) { + return false; + } + } + private Result checkSignup(TourLedger ledger) { if (ledger == null) { return Result.error("参数错误"); @@ -1037,13 +1129,30 @@ public class TourMySignupController { ledger.setJobNo(user == null ? SecurityUtil.getUserLoginname() : defaultIfBlank(user.getLoginname(), SecurityUtil.getUserLoginname())); ledger.setUserName(user == null ? SecurityUtil.getUserUsername() : defaultIfBlank(user.getUsername(), SecurityUtil.getUserUsername())); ledger.setGender(user == null ? ledger.getGender() : defaultIfBlank(user.getSex(), ledger.getGender())); - ledger.setIdCard(user == null ? ledger.getIdCard() : defaultIfBlank(user.getIdCard(), ledger.getIdCard())); + ledger.setIdCard(user == null ? ledger.getIdCard() : defaultIfBlank(ledger.getIdCard(), user.getIdCard())); ledger.setUnitId(user == null ? SecurityUtil.getUnitId() : defaultIfBlank(user.getUnitId(), SecurityUtil.getUnitId())); ledger.setUnitName(user == null ? ledger.getUnitName() : defaultIfBlank(user.getUnitName(), ledger.getUnitName())); ledger.setUnionId(user == null ? SecurityUtil.getUnionId() : defaultIfBlank(user.getUnionId(), ledger.getUnionId())); ledger.setUnionName(user == null ? ledger.getUnionName() : defaultIfBlank(user.getUnionName(), ledger.getUnionName())); } + private void fillLedgerContactFallback(TourLedger ledger) { + if (ledger == null || (StrUtil.isNotBlank(ledger.getIdCard()) && StrUtil.isNotBlank(ledger.getMobile()))) { + return; + } + // My signup detail keeps ledger contact data first; blank contact fields use the user profile snapshot for display only. + View_user user = tourLedgerService.dao().fetch(View_user.class, Cnd.where(View_user::getLoginname, "=", ledger.getJobNo())); + if (user == null) { + return; + } + if (StrUtil.isBlank(ledger.getIdCard())) { + ledger.setIdCard(user.getIdCard()); + } + if (StrUtil.isBlank(ledger.getMobile())) { + ledger.setMobile(user.getMobile()); + } + } + private void clearBedInfo(TourLedger ledger, List familyList) { if (ledger != null) { ledger.setBedType(""); diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSchoolUnionApprovalController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSchoolUnionApprovalController.java index 1c6b7467..0b15d566 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSchoolUnionApprovalController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSchoolUnionApprovalController.java @@ -7,6 +7,7 @@ import com.budwk.app.base.page.Pagination; import com.budwk.app.base.param.PageForm; import com.budwk.app.base.result.Result; import com.budwk.app.flow.enums.ProcessTaskStateEnum; +import com.budwk.app.sys.views.View_user; import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger; import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerDirectRelative; @@ -171,6 +172,7 @@ public class TourSchoolUnionApprovalController { if (ledger == null) { return Result.error("报名记录不存在或无权查看"); } + fillLedgerContactFallback(ledger); ledger.setLineName(getCurrentLineName(ledger)); Cnd familyCnd = Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()) .and(TourLedgerFamily::getDelFlag, "=", false); @@ -465,6 +467,23 @@ public class TourSchoolUnionApprovalController { return sql.getInt() > 0 ? tourLedgerService.fetch(id) : null; } + private void fillLedgerContactFallback(TourLedger ledger) { + if (ledger == null || (StrUtil.isNotBlank(ledger.getIdCard()) && StrUtil.isNotBlank(ledger.getMobile()))) { + return; + } + // Approval detail keeps ledger contact data first; blank contact fields use the user profile snapshot for display only. + View_user user = tourLedgerService.dao().fetch(View_user.class, Cnd.where(View_user::getLoginname, "=", ledger.getJobNo())); + if (user == null) { + return; + } + if (StrUtil.isBlank(ledger.getIdCard())) { + ledger.setIdCard(user.getIdCard()); + } + if (StrUtil.isBlank(ledger.getMobile())) { + ledger.setMobile(user.getMobile()); + } + } + private String getCurrentLineName(TourLedger ledger) { if (ledger == null) { return ""; 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 0034078d..fd80e9ba 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 @@ -26,6 +26,7 @@ import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerFamily; import com.budwk.app.zhgh.dayofficework.tour.models.TourLine; import com.budwk.app.zhgh.dayofficework.tour.models.TourMatter; import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting; +import com.budwk.app.zhgh.dayofficework.tour.models.TourUserAssignment; import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeService; import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService; import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService; @@ -167,6 +168,8 @@ public class TourSignupController { m.`year`, m.unionId, COALESCE(u.name, '校工会') AS unionName, + m.signupStartTime, + m.signupEndTime, m.lineId, l.lineName, l.lineType, @@ -185,6 +188,8 @@ public class TourSignupController { my.instanceState, my.taskKey, my.startTaskId, + my.assignmentId, + IFNULL(my.assignmentCancelled, 0) AS assignmentCancelled, IFNULL(my.canRevoke, 0) AS canRevoke, CASE WHEN my.ledgerId IS NULL THEN 0 ELSE 1 END AS signed FROM tour_matter m @@ -217,9 +222,12 @@ public class TourSignupController { MAX(ins.id) AS instanceId, MAX(ins.state) AS instanceState, MAX(task.taskName) AS taskKey, + MAX(ua.id) AS assignmentId, + MAX(IFNULL(ua.cancelled, 0)) AS assignmentCancelled, MAX((SELECT MAX(st.id) FROM wf_process_task st WHERE st.processInstanceId = ins.id AND st.taskName = 'startTask' AND st.taskState IN (10, 20))) AS startTaskId, MAX(IF((SELECT taskName FROM wf_process_task tt WHERE tt.id = task.taskParentId) = 'startTask', 1, 0)) AS canRevoke FROM tour_ledger t + LEFT JOIN tour_user_assignment ua ON ua.matterId = t.matterId AND ua.loginName = t.jobNo AND ua.delFlag = 0 LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id AND ins.state <> @abandonState LEFT JOIN wf_process_task task ON task.processInstanceId = ins.id AND task.taskState = 10 WHERE t.delFlag = 0 @@ -240,17 +248,73 @@ public class TourSignupController { listSql.setCallback(Sqls.callback.maps()); tourMatterService.dao().execute(listSql); - Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, listSql.getList(NutMap.class)); + List list = listSql.getList(NutMap.class); + appendH5LineActionFlags(list); + Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list); return Result.success(pagination); } + /** + * 移动端退出疗休养入口,复用人员分配退出逻辑并限定只能操作当前登录人的分配记录。 + * + * @param assignmentId 人员分配记录ID + * @return 退出结果 + */ + @At("/h5/doLeaveTour") + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("h5.tour.signup") + public Result h5DoLeaveTour(String assignmentId) { + TourUserAssignment assignment = fetchCurrentUserAssignment(assignmentId); + if (assignment == null) { + return Result.error("人员分配记录不存在或无权操作"); + } + try { + tourUserAssignmentService.cancelAssignment(assignment.getId()); + return Result.success(); + } catch (IllegalArgumentException e) { + return Result.error(e.getMessage()); + } + } + + /** + * 移动端取消当前线路,删除本人的报名台账并清空人员分配中的线路快照。 + * + * @param ledgerId 报名台账ID + * @return 取消结果 + */ + @At("/h5/doCancelLine") + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("h5.tour.signup") + public Result h5DoCancelLine(String ledgerId) { + if (StrUtil.isBlank(ledgerId)) { + return Result.error("报名台账参数错误"); + } + TourLedger ledger = tourLedgerService.fetch(Cnd.where(TourLedger::getId, "=", ledgerId) + .and(TourLedger::getJobNo, "=", currentJobNo()) + .and(TourLedger::getDelFlag, "=", false)); + if (ledger == null) { + return Result.error("报名台账不存在或无权操作"); + } + TourMatter matter = tourMatterService.fetch(ledger.getMatterId()); + Result signupTimeResult = checkSignupTime(matter); + if (signupTimeResult != null) { + return signupTimeResult; + } + tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId())); + tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId())); + tourLedgerService.delete(ledger.getId()); + tourUserAssignmentService.clearExistingAssignmentMatterAfterLedgerDelete(ledger.getMatterId(), ledger.getJobNo()); + return Result.success(); + } + @At @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) - public Result pageData(PageForm pageForm, Integer year, String lineName, String lineType, String unionId) { + public Result pageData(PageForm pageForm, Integer year, String lineName, String lineType, String unionId, String travelPeriod) { if (StrUtil.isBlank(pageForm.getPageOrderName())) { pageForm.defaultSortAsc("lineName"); } Cnd cnd = buildQueryCnd(year, lineName, lineType, unionId); + appendTravelPeriodCnd(cnd, travelPeriod); String currentJobNo = currentJobNo(); Sql countSql = Sqls.create(""" @@ -375,9 +439,10 @@ public class TourSignupController { } /** - * 查询移动端选择路线页的出行时间筛选项,按 yyyyMMdd-yyyyMMdd 格式拼接事项出行起止时间并去重。 + * Queries distinct travel periods for PC signup and H5 line selection. + * The value uses yyyyMMdd-yyyyMMdd so the frontend can pass it back as a stable filter key. */ - @At("/h5/travelPeriodOptions") + @At({"/h5/travelPeriodOptions", "/travelPeriodOptions"}) @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) public Result h5TravelPeriodOptions(Integer year, String lineType, Boolean directFamilyUnitLine) { Cnd cnd = buildQueryCnd(year, null, null, null); @@ -490,20 +555,11 @@ public class TourSignupController { .and(TourLedger::getJobNo, "=", staff.getString("jobNo")); ledgerCnd.desc(TourLedger::getCreatedAt); TourLedger ledger = tourLedgerService.fetch(ledgerCnd); - TourMatter matterEntity = tourMatterService.fetch(matter.getString("matterId")); - TourLedger ruleLedger = new TourLedger(); - ruleLedger.setId(ledger == null ? null : ledger.getId()); - ruleLedger.setYear(matterEntity == null ? matter.getInt("year") : matterEntity.getYear()); - ruleLedger.setMatterId(matter.getString("matterId")); - ruleLedger.setLineId(matter.getString("lineId")); - ruleLedger.setLineName(matter.getString("lineName")); - Result ruleResult = checkSignupRule(ruleLedger, matterEntity, ledger); - if (ruleResult != null) { - return ruleResult; - } + // Viewing a line should only load detail data; signup rules are enforced when the user starts or submits signup. List families = Collections.emptyList(); TourLedgerDirectRelative directRelative = null; if (ledger != null) { + fillLedgerContactFallback(ledger, staff); Cnd familyCnd = Cnd.where(TourLedgerFamily::getDelFlag, "=", false) .and(TourLedgerFamily::getLedgerId, "=", ledger.getId()); familyCnd.asc(TourLedgerFamily::getCreatedAt); @@ -793,7 +849,7 @@ public class TourSignupController { return workflowResult; } } - return Result.success().addMsg(approvalRequired ? (update ? "修改已提交,等待审核" : "申请已提交,等待审核") : (update ? "修改成功" : "报名成功")); + return Result.success().addMsg(approvalRequired ? (update ? "修改已提交,等待审核" : "申请已提交,等待审核") : (update ? "修改成功" : "恭喜您已报名成功")); } @At @@ -1199,6 +1255,65 @@ public class TourSignupController { return setting != null && "ASSIGNED_USER".equals(setting.getSignupEligibilityMode()); } + private void appendH5LineActionFlags(List list) { + if (Lang.isEmpty(list)) { + return; + } + int cancelDeadlineDays = tourUserAssignmentService.getCancelDeadlineDays(); + LocalDateTime now = LocalDateTime.now(); + for (NutMap item : list) { + boolean signed = item.getBoolean("signed", false); + boolean assignmentCancelled = item.getBoolean("assignmentCancelled", false); + String assignmentId = item.getString("assignmentId", ""); + String ledgerId = item.getString("ledgerId", ""); + // 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)); + item.put("canCancelLine", signed && StrUtil.isNotBlank(ledgerId) + && isWithinSignupRange(item.getString("signupStartTime", ""), item.getString("signupEndTime", ""), now)); + } + } + + private boolean isWithinLeaveTimeRange(String travelStartTime, int cancelDeadlineDays, LocalDateTime now) { + LocalDateTime startTime = parseDateTimeValue(travelStartTime, false); + if (startTime == null) { + return false; + } + LocalDateTime deadline = startTime.minusDays(cancelDeadlineDays); + return !now.isBefore(deadline) && now.isBefore(startTime); + } + + private boolean isWithinSignupRange(String signupStartTime, String signupEndTime, LocalDateTime now) { + LocalDateTime startTime = parseDateTimeValue(signupStartTime, false); + LocalDateTime endTime = parseDateTimeValue(signupEndTime, true); + if (startTime == null || endTime == null) { + return false; + } + return !now.isBefore(startTime) && !now.isAfter(endTime); + } + + private LocalDateTime parseDateTimeValue(String value, boolean endOfDay) { + String normalized = normalizeDateTime(value, endOfDay); + if (StrUtil.isBlank(normalized)) { + return null; + } + try { + return LocalDateTime.parse(normalized.substring(0, 19), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + } catch (Exception e) { + return null; + } + } + + private TourUserAssignment fetchCurrentUserAssignment(String assignmentId) { + if (StrUtil.isBlank(assignmentId)) { + return null; + } + return tourLedgerService.dao().fetch(TourUserAssignment.class, Cnd.where(TourUserAssignment::getId, "=", assignmentId) + .and(TourUserAssignment::getUserId, "=", SecurityUtil.getUserId()) + .and(TourUserAssignment::getDelFlag, "=", false)); + } + /** * 统一校验疗休养事项报名时间,确保资格名单校验在时间校验通过后再执行。 */ @@ -1497,13 +1612,26 @@ public class TourSignupController { ledger.setJobNo(user == null ? SecurityUtil.getUserLoginname() : defaultIfBlank(user.getLoginname(), SecurityUtil.getUserLoginname())); ledger.setUserName(user == null ? SecurityUtil.getUserUsername() : defaultIfBlank(user.getUsername(), SecurityUtil.getUserUsername())); ledger.setGender(user == null ? ledger.getGender() : defaultIfBlank(user.getSex(), ledger.getGender())); - ledger.setIdCard(user == null ? ledger.getIdCard() : defaultIfBlank(user.getIdCard(), ledger.getIdCard())); + ledger.setIdCard(user == null ? ledger.getIdCard() : defaultIfBlank(ledger.getIdCard(), user.getIdCard())); ledger.setUnitId(user == null ? SecurityUtil.getUnitId() : defaultIfBlank(user.getUnitId(), SecurityUtil.getUnitId())); ledger.setUnitName(user == null ? ledger.getUnitName() : defaultIfBlank(user.getUnitName(), ledger.getUnitName())); ledger.setUnionId(user == null ? SecurityUtil.getUnionId() : defaultIfBlank(user.getUnionId(), SecurityUtil.getUnionId())); ledger.setUnionName(user == null ? ledger.getUnionName() : defaultIfBlank(user.getUnionName(), ledger.getUnionName())); } + private void fillLedgerContactFallback(TourLedger ledger, NutMap staff) { + if (ledger == null || staff == null) { + return; + } + // Signup detail keeps saved ledger contact data first; blank contact fields use the user profile snapshot for display only. + if (StrUtil.isBlank(ledger.getIdCard())) { + ledger.setIdCard(staff.getString("idCard", "")); + } + if (StrUtil.isBlank(ledger.getMobile())) { + ledger.setMobile(staff.getString("mobile", "")); + } + } + private List parseFamilies(String families) { if (StrUtil.isBlank(families)) { return Collections.emptyList(); diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourUnionApprovalController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourUnionApprovalController.java index 4a222e60..69b6fa00 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourUnionApprovalController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourUnionApprovalController.java @@ -7,6 +7,7 @@ import com.budwk.app.base.page.Pagination; import com.budwk.app.base.param.PageForm; import com.budwk.app.base.result.Result; import com.budwk.app.flow.enums.ProcessTaskStateEnum; +import com.budwk.app.sys.views.View_user; import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger; import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerDirectRelative; @@ -171,6 +172,7 @@ public class TourUnionApprovalController { if (ledger == null) { return Result.error("报名记录不存在或无权查看"); } + fillLedgerContactFallback(ledger); ledger.setLineName(getCurrentLineName(ledger)); Cnd familyCnd = Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()) .and(TourLedgerFamily::getDelFlag, "=", false); @@ -465,6 +467,23 @@ public class TourUnionApprovalController { return sql.getInt() > 0 ? tourLedgerService.fetch(id) : null; } + private void fillLedgerContactFallback(TourLedger ledger) { + if (ledger == null || (StrUtil.isNotBlank(ledger.getIdCard()) && StrUtil.isNotBlank(ledger.getMobile()))) { + return; + } + // Approval detail keeps ledger contact data first; blank contact fields use the user profile snapshot for display only. + View_user user = tourLedgerService.dao().fetch(View_user.class, Cnd.where(View_user::getLoginname, "=", ledger.getJobNo())); + if (user == null) { + return; + } + if (StrUtil.isBlank(ledger.getIdCard())) { + ledger.setIdCard(user.getIdCard()); + } + if (StrUtil.isBlank(ledger.getMobile())) { + ledger.setMobile(user.getMobile()); + } + } + private String getCurrentLineName(TourLedger ledger) { if (ledger == null) { return ""; diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourUnionLedgerController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourUnionLedgerController.java index 9fb31d3d..f5a18876 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourUnionLedgerController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourUnionLedgerController.java @@ -416,9 +416,9 @@ public class TourUnionLedgerController { SELECT t.id, t.userName, - t.idCard, + COALESCE(NULLIF(t.idCard, ''), NULLIF(vu.idCard, ''), '') AS idCard, t.unionName, - vu.mobile, + COALESCE(NULLIF(t.mobile, ''), NULLIF(vu.mobile, ''), '') AS mobile, m.travelStartTime, m.travelEndTime, m.estimatedCost, 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 7fde2dbd..bd7f2597 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 @@ -51,6 +51,11 @@ public class TourLedger extends BaseModel implements Serializable { @ColDefine(type = ColType.VARCHAR, width = 30) private String idCard; + @Column + @Comment("手机号") + @ColDefine(type = ColType.VARCHAR, width = 30) + private String mobile; + @Column @Comment("所在单位ID") @ColDefine(type = ColType.VARCHAR, width = 32) 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 18e837a5..52c953a2 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 @@ -272,6 +272,13 @@ public interface TourUserAssignmentService extends BaseService - + diff --git a/src/main/resources/views/platform/zhgh/dayofficework/Tour/branchUserAssignment/index.html b/src/main/resources/views/platform/zhgh/dayofficework/Tour/branchUserAssignment/index.html index 87f87d0d..4d84af2a 100644 --- a/src/main/resources/views/platform/zhgh/dayofficework/Tour/branchUserAssignment/index.html +++ b/src/main/resources/views/platform/zhgh/dayofficework/Tour/branchUserAssignment/index.html @@ -80,7 +80,7 @@ layout("/layouts/platform.html"){ - +