From 785b9a4b80a135f2fce40fd94b3eddf153e1b96d Mon Sep 17 00:00:00 2001 From: zhouhefeng Date: Thu, 23 Jul 2026 18:30:30 +0800 Subject: [PATCH] commit --- .../app/sys/controller/SysHomeController.java | 10 +- .../sys/controller/SysUnionController.java | 7 +- .../ActivityBasicScopeController.java | 3 +- .../impl/HealthCheckupProjectServiceImpl.java | 12 +- .../OutlayManageClubQueryController.java | 15 ++ .../OutlayManageClubUseDetailController.java | 16 ++ .../service/OutlayManageClubQueryService.java | 11 + .../service/OutlayUseDetailService.java | 11 + .../OutlayManageClubQueryServiceImpl.java | 80 ++++++ .../impl/OutlayUseDetailServiceImpl.java | 74 ++++++ .../transact/ProposalMineController.java | 43 ++- ...roposalCommitteeFilingUnitServiceImpl.java | 40 ++- .../SuggestionBoxMineController.java | 1 + .../LearningCourseDisplayController.java | 70 ++++- .../LearningCourseManageController.java | 15 ++ .../LearningStudyRecordController.java | 155 ++++++++++- .../zhgh/learning/models/LearningCourse.java | 5 + .../learning/models/LearningStudyRecord.java | 5 + .../change/MemberChangeMineController.java | 13 + .../member/models/MemberChangeRecord.java | 5 + .../param/pageform/MemberManagePageForm.java | 4 + .../service/impl/MemberManageServiceImpl.java | 6 + .../WelfareSelectionSituationController.java | 8 + .../WelfareUserSelectController.java | 5 + .../param/WelfareFilterUserPageForm.java | 7 +- .../service/WelfareProjectService.java | 10 + .../service/impl/WelfareListServiceImpl.java | 6 +- .../impl/WelfareProjectServiceImpl.java | 24 ++ .../assets/platform/js/mixin/flowMixins.js | 42 +-- .../platform/js/mixin/initTableMixins.js | 4 +- .../components/plugins/sysUpload/index.vue | 13 +- .../views/layouts/v4/home/index.html | 16 ++ .../zhgh/Learning/chapterContent/index.html | 21 +- .../zhgh/Learning/courseDisplay/indexV2.html | 213 +++++++++++++++ .../zhgh/Learning/courseDisplay/studyV2.html | 251 ++++++++++++++++++ .../zhgh/Learning/courseManage/index.html | 80 +++--- .../zhgh/Learning/statistics/index.html | 2 +- .../outlay/outlayManage/club/query/index.html | 9 + .../outlayManage/club/useDetail/index.html | 8 +- .../zhgh/democratic/proposal/common/info.js | 12 +- .../proposal/transact/seconded/index.html | 2 +- .../proposal/transact/write/index.html | 131 ++++----- .../democratic/suggestionBox/mine/index.html | 4 +- .../democratic/suggestionBox/xgh/index.html | 5 + .../staffmanage/member/change/common/info.js | 16 +- .../member/change/common/memberChange.js | 51 +++- .../member/change/manage/index.html | 44 ++- .../staffmanage/member/change/mine/index.html | 2 +- .../member/info/datamanage/index.html | 2 +- .../staffmanage/member/info/group/index.html | 12 +- .../zhgh/welfare/projectMange/filterUser.js | 46 +++- .../welfare/projectMange/welfareOption.js | 43 +-- .../zhgh/welfare/select/optionSelect.js | 35 ++- .../democratic/suggestionBox/mine/index.html | 2 +- .../zhghh5/learning/course/index.html | 193 +++++++------- .../zhghh5/learning/course/study.html | 180 ++++++++++++- .../platform/zhghh5/welfare/select/index.html | 35 ++- 57 files changed, 1795 insertions(+), 340 deletions(-) create mode 100644 src/main/resources/views/platform/zhgh/Learning/courseDisplay/indexV2.html create mode 100644 src/main/resources/views/platform/zhgh/Learning/courseDisplay/studyV2.html diff --git a/src/main/java/com/budwk/app/sys/controller/SysHomeController.java b/src/main/java/com/budwk/app/sys/controller/SysHomeController.java index cb32dd56..ed8a0e9e 100644 --- a/src/main/java/com/budwk/app/sys/controller/SysHomeController.java +++ b/src/main/java/com/budwk/app/sys/controller/SysHomeController.java @@ -156,6 +156,8 @@ public class SysHomeController { List menus = sysUserService.getMenus(SecurityUtil.getUserId()); List allMenuIds = menus.stream().map(Sys_menu::getId).toList(); List sysMenus = list.stream() + // 推荐项必须同时属于当前用户已授权菜单,避免首页展示无权限入口。 + .filter(menu -> allMenuIds.contains(menu.getId())) .sorted(Comparator.comparing(Sys_menu::getLocation, Comparator.nullsLast(Integer::compareTo)) .thenComparing(Sys_menu::getId)) .toList(); @@ -174,8 +176,12 @@ public class SysHomeController { ); List menus = sysUserService.getMenus(SecurityUtil.getUserId()); List allMenuIds = menus.stream().map(Sys_menu::getId).toList(); - //List sysMenus = list.stream().filter(m -> allMenuIds.contains(m.getId())).sorted(Comparator.comparingInt(Sys_menu::getLocation)).toList(); - List sysMenus = list.stream().sorted(Comparator.comparingInt(Sys_menu::getLocation)).toList(); + List sysMenus = list.stream() + // 推荐项必须同时属于当前用户已授权菜单,避免首页展示无权限入口。 + .filter(menu -> allMenuIds.contains(menu.getId())) + .sorted(Comparator.comparing(Sys_menu::getLocation, Comparator.nullsLast(Integer::compareTo)) + .thenComparing(Sys_menu::getId)) + .toList(); return Result.success(sysMenus); } diff --git a/src/main/java/com/budwk/app/sys/controller/SysUnionController.java b/src/main/java/com/budwk/app/sys/controller/SysUnionController.java index 403c0e1a..7d09a7fa 100644 --- a/src/main/java/com/budwk/app/sys/controller/SysUnionController.java +++ b/src/main/java/com/budwk/app/sys/controller/SysUnionController.java @@ -527,8 +527,8 @@ public class SysUnionController { @ApiOperation("分工会组成单位穿梭框数据") @SaCheckPermission("sys.manager.union.partUnit") public Result branchUnionPartUnitTransferData(String unionId) { -// List units = dao.query(Sys_unit.class, Cnd.where("unitLevel", "=", 2).asc("unitcode")); - List units = dao.query(Sys_unit.class, Cnd.where("unitTypeCode", "=", "1").asc("unitcode")); + // 组成单位仅允许选择二级单位,单位类别不能替代层级判断。 + List units = dao.query(Sys_unit.class, Cnd.where("unitLevel", "=", 2).asc("unitcode")); List selectUnitIds = units.stream().filter(unit -> StrUtil.isNotBlank(unit.getUnionId()) && unit.getUnionId().equals(unionId)).map(Sys_unit::getId).toList(); List matchUnits = units.stream().filter(unit -> StrUtil.isBlank(unit.getUnionId()) || (StrUtil.isNotBlank(unit.getUnionId()) && unit.getUnionId().equals(unionId))).toList(); NutMap transferData = NutMap.NEW().addv("selectUnitIds", selectUnitIds).addv("allUnits", matchUnits); @@ -542,7 +542,8 @@ public class SysUnionController { public Result branchUnionPartUnitSet(String unionId, @Param("unitIds") String[] unitIds) { sysUnitService.update(Chain.make("unionId", null), Cnd.where("unionId", "=", unionId)); if (Lang.isNotEmpty(unitIds)) { - sysUnitService.update(Chain.make("unionId", unionId), Cnd.where("id", "in", unitIds)); + // 保存时再次限制单位层级,避免绕过穿梭框提交三级或其他层级单位。 + sysUnitService.update(Chain.make("unionId", unionId), Cnd.where("id", "in", unitIds).and("unitLevel", "=", 2)); } return Result.success(); } diff --git a/src/main/java/com/budwk/app/zhgh/activity/basic/controller/ActivityBasicScopeController.java b/src/main/java/com/budwk/app/zhgh/activity/basic/controller/ActivityBasicScopeController.java index 76936836..a13cb665 100644 --- a/src/main/java/com/budwk/app/zhgh/activity/basic/controller/ActivityBasicScopeController.java +++ b/src/main/java/com/budwk/app/zhgh/activity/basic/controller/ActivityBasicScopeController.java @@ -245,7 +245,8 @@ public class ActivityBasicScopeController { COLUMN_COMMENT FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'sys_user' - AND TABLE_SCHEMA = 'budwk_v5_mini' + -- 使用当前连接数据库,避免环境切换后查询不到字段元数据。 + AND TABLE_SCHEMA = DATABASE() """); List sqlDataList = activityBasicScopeService.listMap(sql); // sqlDataList.forEach(v -> v.put("DATA_TYPE", new String((byte[]) v.get("DATA_TYPE")))); diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/healthCheckup/service/impl/HealthCheckupProjectServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/healthCheckup/service/impl/HealthCheckupProjectServiceImpl.java index dfc809a2..ab936fca 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/healthCheckup/service/impl/HealthCheckupProjectServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/healthCheckup/service/impl/HealthCheckupProjectServiceImpl.java @@ -3,7 +3,7 @@ package com.budwk.app.zhgh.dayofficework.healthCheckup.service.impl; import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.base.utils.ManyAddOrRenewUtil; import com.budwk.app.sys.views.View_user; -import com.budwk.app.zhgh.activity.basic.models.ActivityUserScope; +import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService; import com.budwk.app.zhgh.dayofficework.healthCheckup.model.*; import com.budwk.app.zhgh.dayofficework.healthCheckup.service.HealthCheckupProjectService; import org.nutz.aop.interceptor.ioc.TransAop; @@ -36,6 +36,9 @@ public class HealthCheckupProjectServiceImpl extends BaseServiceImpl getHealthCheckupUserListByProject(HealthCheckupProject healthCheckupProject) { String projectId = healthCheckupProject.getId(); - List groupUsers = dao().query(ActivityUserScope.class, Cnd.where("groupId", "=", healthCheckupProject.getActivityGroupId())); - List userIds = groupUsers.stream().map(ActivityUserScope::getUserId).collect(Collectors.toList()); - List userMapList = dao().query(View_user.class, Cnd.where("id", "in", userIds)); + // 可报名人员范围同时支持人员结果分组和 SQL 条件分组,统一通过分组服务动态解析人员, + // 避免 SQL 条件分组因未保存 userId 明细而生成空名单。 + List userMapList = dao().query(View_user.class, Cnd.where("id", "in", + activityBasicScopeService.buildGroupUserIdSubSql(healthCheckupProject.getActivityGroupId()))); return userMapList.stream().map(v -> { HealthCheckupUser user = new HealthCheckupUser(); user.setId(R.UU32()); diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/club/controller/OutlayManageClubQueryController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/club/controller/OutlayManageClubQueryController.java index 8c7743bb..c7038f5f 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/club/controller/OutlayManageClubQueryController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/club/controller/OutlayManageClubQueryController.java @@ -97,6 +97,21 @@ public class OutlayManageClubQueryController { return Result.success(pagination); } + /** + * 导出社团拨付核对汇总表。 + * + * @param year 统计年度,未传时默认当前年度 + * @param clubId 社团主键,可为空;为空时导出当前用户权限范围内的全部社团 + * @param response 下载响应,返回 XSSF Excel 文件流 + */ + @At + @Ok("void") + @ApiOperation("导出社团拨付核对汇总表") + @SaCheckPermission("outlay.outlayManage.club.query") + public void exportSummary(Integer year, String clubId, HttpServletResponse response) { + clubQueryService.exportSummary(year, clubId, response); + } + @At @ApiOperation("提交核对") diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/club/controller/OutlayManageClubUseDetailController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/club/controller/OutlayManageClubUseDetailController.java index 4d64c586..b52c15c6 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/club/controller/OutlayManageClubUseDetailController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/club/controller/OutlayManageClubUseDetailController.java @@ -29,6 +29,7 @@ import org.nutz.lang.Strings; import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.Ok; +import javax.servlet.http.HttpServletResponse; import java.util.List; /** @@ -90,6 +91,21 @@ public class OutlayManageClubUseDetailController { return Result.success(pagination); } + /** + * 导出社团预算使用情况汇总表。 + * + * @param year 统计年度,未传时默认当前年度 + * @param clubId 社团主键,可为空;为空时导出当前用户权限范围内的全部社团 + * @param response 下载响应,返回 XSSF Excel 文件流 + */ + @At + @Ok("void") + @ApiOperation("导出社团预算使用情况") + @SaCheckPermission("outlay.outlayManage.club.useDetail") + public void doExport(Integer year, String clubId, HttpServletResponse response) { + outlayUseDetailService.exportClubUseDetail(year, clubId, response); + } + @At @ApiOperation("某个社团预算使用详情") diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/OutlayManageClubQueryService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/OutlayManageClubQueryService.java index 16fb7c90..7c8ce949 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/OutlayManageClubQueryService.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/OutlayManageClubQueryService.java @@ -5,9 +5,20 @@ import com.budwk.app.base.param.PageForm; import com.budwk.app.base.service.BaseService; import org.nutz.dao.sql.Sql; +import javax.servlet.http.HttpServletResponse; + public interface OutlayManageClubQueryService extends BaseService { void doMoney(String clubId); Sql backUserSql(PageForm pageForm, String clubId, String type, Integer year, String payed); + + /** + * 导出社团拨付核对汇总表。 + * + * @param year 统计年度,未传时默认当前年度 + * @param clubId 社团主键,可为空;为空时导出当前用户权限范围内的全部社团 + * @param response 下载响应,用于向浏览器输出 XSSF Excel 文件 + */ + void exportSummary(Integer year, String clubId, HttpServletResponse response); } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/OutlayUseDetailService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/OutlayUseDetailService.java index 5a996f19..db895e18 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/OutlayUseDetailService.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/OutlayUseDetailService.java @@ -3,6 +3,8 @@ package com.budwk.app.zhgh.dayofficework.outlay.outlayManage.service; import com.budwk.app.base.service.BaseService; import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.model.OutlayUseDetail; +import javax.servlet.http.HttpServletResponse; + public interface OutlayUseDetailService extends BaseService { @@ -13,4 +15,13 @@ public interface OutlayUseDetailService extends BaseService { */ void doDeleteDetail(String id,String outlayType); void doEditDetail(OutlayUseDetail outlayUseDetail,String outlayType); + + /** + * 导出社团预算使用情况汇总表。 + * + * @param year 统计年度 + * @param clubId 社团主键,可为空;为空时导出当前用户权限范围内的全部社团 + * @param response 下载响应,用于向浏览器输出 XSSF Excel 文件 + */ + void exportClubUseDetail(Integer year, String clubId, HttpServletResponse response); } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/impl/OutlayManageClubQueryServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/impl/OutlayManageClubQueryServiceImpl.java index 7b1f33ed..f0d456d4 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/impl/OutlayManageClubQueryServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/impl/OutlayManageClubQueryServiceImpl.java @@ -1,5 +1,9 @@ package com.budwk.app.zhgh.dayofficework.outlay.outlayManage.service.impl; +import cn.afterturn.easypoi.excel.ExcelExportUtil; +import cn.afterturn.easypoi.excel.entity.ExportParams; +import cn.afterturn.easypoi.excel.entity.enmus.ExcelType; +import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; @@ -7,6 +11,11 @@ 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.service.impl.BaseServiceImpl; +import com.budwk.app.base.utils.CommonDownloadUtil; +import com.budwk.app.sys.models.Sys_user_role; +import com.budwk.app.sys.services.SysRoleService; +import com.budwk.app.web.commons.auth.utils.AuthUtil; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.zhgh.club.service.impl.SysClubUserServiceImpl; import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.club.model.OutlayClubUserBackHistory; import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.club.model.OutlayManageClub; @@ -19,10 +28,15 @@ 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.apache.poi.ss.usermodel.Workbook; + +import javax.servlet.http.HttpServletResponse; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -36,6 +50,10 @@ import java.util.stream.Collectors; */ @IocBean(args = {"refer:dao"}) public class OutlayManageClubQueryServiceImpl extends BaseServiceImpl implements OutlayManageClubQueryService { + + @Inject + private SysRoleService sysRoleService; + public OutlayManageClubQueryServiceImpl(Dao dao) { super(dao); } @@ -189,4 +207,66 @@ public class OutlayManageClubQueryServiceImpl extends BaseServiceImpl implements sql.setCondition(cnd); return sql; } + + /** + * 导出当前用户可查询的社团拨付核对汇总数据。 + * 年审人数、可拨付人数和不可拨付人数分别取当前年度记录中的历史统计值; + * 拨付标准依据现有规则计算:0 人为 0 元,1 至 49 人为 3000 元,50 人及以上为 5000 元。 + * + * @param year 统计年度,未传时使用当前年度 + * @param clubId 社团主键,可为空 + * @param response 下载响应,用于输出 Excel 文件 + */ + @Override + public void exportSummary(Integer year, String clubId, HttpServletResponse response) { + Integer queryYear = year == null ? DateUtil.thisYear() : year; + Cnd cnd = Cnd.NEW(); + cnd.and("year", "=", queryYear); + if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_CLUB_ADMIN.name())) { + List roleIds = sysRoleService.getRoleIdsByCode(List.of(RoleConstant.CLUB_PRESIDENT, RoleConstant.CLUB_SECRETARY)); + List userRoles = dao().query(Sys_user_role.class, + Cnd.where("userId", "=", SecurityUtil.getUserId()) + .and("roleId", "in", roleIds)); + List myClubIds = userRoles.stream().map(Sys_user_role::getClubId).toList(); + cnd.andEX("clubId", "in", myClubIds); + } + cnd.andEX("clubId", "=", clubId); + cnd.asc("clubCode"); + + Sql summarySql = Sqls.create("SELECT * FROM `outlay_manage_club` $condition"); + summarySql.setCondition(cnd); + List summaryList = listMap(summarySql); + for (NutMap summary : summaryList) { + int giveMoneyNum = summary.getInt("historyGiveMoneyNum"); + summary.put("standard", getAllocateStandard(giveMoneyNum)); + summary.put("isCheckName", summary.getBoolean("isCheck") ? "已核对" : "未核对"); + } + + List exportEntities = new ArrayList<>(); + exportEntities.add(new ExcelExportEntity("社团名称", "clubName", 25)); + exportEntities.add(new ExcelExportEntity("年审人数", "historyUserNum", 15)); + exportEntities.add(new ExcelExportEntity("可拨付人数", "historyGiveMoneyNum", 15)); + exportEntities.add(new ExcelExportEntity("不可拨付人数", "historyNotGiveMoneyNum", 15)); + exportEntities.add(new ExcelExportEntity("标准", "standard", 15)); + exportEntities.add(new ExcelExportEntity("拨付总额", "totalQuota", 15)); + exportEntities.add(new ExcelExportEntity("是否核对", "isCheckName", 15)); + + ExportParams exportParams = new ExportParams(); + exportParams.setType(ExcelType.XSSF); + Workbook workbook = ExcelExportUtil.exportExcel(exportParams, exportEntities, summaryList); + CommonDownloadUtil.download(queryYear + "年社团拨付核对汇总.xlsx", workbook, response); + } + + /** + * 根据可拨付人数返回社团年度拨付标准。 + * + * @param giveMoneyNum 可拨付人数 + * @return 对应的拨付标准金额 + */ + private BigDecimal getAllocateStandard(int giveMoneyNum) { + if (giveMoneyNum == 0) { + return BigDecimal.ZERO; + } + return giveMoneyNum >= 50 ? BigDecimal.valueOf(5000) : BigDecimal.valueOf(3000); + } } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/impl/OutlayUseDetailServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/impl/OutlayUseDetailServiceImpl.java index bc5e46d2..da3b19eb 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/impl/OutlayUseDetailServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/impl/OutlayUseDetailServiceImpl.java @@ -1,13 +1,35 @@ package com.budwk.app.zhgh.dayofficework.outlay.outlayManage.service.impl; +import cn.afterturn.easypoi.excel.ExcelExportUtil; +import cn.afterturn.easypoi.excel.entity.ExportParams; +import cn.afterturn.easypoi.excel.entity.enmus.ExcelType; +import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity; +import cn.hutool.core.date.DateUtil; +import com.budwk.app.base.constant.RoleConstant; import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.base.utils.CommonDownloadUtil; +import com.budwk.app.sys.models.Sys_role; +import com.budwk.app.sys.models.Sys_user_role; +import com.budwk.app.sys.services.SysRoleService; +import com.budwk.app.web.commons.auth.utils.AuthUtil; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.club.model.OutlayManageClub; import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.model.OutlayUseDetail; import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.school.model.OutlayManageSchool; import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.service.OutlayUseDetailService; import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.union.model.OutlayManageUnion; +import org.apache.poi.ss.usermodel.Workbook; +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.util.NutMap; + +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.List; /** * @author zhf @@ -16,6 +38,10 @@ import org.nutz.ioc.loader.annotation.IocBean; */ @IocBean(args = {"refer:dao"}) public class OutlayUseDetailServiceImpl extends BaseServiceImpl implements OutlayUseDetailService { + + @Inject + private SysRoleService sysRoleService; + public OutlayUseDetailServiceImpl(Dao dao) { super(dao); } @@ -34,6 +60,54 @@ public class OutlayUseDetailServiceImpl extends BaseServiceImpl update(outlayUseDetail); } + /** + * 按社团预算使用页面的查询口径导出汇总数据。 + * 剩余额度由分配总额度减已使用额度计算,避免导出内容与页面展示不一致。 + * + * @param year 统计年度,未传时使用当前年度 + * @param clubId 社团主键,可为空 + * @param response 下载响应,用于输出 Excel 文件 + */ + @Override + public void exportClubUseDetail(Integer year, String clubId, HttpServletResponse response) { + Integer queryYear = year == null ? DateUtil.thisYear() : year; + Cnd cnd = Cnd.NEW(); + cnd.and("totalQuota", "IS NOT", null); + cnd.and("year", "=", queryYear); + + if (AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) { + cnd.andEX("clubId", "=", clubId); + } else { + Sys_role sysRole = sysRoleService.getByCode(RoleConstant.CLUB_PRESIDENT); + List userRoles = dao().query(Sys_user_role.class, + Cnd.where("userId", "=", SecurityUtil.getUserId()) + .and("roleId", "=", sysRole.getId())); + List myClubIds = userRoles.stream().map(Sys_user_role::getClubId).toList(); + cnd.andEX("clubId", "in", myClubIds); + } + cnd.asc("clubCode"); + + Sql sql = Sqls.create(""" + SELECT *, COALESCE(totalQuota, 0) - COALESCE(usedQuota, 0) AS surplusQuota + FROM outlay_manage_club $condition + """); + sql.setCondition(cnd); + List clubUseDetailList = listMap(sql); + + List exportEntities = new ArrayList<>(); + exportEntities.add(new ExcelExportEntity("年度", "year", 15)); + exportEntities.add(new ExcelExportEntity("社团名称", "clubName", 25)); + exportEntities.add(new ExcelExportEntity("社团编码", "clubCode", 20)); + exportEntities.add(new ExcelExportEntity("分配总额度(元)", "totalQuota", 18)); + exportEntities.add(new ExcelExportEntity("已使用额度(元)", "usedQuota", 18)); + exportEntities.add(new ExcelExportEntity("剩余额度(元)", "surplusQuota", 18)); + + ExportParams exportParams = new ExportParams(); + exportParams.setType(ExcelType.XSSF); + Workbook workbook = ExcelExportUtil.exportExcel(exportParams, exportEntities, clubUseDetailList); + CommonDownloadUtil.download(queryYear + "年社团预算使用情况.xlsx", workbook, response); + } + /** * 同步预算主表的已使用额度,保证校工会、分工会、协会三类台账口径一致。 * diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalMineController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalMineController.java index fa81cbb8..f517360f 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalMineController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalMineController.java @@ -265,6 +265,7 @@ public class ProposalMineController { Cnd cnd = Cnd.where("t1.sessionId", "=", sessionId); cnd.andEX("t1.delegationId", "=", delegationId); cnd.and("t1.roleId", "=", "72e24b5b4c4f4e90a6641e6af8487e42"); + cnd.and("t1.delFlag", "!=", 1); cnd.and("t1.loginName", "!=", SecurityUtil.getUserLoginname()); if (isInvite) { @@ -286,11 +287,33 @@ public class ProposalMineController { return Result.success(pagination); } + /** + * 邀请当前提案所属教代会届次内的正式代表参与附议。 + * + * @param seconders 附议人工号数组,数组中的每个工号必须属于当前提案届次的有效正式代表 + * @param proposalId 提案ID,用于确定流程实例及可信的教代会届次 + * @return Result;邀请成功返回 success,提案不存在、代表信息无效或人数不足时返回错误信息 + */ @At @SaCheckPermission("proposal.mine") @Aop(TransAop.READ_COMMITTED) @SLog(tag = "提案", msg = "邀请附议人") public Result inviteSeconder(@Param("seconders") String[] seconders, @Valid String proposalId) { + if (seconders == null || seconders.length == 0) { + return Result.error("请选择附议人"); + } + ProposalInfo proposalInfo = dao.fetch(ProposalInfo.class, proposalId); + if (proposalInfo == null || StrUtil.isBlank(proposalInfo.getSessionId())) { + return Result.error("提案信息不存在"); + } + List requestedLoginNames = List.of(seconders).stream() + .filter(StrUtil::isNotBlank) + .distinct() + .toList(); + if (requestedLoginNames.isEmpty()) { + return Result.error("请选择附议人"); + } + // 查询已经邀请的附议人 ProcessInstance processInstance = dao.fetch(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "=", proposalId)); @@ -319,11 +342,7 @@ public class ProposalMineController { ProposalConfig proposalConfig = dao.fetch(ProposalConfig.class, Cnd.NEW()); - if (tasks.size() + seconders.length < proposalConfig.getSeconderNum()) { - return Result.error("至少邀请" + proposalConfig.getSeconderNum() + "个附议人"); - } - - + // 必须使用提案自身的届次查询,不能只按工号匹配,否则同一代表的历史届次记录会污染代表团等快照信息。 Sql sql = Sqls.create(""" SELECT t1.id, @@ -340,8 +359,20 @@ public class ProposalMineController { LEFT JOIN teacher_congress_delegation t2 ON t2.id = t1.delegationId $condition """); - sql.setCondition(Cnd.where("t1.loginName", "in", seconders).groupBy("t1.loginName")); + Cnd seconderCnd = Cnd.where("t1.sessionId", "=", proposalInfo.getSessionId()) + .and("t1.loginName", "in", requestedLoginNames) + .and("t1.roleId", "=", "72e24b5b4c4f4e90a6641e6af8487e42") + .and("t1.delFlag", "!=", 1); + sql.setCondition(seconderCnd); List list = proposalCommonService.listMap(sql); + // 每个工号在当前届次必须且只能匹配一条有效代表记录,防止无效工号或重复基础数据进入流程。 + if (list.size() != requestedLoginNames.size()) { + return Result.error("部分附议人不属于当前提案届次或代表信息重复,请重新选择"); + } + if (tasks.size() + list.size() < proposalConfig.getSeconderNum()) { + return Result.error("至少邀请" + proposalConfig.getSeconderNum() + "个附议人"); + } + // 附议人ID List userIds = list.stream().map(item -> item.getString("userId")).toList(); // 获取正在执行的任务(邀请附议人||提案附议) diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/impl/ProposalCommitteeFilingUnitServiceImpl.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/impl/ProposalCommitteeFilingUnitServiceImpl.java index 0b26d685..8266ada4 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/impl/ProposalCommitteeFilingUnitServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/impl/ProposalCommitteeFilingUnitServiceImpl.java @@ -45,6 +45,7 @@ import java.util.Arrays; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; @IocBean(args = {"refer:dao"}) @@ -76,14 +77,24 @@ public class ProposalCommitteeFilingUnitServiceImpl extends BaseServiceImpl proposalIds = proposalCommonService.mergeProposal(proposalId); if (ObjectUtil.isEmpty(proposalIds)) { proposalIds = List.of(proposalId); } + /* + * 先同步 proposal_info 和承办单位,再执行 WF 流转,使查看页面及下一节点读取到的都是本次最新数据。 + * 当前方法处于同一事务中,后续流程执行失败时这里的业务数据会一并回滚。 + */ + syncCommitteeFilingUnitData(proposalIds, args); + /* * 并案场景下,同一个节点会在每条提案实例上各有一条待办。 * 这里统一按 taskName 找出整组正在办理中的任务,逐条执行,确保并案提案的流程状态保持一致。 @@ -97,8 +108,6 @@ public class ProposalCommitteeFilingUnitServiceImpl extends BaseServiceImpl proposalIds, String caseFilingCode, + String caseFilingResult, String caseFilingType) { + List proposalInfos = dao().query(ProposalInfo.class, + Cnd.where(ProposalInfo::getId, "in", proposalIds)); + if (proposalInfos.size() != new LinkedHashSet<>(proposalIds).size()) { + throw new BaseException("部分提案信息不存在,无法同步立案结果"); + } + boolean syncFailed = proposalInfos.stream().anyMatch(proposalInfo -> + !Objects.equals(StrUtil.nullToDefault(proposalInfo.getCaseFilingCode(), ""), + StrUtil.nullToDefault(caseFilingCode, "")) + || !Objects.equals(StrUtil.nullToDefault(proposalInfo.getCaseFilingResult(), ""), + StrUtil.nullToDefault(caseFilingResult, "")) + || !Objects.equals(StrUtil.nullToDefault(proposalInfo.getCaseFilingType(), ""), + StrUtil.nullToDefault(caseFilingType, ""))); + if (syncFailed) { + throw new BaseException("立案结果同步失败,请稍后重试"); + } + } + /** * 一个提案可以配置多个主办单位和多个协办单位。 * 这里按页面提交顺序逐条生成承办单位记录,兼容多主办的业务场景。 diff --git a/src/main/java/com/budwk/app/zhgh/democratic/suggestionBox/controller/SuggestionBoxMineController.java b/src/main/java/com/budwk/app/zhgh/democratic/suggestionBox/controller/SuggestionBoxMineController.java index d848963c..1d6361a5 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/suggestionBox/controller/SuggestionBoxMineController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/suggestionBox/controller/SuggestionBoxMineController.java @@ -95,6 +95,7 @@ public class SuggestionBoxMineController { } cnd.andEX("YEAR(info.submitTime)", "=", year); cnd.desc("info.submitTime"); + cnd.groupBy("info.id"); sql.setCondition(cnd); Pagination pagination = suggestionBoxService.listPageMap(pageNumber, pageSize, sql); return Result.success(pagination); 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 index 8190e50f..cfb668bc 100644 --- a/src/main/java/com/budwk/app/zhgh/learning/controller/LearningCourseDisplayController.java +++ b/src/main/java/com/budwk/app/zhgh/learning/controller/LearningCourseDisplayController.java @@ -20,6 +20,7 @@ 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.budwk.app.zhgh.learning.models.LearningStudyRule; import com.google.common.net.HttpHeaders; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; @@ -29,6 +30,7 @@ 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.json.Json; import org.nutz.lang.Strings; import org.nutz.lang.util.NutMap; import org.nutz.mvc.annotation.At; @@ -59,7 +61,7 @@ public class LearningCourseDisplayController { private SysDictService sysDictService; @At("") - @Ok("beetl:/platform/zhgh/Learning/courseDisplay/index.html") + @Ok("beetl:/platform/zhgh/Learning/courseDisplay/indexV2.html") @SaCheckPermission("learning.course.display") public void index() { } @@ -71,7 +73,7 @@ public class LearningCourseDisplayController { } @At("/study") - @Ok("beetl:/platform/zhgh/Learning/courseDisplay/study.html") + @Ok("beetl:/platform/zhgh/Learning/courseDisplay/studyV2.html") @SaCheckPermission("learning.course.display") public void study() { } @@ -79,7 +81,12 @@ public class LearningCourseDisplayController { @At @ApiOperation("课程展示列表") @SaCheckPermission("learning.course.display") - public Result pageData(PageForm pageForm, String keyword, String courseTypeId, String recommendFlag) { + /** + * 查询课程展示列表。 + * pageForm 传分页页码和每页条数;keyword、courseTypeId、recommendFlag 分别用于关键词、分类和推荐标识筛选; + * sortType 支持 default(综合排序)和 view_count(观看次数倒序)。返回值为 Pagination,list 中包含课程基础字段、课节数、总时长和观看次数。 + */ + public Result pageData(PageForm pageForm, String keyword, String courseTypeId, String recommendFlag, String sortType) { Cnd cnd = Cnd.where("c.status", "=", "published"); if (StrUtil.isNotBlank(keyword)) { cnd.and(Cnd.exps("c.courseName", "like", "%" + keyword + "%") @@ -103,13 +110,22 @@ public class LearningCourseDisplayController { 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 + c.recommendFlags, c.courseTypeId, c.lecturerName, c.sortNum, + COALESCE(c.viewCount, 0) AS viewCount, t.typeName AS courseTypeName, + (SELECT COUNT(1) FROM learning_outline_resource r + WHERE r.courseId = c.id AND r.status = 'enabled') AS resourceCount, + COALESCE((SELECT SUM(r.durationSeconds) FROM learning_outline_resource r + WHERE r.courseId = c.id AND r.status = 'enabled'), 0) AS totalDurationSeconds FROM learning_course c LEFT JOIN learning_course_type t ON t.id = c.courseTypeId $condition - ORDER BY c.sortNum ASC, c.createdAt DESC + ORDER BY $orderBy """); listSql.setCondition(cnd); + // 排序字段由固定选项映射,禁止将前端参数直接拼接进SQL。 + listSql.setVar("orderBy", "view_count".equals(sortType) + ? "COALESCE(c.viewCount, 0) DESC, c.sortNum ASC, c.createdAt DESC" + : "c.sortNum ASC, c.createdAt DESC"); listSql.setPager(dao.createPager(pageForm.getPageNumber(), pageForm.getPageSize())); listSql.setCallback(Sqls.callback.maps()); dao.execute(listSql); @@ -126,13 +142,24 @@ public class LearningCourseDisplayController { @At @ApiOperation("课程学习基础信息") @SaCheckPermission("learning.course.display") + /** + * 查询学习页课程信息。 + * id 传课程主键;返回 NutMap,包含课程介绍、讲师、分类、课节数、总时长和后台配置的观看次数。 + */ 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 + SELECT c.id, c.courseName, c.courseIntro, c.suitablePeople, c.learningGoal, c.coursePeriod, + c.courseTags, c.startTime, c.endTime, c.openType, c.cover, c.recommendFlags, + c.lecturerName, c.lecturerInfo, COALESCE(c.viewCount, 0) AS viewCount, + t.typeName AS courseTypeName, + (SELECT COUNT(1) FROM learning_outline_resource r + WHERE r.courseId = c.id AND r.status = 'enabled') AS resourceCount, + COALESCE((SELECT SUM(r.durationSeconds) FROM learning_outline_resource r + WHERE r.courseId = c.id AND r.status = 'enabled'), 0) AS totalDurationSeconds FROM learning_course c LEFT JOIN learning_course_type t ON t.id = c.courseTypeId WHERE c.id = @id @@ -161,12 +188,17 @@ public class LearningCourseDisplayController { 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())); + List outlineRules = dao.query(LearningStudyRule.class, Cnd.where("courseId", "=", courseId).and("targetType", "=", "outline").and("status", "=", "enabled")); Map> childrenMap = new HashMap<>(); List roots = new ArrayList<>(); Map recordMap = new HashMap<>(); + Map outlineRuleMap = new HashMap<>(); for (LearningStudyRecord record : records) { recordMap.put(record.getOutlineId(), record); } + for (LearningStudyRule outlineRule : outlineRules) { + outlineRuleMap.put(outlineRule.getTargetId(), outlineRule); + } Map outlineNodeMap = new HashMap<>(); for (LearningCourseOutline outline : outlines) { @@ -194,6 +226,9 @@ public class LearningCourseDisplayController { } } for (LearningOutlineResource resource : resources) { + // 章节学习规则控制其下媒体播放行为;未配置规则时保持原有的可拖动行为。 + LearningStudyRule outlineRule = outlineRuleMap.get(resource.getOutlineId()); + boolean allowDrag = outlineRule == null || !Boolean.FALSE.equals(outlineRule.getAllowDrag()); NutMap node = NutMap.NEW() .addv("id", resource.getId()) .addv("type", "resource") @@ -207,6 +242,8 @@ public class LearningCourseDisplayController { .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("maxPositionSeconds", resourceMaxPosition(recordMap.get(resource.getOutlineId()), resource.getId())) + .addv("allowDrag", allowDrag) .addv("completeStatus", recordMap.containsKey(resource.getOutlineId()) ? recordMap.get(resource.getOutlineId()).getCompleteStatus() : "not_started"); childrenMap.computeIfAbsent(resource.getOutlineId(), key -> new ArrayList<>()).add(node); } @@ -294,4 +331,23 @@ public class LearningCourseDisplayController { private int value(Integer value) { return value == null ? 0 : value; } + + /** + * 读取指定资源的最高已观看秒数。该值用于两端限制快进,最近播放位置仍单独用于续播。 + */ + private int resourceMaxPosition(LearningStudyRecord record, String resourceId) { + if (record == null || StrUtil.isBlank(resourceId)) { + return 0; + } + if (StrUtil.isBlank(record.getResourceMaxPositionData())) { + return value(record.getLastPositionSeconds()); + } + try { + NutMap positions = Json.fromJson(NutMap.class, record.getResourceMaxPositionData()); + return Math.max(0, positions.getInt(resourceId, 0)); + } catch (Exception ignored) { + // 历史记录未保存资源级位置时,从零开始建立最高观看位置。 + return 0; + } + } } 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 index be7e688d..53b93956 100644 --- a/src/main/java/com/budwk/app/zhgh/learning/controller/LearningCourseManageController.java +++ b/src/main/java/com/budwk/app/zhgh/learning/controller/LearningCourseManageController.java @@ -18,6 +18,8 @@ import org.nutz.dao.Cnd; import org.nutz.dao.Dao; import org.nutz.dao.Sqls; import org.nutz.dao.sql.Sql; +import org.nutz.aop.interceptor.ioc.TransAop; +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; @@ -113,6 +115,8 @@ public class LearningCourseManageController { @ApiOperation("新增课程") @SaCheckPermission("learning.course.manage") @SLog(tag = "新增课程", msg = "课程名称:${args[0].courseName}") + @Aop(TransAop.READ_COMMITTED) + /** course 传课程表单字段,viewCount 为非负整数;返回 Result 表示新增是否成功。 */ public Result doAdd(LearningCourse course) { Result checkResult = checkCourse(course, null); if (checkResult != null) { @@ -121,6 +125,9 @@ public class LearningCourseManageController { if (StrUtil.isBlank(course.getStatus())) { course.setStatus("draft"); } + if (course.getViewCount() == null) { + course.setViewCount(0); + } dao.insert(course); return Result.success(); } @@ -129,6 +136,8 @@ public class LearningCourseManageController { @ApiOperation("编辑课程") @SaCheckPermission("learning.course.manage") @SLog(tag = "编辑课程", msg = "课程ID:${args[0].id}") + @Aop(TransAop.READ_COMMITTED) + /** course 传课程主键及需要更新的表单字段;返回 Result 表示编辑是否成功。 */ public Result doEdit(LearningCourse course) { if (StrUtil.isBlank(course.getId())) { return Result.error("请选择要编辑的数据"); @@ -145,6 +154,8 @@ public class LearningCourseManageController { @ApiOperation("删除课程") @SaCheckPermission("learning.course.manage") @SLog(tag = "删除课程", msg = "课程ID:${args[0]}") + @Aop(TransAop.READ_COMMITTED) + /** id 传课程主键;返回 Result,存在章节内容时返回不可删除提示。 */ public Result doDelete(String id) { if (StrUtil.isBlank(id)) { return Result.error("请选择要删除的数据"); @@ -200,6 +211,9 @@ public class LearningCourseManageController { if (course.getSortNum() == null) { return Result.error("排序编码不能为空"); } + if (course.getViewCount() != null && course.getViewCount() < 0) { + return Result.error("观看次数不能小于0"); + } if (!"long_term".equals(course.getOpenType()) && (course.getStartTime() == null || course.getEndTime() == null)) { return Result.error("请选择开课时间"); } @@ -227,6 +241,7 @@ public class LearningCourseManageController { columns.put("status", "c.status"); columns.put("recommendFlags", "c.recommendFlags"); columns.put("sortNum", "c.sortNum"); + columns.put("viewCount", "c.viewCount"); return columns.get(prop); } } 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 index b5fbb986..44cdc4c2 100644 --- a/src/main/java/com/budwk/app/zhgh/learning/controller/LearningStudyRecordController.java +++ b/src/main/java/com/budwk/app/zhgh/learning/controller/LearningStudyRecordController.java @@ -24,8 +24,11 @@ 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.aop.interceptor.ioc.TransAop; +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.At; import org.nutz.mvc.annotation.Ok; @@ -151,6 +154,11 @@ public class LearningStudyRecordController { @At @ApiOperation("开始学习") @SaCheckPermission("learning.course.display") + @Aop(TransAop.READ_COMMITTED) + /** + * courseId、resourceId 分别传课程和资源主键,positionSeconds 传当前播放秒数; + * 返回学习记录、学习时段、学习进度和本次自增后的课程观看次数。 + */ public Result start(@Param("courseId") String courseId, @Param("resourceId") String resourceId, @Param("positionSeconds") Integer positionSeconds) { @@ -176,18 +184,30 @@ public class LearningStudyRecordController { .and("outlineId", "=", outline.getId())); if (record == null) { record = buildRecord(userId, course, outline, now); - record.setLastPositionSeconds(sanitizePosition(positionSeconds, 0)); + int startPositionSeconds = sanitizePosition(positionSeconds, 0); + record.setLastPositionSeconds(startPositionSeconds); + record.setResourceMaxPositionData(resourceMaxPositionData(resourceId, startPositionSeconds)); dao.insert(record); } else { int requiredSeconds = requiredSeconds(outline.getId()); - dao.update(LearningStudyRecord.class, Chain.make("latestStudyTime", now) + int startPositionSeconds = sanitizePosition(positionSeconds, 0); + Map resourceMaxPositions = resourceMaxPositions(record.getResourceMaxPositionData()); + int maxPositionSeconds = value(resourceMaxPositions.get(resourceId)); + boolean needUpdateMaxPosition = !resourceMaxPositions.containsKey(resourceId) || startPositionSeconds > maxPositionSeconds; + if (needUpdateMaxPosition) { + resourceMaxPositions.put(resourceId, Math.max(startPositionSeconds, maxPositionSeconds)); + } + Chain chain = 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())); + .add("completeStatus", "completed".equals(record.getCompleteStatus()) ? "completed" : "studying"); + if (needUpdateMaxPosition) { + chain.add("resourceMaxPositionData", Json.toJson(resourceMaxPositions)); + } + dao.update(LearningStudyRecord.class, chain, Cnd.where("id", "=", record.getId())); record = dao.fetch(LearningStudyRecord.class, record.getId()); } @@ -204,19 +224,28 @@ public class LearningStudyRecordController { segment.setState("studying"); dao.insert(segment); + // 每次成功创建学习时段记为一次观看,使用原子自增避免并发开始学习时丢失计数。 + dao.update(LearningCourse.class, + Chain.makeSpecial("viewCount", "IFNULL(viewCount, 0) + 1"), + Cnd.where("id", "=", courseId)); + LearningCourse updatedCourse = dao.fetch(LearningCourse.class, courseId); + return Result.success(NutMap.NEW() .addv("recordId", record.getId()) .addv("segmentId", segment.getId()) .addv("studySeconds", record.getStudySeconds()) .addv("lastPositionSeconds", record.getLastPositionSeconds()) + .addv("maxPositionSeconds", resourceMaxPosition(record, resourceId)) .addv("requiredSeconds", record.getRequiredSeconds()) .addv("progressPercent", record.getProgressPercent()) - .addv("completeStatus", record.getCompleteStatus())); + .addv("completeStatus", record.getCompleteStatus()) + .addv("viewCount", value(updatedCourse == null ? null : updatedCourse.getViewCount()))); } @At @ApiOperation("学习心跳") @SaCheckPermission("learning.course.display") + @Aop(TransAop.READ_COMMITTED) public Result heartbeat(@Param("segmentId") String segmentId, @Param("activeSeconds") Integer activeSeconds, @Param("positionSeconds") Integer positionSeconds) { @@ -225,21 +254,27 @@ public class LearningStudyRecordController { 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)); + return Result.success(addActiveSeconds(segment, seconds, false, false, positionSeconds)); } @At @ApiOperation("结束学习") @SaCheckPermission("learning.course.display") + @Aop(TransAop.READ_COMMITTED) + /** + * segmentId 传当前学习时段主键,activeSeconds 传本次尚未上报的有效学习秒数,positionSeconds 传当前播放位置; + * completed 仅在视频或音频自然播放结束时传 true。返回值为 NutMap,包含累计学习时长、进度和完成状态。 + */ public Result finish(@Param("segmentId") String segmentId, @Param("activeSeconds") Integer activeSeconds, - @Param("positionSeconds") Integer positionSeconds) { + @Param("positionSeconds") Integer positionSeconds, + @Param("completed") Boolean completed) { 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); + NutMap result = addActiveSeconds(segment, seconds, true, Boolean.TRUE.equals(completed), positionSeconds); dao.update(LearningStudySegment.class, Chain.make("endTime", new Date()).add("state", "finished"), Cnd.where("id", "=", segmentId)); return Result.success(result); } @@ -247,6 +282,7 @@ public class LearningStudyRecordController { @At @ApiOperation("保存播放位置") @SaCheckPermission("learning.course.display") + @Aop(TransAop.READ_COMMITTED) public Result position(@Param("courseId") String courseId, @Param("resourceId") String resourceId, @Param("positionSeconds") Integer positionSeconds) { @@ -321,20 +357,28 @@ public class LearningStudyRecordController { record.setLatestStudyTime(now); record.setStudySeconds(0); record.setLastPositionSeconds(0); + record.setResourceMaxPositionData("{}"); record.setRequiredSeconds(requiredSeconds); record.setProgressPercent(0); record.setCompleteStatus("studying"); return record; } - private NutMap addActiveSeconds(LearningStudySegment segment, int activeSeconds, boolean finish, Integer positionSeconds) { + private NutMap addActiveSeconds(LearningStudySegment segment, int activeSeconds, boolean finish, boolean completed, Integer positionSeconds) { Date now = new Date(); - int segmentSeconds = value(segment.getActiveSeconds()) + activeSeconds; + LearningStudyRecord record = dao.fetch(LearningStudyRecord.class, segment.getRecordId()); + LearningOutlineResource resource = dao.fetch(LearningOutlineResource.class, segment.getResourceId()); + int studyIncrementSeconds = effectiveStudySeconds(record, resource, segment.getResourceId(), activeSeconds, positionSeconds); + int segmentSeconds = value(segment.getActiveSeconds()) + studyIncrementSeconds; 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 studySeconds = value(record.getStudySeconds()) + studyIncrementSeconds; int requiredSeconds = requiredSeconds(record.getOutlineId()); + int maxPositionSeconds = resourceMaxPosition(record, segment.getResourceId()); + // 自然播放结束且当前章节仅有一个媒体资源时,补齐定时器未统计到的尾部秒数,避免跨端状态不一致。 + if (completed && canCompleteByNaturalEnd(segment, positionSeconds, requiredSeconds)) { + studySeconds = Math.max(studySeconds, requiredSeconds); + } int progressPercent = progress(studySeconds, requiredSeconds); String completeStatus = completeStatus(studySeconds, requiredSeconds, finish); if ("completed".equals(completeStatus)) { @@ -350,6 +394,13 @@ public class LearningStudyRecordController { lastPositionSeconds = positionSeconds; chain.add("lastPositionSeconds", positionSeconds); } + if (resource != null && isMediaResource(resource)) { + Map resourceMaxPositions = resourceMaxPositions(record.getResourceMaxPositionData()); + int currentPositionSeconds = Math.max(0, positionSeconds == null ? 0 : positionSeconds); + maxPositionSeconds = Math.max(value(resourceMaxPositions.get(segment.getResourceId())), currentPositionSeconds); + resourceMaxPositions.put(segment.getResourceId(), maxPositionSeconds); + chain.add("resourceMaxPositionData", Json.toJson(resourceMaxPositions)); + } if ("completed".equals(completeStatus) && record.getCompletedAt() == null) { chain.add("completedAt", now); } @@ -358,6 +409,7 @@ public class LearningStudyRecordController { .addv("studySeconds", studySeconds) .addv("studyTimeText", formatStudySeconds(studySeconds)) .addv("lastPositionSeconds", lastPositionSeconds) + .addv("maxPositionSeconds", maxPositionSeconds) .addv("requiredSeconds", requiredSeconds) .addv("progressPercent", progressPercent) .addv("completeStatus", completeStatus); @@ -396,6 +448,85 @@ public class LearningStudyRecordController { return Math.max(0, sql.getInt()); } + /** + * 校验媒体自然播放结束能否完成当前章节。 + * segment 表示当前用户的学习时段;positionSeconds 必须到达资源末尾附近;requiredSeconds 为章节要求学习时长。 + * 仅单资源章节且配置时长不超过媒体时长时返回 true,防止一个资源结束误完成包含多个资源或设置了更长时长的章节。 + */ + private boolean canCompleteByNaturalEnd(LearningStudySegment segment, Integer positionSeconds, int requiredSeconds) { + if (positionSeconds == null || requiredSeconds <= 0) { + return false; + } + LearningOutlineResource resource = dao.fetch(LearningOutlineResource.class, segment.getResourceId()); + if (resource == null || !("video".equals(resource.getResourceType()) || "audio".equals(resource.getResourceType()))) { + return false; + } + int durationSeconds = value(resource.getDurationSeconds()); + if (durationSeconds <= 0 || positionSeconds < Math.max(0, durationSeconds - 2) || requiredSeconds > durationSeconds) { + return false; + } + Sql countSql = Sqls.create("SELECT COUNT(1) FROM learning_outline_resource WHERE outlineId = @outlineId AND status = 'enabled'"); + countSql.params().set("outlineId", segment.getOutlineId()); + countSql.setCallback(Sqls.callback.integer()); + dao.execute(countSql); + return countSql.getInt() == 1; + } + + /** + * 仅统计当前资源超过历史最高观看位置的新增区间,回退复习不会重复累计学习时长。 + * record 保存各资源最高位置,resource 用于区分媒体和非媒体资料,positionSeconds 为本次心跳时的播放器位置。 + */ + private int effectiveStudySeconds(LearningStudyRecord record, LearningOutlineResource resource, String resourceId, int activeSeconds, Integer positionSeconds) { + if (!isMediaResource(resource)) { + return activeSeconds; + } + int currentPositionSeconds = Math.max(0, positionSeconds == null ? 0 : positionSeconds); + int maxPositionSeconds = resourceMaxPosition(record, resourceId); + return Math.min(activeSeconds, Math.max(0, currentPositionSeconds - maxPositionSeconds)); + } + + /** + * 读取学习记录中按资源保存的最高已观看秒数。历史记录或异常 JSON 视为未保存,避免影响已有记录的使用。 + */ + private int resourceMaxPosition(LearningStudyRecord record, String resourceId) { + return value(resourceMaxPositions(record == null ? null : record.getResourceMaxPositionData()).get(resourceId)); + } + + /** + * 将资源 ID 与最高观看秒数转换为可持久化数据;键为资源 ID,值为该资源累计到达过的最远秒数。 + */ + private String resourceMaxPositionData(String resourceId, int positionSeconds) { + Map positions = new HashMap<>(); + positions.put(resourceId, Math.max(0, positionSeconds)); + return Json.toJson(positions); + } + + /** + * 解析资源最高位置 JSON。返回值为资源 ID 到秒数的映射,空值或异常数据返回空映射。 + */ + private Map resourceMaxPositions(String data) { + Map positions = new HashMap<>(); + if (StrUtil.isBlank(data)) { + return positions; + } + try { + NutMap source = Json.fromJson(NutMap.class, data); + for (String resourceId : source.keySet()) { + positions.put(resourceId, Math.max(0, source.getInt(resourceId, 0))); + } + } catch (Exception ignored) { + // 历史记录可能没有该字段或保存了异常内容,按未记录最高位置处理。 + } + return positions; + } + + /** + * 判断资料是否使用播放器计时。视频和音频按未重复观看区间累计,其他资料沿用活跃时长累计。 + */ + private boolean isMediaResource(LearningOutlineResource resource) { + return resource != null && ("video".equals(resource.getResourceType()) || "audio".equals(resource.getResourceType())); + } + private int progress(Integer studySeconds, int requiredSeconds) { if (requiredSeconds <= 0) { return 0; 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 index 32b14e68..e3cfdfed 100644 --- a/src/main/java/com/budwk/app/zhgh/learning/models/LearningCourse.java +++ b/src/main/java/com/budwk/app/zhgh/learning/models/LearningCourse.java @@ -110,6 +110,11 @@ public class LearningCourse extends BaseModel { @ColDefine(type = ColType.INT) private Integer sortNum; + @Column + @Comment("观看次数") + @ColDefine(type = ColType.INT) + private Integer viewCount; + @Column @Comment("课程封面") @ColDefine(type = ColType.VARCHAR, width = 500) 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 index 3723c783..6ea2a74b 100644 --- a/src/main/java/com/budwk/app/zhgh/learning/models/LearningStudyRecord.java +++ b/src/main/java/com/budwk/app/zhgh/learning/models/LearningStudyRecord.java @@ -97,6 +97,11 @@ public class LearningStudyRecord extends BaseModel { @ColDefine(type = ColType.INT) private Integer lastPositionSeconds; + @Column + @Comment("各资源最高已观看位置(JSON)") + @ColDefine(type = ColType.TEXT) + private String resourceMaxPositionData; + @Column @Comment("要求学习时长(秒)") @ColDefine(type = ColType.INT) diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/change/MemberChangeMineController.java b/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/change/MemberChangeMineController.java index c365445d..4a87955d 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/change/MemberChangeMineController.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/change/MemberChangeMineController.java @@ -17,6 +17,7 @@ import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeType; import com.budwk.app.zhgh.staffmanage.member.models.MemberChangeRecord; import com.budwk.app.zhgh.staffmanage.member.param.pageform.MemberChangePageForm; import com.budwk.app.zhgh.staffmanage.member.service.MemberCommonService; +import com.budwk.app.sys.models.Sys_unit; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiOperation; import org.nutz.aop.interceptor.ioc.TransAop; @@ -155,6 +156,12 @@ public class MemberChangeMineController { } + /** + * 查询会员变更记录详情。 + * + * @param id 会员变更记录 ID。 + * @return 包含变更记录基础字段、三级单位名称及变更明细的 JSON 对象。 + */ @At @ApiOperation("根据id查询变更记录") @SaCheckPermission(value = {"member.change.mine", "member.change.unionGroup.audit"}, mode = SaMode.OR) @@ -162,6 +169,12 @@ public class MemberChangeMineController { MemberChangeRecord record = dao.fetch(MemberChangeRecord.class, id); NutMap nutMap = Lang.obj2nutmap(record); + // 变更记录只保存三级单位 ID,查看时补充当前单位名称供页面展示。 + if (StrUtil.isNotBlank(record.getThreeUnitId())) { + Sys_unit threeUnit = dao.fetch(Sys_unit.class, record.getThreeUnitId()); + nutMap.put("threeUnitName", threeUnit == null ? "" : threeUnit.getName()); + } + List changeInfos = memberCommonService.getChangeInfos(nutMap.getString("id")); nutMap.put("changeInfos", changeInfos); return Result.success().addData(nutMap); diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/member/models/MemberChangeRecord.java b/src/main/java/com/budwk/app/zhgh/staffmanage/member/models/MemberChangeRecord.java index 61a1d8bb..c16757b0 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/member/models/MemberChangeRecord.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/member/models/MemberChangeRecord.java @@ -177,6 +177,11 @@ public class MemberChangeRecord extends BaseModel { @ColDefine(type = ColType.VARCHAR, width = 20) private String campus; + @Column + @Comment("三级单位ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String threeUnitId; + @Column @Comment("退休日期") @ColDefine(type = ColType.DATE) diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/member/param/pageform/MemberManagePageForm.java b/src/main/java/com/budwk/app/zhgh/staffmanage/member/param/pageform/MemberManagePageForm.java index 117ab932..2a71076c 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/member/param/pageform/MemberManagePageForm.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/member/param/pageform/MemberManagePageForm.java @@ -30,6 +30,9 @@ public class MemberManagePageForm extends PageForm { private String unitId; + /** 所属二级单位下的三级单位 ID。 */ + private String threeUnitId; + private String sex; private String personType; @@ -91,6 +94,7 @@ public class MemberManagePageForm extends PageForm { cnd.andEX("u.member", "=", pageForm.getIsMember()); cnd.andEX("u.welfareMember", "=", pageForm.getIsWelfareMember()); cnd.andEX("u.unitId", "=", pageForm.getUnitId()); + cnd.andEX("u.threeUnitId", "=", pageForm.getThreeUnitId()); cnd.andEX("u.userState", "=", pageForm.getUserState()); cnd.andEX("u.preparedBy", "=", pageForm.getPreparedBy()); cnd.andEX("u.sex", "=", pageForm.getSex()); diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/impl/MemberManageServiceImpl.java b/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/impl/MemberManageServiceImpl.java index 3673b382..6bc1cbbd 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/impl/MemberManageServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/impl/MemberManageServiceImpl.java @@ -257,6 +257,12 @@ public class MemberManageServiceImpl extends BaseServiceImpl implement Set allowChangeFieldNames = map.getAs("allowChangeFieldNames", Set.class); Map dictMap = map.getAs("dictMap", Map.class); + // 三级单位仅由高级管理页面传入,未传入时不参与比较,避免其他共用页面误判为清空。 + if (record.getThreeUnitId() != null) { + allowChangeFieldNames.add("threeUnitId"); + dictMap.put("threeUnitId", "三级单位"); + } + // 新数据 NutMap newMap = Lang.obj2nutmap(record); // 原数据 diff --git a/src/main/java/com/budwk/app/zhgh/welfare/controller/WelfareSelectionSituationController.java b/src/main/java/com/budwk/app/zhgh/welfare/controller/WelfareSelectionSituationController.java index c66815b0..c682e769 100644 --- a/src/main/java/com/budwk/app/zhgh/welfare/controller/WelfareSelectionSituationController.java +++ b/src/main/java/com/budwk/app/zhgh/welfare/controller/WelfareSelectionSituationController.java @@ -11,6 +11,7 @@ import com.budwk.app.zhgh.welfare.model.WelfareList; import com.budwk.app.zhgh.welfare.model.WelfareUserSelection; import com.budwk.app.zhgh.welfare.param.WelfareSelectionSituationPageForm; import com.budwk.app.zhgh.welfare.service.WelfareListService; +import com.budwk.app.zhgh.welfare.service.WelfareProjectService; import com.budwk.app.zhgh.welfare.service.WelfareSelectionSituationService; import com.budwk.app.zhgh.welfare.service.WelfareStatisticsService; import io.swagger.annotations.Api; @@ -47,6 +48,8 @@ public class WelfareSelectionSituationController { private WelfareStatisticsService welfareStatisticsService; @Inject private WelfareSelectionSituationService situationService; + @Inject + private WelfareProjectService welfareProjectService; @At("") @Ok("beetl:/platform/zhgh/welfare/selectionSituation/index.html") @@ -86,6 +89,11 @@ public class WelfareSelectionSituationController { return Result.error("此用户没有选择的权限"); } + String defaultOptionValidationMessage = welfareProjectService.validateSystemDefaultOptionSelection(projectId, selections); + if (defaultOptionValidationMessage != null) { + return Result.error(defaultOptionValidationMessage); + } + //删除上次选择的 dao.clear(WelfareUserSelection.class, Cnd.where(WelfareUserSelection::getWelfareId, "=", projectId).and(WelfareUserSelection::getSelectUserId, "=", userId)); for (WelfareUserSelection welfareUserSelection : selections) { diff --git a/src/main/java/com/budwk/app/zhgh/welfare/controller/WelfareUserSelectController.java b/src/main/java/com/budwk/app/zhgh/welfare/controller/WelfareUserSelectController.java index 4bc86b91..9a7e6e1a 100644 --- a/src/main/java/com/budwk/app/zhgh/welfare/controller/WelfareUserSelectController.java +++ b/src/main/java/com/budwk/app/zhgh/welfare/controller/WelfareUserSelectController.java @@ -89,6 +89,11 @@ public class WelfareUserSelectController { } WelfareProject project = dao.fetch(WelfareProject.class, projectId); + String defaultOptionValidationMessage = welfareProjectService.validateSystemDefaultOptionSelection(projectId, selections); + if (defaultOptionValidationMessage != null) { + return Result.error(defaultOptionValidationMessage); + } + int sum = Arrays.stream(selections).mapToInt(selection -> Objects.requireNonNullElse(selection.getSelectNum(), 0)).sum(); if (sum > project.getMultiSelectNum()) { return Result.error("选择的数量不能超过" + project.getMultiSelectNum()); diff --git a/src/main/java/com/budwk/app/zhgh/welfare/param/WelfareFilterUserPageForm.java b/src/main/java/com/budwk/app/zhgh/welfare/param/WelfareFilterUserPageForm.java index 1346694a..cf639989 100644 --- a/src/main/java/com/budwk/app/zhgh/welfare/param/WelfareFilterUserPageForm.java +++ b/src/main/java/com/budwk/app/zhgh/welfare/param/WelfareFilterUserPageForm.java @@ -37,8 +37,11 @@ public class WelfareFilterUserPageForm extends PageForm { @ApiModelProperty("工会名称") private String unionName; - @ApiModelProperty("单位id") - private String[] unitIds; + @ApiModelProperty("所属单位ID") + private String unitId; + + @ApiModelProperty("三级单位ID") + private String threeUnitId; @ApiModelProperty("单位名称") private String unitName; diff --git a/src/main/java/com/budwk/app/zhgh/welfare/service/WelfareProjectService.java b/src/main/java/com/budwk/app/zhgh/welfare/service/WelfareProjectService.java index ed9263d5..a061f95a 100644 --- a/src/main/java/com/budwk/app/zhgh/welfare/service/WelfareProjectService.java +++ b/src/main/java/com/budwk/app/zhgh/welfare/service/WelfareProjectService.java @@ -2,6 +2,7 @@ package com.budwk.app.zhgh.welfare.service; import com.budwk.app.base.service.BaseService; import com.budwk.app.zhgh.welfare.model.WelfareProject; +import com.budwk.app.zhgh.welfare.model.WelfareUserSelection; import java.util.List; @@ -16,6 +17,15 @@ public interface WelfareProjectService extends BaseService { */ WelfareProject projectInfo(String projectId); + /** + * 校验福利选择是否保留系统默认福利。 + * + * @param projectId 福利项目 ID,用于查询该项目配置的系统默认福利 + * @param selections 用户提交的福利选择数组,每项需包含 selectOptionId 和大于 0 的 selectNum + * @return 校验通过返回 {@code null};未保留系统默认福利时返回错误提示文本 + */ + String validateSystemDefaultOptionSelection(String projectId, WelfareUserSelection[] selections); + /** * 保存项目信息 * @param project diff --git a/src/main/java/com/budwk/app/zhgh/welfare/service/impl/WelfareListServiceImpl.java b/src/main/java/com/budwk/app/zhgh/welfare/service/impl/WelfareListServiceImpl.java index 189ce5b8..3574af35 100644 --- a/src/main/java/com/budwk/app/zhgh/welfare/service/impl/WelfareListServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/welfare/service/impl/WelfareListServiceImpl.java @@ -471,7 +471,8 @@ public class WelfareListServiceImpl extends BaseServiceImpl impleme cnd.andEX("MONTH(u.birthday)", "in", pageForm.getBirthMonths()); } cnd.andEX("u.unionId", "=", pageForm.getUnionId()); - cnd.andEX("u.unitId", "in", pageForm.getUnitIds()); + cnd.andEX("u.unitId", "=", pageForm.getUnitId()); + cnd.andEX("u.threeUnitId", "=", pageForm.getThreeUnitId()); cnd.andEX("u.personType", "in", pageForm.getPersonTypes()); cnd.andEX("u.userAttribute", "in", pageForm.getUserAttributes()); cnd.andEX("u.preparedBy", "in", pageForm.getPreparedBys()); @@ -524,7 +525,8 @@ public class WelfareListServiceImpl extends BaseServiceImpl impleme cnd.andEX("MONTH(birthday)", "in", pageForm.getBirthMonths()); } cnd.andEX("unionId", "=", pageForm.getUnionId()); - cnd.andEX("unitId", "in", pageForm.getUnitIds()); + cnd.andEX("unitId", "=", pageForm.getUnitId()); + cnd.andEX("threeUnitId", "=", pageForm.getThreeUnitId()); cnd.andEX("personType", "in", pageForm.getPersonTypes()); cnd.andEX("userAttribute", "in", pageForm.getUserAttributes()); cnd.andEX("preparedBy", "in", pageForm.getPreparedBys()); diff --git a/src/main/java/com/budwk/app/zhgh/welfare/service/impl/WelfareProjectServiceImpl.java b/src/main/java/com/budwk/app/zhgh/welfare/service/impl/WelfareProjectServiceImpl.java index ce5eb504..cae1c2b5 100644 --- a/src/main/java/com/budwk/app/zhgh/welfare/service/impl/WelfareProjectServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/welfare/service/impl/WelfareProjectServiceImpl.java @@ -49,6 +49,30 @@ public class WelfareProjectServiceImpl extends BaseServiceImpl i return project; } + @Override + public String validateSystemDefaultOptionSelection(String projectId, WelfareUserSelection[] selections) { + List systemDefaultOptions = dao().query(WelfareProjectSubjectOption.class, + Cnd.where("welfareId", "=", projectId).and("isSystemDefault", "=", true)); + if (systemDefaultOptions.isEmpty()) { + return null; + } + + Set selectedOptionIds = Arrays.stream(selections == null ? new WelfareUserSelection[0] : selections) + .filter(Objects::nonNull) + .filter(selection -> selection.getSelectNum() != null && selection.getSelectNum() > 0) + .map(WelfareUserSelection::getSelectOptionId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + List missingOptionNames = systemDefaultOptions.stream() + .filter(option -> !selectedOptionIds.contains(option.getId())) + .map(WelfareProjectSubjectOption::getOptionName) + .collect(Collectors.toList()); + if (missingOptionNames.isEmpty()) { + return null; + } + return "系统默认福利“" + String.join("、", missingOptionNames) + "”不可取消"; + } + private void fillOptionRank(String projectId, List options) { if (options == null || options.isEmpty()) { return; diff --git a/src/main/resources/static/assets/platform/js/mixin/flowMixins.js b/src/main/resources/static/assets/platform/js/mixin/flowMixins.js index b2a92fe9..9206a3e8 100644 --- a/src/main/resources/static/assets/platform/js/mixin/flowMixins.js +++ b/src/main/resources/static/assets/platform/js/mixin/flowMixins.js @@ -22,25 +22,29 @@ const flowMixins = { background: 'rgba(0, 0, 0, 0.7)' }); - console.log(this.$refs) - console.log(this.$refs.guava) - - // this.oepnAudit({}) - // - // - // this.$axios.post(location.pathname + '/queryTask', {taskId}).then(res => { - // if (res.code === 0) { - // // 延迟1.5秒 显示loading动画 解决Guava异步加载 获取不到实例的问题 - // setTimeout(() => { - // if (res.data.taskState === 10) { - // this.openAudit(res.data); - // } else { - // this.openView(res.data); - // } - // loading.close(); - // }, 1500) - // } - // }) + let delayCloseLoading = false + this.$axios.post(location.pathname + "/queryTask", {taskId}).then((res) => { + if (res.code === 0 && res.data) { + delayCloseLoading = true + // Guava 为异步组件,等待组件实例完成挂载后再打开任务弹窗。 + setTimeout(() => { + try { + if (res.data.taskState === 10) { + this.openAudit(res.data) + } else { + this.openView(res.data) + } + } finally { + loading.close() + } + }, 1500) + } + }).finally(() => { + // 查询失败或未返回任务数据时,必须关闭全屏 Loading。 + if (!delayCloseLoading) { + loading.close() + } + }) }, } } diff --git a/src/main/resources/static/assets/platform/js/mixin/initTableMixins.js b/src/main/resources/static/assets/platform/js/mixin/initTableMixins.js index f0796c12..9b475c3a 100644 --- a/src/main/resources/static/assets/platform/js/mixin/initTableMixins.js +++ b/src/main/resources/static/assets/platform/js/mixin/initTableMixins.js @@ -71,11 +71,13 @@ const initTableMixins = { const address = this.pageDataUrl ? this.pageDataUrl : loc() + "/pageData" this.tableLoading = true this.$axios.post(address, data ? data : this.pageForm).then((res) => { - this.tableLoading = false if (res.code === 0) { this.tableData = res.data.list this.pageForm.totalCount = res.data.totalCount } + }).finally(() => { + // 接口异常时同样结束加载状态,避免表格持续显示加载动画。 + this.tableLoading = false }) }, notifySuccess(msg) { diff --git a/src/main/resources/static/components/plugins/sysUpload/index.vue b/src/main/resources/static/components/plugins/sysUpload/index.vue index b23d1f16..3b3a2c7d 100644 --- a/src/main/resources/static/components/plugins/sysUpload/index.vue +++ b/src/main/resources/static/components/plugins/sysUpload/index.vue @@ -35,8 +35,9 @@ :accept="fileAccept" :multiple="true" list-type="picture-card" + :class="{ 'image-upload-entry-hidden': !showImageUploadEntry }" > - + @@ -208,6 +209,10 @@ module.exports = { const number = Number(this.upload_number) return Number.isFinite(number) && number > 0 ? number : 1 }, + // 单图上传完成后隐藏上传入口;删除图片后 fileList 清空,入口会自动恢复。 + showImageUploadEntry() { + return this.uploadLimitNumber !== 1 || this.fileList.length < this.uploadLimitNumber + }, upload_tips() { const uploadNumber = this.uploadLimitNumber const fileAccept = this.fileAccept @@ -655,4 +660,10 @@ module.exports = { height: var(--upload-height, 148px); } +/* 单图已上传时只隐藏图片卡片上传入口,保留图片预览列表与删除按钮。 */ +.image-upload-entry-hidden.el-upload--picture-card, +.image-upload-entry-hidden .el-upload--picture-card { + display: none !important; +} + diff --git a/src/main/resources/views/layouts/v4/home/index.html b/src/main/resources/views/layouts/v4/home/index.html index a2f9f0f2..2bf702d2 100644 --- a/src/main/resources/views/layouts/v4/home/index.html +++ b/src/main/resources/views/layouts/v4/home/index.html @@ -93,6 +93,22 @@ layout("/layouts/v4/baseLayout.html"){ font-weight: 500; } + /* 推荐入口的 scoped 样式会在组件挂载后注入,此处提前限制图片尺寸,避免刷新时原图短暂撑开页面。 */ + #v4-home-app .entry-card .app-icon { + width: 56px; + height: 56px; + overflow: hidden; + } + + #v4-home-app .entry-card .app-icon img { + display: block; + width: 56px; + height: 56px; + max-width: 56px; + max-height: 56px; + object-fit: contain; + } +
diff --git a/src/main/resources/views/platform/zhgh/Learning/chapterContent/index.html b/src/main/resources/views/platform/zhgh/Learning/chapterContent/index.html index 032213f4..9027f2a7 100644 --- a/src/main/resources/views/platform/zhgh/Learning/chapterContent/index.html +++ b/src/main/resources/views/platform/zhgh/Learning/chapterContent/index.html @@ -140,8 +140,8 @@ layout("/layouts/platform.html"){ - - + + @@ -331,8 +331,8 @@ layout("/layouts/platform.html"){ - - + + @@ -708,8 +708,7 @@ layout("/layouts/platform.html"){ if (row) { this.resourceForm = Object.assign({}, row, { outlineName: target.title, - fileData: this.parseFiles(row.fileData), - durationMinutes: this.formatDurationMinutes(row.durationSeconds) + fileData: this.parseFiles(row.fileData) }) } else { this.resourceForm = { @@ -721,7 +720,6 @@ layout("/layouts/platform.html"){ fileExt: "", fileData: [], durationSeconds: 0, - durationMinutes: 0, sortOrder: 1, required: false, allowPreview: true, @@ -742,14 +740,12 @@ layout("/layouts/platform.html"){ this.$refs.resourceForm.validate(async valid => { if (!valid) return const data = Object.assign({}, this.resourceForm) - data.durationSeconds = Math.round((data.durationMinutes || 0) * 60) data.fileExt = this.getFileExt(data.fileData) if (!this.validResourceExt(data.resourceType, data.fileExt)) { this.$message.warning("当前资料类型不支持上传 ." + data.fileExt + " 格式文件") return } delete data.outlineName - delete data.durationMinutes data.fileData = JSON.stringify(data.fileData || []) const resp = await this.$axios.post(loc() + "/saveResource", data) if (resp.code === 0) { @@ -826,6 +822,10 @@ layout("/layouts/platform.html"){ const seconds = Number(durationSeconds || 0) return seconds > 0 ? Math.ceil(seconds / 60) : 0 }, + formatDurationSeconds(durationSeconds) { + const seconds = Math.ceil(Number(durationSeconds || 0)) + return seconds > 0 ? seconds + "秒" : "0秒" + }, autoFillMediaDuration() { if (!["video", "audio"].includes(this.resourceForm.resourceType)) return const url = this.getFileUrl(this.resourceForm.fileData) @@ -835,7 +835,8 @@ layout("/layouts/platform.html"){ media.onloadedmetadata = () => { window.URL.revokeObjectURL(media.src) if (isFinite(media.duration) && media.duration > 0) { - this.$set(this.resourceForm, "durationMinutes", Math.ceil(media.duration / 60)) + // 媒体时长按秒保存,避免先取整到分钟再换算导致实际时长被放大。 + this.$set(this.resourceForm, "durationSeconds", Math.ceil(media.duration)) } } media.onerror = () => { diff --git a/src/main/resources/views/platform/zhgh/Learning/courseDisplay/indexV2.html b/src/main/resources/views/platform/zhgh/Learning/courseDisplay/indexV2.html new file mode 100644 index 00000000..d334ba62 --- /dev/null +++ b/src/main/resources/views/platform/zhgh/Learning/courseDisplay/indexV2.html @@ -0,0 +1,213 @@ + + + +
+
+ + +
+
+ 共 {{pageForm.totalCount || 0}} 门课程 +
+ 排序: + + + + +
+
+ +
+
+
+ +
{{course.courseTypeName || '在线课程'}}
+
+
+
+ {{course.courseName}} + {{course.courseTypeName || '未分类'}} + {{firstRecommendName(course.recommendFlags)}} +
+
{{course.courseIntro || '暂无课程介绍'}}
+
+ {{course.resourceCount || 0}} 课节 + {{formatDuration(course.totalDurationSeconds)}} + {{course.lecturerName || '未设置讲师'}} + {{course.viewCount || 0}} +
+
+
+ 开始学习 +
+
+
+
暂无相关课程
+ + + +
+
+
+ + + diff --git a/src/main/resources/views/platform/zhgh/Learning/courseDisplay/studyV2.html b/src/main/resources/views/platform/zhgh/Learning/courseDisplay/studyV2.html new file mode 100644 index 00000000..df798e3e --- /dev/null +++ b/src/main/resources/views/platform/zhgh/Learning/courseDisplay/studyV2.html @@ -0,0 +1,251 @@ + + + +
+
+ +
+ + + diff --git a/src/main/resources/views/platform/zhgh/Learning/courseManage/index.html b/src/main/resources/views/platform/zhgh/Learning/courseManage/index.html index e2bb1193..cb91bc47 100644 --- a/src/main/resources/views/platform/zhgh/Learning/courseManage/index.html +++ b/src/main/resources/views/platform/zhgh/Learning/courseManage/index.html @@ -85,6 +85,7 @@ layout("/layouts/platform.html"){
+