This commit is contained in:
=
2026-08-05 08:35:17 +08:00
parent ff3e8075b5
commit de5f8dc9ba
55 changed files with 1667 additions and 449 deletions
@@ -128,27 +128,13 @@ public class ProposalDashboardController {
nodes.addAll(taskNodes);
nodes.addAll(feedbackNodes);
// 查询待办任务
Sql todoSql = Sqls.create("""
SELECT
t.taskName,
t.taskState
FROM
wf_process_task t
INNER JOIN wf_process_instance ins ON ins.id = t.processInstanceId
INNER JOIN proposal_info info ON info.id = ins.businessNo
WHERE
info.sessionId = @sessionId
""");
todoSql.setParam("sessionId", sessionId);
List<NutMap> todoTasks = processDefineService.listMap(todoSql);
Map<String, NutMap> taskProposalCounts = proposalCommonService.countDashboardTaskProposals(sessionId);
for (NutMap node : nodes) {
if (node.getString("type").equals("task")) {
long todoCount = todoTasks.stream().filter(task -> task.getInt("taskState") == ProcessTaskStateEnum.DOING.getCode() && task.getString("taskName").equals(node.getString("id"))).count();
node.put("todoCount", todoCount);
long doneCount = todoTasks.stream().filter(task -> task.getInt("taskState") == ProcessTaskStateEnum.FINISHED.getCode() && task.getString("taskName").equals(node.getString("id"))).count();
node.put("doneCount", doneCount);
NutMap taskCount = taskProposalCounts.get(node.getString("id"));
node.put("todoCount", taskCount == null ? 0 : taskCount.getLong("todoCount", 0L));
node.put("doneCount", taskCount == null ? 0 : taskCount.getLong("doneCount", 0L));
} else if (node.getString("type").equals("total")) {
int count = dao.count(ProposalInfo.class, Cnd.where(ProposalInfo::getSessionId, "=", sessionId));
node.put("count", count);
@@ -1,22 +1,13 @@
package com.budwk.app.zhgh.democratic.proposal.controller.query;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.ArrayUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalQueryComprehensiveParam;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
@@ -27,7 +18,6 @@ import org.nutz.mvc.annotation.Param;
import javax.validation.Valid;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@IocBean
@@ -37,20 +27,6 @@ import java.util.Map;
@Api(tags = "征集进度查询")
public class ProposalQueryCollectProgressController {
/**
* 征集进度表格允许排序的字段映射,值为查询中的真实字段或安全别名。
*/
private static final Map<String, String> COLLECT_PROGRESS_ORDER_COLUMNS = Map.ofEntries(
Map.entry("code", "info.code"),
Map.entry("name", "info.name"),
Map.entry("createUserName", "info.createUserName"),
Map.entry("delegationName", "delegationName"),
Map.entry("inviteCount", "inviteCount"),
Map.entry("finishCount", "finishCount"),
Map.entry("curTaskName", "curTaskName"),
Map.entry("instanceState", "instanceState")
);
static List<NutMap> states = new ArrayList<>() {{
add(NutMap.NEW().addv("code", 10).addv("id", 10).addv("name", "撰写提案"));
add(NutMap.NEW().addv("code", 20).addv("id", 20).addv("name", "附议提案"));
@@ -60,9 +36,6 @@ public class ProposalQueryCollectProgressController {
@Inject
private ProposalCommonService proposalCommonService;
@Inject
private Dao dao;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/query/collectProgress/index.html")
@@ -74,67 +47,7 @@ public class ProposalQueryCollectProgressController {
@SaCheckPermission("proposal.query.collectProgress")
@ApiOperation(value = "分页列表")
public Result pageData(@Valid @Param("pageForm") ProposalQueryComprehensiveParam pageForm) {
Sql sql = Sqls.create("""
SELECT
info.id,
info.name,
info.code,
info.createUserName,
type.NAME AS typeName,
tcs.fullName AS sessionName,
tcd.`name` AS delegationName,
(SELECT count(1) FROM proposal_second WHERE proposalId = info.id) AS inviteCount,
(SELECT count(1) FROM wf_process_task WHERE processInstanceId = ins.id AND taskName = 'second' AND taskState = 20) finishCount,
ins.id AS instanceId,
ins.state instanceState,
t.displayName curTaskName
FROM
proposal_info info
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 wf_process_task t ON t.processInstanceId = ins.id AND t.taskState = 10
$condition
""");
Cnd cnd = Cnd.NEW();
// 征集进度必须按页面选中的教代会过滤,避免共享参数未处理sessionId导致跨届次查询。
cnd.andEX("info.sessionId", "=", pageForm.getSessionId());
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
SqlExpressionGroup seg = new SqlExpressionGroup();
if (ArrayUtil.contains(pageForm.getCollectIds(), 10)) {
seg.or("t.taskName", "=", "startTask");
}
if (ArrayUtil.contains(pageForm.getCollectIds(), 20)) {
seg.or("t.taskName", "=", "second");
}
if (ArrayUtil.contains(pageForm.getCollectIds(), 30)) {
seg.or("t.taskName", "=", "delegation");
}
if (ArrayUtil.contains(pageForm.getCollectIds(), 40)) {
seg.or("t.taskName", "not in", List.of("startTask", "second", "delegation"));
seg.and("t.taskName", "is not", null);
seg.or("ins.state","=", ProcessInstanceStateEnum.FINISHED.getCode());
}
if (!seg.isEmpty()) {
cnd.and(seg);
}
cnd.groupBy("info.id");
// 征集进度包含关联字段和人数统计别名,只允许白名单字段进入排序条件。
proposalCommonService.applySafePageOrder(cnd, pageForm, COLLECT_PROGRESS_ORDER_COLUMNS);
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
List<NutMap> list = (List<NutMap>) pagination.getList();
// for (NutMap row : list) {
// int count = dao.count(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "=", row.getString("instanceId"))
// .and(ProcessTask::getTaskName, "=", "second")
// .and(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.FINISHED.getCode())
// );
//
//
// }
Pagination pagination = proposalCommonService.queryCollectProgress(pageForm);
return Result.success(pagination);
}
@@ -77,6 +77,10 @@ public class ProposalQueryDelegationController {
@ApiOperation("代表团提案统计")
@SaCheckPermission("proposal.query.delegation")
public Result pageData(PageForm pageForm, String sessionId) {
// 统计必须明确限定届次,空参数不能退化为跨届次汇总,避免页面异步初始化时展示错误数据。
if (StrUtil.isBlank(sessionId)) {
return Result.success(List.of());
}
List<Sys_dict> dictList = sysDictService.getSubListByCode("PROPOSAL_CASE_FILING_RESULT");
Sql sql = Sqls.create("""
SELECT
@@ -90,19 +94,17 @@ public class ProposalQueryDelegationController {
FROM
teacher_congress_delegation dbt
$condition
""");
""");
Map<String, String> summaryOrderColumns = new HashMap<>(DELEGATION_SUMMARY_ORDER_COLUMNS);
List<String> resultSqlParts = new ArrayList<>();
int resultIndex = 0;
for (Sys_dict dict : dictList) {
String resultCode = dict.getCode();
// 动态别名仅允许字母、数字和下划线,查询值使用参数绑定,避免字典内容进入SQL结构。
// 动态别名和立案结果编码仅允许字母、数字和下划线;变量替换后新增的 @ 参数不会被 Nutz 再次解析,
// 因此这里使用已校验的字典编码字面量,确保统计条件能正确传递给数据库。
if (StrUtil.isBlank(resultCode) || !resultCode.matches("[A-Za-z0-9_]+")) {
continue;
}
String paramName = "caseFilingResult" + resultIndex++;
resultSqlParts.add(", (select count(1) from proposal_info info where info.delegationId = dbt.id and info.sessionId = dbt.sessionId and info.caseFilingResult = @" + paramName + ") AS `" + resultCode + "`");
sql.setParam(paramName, resultCode);
resultSqlParts.add(", (select count(1) from proposal_info info where info.delegationId = dbt.id and info.sessionId = dbt.sessionId and info.caseFilingResult = '" + resultCode + "') AS `" + resultCode + "`");
summaryOrderColumns.put(resultCode, "`" + resultCode + "`");
}
sql.setVar("resultSql", String.join("", resultSqlParts));
@@ -97,7 +97,7 @@ public class ProposalSeniorBasicController {
return Result.error("提案编号已存在");
}
dao.update(proposalInfo,"^code|name|typeId|brief|measures|excerpt|suggestUnits|files$");
dao.update(proposalInfo,"^code|caseFilingCode|name|typeId|brief|measures|excerpt|suggestUnits|files$");
return Result.success();
}
@@ -103,7 +103,7 @@ public class ProposalMineController {
@At("/h5")
@Ok("beetl:/platform/zhghh5/democratic/proposal/transact/mine/index.html")
@SaCheckPermission("proposal.mine")
@SaCheckPermission("h5.proposal.mine")
public void h5Index() {
}
@@ -111,6 +111,18 @@ public class ProposalMineController {
@SaCheckPermission("proposal.mine")
@Aop(TransAop.READ_COMMITTED)
public Result pageData(@Valid ProposalSearchParam pageForm, String sessionId) {
return queryPageData(pageForm, sessionId);
}
/**
* 查询当前登录人的提案分页数据。
* PC 与 H5 入口共享同一数据范围和排序规则,避免两个端的列表结果不一致。
*
* @param pageForm 页面筛选及分页参数
* @param sessionId 教代会届次ID
* @return 当前登录人的提案分页结果
*/
private Result queryPageData(ProposalSearchParam pageForm, String sessionId) {
Sql sql = Sqls.create("""
SELECT
info.*,
@@ -158,6 +170,20 @@ public class ProposalMineController {
return Result.success(pagination);
}
/**
* 我的提案 H5 分页列表。
* H5 使用独立权限标识,但查询口径与 PC 端保持一致。
*
* @param pageForm 页面筛选及分页参数
* @param sessionId 教代会届次ID
* @return 当前登录人的提案分页结果
*/
@At("/h5/pageData")
@SaCheckPermission("h5.proposal.mine")
public Result h5PageData(@Valid ProposalSearchParam pageForm, String sessionId) {
return queryPageData(pageForm, sessionId);
}
@At
@SaCheckPermission("proposal.mine")
@Aop(TransAop.READ_COMMITTED)
@@ -84,13 +84,25 @@ public class ProposalSecondedController {
@At("/h5")
@Ok("beetl:/platform/zhghh5/democratic/proposal/transact/seconded/index.html")
@SaCheckPermission("proposal.seconded")
@SaCheckPermission("h5.proposal.seconded")
public void h5Index() {
}
@At
@SaCheckPermission("proposal.seconded")
public Result pageData(@Valid ProposalSearchParam pageForm, boolean approval) {
return queryPageData(pageForm, approval);
}
/**
* 查询当前登录附议人的提案分页数据。
* PC 与 H5 入口共享同一待附议、已附议筛选口径。
*
* @param pageForm 页面筛选及分页参数
* @param approval 是否查询已附议记录
* @return 附议提案分页结果
*/
private Result queryPageData(ProposalSearchParam pageForm, boolean approval) {
Sql sql = Sqls.create("""
SELECT
info.*,
@@ -165,6 +177,20 @@ public class ProposalSecondedController {
return Result.success(pagination);
}
/**
* 提案附议 H5 分页列表。
* H5 使用独立权限标识,但查询口径与 PC 端保持一致。
*
* @param pageForm 页面筛选及分页参数
* @param approval 是否查询已附议记录
* @return 附议提案分页结果
*/
@At("/h5/pageData")
@SaCheckPermission("h5.proposal.seconded")
public Result h5PageData(@Valid ProposalSearchParam pageForm, boolean approval) {
return queryPageData(pageForm, approval);
}
@At
@SaCheckPermission("proposal.seconded")
@Aop(TransAop.READ_COMMITTED)
@@ -5,7 +5,9 @@ import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalQueryComprehensiveParam;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSearchParam;
import com.budwk.app.base.page.Pagination;
import org.nutz.dao.Cnd;
import org.nutz.lang.util.NutMap;
@@ -171,4 +173,21 @@ public interface ProposalCommonService extends BaseService<ProposalInfo> {
* 修改提案状态码
*/
void updateStateCode(String id, Integer stateCode);
/**
* 查询提案征集进度列表。
* 查询结果与页面既有筛选、排序语义保持一致,内部使用聚合关联避免逐行执行统计子查询。
*
* @param pageForm 征集进度页面的筛选、排序和分页参数
* @return 包含征集进度统计字段的分页结果
*/
Pagination queryCollectProgress(ProposalQueryComprehensiveParam pageForm);
/**
* 按提案去重统计指定届次各流程节点的待办和已办数量。
*
* @param sessionId 教代会届次ID
* @return 键为流程节点编码、值为待办和已办数量的统计结果
*/
Map<String, NutMap> countDashboardTaskProposals(String sessionId);
}
@@ -3,6 +3,7 @@ package com.budwk.app.zhgh.democratic.proposal.service.common;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
@@ -15,6 +16,7 @@ import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.PageUtil;
@@ -56,6 +58,7 @@ import org.nutz.dao.FieldFilter;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.Daos;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.dao.util.cri.Static;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
@@ -85,6 +88,18 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
/** 办理页面允许导出的列表字段,避免请求参数携带非页面字段。 */
private static final Map<String, Set<String>> WORKFLOW_EXPORT_COLUMNS = createWorkflowExportColumns();
/** 征集进度页面允许排序的字段,避免排序参数直接拼接到SQL。 */
private static final Map<String, String> COLLECT_PROGRESS_ORDER_COLUMNS = Map.ofEntries(
Map.entry("code", "info.code"),
Map.entry("name", "info.name"),
Map.entry("createUserName", "info.createUserName"),
Map.entry("delegationName", "delegationName"),
Map.entry("inviteCount", "inviteCount"),
Map.entry("finishCount", "finishCount"),
Map.entry("curTaskName", "curTaskName"),
Map.entry("instanceState", "instanceState")
);
//找出富文本里面上传的图片
static String IMG_SRC_REGEX = "<img\\s+[^>]*src\\s*=\\s*([\"'])(/platform/sys/file/download\\?id=.*?)\\1[^>]*>";
@@ -107,6 +122,128 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
super(dao);
}
/**
* 分页查询提案征集进度。
* 附议人数和已附议人数使用分组结果关联,避免原查询对每一条提案重复执行统计子查询;
* 总数查询只保留筛选所需关联,避免分页统计重复执行列表中的统计计算。
*
* @param pageForm 页面筛选、排序和分页参数
* @return 征集进度分页数据
*/
@Override
public Pagination queryCollectProgress(ProposalQueryComprehensiveParam pageForm) {
Sql sql = Sqls.create("""
SELECT
info.id,
info.name,
info.code,
info.createUserName,
type.NAME AS typeName,
tcs.fullName AS sessionName,
tcd.`name` AS delegationName,
COALESCE(secondStat.inviteCount, 0) AS inviteCount,
COALESCE(taskStat.finishCount, 0) AS finishCount,
ins.id AS instanceId,
ins.state AS instanceState,
t.displayName AS curTaskName
FROM proposal_info info
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 wf_process_task t ON t.processInstanceId = ins.id AND t.taskState = 10
LEFT JOIN (
SELECT proposalId, COUNT(1) AS inviteCount
FROM proposal_second
GROUP BY proposalId
) secondStat ON secondStat.proposalId = info.id
LEFT JOIN (
SELECT processInstanceId, COUNT(1) AS finishCount
FROM wf_process_task
WHERE taskName = 'second' AND taskState = 20
GROUP BY processInstanceId
) taskStat ON taskStat.processInstanceId = ins.id
$condition
""");
Cnd dataCnd = buildCollectProgressCondition(pageForm);
dataCnd.groupBy("info.id");
applySafePageOrder(dataCnd, pageForm, COLLECT_PROGRESS_ORDER_COLUMNS);
sql.setCondition(dataCnd);
Sql countSql = Sqls.create("""
SELECT COUNT(DISTINCT info.id)
FROM proposal_info info
LEFT JOIN teacher_congress_delegate tcde ON tcde.loginName = info.createUserLoginName
LEFT JOIN wf_process_instance ins ON ins.businessNo = info.id
LEFT JOIN wf_process_task t ON t.processInstanceId = ins.id AND t.taskState = 10
$condition
""");
countSql.setCondition(buildCollectProgressCondition(pageForm));
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql, countSql);
}
/**
* 按提案ID去重统计数据看板的流程节点数量。
* 同一提案因撤回、退回等再次经过同一节点时,任务表会保留多条已办记录;
* 此处按提案统计,使看板数量与下方按提案展示的列表保持一致。
*
* @param sessionId 教代会届次ID
* @return 以流程节点编码为键的待办、已办统计结果
*/
@Override
public Map<String, NutMap> countDashboardTaskProposals(String sessionId) {
Sql sql = Sqls.create("""
SELECT
t.taskName,
COUNT(DISTINCT CASE WHEN t.taskState = @doingState THEN info.id END) AS todoCount,
COUNT(DISTINCT CASE WHEN t.taskState = @finishedState THEN info.id END) AS doneCount
FROM wf_process_task t
INNER JOIN wf_process_instance ins ON ins.id = t.processInstanceId
INNER JOIN proposal_info info ON info.id = ins.businessNo
WHERE info.sessionId = @sessionId
GROUP BY t.taskName
""");
sql.setParam("sessionId", sessionId);
sql.setParam("doingState", ProcessTaskStateEnum.DOING.getCode());
sql.setParam("finishedState", ProcessTaskStateEnum.FINISHED.getCode());
List<NutMap> taskCounts = listMap(sql);
return taskCounts.stream().collect(Collectors.toMap(task -> task.getString("taskName"), task -> task));
}
/**
* 构建征集进度列表和总数查询共用的筛选条件,确保两次查询返回同一数据范围。
* 征集阶段条件保持页面原有的布尔组合方式,避免优化时改变既有查询结果。
*
* @param pageForm 页面筛选参数
* @return 不含分组和排序的查询条件
*/
private Cnd buildCollectProgressCondition(ProposalQueryComprehensiveParam pageForm) {
Cnd cnd = Cnd.NEW();
cnd.andEX("info.sessionId", "=", pageForm.getSessionId());
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
SqlExpressionGroup collectStageCondition = new SqlExpressionGroup();
if (ArrayUtil.contains(pageForm.getCollectIds(), 10)) {
collectStageCondition.or("t.taskName", "=", "startTask");
}
if (ArrayUtil.contains(pageForm.getCollectIds(), 20)) {
collectStageCondition.or("t.taskName", "=", "second");
}
if (ArrayUtil.contains(pageForm.getCollectIds(), 30)) {
collectStageCondition.or("t.taskName", "=", "delegation");
}
if (ArrayUtil.contains(pageForm.getCollectIds(), 40)) {
collectStageCondition.or("t.taskName", "not in", List.of("startTask", "second", "delegation"));
collectStageCondition.and("t.taskName", "is not", null);
collectStageCondition.or("ins.state", "=", ProcessInstanceStateEnum.FINISHED.getCode());
}
if (!collectStageCondition.isEmpty()) {
cnd.and(collectStageCondition);
}
return cnd;
}
/**
* 使用业务页面提供的字段白名单构建排序条件,非法字段或非法排序方向将被忽略。
*
@@ -121,8 +258,14 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
return false;
}
// H5 列表首次加载时可能不传排序字段;不可变 Map 不接受 null 键,直接保留页面默认排序。
String pageOrderName = pageForm.getPageOrderName();
if (StrUtil.isBlank(pageOrderName)) {
return false;
}
// 仅允许白名单中的字段参与排序,防止构造任意SQL排序字段。
String orderColumn = allowedOrderColumns.get(pageForm.getPageOrderName());
String orderColumn = allowedOrderColumns.get(pageOrderName);
String orderBy = PageUtil.getOrder(pageForm.getPageOrderBy());
if (StrUtil.isBlank(orderColumn) || StrUtil.isBlank(orderBy)) {
return false;
@@ -814,35 +957,35 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
// 获取流程实例
ProcessInstance processInstance = dao().fetch(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "=", id));
if (processInstance != null) {
// 主办办理情况
ProcessTask masterUnitReplyTask = dao().fetch(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "=", processInstance.getId())
.and(ProcessTask::getTaskName, "in", List.of("master_reply", "opinion_master_reply"))
.and(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.FINISHED.getCode())
.desc(ProcessTask::getFinishTime)
);
if (masterUnitReplyTask != null) {
// 办理解决情况
String implementState = FlowUtil.variableToDict(masterUnitReplyTask.getVariable()).getStr("tf_implementState");
info.put(implementState, "");
// 承办单位名称
info.put("underTakeName", FlowUtil.variableToDict(masterUnitReplyTask.getVariable()).getStr("underTakeName"));
}
// 主办办理情况
ProcessTask masterUnitReplyTask = dao().fetch(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "=", processInstance.getId())
.and(ProcessTask::getTaskName, "in", List.of("master_reply", "opinion_master_reply"))
.and(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.FINISHED.getCode())
.desc(ProcessTask::getFinishTime)
);
if (masterUnitReplyTask != null) {
// 办理解决情况
String implementState = FlowUtil.variableToDict(masterUnitReplyTask.getVariable()).getStr("tf_implementState");
info.put(implementState, "");
// 承办单位名称
info.put("underTakeName", FlowUtil.variableToDict(masterUnitReplyTask.getVariable()).getStr("underTakeName"));
}
// 反馈评价
ProcessTask feedbackTask = dao().fetch(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "=", processInstance.getId())
.and(ProcessTask::getTaskName, "=", "feedback")
.and(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.FINISHED.getCode())
.desc(ProcessTask::getFinishTime)
);
if (feedbackTask != null) {
info.put(FlowUtil.variableToDict(feedbackTask.getVariable()).getStr("tf_feedback"), "");
info.put("tf_feedback", FlowUtil.variableToDict(feedbackTask.getVariable()).getStr("tf_feedback"));
info.put("tf_opinion", FlowUtil.variableToDict(feedbackTask.getVariable()).getStr("tf_opinion"));
info.put("tf_phone", FlowUtil.variableToDict(feedbackTask.getVariable()).getStr("tf_phone"));
info.put("tf_address", FlowUtil.variableToDict(feedbackTask.getVariable()).getStr("tf_address"));
info.put("tf_postcode", FlowUtil.variableToDict(feedbackTask.getVariable()).getStr("tf_postcode"));
// 反馈评价
ProcessTask feedbackTask = dao().fetch(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "=", processInstance.getId())
.and(ProcessTask::getTaskName, "=", "feedback")
.and(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.FINISHED.getCode())
.desc(ProcessTask::getFinishTime)
);
if (feedbackTask != null) {
info.put(FlowUtil.variableToDict(feedbackTask.getVariable()).getStr("tf_feedback"), "");
info.put("tf_feedback", FlowUtil.variableToDict(feedbackTask.getVariable()).getStr("tf_feedback"));
info.put("tf_opinion", FlowUtil.variableToDict(feedbackTask.getVariable()).getStr("tf_opinion"));
info.put("tf_phone", FlowUtil.variableToDict(feedbackTask.getVariable()).getStr("tf_phone"));
info.put("tf_address", FlowUtil.variableToDict(feedbackTask.getVariable()).getStr("tf_address"));
info.put("tf_postcode", FlowUtil.variableToDict(feedbackTask.getVariable()).getStr("tf_postcode"));
}
}
HtmlRenderPolicy htmlRenderPolicy = new HtmlRenderPolicy();
@@ -82,12 +82,12 @@ public class ProposalCommitteeFilingUnitServiceImpl extends BaseServiceImpl<Prop
throw new BaseException("提案信息不存在");
}
args.put("proposalId", proposalId);
validateAndNormalizeCaseFilingCode(args);
List<String> proposalIds = proposalCommonService.mergeProposal(proposalId);
if (ObjectUtil.isEmpty(proposalIds)) {
proposalIds = List.of(proposalId);
}
validateAndNormalizeCaseFilingCode(args, proposalIds);
/*
* 先同步 proposal_info 和承办单位再执行 WF 流转使查看页面及下一节点读取到的都是本次最新数据
@@ -111,12 +111,14 @@ public class ProposalCommitteeFilingUnitServiceImpl extends BaseServiceImpl<Prop
}
/**
* 校验并规范化立案编号确定立案时必须传入不超过 20 个字符的编号
* 校验并规范化立案编号确定立案时必须传入不超过 20 个字符且不与历史立案编号重复的编号
* 当前提案及并案提案不参与重复校验保证流程回退后可按原编号重新提交
* 其他立案结果不保存编号避免切换选项后把历史输入带入业务表和流程变量
*
* @param args 流程提交参数其中 tf_caseFilingResult 为立案结果tf_caseFilingCode 为手工填写的立案编号
* @param excludedProposalIds 当前办理的提案及并案提案主键不参与历史重复判断
*/
private void validateAndNormalizeCaseFilingCode(Dict args) {
private void validateAndNormalizeCaseFilingCode(Dict args, List<String> excludedProposalIds) {
String caseFilingResultKey = FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingResult";
String caseFilingCodeKey = FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingCode";
String caseFilingTypeKey = FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingType";
@@ -133,6 +135,13 @@ public class ProposalCommitteeFilingUnitServiceImpl extends BaseServiceImpl<Prop
if (caseFilingCode.length() > 20) {
throw new BaseException("立案编号不能超过20个字符");
}
Cnd duplicateCondition = Cnd.where("caseFilingCode", "=", caseFilingCode);
if (!ObjectUtil.isEmpty(excludedProposalIds)) {
duplicateCondition.and("id", "not in", excludedProposalIds);
}
if (dao().count(ProposalInfo.class, duplicateCondition) > 0) {
throw new BaseException("立案编号已存在,请重新填写");
}
args.put(caseFilingCodeKey, caseFilingCode);
}
@@ -654,9 +654,12 @@ public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> imp
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 proposal_reply_unit pru ON pru.proposalId = info.id
$condition
""");
Cnd cnd = Cnd.NEW();
// 综合导出页面按当前选择的届次导出避免公共查询参数的跨届次场景影响导出范围
cnd.andEX("info.sessionId", "=", pageForm.getSessionId());
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
cnd.groupBy("info.id");
cnd.asc("info.code");
@@ -687,46 +690,45 @@ public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> imp
return StrUtil.blankToDefault(name, "unnamed").replaceAll("[\\\\/:*?\"<>|\\r\\n]+", "_");
}
/**
* 生成 ZIP 内唯一且合法的 Word 文件名避免提案名称中的非法字符或重名条目导致整个压缩包缺文件
*
* @param prefix 文档类型前缀
* @param proposalRow 提案基础信息
* @param entryNameCounts 已生成的基础文件名及其出现次数
* @return 可以直接用于 ZipEntry 的文件名
*/
private String createUniqueZipDocxEntryName(String prefix, NutMap proposalRow, Map<String, Integer> entryNameCounts) {
String baseName = sanitizeZipFileName(prefix + "-" + proposalRow.getString("code", "未编号") + "-" + proposalRow.getString("name", "未命名提案"));
int sameNameCount = entryNameCounts.merge(baseName, 1, Integer::sum);
return baseName + (sameNameCount > 1 ? "-" + sameNameCount : "") + ".docx";
}
@Override
public void exportFeedBackAsZip(ProposalQueryComprehensiveParam pageForm, HttpServletResponse response) {
Sql sql = Sqls.create("""
SELECT
info.id,
info.code,
info.name
FROM
proposal_info info
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
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.groupBy("info.id");
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
sql.setCondition(cnd);
List<NutMap> list = listMap(sql);
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ZipOutputStream zipOutputStream = new ZipOutputStream(bos);
List<NutMap> list = queryProposalBaseList(pageForm);
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
ZipOutputStream zipOutputStream = new ZipOutputStream(bos)) {
Map<String, Integer> entryNameCounts = new HashMap<>();
for (NutMap proposalRow : list) {
try (ByteArrayOutputStream docxByteArrayOutputStream = new ByteArrayOutputStream()) {
proposalCommonService.exportProposalFeedBackAsDocx(proposalRow.getString("id"), docxByteArrayOutputStream);
ZipEntry zipEntry = new ZipEntry("反馈表-" + proposalRow.getString("code") + "-" + proposalRow.getString("name") + ".docx");
if (docxByteArrayOutputStream.size() == 0) {
throw new BaseException("提案【{}】反馈表生成失败", proposalRow.getString("code"));
}
ZipEntry zipEntry = new ZipEntry(createUniqueZipDocxEntryName("反馈表", proposalRow, entryNameCounts));
zipOutputStream.putNextEntry(zipEntry);
docxByteArrayOutputStream.writeTo(zipOutputStream);
zipOutputStream.closeEntry();
} catch (IOException e) {
// 处理单个文件生成失败的情况
e.printStackTrace();
throw new BaseException("提案【{}】反馈表写入压缩包失败", proposalRow.getString("code"));
}
}
zipOutputStream.close();
zipOutputStream.finish();
CommonDownloadUtil.download("提案反馈表压缩包.zip", bos.toByteArray(), response);
} catch (Exception e) {
e.printStackTrace();
log.error("导出提案反馈表压缩包失败", e);
throw new BaseException("导出提案反馈表压缩包失败:{}", e.getMessage());
}
}
@@ -834,43 +836,29 @@ public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> imp
@Override
public void exportCollectZip(ProposalQueryComprehensiveParam pageForm, HttpServletResponse response) {
Sql sql = Sqls.create("""
SELECT
info.id,
info.code,
info.name
FROM
proposal_info info
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
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.groupBy("info.id");
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
sql.setCondition(cnd);
List<NutMap> list = listMap(sql);
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ZipOutputStream zipOutputStream = new ZipOutputStream(bos);
List<NutMap> list = queryProposalBaseList(pageForm);
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
ZipOutputStream zipOutputStream = new ZipOutputStream(bos)) {
Map<String, Integer> entryNameCounts = new HashMap<>();
for (NutMap proposalRow : list) {
try (ByteArrayOutputStream docxByteArrayOutputStream = new ByteArrayOutputStream()) {
exportCollectDocx(proposalRow.getString("id"), docxByteArrayOutputStream);
ZipEntry zipEntry = new ZipEntry("征集表-" + proposalRow.getString("code") + "-" + proposalRow.getString("name") + ".docx");
if (docxByteArrayOutputStream.size() == 0) {
throw new BaseException("提案【{}】征集表生成失败", proposalRow.getString("code"));
}
ZipEntry zipEntry = new ZipEntry(createUniqueZipDocxEntryName("征集表", proposalRow, entryNameCounts));
zipOutputStream.putNextEntry(zipEntry);
docxByteArrayOutputStream.writeTo(zipOutputStream);
zipOutputStream.closeEntry();
} catch (IOException e) {
// 处理单个文件生成失败的情况
e.printStackTrace();
throw new BaseException("提案【{}】征集表写入压缩包失败", proposalRow.getString("code"));
}
}
zipOutputStream.close();
zipOutputStream.finish();
CommonDownloadUtil.download("提案征集表压缩包.zip", bos.toByteArray(), response);
} catch (Exception e) {
e.printStackTrace();
log.error("导出提案征集表压缩包失败", e);
throw new BaseException("导出提案征集表压缩包失败:{}", e.getMessage());
}
}
@@ -17,6 +17,8 @@ import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import java.util.List;
@IocBean(args = {"refer:dao"})
public class TeacherCongressInstitutionUserServiceImpl extends BaseServiceImpl<Teacher_congress_institution_user> implements TeacherCongressInstitutionUserService {
@Inject
@@ -49,21 +51,9 @@ public class TeacherCongressInstitutionUserServiceImpl extends BaseServiceImpl<T
dao().insert(institutionUser);
Teacher_congress_institution institution = dao().fetch(Teacher_congress_institution.class, institutionId);
if (institution.getCode().contains("TEACHER_CONGRESS_INSTITUTION_PROPOSAL_COMMITTEE") && identity.equals("主任")) {
Sys_role sysRole = sysRoleService.getByCode(RoleConstant.PROPOSAL_COMMITTEE_DIRECTOR);
int existsRole = dao().count(Sys_user_role.class, Cnd.where(Sys_user_role::getUserId, "=", userId)
.and(Sys_user_role::getTcSessionId, "=", sessionId)
.and(Sys_user_role::getRoleId, "=", sysRole.getId())
);
if (existsRole == 0) {
Sys_user_role insertSysUserRole = new Sys_user_role();
insertSysUserRole.setRoleId(sysRole.getId());
insertSysUserRole.setUserId(userId);
insertSysUserRole.setTcSessionId(sessionId);
dao().insert(insertSysUserRole);
sysRoleService.clearCache();
sysUserService.clearCache();
}
// 提案审查委员会主任按届次同步提案委员会主任角色兼容历史届次的旧机构编码
if (isProposalReviewCommittee(institution) && identity.equals("主任")) {
grantProposalCommitteeDirectorRole(userId, sessionId);
}
if (institution.getCode().contains("TEACHER_CONGRESS_INSTITUTION_PROPOSAL_COMMITTEE") && identity.equals("副主任")) {
Sys_role sysRole = sysRoleService.getByCode(RoleConstant.PROPOSAL_COMMITTEE_DEPUTY_DIRECTOR);
@@ -135,15 +125,8 @@ public class TeacherCongressInstitutionUserServiceImpl extends BaseServiceImpl<T
Teacher_congress_institution_user institutionUser = fetch(id);
String institutionId = institutionUser.getInstitutionId();
Teacher_congress_institution institution = dao().fetch(Teacher_congress_institution.class, institutionId);
if (institution.getName().contains("提案工作委员会") && institutionUser.getIdentity().equals("主任")) {
Sys_role sysRole = sysRoleService.getByCode(RoleConstant.PROPOSAL_COMMITTEE_DIRECTOR);
dao().clear(Sys_user_role.class, Cnd.where(Sys_user_role::getUserId, "=", institutionUser.getUserId())
.and(Sys_user_role::getTcSessionId, "=", institutionUser.getSessionId())
.and(Sys_user_role::getRoleId, "=", sysRole.getId())
);
sysRoleService.clearCache();
sysUserService.clearCache();
}
boolean proposalReviewCommitteeDirector = isProposalReviewCommittee(institution)
&& institutionUser.getIdentity().equals("主任");
if (institution.getName().contains("提案工作委员会") && institutionUser.getIdentity().equals("副主任")) {
Sys_role sysRole = sysRoleService.getByCode(RoleConstant.PROPOSAL_COMMITTEE_DEPUTY_DIRECTOR);
dao().clear(Sys_user_role.class, Cnd.where(Sys_user_role::getUserId, "=", institutionUser.getUserId())
@@ -173,5 +156,78 @@ public class TeacherCongressInstitutionUserServiceImpl extends BaseServiceImpl<T
sysUserService.clearCache();
}
dao().delete(institutionUser);
// 删除主任后仅在同届次不存在其他提案审查委员会主任时撤销系统角色
if (proposalReviewCommitteeDirector) {
revokeProposalCommitteeDirectorRoleIfUnused(institutionUser.getUserId(), institutionUser.getSessionId());
}
}
/**
* 判断机构是否为提案审查委员会ZXT001 为当前机构配置编码旧编码用于兼容历史届次
*
* @param institution 教代会机构
* @return 是否属于提案审查委员会
*/
private boolean isProposalReviewCommittee(Teacher_congress_institution institution) {
if (institution == null) {
return false;
}
return "ZXT001".equals(institution.getCode())
|| "TEACHER_CONGRESS_INSTITUTION_PROPOSAL_COMMITTEE".equals(institution.getCode());
}
/**
* 为提案审查委员会主任授予当前届次的提案委员会主任角色已有角色不重复写入
*
* @param userId 用户主键
* @param sessionId 教代会届次主键
*/
private void grantProposalCommitteeDirectorRole(String userId, String sessionId) {
Sys_role sysRole = sysRoleService.getByCode(RoleConstant.PROPOSAL_COMMITTEE_DIRECTOR);
if (sysRole == null) {
throw new BaseException("提案委员会主任角色不存在");
}
int existsRole = dao().count(Sys_user_role.class, Cnd.where(Sys_user_role::getUserId, "=", userId)
.and(Sys_user_role::getTcSessionId, "=", sessionId)
.and(Sys_user_role::getRoleId, "=", sysRole.getId())
);
if (existsRole > 0) {
return;
}
Sys_user_role insertSysUserRole = new Sys_user_role();
insertSysUserRole.setRoleId(sysRole.getId());
insertSysUserRole.setUserId(userId);
insertSysUserRole.setTcSessionId(sessionId);
dao().insert(insertSysUserRole);
sysRoleService.clearCache();
sysUserService.clearCache();
}
/**
* 当用户在当前届次不再担任任何提案审查委员会主任时撤销对应系统角色
*
* @param userId 用户主键
* @param sessionId 教代会届次主键
*/
private void revokeProposalCommitteeDirectorRoleIfUnused(String userId, String sessionId) {
List<Teacher_congress_institution_user> institutionUsers = dao().query(Teacher_congress_institution_user.class,
Cnd.where(Teacher_congress_institution_user::getUserId, "=", userId)
.and(Teacher_congress_institution_user::getSessionId, "=", sessionId)
.and(Teacher_congress_institution_user::getIdentity, "=", "主任"));
boolean stillDirector = institutionUsers.stream()
.map(institutionUser -> dao().fetch(Teacher_congress_institution.class, institutionUser.getInstitutionId()))
.anyMatch(this::isProposalReviewCommittee);
if (stillDirector) {
return;
}
Sys_role sysRole = sysRoleService.getByCode(RoleConstant.PROPOSAL_COMMITTEE_DIRECTOR);
if (sysRole == null) {
return;
}
dao().clear(Sys_user_role.class, Cnd.where(Sys_user_role::getUserId, "=", userId)
.and(Sys_user_role::getTcSessionId, "=", sessionId)
.and(Sys_user_role::getRoleId, "=", sysRole.getId()));
sysRoleService.clearCache();
sysUserService.clearCache();
}
}