diff --git a/src/main/java/com/budwk/app/flow/controller/FlowTodoCenterController.java b/src/main/java/com/budwk/app/flow/controller/FlowTodoCenterController.java index 14f4f141..e2b9850a 100644 --- a/src/main/java/com/budwk/app/flow/controller/FlowTodoCenterController.java +++ b/src/main/java/com/budwk/app/flow/controller/FlowTodoCenterController.java @@ -213,6 +213,7 @@ public class FlowTodoCenterController { public Result statistics() { // 查询我发起的流程 int startedCount = dao.count(ProcessInstance.class, Cnd.where(ProcessInstance::getOperator, "=", SecurityUtil.getUserId())); + int completedCount = dao.count(ProcessInstance.class, Cnd.where(ProcessInstance::getOperator, "=", SecurityUtil.getUserId()).and(ProcessInstance::getState, "=", 20)); // 查询我的待办任务 Sql todoSql = Sqls.create(""" SELECT @@ -262,7 +263,7 @@ public class FlowTodoCenterController { dao.execute(msgSql); int msgCount = msgSql.getInt(); - return Result.success(Map.of("startedCount", startedCount, "todoCount", todoCount, "doneCount", doneCount, "notifications", msgCount)); + return Result.success(Map.of("startedCount", startedCount, "completedCount", completedCount, "todoCount", todoCount, "doneCount", doneCount, "notifications", msgCount)); } } 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 new file mode 100644 index 00000000..13232d43 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourGroupController.java @@ -0,0 +1,625 @@ +package com.budwk.app.zhgh.dayofficework.tour.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.constant.RoleConstant; +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.base.utils.CommonDownloadUtil; +import com.budwk.app.sys.models.Sys_union; +import com.budwk.app.web.commons.auth.utils.AuthUtil; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService; +import com.budwk.app.zhgh.dayofficework.tour.service.TourMatterService; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.util.CellRangeAddress; +import org.nutz.dao.Cnd; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.dao.util.cri.SqlExpressionGroup; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.util.NutMap; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; + +import javax.servlet.http.HttpServletResponse; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@IocBean +@Ok("json:full") +@At("/platform/tour/group") +public class TourGroupController { + + @Inject + private TourMatterService tourMatterService; + + @Inject + private TourLedgerService tourLedgerService; + + @At("") + @Ok("beetl:/platform/zhgh/dayofficework/Tour/group/index.html") + @SaCheckPermission("tour.group") + public void index() { + } + + @At + @SaCheckPermission("tour.group") + public Result pageData(PageForm pageForm, Integer year, String lineName, String lineType, String unionId) { + if (StrUtil.isBlank(pageForm.getPageOrderName())) { + pageForm.defaultSortAsc("lineName"); + } + Cnd cnd = buildQueryCnd(year, lineName, lineType, unionId); + + Sql countSql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_matter m + INNER JOIN tour_line l ON l.id = m.lineId + LEFT JOIN sys_union u ON u.id = m.unionId + $condition + """); + countSql.setCondition(cnd); + countSql.setCallback(Sqls.callback.integer()); + tourMatterService.dao().execute(countSql); + int count = countSql.getInt(); + + Sql listSql = Sqls.create(""" + SELECT + m.id AS matterId, + m.`year`, + m.matterName, + m.unionId, + COALESCE(u.name, '校工会') AS unionName, + m.lineId, + l.lineName, + l.lineType, + l.directFamilyUnitLine, + m.travelStartTime, + m.travelEndTime, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE '' + END AS travelPeriod, + m.minGroupPeople, + m.maxGroupPeople, + IFNULL(sc.signupCount, 0) AS signupCount, + CASE + WHEN m.maxGroupPeople IS NOT NULL AND m.maxGroupPeople > 0 AND IFNULL(sc.signupCount, 0) > m.maxGroupPeople THEN 'over' + WHEN m.minGroupPeople IS NOT NULL AND m.minGroupPeople > 0 AND IFNULL(sc.signupCount, 0) >= m.minGroupPeople THEN 'formed' + ELSE 'unformed' + END AS groupStatus, + CASE + WHEN m.maxGroupPeople IS NOT NULL AND m.maxGroupPeople > 0 AND IFNULL(sc.signupCount, 0) > m.maxGroupPeople THEN '超员' + WHEN m.minGroupPeople IS NOT NULL AND m.minGroupPeople > 0 AND IFNULL(sc.signupCount, 0) >= m.minGroupPeople THEN '已成团' + ELSE '未成团' + END AS groupStatusName + FROM tour_matter m + INNER JOIN tour_line l ON l.id = m.lineId + LEFT JOIN sys_union u ON u.id = m.unionId + LEFT JOIN ( + SELECT t.matterId, SUM(1 + IFNULL(f.familyCount, 0)) AS signupCount + FROM tour_ledger t + LEFT JOIN ( + SELECT ledgerId, COUNT(1) AS familyCount + FROM tour_ledger_family + WHERE delFlag = 0 + GROUP BY ledgerId + ) f ON f.ledgerId = t.id + WHERE t.delFlag = 0 + AND t.matterId IS NOT NULL + AND t.matterId <> '' + GROUP BY t.matterId + ) sc ON sc.matterId = m.id + $condition + ORDER BY $orderColumn $orderBy, m.`year` DESC, m.travelStartTime ASC, l.lineName ASC, m.createdAt DESC + """); + listSql.setCondition(cnd); + listSql.setVar("orderColumn", getOrderColumn(pageForm.getPageOrderName())); + listSql.setVar("orderBy", "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc"); + listSql.setPager(tourMatterService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize())); + listSql.setCallback(Sqls.callback.maps()); + tourMatterService.dao().execute(listSql); + + Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, listSql.getList(NutMap.class)); + return Result.success(pagination); + } + + @At + @SaCheckPermission("tour.group") + public Result unionOptions() { + Cnd cnd = Cnd.NEW(); + cnd.asc("unionCode"); + cnd.asc("name"); + return Result.success(tourMatterService.dao().query(Sys_union.class, cnd)); + } + + @At + @SaCheckPermission("tour.group") + public Result lineTypeOptions(Integer year) { + Cnd cnd = buildQueryCnd(year, null, null, null); + Sql sql = Sqls.create(""" + SELECT DISTINCT l.lineType + FROM tour_matter m + INNER JOIN tour_line l ON l.id = m.lineId + LEFT JOIN sys_union u ON u.id = m.unionId + $condition + ORDER BY l.lineType ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourMatterService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission("tour.group") + public Result signupPageData(PageForm pageForm, String matterId, String keyword, String unionId) { + if (StrUtil.isBlank(matterId)) { + return Result.error("参数错误"); + } + if (StrUtil.isBlank(pageForm.getPageOrderName())) { + pageForm.defaultSortDesc("signupTime"); + } + Cnd cnd = buildSignupQueryCnd(matterId, keyword, unionId); + Sql countSql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_ledger t + INNER JOIN tour_matter m ON m.id = t.matterId + INNER JOIN tour_line l ON l.id = m.lineId + $condition + """); + countSql.setCondition(cnd); + countSql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(countSql); + int count = countSql.getInt(); + + Sql listSql = Sqls.create(""" + SELECT + t.id, + t.jobNo, + t.userName, + '' AS mobile, + t.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 + $condition + ORDER BY $orderColumn $orderBy, t.signupTime DESC, t.createdAt DESC + """); + listSql.setCondition(cnd); + listSql.setVar("orderColumn", getSignupOrderColumn(pageForm.getPageOrderName())); + listSql.setVar("orderBy", "ascending".equals(pageForm.getPageOrderBy()) ? "asc" : "desc"); + listSql.setPager(tourLedgerService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize())); + listSql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(listSql); + + List list = listSql.getList(NutMap.class); + fillSignupMobile(list); + Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list); + return Result.success(pagination); + } + + @At + @Ok("void") + @SaCheckPermission("tour.group") + public void exportParticipants(String matterId, HttpServletResponse response) { + if (StrUtil.isBlank(matterId)) { + return; + } + NutMap matter = fetchExportMatter(matterId); + if (matter == null || matter.isEmpty()) { + return; + } + List list = queryExportParticipants(matterId); + Workbook workbook = buildParticipantsWorkbook(matter, list); + String lineName = StrUtil.blankToDefault(matter.getString("lineName", ""), "线路"); + CommonDownloadUtil.download(lineName + "参加人员名单.xls", workbook, response); + } + + private Cnd buildSignupQueryCnd(String matterId, String keyword, String unionId) { + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.and("t.matterId", "=", matterId); + cnd.and("m.delFlag", "=", false); + cnd.and("m.enabled", "=", true); + cnd.and("m.lineId", "IS NOT", null); + cnd.and("m.lineId", "<>", ""); + cnd.and("l.delFlag", "=", false); + cnd.and("l.enabled", "=", true); + if (StrUtil.isNotBlank(keyword)) { + SqlExpressionGroup keywordGroup = new SqlExpressionGroup(); + keywordGroup.orLike("t.userName", keyword.trim()); + keywordGroup.orLike("t.jobNo", keyword.trim()); + cnd.and(keywordGroup); + } + cnd.andEX("t.unionId", "=", unionId); + applyMatterDataScope(cnd); + return cnd; + } + + private NutMap fetchExportMatter(String matterId) { + Cnd cnd = Cnd.NEW(); + cnd.and("m.delFlag", "=", false); + cnd.and("m.enabled", "=", true); + cnd.and("m.id", "=", matterId); + cnd.and("m.lineId", "IS NOT", null); + cnd.and("m.lineId", "<>", ""); + cnd.and("l.delFlag", "=", false); + cnd.and("l.enabled", "=", true); + applyMatterDataScope(cnd); + + Sql sql = Sqls.create(""" + SELECT + m.id AS matterId, + m.`year`, + m.unionId, + COALESCE(u.name, '校工会') AS unionName, + l.lineName, + l.lineType, + m.travelStartTime, + m.travelEndTime, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE '' + END AS travelPeriod + FROM tour_matter m + INNER JOIN tour_line l ON l.id = m.lineId + LEFT JOIN sys_union u ON u.id = m.unionId + $condition + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.map()); + tourMatterService.dao().execute(sql); + return sql.getObject(NutMap.class); + } + + private List queryExportParticipants(String matterId) { + Cnd cnd = buildSignupQueryCnd(matterId, null, null); + Sql sql = Sqls.create(""" + SELECT + t.id AS ledgerId, + t.jobNo, + t.userName, + t.idCard, + vu.mobile, + t.unionName, + l.lineName, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE '' + END AS travelPeriod, + 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 t.signupTime DESC, t.createdAt DESC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + List list = sql.getList(NutMap.class); + fillExportFamilies(list); + return list; + } + + private void fillExportFamilies(List list) { + if (list == null || list.isEmpty()) { + return; + } + List ledgerIds = new ArrayList<>(); + for (NutMap item : list) { + String ledgerId = item.getString("ledgerId", ""); + if (StrUtil.isNotBlank(ledgerId)) { + ledgerIds.add(ledgerId); + } + } + if (ledgerIds.isEmpty()) { + return; + } + Sql sql = Sqls.create(""" + SELECT + ledgerId, + familyName, + relationship, + idCard + FROM tour_ledger_family + WHERE delFlag = 0 + AND ledgerId IN (@ledgerIds) + ORDER BY createdAt ASC + """); + sql.setParam("ledgerIds", ledgerIds.toArray(new String[0])); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + + Map> familyMap = new HashMap<>(); + for (NutMap family : sql.getList(NutMap.class)) { + familyMap.computeIfAbsent(family.getString("ledgerId", ""), key -> new ArrayList<>()).add(family); + } + for (NutMap item : list) { + item.put("families", familyMap.getOrDefault(item.getString("ledgerId", ""), List.of())); + } + } + + private Workbook buildParticipantsWorkbook(NutMap matter, List list) { + Workbook workbook = new HSSFWorkbook(); + Sheet sheet = workbook.createSheet("参加人员名单"); + sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 10)); + sheet.addMergedRegion(new CellRangeAddress(1, 1, 0, 10)); + + double[] widths = {6, 14, 12, 22, 16, 24, 34, 26, 12, 12, 16}; + for (int i = 0; i < widths.length; i++) { + sheet.setColumnWidth(i, (int) (widths[i] * 256)); + } + + CellStyle titleStyle = createStyle(workbook, "黑体", (short) 16, true, HorizontalAlignment.CENTER, false, false); + CellStyle unionStyle = createStyle(workbook, "宋体", (short) 12, true, HorizontalAlignment.LEFT, false, false); + CellStyle headerStyle = createStyle(workbook, "宋体", (short) 11, true, HorizontalAlignment.CENTER, true, true); + CellStyle bodyStyle = createStyle(workbook, "宋体", (short) 11, false, HorizontalAlignment.CENTER, true, true); + + Row titleRow = sheet.createRow(0); + titleRow.setHeightInPoints(30); + setCell(titleRow, 0, StrUtil.blankToDefault(matter.getString("lineName", ""), ""), titleStyle); + fillMergedCells(titleRow, 1, 10, titleStyle); + + Row unionRow = sheet.createRow(1); + unionRow.setHeightInPoints(26); + setCell(unionRow, 0, "分工会:" + StrUtil.blankToDefault(matter.getString("unionName", ""), ""), unionStyle); + fillMergedCells(unionRow, 1, 10, unionStyle); + + String[] headers = {"序号", "工号", "姓名", "身份证号码", "电话号码", "所属分工会", "所选线路名称", "疗休养时间", "亲属关系", "姓名", "备注"}; + Row headerRow = sheet.createRow(2); + headerRow.setHeightInPoints(35); + for (int i = 0; i < headers.length; i++) { + setCell(headerRow, i, headers[i], headerStyle); + } + + int rowIndex = 3; + int seq = 1; + for (NutMap item : list == null ? List.of() : list) { + Row staffRow = sheet.createRow(rowIndex++); + staffRow.setHeightInPoints(24); + setParticipantRow(staffRow, seq++, item.getString("jobNo", ""), item.getString("userName", ""), + item.getString("idCard", ""), item.getString("mobile", ""), item.getString("unionName", ""), + item.getString("lineName", matter.getString("lineName", "")), item.getString("travelPeriod", matter.getString("travelPeriod", "")), + "", item.getString("userName", ""), bodyStyle); + + Object familiesObj = item.get("families"); + if (familiesObj instanceof List families) { + for (Object obj : families) { + if (!(obj instanceof NutMap family)) { + continue; + } + Row familyRow = sheet.createRow(rowIndex++); + familyRow.setHeightInPoints(24); + setParticipantRow(familyRow, seq++, "", family.getString("familyName", ""), + family.getString("idCard", ""), "", "", + item.getString("lineName", matter.getString("lineName", "")), + item.getString("travelPeriod", matter.getString("travelPeriod", "")), + family.getString("relationship", ""), item.getString("userName", ""), bodyStyle); + } + } + } + + int minRows = Math.max(rowIndex, 23); + while (rowIndex < minRows) { + Row row = sheet.createRow(rowIndex++); + row.setHeightInPoints(24); + for (int i = 0; i < headers.length; i++) { + setCell(row, i, "", bodyStyle); + } + } + return workbook; + } + + private void setParticipantRow(Row row, int seq, String jobNo, String userName, String idCard, String mobile, + String unionName, String lineName, String travelPeriod, String relationship, + String participantName, CellStyle style) { + setCell(row, 0, String.valueOf(seq), style); + setCell(row, 1, jobNo, style); + setCell(row, 2, userName, style); + setCell(row, 3, idCard, style); + setCell(row, 4, mobile, style); + setCell(row, 5, unionName, style); + setCell(row, 6, lineName, style); + setCell(row, 7, travelPeriod, style); + setCell(row, 8, relationship, style); + setCell(row, 9, participantName, style); + setCell(row, 10, "", style); + } + + private CellStyle createStyle(Workbook workbook, String fontName, short fontSize, boolean bold, HorizontalAlignment alignment, boolean wrap, boolean border) { + Font font = workbook.createFont(); + font.setFontName(fontName); + font.setFontHeightInPoints(fontSize); + font.setBold(bold); + + CellStyle style = workbook.createCellStyle(); + style.setFont(font); + style.setAlignment(alignment); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setWrapText(wrap); + if (border) { + style.setBorderLeft(BorderStyle.THIN); + style.setBorderTop(BorderStyle.THIN); + style.setBorderRight(BorderStyle.THIN); + style.setBorderBottom(BorderStyle.THIN); + } + return style; + } + + private void setCell(Row row, int col, String value, CellStyle style) { + Cell cell = row.createCell(col); + cell.setCellValue(StrUtil.blankToDefault(value, "")); + cell.setCellStyle(style); + } + + private void fillMergedCells(Row row, int startCol, int endCol, CellStyle style) { + for (int i = startCol; i <= endCol; i++) { + setCell(row, i, "", style); + } + } + + 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); + cnd.and("m.enabled", "=", true); + cnd.and("m.lineId", "IS NOT", null); + cnd.and("m.lineId", "<>", ""); + cnd.and("l.delFlag", "=", false); + cnd.and("l.enabled", "=", true); + cnd.andEX("m.`year`", "=", year == null ? LocalDate.now().getYear() : year); + cnd.and(Cnd.likeEX("l.lineName", lineName)); + cnd.andEX("l.lineType", "=", lineType); + cnd.andEX("m.unionId", "=", unionId); + applyMatterDataScope(cnd); + return cnd; + } + + /** + * 线路成团按事项归属控制数据范围:校工会查看全部;分工会查看本工会事项;其它角色仅查看自己创建的事项。 + */ + private void applyMatterDataScope(Cnd cnd) { + if (hasSchoolUnionScope()) { + return; + } + if (hasBranchUnionScope()) { + cnd.and("m.unionId", "=", SecurityUtil.getUnionId()); + return; + } + SqlExpressionGroup creatorGroup = new SqlExpressionGroup(); + creatorGroup.or("m.creatorUserId", "=", SecurityUtil.getUserId()); + creatorGroup.or("m.createdBy", "=", SecurityUtil.getUserId()); + cnd.and(creatorGroup); + } + + private boolean hasSchoolUnionScope() { + return AuthUtil.hasRole(RoleConstant.SYSADMIN.name()) + || AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_ADMIN.name()) + || AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_CHAIRMAN.name()) + || AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_VICE_CHAIRMAN.name()) + || AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_WELFARE_ADMIN.name()) + || AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_MEMBER_ADMIN.name()) + || AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_TC_ADMIN.name()) + || AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_WC_ADMIN.name()) + || AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_DC_ADMIN.name()) + || AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_PROPOSAL_ADMIN.name()) + || AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_CLUB_ADMIN.name()) + || AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_ACTIVITY_ADMIN.name()); + } + + private boolean hasBranchUnionScope() { + return AuthUtil.hasRole(RoleConstant.BRANCH_UNION_ADMIN.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_VICE_CHAIRMAN.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_OPERATOR.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_GROUP_LEADER.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_ARTICLE_WRITER.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_WY.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_ZUZHI_WY.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_XUANCHUAN_WY.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_NVGONG_WY.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_QINGNIAN_WY.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_WENTI_WY.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_SHENGGHUO_WY.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_TIAOJIE_WY.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_WENTI_SPORTS.name()); + } + + private String getOrderColumn(String orderName) { + if ("lineName".equals(orderName)) { + return "l.lineName"; + } + if ("travelPeriod".equals(orderName)) { + return "m.travelStartTime"; + } + if ("matterName".equals(orderName)) { + return "m.matterName"; + } + if ("unionName".equals(orderName)) { + return "u.name"; + } + if ("lineType".equals(orderName)) { + return "l.lineType"; + } + if ("signupCount".equals(orderName)) { + return "signupCount"; + } + if ("minGroupPeople".equals(orderName)) { + return "m.minGroupPeople"; + } + if ("maxGroupPeople".equals(orderName)) { + return "m.maxGroupPeople"; + } + if ("groupStatusName".equals(orderName)) { + return "groupStatus"; + } + return "l.lineName"; + } + + private String getSignupOrderColumn(String orderName) { + if ("jobNo".equals(orderName)) { + return "t.jobNo"; + } + if ("userName".equals(orderName)) { + return "t.userName"; + } + if ("idCard".equals(orderName)) { + return "t.idCard"; + } + if ("unionName".equals(orderName)) { + return "t.unionName"; + } + return "t.signupTime"; + } +} 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 new file mode 100644 index 00000000..7bbbbdf4 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLedgerController.java @@ -0,0 +1,959 @@ +package com.budwk.app.zhgh.dayofficework.tour.controller; + +import cn.afterturn.easypoi.excel.ExcelExportUtil; +import cn.afterturn.easypoi.excel.ExcelImportUtil; +import cn.afterturn.easypoi.excel.entity.ExportParams; +import cn.afterturn.easypoi.excel.entity.ImportParams; +import cn.afterturn.easypoi.excel.entity.enmus.ExcelType; +import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity; +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.annotation.SLog; +import com.budwk.app.base.model.ExcelImportRes; +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.base.utils.CommonDownloadUtil; +import com.budwk.app.flow.enums.ProcessInstanceStateEnum; +import com.budwk.app.sys.models.Sys_union; +import com.budwk.app.sys.views.View_user; +import com.budwk.app.zhgh.dayofficework.tour.mode.TourLedgerImportExcelMode; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerDirectRelative; +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.TourTravelAgency; +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; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.FillPatternType; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.IndexedColors; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.util.CellRangeAddress; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.Chain; +import org.nutz.dao.Cnd; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.dao.util.cri.SqlExpressionGroup; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.json.Json; +import org.nutz.lang.util.NutMap; +import org.nutz.mvc.annotation.AdaptBy; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; +import org.nutz.mvc.annotation.Param; +import org.nutz.mvc.upload.TempFile; +import org.nutz.mvc.upload.UploadAdaptor; + +import javax.servlet.http.HttpServletResponse; +import java.io.BufferedOutputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +@IocBean +@Ok("json:full") +@At("/platform/tour/ledger") +public class TourLedgerController { + + @Inject + private TourLedgerService tourLedgerService; + + @Inject + private TourLedgerFamilyService tourLedgerFamilyService; + + @Inject + private TourLedgerDirectRelativeService tourLedgerDirectRelativeService; + + @At("") + @Ok("beetl:/platform/zhgh/dayofficework/Tour/ledger/index.html") + @SaCheckPermission("tour.ledger") + public void index() { + } + + @At + @SaCheckPermission("tour.ledger") + public Result pageData(PageForm pageForm, Integer startYear, Integer endYear, String keyword, String unionId, String lineId, String travelPeriod, String lineType, Boolean directFamilyOnly, Boolean overCostOnly) { + Cnd cnd = buildQueryCnd(startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType, directFamilyOnly, overCostOnly); + + Sql countSql = Sqls.create(""" + SELECT COUNT(DISTINCT t.id) + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + 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 + $condition + """); + countSql.setCondition(cnd); + countSql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(countSql); + int count = countSql.getInt(); + + Sql listSql = Sqls.create(""" + SELECT + t.*, + ins.id AS instanceId, + ins.state AS instanceState, + ins.processDefineId AS instanceProcessDefineId, + IF(IFNULL(l.directFamilyUnitLine, 0) = 1 OR dr.id IS NOT NULL, 1, 0) AS directFamilyUnitLine, + dr.id AS directRelativeId, + COALESCE(NULLIF(l.lineName, ''), t.lineName) AS currentLineName, + IFNULL(f.familyCount, 0) AS familyCount, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE tp.travelPeriod + END AS travelPeriod + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id + LEFT JOIN ( + SELECT ledgerId, COUNT(1) AS familyCount + FROM tour_ledger_family + WHERE delFlag = 0 + 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_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 ( + SELECT + `year`, + lineId, + GROUP_CONCAT( + DISTINCT CASE + WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> '' + THEN CONCAT(travelStartTime, ' 至 ', travelEndTime) + ELSE NULL + END + ORDER BY travelStartTime ASC + SEPARATOR ';' + ) AS travelPeriod + FROM tour_matter + WHERE delFlag = 0 + AND lineId IS NOT NULL + AND lineId <> '' + GROUP BY `year`, lineId + ) tp ON (t.matterId IS NULL OR t.matterId = '') AND tp.`year` = t.`year` AND tp.lineId = t.lineId + $condition + GROUP BY t.id + ORDER BY $orderColumn $orderBy, t.`year` DESC, t.signupTime DESC, t.createdAt DESC + """); + listSql.setCondition(cnd); + 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); + + var list = listSql.getList(NutMap.class); + list.forEach(item -> item.put("lineName", item.getString("currentLineName"))); + Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list); + return Result.success(pagination); + } + + @At + @SaCheckPermission("tour.ledger") + public Result detail(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("参数错误"); + } + TourLedger ledger = tourLedgerService.fetch(id); + if (ledger == null) { + return Result.error("台账记录不存在"); + } + ledger.setLineName(getCurrentLineName(ledger)); + Cnd familyCnd = Cnd.NEW(); + familyCnd.and(TourLedgerFamily::getLedgerId, "=", id); + familyCnd.and(TourLedgerFamily::getDelFlag, "=", false); + familyCnd.asc(TourLedgerFamily::getCreatedAt); + TourLedgerDirectRelative directRelative = tourLedgerDirectRelativeService.fetch( + Cnd.where(TourLedgerDirectRelative::getDelFlag, "=", false) + .and(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId())); + return Result.success(NutMap.NEW() + .addv("ledger", ledger) + .addv("travelPeriod", getTravelPeriod(ledger)) + .addv("families", tourLedgerFamilyService.query(familyCnd)) + .addv("directRelative", directRelative) + .addv("fillBedInfo", isFillBedInfo(ledger)) + .addv("directFamilyUnitLine", isDirectFamilyUnitLine(ledger))); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.ledger") + @SLog(type = "tour", tag = "疗休养台账", msg = "删除疗休养台账") + public Result doDelete(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("参数错误"); + } + TourLedger ledger = tourLedgerService.fetch(id); + if (ledger == null) { + return Result.error("台账记录不存在"); + } + tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", id)); + tourLedgerService.delete(id); + return Result.success(); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.ledger") + @SLog(type = "tour", tag = "疗休养台账", msg = "设置参加人员") + public Result setParticipants(@Param("ids") String ids) { + List idList = StrUtil.isBlank(ids) ? Collections.emptyList() : Json.fromJsonAsList(String.class, ids); + if (idList.isEmpty()) { + return Result.error("请选择要设置的参加人员"); + } + tourLedgerService.dao().update(TourLedger.class, Chain.make("joined", true), Cnd.where("id", "in", idList)); + return Result.success(); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.ledger") + @SLog(type = "tour", tag = "疗休养台账", msg = "参加人员导入") + @AdaptBy(type = UploadAdaptor.class, args = {"ioc:fileUpload"}) + public Result importParticipants(@Param("file") TempFile file) { + if (file == null) { + return Result.error("请选择导入文件"); + } + List importRows = ExcelImportUtil.importExcel(file.getFile(), TourLedgerImportExcelMode.class, new ImportParams()); + ExcelImportRes excelImportRes = new ExcelImportRes<>(); + excelImportRes.setTotalRecords(importRows.size()); + + for (int i = 0; i < importRows.size(); i++) { + TourLedgerImportExcelMode row = importRows.get(i); + if (!validateImportRow(row, i)) { + continue; + } + Boolean joined = parseJoined(row.getJoined()); + if (joined == null) { + row.setErrInfo("是否参加只能填写1、0、是、否、true、false、已参加、未参加", i); + continue; + } + TourLine line = fetchImportLine(row); + if (line == null) { + row.setErrInfo("未找到唯一匹配的线路", i); + continue; + } + List ledgers = queryImportLedgerCandidates(row); + if (ledgers.isEmpty()) { + Result createResult = insertImportLedger(row, line, joined); + if (createResult.isSuccess()) { + continue; + } + row.setErrInfo(createResult.getMsg(), i); + continue; + } + TourLedger ledger = resolveImportLedger(row, ledgers, line); + if (ledger == null) { + row.setErrInfo("匹配到多条台账记录,报名时间或线路未能唯一匹配,请核对年度、工号、报名时间、线路名称", i); + continue; + } + Chain chain = Chain.make("joined", joined) + .add("lineId", line.getId()) + .add("lineName", line.getLineName()) + .add("lineType", line.getLineType()); + String signupTime = normalizeSignupTime(row.getSignupTime()); + if (StrUtil.isNotBlank(signupTime)) { + chain.add("signupTime", signupTime); + } + tourLedgerService.dao().update(TourLedger.class, chain, Cnd.where("id", "=", ledger.getId())); + } + + excelImportRes.setErrorDetails(importRows.stream().filter(s -> StrUtil.isNotBlank(s.getErrMsg())).collect(Collectors.toList())); + excelImportRes.setFailedCount(excelImportRes.getErrorDetails().size()); + excelImportRes.setSuccessCount(Math.max(importRows.size() - excelImportRes.getFailedCount(), 0)); + return Result.success(excelImportRes); + } + + @At + @SaCheckPermission("tour.ledger") + @Ok("void") + public void downloadTemplate(HttpServletResponse response) { + List entities = new ArrayList<>(); + entities.add(new ExcelExportEntity("年度", "year", 12)); + entities.add(new ExcelExportEntity("工号", "jobNo", 18)); + entities.add(new ExcelExportEntity("姓名", "userName", 18)); + entities.add(new ExcelExportEntity("报名时间", "signupTime", 24)); + entities.add(new ExcelExportEntity("线路名称", "lineName", 30)); + entities.add(new ExcelExportEntity("线路类型", "lineType", 18)); + entities.add(new ExcelExportEntity("是否参加", "joined", 14)); + + ExportParams exportParams = new ExportParams(); + exportParams.setSheetName("tour_ledger"); + exportParams.setType(ExcelType.XSSF); + Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entities, Collections.emptyList()); + CommonDownloadUtil.download("参加人员导入模板.xlsx", workbook, response); + } + + @At + @SaCheckPermission("tour.ledger") + @Ok("void") + public void exportUnionSignupZip(Integer startYear, Integer endYear, String keyword, String unionId, String lineId, + String travelPeriod, String lineType, Boolean directFamilyOnly, Boolean overCostOnly, + HttpServletResponse response) throws Exception { + List rows = queryUnionSignupRows(startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType, + directFamilyOnly, overCostOnly); + Map> unionRows = groupRowsByUnion(rows); + + String zipName = URLEncoder.encode("疗休养分工会报名汇总表.zip", StandardCharsets.UTF_8.toString()); + response.setContentType("application/zip"); + response.setHeader("Content-Disposition", "attachment;filename=" + zipName + ";filename*=UTF-8''" + zipName); + try (ZipOutputStream zipOutputStream = new ZipOutputStream(new BufferedOutputStream(response.getOutputStream()))) { + if (unionRows.isEmpty()) { + zipOutputStream.putNextEntry(new ZipEntry("无数据报名汇总表.xls")); + try (Workbook workbook = buildUnionSignupWorkbook("无数据", Collections.emptyList())) { + workbook.write(zipOutputStream); + } + zipOutputStream.closeEntry(); + } else { + for (Map.Entry> entry : unionRows.entrySet()) { + String unionName = entry.getValue().isEmpty() ? entry.getKey() : entry.getValue().get(0).getString("unionName", entry.getKey()); + String fileName = safeFileName(unionName + "报名汇总表.xls"); + zipOutputStream.putNextEntry(new ZipEntry(fileName)); + try (Workbook workbook = buildUnionSignupWorkbook(unionName, entry.getValue())) { + workbook.write(zipOutputStream); + } + zipOutputStream.closeEntry(); + } + } + zipOutputStream.flush(); + } + } + + private List queryUnionSignupRows(Integer startYear, Integer endYear, String keyword, String unionId, String lineId, + String travelPeriod, String lineType, Boolean directFamilyOnly, Boolean overCostOnly) { + Cnd cnd = buildQueryCnd(startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType, directFamilyOnly, overCostOnly); + Sql sql = Sqls.create(""" + SELECT + t.id, + t.jobNo, + t.userName, + t.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(l.lineName, ''), t.lineName, '') AS lineName, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE tp.travelPeriod + END AS travelPeriod + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + 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 sys_union su ON su.id = t.unionId + LEFT JOIN ( + SELECT + `year`, + lineId, + GROUP_CONCAT( + DISTINCT CASE + WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> '' + THEN CONCAT(travelStartTime, ' 至 ', travelEndTime) + ELSE NULL + END + ORDER BY travelStartTime ASC + SEPARATOR ';' + ) AS travelPeriod + FROM tour_matter + WHERE delFlag = 0 + AND lineId IS NOT NULL + AND lineId <> '' + GROUP BY `year`, lineId + ) tp ON (t.matterId IS NULL OR t.matterId = '') AND tp.`year` = t.`year` AND tp.lineId = t.lineId + $condition + GROUP BY t.id + ORDER BY unionCode ASC, unionName ASC, t.signupTime DESC, t.createdAt DESC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return sql.getList(NutMap.class); + } + + private Map> groupRowsByUnion(List rows) { + Map> unionRows = new LinkedHashMap<>(); + for (NutMap row : rows) { + String unionName = StrUtil.blankToDefault(row.getString("unionName"), "未分工会"); + String unionCode = StrUtil.blankToDefault(row.getString("unionCode"), ""); + String key = StrUtil.isBlank(unionCode) ? unionName : unionCode + "_" + unionName; + unionRows.computeIfAbsent(key, item -> new ArrayList<>()).add(row); + } + return unionRows; + } + + private Workbook buildUnionSignupWorkbook(String unionName, List rows) { + HSSFWorkbook workbook = new HSSFWorkbook(); + Sheet sheet = workbook.createSheet("报名汇总表"); + sheet.setColumnWidth(0, 8 * 256); + sheet.setColumnWidth(1, 14 * 256); + sheet.setColumnWidth(2, 14 * 256); + sheet.setColumnWidth(3, 24 * 256); + sheet.setColumnWidth(4, 16 * 256); + sheet.setColumnWidth(5, 24 * 256); + sheet.setColumnWidth(6, 36 * 256); + sheet.setColumnWidth(7, 26 * 256); + sheet.setColumnWidth(8, 18 * 256); + + CellStyle titleStyle = createUnionSignupTitleStyle(workbook); + CellStyle headerStyle = createUnionSignupHeaderStyle(workbook); + CellStyle bodyStyle = createUnionSignupBodyStyle(workbook); + + Row titleRow = sheet.createRow(0); + titleRow.setHeightInPoints(28); + Cell titleCell = titleRow.createCell(0); + titleCell.setCellValue(StrUtil.blankToDefault(unionName, "未分工会") + "报名汇总表"); + titleCell.setCellStyle(titleStyle); + sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 8)); + for (int i = 1; i <= 8; i++) { + titleRow.createCell(i).setCellStyle(titleStyle); + } + + Row headerRow = sheet.createRow(1); + headerRow.setHeightInPoints(24); + String[] headers = {"序号", "工号", "姓名", "身份证号码", "电话号码", "所属分工会", "所选线路名称", "疗休养时间", "备注"}; + for (int i = 0; i < headers.length; i++) { + setCell(headerRow, i, headers[i], headerStyle); + } + + int rowIndex = 2; + int seq = 1; + for (NutMap row : rows) { + Row dataRow = sheet.createRow(rowIndex++); + dataRow.setHeightInPoints(22); + setCell(dataRow, 0, String.valueOf(seq++), bodyStyle); + setCell(dataRow, 1, row.getString("jobNo", ""), bodyStyle); + setCell(dataRow, 2, row.getString("userName", ""), bodyStyle); + setCell(dataRow, 3, row.getString("idCard", ""), bodyStyle); + setCell(dataRow, 4, row.getString("mobile", ""), bodyStyle); + setCell(dataRow, 5, row.getString("unionName", ""), bodyStyle); + setCell(dataRow, 6, row.getString("lineName", ""), bodyStyle); + setCell(dataRow, 7, row.getString("travelPeriod", ""), bodyStyle); + setCell(dataRow, 8, "", bodyStyle); + } + return workbook; + } + + private CellStyle createUnionSignupTitleStyle(Workbook workbook) { + CellStyle style = workbook.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setBorderTop(BorderStyle.THIN); + style.setBorderRight(BorderStyle.THIN); + style.setBorderBottom(BorderStyle.THIN); + style.setBorderLeft(BorderStyle.THIN); + Font font = workbook.createFont(); + font.setBold(true); + font.setFontHeightInPoints((short) 16); + style.setFont(font); + return style; + } + + private CellStyle createUnionSignupHeaderStyle(Workbook workbook) { + CellStyle style = createUnionSignupBodyStyle(workbook); + style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex()); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + Font font = workbook.createFont(); + font.setBold(true); + style.setFont(font); + return style; + } + + private CellStyle createUnionSignupBodyStyle(Workbook workbook) { + CellStyle style = workbook.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setWrapText(true); + style.setBorderTop(BorderStyle.THIN); + style.setBorderRight(BorderStyle.THIN); + style.setBorderBottom(BorderStyle.THIN); + style.setBorderLeft(BorderStyle.THIN); + return style; + } + + private void setCell(Row row, int index, String value, CellStyle style) { + Cell cell = row.createCell(index); + cell.setCellValue(StrUtil.blankToDefault(value, "")); + cell.setCellStyle(style); + } + + private String safeFileName(String fileName) { + String safeName = StrUtil.blankToDefault(fileName, "报名汇总表.xls") + .replaceAll("[\\\\/:*?\"<>|]", "、"); + return safeName.length() > 120 ? safeName.substring(0, 120) : safeName; + } + + private boolean validateImportRow(TourLedgerImportExcelMode row, int index) { + if (row.getYear() == null) { + row.setErrInfo("年度为空", index); + return false; + } + if (StrUtil.isBlank(row.getJobNo())) { + row.setErrInfo("工号为空", index); + return false; + } + if (StrUtil.isBlank(row.getLineName())) { + row.setErrInfo("线路名称为空", index); + return false; + } + if (StrUtil.isBlank(row.getLineType())) { + row.setErrInfo("线路类型为空", index); + return false; + } + if (StrUtil.isBlank(row.getJoined())) { + row.setErrInfo("是否参加为空", index); + return false; + } + return true; + } + + private TourLine fetchImportLine(TourLedgerImportExcelMode row) { + Cnd cnd = Cnd.where(TourLine::getDelFlag, "=", false) + .and(TourLine::getYear, "=", row.getYear()) + .and(TourLine::getLineName, "=", row.getLineName().trim()) + .and(TourLine::getLineType, "=", row.getLineType().trim()); + List lines = tourLedgerService.dao().query(TourLine.class, cnd); + return lines.size() == 1 ? lines.get(0) : null; + } + + private List queryImportLedgerCandidates(TourLedgerImportExcelMode row) { + Cnd cnd = Cnd.where(TourLedger::getDelFlag, "=", false) + .and(TourLedger::getYear, "=", row.getYear()) + .and(TourLedger::getJobNo, "=", row.getJobNo().trim()); + return tourLedgerService.query(cnd); + } + + private TourLedger resolveImportLedger(TourLedgerImportExcelMode row, List ledgers, TourLine line) { + if (ledgers.size() == 1) { + return ledgers.get(0); + } + List matched = ledgers; + if (row.getSignupTime() != null) { + String importSignupTime = normalizeSignupTime(row.getSignupTime()); + matched = matched.stream() + .filter(item -> signupTimeEquals(importSignupTime, item.getSignupTime())) + .collect(Collectors.toList()); + if (matched.size() == 1) { + return matched.get(0); + } + } + matched = ledgers.stream() + .filter(item -> ledgerLineEquals(item, line)) + .collect(Collectors.toList()); + return matched.size() == 1 ? matched.get(0) : null; + } + + private boolean ledgerLineEquals(TourLedger ledger, TourLine line) { + if (ledger == null || line == null) { + return false; + } + if (StrUtil.isNotBlank(ledger.getLineId()) && ledger.getLineId().equals(line.getId())) { + return true; + } + return StrUtil.equals(ledger.getLineName(), line.getLineName()) + && StrUtil.equals(ledger.getLineType(), line.getLineType()); + } + + private Result insertImportLedger(TourLedgerImportExcelMode row, TourLine line, Boolean joined) { + View_user user = fetchImportUser(row.getJobNo()); + if (user == null) { + return Result.error("系统用户中未找到该工号"); + } + TourLedger ledger = new TourLedger(); + ledger.setYear(row.getYear()); + ledger.setJobNo(StrUtil.blankToDefault(user.getLoginname(), row.getJobNo().trim())); + ledger.setUserName(StrUtil.blankToDefault(user.getUsername(), row.getUserName())); + ledger.setGender(user.getSex()); + ledger.setIdCard(user.getIdCard()); + ledger.setUnitId(user.getUnitId()); + ledger.setUnitName(user.getUnitName()); + ledger.setUnionId(user.getUnionId()); + ledger.setUnionName(user.getUnionName()); + ledger.setSignupTime(normalizeSignupTime(row.getSignupTime())); + ledger.setLineId(line.getId()); + ledger.setLineName(line.getLineName()); + ledger.setLineType(line.getLineType()); + ledger.setTravelAgencyId(line.getTravelAgencyId()); + ledger.setTravelAgencyName(getTravelAgencyName(line.getTravelAgencyId())); + ledger.setHasFamily(false); + ledger.setJoined(joined); + ledger.setReimbursed(false); + ledger.setOverCostReimbursed(false); + tourLedgerService.insert(ledger); + return Result.success(); + } + + private View_user fetchImportUser(String jobNo) { + if (StrUtil.isBlank(jobNo)) { + return null; + } + return tourLedgerService.dao().fetch(View_user.class, Cnd.where(View_user::getLoginname, "=", jobNo.trim())); + } + + private String getTravelAgencyName(String travelAgencyId) { + if (StrUtil.isBlank(travelAgencyId)) { + return ""; + } + TourTravelAgency agency = tourLedgerService.dao().fetch(TourTravelAgency.class, travelAgencyId); + return agency == null ? "" : StrUtil.blankToDefault(agency.getAgencyName(), ""); + } + + private boolean signupTimeEquals(String importSignupTime, String ledgerSignupTime) { + if (StrUtil.isBlank(importSignupTime) || StrUtil.isBlank(ledgerSignupTime)) { + return false; + } + String ledgerTime = normalizeSignupTime(ledgerSignupTime); + if (importSignupTime.equals(ledgerTime)) { + return true; + } + return importSignupTime.length() == 16 && ledgerTime.startsWith(importSignupTime) + || ledgerTime.length() == 16 && importSignupTime.startsWith(ledgerTime); + } + + private String normalizeSignupTime(Object value) { + if (value instanceof Date date) { + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date); + } + String text = StrUtil.trimToEmpty(value == null ? "" : String.valueOf(value)).replace('T', ' '); + if (StrUtil.isBlank(text)) { + return ""; + } + text = text.replaceAll("\\.0$", ""); + String[] dateTimePatterns = { + "yyyy-MM-dd HH:mm:ss", + "yyyy-MM-dd HH:mm", + "yyyy/M/d H:mm:ss", + "yyyy/M/d H:mm", + "M/d/yy H:mm:ss", + "M/d/yy H:mm", + "M/d/yyyy H:mm:ss", + "M/d/yyyy H:mm" + }; + for (String pattern : dateTimePatterns) { + try { + LocalDateTime dateTime = LocalDateTime.parse(text, DateTimeFormatter.ofPattern(pattern)); + return dateTime.format(DateTimeFormatter.ofPattern(pattern.contains(":ss") ? "yyyy-MM-dd HH:mm:ss" : "yyyy-MM-dd HH:mm")); + } catch (DateTimeParseException ignored) { + } + } + String[] datePatterns = {"yyyy-MM-dd", "yyyy/M/d", "M/d/yy", "M/d/yyyy"}; + for (String pattern : datePatterns) { + try { + LocalDate date = LocalDate.parse(text, DateTimeFormatter.ofPattern(pattern)); + return date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + } catch (DateTimeParseException ignored) { + } + } + return text; + } + + private Boolean parseJoined(String value) { + String joined = StrUtil.trimToEmpty(value).toLowerCase(); + if ("1".equals(joined) || "1.0".equals(joined) || "是".equals(joined) || "true".equals(joined) || "已参加".equals(joined)) { + return true; + } + if ("0".equals(joined) || "0.0".equals(joined) || "否".equals(joined) || "false".equals(joined) || "未参加".equals(joined)) { + return false; + } + return null; + } + + @At + @SaCheckPermission("tour.ledger") + public Result unionOptions() { + Cnd cnd = Cnd.NEW(); + cnd.asc("unionCode"); + cnd.asc("name"); + return Result.success(tourLedgerService.dao().query(Sys_union.class, cnd)); + } + + @At + @SaCheckPermission("tour.ledger") + public Result lineOptions(Integer startYear, Integer endYear, String travelPeriod, String lineType, String unionId, String keyword, Boolean directFamilyOnly, Boolean overCostOnly) { + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.andEX("t.`year`", ">=", startYear); + cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear); + cnd.and("t.lineName", "IS NOT", null); + cnd.and("t.lineName", "<>", ""); + cnd.andEX("t.unionId", "=", unionId); + cnd.andEX("t.lineType", "=", lineType); + appendTravelPeriodFilter(cnd, travelPeriod); + appendKeywordFilter(cnd, keyword); + appendScopeFilter(cnd, directFamilyOnly, overCostOnly); + appendApprovedWorkflowFilter(cnd); + + Sql sql = Sqls.create(""" + SELECT DISTINCT + CASE + WHEN IFNULL(t.lineId, '') <> '' THEN t.lineId + ELSE CONCAT('legacy:', t.lineName) + END AS lineId, + COALESCE(NULLIF(l.lineName, ''), t.lineName) AS lineName + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + 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 + $condition + ORDER BY lineName ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission("tour.ledger") + public Result travelPeriodOptions(Integer startYear, Integer endYear, String lineId, String lineType, String unionId, String keyword, Boolean directFamilyOnly, Boolean overCostOnly) { + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.andEX("t.`year`", ">=", startYear); + cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear); + cnd.and("m.travelStartTime", "IS NOT", null); + cnd.and("m.travelStartTime", "<>", ""); + cnd.and("m.travelEndTime", "IS NOT", null); + cnd.and("m.travelEndTime", "<>", ""); + cnd.andEX("t.unionId", "=", unionId); + cnd.andEX("t.lineType", "=", lineType); + appendLineFilter(cnd, lineId); + appendKeywordFilter(cnd, keyword); + appendScopeFilter(cnd, directFamilyOnly, overCostOnly); + appendApprovedWorkflowFilter(cnd); + + Sql sql = Sqls.create(""" + SELECT DISTINCT CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) AS travelPeriod + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + 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 + $condition + ORDER BY travelPeriod ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + private Cnd buildQueryCnd(Integer startYear, Integer endYear, String keyword, String unionId, String lineId, String travelPeriod, String lineType, Boolean directFamilyOnly, Boolean overCostOnly) { + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.andEX("t.`year`", ">=", startYear); + cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear); + cnd.andEX("t.unionId", "=", unionId); + cnd.andEX("t.lineType", "=", lineType); + appendLineFilter(cnd, lineId); + appendTravelPeriodFilter(cnd, travelPeriod); + appendKeywordFilter(cnd, keyword); + appendScopeFilter(cnd, directFamilyOnly, overCostOnly); + appendApprovedWorkflowFilter(cnd); + return cnd; + } + + private void appendApprovedWorkflowFilter(Cnd cnd) { + SqlExpressionGroup group = new SqlExpressionGroup(); + group.or("ins.id", "IS", null); + group.or("ins.state", "=", ProcessInstanceStateEnum.FINISHED.getCode()); + cnd.and(group); + } + + private void appendScopeFilter(Cnd cnd, Boolean directFamilyOnly, Boolean overCostOnly) { + if (Boolean.TRUE.equals(directFamilyOnly)) { + cnd.and("IF(IFNULL(l.directFamilyUnitLine, 0) = 1 OR dr.id IS NOT NULL, 1, 0)", "=", 1); + } + if (Boolean.TRUE.equals(overCostOnly)) { + cnd.and("t.overCostReimbursed", "=", true); + } + } + + private void appendKeywordFilter(Cnd cnd, String keyword) { + if (StrUtil.isNotBlank(keyword)) { + SqlExpressionGroup group = new SqlExpressionGroup(); + group.orLike("t.userName", keyword.trim()); + group.orLike("t.jobNo", keyword.trim()); + cnd.and(group); + } + } + + private String getOrderColumn(String orderName) { + if ("year".equals(orderName)) { + return "t.`year`"; + } + if ("jobNo".equals(orderName)) { + return "t.jobNo"; + } + if ("userName".equals(orderName)) { + return "t.userName"; + } + if ("lineName".equals(orderName)) { + return "COALESCE(NULLIF(l.lineName, ''), t.lineName)"; + } + if ("travelPeriod".equals(orderName)) { + return "COALESCE(m.travelStartTime, tp.travelPeriod)"; + } + if ("lineType".equals(orderName)) { + return "t.lineType"; + } + if ("signupTime".equals(orderName)) { + return "t.signupTime"; + } + if ("familyCount".equals(orderName)) { + return "familyCount"; + } + if ("joined".equals(orderName)) { + return "t.joined"; + } + return "t.`year`"; + } + + private String getTravelPeriod(TourLedger ledger) { + if (ledger == null || ledger.getYear() == null || StrUtil.isBlank(ledger.getLineId())) { + return ""; + } + if (StrUtil.isNotBlank(ledger.getMatterId())) { + Sql exactSql = Sqls.create(""" + SELECT CASE + WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> '' + THEN CONCAT(travelStartTime, ' 至 ', travelEndTime) + ELSE '' + END AS travelPeriod + FROM tour_matter + WHERE delFlag = 0 + AND id = @matterId + """); + exactSql.setParam("matterId", ledger.getMatterId()); + exactSql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(exactSql); + return exactSql.getString(); + } + Sql sql = Sqls.create(""" + SELECT GROUP_CONCAT( + DISTINCT CASE + WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> '' + THEN CONCAT(travelStartTime, ' 至 ', travelEndTime) + ELSE NULL + END + ORDER BY travelStartTime ASC + SEPARATOR ';' + ) AS travelPeriod + FROM tour_matter + WHERE delFlag = 0 + AND `year` = @year + AND lineId = @lineId + """); + sql.setParam("year", ledger.getYear()); + sql.setParam("lineId", ledger.getLineId()); + sql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(sql); + return sql.getString(); + } + + private boolean isDirectFamilyUnitLine(TourLedger ledger) { + if (ledger == null || StrUtil.isBlank(ledger.getLineId())) { + return false; + } + Sql sql = Sqls.create(""" + SELECT IFNULL(directFamilyUnitLine, 0) + FROM tour_line + WHERE delFlag = 0 + AND id = @lineId + """); + sql.setParam("lineId", ledger.getLineId()); + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt() > 0; + } + + private boolean isFillBedInfo(TourLedger ledger) { + if (ledger == null || StrUtil.isBlank(ledger.getMatterId())) { + return true; + } + Sql sql = Sqls.create(""" + SELECT IFNULL(MAX(s.fillBedInfo), 1) + FROM tour_ledger t + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN tour_setting s ON s.id = m.settingId AND s.delFlag = 0 + WHERE t.id = @ledgerId + """); + sql.setParam("ledgerId", ledger.getId()); + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt() > 0; + } + + private void appendTravelPeriodFilter(Cnd cnd, String travelPeriod) { + if (StrUtil.isBlank(travelPeriod)) { + return; + } + cnd.and("CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)", "=", travelPeriod); + } + + private void appendLineFilter(Cnd cnd, String lineId) { + if (StrUtil.isBlank(lineId)) { + return; + } + if (lineId.startsWith("legacy:")) { + SqlExpressionGroup legacyGroup = new SqlExpressionGroup(); + legacyGroup.or("t.lineId", "IS", null); + legacyGroup.or("t.lineId", "=", ""); + cnd.and(legacyGroup); + cnd.and("t.lineName", "=", lineId.substring("legacy:".length())); + return; + } + cnd.and("t.lineId", "=", lineId); + } + + private String getCurrentLineName(TourLedger ledger) { + if (ledger == null) { + return ""; + } + if (StrUtil.isBlank(ledger.getLineId())) { + return ledger.getLineName(); + } + Sql sql = Sqls.create(""" + SELECT lineName + FROM tour_line + WHERE delFlag = 0 + AND id = @lineId + """); + sql.setParam("lineId", ledger.getLineId()); + sql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(sql); + return StrUtil.blankToDefault(sql.getString(), ledger.getLineName()); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLineController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLineController.java new file mode 100644 index 00000000..b6758ac0 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLineController.java @@ -0,0 +1,297 @@ +package com.budwk.app.zhgh.dayofficework.tour.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.annotation.SLog; +import com.budwk.app.base.constant.RoleConstant; +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.sys.models.Sys_dict; +import com.budwk.app.sys.services.SysDictService; +import com.budwk.app.sys.views.View_user; +import com.budwk.app.web.commons.auth.utils.AuthUtil; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLine; +import com.budwk.app.zhgh.dayofficework.tour.models.TourTravelAgency; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLineService; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.Cnd; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.dao.util.cri.SqlExpressionGroup; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.util.NutMap; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +@IocBean +@Ok("json:full") +@At("/platform/tour/route") +public class TourLineController { + + @Inject + private TourLineService tourLineService; + + @Inject + private SysDictService sysDictService; + + @At("") + @Ok("beetl:/platform/zhgh/dayofficework/Tour/route/index.html") + @SaCheckPermission("tour.route") + public void index() { + } + + @At + @SaCheckPermission("tour.route") + public Result pageData(PageForm pageForm, Integer year, String lineName, String lotId) { + if (StrUtil.isBlank(pageForm.getPageOrderName())) { + pageForm.defaultSortDesc("enabled"); + } + + Cnd cnd = Cnd.NEW(); + cnd.and("l.delFlag", "=", false); + cnd.andEX("l.`year`", "=", year); + cnd.and(Cnd.likeEX("l.lineName", lineName)); + cnd.andEX("l.lotId", "=", lotId); + boolean sysAdmin = AuthUtil.hasRole(RoleConstant.SYSADMIN.name()); + boolean branchUnionScope = !sysAdmin && AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name()); + applyDataScope(cnd, sysAdmin, branchUnionScope); + String creatorJoin = branchUnionScope ? """ + LEFT JOIN sys_unit lineUnit ON lineUnit.id = l.unitId + LEFT JOIN sys_user creatorUser ON creatorUser.id = COALESCE(NULLIF(l.creatorUserId, ''), l.createdBy) + LEFT JOIN sys_unit creatorUnit ON creatorUnit.id = creatorUser.unitId + """ : ""; + + Sql countSql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_line l + $creatorJoin + $condition + """); + countSql.setVar("creatorJoin", creatorJoin); + countSql.setCondition(cnd); + countSql.setCallback(Sqls.callback.integer()); + tourLineService.dao().execute(countSql); + int count = countSql.getInt(); + + Sql listSql = Sqls.create(""" + SELECT + l.*, + a.agencyName AS travelAgencyName, + lot.lotName AS lotName + FROM tour_line l + LEFT JOIN tour_travel_agency a ON a.id = l.travelAgencyId + LEFT JOIN tour_setting_lot lot ON lot.id = l.lotId + $creatorJoin + $condition + ORDER BY $orderColumn $orderBy, l.`year` DESC, l.lineCode ASC, l.createdAt DESC + """); + listSql.setVar("creatorJoin", creatorJoin); + listSql.setCondition(cnd); + listSql.setVar("orderColumn", getOrderColumn(pageForm.getPageOrderName())); + listSql.setVar("orderBy", "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc"); + listSql.setPager(tourLineService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize())); + listSql.setCallback(Sqls.callback.maps()); + tourLineService.dao().execute(listSql); + + Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, listSql.getList(NutMap.class)); + return Result.success(pagination); + } + + @At + @SaCheckPermission("tour.route") + public Result detail(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("参数错误"); + } + TourLine line = tourLineService.fetch(id); + return line == null ? Result.error("线路不存在") : Result.success(line); + } + + @At + @SaCheckPermission("tour.route") + public Result travelAgencyOptions(Integer year) { + Cnd cnd = Cnd.where(TourTravelAgency::getEnabled, "=", true); + cnd.andEX(TourTravelAgency::getYear, "=", year); + cnd.asc(TourTravelAgency::getAgencyCode).asc(TourTravelAgency::getAgencyName); + return Result.success(tourLineService.dao().query(TourTravelAgency.class, cnd)); + } + + @At + @SaCheckPermission("tour.route") + public Result lotOptions(Integer year) { + Cnd cnd = Cnd.NEW(); + cnd.andEX("s.`year`", "=", year); + Sql sql = Sqls.create(""" + SELECT lot.id, lot.lotName, lot.lotValue, lot.activityCost + FROM tour_setting_lot lot + INNER JOIN tour_setting s ON s.id = lot.settingId + $condition + ORDER BY lot.lotValue DESC, lot.lotName ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLineService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission("tour.route") + public Result lineTypeOptions() { + List list = sysDictService.getSubListByCode("lineType"); + if (list == null) { + return Result.success(Collections.emptyList()); + } + return Result.success(list.stream().filter(item -> !item.isDisabled()).collect(Collectors.toList())); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.route") + @SLog(type = "tour", tag = "线路管理", msg = "保存线路信息") + public Result doSubmit(TourLine line) { + Result checkResult = check(line); + if (checkResult != null) { + return checkResult; + } + + Cnd sameCodeCnd = Cnd.where(TourLine::getYear, "=", line.getYear()) + .and(TourLine::getLineCode, "=", line.getLineCode()); + if (StrUtil.isNotBlank(line.getId())) { + sameCodeCnd.and(TourLine::getId, "<>", line.getId()); + } + if (tourLineService.count(sameCodeCnd) > 0) { + return Result.error("同年度下线路编号已存在"); + } + if (line.getEnabled() == null) { + line.setEnabled(true); + } + if (line.getOpenFlag() == null) { + line.setOpenFlag(true); + } + if (line.getDirectFamilyUnitLine() == null) { + line.setDirectFamilyUnitLine(false); + } + if (StrUtil.isBlank(line.getId())) { + fillCreatorInfo(line, null); + tourLineService.insert(line); + } else { + fillCreatorInfo(line, tourLineService.fetch(line.getId())); + tourLineService.updateIgnoreNull(line); + } + return Result.success(); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.route") + @SLog(type = "tour", tag = "线路管理", msg = "删除线路信息") + public Result doDelete(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("参数错误"); + } + tourLineService.delete(id); + return Result.success(); + } + + private Result check(TourLine line) { + if (line == null) { + return Result.error("参数错误"); + } + if (line.getYear() == null) { + return Result.error("创建年度不能为空"); + } + if (StrUtil.isBlank(line.getTravelAgencyId())) { + return Result.error("旅行社名称不能为空"); + } + if (StrUtil.isBlank(line.getLineName())) { + return Result.error("线路名称不能为空"); + } + if (StrUtil.isBlank(line.getLineCode())) { + return Result.error("线路编号不能为空"); + } + if (StrUtil.isBlank(line.getLineType())) { + return Result.error("线路类型不能为空"); + } + if (StrUtil.isBlank(line.getLotId())) { + return Result.error("时间标段不能为空"); + } + if (StrUtil.isBlank(line.getMobileThumb())) { + return Result.error("移动端缩略图不能为空"); + } + return null; + } + + /** + * 创建人和所在单位由当前登录人生成,编辑时保留原创建信息。 + */ + private void fillCreatorInfo(TourLine line, TourLine oldLine) { + if (oldLine != null) { + line.setCreatorUserId(defaultIfBlank(oldLine.getCreatorUserId(), SecurityUtil.getUserId())); + line.setCreatorName(defaultIfBlank(oldLine.getCreatorName(), SecurityUtil.getUserUsername())); + line.setUnitId(defaultIfBlank(oldLine.getUnitId(), SecurityUtil.getUnitId())); + line.setUnitName(defaultIfBlank(oldLine.getUnitName(), getCurrentUnitName())); + return; + } + line.setCreatorUserId(SecurityUtil.getUserId()); + line.setCreatorName(SecurityUtil.getUserUsername()); + line.setUnitId(SecurityUtil.getUnitId()); + line.setUnitName(getCurrentUnitName()); + } + + private String getCurrentUnitName() { + View_user user = tourLineService.dao().fetch(View_user.class, Cnd.where("id", "=", SecurityUtil.getUserId())); + return user == null ? "" : defaultIfBlank(user.getUnitName(), ""); + } + + private String defaultIfBlank(String value, String defaultValue) { + return StrUtil.isBlank(value) ? defaultValue : value; + } + + /** + * 线路数据范围: + * SYSADMIN 可查看全部;分工会主席查看本工会创建的线路;其它角色保持原逻辑,仅查看自己创建的线路。 + */ + private void applyDataScope(Cnd cnd, boolean sysAdmin, boolean branchUnionScope) { + if (sysAdmin) { + return; + } + if (branchUnionScope) { + SqlExpressionGroup unionGroup = new SqlExpressionGroup(); + unionGroup.or("lineUnit.unionId", "=", SecurityUtil.getUnionId()); + unionGroup.or("creatorUnit.unionId", "=", SecurityUtil.getUnionId()); + cnd.and(unionGroup); + return; + } + SqlExpressionGroup creatorGroup = new SqlExpressionGroup(); + creatorGroup.or("l.creatorUserId", "=", SecurityUtil.getUserId()); + creatorGroup.or("l.createdBy", "=", SecurityUtil.getUserId()); + cnd.and(creatorGroup); + } + + private String getOrderColumn(String orderName) { + if ("year".equals(orderName)) { + return "l.`year`"; + } + if ("lineCode".equals(orderName)) { + return "l.lineCode"; + } + if ("lotName".equals(orderName)) { + return "lot.lotName"; + } + if ("lineType".equals(orderName)) { + return "l.lineType"; + } + if ("enabled".equals(orderName)) { + return "l.enabled"; + } + return "l.`year`"; + } +} 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 new file mode 100644 index 00000000..9905aaa2 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMatterController.java @@ -0,0 +1,441 @@ +package com.budwk.app.zhgh.dayofficework.tour.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.annotation.SLog; +import com.budwk.app.base.constant.RoleConstant; +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.sys.models.Sys_dict; +import com.budwk.app.sys.models.Sys_union; +import com.budwk.app.sys.services.SysDictService; +import com.budwk.app.web.commons.auth.utils.AuthUtil; +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.TourMatter; +import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting; +import com.budwk.app.zhgh.dayofficework.tour.service.TourMatterService; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.Cnd; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.dao.util.cri.SqlExpressionGroup; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.util.NutMap; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +@IocBean +@Ok("json:full") +@At("/platform/tour/matter") +public class TourMatterController { + + private static final String MOBILE_PATTERN = "^1[3-9]\\d{9}$"; + + @Inject + private TourMatterService tourMatterService; + + @Inject + private SysDictService sysDictService; + + @At("") + @Ok("beetl:/platform/zhgh/dayofficework/Tour/matter/index.html") + @SaCheckPermission("tour.matter") + public void index() { + } + + @At + @SaCheckPermission("tour.matter") + public Result pageData(PageForm pageForm, Integer year, String matterName, String unionId, String organizationType) { + Cnd cnd = Cnd.NEW(); + cnd.and("m.delFlag", "=", false); + cnd.andEX("m.`year`", "=", year); + cnd.and(Cnd.likeEX("m.matterName", matterName)); + cnd.andEX("m.unionId", "=", unionId); + cnd.andEX("m.organizationType", "=", organizationType); + applyMatterDataScope(cnd); + + Sql countSql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_matter m + $condition + """); + countSql.setCondition(cnd); + countSql.setCallback(Sqls.callback.integer()); + tourMatterService.dao().execute(countSql); + int count = countSql.getInt(); + + Sql listSql = Sqls.create(""" + SELECT + m.*, + s.configName AS settingName, + u.name AS unionName, + d.name AS organizationTypeName, + l.lineName AS lineName, + l.lineType AS lineType, + l.directFamilyUnitLine AS directFamilyUnitLine + FROM tour_matter m + LEFT JOIN tour_setting s ON s.id = m.settingId + LEFT JOIN sys_union u ON u.id = m.unionId + LEFT JOIN tour_line l ON l.id = m.lineId + LEFT JOIN sys_dict d ON d.`code` = m.organizationType + AND d.parentId = (SELECT id FROM sys_dict WHERE `code` = 'organizationType' LIMIT 1) + $condition + ORDER BY $orderColumn $orderBy, m.`year` DESC, m.createdAt DESC + """); + listSql.setCondition(cnd); + listSql.setVar("orderColumn", getOrderColumn(pageForm.getPageOrderName())); + listSql.setVar("orderBy", "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc"); + listSql.setPager(tourMatterService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize())); + listSql.setCallback(Sqls.callback.maps()); + tourMatterService.dao().execute(listSql); + + Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, listSql.getList(NutMap.class)); + return Result.success(pagination); + } + + @At + @SaCheckPermission("tour.matter") + public Result detail(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("参数错误"); + } + TourMatter matter = tourMatterService.fetch(id); + return matter == null ? Result.error("事项不存在") : Result.success(matter); + } + + @At + @SaCheckPermission("tour.matter") + public Result settingOptions(Integer year) { + Cnd cnd = Cnd.where(TourSetting::getEnabled, "=", true); + cnd.andEX(TourSetting::getYear, "=", year); + cnd.desc(TourSetting::getCreatedAt); + cnd.desc(TourSetting::getUpdatedAt); + cnd.asc(TourSetting::getConfigName); + return Result.success(tourMatterService.dao().query(TourSetting.class, cnd)); + } + + @At + @SaCheckPermission("tour.matter") + public Result unionOptions() { + Cnd cnd = Cnd.NEW(); + cnd.asc("unionCode"); + cnd.asc("name"); + return Result.success(tourMatterService.dao().query(Sys_union.class, cnd)); + } + + @At + @SaCheckPermission("tour.matter") + public Result lineOptions(Integer year) { + Cnd cnd = Cnd.NEW(); + cnd.and("l.delFlag", "=", false); + cnd.and("l.enabled", "=", true); + cnd.and("l.openFlag", "=", true); + + Sql sql = Sqls.create(""" + SELECT + l.*, + lot.lotName AS lotName, + lot.lotValue AS lotValue, + lot.lotValue AS lotDays, + lot.activityCost AS activityCost + FROM tour_line l + LEFT JOIN tour_setting_lot lot ON lot.id = l.lotId + $condition + ORDER BY l.lineCode ASC, l.lineName ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourMatterService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission("tour.matter") + public Result organizationTypeOptions() { + List list = sysDictService.getSubListByCode("organizationType"); + if (list == null) { + return Result.success(Collections.emptyList()); + } + return Result.success(list.stream().filter(item -> !item.isDisabled()).collect(Collectors.toList())); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.matter") + @SLog(type = "tour", tag = "疗休养事项", msg = "保存疗休养事项") + public Result doSubmit(TourMatter matter) { + Result checkResult = check(matter); + if (checkResult != null) { + return checkResult; + } + + Cnd sameNameCnd = Cnd.where(TourMatter::getYear, "=", matter.getYear()) + .and(TourMatter::getMatterName, "=", matter.getMatterName()); + if (StrUtil.isNotBlank(matter.getId())) { + sameNameCnd.and(TourMatter::getId, "<>", matter.getId()); + } + if (tourMatterService.count(sameNameCnd) > 0) { + return Result.error("同年度下事项名称已存在"); + } + if (matter.getEnabled() == null) { + matter.setEnabled(true); + } + fillCreatorAndUnion(matter); + if (StrUtil.isBlank(matter.getId())) { + tourMatterService.insert(matter); + } else { + tourMatterService.updateIgnoreNull(matter); + } + return Result.success(); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.matter") + @SLog(type = "tour", tag = "疗休养事项", msg = "删除疗休养事项") + public Result doDelete(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("参数错误"); + } + TourMatter matter = tourMatterService.fetch(id); + if (matter == null) { + return Result.error("事项不存在"); + } + long signupCount = countSignup(id); + if (signupCount > 0) { + return Result.error("当前事项对应线路已有人员报名,不能删除"); + } + tourMatterService.delete(id); + return Result.success(); + } + + @At + @SaCheckPermission("tour.matter") + public Result deleteInfo(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("参数错误"); + } + TourMatter matter = tourMatterService.fetch(id); + if (matter == null) { + return Result.error("事项不存在"); + } + long signupCount = countSignup(id); + return Result.success(NutMap.NEW() + .addv("signupCount", signupCount) + .addv("canDelete", signupCount <= 0) + .addv("message", signupCount > 0 ? "当前事项已有人员报名,不能删除" : "当前事项暂无人员报名,可删除")); + } + + @At + @SaCheckPermission("tour.matter") + public Result lineConfig(String matterId) { + if (StrUtil.isBlank(matterId)) { + return Result.error("参数错误"); + } + TourMatter matter = tourMatterService.fetch(matterId); + return matter == null ? Result.error("事项不存在") : Result.success(matter); + } + + @At + @SaCheckPermission("tour.matter") + public Result settingPeople(String matterId) { + if (StrUtil.isBlank(matterId)) { + return Result.error("参数错误"); + } + TourMatter matter = tourMatterService.fetch(matterId); + if (matter == null || StrUtil.isBlank(matter.getSettingId())) { + return Result.success(NutMap.NEW()); + } + TourSetting setting = tourMatterService.dao().fetch(TourSetting.class, matter.getSettingId()); + if (setting == null) { + return Result.success(NutMap.NEW()); + } + return Result.success(NutMap.NEW() + .addv("minGroupPeople", setting.getMinGroupPeople()) + .addv("maxGroupPeople", setting.getMaxGroupPeople())); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.matter") + @SLog(type = "tour", tag = "疗休养事项", msg = "保存事项线路配置") + public Result lineConfigDoSubmit(TourMatter matter) { + Result checkResult = checkLineConfig(matter); + if (checkResult != null) { + return checkResult; + } + TourMatter oldMatter = tourMatterService.fetch(matter.getId()); + if (oldMatter == null) { + return Result.error("事项不存在"); + } + oldMatter.setLineId(matter.getLineId()); + oldMatter.setSignupStartTime(matter.getSignupStartTime()); + oldMatter.setSignupEndTime(matter.getSignupEndTime()); + oldMatter.setTravelStartTime(matter.getTravelStartTime()); + oldMatter.setTravelEndTime(matter.getTravelEndTime()); + oldMatter.setContactName(matter.getContactName()); + oldMatter.setContactPhone(matter.getContactPhone()); + oldMatter.setMinGroupPeople(matter.getMinGroupPeople()); + oldMatter.setMaxGroupPeople(matter.getMaxGroupPeople()); + oldMatter.setEstimatedCost(matter.getEstimatedCost()); + tourMatterService.update(oldMatter); + return Result.success(); + } + + private Result check(TourMatter matter) { + if (matter == null) { + return Result.error("参数错误"); + } + if (matter.getYear() == null) { + return Result.error("年度不能为空"); + } + if (StrUtil.isBlank(matter.getMatterName())) { + return Result.error("事项名称不能为空"); + } + if (StrUtil.isBlank(matter.getSettingId())) { + return Result.error("疗休养配置不能为空"); + } + if (StrUtil.isBlank(matter.getOrganizationType())) { + return Result.error("组织形式不能为空"); + } + if (!"schoolUnion".equals(matter.getOrganizationType()) && StrUtil.isBlank(matter.getUnionId())) { + return Result.error("所属工会不能为空"); + } + return null; + } + + private Result checkLineConfig(TourMatter matter) { + if (matter == null) { + return Result.error("参数错误"); + } + if (StrUtil.isBlank(matter.getId())) { + return Result.error("事项不能为空"); + } + if (StrUtil.isBlank(matter.getLineId())) { + return Result.error("线路不能为空"); + } + if (StrUtil.isBlank(matter.getSignupStartTime()) || StrUtil.isBlank(matter.getSignupEndTime()) + || StrUtil.isBlank(matter.getTravelStartTime()) || StrUtil.isBlank(matter.getTravelEndTime())) { + return Result.error("时间不能为空"); + } + if (matter.getSignupStartTime().compareTo(matter.getSignupEndTime()) >= 0) { + return Result.error("报名开始时间必须小于报名结束时间"); + } + if (matter.getTravelStartTime().compareTo(matter.getTravelEndTime()) > 0) { + return Result.error("出行开始时间不能晚于出行结束时间"); + } + if (matter.getSignupEndTime().compareTo(matter.getTravelStartTime()) >= 0) { + return Result.error("报名结束时间必须小于出行开始时间"); + } + if (StrUtil.isBlank(matter.getContactName())) { + return Result.error("联系人不能为空"); + } + if (StrUtil.isBlank(matter.getContactPhone()) || !matter.getContactPhone().matches(MOBILE_PATTERN)) { + return Result.error("联系方式格式不正确"); + } + if (matter.getMinGroupPeople() == null || matter.getMinGroupPeople() <= 0) { + return Result.error("最少成团人数必须大于0"); + } + if (matter.getMaxGroupPeople() == null || matter.getMaxGroupPeople() <= 0) { + return Result.error("最多成团人数必须大于0"); + } + if (matter.getMinGroupPeople() > matter.getMaxGroupPeople()) { + return Result.error("最少成团人数不能大于最多成团人数"); + } + if (matter.getEstimatedCost() == null || matter.getEstimatedCost().signum() < 0) { + return Result.error("预计费用不能小于0"); + } + return null; + } + + private void fillCreatorAndUnion(TourMatter matter) { + if (StrUtil.isBlank(matter.getId())) { + matter.setCreatorUserId(SecurityUtil.getUserId()); + matter.setCreatorName(SecurityUtil.getUserUsername()); + } else { + TourMatter oldMatter = tourMatterService.fetch(matter.getId()); + if (oldMatter != null) { + matter.setCreatorUserId(oldMatter.getCreatorUserId()); + matter.setCreatorName(oldMatter.getCreatorName()); + } + } + if ("schoolUnion".equals(matter.getOrganizationType())) { + matter.setUnionId(""); + } + } + + private long countSignup(String matterId) { + return tourMatterService.dao().count(TourLedger.class, Cnd.where(TourLedger::getDelFlag, "=", false) + .and(TourLedger::getMatterId, "=", matterId)); + } + + /** + * 事项数据范围:SYSADMIN 查看全部;分工会主席查看本工会事项;其它角色仅查看自己创建的事项。 + */ + private void applyMatterDataScope(Cnd cnd) { + if (AuthUtil.hasRole(RoleConstant.SYSADMIN.name())) { + return; + } + if (AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())) { + cnd.and("m.unionId", "=", SecurityUtil.getUnionId()); + return; + } + SqlExpressionGroup creatorGroup = new SqlExpressionGroup(); + creatorGroup.or("m.creatorUserId", "=", SecurityUtil.getUserId()); + creatorGroup.or("m.createdBy", "=", SecurityUtil.getUserId()); + cnd.and(creatorGroup); + } + + /** + * 事项批次选择线路时沿用线路管理的数据范围,避免在事项页面选到权限外线路。 + */ + private void applyLineDataScope(Cnd cnd, boolean sysAdmin, boolean branchUnionScope) { + if (sysAdmin) { + return; + } + if (branchUnionScope) { + SqlExpressionGroup unionGroup = new SqlExpressionGroup(); + unionGroup.or("lineUnit.unionId", "=", SecurityUtil.getUnionId()); + unionGroup.or("creatorUnit.unionId", "=", SecurityUtil.getUnionId()); + cnd.and(unionGroup); + return; + } + SqlExpressionGroup creatorGroup = new SqlExpressionGroup(); + creatorGroup.or("l.creatorUserId", "=", SecurityUtil.getUserId()); + creatorGroup.or("l.createdBy", "=", SecurityUtil.getUserId()); + cnd.and(creatorGroup); + } + + private String getOrderColumn(String orderName) { + if ("year".equals(orderName)) { + return "m.`year`"; + } + if ("matterName".equals(orderName)) { + return "m.matterName"; + } + if ("unionName".equals(orderName)) { + return "u.name"; + } + if ("lineName".equals(orderName)) { + return "l.lineName"; + } + if ("settingName".equals(orderName)) { + return "s.configName"; + } + if ("organizationTypeName".equals(orderName)) { + return "d.location"; + } + if ("enabled".equals(orderName)) { + return "m.enabled"; + } + return "m.`year`"; + } +} 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 new file mode 100644 index 00000000..06d75f2c --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMySignupController.java @@ -0,0 +1,1845 @@ +package com.budwk.app.zhgh.dayofficework.tour.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.lang.Dict; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.annotation.SLog; +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.base.utils.CommonDownloadUtil; +import com.budwk.app.flow.constant.FlowConst; +import com.budwk.app.flow.engine.FlowEngine; +import com.budwk.app.flow.entity.ProcessInstance; +import com.budwk.app.flow.entity.ProcessTask; +import com.budwk.app.flow.enums.ProcessInstanceStateEnum; +import com.budwk.app.flow.enums.ProcessSubmitTypeEnum; +import com.budwk.app.flow.enums.ProcessTaskStateEnum; +import com.budwk.app.sys.views.View_user; +import com.budwk.app.sys.models.Sys_dict; +import com.budwk.app.sys.services.SysDictService; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerDirectRelative; +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.service.TourLedgerDirectRelativeService; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService; +import com.budwk.app.zhgh.dayofficework.tour.service.TourMatterService; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.Cnd; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.json.Json; +import org.nutz.lang.Lang; +import org.nutz.lang.util.NutMap; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; +import org.nutz.mvc.annotation.Param; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDType0Font; + +import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +@IocBean +@Ok("json:full") +@At("/platform/tour/mysignup") +public class TourMySignupController { + + private static final String DIRECT_FAMILY_WORKFLOW_KEY = "LXYBZXQSXL"; + + @Inject + private TourLedgerService tourLedgerService; + + @Inject + private TourLedgerFamilyService tourLedgerFamilyService; + + @Inject + private TourLedgerDirectRelativeService tourLedgerDirectRelativeService; + + @Inject + private TourMatterService tourMatterService; + + @Inject + private SysDictService sysDictService; + + @Inject + private ActivityBasicScopeService activityBasicScopeService; + + @Inject + private FlowEngine flowEngine; + + @At("") + @Ok("beetl:/platform/zhgh/dayofficework/Tour/mysignup/index.html") + @SaCheckPermission("tour.mysignup") + public void index() { + } + + @At("/h5") + @Ok("beetl:/platform/zhghh5/dayofficework/tour/mysignup/index.html") + @SaCheckPermission("tour.mysignup") + public void h5() { + } + + @At + @SaCheckPermission("tour.mysignup") + public Result pageData(PageForm pageForm, Integer startYear, Integer endYear, String lineName) { + Cnd cnd = buildQueryCnd(startYear, endYear, lineName); + + Sql countSql = Sqls.create(""" + SELECT COUNT(DISTINCT t.id) + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id + LEFT JOIN wf_process_task task ON task.processInstanceId = ins.id AND task.taskState = 10 + $condition + """); + countSql.setCondition(cnd); + countSql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(countSql); + int count = countSql.getInt(); + + Sql listSql = Sqls.create(""" + SELECT + t.*, + ins.id AS instanceId, + ins.businessNo, + ins.state AS instanceState, + ins.variable AS instanceVariable, + ins.processDefineId AS instanceProcessDefineId, + task.id AS taskId, + task.taskName AS taskKey, + task.displayName AS taskName, + task.taskType, + task.performType AS taskPerformType, + task.taskState, + task.finishTime, + task.taskParentId, + task.variable AS taskVariable, + IF((SELECT taskName FROM wf_process_task tt WHERE tt.id = task.taskParentId) = 'startTask', 1, 0) AS canRevoke, + (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, + IFNULL(GROUP_CONCAT(DISTINCT task.displayName), IF(ins.id IS NULL, '', '结束')) AS curTaskName, + IFNULL(f.familyCount, 0) AS familyCount, + COALESCE(m.travelEndTime, tp.travelEndTime) AS travelEndTime, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE tp.travelPeriod + END AS travelPeriod + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id + 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 ( + SELECT ledgerId, COUNT(1) AS familyCount + FROM tour_ledger_family + WHERE delFlag = 0 + 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 ( + SELECT + `year`, + lineId, + GROUP_CONCAT( + DISTINCT CASE + WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> '' + THEN CONCAT(travelStartTime, ' 至 ', travelEndTime) + ELSE NULL + END + ORDER BY travelStartTime ASC + SEPARATOR ';' + ) AS travelPeriod, + MAX(travelEndTime) AS travelEndTime + FROM tour_matter + WHERE delFlag = 0 + AND lineId IS NOT NULL + AND lineId <> '' + GROUP BY `year`, lineId + ) tp ON (t.matterId IS NULL OR t.matterId = '') AND tp.`year` = t.`year` AND tp.lineId = t.lineId + $condition + GROUP BY t.id + ORDER BY $orderColumn $orderBy, t.`year` DESC, t.signupTime DESC, t.createdAt DESC + """); + listSql.setCondition(cnd); + 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)); + return Result.success(pagination); + } + + @At + @SaCheckPermission("tour.mysignup") + public Result detail(String id) { + TourLedger ledger = fetchOwnLedger(id); + if (ledger == null) { + return Result.error("报名记录不存在"); + } + Cnd familyCnd = Cnd.where(TourLedgerFamily::getDelFlag, "=", false) + .and(TourLedgerFamily::getLedgerId, "=", ledger.getId()); + familyCnd.asc(TourLedgerFamily::getCreatedAt); + TourLedgerDirectRelative directRelative = tourLedgerDirectRelativeService.fetch( + Cnd.where(TourLedgerDirectRelative::getDelFlag, "=", false) + .and(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId())); + NutMap signupConfig = getSignupConfig(ledger); + return Result.success(NutMap.NEW() + .addv("ledger", ledger) + .addv("travelPeriod", getTravelPeriod(ledger)) + .addv("families", tourLedgerFamilyService.query(familyCnd)) + .addv("directRelative", directRelative) + .addv("allowFamily", signupConfig.getBoolean("allowFamily", false)) + .addv("fillBedInfo", signupConfig.getBoolean("fillBedInfo", true)) + .addv("directFamilyUnitLine", signupConfig.getBoolean("directFamilyUnitLine", false))); + } + + @At + @SaCheckPermission("tour.mysignup") + public Result exportInfo(String id) { + NutMap exportInfo = buildExportInfo(id); + if (exportInfo == null) { + return Result.error("报名记录不存在"); + } + return Result.success(exportInfo); + } + + @At + @Ok("void") + @SaCheckPermission("tour.mysignup") + public void exportPdf(String id, HttpServletResponse response) { + NutMap exportInfo = buildExportInfo(id); + if (exportInfo == null) { + throw new IllegalArgumentException("报名记录不存在"); + } + byte[] pdfBytes = buildExportPdf(exportInfo); + String fileName = exportInfo.getString("userName", "申请人") + "-5天外超出部分疗休养费用由单位承担申请书.pdf"; + CommonDownloadUtil.download(fileName, pdfBytes, response); + } + + @At + @SaCheckPermission("tour.mysignup") + public Result directRelativeExportInfo(String id) { + NutMap exportInfo = buildDirectRelativeExportInfo(id); + if (exportInfo == null) { + return Result.error("直系亲属线路报名记录不存在"); + } + return Result.success(exportInfo); + } + + @At + @Ok("void") + @SaCheckPermission("tour.mysignup") + public void directRelativeExportPdf(String id, HttpServletResponse response) { + NutMap exportInfo = buildDirectRelativeExportInfo(id); + if (exportInfo == null) { + throw new IllegalArgumentException("直系亲属线路报名记录不存在"); + } + byte[] pdfBytes = buildDirectRelativeExportPdf(exportInfo); + String fileName = exportInfo.getString("userName", "申请人") + "-直系亲属单位疗休养报销申请书.pdf"; + CommonDownloadUtil.download(fileName, pdfBytes, response); + } + + @At + @SaCheckPermission("tour.mysignup") + public Result signupDetail(String id) { + TourLedger ledger = fetchOwnLedger(id); + if (ledger == null) { + return Result.error("报名记录不存在"); + } + if (StrUtil.isBlank(ledger.getMatterId())) { + return Result.error("历史报名记录缺少事项信息,不能修改"); + } + Sql sql = Sqls.create(""" + SELECT + m.id AS matterId, + m.`year`, + m.matterName, + m.unionId AS matterUnionId, + m.travelStartTime, + m.travelEndTime, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE '' + END AS travelPeriod, + l.id AS lineId, + l.lineName, + l.lineType, + l.directFamilyUnitLine, + l.travelAgencyId, + a.agencyName AS travelAgencyName, + IFNULL(lot.allowOverReimbursement, 0) AS allowOverReimbursement, + s.allowFamily, + IFNULL(s.fillBedInfo, 1) AS fillBedInfo + FROM tour_matter m + INNER JOIN tour_line l ON l.id = m.lineId + LEFT JOIN tour_travel_agency a ON a.id = l.travelAgencyId + LEFT JOIN tour_setting_lot lot ON lot.id = l.lotId + LEFT JOIN tour_setting s ON s.id = m.settingId + WHERE m.delFlag = 0 + AND m.enabled = 1 + AND m.id = @matterId + """); + sql.setParam("matterId", ledger.getMatterId()); + sql.setCallback(Sqls.callback.map()); + tourMatterService.dao().execute(sql); + NutMap matter = sql.getObject(NutMap.class); + if (matter == null || matter.isEmpty()) { + return Result.error("报名事项不存在或已停用"); + } + if (isTravelEnded(matter.getString("travelEndTime"))) { + return Result.error("线路出行已结束,不能修改"); + } + Cnd familyCnd = Cnd.where(TourLedgerFamily::getDelFlag, "=", false) + .and(TourLedgerFamily::getLedgerId, "=", ledger.getId()); + familyCnd.asc(TourLedgerFamily::getCreatedAt); + TourLedgerDirectRelative directRelative = tourLedgerDirectRelativeService.fetch(Cnd.where(TourLedgerDirectRelative::getDelFlag, "=", false) + .and(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId())); + return Result.success(NutMap.NEW() + .addv("matter", matter) + .addv("ledger", ledger) + .addv("families", tourLedgerFamilyService.query(familyCnd)) + .addv("directRelative", directRelative)); + } + + @At + @SaCheckPermission("tour.mysignup") + public Result directRelativeOptions() { + List list = sysDictService.getSubListByCode("directRelative"); + if (list == null) { + return Result.success(Collections.emptyList()); + } + return Result.success(list.stream().filter(item -> !item.isDisabled()).collect(Collectors.toList())); + } + + @At + @SaCheckPermission("tour.mysignup") + public Result outProvinceQuotaNotice(String matterId) { + if (StrUtil.isBlank(matterId)) { + return Result.error("参数错误"); + } + TourMatter matter = tourMatterService.fetch(matterId); + if (matter == null || Boolean.TRUE.equals(matter.getDelFlag()) || !Boolean.TRUE.equals(matter.getEnabled())) { + return Result.error("报名出行时段不存在或已停用"); + } + TourLine line = tourLedgerService.dao().fetch(TourLine.class, matter.getLineId()); + if (line == null || Boolean.TRUE.equals(line.getDelFlag()) || !Boolean.TRUE.equals(line.getEnabled())) { + return Result.error("报名线路不存在或已停用"); + } + if (!isOutProvinceLine(line.getLineType())) { + return Result.success(NutMap.NEW().addv("canApply", true).addv("noticeRequired", false)); + } + TourSetting setting = tourLedgerService.dao().fetch(TourSetting.class, matter.getSettingId()); + if (setting == null || Boolean.TRUE.equals(setting.getDelFlag()) || !Boolean.TRUE.equals(setting.getEnabled())) { + return Result.error("报名事项配置不存在或已停用"); + } + Integer activityGroupId = parseInteger(setting.getActivityGroupId()); + if (activityGroupId == null || !activityBasicScopeService.isUserInGroup(activityGroupId, SecurityUtil.getUserId())) { + return Result.error("您不在本次疗休养报名范围内"); + } + TourLedger oldLedger = fetchCurrentUserMatterLedger(matter.getId()); + OutProvinceQuota quota = calculateOutProvinceQuota(matter, setting, oldLedger); + return Result.success(NutMap.NEW() + .addv("noticeRequired", true) + .addv("canApply", quota.canApply()) + .addv("ratioType", quota.ratioType()) + .addv("basePeople", quota.basePeople()) + .addv("totalSignupPeople", quota.totalSignupPeople()) + .addv("outProvinceSignupPeople", quota.outProvinceSignupPeople()) + .addv("allowPeople", quota.allowPeople()) + .addv("message", buildOutProvinceQuotaNotice(quota))); + } + + @At + @SaCheckPermission("tour.mysignup") + public Result bedTypeOptions() { + List list = sysDictService.getSubListByCode("bedType"); + if (list == null) { + return Result.success(Collections.emptyList()); + } + return Result.success(list.stream().filter(item -> !item.isDisabled()).collect(Collectors.toList())); + } + + @At + @SaCheckPermission("tour.mysignup") + public Result familyRelationshipOptions() { + Sql sql = Sqls.create(""" + SELECT d.* + FROM sys_dict p + INNER JOIN sys_dict d ON d.path LIKE CONCAT(p.path, '%') + WHERE p.code = 'familyRelationship' + AND d.id <> p.id + AND d.disabled = 0 + AND d.hasChildren = 0 + ORDER BY d.path ASC, d.location ASC + """); + sql.setCallback(Sqls.callback.entities()); + sql.setEntity(tourLedgerService.dao().getEntity(Sys_dict.class)); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(Sys_dict.class)); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.mysignup") + public Result doSignup(TourLedger ledger, @Param("families") String families, @Param("directRelative") String directRelative) { + Result checkResult = checkSignup(ledger); + if (checkResult != null) { + return checkResult; + } + if (StrUtil.isBlank(ledger.getId())) { + return Result.error("报名记录不存在"); + } + TourLedger oldLedger = fetchOwnLedger(ledger.getId()); + if (oldLedger == null) { + return Result.error("报名记录不存在"); + } + List familyList = parseFamilies(families); + Result familyResult = checkFamilies(familyList); + if (familyResult != null) { + return familyResult; + } + TourMatter matter = tourMatterService.fetch(ledger.getMatterId()); + Result ruleResult = checkSignupRule(ledger, matter, oldLedger); + if (ruleResult != null) { + return ruleResult; + } + Result maxPeopleResult = checkMaxGroupPeople(matter, oldLedger, familyList); + if (maxPeopleResult != null) { + return maxPeopleResult; + } + 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()); + if (setting != null && Boolean.FALSE.equals(setting.getFillBedInfo())) { + clearBedInfo(ledger, familyList); + } + if (!allowOverCostReimbursement(line)) { + ledger.setOverCostReimbursed(false); + } + ledger.setOverCostReimbursed(Boolean.TRUE.equals(ledger.getOverCostReimbursed())); + boolean approvalRequired = directFamilyLine || Boolean.TRUE.equals(ledger.getOverCostReimbursed()); + TourLedgerDirectRelative directRelativeInfo = parseDirectRelative(directRelative); + Result directRelativeResult = checkDirectRelative(directFamilyLine, directRelativeInfo); + if (directRelativeResult != null) { + return directRelativeResult; + } + if (approvalRequired) { + Result workflowCheckResult = checkSignupApprovalWorkflowCanSubmit(ledger.getId()); + if (workflowCheckResult != null) { + return workflowCheckResult; + } + } + + ledger.setYear(matter.getYear()); + ledger.setHasFamily(Lang.isNotEmpty(familyList)); + ledger.setJoined(oldLedger.getJoined()); + ledger.setReimbursed(oldLedger.getReimbursed()); + ledger.setSignupTime(defaultIfBlank(oldLedger.getSignupTime(), ledger.getSignupTime())); + fillStaffInfo(ledger); + + tourLedgerService.updateIgnoreNull(ledger); + tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId())); + tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId())); + if (Lang.isNotEmpty(familyList)) { + familyList.forEach(item -> { + item.setLedgerId(ledger.getId()); + item.setStaffJobNo(ledger.getJobNo()); + item.setStaffName(ledger.getUserName()); + }); + tourLedgerFamilyService.insert(familyList); + } + if (directFamilyLine) { + directRelativeInfo.setId(null); + directRelativeInfo.setLedgerId(ledger.getId()); + directRelativeInfo.setLineId(line.getId()); + tourLedgerDirectRelativeService.insert(directRelativeInfo); + } + if (approvalRequired) { + Result workflowResult = startOrContinueSignupApprovalWorkflow(ledger, directFamilyLine ? directRelativeInfo : null, matter, line); + if (workflowResult != null) { + return workflowResult; + } + } + return Result.success().addMsg(approvalRequired ? "修改已提交,等待审核" : "修改成功"); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.mysignup") + public Result doCancelSignup(String ledgerId, String matterId) { + TourLedger ledger = fetchOwnLedger(ledgerId); + if (ledger == null && StrUtil.isNotBlank(matterId)) { + Cnd cnd = Cnd.where(TourLedger::getDelFlag, "=", false) + .and(TourLedger::getMatterId, "=", matterId) + .and(TourLedger::getJobNo, "=", currentJobNo()); + cnd.desc(TourLedger::getCreatedAt); + ledger = tourLedgerService.fetch(cnd); + } + if (ledger == null) { + 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())); + return Result.success().addMsg("取消报名成功"); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.mysignup") + @SLog(type = "tour", tag = "我的疗休养报名", msg = "删除我的疗休养报名") + public Result doDelete(String id) { + TourLedger ledger = fetchOwnLedger(id); + if (ledger == null) { + 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())); + return Result.success().addMsg("取消报名成功"); + } + + @At + @SaCheckPermission("tour.mysignup") + public Result lineOptions(Integer startYear, Integer endYear) { + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.and("t.jobNo", "=", currentJobNo()); + cnd.andEX("t.`year`", ">=", startYear); + cnd.andEX("t.`year`", "<=", endYear); + cnd.and("t.lineName", "IS NOT", null); + cnd.and("t.lineName", "<>", ""); + + Sql sql = Sqls.create(""" + SELECT DISTINCT t.lineName + FROM tour_ledger t + $condition + ORDER BY t.lineName ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + private Cnd buildQueryCnd(Integer startYear, Integer endYear, String lineName) { + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.and("t.jobNo", "=", currentJobNo()); + cnd.andEX("t.`year`", ">=", startYear); + cnd.andEX("t.`year`", "<=", endYear); + cnd.andEX("t.lineName", "=", lineName); + return cnd; + } + + private TourLedger fetchOwnLedger(String id) { + if (StrUtil.isBlank(id)) { + return null; + } + return tourLedgerService.fetch(Cnd.where(TourLedger::getDelFlag, "=", false) + .and(TourLedger::getId, "=", id) + .and(TourLedger::getJobNo, "=", currentJobNo())); + } + + private Result checkSignup(TourLedger ledger) { + if (ledger == null) { + return Result.error("参数错误"); + } + if (ledger.getYear() == null) { + return Result.error("年度不能为空"); + } + if (StrUtil.isBlank(ledger.getLineId())) { + return Result.error("报名线路不能为空"); + } + if (StrUtil.isBlank(ledger.getMatterId())) { + return Result.error("报名出行时段不能为空"); + } + if (StrUtil.isBlank(ledger.getLineName())) { + return Result.error("报名线路不能为空"); + } + return null; + } + + private Result checkSignupRule(TourLedger ledger, TourMatter matter, TourLedger oldLedger) { + if (matter == null || Boolean.TRUE.equals(matter.getDelFlag()) || !Boolean.TRUE.equals(matter.getEnabled())) { + return Result.error("报名出行时段不存在或已停用"); + } + if (isTravelEnded(matter.getTravelEndTime())) { + return Result.error("线路出行已结束,不能修改"); + } + if (!ledger.getLineId().equals(matter.getLineId())) { + return Result.error("报名线路与出行时段不匹配"); + } + TourLine line = tourLedgerService.dao().fetch(TourLine.class, matter.getLineId()); + if (line == null || Boolean.TRUE.equals(line.getDelFlag()) || !Boolean.TRUE.equals(line.getEnabled())) { + return Result.error("报名线路不存在或已停用"); + } + TourSetting setting = tourLedgerService.dao().fetch(TourSetting.class, matter.getSettingId()); + if (setting == null || Boolean.TRUE.equals(setting.getDelFlag()) || !Boolean.TRUE.equals(setting.getEnabled())) { + return Result.error("报名事项配置不存在或已停用"); + } + Integer activityGroupId = parseInteger(setting.getActivityGroupId()); + if (activityGroupId == null || !activityBasicScopeService.isUserInGroup(activityGroupId, SecurityUtil.getUserId())) { + return Result.error("您不在本次疗休养报名范围内"); + } + if (StrUtil.isBlank(matter.getSignupStartTime()) || StrUtil.isBlank(matter.getSignupEndTime())) { + return Result.error("报名时间未配置"); + } + String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + if (now.compareTo(normalizeDateTime(matter.getSignupStartTime(), false)) < 0) { + return Result.error("报名未开始"); + } + if (now.compareTo(normalizeDateTime(matter.getSignupEndTime(), true)) > 0) { + return Result.error("报名已结束"); + } + CycleAllowedTimes cycleAllowedTimes = calculateCycleAllowedTimes(matter, setting, oldLedger); + if (!cycleAllowedTimes.canApply()) { + return Result.error(buildCycleAllowedTimesNotice(cycleAllowedTimes)); + } + CycleTotalCost cycleTotalCost = calculateCycleTotalCost(matter, setting, line, oldLedger); + if (!cycleTotalCost.canApply()) { + return Result.error(buildCycleTotalCostNotice(cycleTotalCost)); + } + String jobNo = currentJobNo(); + if (isOutProvinceLine(line.getLineType())) { + int startYear = setting.getCycleStartYear() == null ? matter.getYear() : setting.getCycleStartYear(); + int endYear = LocalDate.now().getYear(); + Cnd outProvinceCnd = Cnd.where(TourLedger::getDelFlag, "=", false) + .and(TourLedger::getJobNo, "=", jobNo) + .and(TourLedger::getYear, ">=", startYear) + .and(TourLedger::getYear, "<=", endYear) + .and(TourLedger::getLineType, "in", List.of("省外线路", "省外")); + if (oldLedger != null) { + outProvinceCnd.and(TourLedger::getId, "<>", oldLedger.getId()); + } + if (tourLedgerService.count(outProvinceCnd) > 0) { + return Result.error("您已参加过省外线路,不能报名省外线路,请选择省内线路"); + } + OutProvinceQuota quota = calculateOutProvinceQuota(matter, setting, oldLedger); + if (!quota.canApply()) { + return Result.error(buildOutProvinceQuotaNotice(quota)); + } + Cnd sameYearDomesticCnd = Cnd.where(TourLedger::getDelFlag, "=", false) + .and(TourLedger::getJobNo, "=", jobNo) + .and(TourLedger::getYear, "=", matter.getYear()) + .and(TourLedger::getLineType, "in", List.of("省内线路", "省内", "国内线路", "国内")); + if (oldLedger != null) { + sameYearDomesticCnd.and(TourLedger::getId, "<>", oldLedger.getId()); + } + TourLedger existingDomesticLedger = fetchLatestLedger(sameYearDomesticCnd); + if (existingDomesticLedger != null) { + return Result.error(buildSelectedLineMessage(existingDomesticLedger)); + } + } + if (isInProvinceLine(line.getLineType())) { + Cnd sameYearCnd = Cnd.where(TourLedger::getDelFlag, "=", false) + .and(TourLedger::getJobNo, "=", jobNo) + .and(TourLedger::getYear, "=", matter.getYear()); + if (oldLedger != null) { + sameYearCnd.and(TourLedger::getId, "<>", oldLedger.getId()); + } + TourLedger existingLedger = fetchLatestLedger(sameYearCnd); + if (existingLedger != null) { + return Result.error(buildSelectedLineMessage(existingLedger)); + } + } + return null; + } + + private TourLedger fetchLatestLedger(Cnd cnd) { + cnd.desc(TourLedger::getSignupTime); + cnd.desc(TourLedger::getCreatedAt); + return tourLedgerService.fetch(cnd); + } + + private TourLedger fetchCurrentUserMatterLedger(String matterId) { + if (StrUtil.isBlank(matterId)) { + return null; + } + Cnd cnd = Cnd.where(TourLedger::getDelFlag, "=", false) + .and(TourLedger::getMatterId, "=", matterId) + .and(TourLedger::getJobNo, "=", currentJobNo()); + cnd.desc(TourLedger::getCreatedAt); + return tourLedgerService.fetch(cnd); + } + + private CycleAllowedTimes calculateCycleAllowedTimes(TourMatter matter, TourSetting setting, TourLedger oldLedger) { + int allowedTimes = setting == null || setting.getCycleAllowedTimes() == null ? 0 : setting.getCycleAllowedTimes(); + int endYear = LocalDate.now().getYear(); + int startYear = setting == null || setting.getCycleStartYear() == null ? (matter == null || matter.getYear() == null ? endYear : matter.getYear()) : setting.getCycleStartYear(); + String excludeLedgerId = oldLedger == null ? null : oldLedger.getId(); + int joinedTimes = allowedTimes <= 0 ? 0 : countCycleJoinedTimes(currentJobNo(), startYear, endYear, excludeLedgerId); + return new CycleAllowedTimes(startYear, endYear, allowedTimes, joinedTimes); + } + + private int countCycleJoinedTimes(String jobNo, int startYear, int endYear, String excludeLedgerId) { + if (StrUtil.isBlank(jobNo)) { + return 0; + } + String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND id <> @excludeLedgerId"; + Sql sql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_ledger + WHERE delFlag = 0 + AND joined = 1 + AND jobNo = @jobNo + AND `year` >= @startYear + AND `year` <= @endYear + """ + excludeSql); + sql.setParam("jobNo", jobNo); + sql.setParam("startYear", startYear); + sql.setParam("endYear", endYear); + if (StrUtil.isNotBlank(excludeLedgerId)) { + sql.setParam("excludeLedgerId", excludeLedgerId); + } + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private String buildCycleAllowedTimesNotice(CycleAllowedTimes cycleAllowedTimes) { + return "您在 " + cycleAllowedTimes.startYear() + " 至 " + cycleAllowedTimes.endYear() + + " 周期内已参加 " + cycleAllowedTimes.joinedTimes() + " 次,周期允许 " + + cycleAllowedTimes.allowedTimes() + " 次,不能报名"; + } + + private CycleTotalCost calculateCycleTotalCost(TourMatter matter, TourSetting setting, TourLine line, TourLedger oldLedger) { + int totalCost = setting == null || setting.getCycleTotalCost() == null ? 0 : setting.getCycleTotalCost(); + int endYear = LocalDate.now().getYear(); + int startYear = setting == null || setting.getCycleStartYear() == null ? (matter == null || matter.getYear() == null ? endYear : matter.getYear()) : setting.getCycleStartYear(); + String excludeLedgerId = oldLedger == null ? null : oldLedger.getId(); + int joinedCost = totalCost <= 0 ? 0 : countCycleJoinedCost(currentJobNo(), startYear, endYear, excludeLedgerId); + int currentCost = totalCost <= 0 ? 0 : getLineActivityCost(line); + return new CycleTotalCost(startYear, endYear, totalCost, joinedCost, currentCost); + } + + private int countCycleJoinedCost(String jobNo, int startYear, int endYear, String excludeLedgerId) { + if (StrUtil.isBlank(jobNo)) { + return 0; + } + String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND t.id <> @excludeLedgerId"; + Sql sql = Sqls.create(""" + SELECT IFNULL(SUM(IFNULL(lot.activityCost, 0)), 0) + FROM tour_ledger t + LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 + LEFT JOIN tour_setting_lot lot ON lot.id = l.lotId AND lot.delFlag = 0 + WHERE t.delFlag = 0 + AND t.joined = 1 + AND t.jobNo = @jobNo + AND t.`year` >= @startYear + AND t.`year` <= @endYear + """ + excludeSql); + sql.setParam("jobNo", jobNo); + sql.setParam("startYear", startYear); + sql.setParam("endYear", endYear); + if (StrUtil.isNotBlank(excludeLedgerId)) { + sql.setParam("excludeLedgerId", excludeLedgerId); + } + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private int getLineActivityCost(TourLine line) { + if (line == null || StrUtil.isBlank(line.getLotId())) { + return 0; + } + Sql sql = Sqls.create(""" + SELECT IFNULL(activityCost, 0) AS activityCost + FROM tour_setting_lot + WHERE delFlag = 0 + AND id = @lotId + """); + sql.setParam("lotId", line.getLotId()); + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private String buildCycleTotalCostNotice(CycleTotalCost cycleTotalCost) { + return "您在 " + cycleTotalCost.startYear() + " 至 " + cycleTotalCost.endYear() + + " 周期内已参加线路费用合计 " + cycleTotalCost.joinedCost() + + " 元,本次线路费用 " + cycleTotalCost.currentCost() + + " 元,周期内总费用 " + cycleTotalCost.totalCost() + " 元,超过限制,不能报名"; + } + + private OutProvinceQuota calculateOutProvinceQuota(TourMatter matter, TourSetting setting, TourLedger oldLedger) { + String ratioType = normalizeOutProvinceRatioType(setting == null ? null : setting.getOutProvinceRatioType()); + BigDecimal originalRatio = setting == null ? null : setting.getOutProvinceRatio(); + boolean oneThirdRatio = isOneThirdRatio(originalRatio); + BigDecimal ratio = oneThirdRatio ? BigDecimal.ONE.divide(BigDecimal.valueOf(3), 12, RoundingMode.HALF_UP) + : (originalRatio == null ? BigDecimal.ZERO : originalRatio); + int year = matter == null || matter.getYear() == null ? LocalDate.now().getYear() : matter.getYear(); + String excludeLedgerId = oldLedger == null ? null : oldLedger.getId(); + int totalSignupPeople = countYearSignupPeople(year, excludeLedgerId); + int outProvinceSignupPeople = countYearOutProvinceSignupPeople(year, excludeLedgerId); + int basePeople; + if ("可参加教职工人数".equals(ratioType)) { + Integer groupId = parseInteger(setting == null ? null : setting.getActivityGroupId()); + basePeople = countActivityGroupPeople(groupId); + } else if ("固定人数".equals(ratioType)) { + basePeople = setting == null || setting.getOutProvinceFixedPeople() == null ? 0 : setting.getOutProvinceFixedPeople(); + } else { + basePeople = totalSignupPeople; + } + int quotaPeople = oneThirdRatio + ? basePeople / 3 + : ratio.multiply(BigDecimal.valueOf(basePeople)).setScale(0, RoundingMode.FLOOR).intValue(); + int allowPeople = quotaPeople - outProvinceSignupPeople; + return new OutProvinceQuota(ratioType, ratio, basePeople, totalSignupPeople, outProvinceSignupPeople, allowPeople); + } + + private boolean isOneThirdRatio(BigDecimal ratio) { + if (ratio == null) { + return false; + } + return ratio.compareTo(BigDecimal.valueOf(0.33)) >= 0 && ratio.compareTo(BigDecimal.valueOf(0.3333)) <= 0; + } + + private String normalizeOutProvinceRatioType(String ratioType) { + if ("当年参加人数".equals(ratioType) || StrUtil.isBlank(ratioType)) { + return "当年报名人数"; + } + if ("可参加教职工人数".equals(ratioType) || "固定人数".equals(ratioType)) { + return ratioType; + } + return "当年报名人数"; + } + + private String buildOutProvinceQuotaNotice(OutProvinceQuota quota) { + String suffix = quota.canApply() ? "您可以报名,或您稍后报名" : "请稍后报名"; + if ("当年报名人数".equals(quota.ratioType())) { + return "当前报名人数是" + quota.totalSignupPeople() + "人,已经报名省外线路人数是" + + quota.outProvinceSignupPeople() + "人,按照省外人数不超过" + quota.ratioText() + + "的规则,当前时间允许报名人数是" + quota.allowPeople() + "人," + suffix; + } + if ("可参加教职工人数".equals(quota.ratioType())) { + return "今年有权限报名人数是" + quota.basePeople() + "人,已经报名省外线路人数是" + + quota.outProvinceSignupPeople() + "人,按照省外人数不超过" + quota.ratioText() + + "的规则,当前时间允许报名人数是" + quota.allowPeople() + "人," + suffix; + } + return "按照省外人数不超过" + quota.ratioText() + + "的规则,当前时间允许报名人数是" + quota.allowPeople() + "人," + suffix; + } + + private int countYearSignupPeople(int year, String excludeLedgerId) { + String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND t.id <> @excludeLedgerId"; + Sql sql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id + WHERE t.delFlag = 0 + AND t.`year` = @year + AND (ins.id IS NULL OR ins.state = @finishedState) + """ + excludeSql); + sql.setParam("year", year); + sql.setParam("finishedState", ProcessInstanceStateEnum.FINISHED.getCode()); + if (StrUtil.isNotBlank(excludeLedgerId)) { + sql.setParam("excludeLedgerId", excludeLedgerId); + } + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private int countYearOutProvinceSignupPeople(int year, String excludeLedgerId) { + String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND t.id <> @excludeLedgerId"; + Sql sql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id + WHERE t.delFlag = 0 + AND t.`year` = @year + AND t.lineType IN ('省外线路', '省外') + AND (ins.id IS NULL OR ins.state = @finishedState) + """ + excludeSql); + sql.setParam("year", year); + sql.setParam("finishedState", ProcessInstanceStateEnum.FINISHED.getCode()); + if (StrUtil.isNotBlank(excludeLedgerId)) { + sql.setParam("excludeLedgerId", excludeLedgerId); + } + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private int countActivityGroupPeople(Integer groupId) { + if (groupId == null) { + return 0; + } + Sql sql = Sqls.create("SELECT COUNT(1) FROM (" + activityBasicScopeService.buildGroupUserIdSubSqlText(groupId) + ") scope_users"); + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private boolean isOutProvinceLine(String lineType) { + return "省外线路".equals(lineType) || "省外".equals(lineType); + } + + private boolean isInProvinceLine(String lineType) { + return "省内线路".equals(lineType) || "省内".equals(lineType); + } + + private Integer parseInteger(String value) { + if (StrUtil.isBlank(value)) { + return null; + } + try { + return Integer.valueOf(value); + } catch (NumberFormatException e) { + return null; + } + } + + private String normalizeDateTime(String value, boolean endOfDay) { + if (StrUtil.isBlank(value)) { + return ""; + } + String trimmed = value.trim(); + if (trimmed.length() == 10) { + return trimmed + (endOfDay ? " 23:59:59" : " 00:00:00"); + } + if (trimmed.length() == 16) { + return trimmed + ":00"; + } + return trimmed; + } + + private boolean isTravelEnded(String travelEndTime) { + if (StrUtil.isBlank(travelEndTime)) { + return false; + } + String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + return now.compareTo(normalizeDateTime(travelEndTime, true)) > 0; + } + + private void fillStaffInfo(TourLedger ledger) { + View_user user = tourLedgerService.dao().fetch(View_user.class, Cnd.where(View_user::getId, "=", SecurityUtil.getUserId())); + 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.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 clearBedInfo(TourLedger ledger, List familyList) { + if (ledger != null) { + ledger.setBedType(""); + ledger.setBedInfo(""); + ledger.setIntendedRoommate(""); + } + if (Lang.isNotEmpty(familyList)) { + familyList.forEach(item -> { + item.setBedType(""); + item.setBedInfo(""); + item.setIntendedRoommate(""); + }); + } + } + + private List parseFamilies(String families) { + if (StrUtil.isBlank(families)) { + return Collections.emptyList(); + } + List list = Json.fromJsonAsList(TourLedgerFamily.class, families); + if (Lang.isEmpty(list)) { + return Collections.emptyList(); + } + return list.stream() + .filter(item -> item != null && StrUtil.isNotBlank(item.getFamilyName())) + .collect(Collectors.toList()); + } + + private Result checkFamilies(List familyList) { + if (Lang.isEmpty(familyList)) { + return null; + } + for (TourLedgerFamily family : familyList) { + if (family == null) { + continue; + } + if (StrUtil.isBlank(family.getFamilyName()) || StrUtil.isBlank(family.getGender()) + || StrUtil.isBlank(family.getIdCard()) || StrUtil.isBlank(family.getRelationship())) { + return Result.error("请完善亲属姓名、性别、身份证号码和关系"); + } + String idCard = family.getIdCard().trim().toUpperCase(); + if (!isValidIdCard(idCard)) { + return Result.error("请输入正确的身份证号码"); + } + family.setIdCard(idCard); + } + return null; + } + + 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]$"); + } + + private Result checkMaxGroupPeople(TourMatter matter, TourLedger oldLedger, List familyList) { + if (matter == null || matter.getMaxGroupPeople() == null || matter.getMaxGroupPeople() <= 0) { + return null; + } + int currentPeople = 1 + (Lang.isEmpty(familyList) ? 0 : familyList.size()); + int signedPeople = countSignupPeople(matter.getId(), oldLedger == null ? null : oldLedger.getId()); + int totalPeople = signedPeople + currentPeople; + if (totalPeople > matter.getMaxGroupPeople()) { + return Result.error("当前报名人数 " + signedPeople + " 人,本次报名 " + currentPeople + + " 人,最多成团人数 " + matter.getMaxGroupPeople() + " 人,报名后将超过人数上限,不能提交"); + } + return null; + } + + private int countSignupPeople(String matterId, String excludeLedgerId) { + if (StrUtil.isBlank(matterId)) { + return 0; + } + String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND t.id <> @excludeLedgerId"; + Sql sql = Sqls.create(""" + SELECT IFNULL(SUM(1 + IFNULL(f.familyCount, 0)), 0) AS signupPeople + FROM tour_ledger t + LEFT JOIN ( + SELECT ledgerId, COUNT(1) AS familyCount + FROM tour_ledger_family + WHERE delFlag = 0 + GROUP BY ledgerId + ) f ON f.ledgerId = t.id + WHERE t.delFlag = 0 + AND t.matterId = @matterId + """ + excludeSql); + sql.setParam("matterId", matterId); + if (StrUtil.isNotBlank(excludeLedgerId)) { + sql.setParam("excludeLedgerId", excludeLedgerId); + } + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private String buildSelectedLineMessage(TourLedger ledger) { + String selectedLineName = getLedgerLineName(ledger); + return "您已经选择【" + selectedLineName + "】,取消后再重新选择。"; + } + + private String getLedgerLineName(TourLedger ledger) { + if (ledger == null) { + return ""; + } + if (StrUtil.isNotBlank(ledger.getLineName())) { + return ledger.getLineName(); + } + if (StrUtil.isBlank(ledger.getLineId())) { + return ""; + } + TourLine line = tourLedgerService.dao().fetch(TourLine.class, ledger.getLineId()); + return line == null ? "" : defaultIfBlank(line.getLineName(), ""); + } + + private record CycleAllowedTimes(int startYear, + int endYear, + int allowedTimes, + int joinedTimes) { + boolean canApply() { + return allowedTimes <= 0 || joinedTimes < allowedTimes; + } + } + + private record CycleTotalCost(int startYear, + int endYear, + int totalCost, + int joinedCost, + int currentCost) { + boolean canApply() { + return totalCost <= 0 || joinedCost + currentCost <= totalCost; + } + } + + private record OutProvinceQuota(String ratioType, + BigDecimal ratio, + int basePeople, + int totalSignupPeople, + int outProvinceSignupPeople, + int allowPeople) { + boolean canApply() { + return allowPeople > 0; + } + + String ratioText() { + if (ratio == null || BigDecimal.ZERO.compareTo(ratio) == 0) { + return "0"; + } + BigDecimal oneThird = BigDecimal.ONE.divide(BigDecimal.valueOf(3), 8, RoundingMode.HALF_UP); + if (ratio.subtract(oneThird).abs().compareTo(BigDecimal.valueOf(0.01)) <= 0) { + return "1/3"; + } + return ratio.stripTrailingZeros().toPlainString(); + } + } + + private TourLedgerDirectRelative parseDirectRelative(String directRelative) { + if (StrUtil.isBlank(directRelative)) { + return null; + } + return Json.fromJson(TourLedgerDirectRelative.class, directRelative); + } + + private Result checkDirectRelative(boolean directFamilyLine, TourLedgerDirectRelative directRelativeInfo) { + if (!directFamilyLine) { + return null; + } + if (directRelativeInfo == null) { + return Result.error("请填写直系亲属线路信息"); + } + if (StrUtil.isBlank(directRelativeInfo.getRelativeName())) { + return Result.error("亲属姓名不能为空"); + } + if (StrUtil.isBlank(directRelativeInfo.getUnitName())) { + return Result.error("亲属所在单位不能为空"); + } + if (StrUtil.isBlank(directRelativeInfo.getRelationshipCode())) { + return Result.error("亲属关系不能为空"); + } + if (StrUtil.isBlank(directRelativeInfo.getLineName())) { + return Result.error("直系亲属线路名称不能为空"); + } + if (StrUtil.isBlank(directRelativeInfo.getTravelStartTime())) { + return Result.error("出行开始日期不能为空"); + } + if (StrUtil.isBlank(directRelativeInfo.getTravelEndTime())) { + return Result.error("出行结束日期不能为空"); + } + List directRelativeOptions = sysDictService.getSubListByCode("directRelative"); + Sys_dict dict = directRelativeOptions == null ? null : directRelativeOptions.stream() + .filter(item -> directRelativeInfo.getRelationshipCode().equals(item.getCode()) && !item.isDisabled()) + .findFirst() + .orElse(null); + if (dict == null) { + return Result.error("亲属关系无效"); + } + directRelativeInfo.setRelationshipName(defaultIfBlank(dict.getName(), directRelativeInfo.getRelationshipName())); + return null; + } + + private boolean allowOverCostReimbursement(TourLine line) { + if (line == null || StrUtil.isBlank(line.getLotId())) { + return false; + } + Sql sql = Sqls.create(""" + SELECT IFNULL(allowOverReimbursement, 0) AS allowOverReimbursement + FROM tour_setting_lot + WHERE id = @lotId + """); + sql.setParam("lotId", line.getLotId()); + sql.setCallback(Sqls.callback.map()); + tourLedgerService.dao().execute(sql); + NutMap lot = sql.getObject(NutMap.class); + return lot != null && lot.getBoolean("allowOverReimbursement", false); + } + + private Result startOrContinueSignupApprovalWorkflow(TourLedger ledger, TourLedgerDirectRelative directRelativeInfo, TourMatter matter, TourLine line) { + ProcessInstance instance = tourLedgerService.dao().fetch(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "=", ledger.getId()) + .and(ProcessInstance::getState, "<>", ProcessInstanceStateEnum.ABANDON.getCode()) + .desc(ProcessInstance::getCreatedAt)); + Dict args = buildSignupApprovalArgs(ledger, directRelativeInfo, matter, line); + if (instance == null) { + ProcessInstance newInstance = flowEngine.startProcessInstanceByKey(DIRECT_FAMILY_WORKFLOW_KEY, ledger.getId(), SecurityUtil.getUserId(), args); + List doingTaskList = flowEngine.processTaskService().getDoingTaskList(newInstance.getId(), null); + for (ProcessTask task : doingTaskList) { + flowEngine.executeProcessTask(task.getId(), SecurityUtil.getUserId(), args); + } + return null; + } + if (!ProcessInstanceStateEnum.DOING.getCode().equals(instance.getState())) { + return Result.error("当前审批流程状态不允许修改提交"); + } + ProcessTask startTask = tourLedgerService.dao().fetch(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "=", instance.getId()) + .and(ProcessTask::getTaskName, "=", "startTask") + .and(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.DOING.getCode())); + if (startTask == null) { + return Result.error("当前审批流程节点不允许修改提交"); + } + flowEngine.executeProcessTask(startTask.getId(), SecurityUtil.getUserId(), args); + return null; + } + + private Result checkSignupApprovalWorkflowCanSubmit(String ledgerId) { + ProcessInstance instance = tourLedgerService.dao().fetch(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "=", ledgerId) + .and(ProcessInstance::getState, "<>", ProcessInstanceStateEnum.ABANDON.getCode()) + .desc(ProcessInstance::getCreatedAt)); + if (instance == null) { + return null; + } + if (!ProcessInstanceStateEnum.DOING.getCode().equals(instance.getState())) { + return Result.error("当前审批流程状态不允许修改提交"); + } + ProcessTask startTask = tourLedgerService.dao().fetch(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "=", instance.getId()) + .and(ProcessTask::getTaskName, "=", "startTask") + .and(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.DOING.getCode())); + if (startTask == null) { + return Result.error("当前审批流程节点不允许修改提交"); + } + return null; + } + + private Dict buildSignupApprovalArgs(TourLedger ledger, TourLedgerDirectRelative directRelativeInfo, TourMatter matter, TourLine line) { + Dict args = Dict.create(); + args.set(FlowConst.SUBMIT_TYPE, ProcessSubmitTypeEnum.APPLY.getCode()); + args.set(FlowConst.FORM_DATA, NutMap.NEW() + .addv("ledger", ledger) + .addv("directRelative", directRelativeInfo) + .addv("matter", matter) + .addv("line", line)); + args.set(FlowConst.TASK_FORM_DATA_PREFIX + "matterId", ledger.getMatterId()); + args.set(FlowConst.TASK_FORM_DATA_PREFIX + "lineId", ledger.getLineId()); + args.set(FlowConst.TASK_FORM_DATA_PREFIX + "lineName", ledger.getLineName()); + args.set(FlowConst.TASK_FORM_DATA_PREFIX + "directFamilyUnitLine", line != null && Boolean.TRUE.equals(line.getDirectFamilyUnitLine())); + args.set(FlowConst.TASK_FORM_DATA_PREFIX + "overCostReimbursed", Boolean.TRUE.equals(ledger.getOverCostReimbursed())); + return args; + } + + private String getTravelPeriod(TourLedger ledger) { + if (ledger == null || ledger.getYear() == null || StrUtil.isBlank(ledger.getLineId())) { + return ""; + } + if (StrUtil.isNotBlank(ledger.getMatterId())) { + Sql exactSql = Sqls.create(""" + SELECT CASE + WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> '' + THEN CONCAT(travelStartTime, ' 至 ', travelEndTime) + ELSE '' + END AS travelPeriod + FROM tour_matter + WHERE delFlag = 0 + AND id = @matterId + """); + exactSql.setParam("matterId", ledger.getMatterId()); + exactSql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(exactSql); + return exactSql.getString(); + } + Sql sql = Sqls.create(""" + SELECT GROUP_CONCAT( + DISTINCT CASE + WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> '' + THEN CONCAT(travelStartTime, ' 至 ', travelEndTime) + ELSE NULL + END + ORDER BY travelStartTime ASC + SEPARATOR ';' + ) AS travelPeriod + FROM tour_matter + WHERE delFlag = 0 + AND `year` = @year + AND lineId = @lineId + """); + sql.setParam("year", ledger.getYear()); + sql.setParam("lineId", ledger.getLineId()); + sql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(sql); + return sql.getString(); + } + + private NutMap getSignupConfig(TourLedger ledger) { + if (ledger == null || StrUtil.isBlank(ledger.getId())) { + return NutMap.NEW().addv("allowFamily", false).addv("fillBedInfo", true).addv("directFamilyUnitLine", false); + } + Sql sql = Sqls.create(""" + SELECT + IFNULL(s.allowFamily, 0) AS allowFamily, + IFNULL(s.fillBedInfo, 1) AS fillBedInfo, + IFNULL(l.directFamilyUnitLine, 0) AS directFamilyUnitLine + FROM tour_ledger t + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN tour_setting s ON s.id = m.settingId AND s.delFlag = 0 + LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 + WHERE t.id = @ledgerId + """); + sql.setParam("ledgerId", ledger.getId()); + sql.setCallback(Sqls.callback.map()); + tourLedgerService.dao().execute(sql); + NutMap map = sql.getObject(NutMap.class); + if (map == null || map.isEmpty()) { + return NutMap.NEW().addv("allowFamily", false).addv("fillBedInfo", true).addv("directFamilyUnitLine", false); + } + return map; + } + + private NutMap buildExportInfo(String id) { + TourLedger ledger = fetchOwnLedger(id); + if (ledger == null || !Boolean.TRUE.equals(ledger.getOverCostReimbursed())) { + return null; + } + + NutMap matterInfo = getMatterExportInfo(ledger); + String travelStartTime = matterInfo.getString("travelStartTime", ""); + String travelEndTime = matterInfo.getString("travelEndTime", ""); + String travelPeriod = StrUtil.isNotBlank(travelStartTime) && StrUtil.isNotBlank(travelEndTime) + ? travelStartTime + " 至 " + travelEndTime + : getTravelPeriod(ledger); + if ((StrUtil.isBlank(travelStartTime) || StrUtil.isBlank(travelEndTime)) && StrUtil.isNotBlank(travelPeriod)) { + String[] dateRange = parseTravelPeriodRange(travelPeriod); + travelStartTime = firstNotBlank(travelStartTime, dateRange[0]); + travelEndTime = firstNotBlank(travelEndTime, dateRange[1]); + } + + NutMap approvalTimes = getApprovalTimes(ledger.getId()); + Integer totalDays = calcDays(travelStartTime, travelEndTime); + + return NutMap.NEW() + .addv("id", ledger.getId()) + .addv("userName", StrUtil.blankToDefault(ledger.getUserName(), "")) + .addv("unionName", StrUtil.blankToDefault(ledger.getUnionName(), "")) + .addv("lineName", StrUtil.blankToDefault(ledger.getLineName(), "")) + .addv("travelPeriod", StrUtil.blankToDefault(travelPeriod, "")) + .addv("travelStartTime", travelStartTime) + .addv("travelEndTime", travelEndTime) + .addv("travelStartYear", parseDatePart(travelStartTime, "year")) + .addv("travelStartMonth", parseDatePart(travelStartTime, "month")) + .addv("travelStartDay", parseDatePart(travelStartTime, "day")) + .addv("travelEndYear", parseDatePart(travelEndTime, "year")) + .addv("travelEndMonth", parseDatePart(travelEndTime, "month")) + .addv("travelEndDay", parseDatePart(travelEndTime, "day")) + .addv("totalDays", totalDays == null ? "" : totalDays) + .addv("estimatedCost", formatAmount(matterInfo.get("estimatedCost"))) + .addv("applyDate", firstNotBlank(approvalTimes.getString("applyDate", ""), ledger.getSignupTime())) + .addv("unionAuditorName", approvalTimes.getString("unionAuditorName", "")) + .addv("unionAuditDate", approvalTimes.getString("unionAuditDate", "")) + .addv("schoolUnionAuditDate", approvalTimes.getString("schoolUnionAuditDate", "")); + } + + private NutMap buildDirectRelativeExportInfo(String id) { + TourLedger ledger = fetchOwnLedger(id); + if (ledger == null) { + return null; + } + NutMap signupConfig = getSignupConfig(ledger); + if (!signupConfig.getBoolean("directFamilyUnitLine", false)) { + return null; + } + TourLedgerDirectRelative directRelative = tourLedgerDirectRelativeService.fetch(Cnd.where(TourLedgerDirectRelative::getDelFlag, "=", false) + .and(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId())); + if (directRelative == null) { + return null; + } + + NutMap matterInfo = getMatterExportInfo(ledger); + String travelStartTime = firstNotBlank(directRelative.getTravelStartTime(), matterInfo.getString("travelStartTime", "")); + String travelEndTime = firstNotBlank(directRelative.getTravelEndTime(), matterInfo.getString("travelEndTime", "")); + String travelPeriod = StrUtil.isNotBlank(travelStartTime) && StrUtil.isNotBlank(travelEndTime) + ? travelStartTime + " 至 " + travelEndTime + : getTravelPeriod(ledger); + if ((StrUtil.isBlank(travelStartTime) || StrUtil.isBlank(travelEndTime)) && StrUtil.isNotBlank(travelPeriod)) { + String[] dateRange = parseTravelPeriodRange(travelPeriod); + travelStartTime = firstNotBlank(travelStartTime, dateRange[0]); + travelEndTime = firstNotBlank(travelEndTime, dateRange[1]); + } + NutMap approvalTimes = getApprovalTimes(ledger.getId()); + + return NutMap.NEW() + .addv("id", ledger.getId()) + .addv("userName", StrUtil.blankToDefault(ledger.getUserName(), "")) + .addv("unionName", StrUtil.blankToDefault(ledger.getUnionName(), "")) + .addv("relativeName", StrUtil.blankToDefault(directRelative.getRelativeName(), "")) + .addv("relativeUnitName", StrUtil.blankToDefault(directRelative.getUnitName(), "")) + .addv("relationshipName", StrUtil.blankToDefault(directRelative.getRelationshipName(), "")) + .addv("lineName", firstNotBlank(directRelative.getLineName(), ledger.getLineName())) + .addv("travelPeriod", StrUtil.blankToDefault(travelPeriod, "")) + .addv("travelStartTime", travelStartTime) + .addv("travelEndTime", travelEndTime) + .addv("travelStartYear", parseDatePart(travelStartTime, "year")) + .addv("travelStartMonth", parseDatePart(travelStartTime, "month")) + .addv("travelStartDay", parseDatePart(travelStartTime, "day")) + .addv("travelEndYear", parseDatePart(travelEndTime, "year")) + .addv("travelEndMonth", parseDatePart(travelEndTime, "month")) + .addv("travelEndDay", parseDatePart(travelEndTime, "day")) + .addv("applyDate", firstNotBlank(approvalTimes.getString("applyDate", ""), ledger.getSignupTime())) + .addv("unionAuditorName", approvalTimes.getString("unionAuditorName", "")) + .addv("unionAuditDate", approvalTimes.getString("unionAuditDate", "")) + .addv("schoolUnionAuditDate", approvalTimes.getString("schoolUnionAuditDate", "")) + .addv("relativeUnitAuditDate", ""); + } + + private NutMap getMatterExportInfo(TourLedger ledger) { + if (ledger == null || StrUtil.isBlank(ledger.getMatterId())) { + return NutMap.NEW(); + } + Sql sql = Sqls.create(""" + SELECT + m.travelStartTime, + m.travelEndTime, + m.estimatedCost + FROM tour_matter m + WHERE m.delFlag = 0 + AND m.id = @matterId + """); + sql.setParam("matterId", ledger.getMatterId()); + sql.setCallback(Sqls.callback.map()); + tourMatterService.dao().execute(sql); + NutMap map = sql.getObject(NutMap.class); + return map == null ? NutMap.NEW() : map; + } + + private NutMap getApprovalTimes(String ledgerId) { + NutMap result = NutMap.NEW(); + if (StrUtil.isBlank(ledgerId)) { + return result; + } + Sql sql = Sqls.create(""" + SELECT + t.displayName, + t.variable, + DATE_FORMAT(t.finishTime, '%Y-%m-%d') AS finishDate + FROM wf_process_task t + INNER JOIN wf_process_instance ins ON ins.id = t.processInstanceId + WHERE ins.businessNo = @ledgerId + AND t.taskState = @finished + AND t.displayName IN ('申请', '分工会审核', '校工会审核') + ORDER BY t.finishTime ASC + """); + sql.setParam("ledgerId", ledgerId); + sql.setParam("finished", ProcessTaskStateEnum.FINISHED.getCode()); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + for (NutMap item : sql.getList(NutMap.class)) { + String displayName = item.getString("displayName", ""); + String finishDate = item.getString("finishDate", ""); + if ("申请".equals(displayName) && StrUtil.isBlank(result.getString("applyDate", ""))) { + result.put("applyDate", finishDate); + } else if ("分工会审核".equals(displayName)) { + result.put("unionAuditDate", finishDate); + result.put("unionAuditorName", parseTaskUserName(item.getString("variable", ""))); + } else if ("校工会审核".equals(displayName)) { + result.put("schoolUnionAuditDate", finishDate); + } + } + return result; + } + + private String parseTaskUserName(String variable) { + if (StrUtil.isBlank(variable)) { + return ""; + } + try { + NutMap map = Json.fromJson(NutMap.class, variable); + if (map == null) { + return ""; + } + return firstNotBlank( + map.getString(FlowConst.TASK_FORM_DATA_PREFIX + "userName", ""), + map.getString("userName", "") + ); + } catch (Exception e) { + return ""; + } + } + + private Integer calcDays(String start, String end) { + if (StrUtil.isBlank(start) || StrUtil.isBlank(end)) { + return null; + } + try { + LocalDate startDate = LocalDate.parse(start.substring(0, 10)); + LocalDate endDate = LocalDate.parse(end.substring(0, 10)); + return Math.toIntExact(ChronoUnit.DAYS.between(startDate, endDate) + 1); + } catch (Exception e) { + return null; + } + } + + private String parseDatePart(String date, String part) { + if (StrUtil.isBlank(date) || date.length() < 10) { + return ""; + } + try { + LocalDate localDate = LocalDate.parse(date.substring(0, 10)); + if ("year".equals(part)) { + return String.valueOf(localDate.getYear()); + } + return "month".equals(part) ? String.valueOf(localDate.getMonthValue()) : String.valueOf(localDate.getDayOfMonth()); + } catch (Exception e) { + return ""; + } + } + + private String[] parseTravelPeriodRange(String travelPeriod) { + if (StrUtil.isBlank(travelPeriod)) { + return new String[]{"", ""}; + } + String[] parts = travelPeriod.split("至"); + if (parts.length < 2) { + return new String[]{"", ""}; + } + return new String[]{normalizeDate(parts[0]), normalizeDate(parts[1])}; + } + + private String normalizeDate(String value) { + if (StrUtil.isBlank(value)) { + return ""; + } + String date = value.trim(); + return date.length() >= 10 ? date.substring(0, 10) : date; + } + + private String formatAmount(Object value) { + if (value == null) { + return ""; + } + try { + BigDecimal amount = new BigDecimal(String.valueOf(value)); + return amount.stripTrailingZeros().toPlainString(); + } catch (Exception e) { + return String.valueOf(value); + } + } + + private String firstNotBlank(String first, String second) { + return StrUtil.isNotBlank(first) ? first : StrUtil.blankToDefault(second, ""); + } + + private byte[] buildExportPdf(NutMap data) { + try (PDDocument document = new PDDocument(); + ByteArrayOutputStream out = new ByteArrayOutputStream()) { + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + PDType0Font font = loadChineseFont(document, false); + PDType0Font boldFont = loadChineseFont(document, true); + try (PDPageContentStream content = new PDPageContentStream(document, page)) { + PdfWriter writer = new PdfWriter(content, font, boldFont); + writer.center("5天外超出部分疗休养费用由单位承担申请书", 18, 710, true); + writer.text("学校工会:", 18, 90, 665, false); + + List p1 = new ArrayList<>(); + p1.add(PdfSegment.text("本人为")); + p1.add(PdfSegment.field(data.getString("unionName", ""), 54)); + p1.add(PdfSegment.text("的")); + p1.add(PdfSegment.field(data.getString("userName", ""), 54)); + p1.add(PdfSegment.text(",拟参加")); + p1.add(PdfSegment.field(data.getString("unionName", ""), 54)); + p1.add(PdfSegment.text("于")); + p1.add(PdfSegment.field(data.getString("travelStartYear", ""), 42)); + p1.add(PdfSegment.text("年")); + p1.add(PdfSegment.field(data.getString("travelStartMonth", ""), 28)); + p1.add(PdfSegment.text("月")); + p1.add(PdfSegment.field(data.getString("travelStartDay", ""), 28)); + p1.add(PdfSegment.text("日至")); + p1.add(PdfSegment.field(data.getString("travelEndYear", ""), 42)); + p1.add(PdfSegment.text("年")); + p1.add(PdfSegment.field(data.getString("travelEndMonth", ""), 28)); + p1.add(PdfSegment.text("月")); + p1.add(PdfSegment.field(data.getString("travelEndDay", ""), 28)); + p1.add(PdfSegment.text("日组织的")); + p1.add(PdfSegment.field(data.getString("lineName", ""), 72)); + p1.add(PdfSegment.text("线路疗休养,预计总时长")); + p1.add(PdfSegment.field(data.getString("totalDays", ""), 28)); + p1.add(PdfSegment.text("天(含在途时间)、总费用预计")); + p1.add(PdfSegment.field(data.getString("estimatedCost", ""), 48)); + p1.add(PdfSegment.text("元,本人申请5天以外(不含5天)超出部分费用由学校承担。")); + float paragraphEndY = writer.paragraph(p1, 90, 640, 420, 18, 27f, 36); + writer.text("特此申请,请批准。", 18, 126, paragraphEndY - 27f, false); + + writer.text("申请人:", 18, 300, paragraphEndY - 82, false); + writer.text(data.getString("userName", ""), 18, 372, paragraphEndY - 82, false); + drawPlainDateLine(writer, data.getString("applyDate", ""), 330, paragraphEndY - 109, 18); + + writer.text("分工会主席签字:", 18, 246, paragraphEndY - 136, false); + writer.text(data.getString("unionAuditorName", ""), 18, 390, paragraphEndY - 136, false); + drawPlainDateLine(writer, data.getString("unionAuditDate", ""), 330, paragraphEndY - 163, 18); + + writer.text("学校工会:盖章", 18, 300, paragraphEndY - 190, false); + drawPlainDateLine(writer, data.getString("schoolUnionAuditDate", ""), 330, paragraphEndY - 217, 18); + } + document.save(out); + return out.toByteArray(); + } catch (IOException e) { + throw new RuntimeException("导出PDF失败", e); + } + } + + private byte[] buildDirectRelativeExportPdf(NutMap data) { + try (PDDocument document = new PDDocument(); + ByteArrayOutputStream out = new ByteArrayOutputStream()) { + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + PDType0Font font = loadChineseFont(document, false); + PDType0Font boldFont = loadChineseFont(document, true); + try (PDPageContentStream content = new PDPageContentStream(document, page)) { + PdfWriter writer = new PdfWriter(content, font, boldFont); + writer.center("本人随同参加直系亲属单位疗休养活动暨", 18, 760, true); + writer.center("疗休养费用由我校按规定标准给予报销申请书", 18, 733, true); + writer.text("学校工会:", 15, 90, 680, false); + + List p1 = new ArrayList<>(); + p1.add(PdfSegment.text("我本人是")); + p1.add(PdfSegment.field(data.getString("unionName", ""), 54)); + p1.add(PdfSegment.text("的")); + p1.add(PdfSegment.field(data.getString("userName", ""), 54)); + p1.add(PdfSegment.text(",我申请随同参加本人直系亲属(亲属关系为")); + p1.add(PdfSegment.field(data.getString("relationshipName", ""), 42)); + p1.add(PdfSegment.text(")姓名")); + p1.add(PdfSegment.field(data.getString("relativeName", ""), 54)); + p1.add(PdfSegment.text("所在单位")); + p1.add(PdfSegment.field(data.getString("relativeUnitName", ""), 72)); + p1.add(PdfSegment.text("于")); + p1.add(PdfSegment.field(data.getString("travelStartYear", ""), 42)); + p1.add(PdfSegment.text("年")); + p1.add(PdfSegment.field(data.getString("travelStartMonth", ""), 28)); + p1.add(PdfSegment.text("月")); + p1.add(PdfSegment.field(data.getString("travelStartDay", ""), 28)); + p1.add(PdfSegment.text("日至")); + p1.add(PdfSegment.field(data.getString("travelEndYear", ""), 42)); + p1.add(PdfSegment.text("年")); + p1.add(PdfSegment.field(data.getString("travelEndMonth", ""), 28)); + p1.add(PdfSegment.text("月")); + p1.add(PdfSegment.field(data.getString("travelEndDay", ""), 28)); + p1.add(PdfSegment.text("日期间所开展的")); + p1.add(PdfSegment.field(data.getString("lineName", ""), 72)); + p1.add(PdfSegment.text("线路疗休养活动;同时,申请本人此次疗休养费用由我校按规定标准给予报销。")); + float paragraphEndY = writer.paragraph(p1, 90, 635, 420, 15, 22.5f, 30); + writer.text("特此申请,请批准。", 15, 90, paragraphEndY - 22.5f, false); + + float signX = 250; + float signY = paragraphEndY - 80; + writer.text("申请人签名:", 15, signX, signY, false); + writer.text(data.getString("userName", ""), 15, signX + 90, signY, false); + drawPlainDateLine(writer, data.getString("applyDate", ""), signX + 45, signY - 31, 15); + + signY -= 78; + writer.text("分工会主席签字(盖章):", 15, signX - 60, signY, false); + writer.text(data.getString("unionAuditorName", ""), 15, signX + 135, signY, false); + drawPlainDateLine(writer, data.getString("unionAuditDate", ""), signX + 45, signY - 31, 15); + + signY -= 78; + writer.text("学校工会:盖章", 15, signX + 20, signY, false); + drawPlainDateLine(writer, data.getString("schoolUnionAuditDate", ""), signX + 45, signY - 31, 15); + + signY -= 78; + writer.text("直系亲属所在单位工会:盖章", 15, signX - 35, signY, false); + drawPlainDateLine(writer, data.getString("relativeUnitAuditDate", ""), signX + 45, signY - 31, 15); + } + document.save(out); + return out.toByteArray(); + } catch (IOException e) { + throw new RuntimeException("导出PDF失败", e); + } + } + + private void drawDateLine(PdfWriter writer, String date, float x, float y) throws IOException { + writer.text("时间:", 14, x, y, false); + drawDate(writer, date, x + 48, y, 14); + } + + private void drawPlainDateLine(PdfWriter writer, String date, float x, float y, float size) throws IOException { + writer.text("时间:", size, x, y, false); + writer.text(normalizeDate(date), size, x + size * 3, y, false); + } + + private void drawDate(PdfWriter writer, String date, float x, float y, float size) throws IOException { + writer.field(parseDatePart(date, "year"), size, x, y, 42); + writer.text("年", size, x + 44, y, false); + writer.field(parseDatePart(date, "month"), size, x + 68, y, 28); + writer.text("月", size, x + 98, y, false); + writer.field(parseDatePart(date, "day"), size, x + 122, y, 28); + writer.text("日", size, x + 152, y, false); + } + + private PDType0Font loadChineseFont(PDDocument document, boolean bold) throws IOException { + String[] paths = bold + ? new String[]{"C:/Windows/Fonts/simhei.ttf", "C:/Windows/Fonts/msyhbd.ttf", "C:/Windows/Fonts/simsun.ttf"} + : new String[]{"C:/Windows/Fonts/simfang.ttf", "C:/Windows/Fonts/simsun.ttf", "C:/Windows/Fonts/msyh.ttf"}; + for (String path : paths) { + File file = new File(path); + if (file.exists()) { + return PDType0Font.load(document, file); + } + } + throw new IOException("未找到中文字体文件"); + } + + private static class PdfSegment { + private final String text; + private final boolean bold; + private final boolean underline; + private final float minWidth; + + private PdfSegment(String text, boolean bold, boolean underline, float minWidth) { + this.text = text == null ? "" : text; + this.bold = bold; + this.underline = underline; + this.minWidth = minWidth; + } + + private static PdfSegment text(String text) { + return new PdfSegment(text, false, false, 0); + } + + private static PdfSegment field(String text, float minWidth) { + return new PdfSegment(text, false, true, minWidth); + } + } + + private static class PdfWriter { + private final PDPageContentStream content; + private final PDType0Font font; + private final PDType0Font boldFont; + + private PdfWriter(PDPageContentStream content, PDType0Font font, PDType0Font boldFont) { + this.content = content; + this.font = font; + this.boldFont = boldFont; + } + + private void center(String text, float size, float y, boolean bold) throws IOException { + PDType0Font useFont = bold ? boldFont : font; + float width = useFont.getStringWidth(text) / 1000 * size; + text(text, size, (PDRectangle.A4.getWidth() - width) / 2, y, bold); + } + + private void text(String text, float size, float x, float y, boolean bold) throws IOException { + if (StrUtil.isBlank(text)) { + return; + } + content.beginText(); + content.setFont(bold ? boldFont : font, size); + content.newLineAtOffset(x, y); + content.showText(text); + content.endText(); + } + + private void field(String text, float size, float x, float y, float minWidth) throws IOException { + PDType0Font useFont = font; + float textWidth = StrUtil.isBlank(text) ? 0 : useFont.getStringWidth(text) / 1000 * size; + field(text, size, x, y, Math.max(minWidth, textWidth + 6), textWidth); + } + + private void field(String text, float size, float x, float y, float width, float textWidth) throws IOException { + content.moveTo(x, y - 3); + content.lineTo(x + width, y - 3); + content.stroke(); + if (StrUtil.isNotBlank(text)) { + text(text, size, x + Math.max(3, (width - textWidth) / 2), y, false); + } + } + + private float paragraph(List segments, float x, float y, float maxWidth, float size, float lineHeight, float firstLineIndent) throws IOException { + float cursorX = x + firstLineIndent; + float cursorY = y; + boolean lineStart = true; + for (PdfSegment segment : segments) { + if (segment.underline) { + float segmentWidth = fieldWidth(segment.text, size, segment.minWidth); + if (!lineStart && cursorX + segmentWidth > x + maxWidth) { + cursorX = x; + cursorY -= lineHeight; + lineStart = true; + } + if (lineStart && cursorX + segmentWidth > x + maxWidth && StrUtil.isNotBlank(segment.text)) { + segmentWidth = Math.max(segment.minWidth, x + maxWidth - cursorX); + } + float textWidth = StrUtil.isBlank(segment.text) ? 0 : font.getStringWidth(segment.text) / 1000 * size; + field(segment.text, size, cursorX, cursorY, segmentWidth, textWidth); + cursorX += segmentWidth; + lineStart = false; + continue; + } + for (int i = 0; i < segment.text.length(); i++) { + String ch = String.valueOf(segment.text.charAt(i)); + PDType0Font useFont = segment.bold ? boldFont : font; + float charWidth = useFont.getStringWidth(ch) / 1000 * size; + if (!lineStart && cursorX + charWidth > x + maxWidth) { + cursorX = x; + cursorY -= lineHeight; + lineStart = true; + } + text(ch, size, cursorX, cursorY, segment.bold); + cursorX += charWidth; + lineStart = false; + } + } + return cursorY; + } + + private float fieldWidth(String text, float size, float minWidth) throws IOException { + float textWidth = StrUtil.isBlank(text) ? 0 : font.getStringWidth(text) / 1000 * size; + return Math.max(minWidth, textWidth + 6); + } + } + + private String defaultIfBlank(String value, String defaultValue) { + return StrUtil.isBlank(value) ? defaultValue : value; + } + + private String currentJobNo() { + View_user user = tourLedgerService.dao().fetch(View_user.class, Cnd.where(View_user::getId, "=", SecurityUtil.getUserId())); + return user == null ? SecurityUtil.getUserLoginname() : defaultIfBlank(user.getLoginname(), SecurityUtil.getUserLoginname()); + } + + private String getOrderColumn(String orderName) { + if ("year".equals(orderName)) { + return "t.`year`"; + } + if ("jobNo".equals(orderName)) { + return "t.jobNo"; + } + if ("userName".equals(orderName)) { + return "t.userName"; + } + if ("lineName".equals(orderName)) { + return "t.lineName"; + } + if ("travelPeriod".equals(orderName)) { + return "COALESCE(m.travelStartTime, tp.travelPeriod)"; + } + if ("lineType".equals(orderName)) { + return "t.lineType"; + } + if ("signupTime".equals(orderName)) { + return "t.signupTime"; + } + if ("familyCount".equals(orderName)) { + return "familyCount"; + } + if ("joined".equals(orderName)) { + return "t.joined"; + } + if ("curTaskName".equals(orderName)) { + return "curTaskName"; + } + if ("instanceState".equals(orderName)) { + return "ins.state"; + } + return "t.signupTime"; + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourPlaceholderController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourPlaceholderController.java new file mode 100644 index 00000000..1d669e76 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourPlaceholderController.java @@ -0,0 +1,13 @@ +package com.budwk.app.zhgh.dayofficework.tour.controller; + +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.mvc.annotation.At; + +/** + * 分阶段建设的菜单占位入口。 + */ +@IocBean +@At("/platform/tour") +public class TourPlaceholderController { + +} 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 new file mode 100644 index 00000000..616fcbc1 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSchoolUnionApprovalController.java @@ -0,0 +1,580 @@ +package com.budwk.app.zhgh.dayofficework.tour.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.util.StrUtil; +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.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerDirectRelative; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerFamily; +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; +import org.nutz.dao.Cnd; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.dao.util.cri.SqlExpressionGroup; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.util.NutMap; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; + +import java.time.LocalDate; +import java.util.List; + +@IocBean +@Ok("json:full") +@At("/platform/tour/schoolUnionApproval") +public class TourSchoolUnionApprovalController { + + private static final String WORKFLOW_KEY = "LXYBZXQSXL"; + private static final String TASK_DISPLAY_NAME = "校工会审核"; + + @Inject + private TourLedgerService tourLedgerService; + + @Inject + private TourLedgerFamilyService tourLedgerFamilyService; + + @Inject + private TourLedgerDirectRelativeService tourLedgerDirectRelativeService; + + @At("") + @Ok("beetl:/platform/zhgh/dayofficework/Tour/schoolUnionApproval/index.html") + @SaCheckPermission("tour.schoolUnionApproval") + public void index() { + } + + @At("/h5") + @Ok("beetl:/platform/zhghh5/dayofficework/tour/schoolUnionApproval/index.html") + @SaCheckPermission("tour.schoolUnionApproval") + public void h5() { + } + + @At + @SaCheckPermission("tour.schoolUnionApproval") + public Result pageData(PageForm pageForm, Boolean audit, Integer startYear, Integer endYear, String keyword, String unionId, String lineId, String travelPeriod, String lineType) { + Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType); + + Sql countSql = Sqls.create(""" + SELECT COUNT(DISTINCT task.id) + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 + $condition + """); + countSql.setCondition(cnd); + countSql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(countSql); + int count = countSql.getInt(); + + Sql listSql = Sqls.create(""" + SELECT + t.*, + ins.id AS instanceId, + ins.businessNo, + ins.state AS instanceState, + ins.variable AS instanceVariable, + ins.processDefineId AS instanceProcessDefineId, + task.id AS taskId, + task.taskName AS taskKey, + task.displayName AS taskName, + task.taskType, + task.performType AS taskPerformType, + task.taskState, + task.finishTime, + task.taskParentId, + task.variable AS taskVariable, + IFNULL(GROUP_CONCAT(DISTINCT nt.displayName), '结束') AS curTaskName, + CASE + WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName) + ELSE CONCAT( + COALESCE(NULLIF(l.lineName, ''), t.lineName), + '(', + CASE + WHEN IFNULL(m.unionId, '') = '' THEN '校工会' + ELSE IFNULL(mu.name, '') + END, + ')' + ) + END AS currentLineName, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE tp.travelPeriod + END AS travelPeriod, + IFNULL(f.familyCount, 0) AS familyCount + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10 + LEFT JOIN ( + SELECT ledgerId, COUNT(1) AS familyCount + FROM tour_ledger_family + WHERE delFlag = 0 + 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_line l ON l.id = t.lineId AND l.delFlag = 0 + LEFT JOIN sys_union mu ON mu.id = m.unionId + LEFT JOIN ( + SELECT + `year`, + lineId, + GROUP_CONCAT( + DISTINCT CASE + WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> '' + THEN CONCAT(travelStartTime, ' 至 ', travelEndTime) + ELSE NULL + END + ORDER BY travelStartTime ASC + SEPARATOR ';' + ) AS travelPeriod + FROM tour_matter + WHERE delFlag = 0 + AND lineId IS NOT NULL + AND lineId <> '' + GROUP BY `year`, lineId + ) tp ON (t.matterId IS NULL OR t.matterId = '') AND tp.`year` = t.`year` AND tp.lineId = t.lineId + $condition + GROUP BY task.id + ORDER BY $orderColumn $orderBy, task.createdAt DESC, t.signupTime DESC, t.createdAt DESC + """); + listSql.setCondition(cnd); + listSql.setVar("orderColumn", getOrderColumn(pageForm.getPageOrderName())); + listSql.setVar("orderBy", "ascending".equals(pageForm.getPageOrderBy()) ? "asc" : "desc"); + listSql.setPager(tourLedgerService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize())); + listSql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(listSql); + + List list = listSql.getList(NutMap.class); + list.forEach(item -> item.put("lineName", item.getString("currentLineName"))); + Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list); + return Result.success(pagination); + } + + @At + @SaCheckPermission("tour.schoolUnionApproval") + public Result detail(String id) { + TourLedger ledger = fetchAuditLedger(id); + if (ledger == null) { + return Result.error("报名记录不存在或无权查看"); + } + ledger.setLineName(getCurrentLineName(ledger)); + Cnd familyCnd = Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()) + .and(TourLedgerFamily::getDelFlag, "=", false); + familyCnd.asc(TourLedgerFamily::getCreatedAt); + TourLedgerDirectRelative directRelative = tourLedgerDirectRelativeService.fetch( + Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()) + .and(TourLedgerDirectRelative::getDelFlag, "=", false)); + NutMap signupConfig = getSignupConfig(ledger); + return Result.success(NutMap.NEW() + .addv("ledger", ledger) + .addv("travelPeriod", getTravelPeriod(ledger)) + .addv("families", tourLedgerFamilyService.query(familyCnd)) + .addv("directRelative", directRelative) + .addv("allowFamily", signupConfig.getBoolean("allowFamily", false)) + .addv("fillBedInfo", signupConfig.getBoolean("fillBedInfo", true)) + .addv("directFamilyUnitLine", signupConfig.getBoolean("directFamilyUnitLine", false))); + } + + @At + @SaCheckPermission("tour.schoolUnionApproval") + public Result unionOptions(Boolean audit, Integer startYear, Integer endYear, String keyword, String lineId, String travelPeriod, String lineType) { + Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, null, lineId, travelPeriod, lineType); + cnd.and("t.unionId", "IS NOT", null); + cnd.and("t.unionId", "<>", ""); + + Sql sql = Sqls.create(""" + SELECT DISTINCT + t.unionId AS id, + COALESCE(NULLIF(u.name, ''), t.unionName) AS name, + u.unionCode + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 + LEFT JOIN sys_union u ON u.id = t.unionId + $condition + ORDER BY u.unionCode ASC, name ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission("tour.schoolUnionApproval") + public Result lineOptions(Boolean audit, Integer startYear, Integer endYear, String travelPeriod, String lineType, String unionId, String keyword) { + Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, unionId, null, travelPeriod, lineType); + cnd.and("t.lineName", "IS NOT", null); + cnd.and("t.lineName", "<>", ""); + + Sql sql = Sqls.create(""" + SELECT DISTINCT + CASE + WHEN IFNULL(t.lineId, '') <> '' THEN CONCAT(t.lineId, '|', IFNULL(m.unionId, '')) + ELSE CONCAT('legacy:', t.lineName, '|', IFNULL(m.unionId, '')) + END AS lineId, + CASE + WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName) + ELSE CONCAT( + COALESCE(NULLIF(l.lineName, ''), t.lineName), + '(', + CASE + WHEN IFNULL(m.unionId, '') = '' THEN '校工会' + ELSE IFNULL(mu.name, '') + END, + ')' + ) + END AS lineName + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 + LEFT JOIN sys_union mu ON mu.id = m.unionId + $condition + ORDER BY lineName ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission("tour.schoolUnionApproval") + public Result travelPeriodOptions(Boolean audit, Integer startYear, Integer endYear, String lineId, String lineType, String unionId, String keyword) { + Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, unionId, lineId, null, lineType); + cnd.and("m.travelStartTime", "IS NOT", null); + cnd.and("m.travelStartTime", "<>", ""); + cnd.and("m.travelEndTime", "IS NOT", null); + cnd.and("m.travelEndTime", "<>", ""); + + Sql sql = Sqls.create(""" + SELECT DISTINCT CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) AS travelPeriod + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 + $condition + ORDER BY travelPeriod ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission("tour.schoolUnionApproval") + public Result lineTypeOptions(Boolean audit, Integer startYear, Integer endYear, String lineId, String travelPeriod, String unionId, String keyword) { + Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, unionId, lineId, travelPeriod, null); + cnd.and("t.lineType", "IS NOT", null); + cnd.and("t.lineType", "<>", ""); + + Sql sql = Sqls.create(""" + SELECT DISTINCT t.lineType + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 + $condition + ORDER BY t.lineType ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + private Cnd buildAuditCnd(Boolean audit, Integer startYear, Integer endYear, String keyword, String unionId, String lineId, String travelPeriod, String lineType) { + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.and("def.name", "=", WORKFLOW_KEY); + cnd.and("task.displayName", "=", TASK_DISPLAY_NAME); + cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId())); + appendAuditStateFilter(cnd, audit); + cnd.andEX("t.`year`", ">=", startYear); + cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear); + cnd.andEX("t.unionId", "=", unionId); + cnd.andEX("t.lineType", "=", lineType); + appendLineFilter(cnd, lineId); + appendTravelPeriodFilter(cnd, travelPeriod); + appendKeywordFilter(cnd, keyword); + return cnd; + } + + private void appendAuditStateFilter(Cnd cnd, Boolean audit) { + if (Boolean.TRUE.equals(audit)) { + cnd.and("task.taskState", "in", List.of( + ProcessTaskStateEnum.FINISHED.getCode(), + ProcessTaskStateEnum.WITHDRAW.getCode(), + ProcessTaskStateEnum.INTERRUPT.getCode())); + return; + } + cnd.and("task.taskState", "=", ProcessTaskStateEnum.DOING.getCode()); + } + + private void appendKeywordFilter(Cnd cnd, String keyword) { + if (StrUtil.isBlank(keyword)) { + return; + } + SqlExpressionGroup group = new SqlExpressionGroup(); + group.orLike("t.userName", keyword.trim()); + group.orLike("t.jobNo", keyword.trim()); + group.orLike("t.lineName", keyword.trim()); + group.orLike("l.lineName", keyword.trim()); + cnd.and(group); + } + + private void appendTravelPeriodFilter(Cnd cnd, String travelPeriod) { + if (StrUtil.isBlank(travelPeriod)) { + return; + } + cnd.and("CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)", "=", travelPeriod); + } + + private void appendLineFilter(Cnd cnd, String lineId) { + if (StrUtil.isBlank(lineId)) { + return; + } + LineFilter lineFilter = parseLineFilter(lineId); + if (lineFilter.getLineId().startsWith("legacy:")) { + SqlExpressionGroup legacyGroup = new SqlExpressionGroup(); + legacyGroup.or("t.lineId", "IS", null); + legacyGroup.or("t.lineId", "=", ""); + cnd.and(legacyGroup); + cnd.and("t.lineName", "=", lineFilter.getLineId().substring("legacy:".length())); + } else { + cnd.and("t.lineId", "=", lineFilter.getLineId()); + } + appendMatterUnionFilter(cnd, lineFilter.getUnionId()); + } + + private LineFilter parseLineFilter(String value) { + int delimiterIndex = value.indexOf("|"); + if (delimiterIndex < 0) { + return new LineFilter(value, null); + } + return new LineFilter(value.substring(0, delimiterIndex), value.substring(delimiterIndex + 1)); + } + + private void appendMatterUnionFilter(Cnd cnd, String unionId) { + if (unionId == null) { + return; + } + if (StrUtil.isBlank(unionId)) { + SqlExpressionGroup group = new SqlExpressionGroup(); + group.or("m.unionId", "IS", null); + group.or("m.unionId", "=", ""); + cnd.and(group); + return; + } + cnd.and("m.unionId", "=", unionId); + } + + private String getOrderColumn(String orderName) { + if ("jobNo".equals(orderName)) { + return "t.jobNo"; + } + if ("userName".equals(orderName)) { + return "t.userName"; + } + if ("lineName".equals(orderName)) { + return """ + CASE + WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName) + ELSE CONCAT( + COALESCE(NULLIF(l.lineName, ''), t.lineName), + '(', + CASE + WHEN IFNULL(m.unionId, '') = '' THEN '校工会' + ELSE IFNULL(mu.name, '') + END, + ')' + ) + END + """; + } + if ("travelPeriod".equals(orderName)) { + return "COALESCE(m.travelStartTime, tp.travelPeriod)"; + } + if ("lineType".equals(orderName)) { + return "t.lineType"; + } + if ("signupTime".equals(orderName)) { + return "t.signupTime"; + } + if ("familyCount".equals(orderName)) { + return "familyCount"; + } + if ("instanceState".equals(orderName)) { + return "ins.state"; + } + return "task.createdAt"; + } + + private TourLedger fetchAuditLedger(String id) { + if (StrUtil.isBlank(id)) { + return null; + } + Sql sql = Sqls.create(""" + SELECT COUNT(1) + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + WHERE t.delFlag = 0 + AND t.id = @id + AND def.name = @workflowKey + AND task.displayName = @taskDisplayName + AND ta.actorId = @actorId + """); + sql.setParam("id", id); + sql.setParam("workflowKey", WORKFLOW_KEY); + sql.setParam("taskDisplayName", TASK_DISPLAY_NAME); + sql.setParam("actorId", SecurityUtil.getUserId()); + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt() > 0 ? tourLedgerService.fetch(id) : null; + } + + private String getCurrentLineName(TourLedger ledger) { + if (ledger == null) { + return ""; + } + Sql sql = Sqls.create(""" + SELECT CASE + WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName) + ELSE CONCAT( + COALESCE(NULLIF(l.lineName, ''), t.lineName), + '(', + CASE + WHEN IFNULL(m.unionId, '') = '' THEN '校工会' + ELSE IFNULL(mu.name, '') + END, + ')' + ) + END AS lineName + FROM tour_ledger t + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 + LEFT JOIN sys_union mu ON mu.id = m.unionId + WHERE t.id = @ledgerId + """); + sql.setParam("ledgerId", ledger.getId()); + sql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(sql); + return StrUtil.blankToDefault(sql.getString(), ledger.getLineName()); + } + + private String getTravelPeriod(TourLedger ledger) { + if (ledger == null || ledger.getYear() == null || StrUtil.isBlank(ledger.getLineId())) { + return ""; + } + if (StrUtil.isNotBlank(ledger.getMatterId())) { + Sql exactSql = Sqls.create(""" + SELECT CASE + WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> '' + THEN CONCAT(travelStartTime, ' 至 ', travelEndTime) + ELSE '' + END AS travelPeriod + FROM tour_matter + WHERE delFlag = 0 + AND id = @matterId + """); + exactSql.setParam("matterId", ledger.getMatterId()); + exactSql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(exactSql); + return exactSql.getString(); + } + Sql sql = Sqls.create(""" + SELECT GROUP_CONCAT( + DISTINCT CASE + WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> '' + THEN CONCAT(travelStartTime, ' 至 ', travelEndTime) + ELSE NULL + END + ORDER BY travelStartTime ASC + SEPARATOR ';' + ) AS travelPeriod + FROM tour_matter + WHERE delFlag = 0 + AND `year` = @year + AND lineId = @lineId + """); + sql.setParam("year", ledger.getYear()); + sql.setParam("lineId", ledger.getLineId()); + sql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(sql); + return sql.getString(); + } + + private NutMap getSignupConfig(TourLedger ledger) { + if (ledger == null || StrUtil.isBlank(ledger.getLineId())) { + return NutMap.NEW().addv("allowFamily", false).addv("fillBedInfo", true).addv("directFamilyUnitLine", false); + } + Sql sql = Sqls.create(""" + SELECT + IFNULL(s.allowFamily, 0) AS allowFamily, + IFNULL(s.fillBedInfo, 1) AS fillBedInfo, + IFNULL(l.directFamilyUnitLine, 0) AS directFamilyUnitLine + FROM tour_ledger t + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN tour_setting s ON s.id = m.settingId AND s.delFlag = 0 + LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 + WHERE t.id = @ledgerId + """); + sql.setParam("ledgerId", ledger.getId()); + sql.setCallback(Sqls.callback.map()); + tourLedgerService.dao().execute(sql); + NutMap map = sql.getObject(NutMap.class); + if (map == null || map.isEmpty()) { + return NutMap.NEW().addv("allowFamily", false).addv("fillBedInfo", true).addv("directFamilyUnitLine", false); + } + return map; + } + + private static class LineFilter { + private final String lineId; + private final String unionId; + + private LineFilter(String lineId, String unionId) { + this.lineId = lineId; + this.unionId = unionId; + } + + private String getLineId() { + return lineId; + } + + private String getUnionId() { + return unionId; + } + } +} 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 new file mode 100644 index 00000000..08dab7e9 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSettingController.java @@ -0,0 +1,246 @@ +package com.budwk.app.zhgh.dayofficework.tour.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.annotation.SLog; +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.zhgh.dayofficework.tour.models.TourSetting; +import com.budwk.app.zhgh.dayofficework.tour.models.TourSettingLot; +import com.budwk.app.zhgh.dayofficework.tour.service.TourSettingService; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.Cnd; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.json.Json; +import org.nutz.lang.Lang; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; +import org.nutz.mvc.annotation.Param; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +@IocBean +@Ok("json:full") +@At("/platform/tour/setting") +public class TourSettingController { + + @Inject + private TourSettingService tourSettingService; + + @At("") + @Ok("beetl:/platform/zhgh/dayofficework/Tour/setting/index.html") + @SaCheckPermission("tour.setting") + public void index() { + } + + @At + @SaCheckPermission("tour.setting") + public Result pageData(PageForm pageForm, Integer year, String configName) { + Cnd cnd = Cnd.NEW(); + cnd.andEX(TourSetting::getYear, "=", year); + cnd.and(Cnd.likeEX(TourSetting::getConfigName, configName)); + + cnd.desc(TourSetting::getYear).asc(TourSetting::getSortNo).desc(TourSetting::getCreatedAt); + Pagination pagination = tourSettingService.listPage( + pageForm.getPageNumber(), + pageForm.getPageSize(), + TourSetting.class, + cnd + ); + return Result.success(pagination); + } + + @At + @SaCheckPermission("tour.setting") + public Result detail(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("参数错误"); + } + TourSetting tourSetting = tourSettingService.fetch(id); + if (tourSetting == null) { + return Result.error("配置不存在"); + } + // 编辑页面需要一起带出标段,按标段值倒序保持与老疗休养配置一致。 + Cnd lotCnd = Cnd.NEW(); + lotCnd.desc(TourSettingLot::getLotValue); + tourSettingService.dao().fetchLinks(tourSetting, "lots", lotCnd); + return Result.success(tourSetting); + } + + @At + @SaCheckPermission("tour.setting") + public Result previousYearInfo(Integer year) { + if (year == null) { + return Result.error("请先选择年度"); + } + List settings = tourSettingService.query(Cnd.where(TourSetting::getYear, "=", year - 1) + .and(TourSetting::getDelFlag, "=", false) + .desc(TourSetting::getUpdatedAt) + .desc(TourSetting::getCreatedAt)); + if (Lang.isEmpty(settings)) { + return Result.error("未找到上一年度配置"); + } + TourSetting tourSetting = settings.get(0); + Cnd lotCnd = Cnd.NEW(); + lotCnd.desc(TourSettingLot::getLotValue); + tourSettingService.dao().fetchLinks(tourSetting, "lots", lotCnd); + return Result.success(tourSetting); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.setting") + @SLog(type = "tour", tag = "疗休养设置", msg = "保存疗休养配置") + public Result doSubmit(TourSetting tourSetting, + @Param(value = "lots") String lots, + @Param(value = "lotDeleteList") String[] lotDeleteList) { + Result checkResult = check(tourSetting); + if (checkResult != null) { + return checkResult; + } + + Cnd sameNameCnd = Cnd.where(TourSetting::getYear, "=", tourSetting.getYear()) + .and(TourSetting::getConfigName, "=", tourSetting.getConfigName()); + if (StrUtil.isNotBlank(tourSetting.getId())) { + sameNameCnd.and(TourSetting::getId, "<>", tourSetting.getId()); + } + if (tourSettingService.count(sameNameCnd) > 0) { + return Result.error("同年度下配置名称已存在"); + } + + // 布尔值给默认值,避免前端未传时出现空状态。 + if (tourSetting.getEnabled() == null) { + tourSetting.setEnabled(true); + } + if (tourSetting.getAllowFamily() == null) { + tourSetting.setAllowFamily(false); + } + if (tourSetting.getFillBedInfo() == null) { + tourSetting.setFillBedInfo(true); + } + if (StrUtil.isBlank(tourSetting.getOutProvinceRatioType())) { + tourSetting.setOutProvinceRatioType("当年报名人数"); + } + if (!"固定人数".equals(tourSetting.getOutProvinceRatioType())) { + tourSetting.setOutProvinceFixedPeople(0); + } + // 前端按项目既有约定把子表数组序列化提交,这里显式解析,避免自动绑定漏掉标段。 + if (StrUtil.isNotBlank(lots)) { + try { + tourSetting.setLots(Json.fromJsonAsList(TourSettingLot.class, lots)); + } catch (Exception e) { + return Result.error("标段值和标段费用必须为整数"); + } + } + tourSetting.setLots(normalizeLots(tourSetting)); + Result lotCheckResult = checkLots(tourSetting.getLots()); + if (lotCheckResult != null) { + return lotCheckResult; + } + if (StrUtil.isBlank(tourSetting.getId())) { + if (Lang.isEmpty(tourSetting.getLots())) { + tourSettingService.insert(tourSetting); + } else { + tourSettingService.insertWith(tourSetting, "lots"); + } + } else { + // 编辑时先处理页面删除的标段,再保存配置和当前标段行。 + if (Lang.isNotEmpty(lotDeleteList)) { + tourSettingService.dao().clear(TourSettingLot.class, Cnd.where(TourSettingLot::getId, "in", lotDeleteList)); + } + tourSettingService.updateIgnoreNull(tourSetting); + saveLots(tourSetting); + } + return Result.success(); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.setting") + @SLog(type = "tour", tag = "疗休养设置", msg = "删除疗休养配置") + public Result doDelete(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("参数错误"); + } + tourSettingService.dao().clear(TourSettingLot.class, Cnd.where(TourSettingLot::getSettingId, "=", id)); + tourSettingService.delete(id); + return Result.success(); + } + + private List normalizeLots(TourSetting tourSetting) { + if (tourSetting == null || Lang.isEmpty(tourSetting.getLots())) { + return Collections.emptyList(); + } + // 过滤前端空行,避免误写入只有ID或空字段的标段记录。 + return tourSetting.getLots().stream() + .filter(item -> item != null + && (StrUtil.isNotBlank(item.getLotName()) + || StrUtil.isNotBlank(item.getLotValue()) + || item.getActivityCost() != null)) + .peek(item -> item.setSettingId(tourSetting.getId())) + .collect(Collectors.toList()); + } + + private void saveLots(TourSetting tourSetting) { + List lots = tourSetting.getLots(); + if (Lang.isEmpty(lots)) { + return; + } + lots.forEach(item -> item.setSettingId(tourSetting.getId())); + tourSettingService.dao().insertOrUpdate(lots); + } + + private Result check(TourSetting tourSetting) { + if (tourSetting == null) { + return Result.error("参数错误"); + } + if (tourSetting.getYear() == null) { + return Result.error("年度不能为空"); + } + if (StrUtil.isBlank(tourSetting.getConfigName())) { + return Result.error("配置名称不能为空"); + } + if (tourSetting.getMinGroupPeople() != null && tourSetting.getMaxGroupPeople() != null + && tourSetting.getMinGroupPeople() > tourSetting.getMaxGroupPeople()) { + return Result.error("最少成团人数不能大于最多成团人数"); + } + if (tourSetting.getCycleStartYear() != null && tourSetting.getCycleEndYear() != null + && tourSetting.getCycleStartYear() > tourSetting.getCycleEndYear()) { + return Result.error("周期开始年度不能大于周期结束年度"); + } + if (StrUtil.isNotBlank(tourSetting.getOutProvinceRatioType()) + && !"当年参加人数".equals(tourSetting.getOutProvinceRatioType()) + && !"当年报名人数".equals(tourSetting.getOutProvinceRatioType()) + && !"可参加教职工人数".equals(tourSetting.getOutProvinceRatioType()) + && !"固定人数".equals(tourSetting.getOutProvinceRatioType())) { + return Result.error("省外人数占比类型不正确"); + } + if ("固定人数".equals(tourSetting.getOutProvinceRatioType()) + && (tourSetting.getOutProvinceFixedPeople() == null || tourSetting.getOutProvinceFixedPeople() < 0)) { + return Result.error("固定人数必须大于等于0"); + } + return null; + } + + private Result checkLots(List lots) { + if (Lang.isEmpty(lots)) { + return null; + } + for (int i = 0; i < lots.size(); i++) { + TourSettingLot lot = lots.get(i); + String rowNo = "第" + (i + 1) + "行"; + if (StrUtil.isBlank(lot.getLotValue()) || !lot.getLotValue().matches("^\\d+$")) { + return Result.error(rowNo + "标段值必须为整数"); + } + if (lot.getActivityCost() == null || lot.getActivityCost() < 0) { + return Result.error(rowNo + "标段费用必须为整数"); + } + } + return null; + } +} 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 new file mode 100644 index 00000000..9876437a --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSignupController.java @@ -0,0 +1,1486 @@ +package com.budwk.app.zhgh.dayofficework.tour.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.lang.Dict; +import cn.hutool.core.util.StrUtil; +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.constant.FlowConst; +import com.budwk.app.flow.engine.FlowEngine; +import com.budwk.app.flow.entity.ProcessInstance; +import com.budwk.app.flow.entity.ProcessTask; +import com.budwk.app.flow.enums.ProcessInstanceStateEnum; +import com.budwk.app.flow.enums.ProcessSubmitTypeEnum; +import com.budwk.app.flow.enums.ProcessTaskStateEnum; +import com.budwk.app.sys.models.Sys_dict; +import com.budwk.app.sys.models.Sys_union; +import com.budwk.app.sys.services.SysDictService; +import com.budwk.app.sys.views.View_user; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerDirectRelative; +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.service.TourLedgerDirectRelativeService; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService; +import com.budwk.app.zhgh.dayofficework.tour.service.TourMatterService; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.Cnd; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.json.Json; +import org.nutz.lang.Lang; +import org.nutz.lang.util.NutMap; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; +import org.nutz.mvc.annotation.Param; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +@IocBean +@Ok("json:full") +@At("/platform/tour/signup") +public class TourSignupController { + + private static final String DIRECT_FAMILY_WORKFLOW_KEY = "LXYBZXQSXL"; + + @Inject + private TourMatterService tourMatterService; + + @Inject + private TourLedgerService tourLedgerService; + + @Inject + private TourLedgerFamilyService tourLedgerFamilyService; + + @Inject + private TourLedgerDirectRelativeService tourLedgerDirectRelativeService; + + @Inject + private SysDictService sysDictService; + + @Inject + private ActivityBasicScopeService activityBasicScopeService; + + @Inject + private FlowEngine flowEngine; + + @At("") + @Ok("beetl:/platform/zhgh/dayofficework/Tour/signup/index.html") + @SaCheckPermission("tour.signup") + public void index() { + } + + @At("/h5") + @Ok("beetl:/platform/zhghh5/dayofficework/tour/signup/index.html") + @SaCheckPermission("tour.signup") + public void h5() { + } + + @At("/h5/serviceNotice") + @SaCheckPermission("tour.signup") + public Result h5ServiceNotice() { + int currentYear = LocalDate.now().getYear(); + Cnd cnd = Cnd.where(TourSetting::getDelFlag, "=", false) + .and(TourSetting::getEnabled, "=", true) + .and(TourSetting::getYear, "=", currentYear); + cnd.asc(TourSetting::getSortNo); + cnd.desc(TourSetting::getCreatedAt); + TourSetting setting = tourLedgerService.dao().fetch(TourSetting.class, cnd); + if (setting == null) { + return Result.success(NutMap.NEW() + .addv("year", currentYear) + .addv("configName", "") + .addv("serviceNotice", "")); + } + return Result.success(NutMap.NEW() + .addv("year", setting.getYear()) + .addv("configName", defaultIfBlank(setting.getConfigName(), "")) + .addv("serviceNotice", defaultIfBlank(setting.getServiceNotice(), ""))); + } + + @At("/h5/signup") + @Ok("beetl:/platform/zhghh5/dayofficework/tour/signup/list.html") + @SaCheckPermission("tour.signup") + public void h5Signup() { + } + + @At("/h5/apply") + @Ok("beetl:/platform/zhghh5/dayofficework/tour/signup/apply.html") + @SaCheckPermission("tour.signup") + public void h5Apply() { + } + + @At("/h5/lineInfo") + @Ok("beetl:/platform/zhghh5/dayofficework/tour/signup/lineInfo.html") + @SaCheckPermission("tour.signup") + public void h5LineInfo() { + } + + @At("/h5/pageData") + @SaCheckPermission("tour.signup") + public Result h5PageData(PageForm pageForm, Integer year, String lineName, String lineType, String unionId, Boolean directFamilyUnitLine) { + if (StrUtil.isBlank(pageForm.getPageOrderName())) { + pageForm.defaultSortAsc("lineName"); + } + Cnd cnd = buildQueryCnd(year, lineName, lineType, unionId); + cnd.andEX("l.directFamilyUnitLine", "=", directFamilyUnitLine); + String currentJobNo = currentJobNo(); + + Sql countSql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_matter m + INNER JOIN tour_line l ON l.id = m.lineId + LEFT JOIN sys_union u ON u.id = m.unionId + $condition + """); + countSql.setCondition(cnd); + countSql.setCallback(Sqls.callback.integer()); + tourMatterService.dao().execute(countSql); + int count = countSql.getInt(); + + Sql listSql = Sqls.create(""" + SELECT + m.id AS matterId, + m.`year`, + m.unionId, + COALESCE(u.name, '校工会') AS unionName, + m.lineId, + l.lineName, + l.lineType, + l.directFamilyUnitLine, + l.mobileThumb AS lineMobileThumb, + a.agencyName AS travelAgencyName, + a.contactPhone, + a.mobileThumb AS agencyMobileThumb, + lot.lotName, + m.travelStartTime, + m.travelEndTime, + IFNULL(sc.signupCount, 0) AS signupCount, + IFNULL(sc.familyCount, 0) AS familyCount, + my.ledgerId, + my.instanceId, + my.instanceState, + my.taskKey, + my.startTaskId, + IFNULL(my.canRevoke, 0) AS canRevoke, + CASE WHEN my.ledgerId IS NULL THEN 0 ELSE 1 END AS signed + FROM tour_matter m + INNER JOIN tour_line l ON l.id = m.lineId + LEFT JOIN sys_union u ON u.id = m.unionId + LEFT JOIN tour_travel_agency a ON a.id = l.travelAgencyId + LEFT JOIN tour_setting_lot lot ON lot.id = l.lotId + LEFT JOIN ( + SELECT + t.matterId, + COUNT(1) + SUM(IFNULL(f.familyCount, 0)) AS signupCount, + SUM(IFNULL(f.familyCount, 0)) AS familyCount + FROM tour_ledger t + LEFT JOIN ( + SELECT ledgerId, COUNT(1) AS familyCount + FROM tour_ledger_family + WHERE delFlag = 0 + GROUP BY ledgerId + ) f ON f.ledgerId = t.id + WHERE t.delFlag = 0 + AND t.matterId IS NOT NULL + AND t.matterId <> '' + GROUP BY t.matterId + ) sc ON sc.matterId = m.id + LEFT JOIN ( + SELECT + t.matterId, + t.jobNo, + MAX(t.id) AS ledgerId, + MAX(ins.id) AS instanceId, + MAX(ins.state) AS instanceState, + MAX(task.taskName) AS taskKey, + 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 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 + AND t.jobNo = @currentJobNo + AND t.matterId IS NOT NULL + AND t.matterId <> '' + GROUP BY t.matterId, t.jobNo + ) my ON my.matterId = m.id + $condition + ORDER BY $orderColumn $orderBy, m.`year` DESC, m.travelStartTime ASC, l.lineName ASC, m.createdAt DESC + """); + listSql.setCondition(cnd); + listSql.setParam("currentJobNo", currentJobNo); + listSql.setParam("abandonState", ProcessInstanceStateEnum.ABANDON.getCode()); + listSql.setVar("orderColumn", getOrderColumn(pageForm.getPageOrderName())); + listSql.setVar("orderBy", "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc"); + listSql.setPager(tourMatterService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize())); + listSql.setCallback(Sqls.callback.maps()); + tourMatterService.dao().execute(listSql); + + Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, listSql.getList(NutMap.class)); + return Result.success(pagination); + } + + @At + @SaCheckPermission("tour.signup") + public Result pageData(PageForm pageForm, Integer year, String lineName, String lineType, String unionId) { + if (StrUtil.isBlank(pageForm.getPageOrderName())) { + pageForm.defaultSortAsc("lineName"); + } + Cnd cnd = buildQueryCnd(year, lineName, lineType, unionId); + String currentJobNo = currentJobNo(); + + Sql countSql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_matter m + INNER JOIN tour_line l ON l.id = m.lineId + LEFT JOIN sys_union u ON u.id = m.unionId + $condition + """); + countSql.setCondition(cnd); + countSql.setCallback(Sqls.callback.integer()); + tourMatterService.dao().execute(countSql); + int count = countSql.getInt(); + + Sql listSql = Sqls.create(""" + SELECT + m.id AS matterId, + m.`year`, + m.matterName, + m.minGroupPeople, + m.maxGroupPeople, + m.unionId, + COALESCE(u.name, '校工会') AS unionName, + m.lineId, + l.lineName, + l.lineType, + l.directFamilyUnitLine, + IFNULL(lot.allowOverReimbursement, 0) AS allowOverReimbursement, + m.travelStartTime, + m.travelEndTime, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE '' + END AS travelPeriod, + IFNULL(sc.signupCount, 0) AS signupCount, + my.ledgerId, + my.instanceId, + my.instanceState, + my.taskKey, + my.startTaskId, + IFNULL(my.canRevoke, 0) AS canRevoke, + CASE WHEN my.ledgerId IS NULL THEN 0 ELSE 1 END AS signed + FROM tour_matter m + INNER JOIN tour_line l ON l.id = m.lineId + LEFT JOIN sys_union u ON u.id = m.unionId + LEFT JOIN tour_setting_lot lot ON lot.id = l.lotId + LEFT JOIN ( + SELECT t.matterId, SUM(1 + IFNULL(f.familyCount, 0)) AS signupCount + FROM tour_ledger t + LEFT JOIN ( + SELECT ledgerId, COUNT(1) AS familyCount + FROM tour_ledger_family + WHERE delFlag = 0 + GROUP BY ledgerId + ) f ON f.ledgerId = t.id + WHERE t.delFlag = 0 + AND t.matterId IS NOT NULL + AND t.matterId <> '' + GROUP BY t.matterId + ) sc ON sc.matterId = m.id + LEFT JOIN ( + SELECT + t.matterId, + t.jobNo, + MAX(t.id) AS ledgerId, + MAX(ins.id) AS instanceId, + MAX(ins.state) AS instanceState, + MAX(task.taskName) AS taskKey, + 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 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 + AND t.jobNo = @currentJobNo + AND t.matterId IS NOT NULL + AND t.matterId <> '' + GROUP BY t.matterId, t.jobNo + ) my ON my.matterId = m.id + $condition + ORDER BY $orderColumn $orderBy, m.`year` DESC, m.travelStartTime ASC, l.lineName ASC, m.createdAt DESC + """); + listSql.setCondition(cnd); + listSql.setParam("currentJobNo", currentJobNo); + listSql.setParam("abandonState", ProcessInstanceStateEnum.ABANDON.getCode()); + listSql.setVar("orderColumn", getOrderColumn(pageForm.getPageOrderName())); + listSql.setVar("orderBy", "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc"); + listSql.setPager(tourMatterService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize())); + listSql.setCallback(Sqls.callback.maps()); + tourMatterService.dao().execute(listSql); + + Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, listSql.getList(NutMap.class)); + return Result.success(pagination); + } + + @At + @SaCheckPermission("tour.signup") + public Result unionOptions() { + Cnd cnd = Cnd.NEW(); + cnd.asc("unionCode"); + cnd.asc("name"); + return Result.success(tourMatterService.dao().query(Sys_union.class, cnd)); + } + + @At + @SaCheckPermission("tour.signup") + public Result lineTypeOptions(Integer year) { + Cnd cnd = buildQueryCnd(year, null, null, null); + Sql sql = Sqls.create(""" + SELECT DISTINCT l.lineType + FROM tour_matter m + INNER JOIN tour_line l ON l.id = m.lineId + LEFT JOIN sys_union u ON u.id = m.unionId + $condition + ORDER BY l.lineType ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourMatterService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission("tour.signup") + public Result lineDetail(String lineId) { + if (StrUtil.isBlank(lineId)) { + return Result.error("参数错误"); + } + Sql sql = Sqls.create(""" + SELECT + l.*, + a.agencyName AS travelAgencyName, + a.contactPhone, + lot.lotName AS lotName + FROM tour_line l + LEFT JOIN tour_travel_agency a ON a.id = l.travelAgencyId + LEFT JOIN tour_setting_lot lot ON lot.id = l.lotId + WHERE l.delFlag = 0 + AND l.id = @lineId + """); + sql.setParam("lineId", lineId); + sql.setCallback(Sqls.callback.map()); + tourMatterService.dao().execute(sql); + NutMap line = sql.getObject(NutMap.class); + return line == null || line.isEmpty() ? Result.error("线路不存在") : Result.success(line); + } + + @At + @SaCheckPermission("tour.signup") + public Result signupDetail(String matterId) { + if (StrUtil.isBlank(matterId)) { + return Result.error("参数错误"); + } + Sql sql = Sqls.create(""" + SELECT + m.id AS matterId, + m.`year`, + m.matterName, + m.unionId AS matterUnionId, + m.travelStartTime, + m.travelEndTime, + m.contactName, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE '' + END AS travelPeriod, + l.id AS lineId, + l.lineName, + l.lineType, + l.directFamilyUnitLine, + l.travelAgencyId, + a.agencyName AS travelAgencyName, + IFNULL(lot.allowOverReimbursement, 0) AS allowOverReimbursement, + s.allowFamily, + IFNULL(s.fillBedInfo, 1) AS fillBedInfo + FROM tour_matter m + INNER JOIN tour_line l ON l.id = m.lineId + LEFT JOIN tour_travel_agency a ON a.id = l.travelAgencyId + LEFT JOIN tour_setting_lot lot ON lot.id = l.lotId + LEFT JOIN tour_setting s ON s.id = m.settingId + WHERE m.delFlag = 0 + AND m.enabled = 1 + AND m.id = @matterId + """); + sql.setParam("matterId", matterId); + sql.setCallback(Sqls.callback.map()); + tourMatterService.dao().execute(sql); + NutMap matter = sql.getObject(NutMap.class); + if (matter == null || matter.isEmpty()) { + return Result.error("报名线路不存在"); + } + View_user user = tourMatterService.dao().fetch(View_user.class, Cnd.where(View_user::getId, "=", SecurityUtil.getUserId())); + NutMap staff = NutMap.NEW() + .addv("jobNo", user == null ? SecurityUtil.getUserLoginname() : defaultIfBlank(user.getLoginname(), SecurityUtil.getUserLoginname())) + .addv("userName", user == null ? SecurityUtil.getUserUsername() : defaultIfBlank(user.getUsername(), SecurityUtil.getUserUsername())) + .addv("gender", user == null ? "" : defaultIfBlank(user.getSex(), "")) + .addv("idCard", user == null ? "" : defaultIfBlank(user.getIdCard(), "")) + .addv("mobile", user == null ? "" : defaultIfBlank(user.getMobile(), "")) + .addv("unitId", user == null ? SecurityUtil.getUnitId() : defaultIfBlank(user.getUnitId(), SecurityUtil.getUnitId())) + .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(), "")); + Cnd ledgerCnd = Cnd.where(TourLedger::getDelFlag, "=", false) + .and(TourLedger::getMatterId, "=", matter.getString("matterId")) + .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; + } + List families = Collections.emptyList(); + TourLedgerDirectRelative directRelative = null; + if (ledger != null) { + Cnd familyCnd = Cnd.where(TourLedgerFamily::getDelFlag, "=", false) + .and(TourLedgerFamily::getLedgerId, "=", ledger.getId()); + familyCnd.asc(TourLedgerFamily::getCreatedAt); + families = tourLedgerFamilyService.query(familyCnd); + directRelative = tourLedgerDirectRelativeService.fetch(Cnd.where(TourLedgerDirectRelative::getDelFlag, "=", false) + .and(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId())); + } + return Result.success(NutMap.NEW() + .addv("matter", matter) + .addv("staff", staff) + .addv("ledger", ledger) + .addv("process", getSignupProcessInfo(ledger)) + .addv("families", families) + .addv("directRelative", directRelative)); + } + + @At + @SaCheckPermission("tour.signup") + public Result signupEligibilityNotice(String matterId) { + if (StrUtil.isBlank(matterId)) { + return Result.error("参数错误"); + } + TourMatter matter = tourMatterService.fetch(matterId); + if (matter == null || Boolean.TRUE.equals(matter.getDelFlag()) || !Boolean.TRUE.equals(matter.getEnabled())) { + return Result.error("报名出行时段不存在或已停用"); + } + TourLine line = tourLedgerService.dao().fetch(TourLine.class, matter.getLineId()); + if (line == null || Boolean.TRUE.equals(line.getDelFlag()) || !Boolean.TRUE.equals(line.getEnabled())) { + return Result.error("报名线路不存在或已停用"); + } + TourSetting setting = tourLedgerService.dao().fetch(TourSetting.class, matter.getSettingId()); + if (setting == null || Boolean.TRUE.equals(setting.getDelFlag()) || !Boolean.TRUE.equals(setting.getEnabled())) { + return Result.error("报名事项配置不存在或已停用"); + } + Integer activityGroupId = parseInteger(setting.getActivityGroupId()); + if (activityGroupId == null || !activityBasicScopeService.isUserInGroup(activityGroupId, SecurityUtil.getUserId())) { + return Result.error("您不在本次疗休养报名范围内"); + } + TourLedger oldLedger = fetchCurrentUserMatterLedger(matter.getId()); + CycleAllowedTimes cycleAllowedTimes = calculateCycleAllowedTimes(matter, setting, oldLedger); + if (!cycleAllowedTimes.canApply()) { + return Result.success(NutMap.NEW() + .addv("canApply", false) + .addv("noticeRequired", true) + .addv("noticeType", "cycleAllowedTimes") + .addv("cycleStartYear", cycleAllowedTimes.startYear()) + .addv("cycleEndYear", cycleAllowedTimes.endYear()) + .addv("cycleAllowedTimes", cycleAllowedTimes.allowedTimes()) + .addv("cycleJoinedTimes", cycleAllowedTimes.joinedTimes()) + .addv("message", buildCycleAllowedTimesNotice(cycleAllowedTimes))); + } + CycleTotalCost cycleTotalCost = calculateCycleTotalCost(matter, setting, line, oldLedger); + if (!cycleTotalCost.canApply()) { + return Result.success(NutMap.NEW() + .addv("canApply", false) + .addv("noticeRequired", true) + .addv("noticeType", "cycleTotalCost") + .addv("cycleStartYear", cycleTotalCost.startYear()) + .addv("cycleEndYear", cycleTotalCost.endYear()) + .addv("cycleTotalCost", cycleTotalCost.totalCost()) + .addv("cycleJoinedCost", cycleTotalCost.joinedCost()) + .addv("currentLineCost", cycleTotalCost.currentCost()) + .addv("message", buildCycleTotalCostNotice(cycleTotalCost))); + } + if (!isOutProvinceLine(line.getLineType())) { + return Result.success(NutMap.NEW().addv("canApply", true).addv("noticeRequired", false)); + } + OutProvinceQuota quota = calculateOutProvinceQuota(matter, setting, oldLedger); + return Result.success(NutMap.NEW() + .addv("noticeRequired", true) + .addv("noticeType", "outProvinceQuota") + .addv("canApply", quota.canApply()) + .addv("ratioType", quota.ratioType()) + .addv("basePeople", quota.basePeople()) + .addv("totalSignupPeople", quota.totalSignupPeople()) + .addv("outProvinceSignupPeople", quota.outProvinceSignupPeople()) + .addv("allowPeople", quota.allowPeople()) + .addv("ratioText", quota.ratioText()) + .addv("message", buildOutProvinceQuotaNotice(quota))); + } + + @At + @SaCheckPermission("tour.signup") + public Result outProvinceQuotaNotice(String matterId) { + if (StrUtil.isBlank(matterId)) { + return Result.error("参数错误"); + } + TourMatter matter = tourMatterService.fetch(matterId); + if (matter == null || Boolean.TRUE.equals(matter.getDelFlag()) || !Boolean.TRUE.equals(matter.getEnabled())) { + return Result.error("报名出行时段不存在或已停用"); + } + TourLine line = tourLedgerService.dao().fetch(TourLine.class, matter.getLineId()); + if (line == null || Boolean.TRUE.equals(line.getDelFlag()) || !Boolean.TRUE.equals(line.getEnabled())) { + return Result.error("报名线路不存在或已停用"); + } + if (!isOutProvinceLine(line.getLineType())) { + return Result.success(NutMap.NEW().addv("canApply", true).addv("noticeRequired", false)); + } + TourSetting setting = tourLedgerService.dao().fetch(TourSetting.class, matter.getSettingId()); + if (setting == null || Boolean.TRUE.equals(setting.getDelFlag()) || !Boolean.TRUE.equals(setting.getEnabled())) { + return Result.error("报名事项配置不存在或已停用"); + } + Integer activityGroupId = parseInteger(setting.getActivityGroupId()); + if (activityGroupId == null || !activityBasicScopeService.isUserInGroup(activityGroupId, SecurityUtil.getUserId())) { + return Result.error("您不在本次疗休养报名范围内"); + } + TourLedger oldLedger = fetchCurrentUserMatterLedger(matter.getId()); + OutProvinceQuota quota = calculateOutProvinceQuota(matter, setting, oldLedger); + return Result.success(NutMap.NEW() + .addv("noticeRequired", true) + .addv("canApply", quota.canApply()) + .addv("ratioType", quota.ratioType()) + .addv("basePeople", quota.basePeople()) + .addv("totalSignupPeople", quota.totalSignupPeople()) + .addv("outProvinceSignupPeople", quota.outProvinceSignupPeople()) + .addv("allowPeople", quota.allowPeople()) + .addv("ratioText", quota.ratioText()) + .addv("message", buildOutProvinceQuotaNotice(quota))); + } + + @At + @SaCheckPermission("tour.signup") + public Result directRelativeOptions() { + List list = sysDictService.getSubListByCode("directRelative"); + if (list == null) { + return Result.success(Collections.emptyList()); + } + return Result.success(list.stream().filter(item -> !item.isDisabled()).collect(Collectors.toList())); + } + + @At + @SaCheckPermission("tour.signup") + public Result bedTypeOptions() { + List list = sysDictService.getSubListByCode("bedType"); + if (list == null) { + return Result.success(Collections.emptyList()); + } + return Result.success(list.stream().filter(item -> !item.isDisabled()).collect(Collectors.toList())); + } + + @At + @SaCheckPermission("tour.signup") + public Result familyRelationshipOptions() { + Sql sql = Sqls.create(""" + SELECT d.* + FROM sys_dict p + INNER JOIN sys_dict d ON d.path LIKE CONCAT(p.path, '%') + WHERE p.code = 'familyRelationship' + AND d.id <> p.id + AND d.disabled = 0 + AND d.hasChildren = 0 + ORDER BY d.path ASC, d.location ASC + """); + sql.setCallback(Sqls.callback.entities()); + sql.setEntity(tourLedgerService.dao().getEntity(Sys_dict.class)); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(Sys_dict.class)); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.signup") + public Result doSignup(TourLedger ledger, @Param("families") String families, @Param("directRelative") String directRelative) { + Result checkResult = checkSignup(ledger); + if (checkResult != null) { + return checkResult; + } + List familyList = parseFamilies(families); + Result familyResult = checkFamilies(familyList); + if (familyResult != null) { + return familyResult; + } + TourLedgerDirectRelative directRelativeInfo = parseDirectRelative(directRelative); + boolean update = StrUtil.isNotBlank(ledger.getId()); + TourLedger oldLedger = update ? tourLedgerService.fetch(ledger.getId()) : null; + if (update && oldLedger == null) { + return Result.error("报名记录不存在"); + } + if (update && !currentJobNo().equals(oldLedger.getJobNo())) { + return Result.error("不能修改他人的报名记录"); + } + TourMatter matter = tourMatterService.fetch(ledger.getMatterId()); + Result ruleResult = checkSignupRule(ledger, matter, update ? oldLedger : null); + if (ruleResult != null) { + return ruleResult; + } + if (!update) { + oldLedger = fetchCurrentUserMatterLedger(ledger.getMatterId()); + if (oldLedger != null) { + ledger.setId(oldLedger.getId()); + update = true; + } + } + Result maxPeopleResult = checkMaxGroupPeople(matter, oldLedger, familyList); + if (maxPeopleResult != null) { + return maxPeopleResult; + } + 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()); + if (setting != null && Boolean.FALSE.equals(setting.getFillBedInfo())) { + clearBedInfo(ledger, familyList); + } + if (!allowOverCostReimbursement(line)) { + ledger.setOverCostReimbursed(false); + } + boolean overCostReimbursed = Boolean.TRUE.equals(ledger.getOverCostReimbursed()); + boolean approvalRequired = directFamilyLine || overCostReimbursed; + Result directRelativeResult = checkDirectRelative(directFamilyLine, directRelativeInfo); + if (directRelativeResult != null) { + return directRelativeResult; + } + if (update) { + Result workflowCheckResult = checkSignupApprovalWorkflowCanSubmit(ledger.getId()); + if (workflowCheckResult != null) { + return workflowCheckResult; + } + } + ledger.setYear(matter.getYear()); + ledger.setHasFamily(Lang.isNotEmpty(familyList)); + if (!update) { + ledger.setJoined(false); + ledger.setReimbursed(false); + ledger.setSignupTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + } else { + ledger.setJoined(oldLedger.getJoined()); + ledger.setReimbursed(oldLedger.getReimbursed()); + ledger.setSignupTime(defaultIfBlank(oldLedger.getSignupTime(), ledger.getSignupTime())); + } + ledger.setOverCostReimbursed(Boolean.TRUE.equals(ledger.getOverCostReimbursed())); + fillStaffInfo(ledger); + + if (update) { + tourLedgerService.updateIgnoreNull(ledger); + tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId())); + tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId())); + } else { + tourLedgerService.insert(ledger); + } + if (Lang.isNotEmpty(familyList)) { + familyList.forEach(item -> { + item.setLedgerId(ledger.getId()); + item.setStaffJobNo(ledger.getJobNo()); + item.setStaffName(ledger.getUserName()); + }); + tourLedgerFamilyService.insert(familyList); + } + if (directFamilyLine) { + directRelativeInfo.setId(null); + directRelativeInfo.setLedgerId(ledger.getId()); + directRelativeInfo.setLineId(line.getId()); + tourLedgerDirectRelativeService.insert(directRelativeInfo); + } + if (approvalRequired) { + Result workflowResult = startOrContinueSignupApprovalWorkflow(ledger, directFamilyLine ? directRelativeInfo : null, matter, line); + if (workflowResult != null) { + return workflowResult; + } + } + return Result.success().addMsg(approvalRequired ? (update ? "修改已提交,等待审核" : "申请已提交,等待审核") : (update ? "修改成功" : "报名成功")); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.signup") + public Result doCancelSignup(String ledgerId, String matterId) { + TourLedger ledger = null; + if (StrUtil.isNotBlank(ledgerId)) { + ledger = tourLedgerService.fetch(ledgerId); + } + if (ledger == null && StrUtil.isNotBlank(matterId)) { + Cnd cancelCnd = Cnd.where(TourLedger::getDelFlag, "=", false) + .and(TourLedger::getMatterId, "=", matterId) + .and(TourLedger::getJobNo, "=", currentJobNo()); + cancelCnd.desc(TourLedger::getCreatedAt); + ledger = tourLedgerService.fetch(cancelCnd); + } + if (ledger == null || Boolean.TRUE.equals(ledger.getDelFlag())) { + return Result.error("报名记录不存在"); + } + if (!currentJobNo().equals(ledger.getJobNo())) { + 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())); + return Result.success().addMsg("取消报名成功"); + } + + private Cnd buildQueryCnd(Integer year, String lineName, String lineType, String unionId) { + Cnd cnd = Cnd.NEW(); + cnd.and("m.delFlag", "=", false); + cnd.and("m.enabled", "=", true); + cnd.and("m.lineId", "IS NOT", null); + cnd.and("m.lineId", "<>", ""); + cnd.and("l.delFlag", "=", false); + cnd.and("l.enabled", "=", true); + cnd.andEX("m.`year`", "=", year == null ? LocalDate.now().getYear() : year); + cnd.and(Cnd.likeEX("l.lineName", lineName)); + cnd.andEX("l.lineType", "=", lineType); + cnd.andEX("m.unionId", "=", unionId); + return cnd; + } + + private Result checkSignup(TourLedger ledger) { + if (ledger == null) { + return Result.error("参数错误"); + } + if (ledger.getYear() == null) { + return Result.error("年度不能为空"); + } + if (StrUtil.isBlank(ledger.getLineId())) { + return Result.error("报名线路不能为空"); + } + if (StrUtil.isBlank(ledger.getMatterId())) { + return Result.error("报名出行时段不能为空"); + } + if (StrUtil.isBlank(ledger.getLineName())) { + return Result.error("报名线路不能为空"); + } + return null; + } + + private Result checkDirectRelative(boolean directFamilyLine, TourLedgerDirectRelative directRelativeInfo) { + if (!directFamilyLine) { + return null; + } + if (directRelativeInfo == null) { + return Result.error("请填写直系亲属线路信息"); + } + if (StrUtil.isBlank(directRelativeInfo.getRelativeName())) { + return Result.error("亲属姓名不能为空"); + } + if (StrUtil.isBlank(directRelativeInfo.getUnitName())) { + return Result.error("亲属所在单位不能为空"); + } + if (StrUtil.isBlank(directRelativeInfo.getRelationshipCode())) { + return Result.error("亲属关系不能为空"); + } + if (StrUtil.isBlank(directRelativeInfo.getLineName())) { + return Result.error("直系亲属线路名称不能为空"); + } + if (StrUtil.isBlank(directRelativeInfo.getTravelStartTime())) { + return Result.error("出行开始日期不能为空"); + } + if (StrUtil.isBlank(directRelativeInfo.getTravelEndTime())) { + return Result.error("出行结束日期不能为空"); + } + List directRelativeOptions = sysDictService.getSubListByCode("directRelative"); + Sys_dict dict = directRelativeOptions == null ? null : directRelativeOptions.stream() + .filter(item -> directRelativeInfo.getRelationshipCode().equals(item.getCode()) && !item.isDisabled()) + .findFirst() + .orElse(null); + if (dict == null) { + return Result.error("亲属关系无效"); + } + directRelativeInfo.setRelationshipName(defaultIfBlank(dict.getName(), directRelativeInfo.getRelationshipName())); + return null; + } + + private Result checkFamilies(List familyList) { + if (Lang.isEmpty(familyList)) { + return null; + } + for (TourLedgerFamily family : familyList) { + if (family == null) { + continue; + } + if (StrUtil.isBlank(family.getFamilyName()) || StrUtil.isBlank(family.getGender()) + || StrUtil.isBlank(family.getIdCard()) || StrUtil.isBlank(family.getRelationship())) { + return Result.error("请完善亲属姓名、性别、身份证号码和关系"); + } + String idCard = family.getIdCard().trim().toUpperCase(); + if (!isValidIdCard(idCard)) { + return Result.error("请输入正确的身份证号码"); + } + family.setIdCard(idCard); + } + return null; + } + + private Result checkMaxGroupPeople(TourMatter matter, TourLedger oldLedger, List familyList) { + if (matter == null || matter.getMaxGroupPeople() == null || matter.getMaxGroupPeople() <= 0) { + return null; + } + int currentPeople = 1 + (Lang.isEmpty(familyList) ? 0 : familyList.size()); + int signedPeople = countSignupPeople(matter.getId(), oldLedger == null ? null : oldLedger.getId()); + int totalPeople = signedPeople + currentPeople; + if (totalPeople > matter.getMaxGroupPeople()) { + return Result.error("当前报名人数 " + signedPeople + " 人,本次报名 " + currentPeople + + " 人,最多成团人数 " + matter.getMaxGroupPeople() + " 人,报名后将超过人数上限,不能提交"); + } + return null; + } + + 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]$"); + } + + private Result checkSignupRule(TourLedger ledger, TourMatter matter, TourLedger oldLedger) { + if (matter == null || Boolean.TRUE.equals(matter.getDelFlag()) || !Boolean.TRUE.equals(matter.getEnabled())) { + return Result.error("报名出行时段不存在或已停用"); + } + if (!ledger.getLineId().equals(matter.getLineId())) { + return Result.error("报名线路与出行时段不匹配"); + } + TourLine line = tourLedgerService.dao().fetch(TourLine.class, matter.getLineId()); + if (line == null || Boolean.TRUE.equals(line.getDelFlag()) || !Boolean.TRUE.equals(line.getEnabled())) { + return Result.error("报名线路不存在或已停用"); + } + TourSetting setting = tourLedgerService.dao().fetch(TourSetting.class, matter.getSettingId()); + if (setting == null || Boolean.TRUE.equals(setting.getDelFlag()) || !Boolean.TRUE.equals(setting.getEnabled())) { + return Result.error("报名事项配置不存在或已停用"); + } + Integer activityGroupId = parseInteger(setting.getActivityGroupId()); + if (activityGroupId == null || !activityBasicScopeService.isUserInGroup(activityGroupId, SecurityUtil.getUserId())) { + return Result.error("您不在本次疗休养报名范围内"); + } + if (StrUtil.isBlank(matter.getSignupStartTime()) || StrUtil.isBlank(matter.getSignupEndTime())) { + return Result.error("报名时间未配置"); + } + String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + if (now.compareTo(normalizeDateTime(matter.getSignupStartTime(), false)) < 0) { + return Result.error("报名未开始"); + } + if (now.compareTo(normalizeDateTime(matter.getSignupEndTime(), true)) > 0) { + return Result.error("报名已结束"); + } + CycleAllowedTimes cycleAllowedTimes = calculateCycleAllowedTimes(matter, setting, oldLedger); + if (!cycleAllowedTimes.canApply()) { + return Result.error(buildCycleAllowedTimesNotice(cycleAllowedTimes)); + } + CycleTotalCost cycleTotalCost = calculateCycleTotalCost(matter, setting, line, oldLedger); + if (!cycleTotalCost.canApply()) { + return Result.error(buildCycleTotalCostNotice(cycleTotalCost)); + } + String jobNo = currentJobNo(); + if (isOutProvinceLine(line.getLineType())) { + int startYear = setting.getCycleStartYear() == null ? matter.getYear() : setting.getCycleStartYear(); + int endYear = LocalDate.now().getYear(); + Cnd outProvinceCnd = Cnd.where(TourLedger::getDelFlag, "=", false) + .and(TourLedger::getJobNo, "=", jobNo) + .and(TourLedger::getYear, ">=", startYear) + .and(TourLedger::getYear, "<=", endYear) + .and(TourLedger::getLineType, "in", List.of("省外线路", "省外")); + if (oldLedger != null) { + outProvinceCnd.and(TourLedger::getId, "<>", oldLedger.getId()); + } + if (tourLedgerService.count(outProvinceCnd) > 0) { + return Result.error("您已参加过省外线路,不能报名省外线路,请选择省内线路"); + } + OutProvinceQuota quota = calculateOutProvinceQuota(matter, setting, oldLedger); + if (!quota.canApply()) { + return Result.error(buildOutProvinceQuotaNotice(quota)); + } + Cnd sameYearDomesticCnd = Cnd.where(TourLedger::getDelFlag, "=", false) + .and(TourLedger::getJobNo, "=", jobNo) + .and(TourLedger::getYear, "=", matter.getYear()) + .and(TourLedger::getLineType, "in", List.of("省内线路", "省内", "国内线路", "国内")); + if (oldLedger != null) { + sameYearDomesticCnd.and(TourLedger::getId, "<>", oldLedger.getId()); + } + TourLedger existingDomesticLedger = fetchLatestLedger(sameYearDomesticCnd); + if (existingDomesticLedger != null) { + return Result.error(buildSelectedLineMessage(existingDomesticLedger)); + } + } + if (isInProvinceLine(line.getLineType())) { + Cnd sameYearCnd = Cnd.where(TourLedger::getDelFlag, "=", false) + .and(TourLedger::getJobNo, "=", jobNo) + .and(TourLedger::getYear, "=", matter.getYear()); + if (oldLedger != null) { + sameYearCnd.and(TourLedger::getId, "<>", oldLedger.getId()); + } + TourLedger existingLedger = fetchLatestLedger(sameYearCnd); + if (existingLedger != null) { + return Result.error(buildSelectedLineMessage(existingLedger)); + } + } + return null; + } + + private TourLedger fetchLatestLedger(Cnd cnd) { + cnd.desc(TourLedger::getSignupTime); + cnd.desc(TourLedger::getCreatedAt); + return tourLedgerService.fetch(cnd); + } + + private TourLedger fetchCurrentUserMatterLedger(String matterId) { + if (StrUtil.isBlank(matterId)) { + return null; + } + Cnd cnd = Cnd.where(TourLedger::getDelFlag, "=", false) + .and(TourLedger::getMatterId, "=", matterId) + .and(TourLedger::getJobNo, "=", currentJobNo()); + cnd.desc(TourLedger::getCreatedAt); + return tourLedgerService.fetch(cnd); + } + + private CycleAllowedTimes calculateCycleAllowedTimes(TourMatter matter, TourSetting setting, TourLedger oldLedger) { + int allowedTimes = setting == null || setting.getCycleAllowedTimes() == null ? 0 : setting.getCycleAllowedTimes(); + int endYear = LocalDate.now().getYear(); + int startYear = setting == null || setting.getCycleStartYear() == null ? (matter == null || matter.getYear() == null ? endYear : matter.getYear()) : setting.getCycleStartYear(); + String excludeLedgerId = oldLedger == null ? null : oldLedger.getId(); + int joinedTimes = allowedTimes <= 0 ? 0 : countCycleJoinedTimes(currentJobNo(), startYear, endYear, excludeLedgerId); + return new CycleAllowedTimes(startYear, endYear, allowedTimes, joinedTimes); + } + + private int countCycleJoinedTimes(String jobNo, int startYear, int endYear, String excludeLedgerId) { + if (StrUtil.isBlank(jobNo)) { + return 0; + } + String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND id <> @excludeLedgerId"; + Sql sql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_ledger + WHERE delFlag = 0 + AND joined = 1 + AND jobNo = @jobNo + AND `year` >= @startYear + AND `year` <= @endYear + """ + excludeSql); + sql.setParam("jobNo", jobNo); + sql.setParam("startYear", startYear); + sql.setParam("endYear", endYear); + if (StrUtil.isNotBlank(excludeLedgerId)) { + sql.setParam("excludeLedgerId", excludeLedgerId); + } + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private String buildCycleAllowedTimesNotice(CycleAllowedTimes cycleAllowedTimes) { + return "您在 " + cycleAllowedTimes.startYear() + " 至 " + cycleAllowedTimes.endYear() + + " 周期内已参加 " + cycleAllowedTimes.joinedTimes() + " 次,周期允许 " + + cycleAllowedTimes.allowedTimes() + " 次,不能报名"; + } + + private CycleTotalCost calculateCycleTotalCost(TourMatter matter, TourSetting setting, TourLine line, TourLedger oldLedger) { + int totalCost = setting == null || setting.getCycleTotalCost() == null ? 0 : setting.getCycleTotalCost(); + int endYear = LocalDate.now().getYear(); + int startYear = setting == null || setting.getCycleStartYear() == null ? (matter == null || matter.getYear() == null ? endYear : matter.getYear()) : setting.getCycleStartYear(); + String excludeLedgerId = oldLedger == null ? null : oldLedger.getId(); + int joinedCost = totalCost <= 0 ? 0 : countCycleJoinedCost(currentJobNo(), startYear, endYear, excludeLedgerId); + int currentCost = totalCost <= 0 ? 0 : getLineActivityCost(line); + return new CycleTotalCost(startYear, endYear, totalCost, joinedCost, currentCost); + } + + private int countCycleJoinedCost(String jobNo, int startYear, int endYear, String excludeLedgerId) { + if (StrUtil.isBlank(jobNo)) { + return 0; + } + String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND t.id <> @excludeLedgerId"; + Sql sql = Sqls.create(""" + SELECT IFNULL(SUM(IFNULL(lot.activityCost, 0)), 0) + FROM tour_ledger t + LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 + LEFT JOIN tour_setting_lot lot ON lot.id = l.lotId AND lot.delFlag = 0 + WHERE t.delFlag = 0 + AND t.joined = 1 + AND t.jobNo = @jobNo + AND t.`year` >= @startYear + AND t.`year` <= @endYear + """ + excludeSql); + sql.setParam("jobNo", jobNo); + sql.setParam("startYear", startYear); + sql.setParam("endYear", endYear); + if (StrUtil.isNotBlank(excludeLedgerId)) { + sql.setParam("excludeLedgerId", excludeLedgerId); + } + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private int getLineActivityCost(TourLine line) { + if (line == null || StrUtil.isBlank(line.getLotId())) { + return 0; + } + Sql sql = Sqls.create(""" + SELECT IFNULL(activityCost, 0) AS activityCost + FROM tour_setting_lot + WHERE delFlag = 0 + AND id = @lotId + """); + sql.setParam("lotId", line.getLotId()); + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private String buildCycleTotalCostNotice(CycleTotalCost cycleTotalCost) { + return "您在 " + cycleTotalCost.startYear() + " 至 " + cycleTotalCost.endYear() + + " 周期内已参加线路费用合计 " + cycleTotalCost.joinedCost() + + " 元,本次线路费用 " + cycleTotalCost.currentCost() + + " 元,周期内总费用 " + cycleTotalCost.totalCost() + " 元,超过限制,不能报名"; + } + + private OutProvinceQuota calculateOutProvinceQuota(TourMatter matter, TourSetting setting, TourLedger oldLedger) { + String ratioType = normalizeOutProvinceRatioType(setting == null ? null : setting.getOutProvinceRatioType()); + BigDecimal originalRatio = setting == null ? null : setting.getOutProvinceRatio(); + boolean oneThirdRatio = isOneThirdRatio(originalRatio); + BigDecimal ratio = oneThirdRatio ? BigDecimal.ONE.divide(BigDecimal.valueOf(3), 12, RoundingMode.HALF_UP) + : (originalRatio == null ? BigDecimal.ZERO : originalRatio); + int year = matter == null || matter.getYear() == null ? LocalDate.now().getYear() : matter.getYear(); + String excludeLedgerId = oldLedger == null ? null : oldLedger.getId(); + int totalSignupPeople = countYearSignupPeople(year, excludeLedgerId); + int outProvinceSignupPeople = countYearOutProvinceSignupPeople(year, excludeLedgerId); + int basePeople; + if ("可参加教职工人数".equals(ratioType)) { + Integer groupId = parseInteger(setting == null ? null : setting.getActivityGroupId()); + basePeople = countActivityGroupPeople(groupId); + } else if ("固定人数".equals(ratioType)) { + basePeople = setting == null || setting.getOutProvinceFixedPeople() == null ? 0 : setting.getOutProvinceFixedPeople(); + } else { + basePeople = totalSignupPeople; + } + int quotaPeople = oneThirdRatio + ? basePeople / 3 + : ratio.multiply(BigDecimal.valueOf(basePeople)).setScale(0, RoundingMode.FLOOR).intValue(); + int allowPeople = quotaPeople - outProvinceSignupPeople; + return new OutProvinceQuota(ratioType, ratio, basePeople, totalSignupPeople, outProvinceSignupPeople, allowPeople); + } + + private boolean isOneThirdRatio(BigDecimal ratio) { + if (ratio == null) { + return false; + } + return ratio.compareTo(BigDecimal.valueOf(0.33)) >= 0 && ratio.compareTo(BigDecimal.valueOf(0.3333)) <= 0; + } + + private String normalizeOutProvinceRatioType(String ratioType) { + if ("当年参加人数".equals(ratioType) || StrUtil.isBlank(ratioType)) { + return "当年报名人数"; + } + if ("可参加教职工人数".equals(ratioType) || "固定人数".equals(ratioType)) { + return ratioType; + } + return "当年报名人数"; + } + + private String buildOutProvinceQuotaNotice(OutProvinceQuota quota) { + String suffix = quota.canApply() ? "您可以报名,或您稍后报名" : "请稍后报名"; + if ("当年报名人数".equals(quota.ratioType())) { + return "当前报名人数是" + quota.totalSignupPeople() + "人,已经报名省外线路人数是" + + quota.outProvinceSignupPeople() + "人,按照省外人数不超过" + quota.ratioText() + + "的规则,当前时间允许报名人数是" + quota.allowPeople() + "人," + suffix; + } + if ("可参加教职工人数".equals(quota.ratioType())) { + return "今年有权限报名人数是" + quota.basePeople() + "人,已经报名省外线路人数是" + + quota.outProvinceSignupPeople() + "人,按照省外人数不超过" + quota.ratioText() + + "的规则,当前时间允许报名人数是" + quota.allowPeople() + "人," + suffix; + } + return "按照省外人数不超过" + quota.ratioText() + + "的规则,当前时间允许报名人数是" + quota.allowPeople() + "人," + suffix; + } + + private int countYearSignupPeople(int year, String excludeLedgerId) { + String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND t.id <> @excludeLedgerId"; + Sql sql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id + WHERE t.delFlag = 0 + AND t.`year` = @year + AND (ins.id IS NULL OR ins.state = @finishedState) + """ + excludeSql); + sql.setParam("year", year); + sql.setParam("finishedState", ProcessInstanceStateEnum.FINISHED.getCode()); + if (StrUtil.isNotBlank(excludeLedgerId)) { + sql.setParam("excludeLedgerId", excludeLedgerId); + } + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private int countYearOutProvinceSignupPeople(int year, String excludeLedgerId) { + String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND t.id <> @excludeLedgerId"; + Sql sql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id + WHERE t.delFlag = 0 + AND t.`year` = @year + AND t.lineType IN ('省外线路', '省外') + AND (ins.id IS NULL OR ins.state = @finishedState) + """ + excludeSql); + sql.setParam("year", year); + sql.setParam("finishedState", ProcessInstanceStateEnum.FINISHED.getCode()); + if (StrUtil.isNotBlank(excludeLedgerId)) { + sql.setParam("excludeLedgerId", excludeLedgerId); + } + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private int countActivityGroupPeople(Integer groupId) { + if (groupId == null) { + return 0; + } + Sql sql = Sqls.create("SELECT COUNT(1) FROM (" + activityBasicScopeService.buildGroupUserIdSubSqlText(groupId) + ") scope_users"); + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private int countSignupPeople(String matterId, String excludeLedgerId) { + if (StrUtil.isBlank(matterId)) { + return 0; + } + String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND t.id <> @excludeLedgerId"; + Sql sql = Sqls.create(""" + SELECT IFNULL(SUM(1 + IFNULL(f.familyCount, 0)), 0) AS signupPeople + FROM tour_ledger t + LEFT JOIN ( + SELECT ledgerId, COUNT(1) AS familyCount + FROM tour_ledger_family + WHERE delFlag = 0 + GROUP BY ledgerId + ) f ON f.ledgerId = t.id + WHERE t.delFlag = 0 + AND t.matterId = @matterId + """ + excludeSql); + sql.setParam("matterId", matterId); + if (StrUtil.isNotBlank(excludeLedgerId)) { + sql.setParam("excludeLedgerId", excludeLedgerId); + } + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private String buildSelectedLineMessage(TourLedger ledger) { + String selectedLineName = getLedgerLineName(ledger); + return "您已经选择【" + selectedLineName + "】,取消后再重新选择。"; + } + + private String getLedgerLineName(TourLedger ledger) { + if (ledger == null) { + return ""; + } + if (StrUtil.isNotBlank(ledger.getLineName())) { + return ledger.getLineName(); + } + if (StrUtil.isBlank(ledger.getLineId())) { + return ""; + } + TourLine line = tourLedgerService.dao().fetch(TourLine.class, ledger.getLineId()); + return line == null ? "" : defaultIfBlank(line.getLineName(), ""); + } + + private record CycleAllowedTimes(int startYear, + int endYear, + int allowedTimes, + int joinedTimes) { + boolean canApply() { + return allowedTimes <= 0 || joinedTimes < allowedTimes; + } + } + + private record CycleTotalCost(int startYear, + int endYear, + int totalCost, + int joinedCost, + int currentCost) { + boolean canApply() { + return totalCost <= 0 || joinedCost + currentCost <= totalCost; + } + } + + private boolean isOutProvinceLine(String lineType) { + return "省外线路".equals(lineType) || "省外".equals(lineType); + } + + private boolean isInProvinceLine(String lineType) { + return "省内线路".equals(lineType) || "省内".equals(lineType); + } + + private boolean allowOverCostReimbursement(TourLine line) { + if (line == null || StrUtil.isBlank(line.getLotId())) { + return false; + } + Sql sql = Sqls.create(""" + SELECT IFNULL(allowOverReimbursement, 0) AS allowOverReimbursement + FROM tour_setting_lot + WHERE id = @lotId + """); + sql.setParam("lotId", line.getLotId()); + sql.setCallback(Sqls.callback.map()); + tourLedgerService.dao().execute(sql); + NutMap lot = sql.getObject(NutMap.class); + return lot != null && lot.getBoolean("allowOverReimbursement", false); + } + + private record OutProvinceQuota(String ratioType, + BigDecimal ratio, + int basePeople, + int totalSignupPeople, + int outProvinceSignupPeople, + int allowPeople) { + boolean canApply() { + return allowPeople > 0; + } + + String ratioText() { + if (ratio == null || BigDecimal.ZERO.compareTo(ratio) == 0) { + return "0"; + } + BigDecimal oneThird = BigDecimal.ONE.divide(BigDecimal.valueOf(3), 8, RoundingMode.HALF_UP); + if (ratio.subtract(oneThird).abs().compareTo(BigDecimal.valueOf(0.01)) <= 0) { + return "1/3"; + } + return ratio.stripTrailingZeros().toPlainString(); + } + } + + private Integer parseInteger(String value) { + if (StrUtil.isBlank(value)) { + return null; + } + try { + return Integer.valueOf(value); + } catch (NumberFormatException e) { + return null; + } + } + + private String normalizeDateTime(String value, boolean endOfDay) { + if (StrUtil.isBlank(value)) { + return ""; + } + String trimmed = value.trim(); + if (trimmed.length() == 10) { + return trimmed + (endOfDay ? " 23:59:59" : " 00:00:00"); + } + if (trimmed.length() == 16) { + return trimmed + ":00"; + } + return trimmed; + } + + private void fillStaffInfo(TourLedger ledger) { + View_user user = tourLedgerService.dao().fetch(View_user.class, Cnd.where(View_user::getId, "=", SecurityUtil.getUserId())); + 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.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 List parseFamilies(String families) { + if (StrUtil.isBlank(families)) { + return Collections.emptyList(); + } + List list = Json.fromJsonAsList(TourLedgerFamily.class, families); + if (Lang.isEmpty(list)) { + return Collections.emptyList(); + } + return list.stream() + .filter(item -> item != null && StrUtil.isNotBlank(item.getFamilyName())) + .collect(Collectors.toList()); + } + + private TourLedgerDirectRelative parseDirectRelative(String directRelative) { + if (StrUtil.isBlank(directRelative)) { + return null; + } + return Json.fromJson(TourLedgerDirectRelative.class, directRelative); + } + + private void clearBedInfo(TourLedger ledger, List familyList) { + if (ledger != null) { + ledger.setBedType(""); + ledger.setBedInfo(""); + ledger.setIntendedRoommate(""); + } + if (Lang.isNotEmpty(familyList)) { + familyList.forEach(item -> { + item.setBedType(""); + item.setBedInfo(""); + item.setIntendedRoommate(""); + }); + } + } + + private NutMap getSignupProcessInfo(TourLedger ledger) { + if (ledger == null || StrUtil.isBlank(ledger.getId())) { + return NutMap.NEW(); + } + Sql sql = Sqls.create(""" + SELECT + ins.id AS instanceId, + ins.state AS instanceState, + task.taskName AS taskKey, + (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, + IF((SELECT taskName FROM wf_process_task tt WHERE tt.id = task.taskParentId) = 'startTask', 1, 0) AS canRevoke + FROM wf_process_instance ins + LEFT JOIN wf_process_task task ON task.processInstanceId = ins.id AND task.taskState = @doingState + WHERE ins.businessNo = @ledgerId + AND ins.state <> @abandonState + ORDER BY ins.createdAt DESC + LIMIT 1 + """); + sql.setParam("ledgerId", ledger.getId()); + sql.setParam("doingState", ProcessTaskStateEnum.DOING.getCode()); + sql.setParam("abandonState", ProcessInstanceStateEnum.ABANDON.getCode()); + sql.setCallback(Sqls.callback.map()); + tourLedgerService.dao().execute(sql); + NutMap process = sql.getObject(NutMap.class); + return process == null ? NutMap.NEW() : process; + } + + private Result startOrContinueSignupApprovalWorkflow(TourLedger ledger, TourLedgerDirectRelative directRelativeInfo, TourMatter matter, TourLine line) { + ProcessInstance instance = tourLedgerService.dao().fetch(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "=", ledger.getId()) + .and(ProcessInstance::getState, "<>", ProcessInstanceStateEnum.ABANDON.getCode()) + .desc(ProcessInstance::getCreatedAt)); + Dict args = buildSignupApprovalArgs(ledger, directRelativeInfo, matter, line); + if (instance == null) { + ProcessInstance newInstance = flowEngine.startProcessInstanceByKey(DIRECT_FAMILY_WORKFLOW_KEY, ledger.getId(), SecurityUtil.getUserId(), args); + List doingTaskList = flowEngine.processTaskService().getDoingTaskList(newInstance.getId(), null); + for (ProcessTask task : doingTaskList) { + flowEngine.executeProcessTask(task.getId(), SecurityUtil.getUserId(), args); + } + return null; + } + if (!ProcessInstanceStateEnum.DOING.getCode().equals(instance.getState())) { + return Result.error("当前审批流程状态不允许修改提交"); + } + ProcessTask startTask = tourLedgerService.dao().fetch(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "=", instance.getId()) + .and(ProcessTask::getTaskName, "=", "startTask") + .and(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.DOING.getCode())); + if (startTask == null) { + return Result.error("当前审批流程节点不允许修改提交"); + } + flowEngine.executeProcessTask(startTask.getId(), SecurityUtil.getUserId(), args); + return null; + } + + private Result checkSignupApprovalWorkflowCanSubmit(String ledgerId) { + ProcessInstance instance = tourLedgerService.dao().fetch(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "=", ledgerId) + .and(ProcessInstance::getState, "<>", ProcessInstanceStateEnum.ABANDON.getCode()) + .desc(ProcessInstance::getCreatedAt)); + if (instance == null) { + return null; + } + if (!ProcessInstanceStateEnum.DOING.getCode().equals(instance.getState())) { + return Result.error("当前审批流程状态不允许修改提交"); + } + ProcessTask startTask = tourLedgerService.dao().fetch(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "=", instance.getId()) + .and(ProcessTask::getTaskName, "=", "startTask") + .and(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.DOING.getCode())); + if (startTask == null) { + return Result.error("当前审批流程节点不允许修改提交"); + } + return null; + } + + private Dict buildSignupApprovalArgs(TourLedger ledger, TourLedgerDirectRelative directRelativeInfo, TourMatter matter, TourLine line) { + Dict args = Dict.create(); + args.set(FlowConst.SUBMIT_TYPE, ProcessSubmitTypeEnum.APPLY.getCode()); + args.set(FlowConst.FORM_DATA, NutMap.NEW() + .addv("ledger", ledger) + .addv("directRelative", directRelativeInfo) + .addv("matter", matter) + .addv("line", line)); + args.set(FlowConst.TASK_FORM_DATA_PREFIX + "matterId", ledger.getMatterId()); + args.set(FlowConst.TASK_FORM_DATA_PREFIX + "lineId", ledger.getLineId()); + args.set(FlowConst.TASK_FORM_DATA_PREFIX + "lineName", ledger.getLineName()); + args.set(FlowConst.TASK_FORM_DATA_PREFIX + "directFamilyUnitLine", line != null && Boolean.TRUE.equals(line.getDirectFamilyUnitLine())); + args.set(FlowConst.TASK_FORM_DATA_PREFIX + "overCostReimbursed", Boolean.TRUE.equals(ledger.getOverCostReimbursed())); + return args; + } + + private String defaultIfBlank(String value, String defaultValue) { + return StrUtil.isBlank(value) ? defaultValue : value; + } + + private String currentJobNo() { + View_user user = tourMatterService.dao().fetch(View_user.class, Cnd.where(View_user::getId, "=", SecurityUtil.getUserId())); + return user == null ? SecurityUtil.getUserLoginname() : defaultIfBlank(user.getLoginname(), SecurityUtil.getUserLoginname()); + } + + private String getOrderColumn(String orderName) { + if ("lineName".equals(orderName)) { + return "l.lineName"; + } + if ("travelPeriod".equals(orderName)) { + return "m.travelStartTime"; + } + if ("matterName".equals(orderName)) { + return "m.matterName"; + } + if ("unionName".equals(orderName)) { + return "u.name"; + } + if ("lineType".equals(orderName)) { + return "l.lineType"; + } + if ("minGroupPeople".equals(orderName)) { + return "m.minGroupPeople"; + } + if ("maxGroupPeople".equals(orderName)) { + return "m.maxGroupPeople"; + } + if ("signupCount".equals(orderName)) { + return "signupCount"; + } + return "l.lineName"; + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourTravelAgencyController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourTravelAgencyController.java new file mode 100644 index 00000000..900a571a --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourTravelAgencyController.java @@ -0,0 +1,160 @@ +package com.budwk.app.zhgh.dayofficework.tour.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.annotation.SLog; +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.zhgh.dayofficework.tour.models.TourTravelAgency; +import com.budwk.app.zhgh.dayofficework.tour.service.TourTravelAgencyService; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.Cnd; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; + +@IocBean +@Ok("json:full") +@At("/platform/tour/travelAgency") +public class TourTravelAgencyController { + + private static final String MOBILE_PATTERN = "^1[3-9]\\d{9}$"; + private static final String EMAIL_PATTERN = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$"; + + @Inject + private TourTravelAgencyService travelAgencyService; + + @At("") + @Ok("beetl:/platform/zhgh/dayofficework/Tour/travelAgency/index.html") + @SaCheckPermission("tour.travelAgency") + public void index() { + } + + @At + @SaCheckPermission("tour.travelAgency") + public Result pageData(PageForm pageForm, Integer year, String agencyName, String contactName, String contactPhone) { + Cnd cnd = Cnd.NEW(); + cnd.andEX(TourTravelAgency::getYear, "=", year); + cnd.and(Cnd.likeEX(TourTravelAgency::getAgencyName, agencyName)); + cnd.and(Cnd.likeEX(TourTravelAgency::getContactName, contactName)); + cnd.and(Cnd.likeEX(TourTravelAgency::getContactPhone, contactPhone)); + applyOrder(cnd, pageForm); + Pagination pagination = travelAgencyService.listPage( + pageForm.getPageNumber(), + pageForm.getPageSize(), + TourTravelAgency.class, + cnd + ); + return Result.success(pagination); + } + + @At + @SaCheckPermission("tour.travelAgency") + public Result detail(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("参数错误"); + } + TourTravelAgency agency = travelAgencyService.fetch(id); + return agency == null ? Result.error("旅行社不存在") : Result.success(agency); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.travelAgency") + @SLog(type = "tour", tag = "旅行社管理", msg = "保存旅行社信息") + public Result doSubmit(TourTravelAgency agency) { + Result checkResult = check(agency); + if (checkResult != null) { + return checkResult; + } + + Cnd sameCodeCnd = Cnd.where(TourTravelAgency::getYear, "=", agency.getYear()) + .and(TourTravelAgency::getAgencyCode, "=", agency.getAgencyCode()); + if (StrUtil.isNotBlank(agency.getId())) { + sameCodeCnd.and(TourTravelAgency::getId, "<>", agency.getId()); + } + if (travelAgencyService.count(sameCodeCnd) > 0) { + return Result.error("同年度下旅行社编号已存在"); + } + if (agency.getEnabled() == null) { + agency.setEnabled(true); + } + if (StrUtil.isBlank(agency.getId())) { + travelAgencyService.insert(agency); + } else { + travelAgencyService.updateIgnoreNull(agency); + } + return Result.success(); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.travelAgency") + @SLog(type = "tour", tag = "旅行社管理", msg = "删除旅行社信息") + public Result doDelete(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("参数错误"); + } + travelAgencyService.delete(id); + return Result.success(); + } + + private Result check(TourTravelAgency agency) { + if (agency == null) { + return Result.error("参数错误"); + } + if (agency.getYear() == null) { + return Result.error("年度不能为空"); + } + if (StrUtil.isBlank(agency.getAgencyName())) { + return Result.error("旅行社名称不能为空"); + } + if (StrUtil.isBlank(agency.getAgencyCode())) { + return Result.error("旅行社编号不能为空"); + } + if (StrUtil.isBlank(agency.getContactName())) { + return Result.error("联系人不能为空"); + } + if (StrUtil.isBlank(agency.getContactPhone())) { + return Result.error("联系人手机不能为空"); + } + if (!agency.getContactPhone().matches(MOBILE_PATTERN)) { + return Result.error("联系人手机格式不正确"); + } + if (StrUtil.isBlank(agency.getEmail())) { + return Result.error("邮箱不能为空"); + } + if (!agency.getEmail().matches(EMAIL_PATTERN)) { + return Result.error("邮箱格式不正确"); + } + return null; + } + + private void applyOrder(Cnd cnd, PageForm pageForm) { + String orderName = pageForm.getPageOrderName(); + String orderBy = pageForm.getPageOrderBy(); + if (StrUtil.isBlank(orderName)) { + cnd.asc(TourTravelAgency::getYear).asc(TourTravelAgency::getAgencyCode).desc(TourTravelAgency::getCreatedAt); + return; + } + boolean descending = "descending".equals(orderBy); + if ("year".equals(orderName)) { + if (descending) { + cnd.desc(TourTravelAgency::getYear); + } else { + cnd.asc(TourTravelAgency::getYear); + } + } else if ("agencyCode".equals(orderName)) { + if (descending) { + cnd.desc(TourTravelAgency::getAgencyCode); + } else { + cnd.asc(TourTravelAgency::getAgencyCode); + } + } else { + cnd.asc(TourTravelAgency::getYear).asc(TourTravelAgency::getAgencyCode).desc(TourTravelAgency::getCreatedAt); + } + } +} 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 new file mode 100644 index 00000000..4fe44e27 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourUnionApprovalController.java @@ -0,0 +1,580 @@ +package com.budwk.app.zhgh.dayofficework.tour.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.util.StrUtil; +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.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerDirectRelative; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerFamily; +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; +import org.nutz.dao.Cnd; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.dao.util.cri.SqlExpressionGroup; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.util.NutMap; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; + +import java.time.LocalDate; +import java.util.List; + +@IocBean +@Ok("json:full") +@At("/platform/tour/unionApproval") +public class TourUnionApprovalController { + + private static final String WORKFLOW_KEY = "LXYBZXQSXL"; + private static final String TASK_DISPLAY_NAME = "分工会审核"; + + @Inject + private TourLedgerService tourLedgerService; + + @Inject + private TourLedgerFamilyService tourLedgerFamilyService; + + @Inject + private TourLedgerDirectRelativeService tourLedgerDirectRelativeService; + + @At("") + @Ok("beetl:/platform/zhgh/dayofficework/Tour/unionApproval/index.html") + @SaCheckPermission("tour.unionApproval") + public void index() { + } + + @At("/h5") + @Ok("beetl:/platform/zhghh5/dayofficework/tour/unionApproval/index.html") + @SaCheckPermission("tour.unionApproval") + public void h5() { + } + + @At + @SaCheckPermission("tour.unionApproval") + public Result pageData(PageForm pageForm, Boolean audit, Integer startYear, Integer endYear, String keyword, String unionId, String lineId, String travelPeriod, String lineType) { + Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType); + + Sql countSql = Sqls.create(""" + SELECT COUNT(DISTINCT task.id) + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 + $condition + """); + countSql.setCondition(cnd); + countSql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(countSql); + int count = countSql.getInt(); + + Sql listSql = Sqls.create(""" + SELECT + t.*, + ins.id AS instanceId, + ins.businessNo, + ins.state AS instanceState, + ins.variable AS instanceVariable, + ins.processDefineId AS instanceProcessDefineId, + task.id AS taskId, + task.taskName AS taskKey, + task.displayName AS taskName, + task.taskType, + task.performType AS taskPerformType, + task.taskState, + task.finishTime, + task.taskParentId, + task.variable AS taskVariable, + IFNULL(GROUP_CONCAT(DISTINCT nt.displayName), '结束') AS curTaskName, + CASE + WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName) + ELSE CONCAT( + COALESCE(NULLIF(l.lineName, ''), t.lineName), + '(', + CASE + WHEN IFNULL(m.unionId, '') = '' THEN '校工会' + ELSE IFNULL(mu.name, '') + END, + ')' + ) + END AS currentLineName, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE tp.travelPeriod + END AS travelPeriod, + IFNULL(f.familyCount, 0) AS familyCount + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10 + LEFT JOIN ( + SELECT ledgerId, COUNT(1) AS familyCount + FROM tour_ledger_family + WHERE delFlag = 0 + 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_line l ON l.id = t.lineId AND l.delFlag = 0 + LEFT JOIN sys_union mu ON mu.id = m.unionId + LEFT JOIN ( + SELECT + `year`, + lineId, + GROUP_CONCAT( + DISTINCT CASE + WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> '' + THEN CONCAT(travelStartTime, ' 至 ', travelEndTime) + ELSE NULL + END + ORDER BY travelStartTime ASC + SEPARATOR ';' + ) AS travelPeriod + FROM tour_matter + WHERE delFlag = 0 + AND lineId IS NOT NULL + AND lineId <> '' + GROUP BY `year`, lineId + ) tp ON (t.matterId IS NULL OR t.matterId = '') AND tp.`year` = t.`year` AND tp.lineId = t.lineId + $condition + GROUP BY task.id + ORDER BY $orderColumn $orderBy, task.createdAt DESC, t.signupTime DESC, t.createdAt DESC + """); + listSql.setCondition(cnd); + listSql.setVar("orderColumn", getOrderColumn(pageForm.getPageOrderName())); + listSql.setVar("orderBy", "ascending".equals(pageForm.getPageOrderBy()) ? "asc" : "desc"); + listSql.setPager(tourLedgerService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize())); + listSql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(listSql); + + List list = listSql.getList(NutMap.class); + list.forEach(item -> item.put("lineName", item.getString("currentLineName"))); + Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list); + return Result.success(pagination); + } + + @At + @SaCheckPermission("tour.unionApproval") + public Result detail(String id) { + TourLedger ledger = fetchAuditLedger(id); + if (ledger == null) { + return Result.error("报名记录不存在或无权查看"); + } + ledger.setLineName(getCurrentLineName(ledger)); + Cnd familyCnd = Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()) + .and(TourLedgerFamily::getDelFlag, "=", false); + familyCnd.asc(TourLedgerFamily::getCreatedAt); + TourLedgerDirectRelative directRelative = tourLedgerDirectRelativeService.fetch( + Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()) + .and(TourLedgerDirectRelative::getDelFlag, "=", false)); + NutMap signupConfig = getSignupConfig(ledger); + return Result.success(NutMap.NEW() + .addv("ledger", ledger) + .addv("travelPeriod", getTravelPeriod(ledger)) + .addv("families", tourLedgerFamilyService.query(familyCnd)) + .addv("directRelative", directRelative) + .addv("allowFamily", signupConfig.getBoolean("allowFamily", false)) + .addv("fillBedInfo", signupConfig.getBoolean("fillBedInfo", true)) + .addv("directFamilyUnitLine", signupConfig.getBoolean("directFamilyUnitLine", false))); + } + + @At + @SaCheckPermission("tour.unionApproval") + public Result unionOptions(Boolean audit, Integer startYear, Integer endYear, String keyword, String lineId, String travelPeriod, String lineType) { + Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, null, lineId, travelPeriod, lineType); + cnd.and("t.unionId", "IS NOT", null); + cnd.and("t.unionId", "<>", ""); + + Sql sql = Sqls.create(""" + SELECT DISTINCT + t.unionId AS id, + COALESCE(NULLIF(u.name, ''), t.unionName) AS name, + u.unionCode + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 + LEFT JOIN sys_union u ON u.id = t.unionId + $condition + ORDER BY u.unionCode ASC, name ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission("tour.unionApproval") + public Result lineOptions(Boolean audit, Integer startYear, Integer endYear, String travelPeriod, String lineType, String unionId, String keyword) { + Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, unionId, null, travelPeriod, lineType); + cnd.and("t.lineName", "IS NOT", null); + cnd.and("t.lineName", "<>", ""); + + Sql sql = Sqls.create(""" + SELECT DISTINCT + CASE + WHEN IFNULL(t.lineId, '') <> '' THEN CONCAT(t.lineId, '|', IFNULL(m.unionId, '')) + ELSE CONCAT('legacy:', t.lineName, '|', IFNULL(m.unionId, '')) + END AS lineId, + CASE + WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName) + ELSE CONCAT( + COALESCE(NULLIF(l.lineName, ''), t.lineName), + '(', + CASE + WHEN IFNULL(m.unionId, '') = '' THEN '校工会' + ELSE IFNULL(mu.name, '') + END, + ')' + ) + END AS lineName + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 + LEFT JOIN sys_union mu ON mu.id = m.unionId + $condition + ORDER BY lineName ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission("tour.unionApproval") + public Result travelPeriodOptions(Boolean audit, Integer startYear, Integer endYear, String lineId, String lineType, String unionId, String keyword) { + Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, unionId, lineId, null, lineType); + cnd.and("m.travelStartTime", "IS NOT", null); + cnd.and("m.travelStartTime", "<>", ""); + cnd.and("m.travelEndTime", "IS NOT", null); + cnd.and("m.travelEndTime", "<>", ""); + + Sql sql = Sqls.create(""" + SELECT DISTINCT CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) AS travelPeriod + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 + $condition + ORDER BY travelPeriod ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission("tour.unionApproval") + public Result lineTypeOptions(Boolean audit, Integer startYear, Integer endYear, String lineId, String travelPeriod, String unionId, String keyword) { + Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, unionId, lineId, travelPeriod, null); + cnd.and("t.lineType", "IS NOT", null); + cnd.and("t.lineType", "<>", ""); + + Sql sql = Sqls.create(""" + SELECT DISTINCT t.lineType + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 + $condition + ORDER BY t.lineType ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + private Cnd buildAuditCnd(Boolean audit, Integer startYear, Integer endYear, String keyword, String unionId, String lineId, String travelPeriod, String lineType) { + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.and("def.name", "=", WORKFLOW_KEY); + cnd.and("task.displayName", "=", TASK_DISPLAY_NAME); + cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId())); + appendAuditStateFilter(cnd, audit); + cnd.andEX("t.`year`", ">=", startYear); + cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear); + cnd.andEX("t.unionId", "=", unionId); + cnd.andEX("t.lineType", "=", lineType); + appendLineFilter(cnd, lineId); + appendTravelPeriodFilter(cnd, travelPeriod); + appendKeywordFilter(cnd, keyword); + return cnd; + } + + private void appendAuditStateFilter(Cnd cnd, Boolean audit) { + if (Boolean.TRUE.equals(audit)) { + cnd.and("task.taskState", "in", List.of( + ProcessTaskStateEnum.FINISHED.getCode(), + ProcessTaskStateEnum.WITHDRAW.getCode(), + ProcessTaskStateEnum.INTERRUPT.getCode())); + return; + } + cnd.and("task.taskState", "=", ProcessTaskStateEnum.DOING.getCode()); + } + + private void appendKeywordFilter(Cnd cnd, String keyword) { + if (StrUtil.isBlank(keyword)) { + return; + } + SqlExpressionGroup group = new SqlExpressionGroup(); + group.orLike("t.userName", keyword.trim()); + group.orLike("t.jobNo", keyword.trim()); + group.orLike("t.lineName", keyword.trim()); + group.orLike("l.lineName", keyword.trim()); + cnd.and(group); + } + + private void appendTravelPeriodFilter(Cnd cnd, String travelPeriod) { + if (StrUtil.isBlank(travelPeriod)) { + return; + } + cnd.and("CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)", "=", travelPeriod); + } + + private void appendLineFilter(Cnd cnd, String lineId) { + if (StrUtil.isBlank(lineId)) { + return; + } + LineFilter lineFilter = parseLineFilter(lineId); + if (lineFilter.getLineId().startsWith("legacy:")) { + SqlExpressionGroup legacyGroup = new SqlExpressionGroup(); + legacyGroup.or("t.lineId", "IS", null); + legacyGroup.or("t.lineId", "=", ""); + cnd.and(legacyGroup); + cnd.and("t.lineName", "=", lineFilter.getLineId().substring("legacy:".length())); + } else { + cnd.and("t.lineId", "=", lineFilter.getLineId()); + } + appendMatterUnionFilter(cnd, lineFilter.getUnionId()); + } + + private LineFilter parseLineFilter(String value) { + int delimiterIndex = value.indexOf("|"); + if (delimiterIndex < 0) { + return new LineFilter(value, null); + } + return new LineFilter(value.substring(0, delimiterIndex), value.substring(delimiterIndex + 1)); + } + + private void appendMatterUnionFilter(Cnd cnd, String unionId) { + if (unionId == null) { + return; + } + if (StrUtil.isBlank(unionId)) { + SqlExpressionGroup group = new SqlExpressionGroup(); + group.or("m.unionId", "IS", null); + group.or("m.unionId", "=", ""); + cnd.and(group); + return; + } + cnd.and("m.unionId", "=", unionId); + } + + private String getOrderColumn(String orderName) { + if ("jobNo".equals(orderName)) { + return "t.jobNo"; + } + if ("userName".equals(orderName)) { + return "t.userName"; + } + if ("lineName".equals(orderName)) { + return """ + CASE + WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName) + ELSE CONCAT( + COALESCE(NULLIF(l.lineName, ''), t.lineName), + '(', + CASE + WHEN IFNULL(m.unionId, '') = '' THEN '校工会' + ELSE IFNULL(mu.name, '') + END, + ')' + ) + END + """; + } + if ("travelPeriod".equals(orderName)) { + return "COALESCE(m.travelStartTime, tp.travelPeriod)"; + } + if ("lineType".equals(orderName)) { + return "t.lineType"; + } + if ("signupTime".equals(orderName)) { + return "t.signupTime"; + } + if ("familyCount".equals(orderName)) { + return "familyCount"; + } + if ("instanceState".equals(orderName)) { + return "ins.state"; + } + return "task.createdAt"; + } + + private TourLedger fetchAuditLedger(String id) { + if (StrUtil.isBlank(id)) { + return null; + } + Sql sql = Sqls.create(""" + SELECT COUNT(1) + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + WHERE t.delFlag = 0 + AND t.id = @id + AND def.name = @workflowKey + AND task.displayName = @taskDisplayName + AND ta.actorId = @actorId + """); + sql.setParam("id", id); + sql.setParam("workflowKey", WORKFLOW_KEY); + sql.setParam("taskDisplayName", TASK_DISPLAY_NAME); + sql.setParam("actorId", SecurityUtil.getUserId()); + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt() > 0 ? tourLedgerService.fetch(id) : null; + } + + private String getCurrentLineName(TourLedger ledger) { + if (ledger == null) { + return ""; + } + Sql sql = Sqls.create(""" + SELECT CASE + WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName) + ELSE CONCAT( + COALESCE(NULLIF(l.lineName, ''), t.lineName), + '(', + CASE + WHEN IFNULL(m.unionId, '') = '' THEN '校工会' + ELSE IFNULL(mu.name, '') + END, + ')' + ) + END AS lineName + FROM tour_ledger t + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 + LEFT JOIN sys_union mu ON mu.id = m.unionId + WHERE t.id = @ledgerId + """); + sql.setParam("ledgerId", ledger.getId()); + sql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(sql); + return StrUtil.blankToDefault(sql.getString(), ledger.getLineName()); + } + + private String getTravelPeriod(TourLedger ledger) { + if (ledger == null || ledger.getYear() == null || StrUtil.isBlank(ledger.getLineId())) { + return ""; + } + if (StrUtil.isNotBlank(ledger.getMatterId())) { + Sql exactSql = Sqls.create(""" + SELECT CASE + WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> '' + THEN CONCAT(travelStartTime, ' 至 ', travelEndTime) + ELSE '' + END AS travelPeriod + FROM tour_matter + WHERE delFlag = 0 + AND id = @matterId + """); + exactSql.setParam("matterId", ledger.getMatterId()); + exactSql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(exactSql); + return exactSql.getString(); + } + Sql sql = Sqls.create(""" + SELECT GROUP_CONCAT( + DISTINCT CASE + WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> '' + THEN CONCAT(travelStartTime, ' 至 ', travelEndTime) + ELSE NULL + END + ORDER BY travelStartTime ASC + SEPARATOR ';' + ) AS travelPeriod + FROM tour_matter + WHERE delFlag = 0 + AND `year` = @year + AND lineId = @lineId + """); + sql.setParam("year", ledger.getYear()); + sql.setParam("lineId", ledger.getLineId()); + sql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(sql); + return sql.getString(); + } + + private NutMap getSignupConfig(TourLedger ledger) { + if (ledger == null || StrUtil.isBlank(ledger.getLineId())) { + return NutMap.NEW().addv("allowFamily", false).addv("fillBedInfo", true).addv("directFamilyUnitLine", false); + } + Sql sql = Sqls.create(""" + SELECT + IFNULL(s.allowFamily, 0) AS allowFamily, + IFNULL(s.fillBedInfo, 1) AS fillBedInfo, + IFNULL(l.directFamilyUnitLine, 0) AS directFamilyUnitLine + FROM tour_ledger t + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN tour_setting s ON s.id = m.settingId AND s.delFlag = 0 + LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 + WHERE t.id = @ledgerId + """); + sql.setParam("ledgerId", ledger.getId()); + sql.setCallback(Sqls.callback.map()); + tourLedgerService.dao().execute(sql); + NutMap map = sql.getObject(NutMap.class); + if (map == null || map.isEmpty()) { + return NutMap.NEW().addv("allowFamily", false).addv("fillBedInfo", true).addv("directFamilyUnitLine", false); + } + return map; + } + + private static class LineFilter { + private final String lineId; + private final String unionId; + + private LineFilter(String lineId, String unionId) { + this.lineId = lineId; + this.unionId = unionId; + } + + private String getLineId() { + return lineId; + } + + private String getUnionId() { + return unionId; + } + } +} 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 new file mode 100644 index 00000000..068f047e --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourUnionLedgerController.java @@ -0,0 +1,905 @@ +package com.budwk.app.zhgh.dayofficework.tour.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.annotation.SLog; +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.base.utils.CommonDownloadUtil; +import com.budwk.app.sys.models.Sys_union; +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; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerFamily; +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; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.util.CellRangeAddress; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.Cnd; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.dao.util.cri.SqlExpressionGroup; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.util.NutMap; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; + +import javax.servlet.http.HttpServletResponse; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.List; + +@IocBean +@Ok("json:full") +@At("/platform/tour/unionledger") +public class TourUnionLedgerController { + + @Inject + private TourLedgerService tourLedgerService; + + @Inject + private TourLedgerFamilyService tourLedgerFamilyService; + + @Inject + private TourLedgerDirectRelativeService tourLedgerDirectRelativeService; + + @At("") + @Ok("beetl:/platform/zhgh/dayofficework/Tour/unionledger/index.html") + @SaCheckPermission("tour.unionledger") + public void index() { + } + + @At + @SaCheckPermission("tour.unionledger") + public Result pageData(PageForm pageForm, Integer startYear, Integer endYear, String keyword, String unionId, String lineId, String travelPeriod, String lineType, String scopeType, Boolean overCostOnly) { + String currentUnionId = currentUnionId(); + if (StrUtil.isBlank(currentUnionId)) { + return Result.error("当前登录人所属工会不能为空"); + } + Cnd cnd = buildQueryCnd(currentUnionId, startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType, scopeType, overCostOnly); + + Sql countSql = Sqls.create(""" + SELECT COUNT(DISTINCT t.id) + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id + LEFT JOIN wf_process_task task ON task.processInstanceId = ins.id AND task.taskState = 10 + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + $condition + """); + countSql.setCondition(cnd); + countSql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(countSql); + int count = countSql.getInt(); + + Sql listSql = Sqls.create(""" + SELECT + t.*, + ins.id AS instanceId, + ins.state AS instanceState, + ins.processDefineId AS instanceProcessDefineId, + IFNULL(l.directFamilyUnitLine, 0) AS directFamilyUnitLine, + IFNULL(GROUP_CONCAT(DISTINCT task.displayName), IF(ins.id IS NULL, '', '结束')) AS curTaskName, + CASE + WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName) + ELSE CONCAT( + COALESCE(NULLIF(l.lineName, ''), t.lineName), + '(', + CASE + WHEN IFNULL(m.unionId, '') = '' THEN '校工会' + ELSE IFNULL(mu.name, '') + END, + ')' + ) + END AS currentLineName, + IFNULL(f.familyCount, 0) AS familyCount, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE tp.travelPeriod + END AS travelPeriod + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id + LEFT JOIN wf_process_task task ON task.processInstanceId = ins.id AND task.taskState = 10 + LEFT JOIN ( + SELECT ledgerId, COUNT(1) AS familyCount + FROM tour_ledger_family + WHERE delFlag = 0 + 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_line l ON l.id = t.lineId AND l.delFlag = 0 + LEFT JOIN sys_union mu ON mu.id = m.unionId + LEFT JOIN ( + SELECT + `year`, + lineId, + GROUP_CONCAT( + DISTINCT CASE + WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> '' + THEN CONCAT(travelStartTime, ' 至 ', travelEndTime) + ELSE NULL + END + ORDER BY travelStartTime ASC + SEPARATOR ';' + ) AS travelPeriod + FROM tour_matter + WHERE delFlag = 0 + AND lineId IS NOT NULL + AND lineId <> '' + GROUP BY `year`, lineId + ) tp ON (t.matterId IS NULL OR t.matterId = '') AND tp.`year` = t.`year` AND tp.lineId = t.lineId + $condition + GROUP BY t.id + ORDER BY $orderColumn $orderBy, t.`year` DESC, t.signupTime DESC, t.createdAt DESC + """); + listSql.setCondition(cnd); + 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); + + var list = listSql.getList(NutMap.class); + list.forEach(item -> item.put("lineName", item.getString("currentLineName"))); + Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list); + return Result.success(pagination); + } + + @At + @Ok("void") + @SaCheckPermission("tour.unionledger") + public void exportOverCostSummary(PageForm pageForm, Integer startYear, Integer endYear, String keyword, String unionId, String lineId, String travelPeriod, String lineType, String scopeType, HttpServletResponse response) { + String currentUnionId = currentUnionId(); + if (StrUtil.isBlank(currentUnionId)) { + return; + } + Cnd cnd = buildQueryCnd(currentUnionId, startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType, scopeType, true); + List list = queryOverCostSummaryList(cnd, pageForm); + Workbook workbook = buildOverCostSummaryWorkbook(currentUnionName(), list); + CommonDownloadUtil.download("5天外超出部分疗休养费用由单位承担申请人员汇总表.xls", workbook, response); + } + + @At + @SaCheckPermission("tour.unionledger") + public Result detail(String id) { + TourLedger ledger = fetchScopedLedger(id); + if (ledger == null) { + return Result.error("台账记录不存在或无权查看"); + } + ledger.setLineName(getCurrentLineName(ledger)); + Cnd familyCnd = Cnd.where(TourLedgerFamily::getLedgerId, "=", id) + .and(TourLedgerFamily::getDelFlag, "=", false); + familyCnd.asc(TourLedgerFamily::getCreatedAt); + TourLedgerDirectRelative directRelative = tourLedgerDirectRelativeService.fetch( + Cnd.where(TourLedgerDirectRelative::getDelFlag, "=", false) + .and(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId())); + return Result.success(NutMap.NEW() + .addv("ledger", ledger) + .addv("travelPeriod", getTravelPeriod(ledger)) + .addv("families", tourLedgerFamilyService.query(familyCnd)) + .addv("directRelative", directRelative) + .addv("fillBedInfo", isFillBedInfo(ledger)) + .addv("directFamilyUnitLine", isDirectFamilyUnitLine(ledger))); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.unionledger") + @SLog(type = "tour", tag = "分工会疗休养台账", msg = "删除分工会疗休养台账") + public Result doDelete(String id) { + TourLedger ledger = fetchScopedLedger(id); + if (ledger == null) { + return Result.error("台账记录不存在或无权删除"); + } + tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId())); + tourLedgerService.clear(Cnd.where(TourLedger::getId, "=", ledger.getId())); + return Result.success(); + } + + @At + @SaCheckPermission("tour.unionledger") + public Result unionOptions(Integer startYear, Integer endYear) { + String currentUnionId = currentUnionId(); + if (StrUtil.isBlank(currentUnionId)) { + return Result.error("当前登录人所属工会不能为空"); + } + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.andEX("t.`year`", ">=", startYear); + cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear); + cnd.and("t.unionId", "IS NOT", null); + cnd.and("t.unionId", "<>", ""); + applyUnionLedgerScope(cnd, currentUnionId); + + Sql sql = Sqls.create(""" + SELECT DISTINCT + t.unionId AS id, + COALESCE(NULLIF(u.name, ''), t.unionName) AS name, + u.unionCode + FROM tour_ledger t + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN sys_union u ON u.id = t.unionId + $condition + ORDER BY + CASE WHEN t.unionId = @currentUnionId THEN 0 ELSE 1 END, + u.unionCode ASC, + name ASC + """); + sql.setCondition(cnd); + sql.setParam("currentUnionId", currentUnionId); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission("tour.unionledger") + public Result lineOptions(Integer startYear, Integer endYear, String travelPeriod, String lineType, String unionId, String keyword, String scopeType) { + String currentUnionId = currentUnionId(); + if (StrUtil.isBlank(currentUnionId)) { + return Result.error("当前登录人所属工会不能为空"); + } + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.andEX("t.`year`", ">=", startYear); + cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear); + cnd.and("t.lineName", "IS NOT", null); + cnd.and("t.lineName", "<>", ""); + cnd.andEX("t.unionId", "=", unionId); + cnd.andEX("t.lineType", "=", lineType); + appendTravelPeriodFilter(cnd, travelPeriod); + appendScopeFilter(cnd, currentUnionId, scopeType); + appendKeywordFilter(cnd, keyword); + + Sql sql = Sqls.create(""" + SELECT DISTINCT + CASE + WHEN IFNULL(t.lineId, '') <> '' THEN CONCAT(t.lineId, '|', IFNULL(m.unionId, '')) + ELSE CONCAT('legacy:', t.lineName, '|', IFNULL(m.unionId, '')) + END AS lineId, + CASE + WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName) + ELSE CONCAT( + COALESCE(NULLIF(l.lineName, ''), t.lineName), + '(', + CASE + WHEN IFNULL(m.unionId, '') = '' THEN '校工会' + ELSE IFNULL(mu.name, '') + END, + ')' + ) + END AS lineName + FROM tour_ledger t + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 + LEFT JOIN sys_union mu ON mu.id = m.unionId + $condition + ORDER BY lineName ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission("tour.unionledger") + public Result travelPeriodOptions(Integer startYear, Integer endYear, String lineId, String lineType, String unionId, String keyword, String scopeType) { + String currentUnionId = currentUnionId(); + if (StrUtil.isBlank(currentUnionId)) { + return Result.error("当前登录人所属工会不能为空"); + } + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.andEX("t.`year`", ">=", startYear); + cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear); + cnd.and("m.travelStartTime", "IS NOT", null); + cnd.and("m.travelStartTime", "<>", ""); + cnd.and("m.travelEndTime", "IS NOT", null); + cnd.and("m.travelEndTime", "<>", ""); + cnd.andEX("t.unionId", "=", unionId); + cnd.andEX("t.lineType", "=", lineType); + appendLineFilter(cnd, lineId); + appendScopeFilter(cnd, currentUnionId, scopeType); + appendKeywordFilter(cnd, keyword); + + Sql sql = Sqls.create(""" + SELECT DISTINCT CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) AS travelPeriod + FROM tour_ledger t + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + $condition + ORDER BY travelPeriod ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission("tour.unionledger") + public Result lineTypeOptions(Integer startYear, Integer endYear, String lineId, String travelPeriod, String unionId, String keyword, String scopeType) { + String currentUnionId = currentUnionId(); + if (StrUtil.isBlank(currentUnionId)) { + return Result.error("当前登录人所属工会不能为空"); + } + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.andEX("t.`year`", ">=", startYear); + cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear); + cnd.and("t.lineType", "IS NOT", null); + cnd.and("t.lineType", "<>", ""); + cnd.andEX("t.unionId", "=", unionId); + appendLineFilter(cnd, lineId); + appendTravelPeriodFilter(cnd, travelPeriod); + appendScopeFilter(cnd, currentUnionId, scopeType); + appendKeywordFilter(cnd, keyword); + + Sql sql = Sqls.create(""" + SELECT DISTINCT t.lineType + FROM tour_ledger t + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + $condition + ORDER BY t.lineType ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission("tour.unionledger") + public Result summaryStats(Integer startYear, Integer endYear, String unionId, String lineId, String travelPeriod, String lineType) { + String currentUnionId = currentUnionId(); + if (StrUtil.isBlank(currentUnionId)) { + return Result.error("当前登录人所属工会不能为空"); + } + Cnd ownCnd = buildSummaryCnd(currentUnionId, startYear, endYear, unionId, lineId, travelPeriod, lineType, "ownUnionJoined"); + Cnd organizedCnd = buildSummaryCnd(currentUnionId, startYear, endYear, unionId, lineId, travelPeriod, lineType, "organizedLineJoined"); + + Sql ownSql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_ledger t + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + $condition + """); + ownSql.setCondition(ownCnd); + ownSql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(ownSql); + + Sql organizedSql = Sqls.create(""" + SELECT CAST(IFNULL(SUM(1 + IFNULL(f.familyCount, 0)), 0) AS SIGNED) + FROM tour_ledger t + INNER JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN ( + SELECT ledgerId, COUNT(1) AS familyCount + FROM tour_ledger_family + WHERE delFlag = 0 + GROUP BY ledgerId + ) f ON f.ledgerId = t.id + $condition + """); + organizedSql.setCondition(organizedCnd); + organizedSql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(organizedSql); + + return Result.success(NutMap.NEW() + .addv("ownUnionJoinedStaffCount", ownSql.getInt()) + .addv("organizedLineJoinedTotalCount", organizedSql.getInt())); + } + + private List queryOverCostSummaryList(Cnd cnd, PageForm pageForm) { + Sql sql = Sqls.create(""" + SELECT + t.id, + t.userName, + t.idCard, + t.unionName, + vu.mobile, + m.travelStartTime, + m.travelEndTime, + m.estimatedCost, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE tp.travelPeriod + END AS travelPeriod, + CASE + WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName) + ELSE CONCAT( + COALESCE(NULLIF(l.lineName, ''), t.lineName), + '(', + CASE + WHEN IFNULL(m.unionId, '') = '' THEN '校工会' + ELSE IFNULL(mu.name, '') + END, + ')' + ) + END AS currentLineName + FROM tour_ledger t + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 + LEFT JOIN sys_union mu ON mu.id = m.unionId + LEFT JOIN vw_user vu ON vu.loginname = t.jobNo + LEFT JOIN ( + SELECT + `year`, + lineId, + GROUP_CONCAT( + DISTINCT CASE + WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> '' + THEN CONCAT(travelStartTime, ' 至 ', travelEndTime) + ELSE NULL + END + ORDER BY travelStartTime ASC + SEPARATOR ';' + ) AS travelPeriod + FROM tour_matter + WHERE delFlag = 0 + AND lineId IS NOT NULL + AND lineId <> '' + GROUP BY `year`, lineId + ) tp ON (t.matterId IS NULL OR t.matterId = '') AND tp.`year` = t.`year` AND tp.lineId = t.lineId + $condition + ORDER BY $orderColumn $orderBy, t.`year` DESC, t.signupTime DESC, t.createdAt DESC + """); + sql.setCondition(cnd); + sql.setVar("orderColumn", getOrderColumn(pageForm == null ? null : pageForm.getPageOrderName())); + sql.setVar("orderBy", pageForm != null && "ascending".equals(pageForm.getPageOrderBy()) ? "asc" : "desc"); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + List list = sql.getList(NutMap.class); + for (NutMap item : list) { + item.put("lineName", item.getString("currentLineName")); + item.put("totalDays", calcDays(item.getString("travelStartTime", ""), item.getString("travelEndTime", ""))); + item.put("estimatedCostText", formatAmount(item.get("estimatedCost"))); + } + return list; + } + + private Workbook buildOverCostSummaryWorkbook(String unionName, List list) { + Workbook workbook = new HSSFWorkbook(); + Sheet sheet = workbook.createSheet("汇总表"); + sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 9)); + sheet.addMergedRegion(new CellRangeAddress(1, 1, 0, 9)); + sheet.addMergedRegion(new CellRangeAddress(2, 2, 0, 9)); + + double[] widths = {5.14, 14.14, 21.29, 15.71, 27, 40, 50.43, 13.71, 22.57, 15.43}; + for (int i = 0; i < widths.length; i++) { + sheet.setColumnWidth(i, (int) (widths[i] * 256)); + } + + CellStyle attachStyle = createStyle(workbook, "宋体", (short) 12, false, HorizontalAlignment.LEFT, false, false); + CellStyle titleStyle = createStyle(workbook, "黑体", (short) 18, true, HorizontalAlignment.CENTER, false, false); + CellStyle unionStyle = createStyle(workbook, "仿宋_GB2312", (short) 14, true, HorizontalAlignment.LEFT, false, false); + unionStyle.setBorderBottom(BorderStyle.THIN); + CellStyle headerStyle = createStyle(workbook, "宋体", (short) 12, true, HorizontalAlignment.CENTER, true, true); + CellStyle bodyStyle = createStyle(workbook, "宋体", (short) 12, false, HorizontalAlignment.CENTER, false, true); + + Row row1 = sheet.createRow(0); + row1.setHeightInPoints(32); + setCell(row1, 0, "附件12", attachStyle); + fillMergedCells(row1, 1, 9, attachStyle); + + Row row2 = sheet.createRow(1); + row2.setHeightInPoints(32); + setCell(row2, 0, "5天外超出部分疗休养费用由单位承担申请人员汇总表", titleStyle); + fillMergedCells(row2, 1, 9, titleStyle); + + Row row3 = sheet.createRow(2); + row3.setHeightInPoints(32); + setCell(row3, 0, "分工会:" + StrUtil.blankToDefault(unionName, ""), unionStyle); + fillMergedCells(row3, 1, 9, unionStyle); + + String[] headers = {"序号", "姓名", "身份证号码", "电话号码", "所属分工会", "所选线路名称", "疗休养时间", "疗休养时长", "疗休养费用总额(元)", "备注"}; + Row header = sheet.createRow(3); + header.setHeightInPoints(43); + for (int i = 0; i < headers.length; i++) { + setCell(header, i, headers[i], headerStyle); + } + + int rowCount = Math.max(list == null ? 0 : list.size(), 20); + for (int i = 0; i < rowCount; i++) { + Row row = sheet.createRow(i + 4); + row.setHeightInPoints(25); + NutMap item = list != null && i < list.size() ? list.get(i) : null; + setCell(row, 0, String.valueOf(i + 1), bodyStyle); + setCell(row, 1, item == null ? "" : item.getString("userName", ""), bodyStyle); + setCell(row, 2, item == null ? "" : item.getString("idCard", ""), bodyStyle); + setCell(row, 3, item == null ? "" : item.getString("mobile", ""), bodyStyle); + setCell(row, 4, item == null ? "" : item.getString("unionName", ""), bodyStyle); + setCell(row, 5, item == null ? "" : item.getString("lineName", ""), bodyStyle); + setCell(row, 6, item == null ? "" : item.getString("travelPeriod", ""), bodyStyle); + setCell(row, 7, item == null ? "" : item.getString("totalDays", ""), bodyStyle); + setCell(row, 8, item == null ? "" : item.getString("estimatedCostText", ""), bodyStyle); + setCell(row, 9, "", bodyStyle); + } + return workbook; + } + + private CellStyle createStyle(Workbook workbook, String fontName, short fontSize, boolean bold, HorizontalAlignment alignment, boolean wrap, boolean border) { + Font font = workbook.createFont(); + font.setFontName(fontName); + font.setFontHeightInPoints(fontSize); + font.setBold(bold); + + CellStyle style = workbook.createCellStyle(); + style.setFont(font); + style.setAlignment(alignment); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setWrapText(wrap); + if (border) { + style.setBorderLeft(BorderStyle.THIN); + style.setBorderTop(BorderStyle.THIN); + style.setBorderRight(BorderStyle.THIN); + style.setBorderBottom(BorderStyle.THIN); + } + return style; + } + + private void setCell(Row row, int col, String value, CellStyle style) { + Cell cell = row.createCell(col); + cell.setCellValue(StrUtil.blankToDefault(value, "")); + cell.setCellStyle(style); + } + + private void fillMergedCells(Row row, int startCol, int endCol, CellStyle style) { + for (int i = startCol; i <= endCol; i++) { + setCell(row, i, "", style); + } + } + + private Integer calcDays(String start, String end) { + if (StrUtil.isBlank(start) || StrUtil.isBlank(end)) { + return null; + } + try { + LocalDate startDate = LocalDate.parse(start.substring(0, 10)); + LocalDate endDate = LocalDate.parse(end.substring(0, 10)); + return Math.toIntExact(ChronoUnit.DAYS.between(startDate, endDate) + 1); + } catch (Exception e) { + return null; + } + } + + private String formatAmount(Object value) { + if (value == null) { + return ""; + } + try { + BigDecimal amount = new BigDecimal(String.valueOf(value)); + return amount.stripTrailingZeros().toPlainString(); + } catch (Exception e) { + return String.valueOf(value); + } + } + + private Cnd buildQueryCnd(String currentUnionId, Integer startYear, Integer endYear, String keyword, String unionId, String lineId, String travelPeriod, String lineType, String scopeType, Boolean overCostOnly) { + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.andEX("t.`year`", ">=", startYear); + cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear); + cnd.andEX("t.unionId", "=", unionId); + cnd.andEX("t.lineType", "=", lineType); + if (Boolean.TRUE.equals(overCostOnly)) { + cnd.and("t.overCostReimbursed", "=", true); + } + appendLineFilter(cnd, lineId); + appendTravelPeriodFilter(cnd, travelPeriod); + appendScopeFilter(cnd, currentUnionId, scopeType); + appendKeywordFilter(cnd, keyword); + return cnd; + } + + private Cnd buildSummaryCnd(String currentUnionId, Integer startYear, Integer endYear, String unionId, String lineId, String travelPeriod, String lineType, String scopeType) { + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.andEX("t.`year`", ">=", startYear); + cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear); + cnd.andEX("t.unionId", "=", unionId); + cnd.andEX("t.lineType", "=", lineType); + appendLineFilter(cnd, lineId); + appendTravelPeriodFilter(cnd, travelPeriod); + appendScopeFilter(cnd, currentUnionId, scopeType); + return cnd; + } + + private void appendKeywordFilter(Cnd cnd, String keyword) { + if (StrUtil.isNotBlank(keyword)) { + SqlExpressionGroup group = new SqlExpressionGroup(); + group.orLike("t.userName", keyword.trim()); + group.orLike("t.jobNo", keyword.trim()); + cnd.and(group); + } + } + + private void applyUnionLedgerScope(Cnd cnd, String currentUnionId) { + SqlExpressionGroup group = new SqlExpressionGroup(); + group.or("t.unionId", "=", currentUnionId); + group.or("m.unionId", "=", currentUnionId); + cnd.and(group); + } + + private void appendScopeFilter(Cnd cnd, String currentUnionId, String scopeType) { + if ("ownUnionJoined".equals(scopeType)) { + cnd.and("t.unionId", "=", currentUnionId); + return; + } + if ("organizedLineJoined".equals(scopeType)) { + cnd.and("m.unionId", "=", currentUnionId); + return; + } + applyUnionLedgerScope(cnd, currentUnionId); + } + + private String buildYearCondition(Integer startYear, Integer endYear) { + StringBuilder builder = new StringBuilder(); + if (startYear != null) { + builder.append(" AND t.`year` >= ").append(startYear); + } + if (endYear != null) { + builder.append(" AND t.`year` <= ").append(endYear); + } + return builder.toString(); + } + + private TourLedger fetchScopedLedger(String id) { + if (StrUtil.isBlank(id) || StrUtil.isBlank(currentUnionId())) { + return null; + } + Sql sql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_ledger t + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + WHERE t.delFlag = 0 + AND t.id = @id + AND (t.unionId = @unionId OR m.unionId = @unionId) + """); + sql.setParam("id", id); + sql.setParam("unionId", currentUnionId()); + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt() > 0 ? tourLedgerService.fetch(id) : null; + } + + private String getTravelPeriod(TourLedger ledger) { + if (ledger == null || ledger.getYear() == null || StrUtil.isBlank(ledger.getLineId())) { + return ""; + } + if (StrUtil.isNotBlank(ledger.getMatterId())) { + Sql exactSql = Sqls.create(""" + SELECT CASE + WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> '' + THEN CONCAT(travelStartTime, ' 至 ', travelEndTime) + ELSE '' + END AS travelPeriod + FROM tour_matter + WHERE delFlag = 0 + AND id = @matterId + """); + exactSql.setParam("matterId", ledger.getMatterId()); + exactSql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(exactSql); + return exactSql.getString(); + } + Sql sql = Sqls.create(""" + SELECT GROUP_CONCAT( + DISTINCT CASE + WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> '' + THEN CONCAT(travelStartTime, ' 至 ', travelEndTime) + ELSE NULL + END + ORDER BY travelStartTime ASC + SEPARATOR ';' + ) AS travelPeriod + FROM tour_matter + WHERE delFlag = 0 + AND `year` = @year + AND lineId = @lineId + """); + sql.setParam("year", ledger.getYear()); + sql.setParam("lineId", ledger.getLineId()); + sql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(sql); + return sql.getString(); + } + + private boolean isDirectFamilyUnitLine(TourLedger ledger) { + if (ledger == null || StrUtil.isBlank(ledger.getLineId())) { + return false; + } + Sql sql = Sqls.create(""" + SELECT IFNULL(directFamilyUnitLine, 0) + FROM tour_line + WHERE delFlag = 0 + AND id = @lineId + """); + sql.setParam("lineId", ledger.getLineId()); + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt() > 0; + } + + private boolean isFillBedInfo(TourLedger ledger) { + if (ledger == null || StrUtil.isBlank(ledger.getMatterId())) { + return true; + } + Sql sql = Sqls.create(""" + SELECT IFNULL(MAX(s.fillBedInfo), 1) + FROM tour_ledger t + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN tour_setting s ON s.id = m.settingId AND s.delFlag = 0 + WHERE t.id = @ledgerId + """); + sql.setParam("ledgerId", ledger.getId()); + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt() > 0; + } + + private void appendTravelPeriodFilter(Cnd cnd, String travelPeriod) { + if (StrUtil.isBlank(travelPeriod)) { + return; + } + cnd.and("CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)", "=", travelPeriod); + } + + private void appendLineFilter(Cnd cnd, String lineId) { + if (StrUtil.isBlank(lineId)) { + return; + } + LineFilter lineFilter = parseLineFilter(lineId); + if (lineFilter.getLineId().startsWith("legacy:")) { + SqlExpressionGroup legacyGroup = new SqlExpressionGroup(); + legacyGroup.or("t.lineId", "IS", null); + legacyGroup.or("t.lineId", "=", ""); + cnd.and(legacyGroup); + cnd.and("t.lineName", "=", lineFilter.getLineId().substring("legacy:".length())); + } else { + cnd.and("t.lineId", "=", lineFilter.getLineId()); + } + appendMatterUnionFilter(cnd, lineFilter.getUnionId()); + } + + private LineFilter parseLineFilter(String value) { + int delimiterIndex = value.indexOf("|"); + if (delimiterIndex < 0) { + return new LineFilter(value, null); + } + return new LineFilter(value.substring(0, delimiterIndex), value.substring(delimiterIndex + 1)); + } + + private void appendMatterUnionFilter(Cnd cnd, String unionId) { + if (unionId == null) { + return; + } + if (StrUtil.isBlank(unionId)) { + SqlExpressionGroup group = new SqlExpressionGroup(); + group.or("m.unionId", "IS", null); + group.or("m.unionId", "=", ""); + cnd.and(group); + return; + } + cnd.and("m.unionId", "=", unionId); + } + + private String getCurrentLineName(TourLedger ledger) { + if (ledger == null) { + return ""; + } + Sql sql = Sqls.create(""" + SELECT CASE + WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName) + ELSE CONCAT( + COALESCE(NULLIF(l.lineName, ''), t.lineName), + '(', + CASE + WHEN IFNULL(m.unionId, '') = '' THEN '校工会' + ELSE IFNULL(mu.name, '') + END, + ')' + ) + END AS lineName + FROM tour_ledger t + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 + LEFT JOIN sys_union mu ON mu.id = m.unionId + WHERE t.id = @ledgerId + """); + sql.setParam("ledgerId", ledger.getId()); + sql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(sql); + return StrUtil.blankToDefault(sql.getString(), ledger.getLineName()); + } + + private static class LineFilter { + private final String lineId; + private final String unionId; + + private LineFilter(String lineId, String unionId) { + this.lineId = lineId; + this.unionId = unionId; + } + + private String getLineId() { + return lineId; + } + + private String getUnionId() { + return unionId; + } + } + + private String currentUnionId() { + return SecurityUtil.getUnionId(); + } + + private String currentUnionName() { + String unionId = currentUnionId(); + if (StrUtil.isBlank(unionId)) { + return ""; + } + Sys_union union = tourLedgerService.dao().fetch(Sys_union.class, unionId); + return union == null ? "" : StrUtil.blankToDefault(union.getName(), ""); + } + + private String getOrderColumn(String orderName) { + if ("year".equals(orderName)) { + return "t.`year`"; + } + if ("jobNo".equals(orderName)) { + return "t.jobNo"; + } + if ("userName".equals(orderName)) { + return "t.userName"; + } + if ("lineName".equals(orderName)) { + return """ + CASE + WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName) + ELSE CONCAT( + COALESCE(NULLIF(l.lineName, ''), t.lineName), + '(', + CASE + WHEN IFNULL(m.unionId, '') = '' THEN '校工会' + ELSE IFNULL(mu.name, '') + END, + ')' + ) + END + """; + } + if ("travelPeriod".equals(orderName)) { + return "COALESCE(m.travelStartTime, tp.travelPeriod)"; + } + if ("lineType".equals(orderName)) { + return "t.lineType"; + } + if ("signupTime".equals(orderName)) { + return "t.signupTime"; + } + if ("familyCount".equals(orderName)) { + return "familyCount"; + } + if ("joined".equals(orderName)) { + return "t.joined"; + } + return "t.`year`"; + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/mode/TourLedgerImportExcelMode.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/mode/TourLedgerImportExcelMode.java new file mode 100644 index 00000000..b19393d0 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/mode/TourLedgerImportExcelMode.java @@ -0,0 +1,34 @@ +package com.budwk.app.zhgh.dayofficework.tour.mode; + +import cn.afterturn.easypoi.excel.annotation.Excel; +import com.budwk.app.base.model.ExcelImportError; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +@EqualsAndHashCode(callSuper = true) +@Data +public class TourLedgerImportExcelMode extends ExcelImportError { + + @Excel(name = "年度") + private Integer year; + + @Excel(name = "工号") + private String jobNo; + + @Excel(name = "姓名") + private String userName; + + @Excel(name = "报名时间", format = "yyyy-MM-dd HH:mm:ss") + private Date signupTime; + + @Excel(name = "线路名称") + private String lineName; + + @Excel(name = "线路类型") + private String lineType; + + @Excel(name = "是否参加") + private String joined; +} 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 new file mode 100644 index 00000000..0e1af548 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedger.java @@ -0,0 +1,152 @@ +package com.budwk.app.zhgh.dayofficework.tour.models; + +import com.budwk.app.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.nutz.dao.entity.annotation.*; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +import java.io.Serializable; + +/** + * 疗休养教职工报名台账。 + * 后续报名模块完成后,将已报名或已参加的教职工写入本表,台账页负责跨年度查询和详情查看。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Table("tour_ledger") +@TableMeta("{'mysql-charset':'utf8mb4'}") +@Comment("普惠疗休养台账") +public class TourLedger extends BaseModel implements Serializable { + + @Column + @Name + @Comment("ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + @PrevInsert(uu32 = true) + private String id; + + @Column + @Comment("年度") + @ColDefine(type = ColType.INT, width = 4) + private Integer year; + + @Column + @Comment("工号") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String jobNo; + + @Column + @Comment("姓名") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String userName; + + @Column + @Comment("性别") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String gender; + + @Column + @Comment("身份证号") + @ColDefine(type = ColType.VARCHAR, width = 30) + private String idCard; + + @Column + @Comment("所在单位ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String unitId; + + @Column + @Comment("所在单位") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String unitName; + + @Column + @Comment("所在工会ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String unionId; + + @Column + @Comment("所在工会") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String unionName; + + @Column + @Comment("报名时间") + @ColDefine(type = ColType.VARCHAR, width = 30) + private String signupTime; + + @Column + @Comment("报名事项ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String matterId; + + @Column + @Comment("报名线路ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String lineId; + + @Column + @Comment("报名线路") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String lineName; + + @Column + @Comment("线路类型") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String lineType; + + @Column + @Comment("报名酒店") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String hotelName; + + @Column + @Comment("旅行社ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String travelAgencyId; + + @Column + @Comment("旅行社") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String travelAgencyName; + + @Column + @Comment("是否携带家属") + @ColDefine(type = ColType.BOOLEAN) + @Default("0") + private Boolean hasFamily; + + @Column + @Comment("意向拼床人") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String intendedRoommate; + + @Column + @Comment("床型") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String bedType; + + @Column + @Comment("床位信息") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String bedInfo; + + @Column + @Comment("是否参加") + @ColDefine(type = ColType.BOOLEAN) + @Default("0") + private Boolean joined; + + @Column + @Comment("是否报销") + @ColDefine(type = ColType.BOOLEAN) + @Default("0") + private Boolean reimbursed; + + @Column + @Comment("报销超出费用") + @ColDefine(type = ColType.BOOLEAN) + @Default("0") + private Boolean overCostReimbursed; +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedgerDirectRelative.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedgerDirectRelative.java new file mode 100644 index 00000000..7021209a --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedgerDirectRelative.java @@ -0,0 +1,73 @@ +package com.budwk.app.zhgh.dayofficework.tour.models; + +import com.budwk.app.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.nutz.dao.entity.annotation.*; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +import java.io.Serializable; + +/** + * 直系亲属线路报名信息。 + * 与普通携带亲属信息分表存放,用于直系亲属线路的专属申请信息。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Table("tour_ledger_direct_relative") +@TableMeta("{'mysql-charset':'utf8mb4'}") +@Comment("普惠疗休养直系亲属线路报名信息") +public class TourLedgerDirectRelative extends BaseModel implements Serializable { + + @Column + @Name + @Comment("ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + @PrevInsert(uu32 = true) + private String id; + + @Column + @Comment("台账ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String ledgerId; + + @Column + @Comment("亲属姓名") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String relativeName; + + @Column + @Comment("所在单位") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String unitName; + + @Column + @Comment("亲属关系编码") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String relationshipCode; + + @Column + @Comment("亲属关系") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String relationshipName; + + @Column + @Comment("线路ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String lineId; + + @Column + @Comment("线路名称") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String lineName; + + @Column + @Comment("出行开始日期") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String travelStartTime; + + @Column + @Comment("出行结束日期") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String travelEndTime; +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedgerFamily.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedgerFamily.java new file mode 100644 index 00000000..87c18078 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedgerFamily.java @@ -0,0 +1,83 @@ +package com.budwk.app.zhgh.dayofficework.tour.models; + +import com.budwk.app.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.nutz.dao.entity.annotation.*; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +import java.io.Serializable; + +/** + * 疗休养台账家属信息。 + * 与教职工台账通过 ledgerId 关联,用于查看教职工携带家属的历史记录。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Table("tour_ledger_family") +@TableMeta("{'mysql-charset':'utf8mb4'}") +@Comment("普惠疗休养台账家属信息") +public class TourLedgerFamily extends BaseModel implements Serializable { + + @Column + @Name + @Comment("ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + @PrevInsert(uu32 = true) + private String id; + + @Column + @Comment("台账ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String ledgerId; + + @Column + @Comment("教职工工号") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String staffJobNo; + + @Column + @Comment("教职工姓名") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String staffName; + + @Column + @Comment("家属姓名") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String familyName; + + @Column + @Comment("性别") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String gender; + + @Column + @Comment("身份证号码") + @ColDefine(type = ColType.VARCHAR, width = 30) + private String idCard; + + @Column + @Comment("年龄") + @ColDefine(type = ColType.INT, width = 3) + private Integer age; + + @Column + @Comment("床型") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String bedType; + + @Column + @Comment("床位") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String bedInfo; + + @Column + @Comment("意向拼床人") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String intendedRoommate; + + @Column + @Comment("关系") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String relationship; +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLine.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLine.java new file mode 100644 index 00000000..9fbfaef4 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLine.java @@ -0,0 +1,106 @@ +package com.budwk.app.zhgh.dayofficework.tour.models; + +import com.budwk.app.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.nutz.dao.entity.annotation.*; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +import java.io.Serializable; + +/** + * 疗休养线路管理。 + * 当前阶段先维护线路基础信息,后续报名、台账等模块可通过线路ID继续关联。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Table("tour_line") +@TableMeta("{'mysql-charset':'utf8mb4'}") +@Comment("普惠疗休养线路") +public class TourLine extends BaseModel implements Serializable { + + @Column + @Name + @Comment("ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + @PrevInsert(uu32 = true) + private String id; + + @Column + @Comment("创建年度") + @ColDefine(type = ColType.INT, width = 4) + private Integer year; + + @Column + @Comment("线路编号") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String lineCode; + + @Column + @Comment("线路名称") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String lineName; + + @Column + @Comment("旅行社ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String travelAgencyId; + + @Column + @Comment("创建人ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String creatorUserId; + + @Column + @Comment("创建人") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String creatorName; + + @Column + @Comment("所在单位ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String unitId; + + @Column + @Comment("所在单位") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String unitName; + + @Column + @Comment("线路类型") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String lineType; + + @Column + @Comment("时间标段ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String lotId; + + @Column + @Comment("线路内容") + @ColDefine(type = ColType.TEXT) + private String lineContent; + + @Column + @Comment("移动端缩略图") + @ColDefine(type = ColType.VARCHAR, width = 500) + private String mobileThumb; + + @Column + @Comment("是否对外开放") + @ColDefine(type = ColType.BOOLEAN) + @Default("1") + private Boolean openFlag; + + @Column + @Comment("是否直系亲属线路") + @ColDefine(type = ColType.BOOLEAN) + @Default("0") + private Boolean directFamilyUnitLine; + + @Column + @Comment("激活状态") + @ColDefine(type = ColType.BOOLEAN) + @Default("1") + private Boolean enabled; +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourMatter.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourMatter.java new file mode 100644 index 00000000..f06aec8a --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourMatter.java @@ -0,0 +1,124 @@ +package com.budwk.app.zhgh.dayofficework.tour.models; + +import com.budwk.app.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.nutz.dao.entity.annotation.*; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * 疗休养事项。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Table("tour_matter") +@TableMeta("{'mysql-charset':'utf8mb4'}") +@Comment("普惠疗休养事项") +public class TourMatter extends BaseModel implements Serializable { + + @Column + @Name + @Comment("ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + @PrevInsert(uu32 = true) + private String id; + + @Column + @Comment("年度") + @ColDefine(type = ColType.INT, width = 4) + private Integer year; + + @Column + @Comment("事项名称") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String matterName; + + @Column + @Comment("疗休养配置ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String settingId; + + @Column + @Comment("创建人ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String creatorUserId; + + @Column + @Comment("创建人") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String creatorName; + + @Column + @Comment("所属工会ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String unionId; + + @Column + @Comment("组织形式") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String organizationType; + + @Column + @Comment("移动端缩略图") + @ColDefine(type = ColType.VARCHAR, width = 500) + private String mobileThumb; + + @Column + @Comment("线路ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String lineId; + + @Column + @Comment("报名开始时间") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String signupStartTime; + + @Column + @Comment("报名结束时间") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String signupEndTime; + + @Column + @Comment("出行开始时间") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String travelStartTime; + + @Column + @Comment("出行结束时间") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String travelEndTime; + + @Column + @Comment("联系人") + @ColDefine(type = ColType.VARCHAR, width = 30) + private String contactName; + + @Column + @Comment("联系方式") + @ColDefine(type = ColType.VARCHAR, width = 30) + private String contactPhone; + + @Column + @Comment("最少成团人数") + @ColDefine(type = ColType.INT) + private Integer minGroupPeople; + + @Column + @Comment("最多成团人数") + @ColDefine(type = ColType.INT) + private Integer maxGroupPeople; + + @Column + @Comment("预计费用") + @ColDefine(customType = "decimal(10,2)") + private BigDecimal estimatedCost; + + @Column + @Comment("事项状态") + @ColDefine(type = ColType.BOOLEAN) + @Default("1") + private Boolean enabled; +} 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 new file mode 100644 index 00000000..37e212d4 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSetting.java @@ -0,0 +1,134 @@ +package com.budwk.app.zhgh.dayofficework.tour.models; + +import com.budwk.app.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.nutz.dao.entity.annotation.*; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.List; + +/** + * 疗休养基础配置。 + * 这里先沉淀创建事项会复用的基础字段,后续线路、报名等阶段可以继续通过配置ID关联扩展。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Table("tour_setting") +@TableMeta("{'mysql-charset':'utf8mb4'}") +@Comment("普惠疗休养配置") +public class TourSetting extends BaseModel implements Serializable { + + @Column + @Name + @Comment("ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + @PrevInsert(uu32 = true) + private String id; + + @Column + @Comment("年度") + @ColDefine(type = ColType.INT, width = 4) + private Integer year; + + @Column + @Comment("疗休养配置名称") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String configName; + + @Column + @Comment("疗休养类型") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String tourType; + + @Column + @Comment("可参加人员范围ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String activityGroupId; + + @Column + @Comment("排序编号") + @ColDefine(type = ColType.INT) + private Integer sortNo; + + @Column + @Comment("最少成团人数") + @ColDefine(type = ColType.INT) + private Integer minGroupPeople; + + @Column + @Comment("最多成团人数") + @ColDefine(type = ColType.INT) + private Integer maxGroupPeople; + + @Column + @Comment("省外几年去一次") + @ColDefine(type = ColType.INT) + private Integer outProvinceYears; + + @Column + @Comment("省外人数占比") + @ColDefine(customType = "decimal(10,2)") + private BigDecimal outProvinceRatio; + + @Column + @Comment("省外人数占比类型") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String outProvinceRatioType; + + @Column + @Comment("省外固定人数") + @ColDefine(type = ColType.INT) + private Integer outProvinceFixedPeople; + + @Column + @Comment("周期开始年度") + @ColDefine(type = ColType.INT, width = 4) + private Integer cycleStartYear; + + @Column + @Comment("周期结束年度") + @ColDefine(type = ColType.INT, width = 4) + private Integer cycleEndYear; + + @Column + @Comment("周期内总费用") + @ColDefine(type = ColType.INT) + private Integer cycleTotalCost; + + @Column + @Comment("周期允许次数") + @ColDefine(type = ColType.INT) + private Integer cycleAllowedTimes; + + @Column + @Comment("是否允许携带家属") + @ColDefine(type = ColType.BOOLEAN) + @Default("0") + private Boolean allowFamily; + + @Column + @Comment("是否填报床位信息") + @ColDefine(type = ColType.BOOLEAN) + @Default("1") + private Boolean fillBedInfo; + + @Column + @Comment("是否启用") + @ColDefine(type = ColType.BOOLEAN) + @Default("1") + private Boolean enabled; + + /** + * 标段管理沿用老疗休养配置的子表设计,后续线路、旅行社等模块可通过标段ID继续关联。 + */ + @Many(field = "settingId") + private List lots; + + @Column + @Comment("服务须知") + @ColDefine(type = ColType.TEXT) + private String serviceNotice; +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSettingLot.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSettingLot.java new file mode 100644 index 00000000..9b502365 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSettingLot.java @@ -0,0 +1,53 @@ +package com.budwk.app.zhgh.dayofficework.tour.models; + +import com.budwk.app.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.nutz.dao.entity.annotation.*; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +import java.io.Serializable; + +/** + * 疗休养配置标段。 + * 标段从基础配置中拆成子表,便于后续线路、目的地、报名等模块复用同一标段口径。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Table("tour_setting_lot") +@TableMeta("{'mysql-charset':'utf8mb4'}") +@Comment("普惠疗休养配置标段") +public class TourSettingLot extends BaseModel implements Serializable { + + @Column + @Name + @Comment("ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + @PrevInsert(uu32 = true) + private String id; + + @Column + @Comment("所属配置ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String settingId; + + @Column + @Comment("标段名称") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String lotName; + + @Column + @Comment("标段值") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String lotValue; + + @Column + @Comment("标段费用") + @ColDefine(type = ColType.INT) + private Integer activityCost; + + @Column + @Comment("允许超出报销") + @ColDefine(type = ColType.BOOLEAN) + private Boolean allowOverReimbursement; +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourTravelAgency.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourTravelAgency.java new file mode 100644 index 00000000..88f0865c --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourTravelAgency.java @@ -0,0 +1,74 @@ +package com.budwk.app.zhgh.dayofficework.tour.models; + +import com.budwk.app.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.nutz.dao.entity.annotation.*; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +import java.io.Serializable; + +/** + * 旅行社管理。 + * 先维护疗休养线路创建会复用的旅行社基础信息,后续线路模块可通过旅行社ID关联。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Table("tour_travel_agency") +@TableMeta("{'mysql-charset':'utf8mb4'}") +@Comment("普惠疗休养旅行社") +public class TourTravelAgency extends BaseModel implements Serializable { + + @Column + @Name + @Comment("ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + @PrevInsert(uu32 = true) + private String id; + + @Column + @Comment("年度") + @ColDefine(type = ColType.INT, width = 4) + private Integer year; + + @Column + @Comment("旅行社编号") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String agencyCode; + + @Column + @Comment("旅行社名称") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String agencyName; + + @Column + @Comment("联系人") + @ColDefine(type = ColType.VARCHAR, width = 30) + private String contactName; + + @Column + @Comment("联系人手机") + @ColDefine(type = ColType.VARCHAR, width = 30) + private String contactPhone; + + @Column + @Comment("邮箱") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String email; + + @Column + @Comment("备注") + @ColDefine(type = ColType.TEXT) + private String remark; + + @Column + @Comment("移动端缩略图") + @ColDefine(type = ColType.VARCHAR, width = 500) + private String mobileThumb; + + @Column + @Comment("激活状态") + @ColDefine(type = ColType.BOOLEAN) + @Default("1") + private Boolean enabled; +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerDirectRelativeService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerDirectRelativeService.java new file mode 100644 index 00000000..f92bb263 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerDirectRelativeService.java @@ -0,0 +1,7 @@ +package com.budwk.app.zhgh.dayofficework.tour.service; + +import com.budwk.app.base.service.BaseService; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerDirectRelative; + +public interface TourLedgerDirectRelativeService extends BaseService { +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerFamilyService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerFamilyService.java new file mode 100644 index 00000000..f1ce741f --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerFamilyService.java @@ -0,0 +1,7 @@ +package com.budwk.app.zhgh.dayofficework.tour.service; + +import com.budwk.app.base.service.BaseService; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerFamily; + +public interface TourLedgerFamilyService extends BaseService { +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerService.java new file mode 100644 index 00000000..076a5dd4 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerService.java @@ -0,0 +1,7 @@ +package com.budwk.app.zhgh.dayofficework.tour.service; + +import com.budwk.app.base.service.BaseService; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger; + +public interface TourLedgerService extends BaseService { +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLineService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLineService.java new file mode 100644 index 00000000..240cf45c --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLineService.java @@ -0,0 +1,7 @@ +package com.budwk.app.zhgh.dayofficework.tour.service; + +import com.budwk.app.base.service.BaseService; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLine; + +public interface TourLineService extends BaseService { +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourMatterService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourMatterService.java new file mode 100644 index 00000000..8171b59b --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourMatterService.java @@ -0,0 +1,7 @@ +package com.budwk.app.zhgh.dayofficework.tour.service; + +import com.budwk.app.base.service.BaseService; +import com.budwk.app.zhgh.dayofficework.tour.models.TourMatter; + +public interface TourMatterService extends BaseService { +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourSettingService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourSettingService.java new file mode 100644 index 00000000..ca1cb4ef --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourSettingService.java @@ -0,0 +1,7 @@ +package com.budwk.app.zhgh.dayofficework.tour.service; + +import com.budwk.app.base.service.BaseService; +import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting; + +public interface TourSettingService extends BaseService { +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourTravelAgencyService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourTravelAgencyService.java new file mode 100644 index 00000000..f90ba290 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourTravelAgencyService.java @@ -0,0 +1,7 @@ +package com.budwk.app.zhgh.dayofficework.tour.service; + +import com.budwk.app.base.service.BaseService; +import com.budwk.app.zhgh.dayofficework.tour.models.TourTravelAgency; + +public interface TourTravelAgencyService extends BaseService { +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerDirectRelativeServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerDirectRelativeServiceImpl.java new file mode 100644 index 00000000..8da9d20b --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerDirectRelativeServiceImpl.java @@ -0,0 +1,15 @@ +package com.budwk.app.zhgh.dayofficework.tour.service.impl; + +import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerDirectRelative; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeService; +import org.nutz.dao.Dao; +import org.nutz.ioc.loader.annotation.IocBean; + +@IocBean(args = {"refer:dao"}) +public class TourLedgerDirectRelativeServiceImpl extends BaseServiceImpl implements TourLedgerDirectRelativeService { + + public TourLedgerDirectRelativeServiceImpl(Dao dao) { + super(dao); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerFamilyServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerFamilyServiceImpl.java new file mode 100644 index 00000000..0e045b61 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerFamilyServiceImpl.java @@ -0,0 +1,15 @@ +package com.budwk.app.zhgh.dayofficework.tour.service.impl; + +import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerFamily; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService; +import org.nutz.dao.Dao; +import org.nutz.ioc.loader.annotation.IocBean; + +@IocBean(args = {"refer:dao"}) +public class TourLedgerFamilyServiceImpl extends BaseServiceImpl implements TourLedgerFamilyService { + + public TourLedgerFamilyServiceImpl(Dao dao) { + super(dao); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerServiceImpl.java new file mode 100644 index 00000000..88c2f668 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerServiceImpl.java @@ -0,0 +1,15 @@ +package com.budwk.app.zhgh.dayofficework.tour.service.impl; + +import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService; +import org.nutz.dao.Dao; +import org.nutz.ioc.loader.annotation.IocBean; + +@IocBean(args = {"refer:dao"}) +public class TourLedgerServiceImpl extends BaseServiceImpl implements TourLedgerService { + + public TourLedgerServiceImpl(Dao dao) { + super(dao); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLineServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLineServiceImpl.java new file mode 100644 index 00000000..c06e6cf4 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLineServiceImpl.java @@ -0,0 +1,15 @@ +package com.budwk.app.zhgh.dayofficework.tour.service.impl; + +import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLine; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLineService; +import org.nutz.dao.Dao; +import org.nutz.ioc.loader.annotation.IocBean; + +@IocBean(args = {"refer:dao"}) +public class TourLineServiceImpl extends BaseServiceImpl implements TourLineService { + + public TourLineServiceImpl(Dao dao) { + super(dao); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourMatterServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourMatterServiceImpl.java new file mode 100644 index 00000000..c7e10dbe --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourMatterServiceImpl.java @@ -0,0 +1,15 @@ +package com.budwk.app.zhgh.dayofficework.tour.service.impl; + +import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.zhgh.dayofficework.tour.models.TourMatter; +import com.budwk.app.zhgh.dayofficework.tour.service.TourMatterService; +import org.nutz.dao.Dao; +import org.nutz.ioc.loader.annotation.IocBean; + +@IocBean(args = {"refer:dao"}) +public class TourMatterServiceImpl extends BaseServiceImpl implements TourMatterService { + + public TourMatterServiceImpl(Dao dao) { + super(dao); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourSettingServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourSettingServiceImpl.java new file mode 100644 index 00000000..68f57fcf --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourSettingServiceImpl.java @@ -0,0 +1,15 @@ +package com.budwk.app.zhgh.dayofficework.tour.service.impl; + +import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting; +import com.budwk.app.zhgh.dayofficework.tour.service.TourSettingService; +import org.nutz.dao.Dao; +import org.nutz.ioc.loader.annotation.IocBean; + +@IocBean(args = {"refer:dao"}) +public class TourSettingServiceImpl extends BaseServiceImpl implements TourSettingService { + + public TourSettingServiceImpl(Dao dao) { + super(dao); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourTravelAgencyServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourTravelAgencyServiceImpl.java new file mode 100644 index 00000000..5122784e --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourTravelAgencyServiceImpl.java @@ -0,0 +1,15 @@ +package com.budwk.app.zhgh.dayofficework.tour.service.impl; + +import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.zhgh.dayofficework.tour.models.TourTravelAgency; +import com.budwk.app.zhgh.dayofficework.tour.service.TourTravelAgencyService; +import org.nutz.dao.Dao; +import org.nutz.ioc.loader.annotation.IocBean; + +@IocBean(args = {"refer:dao"}) +public class TourTravelAgencyServiceImpl extends BaseServiceImpl implements TourTravelAgencyService { + + public TourTravelAgencyServiceImpl(Dao dao) { + super(dao); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/institution/controller/TeacherCongressInstitutionController.java b/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/institution/controller/TeacherCongressInstitutionController.java index 2dcb71d7..7e574d3c 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/institution/controller/TeacherCongressInstitutionController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/institution/controller/TeacherCongressInstitutionController.java @@ -25,6 +25,7 @@ import org.nutz.dao.util.cri.SqlExpressionGroup; import org.nutz.ioc.aop.Aop; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.Strings; import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.Ok; @@ -68,11 +69,15 @@ public class TeacherCongressInstitutionController { Teacher_congress_institution rootInstitution = new Teacher_congress_institution(); rootInstitution.setId("0"); rootInstitution.setParentId(""); - rootInstitution.setName("教代会机构"); + rootInstitution.setName("两代会组织机构"); rootInstitution.setLocation(0); institutionList.add(rootInstitution); - List> treeNodes = institutionList.stream().map(institution -> new TreeNode<>(institution.getId(), institution.getParentId(), institution.getName(), institution.getLocation())).toList(); + List> treeNodes = institutionList.stream().map(institution -> { + TreeNode treeNode = new TreeNode<>(institution.getId(), institution.getParentId(), institution.getName(), institution.getLocation()); + treeNode.setExtra(Map.of("code", Strings.sNull(institution.getCode()))); + return treeNode; + }).toList(); List> treeList = TreeUtil.build(treeNodes, ""); return Result.success(treeList); } @@ -97,6 +102,16 @@ public class TeacherCongressInstitutionController { return Result.success(Map.of("treeFlat", children, "treeList", treeList)); } + @At + @SaCheckPermission("tc.institution") + public Result specialCommitteeRoleOptions() { + Sys_dict parent = sysDictService.fetch(Cnd.where(Sys_dict::getCode, "=", "SPECIAL_COMMITTEE_ROLES")); + if (ObjectUtil.isEmpty(parent)) { + return Result.success(List.of()); + } + List children = sysDictService.query(Cnd.where(Sys_dict::getPath, "like", parent.getPath() + "%").and(Sys_dict::getId, "!=", parent.getId()).asc(Sys_dict::getLocation)); + return Result.success(children); + } /** * 机构分页查询 @@ -113,6 +128,7 @@ public class TeacherCongressInstitutionController { cnd.and("parentId", "=", parentId); cnd.and("sessionId", "=", sessionId); cnd.asc("location"); + cnd.asc("code"); Pagination pagination = baseService.listPage(pageForm.getPageNumber(), pageForm.getPageSize(), "teacher_congress_institution", cnd); return Result.success(pagination); } @@ -129,7 +145,7 @@ public class TeacherCongressInstitutionController { if (dao.count(Teacher_congress_institution.class, Cnd.where(Teacher_congress_institution::getCode, "=", institution.getCode()).and(Teacher_congress_institution::getSessionId, "=", institution.getSessionId())) > 0) { return Result.error("机构已存在"); } - if (StrUtil.isNotBlank(institution.getParentId()) && !institution.getParentId().equals("0")) { + if (StrUtil.isNotBlank(institution.getParentId()) && !institution.getParentId().equals("0") && dao.count(Teacher_congress_institution.class, Cnd.where(Teacher_congress_institution::getId, "=", institution.getParentId()).and(Teacher_congress_institution::getSessionId, "=", institution.getSessionId())) == 0) { Sys_dict sysDict = sysDictService.fetch(Cnd.where(Sys_dict::getId, "=", institution.getParentId())); Teacher_congress_institution parentInstitution = dao.fetch(Teacher_congress_institution.class, Cnd.where(Teacher_congress_institution::getCode, "=", sysDict.getCode()) @@ -151,10 +167,34 @@ public class TeacherCongressInstitutionController { * @param id * @return */ + @At + @SaCheckPermission("tc.institution") + public Result update(Teacher_congress_institution institution) { + Teacher_congress_institution dbInstitution = dao.fetch(Teacher_congress_institution.class, institution.getId()); + if (ObjectUtil.isEmpty(dbInstitution)) { + return Result.error("机构不存在"); + } + dbInstitution.setName(institution.getName()); + dbInstitution.setIntroduce(institution.getIntroduce()); + dbInstitution.setLocation(institution.getLocation()); + dao.update(dbInstitution); + return Result.success(); + } + @At @SaCheckPermission("tc.institution") @Aop(TransAop.READ_COMMITTED) public Result delete(@Valid String id) { + Teacher_congress_institution institution = dao.fetch(Teacher_congress_institution.class, id); + if (ObjectUtil.isEmpty(institution)) { + return Result.error("机构不存在"); + } + List siblingList = dao.query(Teacher_congress_institution.class, Cnd.where(Teacher_congress_institution::getParentId, "=", institution.getParentId()).and(Teacher_congress_institution::getSessionId, "=", institution.getSessionId()).asc(Teacher_congress_institution::getLocation).asc(Teacher_congress_institution::getCode)); + for (int i = 0; i < siblingList.size() && i < 6; i++) { + if (id.equals(siblingList.get(i).getId())) { + return Result.error("你选择的组织机构是两代会基本机构,不允许删除。"); + } + } dao.delete(Teacher_congress_institution.class, id); dao.clear(Teacher_congress_institution_user.class, Cnd.where("institutionId", "=", id)); return Result.success(); @@ -214,8 +254,8 @@ public class TeacherCongressInstitutionController { @SaCheckPermission("tc.institution") @SLog(tag = "教代会-机构设置", msg = "添加人员") @Aop(TransAop.READ_COMMITTED) - public Result userInsert(@Valid String userId, @Valid String institutionId, @Valid String sessionId, @Valid String identity) { - teacherCongressInstitutionUserService.userInsert(userId, institutionId, sessionId, identity); + public Result userInsert(@Valid String userId, @Valid String institutionId, @Valid String sessionId, @Valid String identity, String roleCode) { + teacherCongressInstitutionUserService.userInsert(userId, institutionId, sessionId, identity, roleCode); return Result.success(); } diff --git a/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/institution/service/TeacherCongressInstitutionUserService.java b/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/institution/service/TeacherCongressInstitutionUserService.java index 302aa97f..83472c4b 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/institution/service/TeacherCongressInstitutionUserService.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/institution/service/TeacherCongressInstitutionUserService.java @@ -14,7 +14,7 @@ public interface TeacherCongressInstitutionUserService extends BaseService outlines = dao.query(LearningCourseOutline.class, Cnd.where("courseId", "=", courseId).asc("sortOrder").asc("createdAt")); + Map> childrenMap = new HashMap<>(); + List roots = new ArrayList<>(); + for (LearningCourseOutline outline : outlines) { + NutMap map = NutMap.NEW() + .addv("id", outline.getId()) + .addv("courseId", outline.getCourseId()) + .addv("courseName", outline.getCourseName()) + .addv("parentId", outline.getParentId()) + .addv("nodeType", outline.getNodeType()) + .addv("title", outline.getTitle()) + .addv("subtitle", outline.getSubtitle()) + .addv("description", outline.getDescription()) + .addv("sortOrder", outline.getSortOrder()) + .addv("required", outline.getRequired()) + .addv("status", outline.getStatus()) + .addv("children", new ArrayList<>()); + String parentId = StrUtil.blankToDefault(outline.getParentId(), ""); + childrenMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(map); + } + roots.addAll(childrenMap.getOrDefault("", new ArrayList<>())); + appendChildren(roots, childrenMap); + return Result.success(roots); + } + + private void appendChildren(List nodes, Map> childrenMap) { + for (NutMap node : nodes) { + List children = childrenMap.getOrDefault(node.getString("id"), new ArrayList<>()); + node.put("children", children); + appendChildren(children, childrenMap); + } + } + + @At + @ApiOperation("新增/编辑大纲节点") + @SaCheckPermission("learning.chapter.content") + @SLog(tag = "保存大纲节点", msg = "节点标题:${args[0].title}") + public Result saveNode(LearningCourseOutline outline) { + Result checkResult = checkNode(outline); + if (checkResult != null) { + return checkResult; + } + LearningCourse course = dao.fetch(LearningCourse.class, outline.getCourseId()); + outline.setCourseName(course == null ? outline.getCourseName() : course.getCourseName()); + if ("chapter".equals(outline.getNodeType())) { + outline.setParentId(""); + } + if (outline.getRequired() == null) { + outline.setRequired(false); + } + if (StrUtil.isBlank(outline.getStatus())) { + outline.setStatus("enabled"); + } + if (outline.getSortOrder() == null) { + outline.setSortOrder(nextNodeSort(outline.getCourseId(), outline.getParentId())); + } + if (StrUtil.isBlank(outline.getId())) { + dao.insert(outline); + } else { + dao.updateIgnoreNull(outline); + } + ensureRule(outline.getCourseId(), "outline", outline.getId(), outline.getRequired(), "enabled"); + return Result.success(); + } + + @At + @ApiOperation("删除大纲节点") + @SaCheckPermission("learning.chapter.content") + @SLog(tag = "删除大纲节点", msg = "节点ID:${args[0]}") + public Result deleteNode(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("请选择要删除的数据"); + } + int childCount = dao.count(LearningCourseOutline.class, Cnd.where("parentId", "=", id)); + int resourceCount = dao.count(LearningOutlineResource.class, Cnd.where("outlineId", "=", id)); + if (childCount > 0 || resourceCount > 0) { + return Result.error("该节点下存在子节点或学习资料,请先删除后再操作"); + } + dao.clear(LearningStudyRule.class, Cnd.where("targetId", "=", id).and("targetType", "=", "outline")); + dao.clear(LearningCourseOutline.class, Cnd.where("id", "=", id)); + return Result.success(); + } + + @At + @ApiOperation("启用/禁用大纲节点") + @SaCheckPermission("learning.chapter.content") + public Result toggleNode(String id, String status) { + if (StrUtil.isBlank(id)) { + return Result.error("请选择要操作的数据"); + } + dao.update(LearningCourseOutline.class, Chain.make("status", status), Cnd.where("id", "=", id)); + dao.update(LearningStudyRule.class, Chain.make("status", status), Cnd.where("targetId", "=", id).and("targetType", "=", "outline")); + return Result.success(); + } + + @At + @ApiOperation("移动大纲节点") + @SaCheckPermission("learning.chapter.content") + public Result moveNode(String id, String direction) { + LearningCourseOutline current = dao.fetch(LearningCourseOutline.class, id); + if (current == null) { + return Result.error("节点不存在"); + } + Cnd cnd = Cnd.where("courseId", "=", current.getCourseId()).and("parentId", "=", StrUtil.blankToDefault(current.getParentId(), "")); + if ("up".equals(direction)) { + cnd.and("sortOrder", "<", current.getSortOrder()).desc("sortOrder"); + } else { + cnd.and("sortOrder", ">", current.getSortOrder()).asc("sortOrder"); + } + LearningCourseOutline target = dao.fetch(LearningCourseOutline.class, cnd); + if (target == null) { + return Result.success(); + } + Integer currentSort = current.getSortOrder(); + dao.update(LearningCourseOutline.class, Chain.make("sortOrder", target.getSortOrder()), Cnd.where("id", "=", current.getId())); + dao.update(LearningCourseOutline.class, Chain.make("sortOrder", currentSort), Cnd.where("id", "=", target.getId())); + return Result.success(); + } + + @At + @ApiOperation("学习资料分页") + @SaCheckPermission("learning.chapter.content") + public Result resourcePage(PageForm pageForm, String outlineId) { + if (StrUtil.isBlank(outlineId)) { + return Result.success(new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), 0, List.of())); + } + Cnd cnd = Cnd.where("outlineId", "=", outlineId); + String orderColumn = getResourceOrderColumn(pageForm.getPageOrderName()); + if (StrUtil.isBlank(orderColumn)) { + cnd.asc("sortOrder").asc("createdAt"); + } else if ("descending".equals(pageForm.getPageOrderBy())) { + cnd.desc(orderColumn); + } else { + cnd.asc(orderColumn); + } + int count = dao.count(LearningOutlineResource.class, Cnd.where("outlineId", "=", outlineId)); + Pager pager = dao.createPager(pageForm.getPageNumber(), pageForm.getPageSize()); + return Result.success(new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, dao.query(LearningOutlineResource.class, cnd, pager))); + } + + @At + @ApiOperation("新增/编辑学习资料") + @SaCheckPermission("learning.chapter.content") + @SLog(tag = "保存学习资料", msg = "资料标题:${args[0].resourceTitle}") + public Result saveResource(LearningOutlineResource resource) { + Result checkResult = checkResource(resource); + if (checkResult != null) { + return checkResult; + } + if (resource.getRequired() == null) { + resource.setRequired(false); + } + if (resource.getAllowPreview() == null) { + resource.setAllowPreview(true); + } + if (resource.getAllowDownload() == null) { + resource.setAllowDownload(false); + } + if (StrUtil.isBlank(resource.getStatus())) { + resource.setStatus("enabled"); + } + if (resource.getSortOrder() == null) { + resource.setSortOrder(nextResourceSort(resource.getOutlineId())); + } + if (StrUtil.isBlank(resource.getId())) { + dao.insert(resource); + } else { + dao.updateIgnoreNull(resource); + } + ensureRule(resource.getCourseId(), "resource", resource.getId(), resource.getRequired(), resource.getStatus()); + return Result.success(); + } + + @At + @ApiOperation("删除学习资料") + @SaCheckPermission("learning.chapter.content") + @SLog(tag = "删除学习资料", msg = "资料ID:${args[0]}") + public Result deleteResource(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("请选择要删除的数据"); + } + dao.clear(LearningStudyRule.class, Cnd.where("targetId", "=", id).and("targetType", "=", "resource")); + dao.clear(LearningOutlineResource.class, Cnd.where("id", "=", id)); + return Result.success(); + } + + @At + @ApiOperation("查询学习规则") + @SaCheckPermission("learning.chapter.content") + public Result getRule(String courseId, String targetType, String targetId) { + LearningStudyRule rule = dao.fetch(LearningStudyRule.class, Cnd.where("courseId", "=", courseId).and("targetType", "=", targetType).and("targetId", "=", targetId)); + if (rule == null) { + rule = defaultRule(courseId, targetType, targetId, false, "enabled"); + } + return Result.success(rule); + } + + @At + @ApiOperation("保存学习规则") + @SaCheckPermission("learning.chapter.content") + public Result saveRule(LearningStudyRule rule) { + if (StrUtil.isBlank(rule.getCourseId()) || StrUtil.isBlank(rule.getTargetType()) || StrUtil.isBlank(rule.getTargetId())) { + return Result.error("规则对象不能为空"); + } + if (StrUtil.isBlank(rule.getStatus())) { + rule.setStatus("enabled"); + } + LearningStudyRule old = dao.fetch(LearningStudyRule.class, Cnd.where("courseId", "=", rule.getCourseId()).and("targetType", "=", rule.getTargetType()).and("targetId", "=", rule.getTargetId())); + if (old == null) { + dao.insert(rule); + } else { + rule.setId(old.getId()); + dao.updateIgnoreNull(rule); + } + if ("outline".equals(rule.getTargetType())) { + dao.update(LearningCourseOutline.class, Chain.make("required", rule.getRequired()).add("status", rule.getStatus()), Cnd.where("id", "=", rule.getTargetId())); + } + if ("resource".equals(rule.getTargetType())) { + dao.update(LearningOutlineResource.class, Chain.make("required", rule.getRequired()).add("status", rule.getStatus()), Cnd.where("id", "=", rule.getTargetId())); + } + return Result.success(); + } + + private Result checkNode(LearningCourseOutline outline) { + if (outline == null || StrUtil.isBlank(outline.getCourseId())) { + return Result.error("请选择课程"); + } + if (StrUtil.isBlank(outline.getNodeType())) { + return Result.error("请选择节点类型"); + } + if (StrUtil.isBlank(outline.getTitle())) { + return Result.error("标题不能为空"); + } + if ("section".equals(outline.getNodeType()) && StrUtil.isBlank(outline.getParentId())) { + return Result.error("新增节需要选择所属章"); + } + return null; + } + + private Result checkResource(LearningOutlineResource resource) { + if (resource == null || StrUtil.isBlank(resource.getCourseId()) || StrUtil.isBlank(resource.getOutlineId())) { + return Result.error("请选择章/节节点"); + } + if (StrUtil.isBlank(resource.getResourceTitle())) { + return Result.error("资料标题不能为空"); + } + if (StrUtil.isBlank(resource.getResourceType())) { + return Result.error("请选择资料类型"); + } + if (StrUtil.isBlank(resource.getFileData())) { + return Result.error("请上传附件"); + } + return null; + } + + private Integer nextNodeSort(String courseId, String parentId) { + LearningCourseOutline outline = dao.fetch(LearningCourseOutline.class, Cnd.where("courseId", "=", courseId).and("parentId", "=", StrUtil.blankToDefault(parentId, "")).desc("sortOrder")); + return outline == null || outline.getSortOrder() == null ? 1 : outline.getSortOrder() + 1; + } + + private Integer nextResourceSort(String outlineId) { + LearningOutlineResource resource = dao.fetch(LearningOutlineResource.class, Cnd.where("outlineId", "=", outlineId).desc("sortOrder")); + return resource == null || resource.getSortOrder() == null ? 1 : resource.getSortOrder() + 1; + } + + private void ensureRule(String courseId, String targetType, String targetId, Boolean required, String status) { + LearningStudyRule rule = dao.fetch(LearningStudyRule.class, Cnd.where("courseId", "=", courseId).and("targetType", "=", targetType).and("targetId", "=", targetId)); + if (rule == null) { + dao.insert(defaultRule(courseId, targetType, targetId, required, status)); + } else { + dao.update(LearningStudyRule.class, + Chain.make("required", required != null && required).add("status", StrUtil.blankToDefault(status, "enabled")), + Cnd.where("id", "=", rule.getId())); + } + } + + private LearningStudyRule defaultRule(String courseId, String targetType, String targetId, Boolean required, String status) { + LearningStudyRule rule = new LearningStudyRule(); + rule.setCourseId(courseId); + rule.setTargetType(targetType); + rule.setTargetId(targetId); + rule.setRequired(required != null && required); + rule.setStudyMode("mixed"); + rule.setCompletionRule("all_required_resource"); + rule.setCompletePercent(90); + rule.setMinStudySeconds(0); + rule.setUnlockRule("free"); + rule.setAllowSkip(false); + rule.setAllowDrag(true); + rule.setPauseCountTime(false); + rule.setHiddenCountTime(false); + rule.setInactiveCountTime(false); + rule.setStatus(StrUtil.blankToDefault(status, "enabled")); + return rule; + } + + private String getResourceOrderColumn(String prop) { + Map columns = new HashMap<>(); + columns.put("resourceTitle", "resourceTitle"); + columns.put("resourceType", "resourceType"); + columns.put("fileExt", "fileExt"); + columns.put("durationSeconds", "durationSeconds"); + columns.put("sortOrder", "sortOrder"); + columns.put("required", "required"); + columns.put("allowPreview", "allowPreview"); + columns.put("allowDownload", "allowDownload"); + columns.put("status", "status"); + return columns.get(prop); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/learning/controller/LearningCourseDisplayController.java b/src/main/java/com/budwk/app/zhgh/learning/controller/LearningCourseDisplayController.java new file mode 100644 index 00000000..8190e50f --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/learning/controller/LearningCourseDisplayController.java @@ -0,0 +1,297 @@ +package com.budwk.app.zhgh.learning.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; +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.base.utils.OfficePlusUtil; +import com.budwk.app.sys.enums.SysFileEngineTypeEnum; +import com.budwk.app.sys.models.Sys_file; +import com.budwk.app.sys.models.Sys_dict; +import com.budwk.app.sys.services.SysDictService; +import com.budwk.app.sys.utils.SysFileMinIoUtil; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.learning.models.LearningCourseOutline; +import com.budwk.app.zhgh.learning.models.LearningCourseType; +import com.budwk.app.zhgh.learning.models.LearningOutlineResource; +import com.budwk.app.zhgh.learning.models.LearningStudyRecord; +import com.google.common.net.HttpHeaders; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.nutz.dao.Cnd; +import org.nutz.dao.Dao; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.Strings; +import org.nutz.lang.util.NutMap; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; + +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URLEncoder; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import java.util.List; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +@IocBean +@Ok("json:full") +@Api("学习教育平台-课程展示") +@At("/platform/learning/course/display") +public class LearningCourseDisplayController { + + @Inject + private Dao dao; + + @Inject + private SysDictService sysDictService; + + @At("") + @Ok("beetl:/platform/zhgh/Learning/courseDisplay/index.html") + @SaCheckPermission("learning.course.display") + public void index() { + } + + @At("/detail") + @Ok("beetl:/platform/zhgh/Learning/courseDisplay/detail.html") + @SaCheckPermission("learning.course.display") + public void detail() { + } + + @At("/study") + @Ok("beetl:/platform/zhgh/Learning/courseDisplay/study.html") + @SaCheckPermission("learning.course.display") + public void study() { + } + + @At + @ApiOperation("课程展示列表") + @SaCheckPermission("learning.course.display") + public Result pageData(PageForm pageForm, String keyword, String courseTypeId, String recommendFlag) { + Cnd cnd = Cnd.where("c.status", "=", "published"); + if (StrUtil.isNotBlank(keyword)) { + cnd.and(Cnd.exps("c.courseName", "like", "%" + keyword + "%") + .or("c.courseIntro", "like", "%" + keyword + "%")); + } + cnd.andEX("c.courseTypeId", "=", courseTypeId); + if (StrUtil.isNotBlank(recommendFlag)) { + cnd.and("c.recommendFlags", "like", "%" + recommendFlag + "%"); + } + + Sql countSql = Sqls.create(""" + SELECT COUNT(1) + FROM learning_course c + LEFT JOIN learning_course_type t ON t.id = c.courseTypeId + $condition + """); + countSql.setCondition(cnd); + countSql.setCallback(Sqls.callback.integer()); + dao.execute(countSql); + int count = countSql.getInt(); + + Sql listSql = Sqls.create(""" + SELECT c.id, c.courseName, c.courseIntro, c.startTime, c.endTime, c.openType, c.cover, + c.recommendFlags, c.courseTypeId, c.lecturerName, c.sortNum, t.typeName AS courseTypeName + FROM learning_course c + LEFT JOIN learning_course_type t ON t.id = c.courseTypeId + $condition + ORDER BY c.sortNum ASC, c.createdAt DESC + """); + listSql.setCondition(cnd); + listSql.setPager(dao.createPager(pageForm.getPageNumber(), pageForm.getPageSize())); + listSql.setCallback(Sqls.callback.maps()); + dao.execute(listSql); + List rows = listSql.getList(NutMap.class); + rows.forEach(row -> { + Object startTime = row.get("startTime"); + Object endTime = row.get("endTime"); + row.put("startTimeText", startTime == null ? "" : DateUtil.formatDateTime(DateUtil.date((java.util.Date) startTime))); + row.put("endTimeText", endTime == null ? "" : DateUtil.formatDateTime(DateUtil.date((java.util.Date) endTime))); + }); + return Result.success(new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, rows)); + } + + @At + @ApiOperation("课程学习基础信息") + @SaCheckPermission("learning.course.display") + public Result courseInfo(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("请选择课程"); + } + Sql sql = Sqls.create(""" + SELECT c.id, c.courseName, c.courseIntro, c.startTime, c.endTime, c.openType, c.cover, + c.recommendFlags, c.lecturerName, t.typeName AS courseTypeName + FROM learning_course c + LEFT JOIN learning_course_type t ON t.id = c.courseTypeId + WHERE c.id = @id + """); + sql.params().set("id", id); + sql.setCallback(Sqls.callback.map()); + dao.execute(sql); + NutMap course = sql.getObject(NutMap.class); + if (course == null) { + return Result.error("课程不存在"); + } + Object startTime = course.get("startTime"); + Object endTime = course.get("endTime"); + course.put("startTimeText", startTime == null ? "" : DateUtil.formatDateTime(DateUtil.date((java.util.Date) startTime))); + course.put("endTimeText", endTime == null ? "" : DateUtil.formatDateTime(DateUtil.date((java.util.Date) endTime))); + return Result.success(course); + } + + @At + @ApiOperation("课程学习安排") + @SaCheckPermission("learning.course.display") + public Result studyTree(String courseId) { + if (StrUtil.isBlank(courseId)) { + return Result.success(List.of()); + } + List outlines = dao.query(LearningCourseOutline.class, Cnd.where("courseId", "=", courseId).and("status", "=", "enabled").asc("sortOrder").asc("createdAt")); + List resources = dao.query(LearningOutlineResource.class, Cnd.where("courseId", "=", courseId).and("status", "=", "enabled").asc("sortOrder").asc("createdAt")); + List records = dao.query(LearningStudyRecord.class, Cnd.where("courseId", "=", courseId).and("userId", "=", SecurityUtil.getUserId())); + Map> childrenMap = new HashMap<>(); + List roots = new ArrayList<>(); + Map recordMap = new HashMap<>(); + for (LearningStudyRecord record : records) { + recordMap.put(record.getOutlineId(), record); + } + + Map outlineNodeMap = new HashMap<>(); + for (LearningCourseOutline outline : outlines) { + List children = new ArrayList<>(); + NutMap node = NutMap.NEW() + .addv("id", outline.getId()) + .addv("type", "outline") + .addv("nodeType", outline.getNodeType()) + .addv("title", outline.getTitle()) + .addv("required", outline.getRequired()) + .addv("progressPercent", recordMap.containsKey(outline.getId()) ? recordMap.get(outline.getId()).getProgressPercent() : 0) + .addv("studySeconds", recordMap.containsKey(outline.getId()) ? recordMap.get(outline.getId()).getStudySeconds() : 0) + .addv("lastPositionSeconds", recordMap.containsKey(outline.getId()) ? value(recordMap.get(outline.getId()).getLastPositionSeconds()) : 0) + .addv("completeStatus", recordMap.containsKey(outline.getId()) ? recordMap.get(outline.getId()).getCompleteStatus() : "not_started") + .addv("children", children); + outlineNodeMap.put(outline.getId(), node); + childrenMap.put(outline.getId(), children); + } + for (LearningCourseOutline outline : outlines) { + NutMap node = outlineNodeMap.get(outline.getId()); + if (StrUtil.isBlank(outline.getParentId())) { + roots.add(node); + } else { + childrenMap.computeIfAbsent(outline.getParentId(), key -> new ArrayList<>()).add(node); + } + } + for (LearningOutlineResource resource : resources) { + NutMap node = NutMap.NEW() + .addv("id", resource.getId()) + .addv("type", "resource") + .addv("outlineId", resource.getOutlineId()) + .addv("title", resource.getResourceTitle()) + .addv("resourceType", resource.getResourceType()) + .addv("fileExt", resource.getFileExt()) + .addv("fileData", resource.getFileData()) + .addv("durationSeconds", resource.getDurationSeconds()) + .addv("required", resource.getRequired()) + .addv("progressPercent", recordMap.containsKey(resource.getOutlineId()) ? recordMap.get(resource.getOutlineId()).getProgressPercent() : 0) + .addv("studySeconds", recordMap.containsKey(resource.getOutlineId()) ? recordMap.get(resource.getOutlineId()).getStudySeconds() : 0) + .addv("lastPositionSeconds", recordMap.containsKey(resource.getOutlineId()) ? value(recordMap.get(resource.getOutlineId()).getLastPositionSeconds()) : 0) + .addv("completeStatus", recordMap.containsKey(resource.getOutlineId()) ? recordMap.get(resource.getOutlineId()).getCompleteStatus() : "not_started"); + childrenMap.computeIfAbsent(resource.getOutlineId(), key -> new ArrayList<>()).add(node); + } + return Result.success(roots); + } + + @At + @Ok("void") + @ApiOperation("PDF内联预览") + @SaCheckPermission("learning.course.display") + public void pdfPreview(String id, HttpServletResponse response) throws IOException { + if (StrUtil.isBlank(id)) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "文件ID不能为空"); + return; + } + Sys_file file = dao.fetch(Sys_file.class, id); + if (file == null) { + response.sendError(HttpServletResponse.SC_NOT_FOUND, "文件不存在"); + return; + } + byte[] bytes; + if (SysFileEngineTypeEnum.MINIO.getValue().equals(file.getEngine())) { + bytes = SysFileMinIoUtil.getFileBytes(file.getBucket(), file.getStoragePath()); + } else { + File localFile = FileUtil.file(file.getStoragePath()); + if (!FileUtil.exist(localFile)) { + response.sendError(HttpServletResponse.SC_NOT_FOUND, "文件不存在"); + return; + } + bytes = IoUtil.readBytes(FileUtil.getInputStream(localFile)); + } + if (!"pdf".equalsIgnoreCase(file.getSuffix())) { + File sourceFile = File.createTempFile("learning_preview_origin", "." + file.getSuffix()); + File pdfFile = File.createTempFile("learning_preview", ".pdf"); + try { + Files.write(sourceFile.toPath(), bytes, StandardOpenOption.WRITE); + OfficePlusUtil.convert(sourceFile.getPath(), pdfFile.getPath()); + if (!FileUtil.exist(pdfFile) || pdfFile.length() == 0) { + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "文件转换PDF失败"); + return; + } + bytes = IoUtil.readBytes(FileUtil.getInputStream(pdfFile)); + } finally { + FileUtil.del(sourceFile); + FileUtil.del(pdfFile); + } + } + String fileName = StrUtil.blankToDefault(FileUtil.mainName(file.getName()), "preview") + ".pdf"; + String encodedName = URLEncoder.encode(fileName, CharsetUtil.UTF_8).replace("+", "%20"); + response.setContentType("application/pdf"); + response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"preview.pdf\"; filename*=UTF-8''" + encodedName); + response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(bytes.length)); + try (OutputStream out = response.getOutputStream()) { + out.write(bytes); + } + } + + @At + @ApiOperation("启用课程类型") + @SaCheckPermission("learning.course.display") + public Result courseTypes() { + return Result.success(dao.query(LearningCourseType.class, Cnd.where("enabled", "=", true).asc("sortNum").asc("createdAt"))); + } + + @At + @ApiOperation("推荐标识") + @SaCheckPermission("learning.course.display") + public Result recommendOptions() { + Sys_dict root = sysDictService.fetch(Cnd.where("code", "=", "学习教育").or("name", "=", "学习教育")); + if (root == null) { + return Result.success(List.of()); + } + List firstLevel = sysDictService.query(Cnd.where("parentId", "=", Strings.sNull(root.getId())).and("disabled", "=", false).asc("location")); + Sys_dict group = firstLevel.stream() + .filter(dict -> "推荐标识".equals(dict.getName()) || "推荐标识".equals(dict.getCode())) + .findFirst() + .orElse(null); + if (group == null) { + return Result.success(firstLevel); + } + List children = sysDictService.query(Cnd.where("parentId", "=", Strings.sNull(group.getId())).and("disabled", "=", false).asc("location")); + return Result.success(children.isEmpty() ? firstLevel : children); + } + + private int value(Integer value) { + return value == null ? 0 : value; + } +} diff --git a/src/main/java/com/budwk/app/zhgh/learning/controller/LearningCourseManageController.java b/src/main/java/com/budwk/app/zhgh/learning/controller/LearningCourseManageController.java new file mode 100644 index 00000000..be7e688d --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/learning/controller/LearningCourseManageController.java @@ -0,0 +1,232 @@ +package com.budwk.app.zhgh.learning.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.annotation.SLog; +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.sys.models.Sys_dict; +import com.budwk.app.sys.services.SysDictService; +import com.budwk.app.zhgh.learning.models.LearningCourse; +import com.budwk.app.zhgh.learning.models.LearningCourseOutline; +import com.budwk.app.zhgh.learning.models.LearningCourseType; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.nutz.dao.Cnd; +import org.nutz.dao.Dao; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.Strings; +import org.nutz.lang.util.NutMap; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@IocBean +@Ok("json:full") +@Api("学习教育平台-课程管理") +@At("/platform/learning/course/manage") +public class LearningCourseManageController { + + @Inject + private Dao dao; + + @Inject + private SysDictService sysDictService; + + @At("") + @Ok("beetl:/platform/zhgh/Learning/courseManage/index.html") + @SaCheckPermission("learning.course.manage") + public void index() { + } + + @At + @ApiOperation("课程列表") + @SaCheckPermission("learning.course.manage") + public Result pageData(PageForm pageForm, + String courseName, + String courseTypeId, + String startTime, + String endTime, + String lecturerName, + String status, + String recommendFlag) { + Cnd cnd = Cnd.NEW(); + cnd.and(Cnd.likeEX("c.courseName", courseName)); + cnd.andEX("c.courseTypeId", "=", courseTypeId); + cnd.and(Cnd.likeEX("c.lecturerName", lecturerName)); + cnd.andEX("c.status", "=", status); + if (StrUtil.isNotBlank(recommendFlag)) { + cnd.and("c.recommendFlags", "like", "%" + recommendFlag + "%"); + } + if (StrUtil.isNotBlank(startTime)) { + cnd.and("c.startTime", ">=", DateUtil.parse(startTime)); + } + if (StrUtil.isNotBlank(endTime)) { + cnd.and("c.endTime", "<=", DateUtil.parse(endTime)); + } + + String orderColumn = getOrderColumn(pageForm.getPageOrderName()); + String orderBy = "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc"; + if (StrUtil.isBlank(orderColumn)) { + orderColumn = "c.sortNum"; + orderBy = "asc"; + } + + Sql countSql = Sqls.create(""" + SELECT COUNT(1) + FROM learning_course c + LEFT JOIN learning_course_type t ON t.id = c.courseTypeId + $condition + """); + countSql.setCondition(cnd); + countSql.setCallback(Sqls.callback.integer()); + dao.execute(countSql); + int count = countSql.getInt(); + + Sql listSql = Sqls.create(""" + SELECT c.*, t.typeName AS courseTypeName + FROM learning_course c + LEFT JOIN learning_course_type t ON t.id = c.courseTypeId + $condition + ORDER BY $orderColumn $orderBy + """); + listSql.setCondition(cnd); + listSql.setVar("orderColumn", orderColumn); + listSql.setVar("orderBy", orderBy); + listSql.setPager(dao.createPager(pageForm.getPageNumber(), pageForm.getPageSize())); + listSql.setCallback(Sqls.callback.maps()); + dao.execute(listSql); + + Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, listSql.getList(NutMap.class)); + return Result.success(pagination); + } + + @At + @ApiOperation("新增课程") + @SaCheckPermission("learning.course.manage") + @SLog(tag = "新增课程", msg = "课程名称:${args[0].courseName}") + public Result doAdd(LearningCourse course) { + Result checkResult = checkCourse(course, null); + if (checkResult != null) { + return checkResult; + } + if (StrUtil.isBlank(course.getStatus())) { + course.setStatus("draft"); + } + dao.insert(course); + return Result.success(); + } + + @At + @ApiOperation("编辑课程") + @SaCheckPermission("learning.course.manage") + @SLog(tag = "编辑课程", msg = "课程ID:${args[0].id}") + public Result doEdit(LearningCourse course) { + if (StrUtil.isBlank(course.getId())) { + return Result.error("请选择要编辑的数据"); + } + Result checkResult = checkCourse(course, course.getId()); + if (checkResult != null) { + return checkResult; + } + dao.updateIgnoreNull(course); + return Result.success(); + } + + @At + @ApiOperation("删除课程") + @SaCheckPermission("learning.course.manage") + @SLog(tag = "删除课程", msg = "课程ID:${args[0]}") + public Result doDelete(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("请选择要删除的数据"); + } + int outlineCount = dao.count(LearningCourseOutline.class, Cnd.where("courseId", "=", id)); + if (outlineCount > 0) { + return Result.error("该课程存在章节内容,请先删除章节内容后再删除课程"); + } + dao.clear(LearningCourse.class, Cnd.where("id", "=", id)); + return Result.success(); + } + + @At + @ApiOperation("启用课程类型") + @SaCheckPermission("learning.course.manage") + public Result courseTypes() { + return Result.success(dao.query(LearningCourseType.class, Cnd.where("enabled", "=", true).asc("sortNum").asc("createdAt"))); + } + + @At + @ApiOperation("学习教育数据字典") + @SaCheckPermission("learning.course.manage") + public Result learningDictOptions(String name) { + Sys_dict root = sysDictService.fetch(Cnd.where("code", "=", "学习教育").or("name", "=", "学习教育")); + if (root == null) { + return Result.success(List.of()); + } + List firstLevel = sysDictService.query(Cnd.where("parentId", "=", Strings.sNull(root.getId())).and("disabled", "=", false).asc("location")); + if (StrUtil.isBlank(name)) { + return Result.success(firstLevel); + } + Sys_dict group = firstLevel.stream() + .filter(dict -> name.equals(dict.getName()) || name.equals(dict.getCode())) + .findFirst() + .orElse(null); + if (group == null) { + return Result.success(firstLevel); + } + List children = sysDictService.query(Cnd.where("parentId", "=", Strings.sNull(group.getId())).and("disabled", "=", false).asc("location")); + return Result.success(children.isEmpty() ? firstLevel : children); + } + + private Result checkCourse(LearningCourse course, String excludeId) { + if (course == null || StrUtil.isBlank(course.getCourseName())) { + return Result.error("课程名称不能为空"); + } + if (StrUtil.isBlank(course.getCourseTypeId())) { + return Result.error("请选择课程类型"); + } + if (StrUtil.isBlank(course.getLecturerName())) { + return Result.error("授课讲师不能为空"); + } + if (course.getSortNum() == null) { + return Result.error("排序编码不能为空"); + } + if (!"long_term".equals(course.getOpenType()) && (course.getStartTime() == null || course.getEndTime() == null)) { + return Result.error("请选择开课时间"); + } + Cnd cnd = Cnd.where("courseName", "=", course.getCourseName().trim()); + if (StrUtil.isNotBlank(excludeId)) { + cnd.and("id", "!=", excludeId); + } + if (dao.count(LearningCourse.class, cnd) > 0) { + return Result.error("课程名称已存在"); + } + course.setCourseName(course.getCourseName().trim()); + if ("long_term".equals(course.getOpenType())) { + course.setStartTime(null); + course.setEndTime(null); + } + return null; + } + + private String getOrderColumn(String prop) { + Map columns = new HashMap<>(); + columns.put("courseName", "c.courseName"); + columns.put("courseTypeName", "t.typeName"); + columns.put("lecturerName", "c.lecturerName"); + columns.put("startTime", "c.startTime"); + columns.put("status", "c.status"); + columns.put("recommendFlags", "c.recommendFlags"); + columns.put("sortNum", "c.sortNum"); + return columns.get(prop); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/learning/controller/LearningCourseTypeController.java b/src/main/java/com/budwk/app/zhgh/learning/controller/LearningCourseTypeController.java new file mode 100644 index 00000000..5e885e83 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/learning/controller/LearningCourseTypeController.java @@ -0,0 +1,110 @@ +package com.budwk.app.zhgh.learning.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.annotation.SLog; +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.zhgh.learning.models.LearningCourseType; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.nutz.dao.Cnd; +import org.nutz.dao.Dao; +import org.nutz.dao.pager.Pager; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; +import org.nutz.mvc.annotation.Param; + +@IocBean +@Ok("json:full") +@Api("学习教育平台-课程类型设置") +@At("/platform/learning/course/type") +public class LearningCourseTypeController { + + @Inject + private Dao dao; + + @At("") + @Ok("beetl:/platform/zhgh/Learning/courseType/index.html") + @SaCheckPermission("learning.course.type") + public void index() { + } + + @At + @ApiOperation("课程类型列表") + @SaCheckPermission("learning.course.type") + public Result pageData(PageForm pageForm, @Param("typeName") String typeName) { + Cnd cnd = Cnd.NEW(); + cnd.and(Cnd.likeEX("typeName", typeName)); + cnd.asc("sortNum").asc("createdAt"); + int count = dao.count(LearningCourseType.class, cnd); + Pager pager = dao.createPager(pageForm.getPageNumber(), pageForm.getPageSize()); + Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, dao.query(LearningCourseType.class, cnd, pager)); + return Result.success(pagination); + } + + @At + @ApiOperation("新增课程类型") + @SaCheckPermission("learning.course.type") + @SLog(tag = "新增课程类型", msg = "课程类型名称:${args[0].typeName}") + public Result doAdd(LearningCourseType courseType) { + Result checkResult = checkCourseType(courseType, null); + if (checkResult != null) { + return checkResult; + } + if (courseType.getEnabled() == null) { + courseType.setEnabled(true); + } + dao.insert(courseType); + return Result.success(); + } + + @At + @ApiOperation("编辑课程类型") + @SaCheckPermission("learning.course.type") + @SLog(tag = "编辑课程类型", msg = "课程类型ID:${args[0].id}") + public Result doEdit(LearningCourseType courseType) { + if (StrUtil.isBlank(courseType.getId())) { + return Result.error("请选择要编辑的数据"); + } + Result checkResult = checkCourseType(courseType, courseType.getId()); + if (checkResult != null) { + return checkResult; + } + dao.updateIgnoreNull(courseType); + return Result.success(); + } + + @At + @ApiOperation("删除课程类型") + @SaCheckPermission("learning.course.type") + @SLog(tag = "删除课程类型", msg = "课程类型ID:${args[0]}") + public Result doDelete(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("请选择要删除的数据"); + } + dao.clear(LearningCourseType.class, Cnd.where("id", "=", id)); + return Result.success(); + } + + private Result checkCourseType(LearningCourseType courseType, String excludeId) { + if (courseType == null || StrUtil.isBlank(courseType.getTypeName())) { + return Result.error("课程类型名称不能为空"); + } + if (courseType.getSortNum() == null) { + return Result.error("排序编号不能为空"); + } + Cnd cnd = Cnd.where("typeName", "=", courseType.getTypeName().trim()); + if (StrUtil.isNotBlank(excludeId)) { + cnd.and("id", "!=", excludeId); + } + if (dao.count(LearningCourseType.class, cnd) > 0) { + return Result.error("课程类型名称已存在"); + } + courseType.setTypeName(courseType.getTypeName().trim()); + return null; + } +} diff --git a/src/main/java/com/budwk/app/zhgh/learning/controller/LearningPlatformController.java b/src/main/java/com/budwk/app/zhgh/learning/controller/LearningPlatformController.java new file mode 100644 index 00000000..d80ba1eb --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/learning/controller/LearningPlatformController.java @@ -0,0 +1,47 @@ +package com.budwk.app.zhgh.learning.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; + +@IocBean +@At("/platform/learning") +public class LearningPlatformController { + + @At("/activity/manage") + @Ok("beetl:/platform/zhgh/Learning/activityManage/index.html") + @SaCheckPermission("learning.activity.manage") + public void activityManage() { + } + + @At("/my/record") + @Ok("beetl:/platform/zhgh/Learning/myRecord/index.html") + @SaCheckPermission("learning.my.record") + public void myRecord() { + } + + @At("/my/record/h5") + @Ok("beetl:/platform/zhghh5/learning/myRecord/index.html") + @SaCheckPermission("learning.my.record") + public void myRecordH5() { + } + + @At("/statistics") + @Ok("beetl:/platform/zhgh/Learning/statistics/index.html") + @SaCheckPermission("learning.statistics") + public void statistics() { + } + + @At("/course/h5") + @Ok("beetl:/platform/zhghh5/learning/course/index.html") + @SaCheckPermission("learning.course.display") + public void courseH5() { + } + + @At("/course/h5/study") + @Ok("beetl:/platform/zhghh5/learning/course/study.html") + @SaCheckPermission("learning.course.display") + public void courseH5Study() { + } +} diff --git a/src/main/java/com/budwk/app/zhgh/learning/controller/LearningStatisticsController.java b/src/main/java/com/budwk/app/zhgh/learning/controller/LearningStatisticsController.java new file mode 100644 index 00000000..251af0db --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/learning/controller/LearningStatisticsController.java @@ -0,0 +1,330 @@ +package com.budwk.app.zhgh.learning.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.constant.RoleConstant; +import com.budwk.app.base.page.Pagination; +import com.budwk.app.base.result.Result; +import com.budwk.app.base.utils.PageUtil; +import com.budwk.app.sys.models.Sys_union; +import com.budwk.app.web.commons.auth.utils.AuthUtil; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.learning.models.LearningCourse; +import com.budwk.app.zhgh.learning.param.LearningStatisticsPageForm; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.nutz.dao.Cnd; +import org.nutz.dao.Dao; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.dao.util.cri.Static; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.util.NutMap; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@IocBean +@Ok("json:full") +@Api("学习教育平台-学习统计") +@At("/platform/learning/statistics") +public class LearningStatisticsController { + + @Inject + private Dao dao; + + @At("") + @Ok("beetl:/platform/zhgh/Learning/statistics/index.html") + @SaCheckPermission("learning.statistics") + public void index() { + } + + @At + @ApiOperation("课程统计") + @SaCheckPermission("learning.statistics") + public Result coursePageData(LearningStatisticsPageForm pageForm) { + Cnd cnd = courseCondition(pageForm); + Sql countSql = Sqls.create(""" + SELECT COUNT(1) + FROM learning_course c + $condition + """); + countSql.setCondition(cnd); + countSql.setCallback(Sqls.callback.integer()); + dao.execute(countSql); + + Sql listSql = Sqls.create(""" + SELECT + c.id AS courseId, + c.courseName, + IFNULL(COUNT(uc.userId), 0) AS learnerCount, + IFNULL(SUM(CASE WHEN target.targetCount > 0 AND uc.completedOutlineCount >= target.targetCount THEN 1 ELSE 0 END), 0) AS completedCount, + IFNULL(ROUND(AVG(uc.studySeconds)), 0) AS avgStudySeconds + FROM learning_course c + LEFT JOIN ( + SELECT + courseId, + CASE + WHEN SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) > 0 + THEN SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) + ELSE COUNT(1) + END AS targetCount, + SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) AS requiredCount + FROM learning_course_outline + WHERE status = 'enabled' + GROUP BY courseId + ) target ON target.courseId = c.id + LEFT JOIN ( + SELECT + r.courseId, + r.userId, + SUM(IFNULL(r.studySeconds, 0)) AS studySeconds, + COUNT(DISTINCT CASE + WHEN r.completeStatus = 'completed' + AND ((target.requiredCount > 0 AND o.required = 1) OR target.requiredCount = 0) + THEN r.outlineId + ELSE NULL + END) AS completedOutlineCount + FROM learning_study_record r + JOIN learning_course_outline o ON o.id = r.outlineId AND o.courseId = r.courseId AND o.status = 'enabled' + JOIN ( + SELECT + courseId, + CASE + WHEN SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) > 0 + THEN SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) + ELSE COUNT(1) + END AS targetCount, + SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) AS requiredCount + FROM learning_course_outline + WHERE status = 'enabled' + GROUP BY courseId + ) target ON target.courseId = r.courseId + $recordCondition + GROUP BY r.courseId, r.userId + ) uc ON uc.courseId = c.id + $condition + GROUP BY c.id, c.courseName + ORDER BY $orderColumn $orderBy + """); + listSql.setCondition(cnd); + listSql.setVar("recordCondition", new Static(recordWhere(pageForm))); + setOrder(listSql, pageForm, "course"); + listSql.setPager(dao.createPager(pageForm.getPageNumber(), pageForm.getPageSize())); + listSql.setCallback(Sqls.callback.maps()); + dao.execute(listSql); + List rows = listSql.getList(NutMap.class); + rows.forEach(row -> row.put("avgStudyTimeText", formatStudySeconds(row.getInt("avgStudySeconds", 0)))); + return Result.success(new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), countSql.getInt(), rows)); + } + + @At + @ApiOperation("分工会统计") + @SaCheckPermission("learning.statistics") + public Result unionPageData(LearningStatisticsPageForm pageForm) { + Cnd cnd = unionCondition(pageForm); + Sql countSql = Sqls.create(""" + SELECT COUNT(1) + FROM learning_course c + JOIN ( + SELECT r.courseId, r.unionId + FROM learning_study_record r + $recordCondition + GROUP BY r.courseId, r.unionId + ) uc ON uc.courseId = c.id + LEFT JOIN sys_union un ON un.id = uc.unionId + $condition + """); + countSql.setCondition(cnd); + countSql.setVar("recordCondition", new Static(recordWhere(pageForm))); + countSql.setCallback(Sqls.callback.integer()); + dao.execute(countSql); + + Sql listSql = Sqls.create(""" + SELECT + c.id AS courseId, + c.courseName, + un.id AS unionId, + IFNULL(un.name, uc.unionName) AS unionName, + COUNT(uc.userId) AS learnerCount, + IFNULL(SUM(uc.studySeconds), 0) AS studySeconds, + IFNULL(SUM(CASE WHEN target.targetCount > 0 AND uc.completedOutlineCount >= target.targetCount THEN 1 ELSE 0 END), 0) AS completedCount + FROM learning_course c + JOIN ( + SELECT + r.courseId, + r.userId, + r.unionId, + MAX(r.unionName) AS unionName, + SUM(IFNULL(r.studySeconds, 0)) AS studySeconds, + COUNT(DISTINCT CASE + WHEN r.completeStatus = 'completed' + AND ((target.requiredCount > 0 AND o.required = 1) OR target.requiredCount = 0) + THEN r.outlineId + ELSE NULL + END) AS completedOutlineCount + FROM learning_study_record r + JOIN learning_course_outline o ON o.id = r.outlineId AND o.courseId = r.courseId AND o.status = 'enabled' + JOIN ( + SELECT + courseId, + CASE + WHEN SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) > 0 + THEN SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) + ELSE COUNT(1) + END AS targetCount, + SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) AS requiredCount + FROM learning_course_outline + WHERE status = 'enabled' + GROUP BY courseId + ) target ON target.courseId = r.courseId + $recordCondition + GROUP BY r.courseId, r.userId, r.unionId + ) uc ON uc.courseId = c.id + LEFT JOIN sys_union un ON un.id = uc.unionId + LEFT JOIN ( + SELECT + courseId, + CASE + WHEN SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) > 0 + THEN SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) + ELSE COUNT(1) + END AS targetCount, + SUM(CASE WHEN required = 1 THEN 1 ELSE 0 END) AS requiredCount + FROM learning_course_outline + WHERE status = 'enabled' + GROUP BY courseId + ) target ON target.courseId = c.id + $condition + GROUP BY c.id, c.courseName, uc.unionId, un.id, un.name + ORDER BY $orderColumn $orderBy + """); + listSql.setCondition(cnd); + listSql.setVar("recordCondition", new Static(recordWhere(pageForm))); + setOrder(listSql, pageForm, "union"); + listSql.setPager(dao.createPager(pageForm.getPageNumber(), pageForm.getPageSize())); + listSql.setCallback(Sqls.callback.maps()); + dao.execute(listSql); + List rows = listSql.getList(NutMap.class); + rows.forEach(row -> { + int learnerCount = row.getInt("learnerCount", 0); + int completedCount = row.getInt("completedCount", 0); + row.put("studyTimeText", formatStudySeconds(row.getInt("studySeconds", 0))); + row.put("completeRate", learnerCount == 0 ? "0%" : BigDecimal.valueOf(completedCount * 100.0 / learnerCount).setScale(2, RoundingMode.HALF_UP).stripTrailingZeros().toPlainString() + "%"); + }); + return Result.success(new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), countSql.getInt(), rows)); + } + + @At + @ApiOperation("可选课程") + @SaCheckPermission("learning.statistics") + public Result courseOptions(String courseName) { + Cnd cnd = Cnd.NEW(); + cnd.and(Cnd.likeEX("courseName", courseName)); + cnd.desc("createdAt").asc("sortNum"); + return Result.success(dao.query(LearningCourse.class, cnd, dao.createPager(1, 50))); + } + + @At + @ApiOperation("可选分工会") + @SaCheckPermission("learning.statistics") + public Result unionOptions() { + if (hasSchoolScope()) { + return Result.success(dao.query(Sys_union.class, Cnd.NEW().asc("unionCode").asc("name"))); + } + if (hasBranchScope()) { + return Result.success(dao.query(Sys_union.class, Cnd.where("id", "=", SecurityUtil.getUnionId()))); + } + return Result.success(List.of()); + } + + private Cnd courseCondition(LearningStatisticsPageForm pageForm) { + Cnd cnd = Cnd.NEW(); + cnd.andEX("c.id", "=", pageForm.getCourseId()); + cnd.and(Cnd.likeEX("c.courseName", pageForm.getCourseName())); + return cnd; + } + + private Cnd unionCondition(LearningStatisticsPageForm pageForm) { + Cnd cnd = courseCondition(pageForm); + cnd.andEX("uc.unionId", "=", pageForm.getUnionId()); + return cnd; + } + + private String recordWhere(LearningStatisticsPageForm pageForm) { + StringBuilder where = new StringBuilder("WHERE 1 = 1"); + if (hasBranchScope()) { + where.append(" AND r.unionId = '").append(escapeSql(SecurityUtil.getUnionId())).append("'"); + } else if (!hasSchoolScope()) { + where.append(" AND r.userId = '").append(escapeSql(SecurityUtil.getUserId())).append("'"); + } + if (StrUtil.isNotBlank(pageForm.getUnionId())) { + where.append(" AND r.unionId = '").append(escapeSql(pageForm.getUnionId())).append("'"); + } + return where.toString(); + } + + private void setOrder(Sql sql, LearningStatisticsPageForm pageForm, String type) { + String orderColumn = "union".equals(type) ? getUnionOrderColumn(pageForm.getPageOrderName()) : getCourseOrderColumn(pageForm.getPageOrderName()); + String orderBy = PageUtil.getOrder(pageForm.getPageOrderBy()); + if (StrUtil.isBlank(orderColumn)) { + orderColumn = "union".equals(type) ? "c.courseName ASC, un.unionCode" : "c.sortNum ASC, c.createdAt"; + orderBy = "DESC"; + } else if (StrUtil.isBlank(orderBy)) { + orderBy = "ASC"; + } + sql.setVar("orderColumn", new Static(orderColumn)); + sql.setVar("orderBy", new Static(orderBy)); + } + + private String getCourseOrderColumn(String prop) { + Map columns = new HashMap<>(); + columns.put("courseName", "c.courseName"); + columns.put("learnerCount", "learnerCount"); + columns.put("completedCount", "completedCount"); + columns.put("avgStudySeconds", "avgStudySeconds"); + return columns.get(prop); + } + + private String getUnionOrderColumn(String prop) { + Map columns = new HashMap<>(); + columns.put("courseName", "c.courseName"); + columns.put("unionName", "unionName"); + columns.put("learnerCount", "learnerCount"); + columns.put("studySeconds", "studySeconds"); + columns.put("completeRate", "completedCount"); + return columns.get(prop); + } + + private boolean hasSchoolScope() { + return AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name()); + } + + private boolean hasBranchScope() { + return AuthUtil.hasRoleOr(RoleConstant.BRANCH_UNION_ADMIN.name()); + } + + private String escapeSql(String value) { + return value == null ? "" : value.replace("'", "''"); + } + + private String formatStudySeconds(Integer seconds) { + int value = seconds == null ? 0 : seconds; + int hour = value / 3600; + int minute = value % 3600 / 60; + int second = value % 60; + if (hour > 0) { + return hour + "小时" + minute + "分" + second + "秒"; + } + if (minute > 0) { + return minute + "分" + second + "秒"; + } + return second + "秒"; + } +} diff --git a/src/main/java/com/budwk/app/zhgh/learning/controller/LearningStudyRecordController.java b/src/main/java/com/budwk/app/zhgh/learning/controller/LearningStudyRecordController.java new file mode 100644 index 00000000..b5fbb986 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/learning/controller/LearningStudyRecordController.java @@ -0,0 +1,493 @@ +package com.budwk.app.zhgh.learning.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.constant.RoleConstant; +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.base.utils.PageUtil; +import com.budwk.app.sys.models.Sys_union; +import com.budwk.app.web.commons.auth.utils.AuthUtil; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.learning.models.LearningCourse; +import com.budwk.app.zhgh.learning.models.LearningCourseOutline; +import com.budwk.app.zhgh.learning.models.LearningOutlineResource; +import com.budwk.app.zhgh.learning.models.LearningStudyRecord; +import com.budwk.app.zhgh.learning.models.LearningStudyRule; +import com.budwk.app.zhgh.learning.models.LearningStudySegment; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.nutz.dao.Chain; +import org.nutz.dao.Cnd; +import org.nutz.dao.Dao; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.dao.util.cri.SqlExpressionGroup; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.util.NutMap; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; +import org.nutz.mvc.annotation.Param; + +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@IocBean +@Ok("json:full") +@Api("学习教育平台-学习记录") +@At("/platform/learning/study/record") +public class LearningStudyRecordController { + + private static final int MAX_HEARTBEAT_SECONDS = 30; + + @Inject + private Dao dao; + + @At + @ApiOperation("学习记录分页") + @SaCheckPermission("learning.my.record") + public Result pageData(PageForm pageForm, + @Param("keyword") String keyword, + @Param("unionId") String unionId, + @Param("courseId") String courseId, + @Param("completeStatus") String completeStatus) { + Cnd cnd = Cnd.NEW(); + if (StrUtil.isNotBlank(keyword)) { + SqlExpressionGroup seg = new SqlExpressionGroup(); + seg.or("r.loginName", "like", "%" + keyword + "%"); + seg.or("r.userName", "like", "%" + keyword + "%"); + cnd.and(seg); + } + cnd.andEX("r.unionId", "=", unionId); + cnd.andEX("r.courseId", "=", courseId); + cnd.andEX("r.completeStatus", "=", completeStatus); + appendScope(cnd); + + String orderColumn = getOrderColumn(pageForm.getPageOrderName()); + if (StrUtil.isNotBlank(orderColumn) && StrUtil.isNotBlank(pageForm.getPageOrderBy())) { + cnd.orderBy(orderColumn, PageUtil.getOrder(pageForm.getPageOrderBy())); + } else { + cnd.desc("r.latestStudyTime"); + } + + Sql countSql = Sqls.create("SELECT COUNT(1) FROM learning_study_record r $condition"); + countSql.setCondition(cnd); + countSql.setCallback(Sqls.callback.integer()); + dao.execute(countSql); + + Sql listSql = Sqls.create(""" + SELECT r.* + FROM learning_study_record r + $condition + """); + listSql.setCondition(cnd); + listSql.setPager(dao.createPager(pageForm.getPageNumber(), pageForm.getPageSize())); + listSql.setCallback(Sqls.callback.maps()); + dao.execute(listSql); + List rows = listSql.getList(NutMap.class); + rows.forEach(row -> row.put("studyTimeText", formatStudySeconds(row.getInt("studySeconds", 0)))); + return Result.success(new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), countSql.getInt(), rows)); + } + + @At + @ApiOperation("移动端我的学习汇总") + @SaCheckPermission("learning.my.record") + public Result h5Summary() { + String userId = SecurityUtil.getUserId(); + Sql sql = Sqls.create(""" + SELECT IFNULL(SUM(IFNULL(studySeconds, 0)), 0) AS studySeconds, + COUNT(DISTINCT courseId) AS courseCount, + COUNT(DISTINCT CASE WHEN completeStatus = 'completed' THEN courseId END) AS completedCourseCount, + COUNT(DISTINCT outlineId) AS outlineCount, + COUNT(DISTINCT CASE WHEN completeStatus = 'completed' THEN outlineId END) AS completedOutlineCount + FROM learning_study_record + WHERE userId = @userId + """); + sql.params().set("userId", userId); + sql.setCallback(Sqls.callback.map()); + dao.execute(sql); + NutMap data = sql.getObject(NutMap.class); + if (data == null) { + data = NutMap.NEW(); + } + int studySeconds = data.getInt("studySeconds", 0); + data.put("studyHour", studySeconds / 3600); + data.put("studyMinute", studySeconds % 3600 / 60); + return Result.success(data); + } + + @At + @ApiOperation("移动端我的学习课程") + @SaCheckPermission("learning.my.record") + public Result h5Courses() { + String userId = SecurityUtil.getUserId(); + Sql sql = Sqls.create(""" + SELECT r.courseId, + MAX(r.courseName) AS courseName, + MAX(c.cover) AS cover, + COUNT(1) AS outlineCount, + SUM(CASE WHEN r.completeStatus = 'completed' THEN 1 ELSE 0 END) AS completedOutlineCount, + IFNULL(SUM(IFNULL(r.studySeconds, 0)), 0) AS studySeconds, + IFNULL(ROUND(AVG(IFNULL(r.progressPercent, 0))), 0) AS progressPercent, + MAX(r.latestStudyTime) AS latestStudyTime + FROM learning_study_record r + LEFT JOIN learning_course c ON c.id = r.courseId + WHERE r.userId = @userId + GROUP BY r.courseId + ORDER BY latestStudyTime DESC + """); + sql.params().set("userId", userId); + sql.setCallback(Sqls.callback.maps()); + dao.execute(sql); + List rows = sql.getList(NutMap.class); + rows.forEach(row -> row.put("studyTimeText", formatStudySeconds(row.getInt("studySeconds", 0)))); + return Result.success(rows); + } + + @At + @ApiOperation("开始学习") + @SaCheckPermission("learning.course.display") + public Result start(@Param("courseId") String courseId, + @Param("resourceId") String resourceId, + @Param("positionSeconds") Integer positionSeconds) { + if (StrUtil.hasBlank(courseId, resourceId)) { + return Result.error("请选择课程资料后开始学习"); + } + LearningCourse course = dao.fetch(LearningCourse.class, courseId); + LearningOutlineResource resource = dao.fetch(LearningOutlineResource.class, resourceId); + if (course == null || resource == null || !courseId.equals(resource.getCourseId())) { + return Result.error("课程资料不存在"); + } + LearningCourseOutline outline = dao.fetch(LearningCourseOutline.class, resource.getOutlineId()); + if (outline == null) { + return Result.error("章节不存在"); + } + + String userId = SecurityUtil.getUserId(); + Date now = new Date(); + closeUnfinishedSegments(userId, "interrupted"); + + LearningStudyRecord record = dao.fetch(LearningStudyRecord.class, Cnd.where("userId", "=", userId) + .and("courseId", "=", courseId) + .and("outlineId", "=", outline.getId())); + if (record == null) { + record = buildRecord(userId, course, outline, now); + record.setLastPositionSeconds(sanitizePosition(positionSeconds, 0)); + dao.insert(record); + } else { + int requiredSeconds = requiredSeconds(outline.getId()); + dao.update(LearningStudyRecord.class, Chain.make("latestStudyTime", now) + .add("courseName", course.getCourseName()) + .add("outlineName", outline.getTitle()) + .add("lastPositionSeconds", sanitizePosition(positionSeconds, record.getLastPositionSeconds())) + .add("requiredSeconds", requiredSeconds) + .add("progressPercent", progress(record.getStudySeconds(), requiredSeconds)) + .add("completeStatus", "completed".equals(record.getCompleteStatus()) ? "completed" : "studying"), + Cnd.where("id", "=", record.getId())); + record = dao.fetch(LearningStudyRecord.class, record.getId()); + } + + LearningStudySegment segment = new LearningStudySegment(); + segment.setRecordId(record.getId()); + segment.setUserId(userId); + segment.setCourseId(courseId); + segment.setOutlineId(outline.getId()); + segment.setResourceId(resourceId); + segment.setResourceName(resource.getResourceTitle()); + segment.setStartTime(now); + segment.setLastHeartbeatTime(now); + segment.setActiveSeconds(0); + segment.setState("studying"); + dao.insert(segment); + + return Result.success(NutMap.NEW() + .addv("recordId", record.getId()) + .addv("segmentId", segment.getId()) + .addv("studySeconds", record.getStudySeconds()) + .addv("lastPositionSeconds", record.getLastPositionSeconds()) + .addv("requiredSeconds", record.getRequiredSeconds()) + .addv("progressPercent", record.getProgressPercent()) + .addv("completeStatus", record.getCompleteStatus())); + } + + @At + @ApiOperation("学习心跳") + @SaCheckPermission("learning.course.display") + public Result heartbeat(@Param("segmentId") String segmentId, + @Param("activeSeconds") Integer activeSeconds, + @Param("positionSeconds") Integer positionSeconds) { + LearningStudySegment segment = fetchOwnStudyingSegment(segmentId); + if (segment == null) { + return Result.error("学习时段已结束,请重新开始学习"); + } + int seconds = Math.max(0, Math.min(activeSeconds == null ? 0 : activeSeconds, MAX_HEARTBEAT_SECONDS)); + return Result.success(addActiveSeconds(segment, seconds, false, positionSeconds)); + } + + @At + @ApiOperation("结束学习") + @SaCheckPermission("learning.course.display") + public Result finish(@Param("segmentId") String segmentId, + @Param("activeSeconds") Integer activeSeconds, + @Param("positionSeconds") Integer positionSeconds) { + LearningStudySegment segment = fetchOwnStudyingSegment(segmentId); + if (segment == null) { + return Result.success(); + } + int seconds = Math.max(0, Math.min(activeSeconds == null ? 0 : activeSeconds, MAX_HEARTBEAT_SECONDS)); + NutMap result = addActiveSeconds(segment, seconds, true, positionSeconds); + dao.update(LearningStudySegment.class, Chain.make("endTime", new Date()).add("state", "finished"), Cnd.where("id", "=", segmentId)); + return Result.success(result); + } + + @At + @ApiOperation("保存播放位置") + @SaCheckPermission("learning.course.display") + public Result position(@Param("courseId") String courseId, + @Param("resourceId") String resourceId, + @Param("positionSeconds") Integer positionSeconds) { + if (StrUtil.hasBlank(courseId, resourceId) || positionSeconds == null || positionSeconds < 0) { + return Result.success(); + } + LearningOutlineResource resource = dao.fetch(LearningOutlineResource.class, resourceId); + if (resource == null || !courseId.equals(resource.getCourseId())) { + return Result.success(); + } + dao.update(LearningStudyRecord.class, + Chain.make("lastPositionSeconds", positionSeconds).add("latestStudyTime", new Date()), + Cnd.where("userId", "=", SecurityUtil.getUserId()) + .and("courseId", "=", courseId) + .and("outlineId", "=", resource.getOutlineId())); + return Result.success(); + } + + @At + @ApiOperation("可选课程") + @SaCheckPermission("learning.my.record") + public Result courseOptions() { + return Result.success(dao.query(LearningCourse.class, Cnd.NEW().asc("sortNum").desc("createdAt"))); + } + + @At + @ApiOperation("可选分工会") + @SaCheckPermission("learning.my.record") + public Result unionOptions() { + if (hasSchoolScope()) { + return Result.success(dao.query(Sys_union.class, Cnd.NEW().asc("unionCode").asc("name"))); + } + if (hasBranchScope()) { + return Result.success(dao.query(Sys_union.class, Cnd.where("id", "=", SecurityUtil.getUnionId()))); + } + return Result.success(List.of()); + } + + @At + @ApiOperation("删除学习记录") + @SaCheckPermission("learning.my.record") + public Result delete(@Param("id") String id) { + if (StrUtil.isBlank(id)) { + return Result.error("请选择要删除的学习记录"); + } + LearningStudyRecord record = dao.fetch(LearningStudyRecord.class, id); + if (record == null) { + return Result.error("学习记录不存在"); + } + if (!canOperate(record)) { + return Result.error("无权删除该学习记录"); + } + dao.clear(LearningStudySegment.class, Cnd.where("recordId", "=", id)); + dao.clear(LearningStudyRecord.class, Cnd.where("id", "=", id)); + return Result.success(); + } + + private LearningStudyRecord buildRecord(String userId, LearningCourse course, LearningCourseOutline outline, Date now) { + NutMap user = currentUserInfo(userId); + int requiredSeconds = requiredSeconds(outline.getId()); + LearningStudyRecord record = new LearningStudyRecord(); + record.setUserId(userId); + record.setLoginName(user.getString("loginname", SecurityUtil.getUserLoginname())); + record.setUserName(user.getString("username", SecurityUtil.getUserUsername())); + record.setUnionId(user.getString("unionId", SecurityUtil.getUnionId())); + record.setUnionName(user.getString("unionName", "")); + record.setCourseId(course.getId()); + record.setCourseName(course.getCourseName()); + record.setOutlineId(outline.getId()); + record.setOutlineName(outline.getTitle()); + record.setFirstStudyTime(now); + record.setLatestStudyTime(now); + record.setStudySeconds(0); + record.setLastPositionSeconds(0); + record.setRequiredSeconds(requiredSeconds); + record.setProgressPercent(0); + record.setCompleteStatus("studying"); + return record; + } + + private NutMap addActiveSeconds(LearningStudySegment segment, int activeSeconds, boolean finish, Integer positionSeconds) { + Date now = new Date(); + int segmentSeconds = value(segment.getActiveSeconds()) + activeSeconds; + dao.update(LearningStudySegment.class, Chain.make("activeSeconds", segmentSeconds).add("lastHeartbeatTime", now), Cnd.where("id", "=", segment.getId())); + + LearningStudyRecord record = dao.fetch(LearningStudyRecord.class, segment.getRecordId()); + int studySeconds = value(record.getStudySeconds()) + activeSeconds; + int requiredSeconds = requiredSeconds(record.getOutlineId()); + int progressPercent = progress(studySeconds, requiredSeconds); + String completeStatus = completeStatus(studySeconds, requiredSeconds, finish); + if ("completed".equals(completeStatus)) { + progressPercent = 100; + } + Chain chain = Chain.make("latestStudyTime", now) + .add("studySeconds", studySeconds) + .add("requiredSeconds", requiredSeconds) + .add("progressPercent", progressPercent) + .add("completeStatus", completeStatus); + int lastPositionSeconds = value(record.getLastPositionSeconds()); + if (positionSeconds != null && positionSeconds >= 0) { + lastPositionSeconds = positionSeconds; + chain.add("lastPositionSeconds", positionSeconds); + } + if ("completed".equals(completeStatus) && record.getCompletedAt() == null) { + chain.add("completedAt", now); + } + dao.update(LearningStudyRecord.class, chain, Cnd.where("id", "=", record.getId())); + return NutMap.NEW() + .addv("studySeconds", studySeconds) + .addv("studyTimeText", formatStudySeconds(studySeconds)) + .addv("lastPositionSeconds", lastPositionSeconds) + .addv("requiredSeconds", requiredSeconds) + .addv("progressPercent", progressPercent) + .addv("completeStatus", completeStatus); + } + + private LearningStudySegment fetchOwnStudyingSegment(String segmentId) { + if (StrUtil.isBlank(segmentId)) { + return null; + } + return dao.fetch(LearningStudySegment.class, Cnd.where("id", "=", segmentId) + .and("userId", "=", SecurityUtil.getUserId()) + .and("state", "=", "studying")); + } + + private void closeUnfinishedSegments(String userId, String state) { + dao.update(LearningStudySegment.class, + Chain.make("endTime", new Date()).add("state", state), + Cnd.where("userId", "=", userId).and("state", "=", "studying")); + } + + private int requiredSeconds(String outlineId) { + LearningStudyRule rule = dao.fetch(LearningStudyRule.class, Cnd.where("targetType", "=", "outline") + .and("targetId", "=", outlineId) + .and("status", "=", "enabled")); + if (rule != null && value(rule.getMinStudySeconds()) > 0) { + return rule.getMinStudySeconds(); + } + Sql sql = Sqls.create(""" + SELECT IFNULL(SUM(IFNULL(durationSeconds, 0)), 0) + FROM learning_outline_resource + WHERE outlineId = @outlineId AND status = 'enabled' + """); + sql.params().set("outlineId", outlineId); + sql.setCallback(Sqls.callback.integer()); + dao.execute(sql); + return Math.max(0, sql.getInt()); + } + + private int progress(Integer studySeconds, int requiredSeconds) { + if (requiredSeconds <= 0) { + return 0; + } + return Math.min(100, (int) Math.floor(value(studySeconds) * 100.0 / requiredSeconds)); + } + + private String completeStatus(Integer studySeconds, int requiredSeconds, boolean finish) { + if (requiredSeconds <= 0) { + return finish ? "completed" : "studying"; + } + if (value(studySeconds) >= requiredSeconds) { + return "completed"; + } + return value(studySeconds) > 0 || finish ? "studying" : "not_started"; + } + + private NutMap currentUserInfo(String userId) { + Sql sql = Sqls.create("SELECT id, loginname, username, unionId AS unionId, unionName AS unionName FROM vw_user WHERE id = @id"); + sql.params().set("id", userId); + sql.setCallback(Sqls.callback.map()); + dao.execute(sql); + NutMap user = sql.getObject(NutMap.class); + return user == null ? NutMap.NEW() : user; + } + + private void appendScope(Cnd cnd) { + if (hasSchoolScope()) { + return; + } + if (hasBranchScope()) { + cnd.and("r.unionId", "=", SecurityUtil.getUnionId()); + return; + } + cnd.and("r.userId", "=", SecurityUtil.getUserId()); + } + + private boolean hasSchoolScope() { + return AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name()); + } + + private boolean hasBranchScope() { + return AuthUtil.hasRoleOr(RoleConstant.BRANCH_UNION_ADMIN.name()); + } + + private boolean canOperate(LearningStudyRecord record) { + if (hasSchoolScope()) { + return true; + } + if (hasBranchScope()) { + return StrUtil.equals(record.getUnionId(), SecurityUtil.getUnionId()); + } + return StrUtil.equals(record.getUserId(), SecurityUtil.getUserId()); + } + + private String getOrderColumn(String prop) { + Map columns = new HashMap<>(); + columns.put("loginName", "r.loginName"); + columns.put("userName", "r.userName"); + columns.put("unionName", "r.unionName"); + columns.put("courseName", "r.courseName"); + columns.put("outlineName", "r.outlineName"); + columns.put("firstStudyTime", "r.firstStudyTime"); + columns.put("latestStudyTime", "r.latestStudyTime"); + columns.put("studySeconds", "r.studySeconds"); + columns.put("progressPercent", "r.progressPercent"); + columns.put("completeStatus", "r.completeStatus"); + return columns.get(prop); + } + + private String formatStudySeconds(Integer seconds) { + int value = value(seconds); + int hour = value / 3600; + int minute = value % 3600 / 60; + int second = value % 60; + if (hour > 0) { + return hour + "小时" + minute + "分" + second + "秒"; + } + if (minute > 0) { + return minute + "分" + second + "秒"; + } + return second + "秒"; + } + + private int value(Integer value) { + return value == null ? 0 : value; + } + + private int sanitizePosition(Integer positionSeconds, Integer defaultValue) { + if (positionSeconds == null || positionSeconds < 0) { + return value(defaultValue); + } + return positionSeconds; + } +} diff --git a/src/main/java/com/budwk/app/zhgh/learning/models/LearningCourse.java b/src/main/java/com/budwk/app/zhgh/learning/models/LearningCourse.java new file mode 100644 index 00000000..32b14e68 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/learning/models/LearningCourse.java @@ -0,0 +1,117 @@ +package com.budwk.app.zhgh.learning.models; + +import com.budwk.app.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.nutz.dao.entity.annotation.ColDefine; +import org.nutz.dao.entity.annotation.ColType; +import org.nutz.dao.entity.annotation.Column; +import org.nutz.dao.entity.annotation.Comment; +import org.nutz.dao.entity.annotation.Name; +import org.nutz.dao.entity.annotation.Table; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +import java.util.Date; + +@Data +@Table +@EqualsAndHashCode(callSuper = true) +public class LearningCourse extends BaseModel { + + @Column + @Name + @Comment("ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + @PrevInsert(uu32 = true) + private String id; + + @Column + @Comment("课程名称") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String courseName; + + @Column + @Comment("课程类型") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String courseTypeId; + + @Column + @Comment("授课讲师") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String lecturerName; + + @Column + @Comment("讲师基本信息") + @ColDefine(type = ColType.VARCHAR, width = 500) + private String lecturerInfo; + + @Column + @Comment("课程简介") + @ColDefine(customType = "longtext") + private String courseIntro; + + @Column + @Comment("适合人群") + @ColDefine(type = ColType.VARCHAR, width = 500) + private String suitablePeople; + + @Column + @Comment("学习目标") + @ColDefine(type = ColType.VARCHAR, width = 500) + private String learningGoal; + + @Column + @Comment("课程周期") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String coursePeriod; + + @Column + @Comment("开课类型") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String openType; + + @Column + @Comment("开始时间") + @ColDefine(type = ColType.DATETIME) + private Date startTime; + + @Column + @Comment("结束时间") + @ColDefine(type = ColType.DATETIME) + private Date endTime; + + @Column + @Comment("课程状态") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String status; + + @Column + @Comment("推荐标识") + @ColDefine(type = ColType.VARCHAR, width = 500) + private String recommendFlags; + + @Column + @Comment("课程标签") + @ColDefine(type = ColType.VARCHAR, width = 500) + private String courseTags; + + @Column + @Comment("学习对象") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String targetType; + + @Column + @Comment("指定组织") + @ColDefine(type = ColType.VARCHAR, width = 500) + private String targetOrgText; + + @Column + @Comment("排序编码") + @ColDefine(type = ColType.INT) + private Integer sortNum; + + @Column + @Comment("课程封面") + @ColDefine(type = ColType.VARCHAR, width = 500) + private String cover; +} diff --git a/src/main/java/com/budwk/app/zhgh/learning/models/LearningCourseOutline.java b/src/main/java/com/budwk/app/zhgh/learning/models/LearningCourseOutline.java new file mode 100644 index 00000000..d8fd1480 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/learning/models/LearningCourseOutline.java @@ -0,0 +1,75 @@ +package com.budwk.app.zhgh.learning.models; + +import com.budwk.app.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.nutz.dao.entity.annotation.ColDefine; +import org.nutz.dao.entity.annotation.ColType; +import org.nutz.dao.entity.annotation.Column; +import org.nutz.dao.entity.annotation.Comment; +import org.nutz.dao.entity.annotation.Name; +import org.nutz.dao.entity.annotation.Table; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +@Data +@Table +@EqualsAndHashCode(callSuper = true) +public class LearningCourseOutline extends BaseModel { + + @Column + @Name + @Comment("ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + @PrevInsert(uu32 = true) + private String id; + + @Column + @Comment("所属课程ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String courseId; + + @Column + @Comment("所属课程名称") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String courseName; + + @Column + @Comment("父节点ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String parentId; + + @Column + @Comment("节点类型") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String nodeType; + + @Column + @Comment("章/节标题") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String title; + + @Column + @Comment("副标题") + @ColDefine(type = ColType.VARCHAR, width = 200) + private String subtitle; + + @Column + @Comment("简介") + @ColDefine(customType = "longtext") + private String description; + + @Column + @Comment("排序值") + @ColDefine(type = ColType.INT) + private Integer sortOrder; + + @Column + @Comment("是否必学") + @ColDefine(type = ColType.BOOLEAN) + private Boolean required; + + @Column + @Comment("状态") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String status; +} diff --git a/src/main/java/com/budwk/app/zhgh/learning/models/LearningCourseType.java b/src/main/java/com/budwk/app/zhgh/learning/models/LearningCourseType.java new file mode 100644 index 00000000..ba5d47d8 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/learning/models/LearningCourseType.java @@ -0,0 +1,45 @@ +package com.budwk.app.zhgh.learning.models; + +import com.budwk.app.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.nutz.dao.entity.annotation.ColDefine; +import org.nutz.dao.entity.annotation.ColType; +import org.nutz.dao.entity.annotation.Column; +import org.nutz.dao.entity.annotation.Comment; +import org.nutz.dao.entity.annotation.Name; +import org.nutz.dao.entity.annotation.Table; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +@Data +@Table +@EqualsAndHashCode(callSuper = true) +public class LearningCourseType extends BaseModel { + + @Column + @Name + @Comment("ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + @PrevInsert(uu32 = true) + private String id; + + @Column + @Comment("课程类型名称") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String typeName; + + @Column + @Comment("排序编号") + @ColDefine(type = ColType.INT) + private Integer sortNum; + + @Column + @Comment("是否启用") + @ColDefine(type = ColType.BOOLEAN) + private Boolean enabled; + + @Column + @Comment("备注") + @ColDefine(type = ColType.VARCHAR, width = 500) + private String remark; +} diff --git a/src/main/java/com/budwk/app/zhgh/learning/models/LearningOutlineResource.java b/src/main/java/com/budwk/app/zhgh/learning/models/LearningOutlineResource.java new file mode 100644 index 00000000..27648a17 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/learning/models/LearningOutlineResource.java @@ -0,0 +1,85 @@ +package com.budwk.app.zhgh.learning.models; + +import com.budwk.app.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.nutz.dao.entity.annotation.ColDefine; +import org.nutz.dao.entity.annotation.ColType; +import org.nutz.dao.entity.annotation.Column; +import org.nutz.dao.entity.annotation.Comment; +import org.nutz.dao.entity.annotation.Name; +import org.nutz.dao.entity.annotation.Table; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +@Data +@Table +@EqualsAndHashCode(callSuper = true) +public class LearningOutlineResource extends BaseModel { + + @Column + @Name + @Comment("ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + @PrevInsert(uu32 = true) + private String id; + + @Column + @Comment("所属课程ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String courseId; + + @Column + @Comment("所属章/节ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String outlineId; + + @Column + @Comment("资料标题") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String resourceTitle; + + @Column + @Comment("资料类型") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String resourceType; + + @Column + @Comment("文件扩展名") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String fileExt; + + @Column + @Comment("附件") + @ColDefine(customType = "longtext") + private String fileData; + + @Column + @Comment("视频/音频时长") + @ColDefine(type = ColType.INT) + private Integer durationSeconds; + + @Column + @Comment("排序值") + @ColDefine(type = ColType.INT) + private Integer sortOrder; + + @Column + @Comment("是否必学") + @ColDefine(type = ColType.BOOLEAN) + private Boolean required; + + @Column + @Comment("是否允许预览") + @ColDefine(type = ColType.BOOLEAN) + private Boolean allowPreview; + + @Column + @Comment("是否允许下载") + @ColDefine(type = ColType.BOOLEAN) + private Boolean allowDownload; + + @Column + @Comment("状态") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String status; +} diff --git a/src/main/java/com/budwk/app/zhgh/learning/models/LearningStudyRecord.java b/src/main/java/com/budwk/app/zhgh/learning/models/LearningStudyRecord.java new file mode 100644 index 00000000..3723c783 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/learning/models/LearningStudyRecord.java @@ -0,0 +1,119 @@ +package com.budwk.app.zhgh.learning.models; + +import com.budwk.app.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.nutz.dao.entity.annotation.ColDefine; +import org.nutz.dao.entity.annotation.ColType; +import org.nutz.dao.entity.annotation.Column; +import org.nutz.dao.entity.annotation.Comment; +import org.nutz.dao.entity.annotation.Index; +import org.nutz.dao.entity.annotation.Name; +import org.nutz.dao.entity.annotation.Table; +import org.nutz.dao.entity.annotation.TableIndexes; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +import java.util.Date; + +@Data +@Table("learning_study_record") +@TableIndexes({ + @Index(name = "idx_learning_record_user_course_outline", fields = {"userId", "courseId", "outlineId"}, unique = true), + @Index(name = "idx_learning_record_union", fields = {"unionId"}, unique = false), + @Index(name = "idx_learning_record_course", fields = {"courseId"}, unique = false) +}) +@EqualsAndHashCode(callSuper = true) +public class LearningStudyRecord extends BaseModel { + + @Column + @Name + @Comment("ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + @PrevInsert(uu32 = true) + private String id; + + @Column + @Comment("学习人ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String userId; + + @Column + @Comment("工号") + @ColDefine(type = ColType.VARCHAR, width = 120) + private String loginName; + + @Column + @Comment("姓名") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String userName; + + @Column + @Comment("所属分工会ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String unionId; + + @Column + @Comment("所属分工会") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String unionName; + + @Column + @Comment("课程ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String courseId; + + @Column + @Comment("课程名称") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String courseName; + + @Column + @Comment("章节ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String outlineId; + + @Column + @Comment("章节名称") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String outlineName; + + @Column + @Comment("第一次进入时间") + @ColDefine(type = ColType.DATETIME) + private Date firstStudyTime; + + @Column + @Comment("最近学习时间") + @ColDefine(type = ColType.DATETIME) + private Date latestStudyTime; + + @Column + @Comment("累计有效学习时长(秒)") + @ColDefine(type = ColType.INT) + private Integer studySeconds; + + @Column + @Comment("最近播放位置(秒)") + @ColDefine(type = ColType.INT) + private Integer lastPositionSeconds; + + @Column + @Comment("要求学习时长(秒)") + @ColDefine(type = ColType.INT) + private Integer requiredSeconds; + + @Column + @Comment("学习进度") + @ColDefine(type = ColType.INT) + private Integer progressPercent; + + @Column + @Comment("完成状态") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String completeStatus; + + @Column + @Comment("完成时间") + @ColDefine(type = ColType.DATETIME) + private Date completedAt; +} diff --git a/src/main/java/com/budwk/app/zhgh/learning/models/LearningStudyRule.java b/src/main/java/com/budwk/app/zhgh/learning/models/LearningStudyRule.java new file mode 100644 index 00000000..03c03a3a --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/learning/models/LearningStudyRule.java @@ -0,0 +1,100 @@ +package com.budwk.app.zhgh.learning.models; + +import com.budwk.app.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.nutz.dao.entity.annotation.ColDefine; +import org.nutz.dao.entity.annotation.ColType; +import org.nutz.dao.entity.annotation.Column; +import org.nutz.dao.entity.annotation.Comment; +import org.nutz.dao.entity.annotation.Name; +import org.nutz.dao.entity.annotation.Table; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +@Data +@Table +@EqualsAndHashCode(callSuper = true) +public class LearningStudyRule extends BaseModel { + + @Column + @Name + @Comment("ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + @PrevInsert(uu32 = true) + private String id; + + @Column + @Comment("所属课程ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String courseId; + + @Column + @Comment("规则对象类型") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String targetType; + + @Column + @Comment("规则对象ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String targetId; + + @Column + @Comment("是否必学") + @ColDefine(type = ColType.BOOLEAN) + private Boolean required; + + @Column + @Comment("学习方式") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String studyMode; + + @Column + @Comment("完成规则") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String completionRule; + + @Column + @Comment("完成比例") + @ColDefine(type = ColType.INT) + private Integer completePercent; + + @Column + @Comment("最少学习时长") + @ColDefine(type = ColType.INT) + private Integer minStudySeconds; + + @Column + @Comment("解锁规则") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String unlockRule; + + @Column + @Comment("是否允许跳过") + @ColDefine(type = ColType.BOOLEAN) + private Boolean allowSkip; + + @Column + @Comment("是否允许拖动") + @ColDefine(type = ColType.BOOLEAN) + private Boolean allowDrag; + + @Column + @Comment("暂停是否计时") + @ColDefine(type = ColType.BOOLEAN) + private Boolean pauseCountTime; + + @Column + @Comment("页面隐藏是否计时") + @ColDefine(type = ColType.BOOLEAN) + private Boolean hiddenCountTime; + + @Column + @Comment("长时间无操作是否计时") + @ColDefine(type = ColType.BOOLEAN) + private Boolean inactiveCountTime; + + @Column + @Comment("状态") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String status; +} diff --git a/src/main/java/com/budwk/app/zhgh/learning/models/LearningStudySegment.java b/src/main/java/com/budwk/app/zhgh/learning/models/LearningStudySegment.java new file mode 100644 index 00000000..4a146a5c --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/learning/models/LearningStudySegment.java @@ -0,0 +1,88 @@ +package com.budwk.app.zhgh.learning.models; + +import com.budwk.app.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.nutz.dao.entity.annotation.ColDefine; +import org.nutz.dao.entity.annotation.ColType; +import org.nutz.dao.entity.annotation.Column; +import org.nutz.dao.entity.annotation.Comment; +import org.nutz.dao.entity.annotation.Index; +import org.nutz.dao.entity.annotation.Name; +import org.nutz.dao.entity.annotation.Table; +import org.nutz.dao.entity.annotation.TableIndexes; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +import java.util.Date; + +@Data +@Table("learning_study_segment") +@TableIndexes({ + @Index(name = "idx_learning_segment_record", fields = {"recordId"}, unique = false), + @Index(name = "idx_learning_segment_user_state", fields = {"userId", "state"}, unique = false) +}) +@EqualsAndHashCode(callSuper = true) +public class LearningStudySegment extends BaseModel { + + @Column + @Name + @Comment("ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + @PrevInsert(uu32 = true) + private String id; + + @Column + @Comment("学习记录ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String recordId; + + @Column + @Comment("学习人ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String userId; + + @Column + @Comment("课程ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String courseId; + + @Column + @Comment("章节ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String outlineId; + + @Column + @Comment("资源ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String resourceId; + + @Column + @Comment("资源名称") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String resourceName; + + @Column + @Comment("开始时间") + @ColDefine(type = ColType.DATETIME) + private Date startTime; + + @Column + @Comment("结束时间") + @ColDefine(type = ColType.DATETIME) + private Date endTime; + + @Column + @Comment("最近心跳时间") + @ColDefine(type = ColType.DATETIME) + private Date lastHeartbeatTime; + + @Column + @Comment("本时段有效学习时长(秒)") + @ColDefine(type = ColType.INT) + private Integer activeSeconds; + + @Column + @Comment("状态") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String state; +} diff --git a/src/main/java/com/budwk/app/zhgh/learning/param/LearningStatisticsPageForm.java b/src/main/java/com/budwk/app/zhgh/learning/param/LearningStatisticsPageForm.java new file mode 100644 index 00000000..23b554b7 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/learning/param/LearningStatisticsPageForm.java @@ -0,0 +1,16 @@ +package com.budwk.app.zhgh.learning.param; + +import com.budwk.app.base.param.PageForm; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class LearningStatisticsPageForm extends PageForm { + + private String courseId; + + private String courseName; + + private String unionId; +} diff --git a/src/main/resources/static/assets/platform/images/tour/tour-direct-family.jpg b/src/main/resources/static/assets/platform/images/tour/tour-direct-family.jpg new file mode 100644 index 00000000..530371e6 Binary files /dev/null and b/src/main/resources/static/assets/platform/images/tour/tour-direct-family.jpg differ diff --git a/src/main/resources/static/assets/platform/images/tour/tour-h5-banner-1.jpg b/src/main/resources/static/assets/platform/images/tour/tour-h5-banner-1.jpg new file mode 100644 index 00000000..4d01bd6e Binary files /dev/null and b/src/main/resources/static/assets/platform/images/tour/tour-h5-banner-1.jpg differ diff --git a/src/main/resources/static/assets/platform/images/tour/tour-h5-banner-2.jpg b/src/main/resources/static/assets/platform/images/tour/tour-h5-banner-2.jpg new file mode 100644 index 00000000..f304104d Binary files /dev/null and b/src/main/resources/static/assets/platform/images/tour/tour-h5-banner-2.jpg differ diff --git a/src/main/resources/static/assets/platform/images/tour/tour-in-province.png b/src/main/resources/static/assets/platform/images/tour/tour-in-province.png new file mode 100644 index 00000000..82be9792 Binary files /dev/null and b/src/main/resources/static/assets/platform/images/tour/tour-in-province.png differ diff --git a/src/main/resources/static/assets/platform/images/tour/tour-out-province.png b/src/main/resources/static/assets/platform/images/tour/tour-out-province.png new file mode 100644 index 00000000..c67e5b9e Binary files /dev/null and b/src/main/resources/static/assets/platform/images/tour/tour-out-province.png differ diff --git a/src/main/resources/static/assets/platform/js/util/voiceMenuNavigator.js b/src/main/resources/static/assets/platform/js/util/voiceMenuNavigator.js new file mode 100644 index 00000000..5fb5aae7 --- /dev/null +++ b/src/main/resources/static/assets/platform/js/util/voiceMenuNavigator.js @@ -0,0 +1,439 @@ +;(function (window, $) { + const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition + const commandWords = [ + "打开", + "进入", + "跳转", + "跳到", + "去", + "访问", + "帮我", + "请", + "页面", + "菜单", + "一下" + ] + + const state = { + recognition: null, + listening: false, + menus: null, + parentMap: {}, + lastText: "", + pendingMatches: [], + awaitingChoice: false, + afterRecognitionEnd: null + } + + function normalizePlain(text) { + return String(text || "") + .toLowerCase() + .replace(/[,。!?、,.!?;;::\s]/g, "") + } + + function normalizeCommand(text) { + return normalizePlain(text) + .replace(new RegExp(commandWords.join("|"), "g"), "") + } + + function notify(message, type) { + if (window.ELEMENT && ELEMENT.Message) { + ELEMENT.Message({message, type: type || "info"}) + return + } + window.alert(message) + } + + function speak(message, onEnd) { + if (!window.speechSynthesis || !window.SpeechSynthesisUtterance) { + if (typeof onEnd === "function") { + onEnd() + } + return + } + + window.speechSynthesis.cancel() + const utterance = new SpeechSynthesisUtterance(message) + let ended = false + function finish() { + if (ended) { + return + } + ended = true + if (typeof onEnd === "function") { + onEnd() + } + } + + utterance.lang = "zh-CN" + utterance.rate = 1 + utterance.volume = 1 + utterance.onend = function () { + finish() + } + utterance.onerror = function () { + finish() + } + window.speechSynthesis.speak(utterance) + setTimeout(finish, Math.max(2500, message.length * 220)) + } + + function runAfterRecognitionEnd(callback) { + state.afterRecognitionEnd = callback + if (!state.listening && typeof state.afterRecognitionEnd === "function") { + const next = state.afterRecognitionEnd + state.afterRecognitionEnd = null + setTimeout(next, 150) + } + } + + function escapeHtml(text) { + return String(text || "") + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'") + } + + function flattenMenus(menus, parentId, result) { + result = result || [] + ;(menus || []).forEach(function (menu) { + const id = menu.id + const realParentId = menu.parentId || parentId || "" + if (id) { + state.parentMap[id] = realParentId + } + result.push(menu) + if (menu.children && menu.children.length) { + flattenMenus(menu.children, id, result) + } + }) + return result + } + + function getStoreMenus() { + try { + return window.store && window.store.state && window.store.state.user && window.store.state.user.menus + } catch (e) { + return null + } + } + + function getSessionMenus() { + try { + return JSON.parse(window.sessionStorage.getItem("zhgh_sub_app_menus") || "[]") + } catch (e) { + return [] + } + } + + function getMenus() { + const cached = state.menus + if (cached && cached.length) { + return $.Deferred().resolve(cached).promise() + } + + const storeMenus = getStoreMenus() + if (storeMenus && storeMenus.length) { + state.menus = flattenMenus(storeMenus, "", []).filter(function (menu) { + return menu.href + }) + return $.Deferred().resolve(state.menus).promise() + } + + return $.get("/platform/sys/user/getLogonUser").then(function (res) { + if (res && res.code === 0 && res.data && res.data.menus) { + state.parentMap = {} + state.menus = flattenMenus(res.data.menus, "", []).filter(function (menu) { + return menu.href + }) + return state.menus + } + const sessionMenus = getSessionMenus() + if (sessionMenus && sessionMenus.length) { + state.menus = flattenMenus(sessionMenus, "", []).filter(function (menu) { + return menu.href + }) + return state.menus + } + return [] + }, function () { + const sessionMenus = getSessionMenus() + if (sessionMenus && sessionMenus.length) { + state.menus = flattenMenus(sessionMenus, "", []).filter(function (menu) { + return menu.href + }) + return state.menus + } + return [] + }) + } + + function scoreMenu(menu, rawText) { + const query = normalizeCommand(rawText) + const rawQuery = normalizePlain(rawText) + const name = normalizePlain(menu.name) + const aliasName = normalizePlain(menu.aliasName) + const href = normalizePlain(menu.href) + const permission = normalizePlain(menu.permission) + + if (!query || !name) { + return 0 + } + if (query === name || rawQuery === name || query === aliasName || rawQuery === aliasName) { + return 100 + } + if (name.indexOf(query) > -1 || name.indexOf(rawQuery) > -1 || aliasName.indexOf(query) > -1 || aliasName.indexOf(rawQuery) > -1) { + return 80 + } + if (query.indexOf(name) > -1 || rawQuery.indexOf(name) > -1 || (aliasName && (query.indexOf(aliasName) > -1 || rawQuery.indexOf(aliasName) > -1))) { + return 70 + } + if (href.indexOf(query) > -1 || permission.indexOf(query) > -1) { + return 45 + } + return 0 + } + + function matchMenus(text, menus) { + return (menus || []) + .map(function (menu) { + return { + menu, + score: scoreMenu(menu, text) + } + }) + .filter(function (item) { + return item.score > 0 + }) + .sort(function (a, b) { + if (b.score !== a.score) { + return b.score - a.score + } + return String(a.menu.name || "").length - String(b.menu.name || "").length + }) + .slice(0, 5) + } + + function getRootMenuId(menu) { + let id = menu.id + let parentId = state.parentMap[id] || menu.parentId + while (parentId) { + id = parentId + parentId = state.parentMap[id] + } + return id + } + + function getCurrentSubAppId() { + try { + const app = JSON.parse(window.sessionStorage.getItem("zhgh_sub_app") || "{}") + return app.id + } catch (e) { + return null + } + } + + function openMenu(menu) { + state.pendingMatches = [] + state.awaitingChoice = false + if (!menu || !menu.href) { + notify("该菜单没有配置可打开的地址", "warning") + return + } + + const targetRootId = getRootMenuId(menu) + const currentSubAppId = getCurrentSubAppId() + const hasSubAppContainer = $("#sub-app-container-main-content-body").length > 0 + + if (hasSubAppContainer && currentSubAppId && currentSubAppId === targetRootId && typeof commonUtil !== "undefined" && commonUtil.pjaxPush) { + commonUtil.pjaxPush(menu.href) + return + } + window.location.href = menu.href + } + + function showCandidateMessage(matches) { + const items = matches + .map(function (item, index) { + const href = item.menu.href ? " " + escapeHtml(item.menu.href) + "" : "" + return "

" + (index + 1) + ". " + escapeHtml(item.menu.name) + href + "

" + }) + .join("") + + if (window.ELEMENT && ELEMENT.MessageBox) { + ELEMENT.MessageBox.alert(items, "找到多个菜单,请说“打开第几个”", { + dangerouslyUseHTMLString: true, + confirmButtonText: "知道了", + type: "info", + callback: function () {} + }) + } + } + + function getChoiceIndex(text) { + const normalized = normalizePlain(text) + const numberMap = { + "1": 0, + "一": 0, + "壹": 0, + "幺": 0, + "2": 1, + "二": 1, + "两": 1, + "贰": 1, + "3": 2, + "三": 2, + "叁": 2, + "4": 3, + "四": 3, + "肆": 3, + "5": 4, + "五": 4, + "伍": 4 + } + + const digitMatch = normalized.match(/第?([1-5])个?/) + if (digitMatch) { + return numberMap[digitMatch[1]] + } + + const chineseMatch = normalized.match(/第?([一二两三四五壹贰叁肆伍幺])个?/) + if (chineseMatch) { + return numberMap[chineseMatch[1]] + } + return -1 + } + + function handleChoice(text) { + const index = getChoiceIndex(text) + const matches = state.pendingMatches || [] + if (index >= 0 && index < matches.length) { + if (window.ELEMENT && ELEMENT.MessageBox && typeof ELEMENT.MessageBox.close === "function") { + ELEMENT.MessageBox.close() + } + openMenu(matches[index].menu) + return + } + + notify("没有识别到有效序号,请说打开第几个", "warning") + speak("没有识别到有效序号,请说打开第几个", function () { + listenForChoice() + }) + } + + function listenForChoice() { + state.awaitingChoice = true + setTimeout(function () { + start(true) + }, 300) + } + + function chooseMenu(matches) { + if (!matches.length) { + notify("未找到可访问的菜单,请换个名称再试", "warning") + return + } + if (matches.length === 1 || matches[0].score > matches[1].score) { + openMenu(matches[0].menu) + return + } + + state.pendingMatches = matches + state.awaitingChoice = true + showCandidateMessage(matches) + + const prompt = matches + .map(function (item, index) { + return "第" + (index + 1) + "个," + item.menu.name + }) + .join("。") + speak("找到多个菜单,您需要打开第几个。" + prompt, function () { + listenForChoice() + }) + } + + function updateButton(listening) { + const $button = $("#voice-menu-btn") + $button.toggleClass("is-listening", listening) + $button.attr("title", listening ? "正在听,请说出菜单名称" : "语音打开菜单") + $button.find(".voice-menu-text").text(listening ? "聆听中" : "语音") + } + + function start(choiceMode) { + choiceMode = choiceMode || state.awaitingChoice + if (!SpeechRecognition) { + notify("当前浏览器不支持语音识别,请使用 Chrome 或 Edge", "warning") + return + } + if (state.listening) { + state.recognition.stop() + return + } + + const recognition = new SpeechRecognition() + recognition.lang = "zh-CN" + recognition.interimResults = false + recognition.continuous = false + recognition.maxAlternatives = 1 + + recognition.onstart = function () { + state.listening = true + updateButton(true) + notify(choiceMode ? "请说打开第几个" : "请说出要打开的菜单名称", "info") + } + recognition.onend = function () { + state.listening = false + updateButton(false) + if (typeof state.afterRecognitionEnd === "function") { + const next = state.afterRecognitionEnd + state.afterRecognitionEnd = null + setTimeout(next, 150) + } + } + recognition.onerror = function (event) { + const message = event.error === "not-allowed" ? "麦克风授权失败,请确认 HTTPS 或 localhost 环境并允许浏览器使用麦克风" : "语音识别失败,请再试一次" + notify(message, "warning") + } + recognition.onresult = function (event) { + const text = event.results && event.results[0] && event.results[0][0] && event.results[0][0].transcript + state.lastText = text || "" + if (!state.lastText) { + notify("没有识别到语音内容", "warning") + return + } + if (choiceMode || state.awaitingChoice) { + runAfterRecognitionEnd(function () { + handleChoice(state.lastText) + }) + return + } + getMenus().then(function (menus) { + const matches = matchMenus(state.lastText, menus) + runAfterRecognitionEnd(function () { + chooseMenu(matches) + }) + }) + } + + state.recognition = recognition + recognition.start() + } + + function init() { + $(document).on("click", "#voice-menu-btn", function () { + start() + }) + } + + window.voiceMenuNavigator = { + start, + refreshMenus: function () { + state.menus = null + state.parentMap = {} + } + } + + $(init) +})(window, jQuery) diff --git a/src/main/resources/templates/tour/direct_relative_reimbursement_apply.docx b/src/main/resources/templates/tour/direct_relative_reimbursement_apply.docx new file mode 100644 index 00000000..d4a8abc3 Binary files /dev/null and b/src/main/resources/templates/tour/direct_relative_reimbursement_apply.docx differ diff --git a/src/main/resources/templates/tour/over_cost_reimbursement_apply.docx b/src/main/resources/templates/tour/over_cost_reimbursement_apply.docx new file mode 100644 index 00000000..7668c7ef Binary files /dev/null and b/src/main/resources/templates/tour/over_cost_reimbursement_apply.docx differ diff --git a/src/main/resources/views/layouts/v4/baseLayout.html b/src/main/resources/views/layouts/v4/baseLayout.html index dc9a561d..32d3fdbe 100644 --- a/src/main/resources/views/layouts/v4/baseLayout.html +++ b/src/main/resources/views/layouts/v4/baseLayout.html @@ -85,6 +85,7 @@ + @@ -541,6 +542,33 @@ background-color: rgba(255, 255, 255, 0.15); } + .v4-voice-menu { + color: #ffffff; + cursor: pointer; + padding: 0 14px; + height: 100%; + border: 0; + background: transparent; + border-radius: 0; + transition: all 0.2s ease; + display: flex; + align-items: center; + gap: 6px; + white-space: nowrap; + font: inherit; + outline: none; + } + + .v4-voice-menu:hover, + .v4-voice-menu.is-listening { + background-color: rgba(255, 255, 255, 0.15); + color: #ffffff; + } + + .v4-voice-menu.is-listening i { + color: #ffdf6b; + } + .v4-avatar { width: 36px; height: 36px; @@ -606,6 +634,10 @@ .v4-nav { display: none; } + + .voice-menu-text { + display: none; + } } /* 页脚样式 */ @@ -677,6 +709,10 @@
+