diff --git a/schoolNewsServ/.bin/mysql/sql/initMenuData.sql b/schoolNewsServ/.bin/mysql/sql/initMenuData.sql index 017d903..72c9fed 100644 --- a/schoolNewsServ/.bin/mysql/sql/initMenuData.sql +++ b/schoolNewsServ/.bin/mysql/sql/initMenuData.sql @@ -114,6 +114,7 @@ INSERT INTO `tb_sys_permission` (id, permission_id, name, code, description, mod -- 1100-1199: 文章管理 ('1100', 'perm_admin_article_manage', '文章管理', 'admin:article:manage', '访问文章管理视图权限', 'module_news', '1', now()), +('1101', 'perm_admin_article_force_publish', '文章强制发布', 'admin:article:force-publish', '强制发布文章权限(跳过敏感词校验)', 'module_news', '1', now()), -- 1200-1299: Banner管理 ('1200', 'perm_admin_banner_manage', 'Banner管理', 'admin:banner:manage', '访问Banner管理视图权限', 'module_news', '1', now()), @@ -135,6 +136,7 @@ INSERT INTO `tb_sys_permission` (id, permission_id, name, code, description, mod -- 2200-2299: 课程管理 ('2200', 'perm_admin_course_manage', '课程管理', 'admin:course:manage', '访问课程管理视图权限', 'module_study', '1', now()), +('2201', 'perm_admin_course_force_publish', '课程强制发布', 'admin:course:force-publish', '强制发布课程权限(跳过敏感词校验)', 'module_study', '1', now()), -- 2300-2399: 成就管理 ('2300', 'perm_admin_achievement_manage', '成就管理', 'admin:achievement:manage', '访问成就管理视图权限', 'module_study', '1', now()), @@ -200,6 +202,7 @@ INSERT INTO `tb_sys_role_permission` (id, role_id, permission_id, creator, creat -- 普通管理员权限 (1000-8999) ('29', 'superadmin', 'perm_admin_resource_manage', '1', now()), ('30', 'superadmin', 'perm_admin_article_manage', '1', now()), +('46', 'superadmin', 'perm_admin_article_force_publish', '1', now()), ('31', 'superadmin', 'perm_admin_banner_manage', '1', now()), ('32', 'superadmin', 'perm_admin_tag_manage', '1', now()), ('33', 'superadmin', 'perm_admin_column_manage', '1', now()), @@ -207,6 +210,7 @@ INSERT INTO `tb_sys_role_permission` (id, role_id, permission_id, creator, creat ('35', 'superadmin', 'perm_admin_task_manage', '1', now()), ('36', 'superadmin', 'perm_admin_study_records', '1', now()), ('37', 'superadmin', 'perm_admin_course_manage', '1', now()), +('47', 'superadmin', 'perm_admin_course_force_publish', '1', now()), ('38', 'superadmin', 'perm_admin_achievement_manage', '1', now()), ('39', 'superadmin', 'perm_admin_ai_config', '1', now()), ('40', 'superadmin', 'perm_admin_knowledge_manage', '1', now()), @@ -249,6 +253,7 @@ INSERT INTO `tb_sys_role_permission` (id, role_id, permission_id, creator, creat -- 普通管理员权限 (1000-8999) ('122', 'admin', 'perm_admin_resource_manage', '1', now()), ('123', 'admin', 'perm_admin_article_manage', '1', now()), +('140', 'admin', 'perm_admin_article_force_publish', '1', now()), ('124', 'admin', 'perm_admin_banner_manage', '1', now()), ('125', 'admin', 'perm_admin_tag_manage', '1', now()), ('126', 'admin', 'perm_admin_column_manage', '1', now()), @@ -256,6 +261,7 @@ INSERT INTO `tb_sys_role_permission` (id, role_id, permission_id, creator, creat ('128', 'admin', 'perm_admin_task_manage', '1', now()), ('129', 'admin', 'perm_admin_study_records', '1', now()), ('130', 'admin', 'perm_admin_course_manage', '1', now()), +('141', 'admin', 'perm_admin_course_force_publish', '1', now()), ('131', 'admin', 'perm_admin_achievement_manage', '1', now()), ('132', 'admin', 'perm_admin_ai_config', '1', now()), ('133', 'admin', 'perm_admin_knowledge_manage', '1', now()), diff --git a/schoolNewsServ/.bin/mysql/sql/update_force_publish_permission.sql b/schoolNewsServ/.bin/mysql/sql/update_force_publish_permission.sql new file mode 100644 index 0000000..a4752cc --- /dev/null +++ b/schoolNewsServ/.bin/mysql/sql/update_force_publish_permission.sql @@ -0,0 +1,41 @@ +-- ============================================ +-- 强制发布权限更新脚本 +-- 用于更新现有数据库,添加文章和课程的强制发布权限 +-- 执行时间:2026-01-14 +-- ============================================ + +USE school_news; + +-- 1. 插入新的权限数据 +INSERT INTO `tb_sys_permission` (id, permission_id, name, code, description, module_id, creator, create_time) VALUES +('1101', 'perm_admin_article_force_publish', '文章强制发布', 'admin:article:force-publish', '强制发布文章权限(跳过敏感词校验)', 'module_news', '1', now()), +('2201', 'perm_admin_course_force_publish', '课程强制发布', 'admin:course:force-publish', '强制发布课程权限(跳过敏感词校验)', 'module_study', '1', now()) +ON DUPLICATE KEY UPDATE + name = VALUES(name), + code = VALUES(code), + description = VALUES(description), + update_time = now(); + +-- 2. 为超级管理员角色添加强制发布权限 +INSERT INTO `tb_sys_role_permission` (id, role_id, permission_id, creator, create_time) VALUES +('46', 'superadmin', 'perm_admin_article_force_publish', '1', now()), +('47', 'superadmin', 'perm_admin_course_force_publish', '1', now()) +ON DUPLICATE KEY UPDATE update_time = now(); + +-- 3. 为管理员角色添加强制发布权限 +INSERT INTO `tb_sys_role_permission` (id, role_id, permission_id, creator, create_time) VALUES +('140', 'admin', 'perm_admin_article_force_publish', '1', now()), +('141', 'admin', 'perm_admin_course_force_publish', '1', now()) +ON DUPLICATE KEY UPDATE update_time = now(); + +-- 验证插入结果 +SELECT '=== 新增权限 ===' AS info; +SELECT permission_id, name, code, description FROM tb_sys_permission +WHERE permission_id IN ('perm_admin_article_force_publish', 'perm_admin_course_force_publish'); + +SELECT '=== 角色权限关联 ===' AS info; +SELECT r.name AS role_name, p.name AS permission_name, p.code +FROM tb_sys_role_permission rp +JOIN tb_sys_role r ON r.role_id = rp.role_id +JOIN tb_sys_permission p ON p.permission_id = rp.permission_id +WHERE p.permission_id IN ('perm_admin_article_force_publish', 'perm_admin_course_force_publish'); diff --git a/schoolNewsServ/api/api-study/src/main/java/org/xyzh/api/study/course/CourseService.java b/schoolNewsServ/api/api-study/src/main/java/org/xyzh/api/study/course/CourseService.java index 05c036b..a963adb 100644 --- a/schoolNewsServ/api/api-study/src/main/java/org/xyzh/api/study/course/CourseService.java +++ b/schoolNewsServ/api/api-study/src/main/java/org/xyzh/api/study/course/CourseService.java @@ -199,4 +199,13 @@ public interface CourseService { * @since 2025-11-14 */ ResultDomain getCourseCount(TbCourse filter); + + /** + * @description 强制发布课程(跳过敏感词校验) + * @param courseID 课程ID + * @return ResultDomain 发布结果 + * @author kiro + * @since 2026-01-14 + */ + ResultDomain forcePublishCourse(String courseID); } diff --git a/schoolNewsServ/study/src/main/java/org/xyzh/study/controller/CourseController.java b/schoolNewsServ/study/src/main/java/org/xyzh/study/controller/CourseController.java index df9dcfb..36c4172 100644 --- a/schoolNewsServ/study/src/main/java/org/xyzh/study/controller/CourseController.java +++ b/schoolNewsServ/study/src/main/java/org/xyzh/study/controller/CourseController.java @@ -166,4 +166,12 @@ public class CourseController { return courseService.incrementLearnCount(courseID); } + /** + * 强制发布课程(跳过敏感词校验) + */ + @PostMapping("/{courseID}/force-publish") + public ResultDomain forcePublishCourse(@PathVariable("courseID") String courseID) { + return courseService.forcePublishCourse(courseID); + } + } diff --git a/schoolNewsServ/study/src/main/java/org/xyzh/study/service/impl/SCCourseServiceImpl.java b/schoolNewsServ/study/src/main/java/org/xyzh/study/service/impl/SCCourseServiceImpl.java index 542825a..c7483c2 100644 --- a/schoolNewsServ/study/src/main/java/org/xyzh/study/service/impl/SCCourseServiceImpl.java +++ b/schoolNewsServ/study/src/main/java/org/xyzh/study/service/impl/SCCourseServiceImpl.java @@ -154,6 +154,72 @@ public class SCCourseServiceImpl implements SCCourseService { return resultDomain; } + // 验证课程基本信息 + if (courseItemVO.getName() == null || courseItemVO.getName().trim().isEmpty()) { + resultDomain.fail("课程名称不能为空"); + return resultDomain; + } + + // 验证章节 + List chapterVOs = courseItemVO.getChapters(); + if (chapterVOs == null || chapterVOs.isEmpty()) { + resultDomain.fail("课程至少需要一个章节"); + return resultDomain; + } + + // 验证每个章节 + for (int i = 0; i < chapterVOs.size(); i++) { + CourseItemVO chapterVO = chapterVOs.get(i); + if (chapterVO.getName() == null || chapterVO.getName().trim().isEmpty()) { + resultDomain.fail("第" + (i + 1) + "个章节名称不能为空"); + return resultDomain; + } + + // 验证章节节点 + List nodeVOs = chapterVO.getChapters(); + if (nodeVOs == null || nodeVOs.isEmpty()) { + resultDomain.fail("章节「" + chapterVO.getName() + "」至少需要一个学习节点"); + return resultDomain; + } + + // 验证每个节点 + for (int j = 0; j < nodeVOs.size(); j++) { + CourseItemVO nodeVO = nodeVOs.get(j); + if (nodeVO.getName() == null || nodeVO.getName().trim().isEmpty()) { + resultDomain.fail("章节「" + chapterVO.getName() + "」的第" + (j + 1) + "个节点名称不能为空"); + return resultDomain; + } + + // 根据节点类型验证内容 + Integer nodeType = nodeVO.getNodeType(); + if (nodeType == null) { + resultDomain.fail("章节「" + chapterVO.getName() + "」的节点「" + nodeVO.getName() + "」类型不能为空"); + return resultDomain; + } + + // 类型1:文章,需要resourceID + // 类型2:文件/视频,需要resourceID或videoUrl + // 类型3:文本,需要content + if (nodeType == 1) { + if (nodeVO.getResourceID() == null || nodeVO.getResourceID().trim().isEmpty()) { + resultDomain.fail("章节「" + chapterVO.getName() + "」的节点「" + nodeVO.getName() + "」需要关联文章"); + return resultDomain; + } + } else if (nodeType == 2) { + if ((nodeVO.getResourceID() == null || nodeVO.getResourceID().trim().isEmpty()) + && (nodeVO.getVideoUrl() == null || nodeVO.getVideoUrl().trim().isEmpty())) { + resultDomain.fail("章节「" + chapterVO.getName() + "」的节点「" + nodeVO.getName() + "」需要上传文件或视频"); + return resultDomain; + } + } else if (nodeType == 3) { + if (nodeVO.getContent() == null || nodeVO.getContent().trim().isEmpty()) { + resultDomain.fail("章节「" + chapterVO.getName() + "」的节点「" + nodeVO.getName() + "」内容不能为空"); + return resultDomain; + } + } + } + } + // 转换为课程实体并保存 TbCourse course = courseItemVO.toCourse(); String courseID = IDUtils.generateID(); @@ -763,4 +829,72 @@ public class SCCourseServiceImpl implements SCCourseService { } return resultDomain; } + + @Override + @Transactional(rollbackFor = Exception.class) + public ResultDomain forcePublishCourse(String courseID) { + ResultDomain resultDomain = new ResultDomain<>(); + try { + // 参数验证 + if (courseID == null || courseID.isEmpty()) { + resultDomain.fail("课程ID不能为空"); + return resultDomain; + } + + // 查询课程 + TbCourse course = courseMapper.selectByCourseId(courseID); + if (course == null) { + resultDomain.fail("课程不存在"); + return resultDomain; + } + + // 获取所有课程节点,强制设置为已审核 + List nodeList = courseNodeMapper.selectByCourseId(courseID); + if (nodeList != null && !nodeList.isEmpty()) { + List nodesToUpdate = new ArrayList<>(); + for (TbCourseNode node : nodeList) { + if (!node.getIsAudited()) { + node.setIsAudited(true); + nodesToUpdate.add(node); + + // 如果是文章类型节点,同时更新文章的审核状态 + if (node.getNodeType() == 1 && node.getResourceID() != null) { + try { + ResourceVO resource = new ResourceVO(); + resource.setResource(new TbResource()); + resource.getResource().setResourceID(node.getResourceID()); + resource.getResource().setIsAudited(true); + resourceService.updateResource(resource); + } catch (Exception e) { + logger.warn("更新节点关联文章审核状态失败: {}", e.getMessage()); + } + } + } + } + + // 批量更新节点审核状态 + if (!nodesToUpdate.isEmpty()) { + courseNodeMapper.batchUpdateNodeAudited(nodesToUpdate); + } + } + + // 强制发布:跳过敏感词校验,直接设置为已发布 + course.setStatus(1); + course.setUpdateTime(new Date()); + int result = courseMapper.updateCourse(course); + + if (result > 0) { + logger.info("强制发布课程成功: {}", courseID); + // 重新查询返回完整数据 + TbCourse updated = courseMapper.selectByCourseId(courseID); + resultDomain.success("强制发布课程成功", updated); + } else { + resultDomain.fail("强制发布课程失败"); + } + } catch (Exception e) { + logger.error("强制发布课程异常: {}", e.getMessage(), e); + resultDomain.fail("强制发布课程失败: " + e.getMessage()); + } + return resultDomain; + } } diff --git a/schoolNewsServ/study/src/main/resources/mapper/CourseNodeMapper.xml b/schoolNewsServ/study/src/main/resources/mapper/CourseNodeMapper.xml index bf1d0cb..d01eee3 100644 --- a/schoolNewsServ/study/src/main/resources/mapper/CourseNodeMapper.xml +++ b/schoolNewsServ/study/src/main/resources/mapper/CourseNodeMapper.xml @@ -241,8 +241,8 @@ - UPDATE tb_course_node - SET is_audited = #{item.isAudited} + UPDATE tb_course_node + SET is_audited = #{item.isAudited} WHERE node_id = #{item.nodeID} diff --git a/schoolNewsWeb/src/apis/study/course.ts b/schoolNewsWeb/src/apis/study/course.ts index e02f70d..4b0e762 100644 --- a/schoolNewsWeb/src/apis/study/course.ts +++ b/schoolNewsWeb/src/apis/study/course.ts @@ -176,6 +176,16 @@ export const courseApi = { return response.data; }, + /** + * 强制发布课程(跳过敏感词校验) + * @param courseID 课程ID + * @returns Promise> + */ + async forcePublishCourse(courseID: string): Promise> { + const response = await api.post(`${this.prefixCourse}/${courseID}/force-publish`); + return response.data; + }, + /** * 获取课程章节列表 * @param courseID 课程ID diff --git a/schoolNewsWeb/src/main.ts b/schoolNewsWeb/src/main.ts index 2bc768e..1e8e160 100644 --- a/schoolNewsWeb/src/main.ts +++ b/schoolNewsWeb/src/main.ts @@ -6,7 +6,7 @@ import App from "./App.vue"; import "./registerServiceWorker"; import router from "./router"; import store from "./store"; -import { setupRouterGuards, setupTokenRefresh } from "@/utils/permission"; +import { setupRouterGuards, setupTokenRefresh, setupPermissionUtils } from "@/utils/permission"; import { setupPermissionDirectives } from "@/directives/permission"; // 引入 Quill 富文本编辑器样式(全局) @@ -49,6 +49,9 @@ async function initApp() { // 设置权限指令 setupPermissionDirectives(app, store); + // 设置权限工具 + setupPermissionUtils(store); + // 设置路由守卫 setupRouterGuards(router, store); diff --git a/schoolNewsWeb/src/utils/permission.ts b/schoolNewsWeb/src/utils/permission.ts index ddd0795..43d104b 100644 --- a/schoolNewsWeb/src/utils/permission.ts +++ b/schoolNewsWeb/src/utils/permission.ts @@ -266,3 +266,64 @@ export class PermissionChecker { return roleCodes.some(code => this.hasRole(code)); } } + +// 全局store引用,由setupPermissionUtils初始化 +let globalStore: Store | null = null; + +/** + * 初始化权限工具(在main.ts中调用) + */ +export function setupPermissionUtils(store: Store) { + globalStore = store; +} + +/** + * 权限检查 Composition API(用于Vue组件) + */ +export function usePermission() { + return { + /** + * 检查是否有指定权限 + */ + hasPermission: (permissionCode: string): boolean => { + if (!globalStore) return false; + return globalStore.getters['auth/hasPermission'](permissionCode); + }, + + /** + * 检查是否有任意一个权限 + */ + hasAnyPermission: (permissionCodes: string[]): boolean => { + if (!globalStore) return false; + return globalStore.getters['auth/hasAnyPermission'](permissionCodes); + }, + + /** + * 检查是否有所有权限 + */ + hasAllPermissions: (permissionCodes: string[]): boolean => { + if (!globalStore) return false; + return globalStore.getters['auth/hasAllPermissions'](permissionCodes); + }, + + /** + * 检查是否有指定角色 + */ + hasRole: (roleCode: string): boolean => { + if (!globalStore) return false; + const userRoles = globalStore.getters['auth/userRoles'] || []; + return userRoles.some((role: any) => role.code === roleCode); + }, + + /** + * 检查是否有任意一个角色 + */ + hasAnyRole: (roleCodes: string[]): boolean => { + if (!globalStore) return false; + const userRoles = globalStore.getters['auth/userRoles'] || []; + return roleCodes.some(code => + userRoles.some((role: any) => role.code === code) + ); + } + }; +} diff --git a/schoolNewsWeb/src/views/admin/manage/resource/ArticleManagementView.vue b/schoolNewsWeb/src/views/admin/manage/resource/ArticleManagementView.vue index d435451..72e7105 100644 --- a/schoolNewsWeb/src/views/admin/manage/resource/ArticleManagementView.vue +++ b/schoolNewsWeb/src/views/admin/manage/resource/ArticleManagementView.vue @@ -53,7 +53,7 @@ {{ getActionButtonText(row.status) }} hasPermission('admin:article:force-publish')); + const searchKeyword = ref(''); const pageParam = ref({ pageNumber: 1, diff --git a/schoolNewsWeb/src/views/public/course/components/CourseList.vue b/schoolNewsWeb/src/views/public/course/components/CourseList.vue index 98db1e7..4692e77 100644 --- a/schoolNewsWeb/src/views/public/course/components/CourseList.vue +++ b/schoolNewsWeb/src/views/public/course/components/CourseList.vue @@ -73,7 +73,7 @@ - +