diff --git a/src/main/java/com/budwk/app/base/utils/TextSimilarityUtil.java b/src/main/java/com/budwk/app/base/utils/TextSimilarityUtil.java new file mode 100644 index 00000000..aeca9c11 --- /dev/null +++ b/src/main/java/com/budwk/app/base/utils/TextSimilarityUtil.java @@ -0,0 +1,273 @@ +package com.budwk.app.base.utils; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import org.nutz.lang.util.NutMap; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * 文本相似度计算工具类 + */ +public class TextSimilarityUtil { + + /** + * 提案与关键词的相似度 + * + * @param proposal 提案数据 + * @param keywords 关键词 + * @param matchScope 匹配范围 + * @return 相似度百分比 (0-100) + */ + public static double calculateProposalSimilarity(NutMap proposal, String keywords, String[] matchScope) { + String title = proposal.getString("name", ""); + String brief = proposal.getString("brief", ""); + String measures = proposal.getString("measures", ""); + + StringBuilder fullText = new StringBuilder(); + if (ArrayUtil.isEmpty(matchScope)) { + fullText.append(title); + fullText.append(" "); + fullText.append(brief); + fullText.append(" "); + fullText.append(measures); + } else { + for (String scope : matchScope) { + String value = removeHtmlTags(proposal.getString(scope, "")); + fullText.append(value); + fullText.append(" "); + } + } + + // 计算关键词匹配度 + return calculateTextSimilarity(fullText.toString(), keywords); + } + + /** + * 计算两个文本的相似度 + * + * @param text1 文本1 + * @param text2 文本2(关键词) + * @return 相似度百分比 (0-100) + */ + public static double calculateTextSimilarity(String text1, String text2) { + if (StrUtil.isBlank(text1) || StrUtil.isBlank(text2)) { + return 0.0; + } + + text1 = text1.toLowerCase().trim(); + text2 = text2.toLowerCase().trim(); + + // 使用多种算法综合计算相似度 + double keywordSimilarity = calculateKeywordSimilarity(text1, text2); + double jaccardSimilarity = calculateJaccardSimilarity(text1, text2); + + // 加权平均 + return (keywordSimilarity * 0.7 + jaccardSimilarity * 0.3); + } + + /** + * 基于关键词匹配的相似度计算 + */ + private static double calculateKeywordSimilarity(String text1, String text2) { + // 分词处理 + String[] keywords = text2.split("[\\s\\p{Punct}]+"); + int matchCount = 0; + int totalKeywords = 0; + + for (String keyword : keywords) { + keyword = keyword.trim(); + if (!keyword.isEmpty()) { + totalKeywords++; + if (text1.contains(keyword)) { + matchCount++; + } + } + } + + if (totalKeywords == 0) { + return 0.0; + } + + // 计算匹配率 + double matchRate = (double) matchCount / totalKeywords; + + // 考虑文本长度因素,避免短文本获得过高分数 + double lengthFactor = Math.min(text2.length() / (double) Math.max(text1.length(), 1), 1.0); + lengthFactor = Math.max(lengthFactor, 0.3); // 最小权重0.3 + + return matchRate * lengthFactor * 100; + } + + /** + * Jaccard相似度计算 + * 基于集合的交集与并集比值 + */ + private static double calculateJaccardSimilarity(String text1, String text2) { + Set set1 = new HashSet<>(Arrays.asList(text1.split("[\\s\\p{Punct}]+"))); + Set set2 = new HashSet<>(Arrays.asList(text2.split("[\\s\\p{Punct}]+"))); + + // 移除空字符串 + set1.removeIf(String::isEmpty); + set2.removeIf(String::isEmpty); + + if (set1.isEmpty() && set2.isEmpty()) { + return 0.0; + } + + // 计算交集 + Set intersection = new HashSet<>(set1); + intersection.retainAll(set2); + + // 计算并集 + Set union = new HashSet<>(set1); + union.addAll(set2); + + return union.isEmpty() ? 0.0 : (double) intersection.size() / union.size() * 100; + } + + /** + * 提取相似内容片段 + * + * @param proposal 提案数据 + * @param keywords 关键词 + * @return 相似内容片段 + */ + public static String extractSimilarContent(NutMap proposal, String keywords) { + String brief = removeHtmlTags(proposal.getString("brief", "")); + String measures = removeHtmlTags(proposal.getString("measures", "")); + String title = proposal.getString("name", ""); + + // 优先从标题中查找 + if (containsKeywords(title, keywords)) { + return title; + } + + // 查找包含关键词的句子 + String[] sentences = (brief + " " + measures).split("[。!?\\.\\!\\?]"); + + for (String sentence : sentences) { + sentence = sentence.trim(); + if (!sentence.isEmpty() && containsKeywords(sentence, keywords)) { + return truncateText(sentence, 150); + } + } + + // 如果没找到,返回摘要的前150个字符 + String fallback = !brief.isEmpty() ? brief : measures; + return truncateText(fallback, 150); + } + + /** + * 检查文本是否包含关键词 + */ + public static boolean containsKeywords(String text, String keywords) { + if (StrUtil.isBlank(text) || StrUtil.isBlank(keywords)) { + return false; + } + + String[] keywordArray = keywords.toLowerCase().split("[\\s\\p{Punct}]+"); + String lowerText = text.toLowerCase(); + + for (String keyword : keywordArray) { + keyword = keyword.trim(); + if (!keyword.isEmpty() && lowerText.contains(keyword)) { + return true; + } + } + return false; + } + + /** + * 移除HTML标签 + */ + public static String removeHtmlTags(String html) { + if (StrUtil.isBlank(html)) { + return ""; + } + return html.replaceAll("<[^>]+>", "") + .replaceAll(" ", " ") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll("&", "&") + .trim(); + } + + /** + * 截断文本到指定长度 + */ + public static String truncateText(String text, int maxLength) { + if (StrUtil.isBlank(text)) { + return ""; + } + + text = text.trim(); + if (text.length() <= maxLength) { + return text; + } + + return text.substring(0, maxLength) + "..."; + } + + /** + * 高亮关键词 + * + * @param text 原文本 + * @param keywords 关键词 + * @return 高亮后的HTML文本 + */ + public static String highlightKeywords(String text, String keywords) { + if (StrUtil.isBlank(text) || StrUtil.isBlank(keywords)) { + return text; + } + + String[] keywordArray = keywords.split("[\\s\\p{Punct}]+"); + String result = text; + + for (String keyword : keywordArray) { + keyword = keyword.trim(); + if (!keyword.isEmpty()) { + // 使用正则表达式进行大小写不敏感的替换 + result = result.replaceAll("(?i)(" + escapeRegex(keyword) + ")", + "$1"); + } + } + + return result; + } + + /** + * 转义正则表达式特殊字符 + */ + private static String escapeRegex(String text) { + return text.replaceAll("([\\\\\\[\\]{}()*+?.^$|])", "\\\\$1"); + } + + /** + * 计算文本的词频 + * + * @param text 文本 + * @return 词频统计Map + */ + public static NutMap calculateWordFrequency(String text) { + NutMap frequency = new NutMap(); + + if (StrUtil.isBlank(text)) { + return frequency; + } + + String[] words = text.toLowerCase().split("[\\s\\p{Punct}]+"); + + for (String word : words) { + word = word.trim(); + if (word.length() > 1) { // 忽略单字符 + frequency.put(word, frequency.getInt(word, 0) + 1); + } + } + + return frequency; + } + +} diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalPlagiarismCheckController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalPlagiarismCheckController.java new file mode 100644 index 00000000..e2e1afa2 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalPlagiarismCheckController.java @@ -0,0 +1,124 @@ +package com.budwk.app.zhgh.democratic.proposal.controller.query; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.budwk.app.base.result.Result; +import com.budwk.app.base.utils.TextSimilarityUtil; +import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.nutz.dao.Cnd; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.util.NutMap; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; +import org.nutz.mvc.annotation.Param; + +import java.util.ArrayList; +import java.util.List; + +@IocBean +@At("/platform/proposal/query/plagiarismCheck") +@Api(tags = "提案查重") +@Ok("json:full") +public class ProposalPlagiarismCheckController { + + @Inject + private ProposalCommonService proposalCommonService; + + @At("") + @Ok("beetl:/platform/zhgh/democratic/proposal/query/plagiarismCheck/index.html") + @SaCheckPermission("proposal.query.plagiarismCheck") + public void index() { + } + + /** + * 查询 + * + * @param keywords 关键字 + * @param sessionId 会话id + * @param similarityThreshold 相似度阈值 + * @param typeId 类型id + * @return + */ + @At + @SaCheckPermission("proposal.query.plagiarismCheck") + @ApiOperation(value = "查询") + public Result query(String keywords, String sessionId, Integer similarityThreshold, @Param("matchScope") String[] matchScope, String typeId) { + // 1. 获取所有提案数据 + Sql sql = Sqls.create(""" + SELECT + info.*, + COUNT(p.consolidationIds) > 0 AS isConsolidation, + type.NAME AS typeName, + tcs.fullName AS sessionName, + tcd.`name` AS delegationName, + ins.id AS instanceId, + ins.businessNo, + ins.state instanceState, + ins.variable instanceVariable, + ins.processDefineId instanceProcessDefineId, + t.displayName curTaskName, + JSON_UNQUOTE(JSON_EXTRACT(caseTasks.variable, '$.tf_hostUnitName')) AS masterUnitName, + caseTasks.variable ->> '$.tf_helpUnitNameStr' AS slaveUnitNames + FROM + proposal_info info + LEFT JOIN proposal_consolidation p ON JSON_CONTAINS(p.consolidationIds, JSON_QUOTE(info.id)) + LEFT JOIN proposal_type type ON type.id = info.typeId + LEFT JOIN teacher_congress_session tcs ON tcs.id = info.sessionId + LEFT JOIN teacher_congress_delegation tcd ON tcd.id = info.delegationId + LEFT JOIN teacher_congress_delegate tcde ON tcde.loginName = info.createUserLoginName + LEFT JOIN wf_process_instance ins ON ins.businessNo = info.id + LEFT JOIN (SELECT processInstanceId, MAX(finishTime) AS finishTime FROM wf_process_task WHERE taskName = '9846ab38-40c5-4093-bafc-a9b3b443338b' AND taskState = 20 GROUP BY processInstanceId) latestTasks ON latestTasks.processInstanceId = ins.id + LEFT JOIN wf_process_task caseTasks ON caseTasks.processInstanceId = ins.id + AND caseTasks.taskName = '9846ab38-40c5-4093-bafc-a9b3b443338b' + AND caseTasks.taskState = 20 + AND caseTasks.finishTime = latestTasks.finishTime + LEFT JOIN wf_process_task t ON t.processInstanceId = ins.id + AND t.taskState = 10 + $condition + """); + Cnd cnd = Cnd.NEW(); + cnd.andEX("info.sessionId", "=", sessionId); + cnd.andEX("info.typeId", "=", typeId); + cnd.groupBy("info.id"); + sql.setCondition(cnd); + List list = proposalCommonService.listMap(sql); + + // 2. 进行查重分析 + List plagiarismResults = new ArrayList<>(); + + for (NutMap proposal : list) { + // 计算相似度 + double similarity = TextSimilarityUtil.calculateProposalSimilarity(proposal, keywords, matchScope); + System.out.println(similarity); + + // 只返回相似度大于阈值的结果 + if (similarity >= similarityThreshold) { + // 构建查重结果 + NutMap result = new NutMap(); + result.put("id", proposal.getString("id")); + result.put("title", proposal.getString("name")); // 提案标题 + result.put("sessionName", proposal.getString("sessionName")); + result.put("typeName", proposal.getString("typeName")); + result.put("createUserName", proposal.getString("createUserName")); + result.put("createTime", proposal.getString("createTime")); + result.put("similarity", Math.round(similarity)); // 相似度百分比 + result.put("similarContent", TextSimilarityUtil.extractSimilarContent(proposal, keywords)); + result.put("delegationName", proposal.getString("delegationName")); + result.put("unitName", proposal.getString("unitName")); + + plagiarismResults.add(result); + } + } + + // 3. 按相似度降序排序 + plagiarismResults.sort((a, b) -> Integer.compare(b.getInt("similarity"), a.getInt("similarity"))); + + return Result.success(plagiarismResults); + } + + +} diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalUnderTakeSatisfactionController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalUnderTakeSatisfactionController.java new file mode 100644 index 00000000..beb8a3a0 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalUnderTakeSatisfactionController.java @@ -0,0 +1,142 @@ +package com.budwk.app.zhgh.democratic.proposal.controller.query; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.budwk.app.base.result.Result; +import com.budwk.app.flow.entity.ProcessInstance; +import com.budwk.app.flow.entity.ProcessTask; +import com.budwk.app.flow.enums.ProcessTaskStateEnum; +import com.budwk.app.sys.models.Sys_dict; +import com.budwk.app.sys.services.SysDictService; +import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo; +import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake; +import io.swagger.annotations.Api; +import org.nutz.dao.Cnd; +import org.nutz.dao.Dao; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.json.Json; +import org.nutz.lang.util.NutMap; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@IocBean +@At("/platform/proposal/query/underTake/satisfaction") +@Ok("json:full") +@Api(tags = "提案承办单位满意度") +public class ProposalUnderTakeSatisfactionController { + + @Inject + private Dao dao; + @Inject + private SysDictService sysDictService; + + @At("") + @Ok("beetl:/platform/zhgh/democratic/proposal/query/underTakeSatisfaction/index.html") + @SaCheckPermission("proposal.query.underTake.satisfaction") + public void index() { + } + + + @At + @SaCheckPermission("proposal.query.underTake.satisfaction") + public Result data(String sessionId){ + List infos = dao.query(ProposalInfo.class, Cnd.where(ProposalInfo::getSessionId, "=", sessionId)); + List infoIds = infos.stream().map(v -> v.getId()).toList(); + List instances = dao.query(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "in", infoIds)); + List instIds = instances.stream().map(v -> v.getId()).toList(); + + // 立案任务记录 + List caseTasks = dao.query(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "in", instIds) + .and(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.FINISHED.getCode()) + .and(ProcessTask::getTaskName, "=", "9846ab38-40c5-4093-bafc-a9b3b443338b")); + + // 主办任务记录 + List masterUnitTasks = dao.query(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "in", instIds) + .and(ProcessTask::getTaskState, "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.DOING.getCode())) + .and(ProcessTask::getTaskName, "in", List.of("85cf23da-2dd5-4007-a1e9-bd9bfddc68f0", "5ed5a1be-0cd3-4b47-a0d5-238d53b61f34"))); + + // 协办任务记录 + List slaveUnitTasks = dao.query(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "in", instIds) + .and(ProcessTask::getTaskState, "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.DOING.getCode())) + .and(ProcessTask::getTaskName, "=", "c9a1586e-272b-41b8-b438-0be9f9e3781d")); + + + // 所有承办单位任务记录 + List allUnitTasks = new ArrayList<>(); + allUnitTasks.addAll(masterUnitTasks); + allUnitTasks.addAll(slaveUnitTasks); + + + //该届次所有涉及到的承办单位 + List underTakeIds = caseTasks.stream().map(v -> { + NutMap variable = Json.fromJson(NutMap.class, v.getVariable()); + String hostUnitId = variable.getString("tf_hostUnitId"); + List helpUnitIds = variable.getAsList("tf_helpUnitIds", String.class); + List list = new ArrayList<>(); + list.add(hostUnitId); + list.addAll(helpUnitIds); + return list; + }) + .flatMap(List::stream) + .distinct() + .toList(); + + + List undertakes = dao.query(ProposalUndertake.class, Cnd.where(ProposalUndertake::getId, "in", underTakeIds)); + Map undertakeMap = undertakes.stream().collect(Collectors.toMap(ProposalUndertake::getId, v -> v)); + + List tableData = new ArrayList<>(); + + // 反馈评分 + List proposalFeedback = sysDictService.getSubListByCode("PROPOSAL_FEEDBACK"); + + for (String underTakeId : underTakeIds) { + NutMap tableRow = NutMap.NEW(); + tableRow.put("id", underTakeId); + if (undertakeMap.get(underTakeId) == null) { + continue; + } + tableRow.put("name", undertakeMap.get(underTakeId).getName()); + + //找出承办提案总数 + long sum = caseTasks.stream() + .filter(v -> { + NutMap variable = Json.fromJson(NutMap.class, v.getVariable()); + if (variable == null) { + return false; + } + + String hostUnitId = variable.getString("tf_hostUnitId"); + List helpUnitIds = variable.getAsList("tf_helpUnitIds", String.class); + + boolean isHost = underTakeId != null && underTakeId.equals(hostUnitId); + boolean isHelper = helpUnitIds != null && helpUnitIds.contains(underTakeId); + + return isHost || isHelper; + }) + .map(ProcessTask::getProcessInstanceId) + .distinct() + .count(); + + // 主办提案数量 + long hostSum = masterUnitTasks.stream().map(v -> Json.fromJson(NutMap.class, v.getVariable())).filter(variable -> underTakeId.equals(variable.getString("unitId")) && variable.getBoolean("isMaster")).count(); + + // 协办提案数量 + long slaveSum = slaveUnitTasks.stream().map(v -> Json.fromJson(NutMap.class, v.getVariable())).filter(variable -> underTakeId.equals(variable.getString("unitId")) && !variable.getBoolean("isMaster")).count(); + + // 满意 +// allUnitTasks + + } + + + return Result.success(); + } + + +} diff --git a/src/main/resources/views/platform/zhgh/democratic/proposal/query/plagiarismCheck/index.html b/src/main/resources/views/platform/zhgh/democratic/proposal/query/plagiarismCheck/index.html new file mode 100644 index 00000000..87fa8137 --- /dev/null +++ b/src/main/resources/views/platform/zhgh/democratic/proposal/query/plagiarismCheck/index.html @@ -0,0 +1,571 @@ + + + + +
+
+ + +
+ 📝 提案查重检测 +
+ + + + + +
+ + 开始查重 + + + 清空重置 + +
+
+
+
+ + + +
+ ⚙️ 查重设置 +
+ + +
+ + + + + +
+
+ +
+ + +
+
+ +
+ + + + + + + +
+
+ +
+ + + + + +
+
+
+
+ + + +
+ 📊 查重结果统计 +
+ + +
+
{{ totalResults }}
+
检测到相似提案
+
+
+ +
+
{{ highSimilarityCount }}
+
高度相似 (≥80%)
+
+
+ +
+
{{ mediumSimilarityCount }}
+
中度相似 (60-79%)
+
+
+ +
+
{{ lowSimilarityCount }}
+
低度相似 (10-59%)
+
+
+
+
+ + +
+ +

正在进行查重检测,请稍候...

+
+ + +
+ +
+ +

未发现相似提案

+

您可以尝试调整查重设置或更换关键词

+
+
+ +
+ + + + + 共找到 {{ plagiarismResults.length }} 个相似提案 + + + + + 按相似度排序 + + + 按时间排序 + + + + + + + + +
+ 快速筛选: + + + 全部 ({{ totalResults }}) + + + 高度相似 ({{ highSimilarityCount }}) + + + 中度相似 ({{ mediumSimilarityCount }}) + + + 低度相似 ({{ lowSimilarityCount }}) + + +
+
+ + + +
+ + +

+ {{ result.title }} +

+
+ {{ result.sessionName }} + {{ result.typeName }} + + + 提案人: {{ result.createUserName }} | {{ result.createTime }} + +
+
+ +
+ + {{ result.similarity }}% + +
+ +
+
+ +
+ 相似内容片段: +
+
+ +
+ + 查看详情 + +
+
+
+
+
+
+ + + + +
+ + + + diff --git a/src/main/resources/views/platform/zhgh/democratic/proposal/query/underTakeSatisfaction/index.html b/src/main/resources/views/platform/zhgh/democratic/proposal/query/underTakeSatisfaction/index.html new file mode 100644 index 00000000..e149a394 --- /dev/null +++ b/src/main/resources/views/platform/zhgh/democratic/proposal/query/underTakeSatisfaction/index.html @@ -0,0 +1,10 @@ + + + + + Title + + + + +