课程强制发布

This commit is contained in:
2026-01-14 11:25:51 +08:00
parent d18a09e639
commit 109f552613
11 changed files with 327 additions and 8 deletions

View File

@@ -114,6 +114,7 @@ INSERT INTO `tb_sys_permission` (id, permission_id, name, code, description, mod
-- 1100-1199: 文章管理 -- 1100-1199: 文章管理
('1100', 'perm_admin_article_manage', '文章管理', 'admin:article:manage', '访问文章管理视图权限', 'module_news', '1', now()), ('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-1299: Banner管理
('1200', 'perm_admin_banner_manage', 'Banner管理', 'admin:banner:manage', '访问Banner管理视图权限', 'module_news', '1', now()), ('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-2299: 课程管理
('2200', 'perm_admin_course_manage', '课程管理', 'admin:course:manage', '访问课程管理视图权限', 'module_study', '1', now()), ('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-2399: 成就管理
('2300', 'perm_admin_achievement_manage', '成就管理', 'admin:achievement:manage', '访问成就管理视图权限', 'module_study', '1', now()), ('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) -- 普通管理员权限 (1000-8999)
('29', 'superadmin', 'perm_admin_resource_manage', '1', now()), ('29', 'superadmin', 'perm_admin_resource_manage', '1', now()),
('30', 'superadmin', 'perm_admin_article_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()), ('31', 'superadmin', 'perm_admin_banner_manage', '1', now()),
('32', 'superadmin', 'perm_admin_tag_manage', '1', now()), ('32', 'superadmin', 'perm_admin_tag_manage', '1', now()),
('33', 'superadmin', 'perm_admin_column_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()), ('35', 'superadmin', 'perm_admin_task_manage', '1', now()),
('36', 'superadmin', 'perm_admin_study_records', '1', now()), ('36', 'superadmin', 'perm_admin_study_records', '1', now()),
('37', 'superadmin', 'perm_admin_course_manage', '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()), ('38', 'superadmin', 'perm_admin_achievement_manage', '1', now()),
('39', 'superadmin', 'perm_admin_ai_config', '1', now()), ('39', 'superadmin', 'perm_admin_ai_config', '1', now()),
('40', 'superadmin', 'perm_admin_knowledge_manage', '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) -- 普通管理员权限 (1000-8999)
('122', 'admin', 'perm_admin_resource_manage', '1', now()), ('122', 'admin', 'perm_admin_resource_manage', '1', now()),
('123', 'admin', 'perm_admin_article_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()), ('124', 'admin', 'perm_admin_banner_manage', '1', now()),
('125', 'admin', 'perm_admin_tag_manage', '1', now()), ('125', 'admin', 'perm_admin_tag_manage', '1', now()),
('126', 'admin', 'perm_admin_column_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()), ('128', 'admin', 'perm_admin_task_manage', '1', now()),
('129', 'admin', 'perm_admin_study_records', '1', now()), ('129', 'admin', 'perm_admin_study_records', '1', now()),
('130', 'admin', 'perm_admin_course_manage', '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()), ('131', 'admin', 'perm_admin_achievement_manage', '1', now()),
('132', 'admin', 'perm_admin_ai_config', '1', now()), ('132', 'admin', 'perm_admin_ai_config', '1', now()),
('133', 'admin', 'perm_admin_knowledge_manage', '1', now()), ('133', 'admin', 'perm_admin_knowledge_manage', '1', now()),

View File

@@ -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');

View File

@@ -199,4 +199,13 @@ public interface CourseService {
* @since 2025-11-14 * @since 2025-11-14
*/ */
ResultDomain<Integer> getCourseCount(TbCourse filter); ResultDomain<Integer> getCourseCount(TbCourse filter);
/**
* @description 强制发布课程(跳过敏感词校验)
* @param courseID 课程ID
* @return ResultDomain<TbCourse> 发布结果
* @author kiro
* @since 2026-01-14
*/
ResultDomain<TbCourse> forcePublishCourse(String courseID);
} }

View File

@@ -166,4 +166,12 @@ public class CourseController {
return courseService.incrementLearnCount(courseID); return courseService.incrementLearnCount(courseID);
} }
/**
* 强制发布课程(跳过敏感词校验)
*/
@PostMapping("/{courseID}/force-publish")
public ResultDomain<TbCourse> forcePublishCourse(@PathVariable("courseID") String courseID) {
return courseService.forcePublishCourse(courseID);
}
} }

View File

@@ -154,6 +154,72 @@ public class SCCourseServiceImpl implements SCCourseService {
return resultDomain; return resultDomain;
} }
// 验证课程基本信息
if (courseItemVO.getName() == null || courseItemVO.getName().trim().isEmpty()) {
resultDomain.fail("课程名称不能为空");
return resultDomain;
}
// 验证章节
List<CourseItemVO> 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<CourseItemVO> 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(); TbCourse course = courseItemVO.toCourse();
String courseID = IDUtils.generateID(); String courseID = IDUtils.generateID();
@@ -763,4 +829,72 @@ public class SCCourseServiceImpl implements SCCourseService {
} }
return resultDomain; return resultDomain;
} }
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<TbCourse> forcePublishCourse(String courseID) {
ResultDomain<TbCourse> 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<TbCourseNode> nodeList = courseNodeMapper.selectByCourseId(courseID);
if (nodeList != null && !nodeList.isEmpty()) {
List<TbCourseNode> 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;
}
} }

View File

@@ -241,8 +241,8 @@
<!-- batchUpdateNodeAudited --> <!-- batchUpdateNodeAudited -->
<update id="batchUpdateNodeAudited"> <update id="batchUpdateNodeAudited">
<foreach collection="courseNodeList" item="item" separator=";"> <foreach collection="courseNodeList" item="item" separator=";">
UPDATE tb_course_node UPDATE tb_course_node
SET is_audited = #{item.isAudited} SET is_audited = #{item.isAudited}
WHERE node_id = #{item.nodeID} WHERE node_id = #{item.nodeID}
</foreach> </foreach>
</update> </update>

View File

@@ -176,6 +176,16 @@ export const courseApi = {
return response.data; return response.data;
}, },
/**
* 强制发布课程(跳过敏感词校验)
* @param courseID 课程ID
* @returns Promise<ResultDomain<Course>>
*/
async forcePublishCourse(courseID: string): Promise<ResultDomain<Course>> {
const response = await api.post<Course>(`${this.prefixCourse}/${courseID}/force-publish`);
return response.data;
},
/** /**
* 获取课程章节列表 * 获取课程章节列表
* @param courseID 课程ID * @param courseID 课程ID

View File

@@ -6,7 +6,7 @@ import App from "./App.vue";
import "./registerServiceWorker"; import "./registerServiceWorker";
import router from "./router"; import router from "./router";
import store from "./store"; import store from "./store";
import { setupRouterGuards, setupTokenRefresh } from "@/utils/permission"; import { setupRouterGuards, setupTokenRefresh, setupPermissionUtils } from "@/utils/permission";
import { setupPermissionDirectives } from "@/directives/permission"; import { setupPermissionDirectives } from "@/directives/permission";
// 引入 Quill 富文本编辑器样式(全局) // 引入 Quill 富文本编辑器样式(全局)
@@ -49,6 +49,9 @@ async function initApp() {
// 设置权限指令 // 设置权限指令
setupPermissionDirectives(app, store); setupPermissionDirectives(app, store);
// 设置权限工具
setupPermissionUtils(store);
// 设置路由守卫 // 设置路由守卫
setupRouterGuards(router, store); setupRouterGuards(router, store);

View File

@@ -266,3 +266,64 @@ export class PermissionChecker {
return roleCodes.some(code => this.hasRole(code)); return roleCodes.some(code => this.hasRole(code));
} }
} }
// 全局store引用由setupPermissionUtils初始化
let globalStore: Store<any> | null = null;
/**
* 初始化权限工具在main.ts中调用
*/
export function setupPermissionUtils(store: Store<any>) {
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)
);
}
};
}

