From b23d3bfa798c40a50710bdfbd36a3c2c1e546776 Mon Sep 17 00:00:00 2001 From: zhangrui Date: Fri, 12 Jun 2026 15:30:50 +0800 Subject: [PATCH] 1 --- .../sys/controller/SysH5IndexController.java | 33 +- .../app/sys/controller/SysHomeController.java | 24 + .../app/sys/controller/SysUserController.java | 6 +- .../controller/v4/SysV4AppsController.java | 12 +- .../controller/HonorManageController.java | 43 + .../impl/TourUserAssignmentServiceImpl.java | 25 +- .../LearningChapterContentController.java | 383 ++++++++ .../LearningCourseDisplayController.java | 298 ++++++ .../LearningCourseManageController.java | 232 +++++ .../LearningCourseTypeController.java | 110 +++ .../LearningPlatformController.java | 47 + .../LearningStatisticsController.java | 330 +++++++ .../LearningStudyRecordController.java | 494 ++++++++++ .../zhgh/learning/models/LearningCourse.java | 117 +++ .../models/LearningCourseOutline.java | 75 ++ .../learning/models/LearningCourseType.java | 45 + .../models/LearningOutlineResource.java | 85 ++ .../learning/models/LearningStudyRecord.java | 119 +++ .../learning/models/LearningStudyRule.java | 100 ++ .../learning/models/LearningStudySegment.java | 88 ++ .../param/LearningStatisticsPageForm.java | 16 + .../db/learning_chapter_content_create.sql | 72 ++ .../resources/db/learning_course_create.sql | 27 + .../db/learning_course_type_add_remark.sql | 2 + src/main/resources/db/learning_dict_init.sql | 132 +++ ...earning_study_record_add_last_position.sql | 2 + .../db/learning_study_record_create.sql | 52 ++ .../resources/db/menu/init_learning_menu.sql | 105 +++ .../views/layouts/v4/baseLayout.html | 108 ++- src/main/resources/views/layouts/v4/home.html | 7 +- .../resources/views/layouts/v4/home/act.js | 133 +-- .../resources/views/layouts/v4/home/entry.js | 212 ++--- .../views/layouts/v4/home/index.html | 94 +- .../resources/views/layouts/v4/home/jcdt.js | 86 +- .../resources/views/layouts/v4/home/stats.js | 151 +-- .../resources/views/layouts/v4/home/user.js | 44 +- src/main/resources/views/layouts/v4/msg.html | 10 +- .../zhgh/Learning/activityManage/index.html | 9 + .../zhgh/Learning/chapterContent/index.html | 883 ++++++++++++++++++ .../zhgh/Learning/courseDisplay/detail.html | 12 + .../zhgh/Learning/courseDisplay/index.html | 350 +++++++ .../zhgh/Learning/courseDisplay/study.html | 755 +++++++++++++++ .../zhgh/Learning/courseManage/index.html | 396 ++++++++ .../zhgh/Learning/courseType/index.html | 189 ++++ .../zhgh/Learning/myRecord/index.html | 180 ++++ .../zhgh/Learning/statistics/index.html | 167 ++++ .../dayofficework/honor/manage/index.html | 494 ++++++++++ .../dayofficework/tour/signup/apply.html | 1 + .../dayofficework/tour/signup/list.html | 182 ++-- .../zhghh5/learning/course/index.html | 612 ++++++++++++ .../zhghh5/learning/course/study.html | 732 +++++++++++++++ .../zhghh5/learning/myRecord/index.html | 371 ++++++++ .../views/platform/zhghh5/sys/home/apps.js | 239 ++++- .../zhghh5/sys/home/featuredActivity.html | 303 ++++++ .../zhghh5/sys/home/festivalBenefit.html | 303 ++++++ .../views/platform/zhghh5/sys/home/home.js | 830 +++++++++++++++- .../views/platform/zhghh5/sys/home/index.html | 20 +- .../views/platform/zhghh5/sys/home/todo.js | 2 +- 58 files changed, 10475 insertions(+), 474 deletions(-) create mode 100644 src/main/java/com/budwk/app/zhgh/learning/controller/LearningChapterContentController.java create mode 100644 src/main/java/com/budwk/app/zhgh/learning/controller/LearningCourseDisplayController.java create mode 100644 src/main/java/com/budwk/app/zhgh/learning/controller/LearningCourseManageController.java create mode 100644 src/main/java/com/budwk/app/zhgh/learning/controller/LearningCourseTypeController.java create mode 100644 src/main/java/com/budwk/app/zhgh/learning/controller/LearningPlatformController.java create mode 100644 src/main/java/com/budwk/app/zhgh/learning/controller/LearningStatisticsController.java create mode 100644 src/main/java/com/budwk/app/zhgh/learning/controller/LearningStudyRecordController.java create mode 100644 src/main/java/com/budwk/app/zhgh/learning/models/LearningCourse.java create mode 100644 src/main/java/com/budwk/app/zhgh/learning/models/LearningCourseOutline.java create mode 100644 src/main/java/com/budwk/app/zhgh/learning/models/LearningCourseType.java create mode 100644 src/main/java/com/budwk/app/zhgh/learning/models/LearningOutlineResource.java create mode 100644 src/main/java/com/budwk/app/zhgh/learning/models/LearningStudyRecord.java create mode 100644 src/main/java/com/budwk/app/zhgh/learning/models/LearningStudyRule.java create mode 100644 src/main/java/com/budwk/app/zhgh/learning/models/LearningStudySegment.java create mode 100644 src/main/java/com/budwk/app/zhgh/learning/param/LearningStatisticsPageForm.java create mode 100644 src/main/resources/db/learning_chapter_content_create.sql create mode 100644 src/main/resources/db/learning_course_create.sql create mode 100644 src/main/resources/db/learning_course_type_add_remark.sql create mode 100644 src/main/resources/db/learning_dict_init.sql create mode 100644 src/main/resources/db/learning_study_record_add_last_position.sql create mode 100644 src/main/resources/db/learning_study_record_create.sql create mode 100644 src/main/resources/db/menu/init_learning_menu.sql create mode 100644 src/main/resources/views/platform/zhgh/Learning/activityManage/index.html create mode 100644 src/main/resources/views/platform/zhgh/Learning/chapterContent/index.html create mode 100644 src/main/resources/views/platform/zhgh/Learning/courseDisplay/detail.html create mode 100644 src/main/resources/views/platform/zhgh/Learning/courseDisplay/index.html create mode 100644 src/main/resources/views/platform/zhgh/Learning/courseDisplay/study.html create mode 100644 src/main/resources/views/platform/zhgh/Learning/courseManage/index.html create mode 100644 src/main/resources/views/platform/zhgh/Learning/courseType/index.html create mode 100644 src/main/resources/views/platform/zhgh/Learning/myRecord/index.html create mode 100644 src/main/resources/views/platform/zhgh/Learning/statistics/index.html create mode 100644 src/main/resources/views/platform/zhghh5/dayofficework/honor/manage/index.html create mode 100644 src/main/resources/views/platform/zhghh5/learning/course/index.html create mode 100644 src/main/resources/views/platform/zhghh5/learning/course/study.html create mode 100644 src/main/resources/views/platform/zhghh5/learning/myRecord/index.html create mode 100644 src/main/resources/views/platform/zhghh5/sys/home/featuredActivity.html create mode 100644 src/main/resources/views/platform/zhghh5/sys/home/festivalBenefit.html diff --git a/src/main/java/com/budwk/app/sys/controller/SysH5IndexController.java b/src/main/java/com/budwk/app/sys/controller/SysH5IndexController.java index 4cae5a10..c3a294fd 100644 --- a/src/main/java/com/budwk/app/sys/controller/SysH5IndexController.java +++ b/src/main/java/com/budwk/app/sys/controller/SysH5IndexController.java @@ -6,7 +6,7 @@ import cn.hutool.core.util.StrUtil; import com.budwk.app.base.result.Result; import com.budwk.app.sys.models.Sys_home_activity; import com.budwk.app.web.commons.auth.utils.SecurityUtil; -import com.budwk.app.zhgh.activity.basic.models.ActivityUserScope; +import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService; import io.swagger.annotations.Api; import org.nutz.dao.Cnd; import org.nutz.dao.Dao; @@ -32,6 +32,8 @@ public class SysH5IndexController { @Inject private Dao dao; + @Inject + private ActivityBasicScopeService activityBasicScopeService; @At @Ok("beetl:/platform/zhghh5/sys/home/index.html") @@ -40,6 +42,26 @@ public class SysH5IndexController { } + /** + * Render the mobile featured activity aggregation page. + */ + @At + @Ok("beetl:/platform/zhghh5/sys/home/featuredActivity.html") + @SaCheckLogin + public void featuredActivity() { + + } + + /** + * Render the mobile festival benefit aggregation page. + */ + @At + @Ok("beetl:/platform/zhghh5/sys/home/festivalBenefit.html") + @SaCheckLogin + public void festivalBenefit() { + + } + @At @Ok("beetl:/platform/zhghh5/index/mine/index.html") @SaCheckLogin @@ -88,8 +110,8 @@ public class SysH5IndexController { } if (allowUserGroupId != null) { - int count = dao.count(ActivityUserScope.class, Cnd.where("groupId", "=", allowUserGroupId).and("userId", "=", userId)); - if (count > 0) { + // Delegate group membership checks to the service so SQL based groups and stored groups stay consistent. + if (activityBasicScopeService.isUserInGroup(allowUserGroupId, userId)) { allowActivityList.add(activity); continue; } @@ -97,7 +119,10 @@ public class SysH5IndexController { if (StrUtil.isNotBlank(allowUserSql)){ Sql sql = Sqls.create(allowUserSql).setParam("userId", userId); - NutMap nutMap = (NutMap) Daos.query(dao, sql.toString(), Sqls.callback.map()); + // Execute the configured visibility SQL with Nutz parameters to avoid rebuilding raw SQL text in controller logic. + sql.setCallback(Sqls.callback.map()); + dao.execute(sql); + NutMap nutMap = (NutMap) sql.getResult(); if (Lang.isNotEmpty(nutMap) && StrUtil.isNotBlank(nutMap.getString("userId"))) { allowActivityList.add(activity); } 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 7e7bba02..60d71311 100644 --- a/src/main/java/com/budwk/app/sys/controller/SysHomeController.java +++ b/src/main/java/com/budwk/app/sys/controller/SysHomeController.java @@ -6,11 +6,14 @@ import cn.hutool.core.util.StrUtil; import cn.hutool.http.useragent.UserAgent; import cn.hutool.http.useragent.UserAgentUtil; import cn.hutool.json.JSONObject; +import com.budwk.app.base.constant.RoleConstant; import com.budwk.app.base.result.Result; import com.budwk.app.sys.models.Sys_home_activity; +import com.budwk.app.sys.models.Sys_home_template; import com.budwk.app.sys.models.Sys_menu; import com.budwk.app.sys.services.SysMenuService; import com.budwk.app.sys.services.SysUserService; +import com.budwk.app.web.commons.auth.utils.AuthUtil; import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.zhgh.activity.basic.models.ActivityUserScope; import io.swagger.annotations.ApiOperation; @@ -39,6 +42,7 @@ import org.nutz.mvc.annotation.Param; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; @@ -255,6 +259,26 @@ public class SysHomeController { // return Result.success(hasPermisiionList); } + /** + * 查询 PC 首页工作模板。 + * 仅系统管理员和校工会管理员可见,返回前隐藏模板 SQL 和后端类路径等敏感字段。 + * + * @return 首页工作模板列表 + */ + @At + @SaCheckLogin + @ApiOperation("首页工作模板") + @Ok("json") + public Result listHomeTemplate() { + if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) { + return Result.success(Collections.emptyList()); + } + FieldFilter fieldFilter = FieldFilter.locked(Sys_home_template.class, "allowUserSql|classPath"); + List list = Daos.ext(dao, fieldFilter).query(Sys_home_template.class, + Cnd.where("enable", "=", 1).asc("sortNo")); + return Result.success(list); + } + @At @SaCheckLogin diff --git a/src/main/java/com/budwk/app/sys/controller/SysUserController.java b/src/main/java/com/budwk/app/sys/controller/SysUserController.java index 48df3f1a..607a58c2 100644 --- a/src/main/java/com/budwk/app/sys/controller/SysUserController.java +++ b/src/main/java/com/budwk/app/sys/controller/SysUserController.java @@ -338,8 +338,12 @@ public class SysUserController { @At @Ok("json") @SaCheckLogin - public Result subAppMenus(@Param("appId") String appId) { + public Result subAppMenus(@Param("appId") String appId, @Param("platform") String platform) { List menus = sysUserService.getMenus(SecurityUtil.getUserId()); + if (StrUtil.isNotBlank(platform)) { + // Keep sub-application menus aligned with the requested client platform. + menus = menus.stream().filter(menu -> platform.equals(menu.getPlatform())).toList(); + } List list = SysMenuUtil.createTreeMenus(menus, appId); if (ObjectUtil.isEmpty(list)) { List self = menus.stream().filter(menu -> menu.getId().equals(appId)).toList(); diff --git a/src/main/java/com/budwk/app/sys/controller/v4/SysV4AppsController.java b/src/main/java/com/budwk/app/sys/controller/v4/SysV4AppsController.java index 61e213d1..a47624f6 100644 --- a/src/main/java/com/budwk/app/sys/controller/v4/SysV4AppsController.java +++ b/src/main/java/com/budwk/app/sys/controller/v4/SysV4AppsController.java @@ -75,6 +75,7 @@ public class SysV4AppsController { m.href, m.icon, m.picIcon, + m.location, sm.`name` AS moduleName, CASE WHEN f.userId IS NOT NULL THEN @@ -97,6 +98,7 @@ public class SysV4AppsController { cnd.and("m.id","in", menus.stream().map(Sys_menu::getId).toArray()); cnd.andEX("m.moduleId", "=", categoryId); cnd.asc("m.location"); + cnd.asc("m.id"); if (StrUtil.isNotBlank(keyword)) { cnd.where().andLike("m.name", keyword); } @@ -143,7 +145,7 @@ public class SysV4AppsController { @Ok("json:full") @SaCheckLogin @ApiOperation("获取收藏的应用") - public Result favorite(HttpServletRequest req) { + public Result favorite(@Param(value = "platform", df = "PC") String platform, HttpServletRequest req) { Sql sql = Sqls.create(""" SELECT m.id, @@ -151,6 +153,7 @@ public class SysV4AppsController { m.href, m.icon, m.picIcon, + m.location, sm.`name` AS moduleName, 1 AS isFavorite FROM @@ -162,12 +165,15 @@ public class SysV4AppsController { LEFT JOIN sys_module sm ON sm.id = m.moduleId WHERE f.userId = @userId - AND m.platform = 'PC' + AND m.platform = @platform AND m.disabled = 0 ORDER BY - m.location ASC; + m.location ASC, + m.id ASC; """); sql.setParam("userId", SecurityUtil.getUserId()); + // The H5 home sends platform=H5; defaulting to PC keeps the existing desktop caller compatible. + sql.setParam("platform", platform); List list = sysMenuService.listMap(sql); return Result.success(list); } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/honor/controller/HonorManageController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/honor/controller/HonorManageController.java index 418ea06a..e46f30c2 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/honor/controller/HonorManageController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/honor/controller/HonorManageController.java @@ -55,6 +55,49 @@ public class HonorManageController { } + /** + * Render the mobile honor display page used by the H5 home feature card. + */ + @At("/h5") + @SaCheckLogin + @Ok("beetl:/platform/zhghh5/dayofficework/honor/manage/index.html") + public void h5() { + + } + + /** + * Query honor records for the mobile honor display page. + */ + @At + @SaCheckLogin + public Result h5Data(HonorPageForm pageForm) { + Sql sql = Sqls.create(""" + SELECT + h.id, + h.userName, + h.applyUnionName, + h.unionName, + h.grantDate, + h.files AS photoFiles, + prize.`name` AS prizeName, + type.`name` AS typeName, + type.queryTypeCode AS typeQueryTypeCode + FROM + `honor` h + LEFT JOIN honor_basic_settings prize ON prize.id = h.prize + LEFT JOIN honor_basic_settings type ON type.id = h.honorType + $condition + """); + Cnd cnd = Cnd.NEW(); + Integer year = pageForm.getYear() == null ? Calendar.getInstance().get(Calendar.YEAR) : pageForm.getYear(); + cnd.andEX("YEAR(h.grantDate)", "=", year); + cnd.andEX("h.honorType", "=", pageForm.getHonorType()); + cnd.desc("h.grantDate"); + sql.setCondition(cnd); + Pagination pagination = honorViService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql); + return Result.success(pagination); + } + @At @SaCheckPermission("honor.manage") public Result pageData(HonorPageForm pageForm) { diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourUserAssignmentServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourUserAssignmentServiceImpl.java index 1fb4d32b..0037b26c 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourUserAssignmentServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourUserAssignmentServiceImpl.java @@ -642,8 +642,10 @@ public class TourUserAssignmentServiceImpl extends BaseServiceImpl 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..7c54cc22 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/learning/controller/LearningCourseDisplayController.java @@ -0,0 +1,298 @@ +package com.budwk.app.zhgh.learning.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.annotation.SaMode; +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(value = {"learning.course.display", "h5.learning.course.display"}, mode = SaMode.OR) + public void index() { + } + + @At("/detail") + @Ok("beetl:/platform/zhgh/Learning/courseDisplay/detail.html") + @SaCheckPermission(value = {"learning.course.display", "h5.learning.course.display"}, mode = SaMode.OR) + public void detail() { + } + + @At("/study") + @Ok("beetl:/platform/zhgh/Learning/courseDisplay/study.html") + @SaCheckPermission(value = {"learning.course.display", "h5.learning.course.display"}, mode = SaMode.OR) + public void study() { + } + + @At + @ApiOperation("课程展示列表") + @SaCheckPermission(value = {"learning.course.display", "h5.learning.course.display"}, mode = SaMode.OR) + 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(value = {"learning.course.display", "h5.learning.course.display"}, mode = SaMode.OR) + 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(value = {"learning.course.display", "h5.learning.course.display"}, mode = SaMode.OR) + 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(value = {"learning.course.display", "h5.learning.course.display"}, mode = SaMode.OR) + 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(value = {"learning.course.display", "h5.learning.course.display"}, mode = SaMode.OR) + public Result courseTypes() { + return Result.success(dao.query(LearningCourseType.class, Cnd.where("enabled", "=", true).asc("sortNum").asc("createdAt"))); + } + + @At + @ApiOperation("推荐标识") + @SaCheckPermission(value = {"learning.course.display", "h5.learning.course.display"}, mode = SaMode.OR) + 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..ddbebc03 --- /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("h5.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("h5.learning.course.display") + public void courseH5() { + } + + @At("/course/h5/study") + @Ok("beetl:/platform/zhghh5/learning/course/study.html") + @SaCheckPermission("h5.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..43a8164c --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/learning/controller/LearningStudyRecordController.java @@ -0,0 +1,494 @@ +package com.budwk.app.zhgh.learning.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.annotation.SaMode; +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(value = {"learning.my.record", "h5.learning.my.record"}, mode = SaMode.OR) + 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(value = {"learning.my.record", "h5.learning.my.record"}, mode = SaMode.OR) + 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(value = {"learning.my.record", "h5.learning.my.record"}, mode = SaMode.OR) + 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(value = {"learning.course.display", "h5.learning.course.display"}, mode = SaMode.OR) + 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(value = {"learning.course.display", "h5.learning.course.display"}, mode = SaMode.OR) + 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(value = {"learning.course.display", "h5.learning.course.display"}, mode = SaMode.OR) + 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(value = {"learning.course.display", "h5.learning.course.display"}, mode = SaMode.OR) + 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(value = {"learning.my.record", "h5.learning.my.record"}, mode = SaMode.OR) + public Result courseOptions() { + return Result.success(dao.query(LearningCourse.class, Cnd.NEW().asc("sortNum").desc("createdAt"))); + } + + @At + @ApiOperation("可选分工会") + @SaCheckPermission(value = {"learning.my.record", "h5.learning.my.record"}, mode = SaMode.OR) + 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(value = {"learning.my.record", "h5.learning.my.record"}, mode = SaMode.OR) + 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/db/learning_chapter_content_create.sql b/src/main/resources/db/learning_chapter_content_create.sql new file mode 100644 index 00000000..1cec4e69 --- /dev/null +++ b/src/main/resources/db/learning_chapter_content_create.sql @@ -0,0 +1,72 @@ +CREATE TABLE IF NOT EXISTS learning_course_outline ( + id VARCHAR(32) NOT NULL COMMENT 'ID', + courseId VARCHAR(32) NULL COMMENT '所属课程ID', + courseName VARCHAR(100) NULL COMMENT '所属课程名称', + parentId VARCHAR(32) NULL COMMENT '父节点ID', + nodeType VARCHAR(20) NULL COMMENT '节点类型', + title VARCHAR(100) NULL COMMENT '章/节标题', + subtitle VARCHAR(200) NULL COMMENT '副标题', + description LONGTEXT NULL COMMENT '简介', + sortOrder INT NULL COMMENT '排序值', + required TINYINT(1) NULL COMMENT '是否必学', + status VARCHAR(20) NULL COMMENT '状态', + createdBy VARCHAR(32) NULL COMMENT '创建人', + createdAt BIGINT NULL COMMENT '创建时间', + updatedBy VARCHAR(32) NULL COMMENT '修改人', + updatedAt BIGINT NULL COMMENT '修改时间', + delFlag TINYINT(1) NULL COMMENT '删除标记', + PRIMARY KEY (id), + INDEX idx_learning_outline_course (courseId), + INDEX idx_learning_outline_parent (parentId) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学习教育课程大纲'; + +CREATE TABLE IF NOT EXISTS learning_outline_resource ( + id VARCHAR(32) NOT NULL COMMENT 'ID', + courseId VARCHAR(32) NULL COMMENT '所属课程ID', + outlineId VARCHAR(32) NULL COMMENT '所属章/节ID', + resourceTitle VARCHAR(100) NULL COMMENT '资料标题', + resourceType VARCHAR(20) NULL COMMENT '资料类型', + fileExt VARCHAR(20) NULL COMMENT '文件扩展名', + fileData LONGTEXT NULL COMMENT '附件', + durationSeconds INT NULL COMMENT '视频/音频时长', + sortOrder INT NULL COMMENT '排序值', + required TINYINT(1) NULL COMMENT '是否必学', + allowPreview TINYINT(1) NULL COMMENT '是否允许预览', + allowDownload TINYINT(1) NULL COMMENT '是否允许下载', + status VARCHAR(20) NULL COMMENT '状态', + createdBy VARCHAR(32) NULL COMMENT '创建人', + createdAt BIGINT NULL COMMENT '创建时间', + updatedBy VARCHAR(32) NULL COMMENT '修改人', + updatedAt BIGINT NULL COMMENT '修改时间', + delFlag TINYINT(1) NULL COMMENT '删除标记', + PRIMARY KEY (id), + INDEX idx_learning_resource_course (courseId), + INDEX idx_learning_resource_outline (outlineId) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学习教育大纲资料'; + +CREATE TABLE IF NOT EXISTS learning_study_rule ( + id VARCHAR(32) NOT NULL COMMENT 'ID', + courseId VARCHAR(32) NULL COMMENT '所属课程ID', + targetType VARCHAR(20) NULL COMMENT '规则对象类型', + targetId VARCHAR(32) NULL COMMENT '规则对象ID', + required TINYINT(1) NULL COMMENT '是否必学', + studyMode VARCHAR(20) NULL COMMENT '学习方式', + completionRule VARCHAR(50) NULL COMMENT '完成规则', + completePercent INT NULL COMMENT '完成比例', + minStudySeconds INT NULL COMMENT '最少学习时长', + unlockRule VARCHAR(20) NULL COMMENT '解锁规则', + allowSkip TINYINT(1) NULL COMMENT '是否允许跳过', + allowDrag TINYINT(1) NULL COMMENT '是否允许拖动', + pauseCountTime TINYINT(1) NULL COMMENT '暂停是否计时', + hiddenCountTime TINYINT(1) NULL COMMENT '页面隐藏是否计时', + inactiveCountTime TINYINT(1) NULL COMMENT '长时间无操作是否计时', + status VARCHAR(20) NULL COMMENT '状态', + createdBy VARCHAR(32) NULL COMMENT '创建人', + createdAt BIGINT NULL COMMENT '创建时间', + updatedBy VARCHAR(32) NULL COMMENT '修改人', + updatedAt BIGINT NULL COMMENT '修改时间', + delFlag TINYINT(1) NULL COMMENT '删除标记', + PRIMARY KEY (id), + INDEX idx_learning_rule_target (targetType, targetId), + INDEX idx_learning_rule_course (courseId) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学习教育学习规则'; diff --git a/src/main/resources/db/learning_course_create.sql b/src/main/resources/db/learning_course_create.sql new file mode 100644 index 00000000..c331226f --- /dev/null +++ b/src/main/resources/db/learning_course_create.sql @@ -0,0 +1,27 @@ +CREATE TABLE IF NOT EXISTS learning_course ( + id VARCHAR(32) NOT NULL COMMENT 'ID', + courseName VARCHAR(100) NULL COMMENT '课程名称', + courseTypeId VARCHAR(32) NULL COMMENT '课程类型', + lecturerName VARCHAR(100) NULL COMMENT '授课讲师', + lecturerInfo VARCHAR(500) NULL COMMENT '讲师基本信息', + courseIntro LONGTEXT NULL COMMENT '课程简介', + suitablePeople VARCHAR(500) NULL COMMENT '适合人群', + learningGoal VARCHAR(500) NULL COMMENT '学习目标', + coursePeriod VARCHAR(50) NULL COMMENT '课程周期', + openType VARCHAR(20) NULL COMMENT '开课类型', + startTime DATETIME NULL COMMENT '开始时间', + endTime DATETIME NULL COMMENT '结束时间', + status VARCHAR(20) NULL COMMENT '课程状态', + recommendFlags VARCHAR(500) NULL COMMENT '推荐标识', + courseTags VARCHAR(500) NULL COMMENT '课程标签', + targetType VARCHAR(20) NULL COMMENT '学习对象', + targetOrgText VARCHAR(500) NULL COMMENT '指定组织', + sortNum INT NULL COMMENT '排序编码', + cover VARCHAR(500) NULL COMMENT '课程封面', + createdBy VARCHAR(32) NULL COMMENT '创建人', + createdAt BIGINT NULL COMMENT '创建时间', + updatedBy VARCHAR(32) NULL COMMENT '修改人', + updatedAt BIGINT NULL COMMENT '修改时间', + delFlag TINYINT(1) NULL COMMENT '删除标记', + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学习教育课程'; diff --git a/src/main/resources/db/learning_course_type_add_remark.sql b/src/main/resources/db/learning_course_type_add_remark.sql new file mode 100644 index 00000000..417865cd --- /dev/null +++ b/src/main/resources/db/learning_course_type_add_remark.sql @@ -0,0 +1,2 @@ +ALTER TABLE learning_course_type + ADD COLUMN remark VARCHAR(500) NULL COMMENT '备注'; diff --git a/src/main/resources/db/learning_dict_init.sql b/src/main/resources/db/learning_dict_init.sql new file mode 100644 index 00000000..123e2e05 --- /dev/null +++ b/src/main/resources/db/learning_dict_init.sql @@ -0,0 +1,132 @@ +-- 学习教育移动端字典初始化。 +-- 移动端课程列表通过 /platform/learning/course/display/recommendOptions 读取“学习教育 -> 推荐标识”字典。 + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'a0f47f6a6d734c8b9c6ce5c0dfd81001', + '', + LPAD(IFNULL(MAX(CAST(`path` AS UNSIGNED)), 0) + 1, 4, '0'), + '学习教育', + '学习教育平台字典', + '学习教育', + 0, + 990, + 1, + '', + UNIX_TIMESTAMP(NOW()) * 1000, + '', + UNIX_TIMESTAMP(NOW()) * 1000, + 0 +FROM `sys_dict` +WHERE (`parentId` = '' OR `parentId` IS NULL) + AND CHAR_LENGTH(`path`) = 4 +HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = '学习教育' OR `name` = '学习教育') t); + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'a0f47f6a6d734c8b9c6ce5c0dfd81002', + p.id, + CONCAT(p.`path`, '0001'), + '推荐标识', + '学习教育课程推荐标识', + '推荐标识', + 0, + 1, + 1, + '', + UNIX_TIMESTAMP(NOW()) * 1000, + '', + UNIX_TIMESTAMP(NOW()) * 1000, + 0 +FROM `sys_dict` p +WHERE (p.`code` = '学习教育' OR p.`name` = '学习教育') + AND NOT EXISTS ( + SELECT 1 + FROM (SELECT id FROM `sys_dict` WHERE `parentId` = p.id AND (`code` = '推荐标识' OR `name` = '推荐标识')) t + ); + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'a0f47f6a6d734c8b9c6ce5c0dfd81003', + p.id, + CONCAT(p.`path`, '0001'), + '推荐课程', + '移动端课程列表推荐筛选项', + 'recommend', + 0, + 1, + 0, + '', + UNIX_TIMESTAMP(NOW()) * 1000, + '', + UNIX_TIMESTAMP(NOW()) * 1000, + 0 +FROM `sys_dict` p +JOIN `sys_dict` r ON r.id = p.`parentId` +WHERE (r.`code` = '学习教育' OR r.`name` = '学习教育') + AND (p.`code` = '推荐标识' OR p.`name` = '推荐标识') + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `parentId` = p.id AND `code` = 'recommend') t); + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'a0f47f6a6d734c8b9c6ce5c0dfd81004', + p.id, + CONCAT(p.`path`, '0002'), + '热门课程', + '移动端课程列表热门筛选项', + 'hot', + 0, + 2, + 0, + '', + UNIX_TIMESTAMP(NOW()) * 1000, + '', + UNIX_TIMESTAMP(NOW()) * 1000, + 0 +FROM `sys_dict` p +JOIN `sys_dict` r ON r.id = p.`parentId` +WHERE (r.`code` = '学习教育' OR r.`name` = '学习教育') + AND (p.`code` = '推荐标识' OR p.`name` = '推荐标识') + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `parentId` = p.id AND `code` = 'hot') t); + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'a0f47f6a6d734c8b9c6ce5c0dfd81005', + p.id, + CONCAT(p.`path`, '0003'), + '最新课程', + '移动端课程列表最新筛选项', + 'new', + 0, + 3, + 0, + '', + UNIX_TIMESTAMP(NOW()) * 1000, + '', + UNIX_TIMESTAMP(NOW()) * 1000, + 0 +FROM `sys_dict` p +JOIN `sys_dict` r ON r.id = p.`parentId` +WHERE (r.`code` = '学习教育' OR r.`name` = '学习教育') + AND (p.`code` = '推荐标识' OR p.`name` = '推荐标识') + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `parentId` = p.id AND `code` = 'new') t); + +UPDATE `sys_dict` +SET `hasChildren` = 1 +WHERE `code` IN ('学习教育', '推荐标识') + OR `name` IN ('学习教育', '推荐标识'); diff --git a/src/main/resources/db/learning_study_record_add_last_position.sql b/src/main/resources/db/learning_study_record_add_last_position.sql new file mode 100644 index 00000000..5b7bcd97 --- /dev/null +++ b/src/main/resources/db/learning_study_record_add_last_position.sql @@ -0,0 +1,2 @@ +ALTER TABLE learning_study_record + ADD COLUMN lastPositionSeconds INT NULL COMMENT '最近播放位置(秒)' AFTER studySeconds; diff --git a/src/main/resources/db/learning_study_record_create.sql b/src/main/resources/db/learning_study_record_create.sql new file mode 100644 index 00000000..622f68e6 --- /dev/null +++ b/src/main/resources/db/learning_study_record_create.sql @@ -0,0 +1,52 @@ +CREATE TABLE IF NOT EXISTS learning_study_record ( + id VARCHAR(32) NOT NULL COMMENT 'ID', + userId VARCHAR(32) NULL COMMENT '学习人ID', + loginName VARCHAR(120) NULL COMMENT '工号', + userName VARCHAR(100) NULL COMMENT '姓名', + unionId VARCHAR(32) NULL COMMENT '所属分工会ID', + unionName VARCHAR(100) NULL COMMENT '所属分工会', + courseId VARCHAR(32) NULL COMMENT '课程ID', + courseName VARCHAR(100) NULL COMMENT '课程名称', + outlineId VARCHAR(32) NULL COMMENT '章节ID', + outlineName VARCHAR(100) NULL COMMENT '章节名称', + firstStudyTime DATETIME NULL COMMENT '第一次进入时间', + latestStudyTime DATETIME NULL COMMENT '最近学习时间', + studySeconds INT NULL COMMENT '累计有效学习时长(秒)', + lastPositionSeconds INT NULL COMMENT '最近播放位置(秒)', + requiredSeconds INT NULL COMMENT '要求学习时长(秒)', + progressPercent INT NULL COMMENT '学习进度', + completeStatus VARCHAR(20) NULL COMMENT '完成状态', + completedAt DATETIME NULL COMMENT '完成时间', + createdBy VARCHAR(32) NULL COMMENT '创建人', + createdAt BIGINT NULL COMMENT '创建时间', + updatedBy VARCHAR(32) NULL COMMENT '修改人', + updatedAt BIGINT NULL COMMENT '修改时间', + delFlag TINYINT(1) NULL COMMENT '删除标记', + PRIMARY KEY (id), + UNIQUE KEY idx_learning_record_user_course_outline (userId, courseId, outlineId), + INDEX idx_learning_record_union (unionId), + INDEX idx_learning_record_course (courseId) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学习教育学习记录'; + +CREATE TABLE IF NOT EXISTS learning_study_segment ( + id VARCHAR(32) NOT NULL COMMENT 'ID', + recordId VARCHAR(32) NULL COMMENT '学习记录ID', + userId VARCHAR(32) NULL COMMENT '学习人ID', + courseId VARCHAR(32) NULL COMMENT '课程ID', + outlineId VARCHAR(32) NULL COMMENT '章节ID', + resourceId VARCHAR(32) NULL COMMENT '资源ID', + resourceName VARCHAR(100) NULL COMMENT '资源名称', + startTime DATETIME NULL COMMENT '开始时间', + endTime DATETIME NULL COMMENT '结束时间', + lastHeartbeatTime DATETIME NULL COMMENT '最近心跳时间', + activeSeconds INT NULL COMMENT '本时段有效学习时长(秒)', + state VARCHAR(20) NULL COMMENT '状态', + createdBy VARCHAR(32) NULL COMMENT '创建人', + createdAt BIGINT NULL COMMENT '创建时间', + updatedBy VARCHAR(32) NULL COMMENT '修改人', + updatedAt BIGINT NULL COMMENT '修改时间', + delFlag TINYINT(1) NULL COMMENT '删除标记', + PRIMARY KEY (id), + INDEX idx_learning_segment_record (recordId), + INDEX idx_learning_segment_user_state (userId, state) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学习教育学习时段'; diff --git a/src/main/resources/db/menu/init_learning_menu.sql b/src/main/resources/db/menu/init_learning_menu.sql new file mode 100644 index 00000000..4ee23030 --- /dev/null +++ b/src/main/resources/db/menu/init_learning_menu.sql @@ -0,0 +1,105 @@ +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT + 'a0f47f6a6d734c8b9c6ce5c0dfd80001', + '', + LPAD(IFNULL(MAX(CAST(path AS UNSIGNED)), 0) + 1, 4, '0'), + '学习教育平台', + 'Learning', + 'menu', + '', + '', + 'ti-book', + 1, + 0, + 'learning', + NULL, + 990, + 1, + '', + UNIX_TIMESTAMP(NOW()) * 1000, + '', + UNIX_TIMESTAMP(NOW()) * 1000, + 0, + 'PC', + NULL, + NULL, + 'x', + 0, + 0 +FROM sys_menu +WHERE (parentId = '' OR parentId IS NULL) + AND CHAR_LENGTH(path) = 4 +HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'learning') t); + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT 'a0f47f6a6d734c8b9c6ce5c0dfd80002', p.id, CONCAT(p.path, '0001'), '课程类型设置', 'Course Type', 'menu', '/platform/learning/course/type', 'data-pjax', '', 1, 0, 'learning.course.type', NULL, 1, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'k', 0, 0 +FROM sys_menu p +WHERE p.permission = 'learning' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'learning.course.type') t); + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT 'a0f47f6a6d734c8b9c6ce5c0dfd80003', p.id, CONCAT(p.path, '0002'), '课程管理', 'Course Manage', 'menu', '/platform/learning/course/manage', 'data-pjax', '', 1, 0, 'learning.course.manage', NULL, 2, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'k', 0, 0 +FROM sys_menu p +WHERE p.permission = 'learning' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'learning.course.manage') t); + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT 'a0f47f6a6d734c8b9c6ce5c0dfd80004', p.id, CONCAT(p.path, '0003'), '章节内容管理', 'Chapter Content', 'menu', '/platform/learning/chapter/content', 'data-pjax', '', 1, 0, 'learning.chapter.content', NULL, 3, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'z', 0, 0 +FROM sys_menu p +WHERE p.permission = 'learning' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'learning.chapter.content') t); + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT 'a0f47f6a6d734c8b9c6ce5c0dfd80005', p.id, CONCAT(p.path, '0004'), '课程展示', 'Course Display', 'menu', '/platform/learning/course/display', 'data-pjax', '', 1, 0, 'learning.course.display', NULL, 4, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'k', 0, 0 +FROM sys_menu p +WHERE p.permission = 'learning' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'learning.course.display') t); + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT 'a0f47f6a6d734c8b9c6ce5c0dfd80006', p.id, CONCAT(p.path, '0005'), '学习活动管理', 'Activity Manage', 'menu', '/platform/learning/activity/manage', 'data-pjax', '', 1, 0, 'learning.activity.manage', NULL, 5, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'x', 0, 0 +FROM sys_menu p +WHERE p.permission = 'learning' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'learning.activity.manage') t); + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT 'a0f47f6a6d734c8b9c6ce5c0dfd80007', p.id, CONCAT(p.path, '0006'), '学习统计', 'Learning Statistics', 'menu', '/platform/learning/statistics', 'data-pjax', '', 1, 0, 'learning.statistics', NULL, 6, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'x', 0, 0 +FROM sys_menu p +WHERE p.permission = 'learning' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'learning.statistics') t); + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT 'a0f47f6a6d734c8b9c6ce5c0dfd80008', p.id, CONCAT(p.path, '0007'), '我的学习记录', 'My Learning Record', 'menu', '/platform/learning/my/record', 'data-pjax', '', 1, 0, 'learning.my.record', NULL, 7, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'w', 0, 0 +FROM sys_menu p +WHERE p.permission = 'learning' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'learning.my.record') t); + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT 'a0f47f6a6d734c8b9c6ce5c0dfd80009', p.id, CONCAT(p.path, '0008'), '我的课堂', 'My Course', 'menu', '/platform/learning/course/h5', 'data-pjax', '', 1, 0, 'h5.learning.course.display', NULL, 8, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'H5', NULL, NULL, 'w', 0, 0 +FROM sys_menu p +WHERE p.permission = 'learning' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'h5.learning.course.display') t); + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT 'a0f47f6a6d734c8b9c6ce5c0dfd80010', p.id, CONCAT(p.path, '0009'), '我的学习记录', 'My Learning Record H5', 'menu', '/platform/learning/my/record/h5', 'data-pjax', '', 1, 0, 'h5.learning.my.record', NULL, 9, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'H5', NULL, NULL, 'w', 0, 0 +FROM sys_menu p +WHERE p.permission = 'learning' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'h5.learning.my.record') t); + +INSERT INTO sys_role_menu (roleId, menuId) +SELECT r.id, m.id +FROM sys_role r +JOIN sys_menu m ON m.permission IN ( + 'learning', + 'learning.course.type', + 'learning.course.manage', + 'learning.chapter.content', + 'learning.course.display', + 'learning.activity.manage', + 'learning.statistics', + 'learning.my.record', + 'h5.learning.course.display', + 'h5.learning.my.record' +) +LEFT JOIN sys_role_menu rm ON rm.roleId = r.id AND rm.menuId = m.id +WHERE r.code = 'SYSADMIN' + AND rm.roleId IS NULL; diff --git a/src/main/resources/views/layouts/v4/baseLayout.html b/src/main/resources/views/layouts/v4/baseLayout.html index 8c5ac578..63e55724 100644 --- a/src/main/resources/views/layouts/v4/baseLayout.html +++ b/src/main/resources/views/layouts/v4/baseLayout.html @@ -71,7 +71,7 @@ - l + @@ -80,11 +80,12 @@ - + + @@ -100,13 +101,32 @@ - - - - - - - - + diff --git a/src/main/resources/views/platform/zhgh/Learning/courseDisplay/detail.html b/src/main/resources/views/platform/zhgh/Learning/courseDisplay/detail.html new file mode 100644 index 00000000..a4b8ef21 --- /dev/null +++ b/src/main/resources/views/platform/zhgh/Learning/courseDisplay/detail.html @@ -0,0 +1,12 @@ + +
+ +
课程介绍
+ +
+
+ diff --git a/src/main/resources/views/platform/zhgh/Learning/courseDisplay/index.html b/src/main/resources/views/platform/zhgh/Learning/courseDisplay/index.html new file mode 100644 index 00000000..91b05cae --- /dev/null +++ b/src/main/resources/views/platform/zhgh/Learning/courseDisplay/index.html @@ -0,0 +1,350 @@ + + + +
+
+
+
推荐标识
+
+ + +
+
+
+
课程类型
+
+ + +
+
+
+ +
+
为您找到相关课程{{pageForm.totalCount || 0}}
+ + + +
+ +
+
+
+ +
{{course.courseTypeName || '课程'}}
+
{{firstRecommendName(course.recommendFlags)}}
+
+
+
+ {{course.courseName}} + + {{getRecommendName(code)}} + +
+
+ {{course.courseTypeName || '未设置课程类型'}} + {{course.lecturerName || '未设置讲师'}} +
+
{{course.courseIntro || '暂无课程介绍'}}
+
+ + 长期开放 + {{course.startTimeText || '-'}} 至 {{course.endTimeText || '-'}} +
+
+
+
+
暂无相关课程
+ + + +
+ + + diff --git a/src/main/resources/views/platform/zhgh/Learning/courseDisplay/study.html b/src/main/resources/views/platform/zhgh/Learning/courseDisplay/study.html new file mode 100644 index 00000000..3717a545 --- /dev/null +++ b/src/main/resources/views/platform/zhgh/Learning/courseDisplay/study.html @@ -0,0 +1,755 @@ + + + +
+
+ 返回 + +
课程图片
+
+
+ {{course.courseName || '课程学习'}} + {{getRecommendName(code)}} +
+
+ 授课讲师:{{course.lecturerName || '未设置'}} + {{course.openType === 'long_term' ? '长期开放' : ((course.startTimeText || '-') + ' 至 ' + (course.endTimeText || '-'))}} + 课程类型:{{course.courseTypeName || '未设置'}} +
+
+
+ +
+
+
+ 课程安排 + 收起 +
+
+ + + + {{data.title}} + 必学 + + +
+
+ +
+
+
{{selectedResource.title || '请选择课程资料'}}
+
+ + + 学习中 {{recordStudyTime}} + 开始学习 + 结束学习 + 打开原文件 +
+
+
+ + +
+
+
+
+ + + diff --git a/src/main/resources/views/platform/zhgh/Learning/courseManage/index.html b/src/main/resources/views/platform/zhgh/Learning/courseManage/index.html new file mode 100644 index 00000000..9b15cbf1 --- /dev/null +++ b/src/main/resources/views/platform/zhgh/Learning/courseManage/index.html @@ -0,0 +1,396 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 搜索 + 重置 +
+
+
+ + + + + + 新增课程 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 固定开课时间 + 长期开放 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
只能上传1个文件;只能上传.jpg,.jpeg,.png文件;单个文件大小不能超过5M。
+
+
+ + 取消 + 确定 + +
+
+ + + diff --git a/src/main/resources/views/platform/zhgh/Learning/courseType/index.html b/src/main/resources/views/platform/zhgh/Learning/courseType/index.html new file mode 100644 index 00000000..92d764a2 --- /dev/null +++ b/src/main/resources/views/platform/zhgh/Learning/courseType/index.html @@ -0,0 +1,189 @@ + +
+ + + + + + + +
+ 搜索 + 重置 +
+
+
+ + + + + + 新建 + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + 取消 + 确定 + + +
+ + + diff --git a/src/main/resources/views/platform/zhgh/Learning/myRecord/index.html b/src/main/resources/views/platform/zhgh/Learning/myRecord/index.html new file mode 100644 index 00000000..8a415f15 --- /dev/null +++ b/src/main/resources/views/platform/zhgh/Learning/myRecord/index.html @@ -0,0 +1,180 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ 搜索 + 重置 +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + diff --git a/src/main/resources/views/platform/zhgh/Learning/statistics/index.html b/src/main/resources/views/platform/zhgh/Learning/statistics/index.html new file mode 100644 index 00000000..defb6c90 --- /dev/null +++ b/src/main/resources/views/platform/zhgh/Learning/statistics/index.html @@ -0,0 +1,167 @@ + +
+ + + + + + + + + + + + + +
+ 搜索 + 重置 +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + diff --git a/src/main/resources/views/platform/zhghh5/dayofficework/honor/manage/index.html b/src/main/resources/views/platform/zhghh5/dayofficework/honor/manage/index.html new file mode 100644 index 00000000..60ccd846 --- /dev/null +++ b/src/main/resources/views/platform/zhghh5/dayofficework/honor/manage/index.html @@ -0,0 +1,494 @@ + + +
+
+ + +
+ + +
+ +
+
+
+

一份坚守,淬炼出奋斗的生命价值;一份专注,迸发不断创新的发展动力。

+

他们扎根岗位、追求卓越,以实干书写担当,用匠心点亮前行方向。

+

致敬劳模先进,礼赞奋斗精神。

+
+ + 加载中... + +
+
+
+ +
+
{{ getDisplayName(item) }}
+
劳模先进
+
+
{{ getDisplayName(item) }}
+
+
+
+
《{{ item.prizeName || '荣誉奖项' }}》
+
授予时间:{{ item.grantDate }}
+
+
+ + +
+
+
+ + + diff --git a/src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/apply.html b/src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/apply.html index e3b6ebdb..fe308343 100644 --- a/src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/apply.html +++ b/src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/apply.html @@ -129,6 +129,7 @@ layout("/layouts/platform_h5.html"){ + diff --git a/src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/list.html b/src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/list.html index bbf33622..6c023431 100644 --- a/src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/list.html +++ b/src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/list.html @@ -156,7 +156,7 @@ layout("/layouts/platform_h5.html"){ } .tour-line-shortcuts { - display: grid; + display: none; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin-top: 10px; @@ -212,29 +212,54 @@ layout("/layouts/platform_h5.html"){ padding-right: 16px; } - .tour-line-period-entry { - width: 100%; - min-height: 40px; - border: 0; - border-radius: 8px; - padding: 0 12px; - background: #ffffff; - color: #0f172a; + .tour-line-period-list { display: flex; align-items: center; - justify-content: space-between; + gap: 8px; + overflow-x: auto; + overflow-y: hidden; + padding-bottom: 2px; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + -ms-overflow-style: none; + } + + .tour-line-period-list::-webkit-scrollbar { + display: none; + } + + .tour-line-period-chip { + min-width: 78px; + max-width: 168px; + min-height: 34px; + flex: 0 0 auto; + border: 1px solid #dbe3ee; + border-radius: 17px; + padding: 0 12px; + background: #ffffff; + color: #334155; font-size: 14px; - line-height: 1.4; + line-height: 32px; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; box-shadow: 0 2px 10px rgba(15, 23, 42, 0.05); box-sizing: border-box; } - .tour-line-period-entry--active { - color: #0b75bd; + .tour-line-period-chip--active { + border-color: #0b75bd; + background: #0b75bd; + color: #ffffff; + font-weight: 700; } - .tour-line-period-placeholder { - color: #64748b; + .tour-line-period-tip { + margin-top: 6px; + color: #d97706; + font-size: 12px; + line-height: 18px; } .tour-line-list { @@ -465,14 +490,18 @@ layout("/layouts/platform_h5.html"){
- +
+ +
+
可选择指定出行时间,快速查看对应线路。
@@ -485,7 +514,7 @@ layout("/layouts/platform_h5.html"){
- + @@ -556,25 +585,6 @@ layout("/layouts/platform_h5.html"){
- -
-
选择出行时段
-
- - - -
-
-
+ + diff --git a/src/main/resources/views/platform/zhghh5/learning/course/study.html b/src/main/resources/views/platform/zhghh5/learning/course/study.html new file mode 100644 index 00000000..43181e4b --- /dev/null +++ b/src/main/resources/views/platform/zhghh5/learning/course/study.html @@ -0,0 +1,732 @@ + + + + +
+
+
+ +
{{ course.courseName || '课程学习' }}
+
+ +
+
{{ course.courseName || '课程学习' }}
+
+ {{ studyCountText }} + 收藏 + 分享 +
+
+ + + +
+
{{ course.courseIntro || '暂无课程介绍' }}
+
+
+ +
+
+
{{ chapterCount }} 个章节,{{ totalMinutes }} 分钟
+
总学习进度 {{ totalProgress }}%
+
+ +
+
+
+ + {{ chapter.title }} +
+
+
+ +
+
+
{{ resource.title }}
+
{{ statusText(resource) }}
+
+
+
+
+ + +
+
+
+ + +
+
{{ selectedResource.title || '学习资料' }}
+
+ 横屏 + 退出 +
+
+
+ +
+ +
+ + +
+

该资料暂不支持内嵌预览

+ 打开资料 +
+
+
+
+ + + + diff --git a/src/main/resources/views/platform/zhghh5/learning/myRecord/index.html b/src/main/resources/views/platform/zhghh5/learning/myRecord/index.html new file mode 100644 index 00000000..15e90312 --- /dev/null +++ b/src/main/resources/views/platform/zhghh5/learning/myRecord/index.html @@ -0,0 +1,371 @@ + + + + +
+
+
+
+ 累计学习时长 {{ summary.studyHour || 0 }} 小时 {{ summary.studyMinute || 0 }} 分钟 +
+
+
+
+
{{ item.value }}
+
{{ item.label }}
+
+
+
+ +
+
+
AI学助手
+
来问问AI学习助手,获取专属学习计划
+
查看
+
+ +
+
+
我学习的课程
+
全部
+
+
+
+
+ +
{{ course.courseName || '课程' }}
+
+
{{ course.courseName || '未命名课程' }}
+
已学完{{ course.completedOutlineCount || 0 }}讲
+
+
+ +
+ +
+
+
+ +
{{ course.courseName || '课程' }}
+
+
+
{{ course.courseName || '未命名课程' }}
+
学习进度 {{ course.progressPercent || 0 }}% · {{ course.studyTimeText || '0秒' }}
+
+
+
+
+
+ + + + diff --git a/src/main/resources/views/platform/zhghh5/sys/home/apps.js b/src/main/resources/views/platform/zhghh5/sys/home/apps.js index b482b25e..c2dbf74d 100644 --- a/src/main/resources/views/platform/zhghh5/sys/home/apps.js +++ b/src/main/resources/views/platform/zhghh5/sys/home/apps.js @@ -35,9 +35,12 @@ const apps = {
- +
+ 加载中... +
+
+ v-else-if="isFixedCategory && applications.length > 0">
-
-
{{ app.name }}
-
+ +
+
+
{{ section.name }}
+
+
+ +
+ +
+
{{ menu.name }}
+
+
+
+
+ - +
@@ -93,6 +114,7 @@ const apps = { return { // 应用列表 applications: [], + moduleSections: [], // 固定分类列表 categories: [ {id: "all", name: "全部应用", icon: "fa fa-th-large"}, @@ -111,6 +133,7 @@ const apps = { showMenuPopup: false, currentApp: {}, currentMenus: [], + requestSeq: 0, touchTimer: null, isTouchMoved: false, @@ -124,6 +147,9 @@ const apps = { // 当前选中的分类ID currentCategoryId() { return this.allCategories[this.activeTab]?.id || "all" + }, + isFixedCategory() { + return ["all", "favorites", "recommended"].includes(this.currentCategoryId) } }, mounted() { @@ -145,7 +171,9 @@ const apps = { // 加载应用数据 loadApps() { + const requestSeq = ++this.requestSeq this.loading = true + this.moduleSections = [] // 构建请求参数 const params = { @@ -172,12 +200,14 @@ const apps = { // 如果是"推荐应用"分类 if (this.currentCategoryId === "recommended") { this.$axios.post("/platform/home/listRecommendApp", { platform: "H5" }).then((result) => { + if (requestSeq !== this.requestSeq) return if (result.code === 0) { this.applications = result.data || [] + this.finishLoadApps(requestSeq) } else { console.error("获取推荐应用失败:", result.msg) + this.loading = false } - this.loading = false }) return } @@ -185,28 +215,105 @@ const apps = { // 发送请求获取普通应用列表 this.$axios.post("/platform/v4/apps/list?" + queryString) .then((result) => { + if (requestSeq !== this.requestSeq) return if (result.code === 0) { this.applications = result.data || [] + this.finishLoadApps(requestSeq) } else { console.error("获取应用列表失败:", result.msg) + this.loading = false } - this.loading = false }) }, // 加载收藏的应用 loadFavorites() { - this.$axios.post("/platform/v4/apps/favorite") + const requestSeq = this.requestSeq + this.$axios.post("/platform/v4/apps/favorite", {platform: 'H5'}) .then((result) => { + if (requestSeq !== this.requestSeq) return if (result.code === 0) { this.applications = result.data || [] + this.finishLoadApps(requestSeq) } else { console.error("获取收藏应用失败:", result.msg) + this.loading = false } - this.loading = false }) }, + finishLoadApps(requestSeq) { + if (requestSeq !== this.requestSeq) return + if (this.isFixedCategory) { + this.moduleSections = [] + this.loading = false + return + } + this.loadModuleSections(this.applications, requestSeq) + }, + + // 加载每个模块下的功能菜单 + loadModuleSections(apps, requestSeq) { + const list = apps || [] + if (!list.length) { + this.moduleSections = [] + this.loading = false + return + } + Promise.all(list.map(app => { + return this.$axios.post("/platform/sys/user/subAppMenus", {appId: app.id, platform: "H5"}).then((res) => { + const menus = res.code === 0 ? (res.data || []) : [] + return { + id: app.id, + name: app.name, + app: app, + menus: this.resolveModuleMenus(app, menus) + } + }).catch(() => { + return { + id: app.id, + name: app.name, + app: app, + menus: this.resolveModuleMenus(app, []) + } + }) + })).then(sections => { + if (requestSeq !== this.requestSeq) return + this.moduleSections = sections.filter(section => section.menus.length > 0) + this.loading = false + }) + }, + + resolveModuleMenus(app, menus) { + const items = this.flattenMenus(menus || []) + if (items.length > 0) { + return items + } + if (app && app.href) { + return [{ + id: app.id, + name: app.name, + href: app.href, + icon: app.icon, + picIcon: app.picIcon, + aliasName: app.aliasName + }] + } + return [] + }, + + flattenMenus(menus) { + const result = [] + ;(menus || []).forEach(menu => { + if (menu.children && menu.children.length > 0) { + result.push.apply(result, this.flattenMenus(menu.children)) + } else if (menu.href) { + result.push(menu) + } + }) + return result + }, + // 切换应用收藏状态 toggleFavorite(appId) { const app = this.applications.find((a) => a.id === appId) @@ -254,6 +361,7 @@ const apps = { // 搜索 onSearch() { this.applications = [] + this.moduleSections = [] this.loadApps() }, @@ -267,12 +375,13 @@ const apps = { onClickNav(index) { this.activeTab = index this.applications = [] + this.moduleSections = [] this.loadApps() }, // 打开应用 openApp(app) { - this.$axios.post("/platform/sys/user/subAppMenus", {appId: app.id}).then((res) => { + this.$axios.post("/platform/sys/user/subAppMenus", {appId: app.id, platform: "H5"}).then((res) => { if (res.code === 0) { this.showAppMenus(app, res.data) } @@ -321,12 +430,15 @@ const apps = { .apps-container { display: flex; flex-direction: column; - height: 100%; + height: calc(100vh - 50px); + overflow: hidden; } /* 自定义树形选择组件样式 */ .custom-tree-select { display: flex; + min-height: 0; + flex: 1; height: calc(100vh - 150px); background: #fff; } @@ -403,10 +515,109 @@ const apps = { .tree-content { flex: 1; + min-width: 0; overflow-y: auto; + -webkit-overflow-scrolling: touch; background: #fff; } + .module-loading { + min-height: 180px; + display: flex; + align-items: center; + justify-content: center; + color: #8a94a6; + } + + .module-sections { + padding: 12px 12px 18px; + background: #fff; + } + + .module-section { + padding: 0 0 18px; + } + + .module-section + .module-section { + border-top: 1px solid #f1f3f6; + padding-top: 14px; + } + + .module-title { + position: relative; + padding-left: 10px; + margin-bottom: 10px; + color: #202733; + font-size: 15px; + font-weight: 700; + line-height: 1.4; + } + + .module-title::before { + content: ''; + position: absolute; + left: 0; + top: 4px; + width: 3px; + height: 14px; + border-radius: 2px; + background: #1989fa; + } + + .module-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px 8px; + } + + .module-grid-single { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .module-feature { + min-width: 0; + display: flex; + flex-direction: column; + align-items: center; + padding: 8px 2px; + border-radius: 8px; + background: #fff; + } + + .module-feature:active { + background: #f2f7ff; + } + + .module-feature-img { + width: 42px; + height: 42px; + object-fit: contain; + margin-bottom: 6px; + } + + .module-feature-icon { + width: 42px; + height: 42px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 6px; + color: #ffffff; + background: linear-gradient(135deg, #2f8af5 0%, #49c3dc 100%); + } + + .module-feature-name { + width: 100%; + color: #323233; + font-size: 13px; + line-height: 1.25; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .app-grid { display: grid; /* 改为自适应列数:每格最小 80px,自动换行 */ @@ -414,7 +625,7 @@ const apps = { justify-items: center; /* 子项内容居中(图标和文字) */ justify-content: start; /* 整体网格靠左对齐,避免居中 */ - + gap: 12px; padding: 12px; background: #fff; @@ -590,6 +801,6 @@ const apps = { font-size: 16px; color: #c8c9cc; } - + ` } diff --git a/src/main/resources/views/platform/zhghh5/sys/home/featuredActivity.html b/src/main/resources/views/platform/zhghh5/sys/home/featuredActivity.html new file mode 100644 index 00000000..48c17cc6 --- /dev/null +++ b/src/main/resources/views/platform/zhghh5/sys/home/featuredActivity.html @@ -0,0 +1,303 @@ + + +
+ +
+ + + diff --git a/src/main/resources/views/platform/zhghh5/sys/home/festivalBenefit.html b/src/main/resources/views/platform/zhghh5/sys/home/festivalBenefit.html new file mode 100644 index 00000000..f5dd1867 --- /dev/null +++ b/src/main/resources/views/platform/zhghh5/sys/home/festivalBenefit.html @@ -0,0 +1,303 @@ + + +
+
+ + +
+ +
+
+
节日福利
+
查看可参与的节日福利活动
+
+
+ +
+ + +
+
+
+ + + diff --git a/src/main/resources/views/platform/zhghh5/sys/home/home.js b/src/main/resources/views/platform/zhghh5/sys/home/home.js index efc44e25..1b5ddee2 100644 --- a/src/main/resources/views/platform/zhghh5/sys/home/home.js +++ b/src/main/resources/views/platform/zhghh5/sys/home/home.js @@ -1,37 +1,88 @@ const home = { template: /*language=HTML*/ `
- - - - - - - - - - - - - - -
-
推荐服务
- - - - - +
+ +
+ + + +
+ +
+
+
- -
-
最新活动
+ +
+
推荐服务
+
+
+
+
+ + +
+
{{ item.name }}
+
+
+
+
+ + +
+
+
+
{{ item.name }}
+
{{ item.desc }}
+
+ +
+
+ +
+
我的课堂
+
{{ currentClassroomCourseName }}
+
+ 个人学习 + +
+
+ +
+
政策文件
+
+ 更多 + +
+
+ +
+
最新活动