This commit is contained in:
那些花儿
2025-09-23 16:18:41 +08:00
parent 4013fd72e6
commit b3d641d1f2
5 changed files with 1120 additions and 0 deletions
@@ -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<String> set1 = new HashSet<>(Arrays.asList(text1.split("[\\s\\p{Punct}]+")));
Set<String> 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<String> intersection = new HashSet<>(set1);
intersection.retainAll(set2);
// 计算并集
Set<String> 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("&nbsp;", " ")
.replaceAll("&lt;", "<")
.replaceAll("&gt;", ">")
.replaceAll("&amp;", "&")
.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) + ")",
"<span class=\"highlight\">$1</span>");
}
}
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;
}
}
@@ -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<NutMap> list = proposalCommonService.listMap(sql);
// 2. 进行查重分析
List<NutMap> 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);
}
}
@@ -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<ProposalInfo> infos = dao.query(ProposalInfo.class, Cnd.where(ProposalInfo::getSessionId, "=", sessionId));
List<String> infoIds = infos.stream().map(v -> v.getId()).toList();
List<ProcessInstance> instances = dao.query(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "in", infoIds));
List<Long> instIds = instances.stream().map(v -> v.getId()).toList();
// 立案任务记录
List<ProcessTask> 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<ProcessTask> 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<ProcessTask> 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<ProcessTask> allUnitTasks = new ArrayList<>();
allUnitTasks.addAll(masterUnitTasks);
allUnitTasks.addAll(slaveUnitTasks);
//该届次所有涉及到的承办单位
List<String> underTakeIds = caseTasks.stream().map(v -> {
NutMap variable = Json.fromJson(NutMap.class, v.getVariable());
String hostUnitId = variable.getString("tf_hostUnitId");
List<String> helpUnitIds = variable.getAsList("tf_helpUnitIds", String.class);
List<String> list = new ArrayList<>();
list.add(hostUnitId);
list.addAll(helpUnitIds);
return list;
})
.flatMap(List::stream)
.distinct()
.toList();
List<ProposalUndertake> undertakes = dao.query(ProposalUndertake.class, Cnd.where(ProposalUndertake::getId, "in", underTakeIds));
Map<String, ProposalUndertake> undertakeMap = undertakes.stream().collect(Collectors.toMap(ProposalUndertake::getId, v -> v));
List<NutMap> tableData = new ArrayList<>();
// 反馈评分
List<Sys_dict> 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<String> 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();
}
}