实现敏感词检测后,失败发生邮箱

This commit is contained in:
2025-11-22 14:03:40 +08:00
parent c2cac51762
commit f3a9926caf
35 changed files with 1233 additions and 43916 deletions

View File

@@ -0,0 +1,181 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
敏感词批量导入脚本
从 sensitive_word_dict.txt 文件读取敏感词并批量插入到数据库
"""
import pymysql
import os
import sys
from datetime import datetime
# 数据库配置
DB_CONFIG = {
'host': 'localhost',
'port': 3306,
'user': 'root',
'password': '123456',
'database': 'school_news',
'charset': 'utf8mb4'
}
def get_db_connection():
"""获取数据库连接"""
try:
connection = pymysql.connect(**DB_CONFIG)
return connection
except Exception as e:
print(f"数据库连接失败: {e}")
return None
def read_sensitive_words(file_path):
"""读取敏感词文件"""
words = []
try:
with open(file_path, 'r', encoding='utf-8') as f:
for line in f:
word = line.strip()
if word and len(word) > 0:
words.append(word)
print(f"成功读取 {len(words)} 个敏感词")
return words
except Exception as e:
print(f"读取敏感词文件失败: {e}")
return []
def batch_insert_words(connection, words, batch_size=1000):
"""批量插入敏感词到数据库"""
cursor = connection.cursor()
try:
# 清空现有的deny类型敏感词可选
print("清理现有的deny类型敏感词...")
cursor.execute("DELETE FROM tb_sensitive_word WHERE type = 'deny'")
# 准备批量插入SQL
insert_sql = "INSERT INTO tb_sensitive_word (word, type) VALUES (%s, %s)"
# 分批插入
total_words = len(words)
inserted_count = 0
for i in range(0, total_words, batch_size):
batch_words = words[i:i + batch_size]
batch_data = [(word, 'deny') for word in batch_words]
try:
cursor.executemany(insert_sql, batch_data)
connection.commit()
inserted_count += len(batch_data)
print(f"已插入 {inserted_count}/{total_words} 个敏感词")
except Exception as e:
print(f"批量插入失败: {e}")
connection.rollback()
break
print(f"批量插入完成,共插入 {inserted_count} 个敏感词")
return inserted_count
except Exception as e:
print(f"批量插入过程中发生错误: {e}")
connection.rollback()
return 0
finally:
cursor.close()
def check_duplicates(connection, words):
"""检查重复的敏感词"""
cursor = connection.cursor()
try:
# 查询已存在的敏感词
cursor.execute("SELECT word FROM tb_sensitive_word WHERE type = 'deny'")
existing_words = set(row[0] for row in cursor.fetchall())
# 过滤重复词
new_words = [word for word in words if word not in existing_words]
duplicate_count = len(words) - len(new_words)
if duplicate_count > 0:
print(f"发现 {duplicate_count} 个重复敏感词,将跳过")
return new_words
except Exception as e:
print(f"检查重复词时发生错误: {e}")
return words
finally:
cursor.close()
def main():
"""主函数"""
print("=" * 50)
print("敏感词批量导入工具")
print("=" * 50)
# 获取脚本所在目录
script_dir = os.path.dirname(os.path.abspath(__file__))
dict_file = os.path.join(script_dir, 'sensitive_word_dict.txt')
# 检查敏感词文件是否存在
if not os.path.exists(dict_file):
print(f"敏感词文件不存在: {dict_file}")
return
# 读取敏感词
print("正在读取敏感词文件...")
words = read_sensitive_words(dict_file)
if not words:
print("没有读取到有效的敏感词")
return
# 连接数据库
print("正在连接数据库...")
connection = get_db_connection()
if not connection:
print("数据库连接失败,程序退出")
return
try:
# 检查重复词(可选,如果不需要可以注释掉)
# print("正在检查重复敏感词...")
# words = check_duplicates(connection, words)
if not words:
print("所有敏感词都已存在,无需导入")
return
# 确认导入
print(f"准备导入 {len(words)} 个敏感词到数据库")
confirm = input("是否继续?(y/N): ").strip().lower()
if confirm != 'y':
print("用户取消导入")
return
# 批量插入
print("开始批量导入敏感词...")
start_time = datetime.now()
inserted_count = batch_insert_words(connection, words)
end_time = datetime.now()
duration = (end_time - start_time).total_seconds()
print("=" * 50)
print(f"导入完成!")
print(f"成功导入: {inserted_count} 个敏感词")
print(f"耗时: {duration:.2f}")
print("=" * 50)
except Exception as e:
print(f"程序执行过程中发生错误: {e}")
finally:
connection.close()
print("数据库连接已关闭")
if __name__ == "__main__":
main()

View File

@@ -15,7 +15,7 @@ CREATE TABLE `tb_resource` (
`view_count` INT(11) DEFAULT 0 COMMENT '浏览次数',
`like_count` INT(11) DEFAULT 0 COMMENT '点赞次数',
`collect_count` INT(11) DEFAULT 0 COMMENT '收藏次数',
`status` INT(4) DEFAULT 0 COMMENT '状态0草稿 1已发布 2下架',
`status` INT(4) DEFAULT 0 COMMENT '状态0草稿 1已发布 2下架 3审核中 4敏感词未通过',
`is_audited` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否已审核',
`is_recommend` TINYINT(1) DEFAULT 0 COMMENT '是否推荐',
`is_banner` TINYINT(1) DEFAULT 0 COMMENT '是否轮播',

View File

@@ -3,8 +3,8 @@ use school_news;
DROP TABLE IF EXISTS `tb_sensitive_word`;
CREATE TABLE `tb_sensitive_word` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`word` varchar(255) NOT NULL COMMENT '敏感词',
`type` VARCHAR(10) NOT NULL COMMENT '类型allow\deny'
`word` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '敏感词',
`type` varchar(10) NOT NULL COMMENT '类型allow-允许词deny-禁用词',
PRIMARY KEY (`id`),
UNIQUE KEY `word` (`word`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='敏感词表';
UNIQUE KEY `uk_word` (`word`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='敏感词表';

View File

@@ -41,7 +41,8 @@ INSERT INTO `tb_sys_module` (id, module_id, name, code, description, icon, order
('5', 'module_usercenter', '用户中心', 'usercenter', '用户中心模块', 'el-icon-user', 5, 1, '1', now()),
('6', 'module_file', '文件管理', 'file', '文件管理模块', 'el-icon-folder', 6, 1, '1', now()),
('7', 'module_crontab', '定时任务', 'crontab', '定时任务管理模块', 'el-icon-alarm-clock', 7, 1, '1', now()),
('8', 'module_message', '消息通知', 'message', '消息通知管理模块', 'el-icon-message', 8, 1, '1', now());
('8', 'module_message', '消息通知', 'message', '消息通知管理模块', 'el-icon-message', 8, 1, '1', now()),
('9', 'module_sensitive', '敏感词管理', 'sensitive', '敏感词管理模块', 'el-icon-warning', 9, 1, '1', now());
-- 插入权限数据
INSERT INTO `tb_sys_permission` (id,permission_id, name, code, description, module_id, creator, create_time) VALUES
@@ -64,7 +65,8 @@ INSERT INTO `tb_sys_permission` (id,permission_id, name, code, description, modu
('15','perm_crontab_execute', '定时任务执行', 'crontab:execute', '定时任务执行权限', 'module_crontab', '1', now()),
('16','perm_message_manage', '消息管理', 'message:manage', '消息管理权限(管理端)', 'module_message', '1', now()),
('17','perm_message_send', '消息发送', 'message:send', '消息发送权限', 'module_message', '1', now()),
('18','perm_message_view', '消息查看', 'message:view', '消息查看权限(用户端)', 'module_message', '1', now());
('18','perm_message_view', '消息查看', 'message:view', '消息查看权限(用户端)', 'module_message', '1', now()),
('19','perm_sensitive_manage', '敏感词管理', 'sensitive:manage', '敏感词管理权限', 'module_sensitive', '1', now());
-- 插入角色-权限关联数据
INSERT INTO `tb_sys_role_permission` (id, role_id, permission_id, creator, create_time) VALUES
@@ -88,28 +90,30 @@ INSERT INTO `tb_sys_role_permission` (id, role_id, permission_id, creator, creat
('17', 'superadmin', 'perm_crontab_execute', '1', now()),
('18', 'superadmin', 'perm_message_manage', '1', now()),
('19', 'superadmin', 'perm_message_send', '1', now()),
('19.1', 'superadmin', 'perm_message_view', '1', now()),
('20', 'superadmin', 'perm_message_view', '1', now()),
('21', 'superadmin', 'perm_sensitive_manage', '1', now()),
-- 管理员:拥有业务管理权限,但没有系统日志等系统管理权限
('20', 'admin', 'perm_default', '1', now()),
('21', 'admin', 'perm_news_manage', '1', now()),
('22', 'admin', 'perm_news_article_add', '1', now()),
('23', 'admin', 'perm_study_manage', '1', now()),
('24', 'admin', 'perm_achievement_manage', '1', now()),
('25', 'admin', 'perm_ai_manage', '1', now()),
('26', 'admin', 'perm_usercenter_manage', '1', now()),
('27', 'admin', 'perm_file_manage', '1', now()),
('28', 'admin', 'perm_message_manage', '1', now()),
('29', 'admin', 'perm_message_send', '1', now()),
('29.1', 'admin', 'perm_message_view', '1', now()),
('22', 'admin', 'perm_default', '1', now()),
('23', 'admin', 'perm_news_manage', '1', now()),
('24', 'admin', 'perm_news_article_add', '1', now()),
('25', 'admin', 'perm_study_manage', '1', now()),
('26', 'admin', 'perm_achievement_manage', '1', now()),
('27', 'admin', 'perm_ai_manage', '1', now()),
('28', 'admin', 'perm_usercenter_manage', '1', now()),
('29', 'admin', 'perm_file_manage', '1', now()),
('30', 'admin', 'perm_message_manage', '1', now()),
('31', 'admin', 'perm_message_send', '1', now()),
('32', 'admin', 'perm_message_view', '1', now()),
('33', 'admin', 'perm_sensitive_manage', '1', now()),
-- 自由角色:拥有用户视图相关的所有权限(前台用户权限)
('30', 'freedom', 'perm_default', '1', now()),
('31', 'freedom', 'perm_news_article_add', '1', now()),
('32', 'freedom', 'perm_ai_manage', '1', now()),
('33', 'freedom', 'perm_usercenter_manage', '1', now()),
('34', 'freedom', 'perm_file_manage', '1', now()),
('35', 'freedom', 'perm_message_view', '1', now());
('40', 'freedom', 'perm_default', '1', now()),
('41', 'freedom', 'perm_news_article_add', '1', now()),
('42', 'freedom', 'perm_ai_manage', '1', now()),
('43', 'freedom', 'perm_usercenter_manage', '1', now()),
('44', 'freedom', 'perm_file_manage', '1', now()),
('45', 'freedom', 'perm_message_view', '1', now());
-- 插入前端菜单数据
INSERT INTO `tb_sys_menu` VALUES
@@ -149,6 +153,7 @@ INSERT INTO `tb_sys_menu` VALUES
('4001', 'menu_admin_banner', 'Banner管理', 'menu_admin_content_manage', '/admin/manage/content/banner', 'admin/manage/content/BannerManagementView', NULL, 1, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:49:56', NULL, 0),
('4002', 'menu_admin_tag', '标签管理', 'menu_admin_content_manage', '/admin/manage/content/tag', 'admin/manage/content/TagManagementView', NULL, 2, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:49:56', NULL, 0),
('4003', 'menu_admin_column', '栏目管理', 'menu_admin_content_manage', '/admin/manage/content/column', 'admin/manage/content/ColumnManagementView', NULL, 3, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:49:56', NULL, 0),
('4004', 'menu_admin_sensitive', '敏感词管理', 'menu_admin_content_manage', '/admin/manage/content/sensitive', 'admin/manage/content/SensitiveManagementView', NULL, 4, 0, 'SidebarLayout', '1', NULL, '2025-11-22 11:30:00', '2025-11-22 11:30:00', NULL, 0),
('5000', 'menu_admin_study_manage', '学习管理', NULL, '', '', 'admin/study.svg', 5, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:52:46', NULL, 0),
('5002', 'menu_admin_task_manage', '任务管理', 'menu_admin_study_manage', '/admin/manage/study/task-manage', 'admin/manage/study/TaskManageView', NULL, 2, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('5003', 'menu_admin_study_records', '学习记录', 'menu_admin_study_manage', '/admin/manage/study/study-records', 'admin/manage/study/StudyRecordsView', NULL, 3, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
@@ -212,6 +217,7 @@ INSERT INTO `tb_sys_menu_permission` (id, permission_id, menu_id, creator, creat
('214', 'perm_news_manage', 'menu_admin_banner', '1', now()),
('215', 'perm_news_manage', 'menu_admin_tag', '1', now()),
('216', 'perm_news_manage', 'menu_admin_column', '1', now()),
('217', 'perm_sensitive_manage', 'menu_admin_sensitive', '1', now()),
('218', 'perm_study_manage', 'menu_admin_study_manage', '1', now()),
('220', 'perm_study_manage', 'menu_admin_task_manage', '1', now()),
('221', 'perm_study_manage', 'menu_admin_study_records', '1', now()),

View File

@@ -91,6 +91,11 @@
<artifactId>message</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.xyzh</groupId>
<artifactId>sensitive</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
<build>

View File

@@ -18,7 +18,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
@EnableScheduling
@MapperScan({"org.xyzh.system.mapper", "org.xyzh.file.mapper", "org.xyzh.news.mapper", "org.xyzh.study.mapper",
"org.xyzh.usercenter.mapper", "org.xyzh.ai.mapper", "org.xyzh.achievement.mapper", "org.xyzh.crontab.mapper",
"org.xyzh.message.mapper"})
"org.xyzh.message.mapper", "org.xyzh.sensitive.mapper"})
public class App {
public static void main(String[] args) {

View File

@@ -186,6 +186,14 @@
<AppenderRef ref="RollingFileError"/>
<AppenderRef ref="DatabaseAppender"/>
</Logger>
<Logger name="org.xyzh.sensitive" level="debug" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="Filelog"/>
<AppenderRef ref="RollingFileInfo"/>
<AppenderRef ref="RollingFileWarn"/>
<AppenderRef ref="RollingFileError"/>
<AppenderRef ref="DatabaseAppender"/>
</Logger>
<root level="info">
<appender-ref ref="Console"/>

View File

@@ -1,5 +1,7 @@
package org.xyzh.api.news.resource;
import java.util.List;
import org.xyzh.common.core.domain.ResultDomain;
/**
@@ -14,12 +16,21 @@ public interface ResourceAuditService {
* @param text 待审核文本
* @return ResultDomain<Boolean> 审核结果true 表示通过)
*/
ResultDomain<Boolean> auditText(String text);
ResultDomain<String> auditText(String text);
/**
* 根据文件ID进行审核fileId 对应 tb_sys_file.id
* @param fileId 文件ID
* @return ResultDomain<Boolean> 审核结果true 表示通过)
*/
ResultDomain<Boolean> auditByFileId(String fileId);
ResultDomain<String> auditByFileId(String fileId);
/**
* 发送审核结果消息
* @param receiver 接收者用户ID
* @param target 审核目标(文章、课程名称)
* @param sensitiveWords 检测到的敏感词列表
* @return ResultDomain<Boolean> 发送结果
*/
ResultDomain<Boolean> sendAuditResultMessage(String receiver, String target, List<String> sensitiveWords);
}

View File

@@ -1,13 +1,18 @@
package org.xyzh.api.sensitive;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageRequest;
import org.xyzh.common.dto.sensitive.TbSensitive;
public interface SensitiveService {
ResultDomain<TbSensitive> page(PageRequest<TbSensitive> pageRequest);
ResultDomain<Boolean> addSensitiveWord(TbSensitive sensitive);
ResultDomain<Boolean> changeSensitiveWordType(TbSensitive sensitive);
ResultDomain<Boolean> deleteSensitiveWord(TbSensitive sensitive);
ResultDomain<Boolean> judgeSensitive(String text);
ResultDomain<String> judgeSensitive(String text);
}

View File

@@ -356,6 +356,38 @@ public class ResultDomain<T> implements Serializable{
this.data = null;
}
/**
* @description 操作失败
* @param message 返回消息
* @param data 返回数据
* @author yslg
* @since 2025-09-07
*/
public void fail(String message, T data) {
this.code = HttpStatus.BAD_REQUEST.value();
this.message = message;
this.success = false;
this.auth = true;
this.login = true;
this.data = data;
}
/**
* @description 操作失败
* @param message 返回消息
* @param dataList 返回数据列表
* @author yslg
* @since 2025-09-07
*/
public void fail(String message, List<T> dataList) {
this.code = HttpStatus.BAD_REQUEST.value();
this.message = message;
this.success = false;
this.auth = true;
this.login = true;
this.dataList = dataList;
}
/**
* @description 操作失败
* @param code 状态码

View File

@@ -29,6 +29,11 @@
<artifactId>api-usercenter</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.xyzh</groupId>
<artifactId>api-sensitive</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.xyzh</groupId>
<artifactId>system</artifactId>

View File

@@ -18,7 +18,6 @@ import org.xyzh.common.utils.IDUtils;
import org.xyzh.news.mapper.BannerMapper;
import org.xyzh.api.news.banner.BannerService;
import org.xyzh.api.system.permission.ResourcePermissionService;
import org.xyzh.common.dto.user.TbSysUser;
import org.xyzh.common.vo.UserDeptRoleVO;
import org.xyzh.common.core.enums.ResourceType;
import org.xyzh.system.utils.LoginUtil;

View File

@@ -281,14 +281,17 @@ public class NCResourceServiceImpl implements ResourceService {
TbResource resource = resourceVO.getResource();
if(resource.getStatus()==1 && !resource.getIsAudited()){
// 进行审核
ResultDomain<Boolean> pass =auditService.auditText(resource.getContent());
if(pass.isSuccess() && pass.getData()){
ResultDomain<String> pass =auditService.auditText(resource.getTitle() + " "+resource.getContent());
if(pass.isSuccess()){
resource.setIsAudited(true);
}else {
auditService.sendAuditResultMessage(resource.getCreator(), "文章"+resource.getTitle(), pass.getDataList());
resource.setStatus(4);
}
}
// 插入数据库
int result = resourceMapper.insertResource(resourceVO.getResource());
int result = resourceMapper.insertResource(resource);
// 插入资源标签
if (resourceVO.getTags() != null && resourceVO.getTags().size() > 0) {
List<TbResourceTag> resourceTagList = new ArrayList<>();
@@ -360,10 +363,37 @@ public class NCResourceServiceImpl implements ResourceService {
return resultDomain;
}
resource.setIsAudited(existing.getIsAudited());
if(!existing.getContent().equals(resource.getContent())){
// 检查内容是否发生变化,如果变化则需要重新审核
boolean contentChanged = !existing.getContent().equals(resource.getContent());
boolean titleChanged = !existing.getTitle().equals(resource.getTitle());
// 如果内容或标题发生变化,需要重新审核
if (contentChanged || titleChanged) {
resource.setIsAudited(false);
// 如果当前状态是已发布(1),且要发布状态(1),需要先设置为审核中(3)再进行审核
if (resource.getStatus() != null && resource.getStatus() == 1) {
// 先设置为审核中状态
resource.setStatus(3);
// 进行敏感词审核
ResultDomain<String> auditResult = auditService.auditText(resource.getTitle() +" "+resource.getContent());
if (auditResult.isSuccess()) {
// 审核通过,设置为已发布
resource.setStatus(1);
resource.setIsAudited(true);
} else {
// 审核失败,设置为敏感词未通过
resource.setStatus(4);
resource.setIsAudited(false);
auditService.sendAuditResultMessage(existing.getCreator(), "文章:"+resource.getTitle(), auditResult.getDataList());
}
}
} else {
// 内容未变化,保持原有审核状态
resource.setIsAudited(existing.getIsAudited());
}
Date now = new Date();
// tag先删后增
TbResourceTag resourceTag = new TbResourceTag();
@@ -376,6 +406,7 @@ public class NCResourceServiceImpl implements ResourceService {
resourceTagMapper.deleteByResourceId(resource.getResourceID());
resourceTagMapper.batchInsertResourceTags(Arrays.asList(resourceTag));
// 更新时间
resource.setUpdateTime(now);
@@ -388,7 +419,15 @@ public class NCResourceServiceImpl implements ResourceService {
ResourceVO updatedResourceVO = new ResourceVO();
updatedResourceVO.setResource(updated);
updatedResourceVO.setTags(resourceVO.getTags());
resultDomain.success("更新资源成功", updatedResourceVO);
// 根据审核结果返回不同消息
if (updated.getStatus() == 4) {
resultDomain.success("更新成功,但内容包含敏感词,请修改后重新发布", updatedResourceVO);
} else if (updated.getStatus() == 3) {
resultDomain.success("更新成功,正在审核中", updatedResourceVO);
} else {
resultDomain.success("更新资源成功", updatedResourceVO);
}
return resultDomain;
} else {
resultDomain.fail("更新资源失败");
@@ -457,22 +496,29 @@ public class NCResourceServiceImpl implements ResourceService {
resultDomain.fail("资源不存在");
return resultDomain;
}
if (status == 1 && !resource.getIsAudited()) {
ResultDomain<Boolean> pass = auditService.auditText(resource.getContent());
if (pass.isSuccess() && pass.getData()) {
// 更新状态
resource.setStatus(status);
if (status == 1) { // 发布状态下更新都有重新审核
// 审核中状态变更
TbResource resourceStautsChange = new TbResource();
resourceStautsChange.setResourceID(resourceID);
resourceStautsChange.setStatus(3);
resourceMapper.updateResource(resourceStautsChange);
ResultDomain<String> pass = auditService.auditText(resource.getTitle()+" "+resource.getContent());
if (pass.isSuccess()) {
resource.setIsAudited(true);
} else {
// 审核失败,标记状态为3(审核失败)
resource.setStatus(3);
// 审核失败,标记状态为4(审核失败)
resource.setStatus(4);
resource.setUpdateTime(new Date());
resourceMapper.updateResource(resource);
auditService.sendAuditResultMessage(resource.getCreator(), "文章"+resource.getTitle(), pass.getDataList());
resultDomain.fail("审核失败");
return resultDomain;
}
}
// 更新状态
resource.setStatus(status);
resource.setUpdateTime(new Date());
int result = resourceMapper.updateResource(resource);
@@ -512,15 +558,15 @@ public class NCResourceServiceImpl implements ResourceService {
}
if (!resource.getIsAudited()) {
ResultDomain<Boolean> pass = auditService.auditText(resource.getContent());
if (pass.isSuccess() && pass.getData()) {
ResultDomain<String> pass = auditService.auditText(resource.getTitle()+" "+resource.getContent());
if (pass.isSuccess()) {
resource.setIsAudited(true);
} else {
// 审核失败标记状态为3审核失败
resource.setStatus(3);
resource.setUpdateTime(new Date());
resourceMapper.updateResource(resource);
auditService.sendAuditResultMessage(resource.getCreator(), "文章"+resource.getTitle(), pass.getDataList());
resultDomain.fail("审核失败");
return resultDomain;
}

View File

@@ -1,8 +1,18 @@
package org.xyzh.news.service.impl;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.xyzh.api.news.resource.ResourceAuditService;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.dto.user.TbSysUser;
import org.xyzh.common.utils.EmailUtils;
import org.xyzh.api.sensitive.SensitiveService;
import org.xyzh.api.system.user.UserService;
/**
* @description 资源审核服务实现类
@@ -11,24 +21,185 @@ import org.xyzh.common.core.domain.ResultDomain;
@Service
public class ResourceAuditServiceImpl implements ResourceAuditService {
private static final Logger logger = LoggerFactory.getLogger(ResourceAuditServiceImpl.class);
@Autowired
private SensitiveService sensitiveService;
@Autowired
private UserService userService;
@Autowired
private EmailUtils emailUtils;
@Override
public ResultDomain<Boolean> auditText(String text) {
ResultDomain<Boolean> result = new ResultDomain<Boolean>();
public ResultDomain<String> auditText(String text) {
ResultDomain<String> result = new ResultDomain<>();
ResultDomain<String> sensitiveRet = sensitiveService.judgeSensitive(text);
// TODO: 文本审核逻辑(敏感词、违规词等规则校验)
// 示例:直接通过
result.success("审核通过", Boolean.TRUE);
if (sensitiveRet.isSuccess()) {
result.success("审核通过", "");
} else {
result.fail("审核失败", sensitiveRet.getDataList());
}
return result;
}
@Override
public ResultDomain<Boolean> auditByFileId(String fileId) {
ResultDomain<Boolean> result = new ResultDomain<Boolean>();
public ResultDomain<String> auditByFileId(String fileId) {
ResultDomain<String> result = new ResultDomain<>();
// TODO:
// 1. 根据 tb_sys_file.id 查询文件信息
// 2. 读取文件内容,提取文本
// 3. 调用 auditText(text) 进行审核
// 4. 审核通过后,更新对应业务记录(tb_resource / tb_data_collection_item / tb_course_node) 的 is_audited = 1
result.success("审核通过", Boolean.TRUE);
String text = "";
ResultDomain<String> sensitiveRet = sensitiveService.judgeSensitive(text);
if (sensitiveRet.isSuccess()) {
result.success("审核通过", "");
} else {
result.fail("审核失败", sensitiveRet.getDataList());
}
return result;
}
@Override
public ResultDomain<Boolean> sendAuditResultMessage(String receiver, String target, List<String> sensitiveWords) {
ResultDomain<Boolean> result = new ResultDomain<>();
try {
// 参数验证
if (!StringUtils.hasText(receiver)) {
logger.warn("发送审核结果邮件失败接收者ID为空");
result.fail("接收者ID不能为空");
return result;
}
if (!StringUtils.hasText(target)) {
logger.warn("发送审核结果邮件失败:目标内容名称为空");
result.fail("目标内容名称不能为空");
return result;
}
// 获取用户信息
ResultDomain<TbSysUser> userDomain = userService.getUserById(receiver);
if (!userDomain.isSuccess() || userDomain.getData() == null) {
logger.warn("发送审核结果邮件失败用户不存在用户ID: {}", receiver);
result.fail("用户不存在");
return result;
}
TbSysUser user = userDomain.getData();
if (!StringUtils.hasText(user.getEmail())) {
logger.warn("发送审核结果邮件失败用户邮箱为空用户ID: {}", receiver);
result.fail("用户邮箱为空");
return result;
}
// 构建邮件内容
String emailBody = buildEmailBody(target, sensitiveWords);
String subject = "【校园新闻平台】审核结果通知";
// 发送HTML邮件
boolean sent = emailUtils.sendHtmlEmail(user.getEmail(), subject, emailBody);
if (sent) {
logger.info("审核结果邮件发送成功用户ID: {}, 邮箱: {}, 内容: {}", receiver, user.getEmail(), target);
result.success("发送消息成功", true);
} else {
logger.error("审核结果邮件发送失败用户ID: {}, 邮箱: {}, 内容: {}", receiver, user.getEmail(), target);
result.fail("邮件发送失败");
}
} catch (Exception e) {
logger.error("发送审核结果邮件异常用户ID: {}, 内容: {}, 错误: {}", receiver, target, e.getMessage(), e);
result.fail("发送消息失败:" + e.getMessage());
}
return result;
}
/**
* @description 构建审核失败邮件内容
* @param target 文章、课程名称
* @param sensitiveWords 敏感词列表
* @return 邮件正文内容
* @author yslg
* @since 2025-11-22
*/
private String buildEmailBody(String target, List<String> sensitiveWords) {
StringBuilder emailBody = new StringBuilder();
String currentTime = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date());
// 构建HTML格式的邮件内容
emailBody.append("<!DOCTYPE html>");
emailBody.append("<html><head><meta charset='UTF-8'>");
emailBody.append("<style>");
emailBody.append("body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }");
emailBody.append(".container { max-width: 600px; margin: 0 auto; padding: 20px; }");
emailBody.append(".header { background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 20px; }");
emailBody.append(".content { background-color: #fff; padding: 20px; border: 1px solid #dee2e6; border-radius: 8px; }");
emailBody.append(".alert { background-color: #f8d7da; color: #721c24; padding: 15px; border-radius: 4px; margin: 15px 0; }");
emailBody.append(".sensitive-words { background-color: #fff3cd; padding: 15px; border-radius: 4px; margin: 15px 0; }");
emailBody.append(".steps { background-color: #d1ecf1; padding: 15px; border-radius: 4px; margin: 15px 0; }");
emailBody.append(".footer { text-align: center; margin-top: 20px; color: #6c757d; font-size: 12px; }");
emailBody.append("ul { padding-left: 20px; }");
emailBody.append("li { margin: 5px 0; }");
emailBody.append("</style></head><body>");
emailBody.append("<div class='container'>");
// 头部
emailBody.append("<div class='header'>");
emailBody.append("<h2 style='margin: 0; color: #495057;'>📧 校园新闻平台 - 审核结果通知</h2>");
emailBody.append("</div>");
// 主要内容
emailBody.append("<div class='content'>");
emailBody.append("<p>尊敬的用户,您好!</p>");
emailBody.append("<div class='alert'>");
emailBody.append("<strong>⚠️ 审核未通过</strong><br>");
emailBody.append("您提交的内容「<strong>").append(target).append("</strong>」审核未通过。");
emailBody.append("</div>");
emailBody.append("<p><strong>审核失败原因:</strong>内容包含敏感词汇</p>");
// 敏感词列表
if (sensitiveWords != null && !sensitiveWords.isEmpty()) {
emailBody.append("<div class='sensitive-words'>");
emailBody.append("<strong>🔍 检测到的敏感词汇:</strong>");
emailBody.append("<ul>");
for (String word : sensitiveWords) {
emailBody.append("<li><code style='background-color: #f8f9fa; padding: 2px 4px; border-radius: 3px;'>")
.append(word).append("</code></li>");
}
emailBody.append("</ul>");
emailBody.append("</div>");
}
// 处理步骤
emailBody.append("<div class='steps'>");
emailBody.append("<strong>📋 请您按照以下步骤处理:</strong>");
emailBody.append("<ol>");
emailBody.append("<li>检查并修改上述敏感词汇</li>");
emailBody.append("<li>确保内容符合平台发布规范</li>");
emailBody.append("<li>重新提交内容进行审核</li>");
emailBody.append("</ol>");
emailBody.append("</div>");
emailBody.append("<p>如有疑问,请联系平台管理员。</p>");
emailBody.append("</div>");
// 页脚
emailBody.append("<div class='footer'>");
emailBody.append("<p>此邮件为系统自动发送,请勿回复。</p>");
emailBody.append("<p>校园新闻平台 | 发送时间:").append(currentTime).append("</p>");
emailBody.append("</div>");
emailBody.append("</div></body></html>");
return emailBody.toString();
}
}

View File

@@ -0,0 +1,46 @@
package org.xyzh.sensitive.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.xyzh.api.sensitive.SensitiveService;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageRequest;
import org.xyzh.common.dto.sensitive.TbSensitive;
@RestController
@RequestMapping("/sensitive")
public class SensitiveController {
@Autowired
private SensitiveService sensitiveService;
@PostMapping("/page")
public ResultDomain<TbSensitive> page(@RequestBody PageRequest<TbSensitive> pageRequest){
return sensitiveService.page(pageRequest);
}
@PostMapping
public ResultDomain<Boolean> addSensitiveWord(@RequestBody TbSensitive tbSensitive){
return sensitiveService.addSensitiveWord(tbSensitive);
}
@DeleteMapping
public ResultDomain<Boolean> deleteSensitiveWord(@RequestBody TbSensitive tbSensitive){
return sensitiveService.deleteSensitiveWord(tbSensitive);
}
@PutMapping
public ResultDomain<Boolean> changeSensitiveWordType(@RequestBody TbSensitive tbSensitive){
return sensitiveService.changeSensitiveWordType(tbSensitive);
}
@PostMapping("/judge")
public ResultDomain<String> judgeSensitive(@RequestBody String text){
return sensitiveService.judgeSensitive(text);
}
}

View File

@@ -3,6 +3,8 @@ package org.xyzh.sensitive.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.xyzh.common.core.page.PageParam;
import org.xyzh.common.dto.sensitive.TbSensitive;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
@@ -14,5 +16,11 @@ public interface SensitiveMapper extends BaseMapper<TbSensitive> {
public Integer addSensitiveWord(TbSensitive tbSensitive);
public Integer changeWordType(TbSensitive tbSensitive);
public Integer deleteSensitiveWord(TbSensitive tbSensitive);
public List<TbSensitive> selectTbSensitivePage(@Param("filter") TbSensitive filter, @Param("pageParam") PageParam pageParam);
public Integer countByFilter(@Param("filter") TbSensitive filter);
}

View File

@@ -4,6 +4,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.xyzh.api.sensitive.SensitiveService;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageDomain;
import org.xyzh.common.core.page.PageRequest;
import org.xyzh.common.dto.sensitive.TbSensitive;
import org.xyzh.common.redis.service.RedisService;
import org.xyzh.sensitive.constants.SensitiveRedisContants;
@@ -19,12 +21,22 @@ public class SensitiveServiceImpl implements SensitiveService{
@Autowired
private SensitiveMapper sensitiveMapper;
@Autowired
private SensitiveWordHelper sensitiveWordHelper;
@Autowired
private RedisService redisService;
@Override
public ResultDomain<TbSensitive> page(PageRequest<TbSensitive> pageRequest) {
ResultDomain<TbSensitive> resultDomain = new ResultDomain<>();
List<TbSensitive> tbSensitiveList = sensitiveMapper.selectTbSensitivePage(pageRequest.getFilter(), pageRequest.getPageParam());
int count = sensitiveMapper.countByFilter(pageRequest.getFilter());
PageDomain<TbSensitive> pageDomain = new PageDomain<>();
pageRequest.getPageParam().setTotalElements(count);
pageDomain.setPageParam(pageRequest.getPageParam());
pageDomain.setDataList(tbSensitiveList);
resultDomain.success("查询成功", pageDomain);
return resultDomain;
}
@Override
public ResultDomain<Boolean> addSensitiveWord(TbSensitive tbSensitive) {
ResultDomain<Boolean> resultDomain = new ResultDomain<>();
@@ -59,6 +71,44 @@ public class SensitiveServiceImpl implements SensitiveService{
return resultDomain;
}
@Override
public ResultDomain<Boolean> changeSensitiveWordType(TbSensitive tbSensitive) {
ResultDomain<Boolean> resultDomain = new ResultDomain<>();
String word = tbSensitive.getWord();
String newType = tbSensitive.getType();
// 目标Redis键
String targetRedisKey = "allow".equals(newType) ? SensitiveRedisContants.SENSITIVE_WORD_ALLOW : SensitiveRedisContants.SENSITIVE_WORD_DENY;
// 检查目标类型中是否已存在该词
if (redisService.sMembers(targetRedisKey).contains(word)) {
resultDomain.fail("该敏感词在目标类型中已存在");
return resultDomain;
}
// 加锁
synchronized (this) {
// 再次检查
if (redisService.sMembers(targetRedisKey).contains(word)) {
resultDomain.fail("该敏感词在目标类型中已存在");
return resultDomain;
}
// 执行数据库更新
int i = sensitiveMapper.changeWordType(tbSensitive);
if (i > 0) {
// 同步到Redis从原类型中删除添加到新类型中
String oldRedisKey = "allow".equals(newType) ? SensitiveRedisContants.SENSITIVE_WORD_DENY : SensitiveRedisContants.SENSITIVE_WORD_ALLOW;
redisService.sRemove(oldRedisKey, word);
redisService.sAdd(targetRedisKey, word);
resultDomain.success("修改敏感词类型成功", true);
} else {
resultDomain.fail("修改敏感词类型失败");
}
}
return resultDomain;
}
@Override
public ResultDomain<Boolean> deleteSensitiveWord(TbSensitive tbSensitive) {
ResultDomain<Boolean> resultDomain = new ResultDomain<>();
@@ -94,11 +144,15 @@ public class SensitiveServiceImpl implements SensitiveService{
}
@Override
public ResultDomain<Boolean> judgeSensitive(String text) {
ResultDomain<Boolean> resultDomain = new ResultDomain<>();
boolean containsSensitive = sensitiveWordHelper.contains(text);
resultDomain.success("敏感词检测完成", containsSensitive);
public ResultDomain<String> judgeSensitive(String text) {
ResultDomain<String> resultDomain = new ResultDomain<>();
List<String> sensitiveWords = SensitiveWordHelper.findAll(text);
// boolean containsSensitive = sensitiveWordHelper.contains(text);
if (sensitiveWords.size() > 0) {
resultDomain.fail(text, sensitiveWords);
}else {
resultDomain.success("不包含敏感词", text);
}
return resultDomain;
}

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.xyzh.sensitive.mapper.SensitiveMapper">
<resultMap id="BaseMap" resultType="org.xyzh.common.dto.sensitive.TbSensitive">
<resultMap id="BaseMap" type="org.xyzh.common.dto.sensitive.TbSensitive">
<id column="id" property="id"/>
<result column="word" property="word"/>
<result column="type" property="type"/>
@@ -14,20 +14,65 @@
<select id="selectAll" resultMap="BaseMap">
SELECT
<include refid="Base_Column_List"/>
FROM tb_sensitive
FROM tb_sensitive_word
</select>
<insert id="addSensitiveWord" parameterType="org.xyzh.common.dto.sensitive.TbSensitive" useGeneratedKeys="true" keyProperty="id" keyColumn="id">
INSERT INTO tb_sensitive (word, type)
INSERT INTO tb_sensitive_word (word, type)
VALUES (#{word}, #{type})
</insert>
<!-- updateSensitiveWord -->
<update id="changeWordType">
UPDATE tb_sensitive_word
SET type = #{type}
WHERE id = #{id}
</update>
<delete id="deleteSensitiveWord" parameterType="org.xyzh.common.dto.sensitive.TbSensitive">
DELETE FROM tb_sensitive
DELETE FROM tb_sensitive_word
WHERE word = #{word}
<if test="type != null and type != ''">
AND type = #{type}
</if>
</delete>
<!-- selectTbSensitivePage -->
<select id="selectTbSensitivePage" resultMap="BaseMap">
SELECT
<include refid="Base_Column_List"/>
FROM tb_sensitive_word
WHERE 1=1
<if test="filter.id != null and filter.id != ''">
AND id = #{filter.id}
</if>
<if test="filter.word != null and filter.word != ''">
AND word LIKE CONCAT('%', #{filter.word}, '%')
</if>
<if test="filter.type != null and filter.type != ''">
AND type = #{filter.type}
</if>
LIMIT #{pageParam.offset}, #{pageParam.pageSize}
</select>
<!-- countByFilter -->
<select id="countByFilter" resultType="int">
SELECT COUNT(*)
FROM tb_sensitive_word
WHERE 1=1
<if test="filter.id != null and filter.id != ''">
AND id = #{filter.id}
</if>
<if test="filter.word != null and filter.word != ''">
AND word LIKE CONCAT('%', #{filter.word}, '%')
</if>
<if test="filter.type != null and filter.type != ''">
AND type = #{filter.type}
</if>
</select>
</mapper>

View File

@@ -1,12 +0,0 @@
生日快乐
曹操
幸运
幸运儿
17年前
1条
1梯两户
1比1
年检
幸存
恶搞
游戏机

View File

@@ -1,12 +0,0 @@
fuck
duck
shit
chicken
fowl
sex
sexy
prostitute
whore
harlot
hooker
gender

View File

@@ -429,6 +429,12 @@ public class SCCourseServiceImpl implements SCCourseService {
public ResultDomain<TbCourse> updateCourseStatus(TbCourse course) {
ResultDomain<TbCourse> resultDomain = new ResultDomain<>();
if(course.getStatus()==1){ // 发布前审核
// 更新为审核中
TbCourse course2 = new TbCourse();
course2.setCourseID(course.getCourseID());
course2.setStatus(3);
course2.setUpdateTime(new Date());
courseMapper.updateCourse(course2);
// 获取所有课程节点
List<TbCourseNode> nodeList = courseNodeMapper.selectByCourseId(course.getCourseID());
List<TbCourseNode> notPassList = new ArrayList<>();
@@ -441,39 +447,52 @@ public class SCCourseServiceImpl implements SCCourseService {
ResultDomain<ResourceVO> resourceDomain = resourceService.getResourceById(node.getResourceID());
if (resourceDomain.isSuccess()) {
if (!resourceDomain.getData().getResource().getIsAudited()) {
ResultDomain<Boolean> pass = auditService.auditText(resourceDomain.getData().getResource().getContent());
if(!pass.isSuccess() || !pass.getData()){
// 审核失败,标记课程状态为3(审核失败)
course.setStatus(3);
ResultDomain<String> pass = auditService.auditText(resourceDomain.getData().getResource().getTitle()+" "+resourceDomain.getData().getResource().getContent());
if(!pass.isSuccess()){
// 审核失败,标记课程状态为4(审核失败)
course.setStatus(4);
course.setUpdateTime(new Date());
courseMapper.updateCourse(course);
auditService.sendAuditResultMessage(node.getCreator(), "课程节点:"+node.getName(), pass.getDataList());
resultDomain.fail("课程节点:"+node.getName()+"审核未通过");
return resultDomain;
}else{
// 更新文章的审核状态
ResourceVO resource = new ResourceVO();
resource.setResource(new TbResource());
resource.getResource().setResourceID(node.getResourceID());
resource.getResource().setIsAudited(true);
resourceService.updateResource(resource);
}
}
}
}else if (type == 2){
ResultDomain<Boolean> pass = auditService.auditByFileId(node.getResourceID());
if(!pass.isSuccess() || !pass.getData()){
// 审核失败,标记课程状态为3(审核失败)
course.setStatus(3);
ResultDomain<String> pass = auditService.auditByFileId(node.getResourceID());
if(!pass.isSuccess()){
// 审核失败,标记课程状态为4(审核失败)
course.setStatus(4);
course.setUpdateTime(new Date());
courseMapper.updateCourse(course);
auditService.sendAuditResultMessage(node.getCreator(), "课程节点:"+node.getName(), pass.getDataList());
resultDomain.fail("课程节点:"+node.getName()+"审核未通过");
return resultDomain;
}
}else{
ResultDomain<Boolean> pass = auditService.auditText(node.getContent());
if(!pass.isSuccess() || !pass.getData()){
ResultDomain<String> pass = auditService.auditText(node.getContent());
if(!pass.isSuccess()){
// 审核失败标记课程状态为3审核失败
course.setStatus(3);
course.setUpdateTime(new Date());
courseMapper.updateCourse(course);
auditService.sendAuditResultMessage(node.getCreator(), "课程节点:"+node.getName(), pass.getDataList());
resultDomain.fail("课程节点:"+node.getName()+"审核未通过");
return resultDomain;
}else{
// 更新node审核状态
node.setIsAudited(true);
courseNodeMapper.updateCourseNode(node);
}
}
node.setIsAudited(true);

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.xyzh.system.mapper.UserDeptRoleMapper">
<resultMap id="UserDeptRoleResultMap" type="TbSysUserDeptRole">
<resultMap id="UserDeptRoleResultMap" type="org.xyzh.common.dto.user.TbSysUserDeptRole">
<id column="id" property="id" />
<result column="user_id" property="userID" />
<result column="dept_id" property="deptID" />

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.xyzh.system.mapper.UserInfoMapper">
<resultMap id="BaseResultMap" type="TbSysUserInfo">
<resultMap id="BaseResultMap" type="org.xyzh.common.dto.user.TbSysUserInfo">
<id column="id" property="id" />
<result column="user_id" property="userID" />
<result column="avatar" property="avatar" />