View File

@@ -53,7 +53,7 @@
{{ getActionButtonText(row.status) }} {{ getActionButtonText(row.status) }}
</el-button> </el-button>
<el-button <el-button
v-if="row.status === ResourceStatus.SENSITIVE_FAILED" v-if="row.status === ResourceStatus.SENSITIVE_FAILED && canForcePublish"
size="small" size="small"
type="warning" type="warning"
@click="forcePublishArticle(row)" @click="forcePublishArticle(row)"
@@ -108,7 +108,7 @@ import { AdminLayout } from '@/views/admin';
defineOptions({ defineOptions({
name: 'ArticleManagementView' name: 'ArticleManagementView'
}); });
import { ref, onMounted } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { ElButton, ElInput, ElTable, ElTableColumn, ElTag, ElPagination, ElMessage, ElMessageBox, ElIcon } from 'element-plus'; import { ElButton, ElInput, ElTable, ElTableColumn, ElTag, ElPagination, ElMessage, ElMessageBox, ElIcon } from 'element-plus';
import { Search } from '@element-plus/icons-vue'; import { Search } from '@element-plus/icons-vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
@@ -116,8 +116,14 @@ import { resourceApi, resourceTagApi } from '@/apis/resource'
import type { PageParam, ResourceSearchParams, Resource, Tag } from '@/types'; import type { PageParam, ResourceSearchParams, Resource, Tag } from '@/types';
import { ArticleShowView } from '@/views/public/article'; import { ArticleShowView } from '@/views/public/article';
import { ResourceStatus } from '@/types/enums'; import { ResourceStatus } from '@/types/enums';
import { usePermission } from '@/utils/permission';
const router = useRouter(); const router = useRouter();
// 权限检查
const { hasPermission } = usePermission();
const canForcePublish = computed(() => hasPermission('admin:article:force-publish'));
const searchKeyword = ref(''); const searchKeyword = ref('');
const pageParam = ref<PageParam>({ const pageParam = ref<PageParam>({
pageNumber: 1, pageNumber: 1,

View File

@@ -73,7 +73,7 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="orderNum" label="排序" width="80" /> <el-table-column prop="orderNum" label="排序" width="80" />
<el-table-column label="操作" width="250" fixed="right"> <el-table-column label="操作" width="300" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button type="primary" size="small" link @click="handleEdit(row)"> <el-button type="primary" size="small" link @click="handleEdit(row)">
编辑 编辑
@@ -87,6 +87,15 @@
> >
发布 发布
</el-button> </el-button>
<el-button
v-if="row.status === 4 && canForcePublish"
type="warning"
size="small"
link
@click="handleForcePublish(row)"
>
强制发布
</el-button>
<el-button <el-button
v-if="row.status === 1" v-if="row.status === 1"
type="warning" type="warning"
@@ -121,12 +130,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'; import { ref, reactive, onMounted, computed } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { Search, Plus } from '@element-plus/icons-vue'; import { Search, Plus } from '@element-plus/icons-vue';
import { courseApi } from '@/apis/study'; import { courseApi } from '@/apis/study';
import { FILE_DOWNLOAD_URL } from '@/config'; import { FILE_DOWNLOAD_URL } from '@/config';
import type { Course } from '@/types'; import type { Course } from '@/types';
import { usePermission } from '@/utils/permission';
defineOptions({ defineOptions({
name: 'CourseList' name: 'CourseList'
@@ -137,6 +147,10 @@ const emit = defineEmits<{
edit: [course: Course]; edit: [course: Course];
}>(); }>();
// 权限检查
const { hasPermission } = usePermission();
const canForcePublish = computed(() => hasPermission('admin:course:force-publish'));
const loading = ref(false); const loading = ref(false);
const courseList = ref<Course[]>([]); const courseList = ref<Course[]>([]);
const total = ref(0); const total = ref(0);
@@ -211,7 +225,7 @@ async function handleUpdateStatus(course: Course, status: number) {
ElMessage.success(`${statusText}成功`); ElMessage.success(`${statusText}成功`);
loadCourses(); loadCourses();
} else { } else {
ElMessage.error(`${statusText}失败`); ElMessage.error(res.message || `${statusText}失败`);
} }
} catch (error) { } catch (error) {
if (error !== 'cancel') { if (error !== 'cancel') {
@@ -221,6 +235,33 @@ async function handleUpdateStatus(course: Course, status: number) {
} }
} }
// 强制发布
async function handleForcePublish(course: Course) {
try {
await ElMessageBox.confirm(
`确定要强制发布课程「${course.name}」吗?此操作将跳过敏感词校验。`,
'强制发布确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
);
const res = await courseApi.forcePublishCourse(course.courseID!);
if (res.success) {
ElMessage.success('强制发布成功');
loadCourses();
} else {
ElMessage.error(res.message || '强制发布失败');
}
} catch (error) {
if (error !== 'cancel') {
console.error('强制发布失败:', error);
ElMessage.error('强制发布失败');
}
}
}
// 删除 // 删除
async function handleDelete(course: Course) { async function handleDelete(course: Course) {
try { try {