This commit is contained in:
2026-08-13 08:39:26 +08:00
parent ec900e5b4d
commit e4485d7b02
218 changed files with 2471 additions and 1427 deletions
@@ -12,6 +12,8 @@ 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.dao.util.cri.Static;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
@@ -51,7 +53,8 @@ public class FlowTodoCenterController {
t.h5FormKey,
t.createdAt,
t.finishTime,
t.variable,
t.variable AS taskVariable,
ins.variable,
ins.state AS instanceState,
ins.id AS instanceId,
ins.businessNo,
@@ -66,7 +69,14 @@ public class FlowTodoCenterController {
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
// 普通待办仍按进行中任务查询;提案进入团长审核后,补充展示尚未附议的已废弃 second 任务。
SqlExpressionGroup todoTaskGroup = Cnd.exps("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
SqlExpressionGroup proposalSecondGroup = Cnd.exps("t.taskState", "=", ProcessTaskStateEnum.ABANDON.getCode())
.and("t.taskName", "=", "second")
.and(new Static("EXISTS (SELECT 1 FROM proposal_second ps WHERE ps.proposalId = ins.businessNo AND ps.seconderId = ta.actorId AND ps.isAgree IS NULL)"))
.and(new Static("EXISTS (SELECT 1 FROM wf_process_task dt WHERE dt.processInstanceId = t.processInstanceId AND dt.taskName = 'delegation' AND dt.taskState = 10)"));
todoTaskGroup.or(proposalSecondGroup);
cnd.and(todoTaskGroup);
cnd.and("ta.actorId", "=", SecurityUtil.getUserId());
cnd.andEX("def.category", "=", category);
if (StrUtil.isNotBlank(searchKeyword)) {
@@ -96,7 +106,8 @@ public class FlowTodoCenterController {
t.h5FormKey,
t.createdAt,
t.finishTime,
t.variable,
t.variable AS taskVariable,
ins.variable,
ins.state AS instanceState,
ins.id AS instanceId,
ins.businessNo,
@@ -223,7 +234,27 @@ public class FlowTodoCenterController {
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id
WHERE
ta.actorId = @userId
AND t.taskState = 10
AND (
t.taskState = 10
OR (
t.taskState = 99
AND t.taskName = 'second'
AND EXISTS (
SELECT 1
FROM proposal_second ps
WHERE ps.proposalId = (SELECT businessNo FROM wf_process_instance WHERE id = t.processInstanceId)
AND ps.seconderId = ta.actorId
AND ps.isAgree IS NULL
)
AND EXISTS (
SELECT 1
FROM wf_process_task dt
WHERE dt.processInstanceId = t.processInstanceId
AND dt.taskName = 'delegation'
AND dt.taskState = 10
)
)
)
""");
todoSql.setParam("userId", SecurityUtil.getUserId());
todoSql.setCallback(Sqls.callback.integer());
@@ -6,7 +6,9 @@ import lombok.Setter;
import org.nutz.dao.entity.annotation.Column;
import org.nutz.dao.entity.annotation.Comment;
import org.nutz.dao.entity.annotation.Id;
import org.nutz.dao.entity.annotation.Index;
import org.nutz.dao.entity.annotation.Table;
import org.nutz.dao.entity.annotation.TableIndexes;
import java.io.Serializable;
import java.util.Date;
@@ -22,6 +24,9 @@ import java.util.Date;
@Getter
@Setter
@Table("wf_process_instance")
@TableIndexes({
@Index(name = "INDEX_WF_PROCESS_INSTANCE_BUSINESS_NO", fields = {"businessNo"}, unique = false)
})
@Comment("流程实例")
public class ProcessInstance extends BaseModel {
@@ -6,7 +6,9 @@ import lombok.Setter;
import org.nutz.dao.entity.annotation.Column;
import org.nutz.dao.entity.annotation.Comment;
import org.nutz.dao.entity.annotation.Id;
import org.nutz.dao.entity.annotation.Index;
import org.nutz.dao.entity.annotation.Table;
import org.nutz.dao.entity.annotation.TableIndexes;
import java.io.Serializable;
import java.util.Date;
@@ -19,6 +21,9 @@ import java.util.Date;
@Getter
@Setter
@Table("wf_process_task")
@TableIndexes({
@Index(name = "INDEX_WF_PROCESS_TASK_INSTANCE_STATE", fields = {"processInstanceId", "taskState"}, unique = false)
})
@Comment("流程任务")
public class ProcessTask extends BaseModel {
@@ -154,10 +154,15 @@ public class SysHomeController {
.and(Sys_menu::getPlatform, "=", platform)
);
List<Sys_menu> menus = sysUserService.getMenus(SecurityUtil.getUserId());
List<String> allMenuIds = menus.stream().map(Sys_menu::getId).toList();
Set<String> allMenuIds = menus.stream().map(Sys_menu::getId).collect(Collectors.toSet());
Set<String> allParentMenuIds = menus.stream()
.map(Sys_menu::getParentId)
.filter(StrUtil::isNotBlank)
.collect(Collectors.toSet());
List<Sys_menu> sysMenus = list.stream()
// 推荐项必须同时属于当前用户已授权菜单,避免首页展示无权限入口。
.filter(menu -> allMenuIds.contains(menu.getId()))
// 应用有已授权菜单,或根菜单自身有链接且已授权时,才允许在首页展示入口。
.filter(menu -> allParentMenuIds.contains(menu.getId())
|| (StrUtil.isNotBlank(menu.getHref()) && allMenuIds.contains(menu.getId())))
.sorted(Comparator.comparing(Sys_menu::getLocation, Comparator.nullsLast(Integer::compareTo))
.thenComparing(Sys_menu::getId))
.toList();
@@ -175,7 +180,7 @@ public class SysHomeController {
.and(Sys_menu::getPlatform, "=", platform)
);
List<Sys_menu> menus = sysUserService.getMenus(SecurityUtil.getUserId());
List<String> allMenuIds = menus.stream().map(Sys_menu::getId).toList();
Set<String> allMenuIds = menus.stream().map(Sys_menu::getId).collect(Collectors.toSet());
List<Sys_menu> sysMenus = list.stream()
// 推荐项必须同时属于当前用户已授权菜单,避免首页展示无权限入口。
.filter(menu -> allMenuIds.contains(menu.getId()))
@@ -42,6 +42,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* Created by wizzer on 2016/6/28.
@@ -554,20 +555,13 @@ public class SysRoleController {
@Ok("json:full")
@SaCheckLogin
public Result getRoleNames(){
Sql sql = Sqls.create("""
SELECT
GROUP_CONCAT(DISTINCT sr.`name`) roleNames
FROM
`sys_user_role` sur
LEFT JOIN sys_role sr ON sr.id = sur.roleId
WHERE
sur.userId = @userId
ORDER BY
sr.sort DESC
""").setParam("userId", SecurityUtil.getUserId());
sql.setCallback(Sqls.callback.map());
sysRoleService.dao().execute(sql);
return Result.success(sql.getResult());
// 首页只展示当前有效角色,历史教代会届次角色仍保留数据但不参与当前身份展示。
String roleNames = sysUserService.getEffectiveRoles(SecurityUtil.getUserId()).stream()
.map(Sys_role::getName)
.filter(StrUtil::isNotBlank)
.distinct()
.collect(Collectors.joining(","));
return Result.success(NutMap.NEW().addv("roleNames", roleNames));
}
@@ -624,22 +624,21 @@ public class SysUnionController {
}
/**
* 分页查询当前分工会的组成单位。
*
* @param pageForm 分页和查询参数;pageNumber 为页码,pageSize 为每页条数,searchKeyword 为单位编码或名称
* @param unionId 分工会ID,不能为空
* @return Resultdata 为 Paginationlist 是当前页单位列表,totalCount 是符合条件的单位总数
*/
@At
@ApiOperation("分工会组成单位分页")
@SaCheckPermission("sys.manager.union")
public Result branchUnionPartUnitPageData(PageForm pageForm, String unionId) {
Cnd cnd = Cnd.NEW();
cnd.and("unionId", "=", unionId);
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.orLike("`name`", pageForm.getSearchKeyword());
seg.orLike("unitcode", pageForm.getSearchKeyword());
cnd.and(seg);
public Result branchUnionPartUnitPageData(@Valid PageForm pageForm, String unionId) {
if (StrUtil.isBlank(unionId)) {
return Result.error("分工会参数不能为空");
}
cnd.desc("unitcode");
List<Sys_unit> list = dao.query(Sys_unit.class, cnd);
return Result.success(list);
return Result.success(sysUnitService.pageBranchUnionPartUnits(pageForm, unionId));
}
@At
@@ -1,5 +1,7 @@
package com.budwk.app.sys.services;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.sys.models.Sys_unit;
@@ -7,6 +9,15 @@ import com.budwk.app.sys.models.Sys_unit;
* Created by wizzer on 2016/12/22.
*/
public interface SysUnitService extends BaseService<Sys_unit> {
/**
* 分页查询指定分工会的组成单位。
*
* @param pageForm 分页和查询参数;pageNumber 为页码,pageSize 为每页条数,searchKeyword 为单位编码或名称
* @param unionId 分工会ID,用于限制只查询当前分工会的组成单位
* @return 分页结果;list 为当前页单位数据,totalCount 为符合条件的单位总数
*/
Pagination<Sys_unit> pageBranchUnionPartUnits(PageForm pageForm, String unionId);
/**
* 保存单位
*
@@ -6,6 +6,7 @@ import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.exception.UnknownAccountException;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.sys.models.Sys_menu;
import com.budwk.app.sys.models.Sys_role;
import com.budwk.app.sys.models.Sys_user;
import javax.servlet.http.HttpServletRequest;
@@ -31,6 +32,14 @@ public interface SysUserService extends BaseService<Sys_user> {
*/
List<String> getRoleCodeList(Sys_user user);
/**
* 查询用户当前有效的角色。普通角色始终有效,教代会届次角色仅保留最新创建届次的数据。
*
* @param userId 用户ID
* @return 当前有效的系统角色列表,按角色排序倒序返回
*/
List<Sys_role> getEffectiveRoles(String userId);
/**
* 获取用户的菜单
@@ -2,6 +2,8 @@ package com.budwk.app.sys.services.impl;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RedisConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_unit;
import com.budwk.app.sys.services.SysUnitService;
@@ -10,6 +12,7 @@ import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Strings;
@@ -26,6 +29,22 @@ public class SysUnitServiceImpl extends BaseServiceImpl<Sys_unit> implements Sys
super(dao);
}
/**
* 按单位编码或名称筛选当前分工会的组成单位,并按单位编码倒序分页。
*/
@Override
public Pagination<Sys_unit> pageBranchUnionPartUnits(PageForm pageForm, String unionId) {
Cnd cnd = Cnd.where("unionId", "=", unionId);
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup keywordCondition = new SqlExpressionGroup();
keywordCondition.orLike("`name`", pageForm.getSearchKeyword());
keywordCondition.orLike("unitcode", pageForm.getSearchKeyword());
cnd.and(keywordCondition);
}
cnd.desc("unitcode");
return listPage(pageForm.getPageNumber(), pageForm.getPageSize(), Sys_unit.class, cnd);
}
/**
* 新增单位
*
@@ -65,18 +65,13 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> implements Sys
private RedisService redisService;
@Override
@CacheResult(cacheKey = "${userId}_getPermissionList")
@CacheResult(cacheKey = "${userId}_getPermissionList_currentSession")
public List<String> getPermissionList(String userId) {
Sys_user user = this.fetch(userId);
if (user == null) {
return new ArrayList<>();
}
this.fetchLinks(user, "roles");
if (user.getRoles() == null) {
if (this.fetch(userId) == null) {
return new ArrayList<>();
}
List<String> permissionList = new ArrayList<String>();
for (Sys_role role : user.getRoles()) {
for (Sys_role role : getEffectiveRoles(userId)) {
if (!role.isDisabled()) {
permissionList.addAll(sysRoleService.getPermissionList(role));
}
@@ -92,16 +87,64 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> implements Sys
* @param user
* @return
*/
@CacheResult(cacheKey = "${user.id}_getRoleCodeList")
@CacheResult(cacheKey = "${user.id}_getRoleCodeList_currentSession")
public List<String> getRoleCodeList(Sys_user user) {
dao().fetchLinks(user, "roles");
List<String> roleNameList = new ArrayList<String>();
for (Sys_role role : user.getRoles()) {
for (Sys_role role : getEffectiveRoles(user.getId())) {
if (!role.isDisabled()) roleNameList.add(role.getCode());
}
return roleNameList;
}
/**
* 查询用户当前有效角色。tcSessionId 为空的普通角色不受届次影响;教代会角色只认创建时间最新的届次。
* 历史届次关系继续保存在 sys_user_role 中,以便删除最新届次后自动恢复上一届身份。
*
* @param userId 用户ID
* @return 当前有效的角色实体列表
*/
@Override
public List<Sys_role> getEffectiveRoles(String userId) {
Sql sql = Sqls.create("""
SELECT DISTINCT
sr.*
FROM
sys_role sr
INNER JOIN sys_user_role userRole ON userRole.roleId = sr.id
WHERE
userRole.userId = @userId
AND (
userRole.tcSessionId IS NULL
OR userRole.tcSessionId = ''
OR userRole.tcSessionId = (
SELECT tcs.id
FROM teacher_congress_session tcs
ORDER BY tcs.createdAt DESC, tcs.id DESC
LIMIT 1
)
)
ORDER BY sr.sort DESC, sr.id
""");
sql.setParam("userId", userId);
sql.setCallback(Sqls.callback.entities());
sql.setEntity(dao().getEntity(Sys_role.class));
dao().execute(sql);
return sql.getList(Sys_role.class);
}
/**
* 查询用户当前有效且已启用的角色ID,供菜单和按钮权限查询统一复用。
*
* @param userId 用户ID
* @return 有效角色ID列表
*/
private List<String> getEnabledEffectiveRoleIds(String userId) {
return getEffectiveRoles(userId).stream()
.filter(role -> !role.isDisabled())
.map(Sys_role::getId)
.toList();
}
/**
* 获取用户菜单
*
@@ -161,8 +204,12 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> implements Sys
*/
// @CacheResult(cacheKey = "${userId}_getMenus")
public List<Sys_menu> getMenus(String userId) {
Sql sql = Sqls.create("select distinct a.* from sys_menu a,sys_role_menu b where a.id=b.menuId and " + " b.roleId in(select c.roleId from sys_user_role c,sys_role d where c.roleId=d.id and c.userId=@userId and d.disabled=@f) and a.disabled=@f and a.showit=@t and a.type='menu' order by a.location ASC,a.path asc");
sql.params().set("userId", userId);
List<String> roleIds = getEnabledEffectiveRoleIds(userId);
if (roleIds.isEmpty()) {
return new ArrayList<>();
}
Sql sql = Sqls.create("select distinct a.* from sys_menu a,sys_role_menu b where a.id=b.menuId and " + " b.roleId in(@roleIds) and a.disabled=@f and a.showit=@t and a.type='menu' order by a.location ASC,a.path asc");
sql.params().set("roleIds", roleIds);
sql.params().set("f", false);
sql.params().set("t", true);
return sysMenuService.listEntity(sql);
@@ -176,8 +223,12 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> implements Sys
*/
// @CacheResult(cacheKey = "${userId}_getMenusAndButtons")
public List<Sys_menu> getMenusAndButtons(String userId) {
Sql sql = Sqls.create("select distinct a.* from sys_menu a,sys_role_menu b where a.id=b.menuId and " + " b.roleId in(select c.roleId from sys_user_role c,sys_role d where c.roleId=d.id and c.userId=@userId and d.disabled=@f) and a.disabled=@f order by a.location ASC,a.path asc");
sql.params().set("userId", userId);
List<String> roleIds = getEnabledEffectiveRoleIds(userId);
if (roleIds.isEmpty()) {
return new ArrayList<>();
}
Sql sql = Sqls.create("select distinct a.* from sys_menu a,sys_role_menu b where a.id=b.menuId and " + " b.roleId in(@roleIds) and a.disabled=@f order by a.location ASC,a.path asc");
sql.params().set("roleIds", roleIds);
sql.params().set("f", false);
return sysMenuService.listEntity(sql);
}
@@ -193,10 +244,14 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> implements Sys
* @param userId
* @return
*/
@CacheResult(cacheKey = "${userId}_getDatas")
@CacheResult(cacheKey = "${userId}_getDatas_currentSession")
public List<Sys_menu> getDatas(String userId) {
Sql sql = Sqls.create("select distinct a.* from sys_menu a,sys_role_menu b where a.id=b.menuId and " + " b.roleId in(select c.roleId from sys_user_role c,sys_role d where c.roleId=d.id and c.userId=@userId and d.disabled=@f) and a.disabled=@f and a.type='data' order by a.location ASC,a.path asc");
sql.params().set("userId", userId);
List<String> roleIds = getEnabledEffectiveRoleIds(userId);
if (roleIds.isEmpty()) {
return new ArrayList<>();
}
Sql sql = Sqls.create("select distinct a.* from sys_menu a,sys_role_menu b where a.id=b.menuId and " + " b.roleId in(@roleIds) and a.disabled=@f and a.type='data' order by a.location ASC,a.path asc");
sql.params().set("roleIds", roleIds);
sql.params().set("f", false);
return sysMenuService.listEntity(sql);
}
@@ -230,10 +285,14 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> implements Sys
* @param pid
* @return
*/
@CacheResult(cacheKey = "${userId}_${pid}_getRoleMenus")
@CacheResult(cacheKey = "${userId}_${pid}_getRoleMenus_currentSession")
public List<Sys_menu> getRoleMenus(String userId, String pid) {
Sql sql = Sqls.create("select distinct a.* from sys_menu a,sys_role_menu b where a.id=b.menuId and " + "$m and b.roleId in(select c.roleId from sys_user_role c,sys_role d where c.roleId=d.id and c.userId=@userId and d.disabled=@f) and a.disabled=@f order by a.location ASC,a.path asc");
sql.params().set("userId", userId);
List<String> roleIds = getEnabledEffectiveRoleIds(userId);
if (roleIds.isEmpty()) {
return new ArrayList<>();
}
Sql sql = Sqls.create("select distinct a.* from sys_menu a,sys_role_menu b where a.id=b.menuId and " + "$m and b.roleId in(@roleIds) and a.disabled=@f order by a.location ASC,a.path asc");
sql.params().set("roleIds", roleIds);
sql.params().set("f", false);
if (Strings.isNotBlank(pid)) {
sql.vars().set("m", "a.parentId='" + pid + "'");
@@ -249,10 +308,14 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> implements Sys
* @param pid
* @return
*/
@CacheResult(cacheKey = "${userId}_${pid}_hasChildren")
@CacheResult(cacheKey = "${userId}_${pid}_hasChildren_currentSession")
public boolean hasChildren(String userId, String pid) {
Sql sql = Sqls.create("select count(*) from sys_menu a,sys_role_menu b where a.id=b.menuId and " + "$m and b.roleId in(select c.roleId from sys_user_role c,sys_role d where c.roleId=d.id and c.userId=@userId and d.disabled=@f) and a.disabled=@f order by a.location ASC,a.path asc");
sql.params().set("userId", userId);
List<String> roleIds = getEnabledEffectiveRoleIds(userId);
if (roleIds.isEmpty()) {
return false;
}
Sql sql = Sqls.create("select count(*) from sys_menu a,sys_role_menu b where a.id=b.menuId and " + "$m and b.roleId in(@roleIds) and a.disabled=@f order by a.location ASC,a.path asc");
sql.params().set("roleIds", roleIds);
sql.params().set("f", false);
if (Strings.isNotBlank(pid)) {
sql.vars().set("m", "a.parentId='" + pid + "'");
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.democratic.proposal.controller.query;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalQueryComprehensiveParam;
@@ -15,6 +16,7 @@ import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.util.ArrayList;
import java.util.List;
@@ -51,6 +53,23 @@ public class ProposalQueryCollectProgressController {
return Result.success(pagination);
}
/**
* 按征集进度页面筛选条件及当前可见字段导出 Excel。
*
* @param pageForm 页面筛选和排序参数
* @param tableColumns 当前可见字段,顺序即导出列顺序
* @param response HTTP 响应,返回 XSSF Excel
*/
@At
@Ok("void")
@SaCheckPermission("proposal.query.collectProgress")
@ApiOperation(value = "自定义导出征集进度")
public void exportCustomAsExcel(@Valid @Param("pageForm") ProposalQueryComprehensiveParam pageForm,
@Param("tableColumns") ExportTableColumns[] tableColumns,
HttpServletResponse response) {
proposalCommonService.exportCollectProgressAsExcel(pageForm, tableColumns, response);
}
@At
@ApiOperation(value = "获取征集状态")
@SaCheckPermission("proposal.query.collectProgress")
@@ -1,21 +1,13 @@
package com.budwk.app.zhgh.democratic.proposal.controller.query;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.democratic.proposal.controller.vo.ProposalInfoPageVO;
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.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.mvc.annotation.At;
@@ -24,9 +16,6 @@ import org.nutz.mvc.annotation.Param;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.util.Arrays;
import java.util.Map;
@IocBean
@At("/platform/proposal/query/comprehensive")
@Slf4j
@@ -34,25 +23,6 @@ import java.util.Map;
@Api(tags = "提案综合查询")
public class ProposalQueryComprehensiveController {
/**
* 综合查询表格允许排序的字段映射,值为查询中的真实字段或安全别名。
*/
private static final Map<String, String> COMPREHENSIVE_ORDER_COLUMNS = Map.ofEntries(
Map.entry("code", "info.code"),
Map.entry("name", "info.name"),
Map.entry("createUserName", "info.createUserName"),
Map.entry("typeName", "typeName"),
Map.entry("delegationName", "delegationName"),
Map.entry("caseFilingResult", "info.caseFilingResult"),
Map.entry("caseFilingType", "info.caseFilingType"),
Map.entry("merge", "merge"),
Map.entry("undertakeUnits", "undertakeUnitsOrder"),
Map.entry("curTaskName", "curTaskName"),
Map.entry("instanceState", "instanceState")
);
@Inject
private BaseService baseService;
@Inject
private ProposalCommonService proposalCommonService;
@@ -66,84 +36,24 @@ public class ProposalQueryComprehensiveController {
@SaCheckPermission("proposal.query.comprehensive")
@ApiOperation(value = "分页列表")
public Result pageData(@Valid @Param("pageForm") ProposalQueryComprehensiveParam pageForm) {
Sql sql = Sqls.create("""
SELECT
info.*,
IF(mer.proposalId IS NOT NULL, 1, 0) AS merge,
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,
GROUP_CONCAT(DISTINCT CASE WHEN pru.isMaster = 1 THEN pru.unitName END) AS masterUnitName,
GROUP_CONCAT(DISTINCT CASE WHEN pru.isMaster = 0 THEN pru.unitName END) AS slaveUnitNames,
GROUP_CONCAT(DISTINCT pru.unitName ORDER BY pru.isMaster DESC, pru.unitName) AS undertakeUnitsOrder
FROM
proposal_info info
LEFT JOIN proposal_type type on type.id = info.typeId
LEFT JOIN proposal_merge mer ON mer.proposalId = info.id
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 proposal_reply_unit pru ON pru.proposalId = info.id
LEFT JOIN wf_process_task replyTask on replyTask.processInstanceId = ins.id
$condition
""");
// AND replyTask.taskState = 20 AND replyTask.taskName in ('master_reply','slave_reply','opinion_master_reply')
Cnd cnd = Cnd.NEW();
if (ArrayUtil.isNotEmpty(pageForm.getOrigins()) && StrUtil.isNotBlank(pageForm.getCommonKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
return Result.success(proposalCommonService.queryComprehensive(pageForm));
}
// 案名
if (ArrayUtil.contains(pageForm.getOrigins(), "name")) {
for (String keyword : pageForm.getCommonKeyword().split(" ")) {
seg.orLike("info.name", keyword);
}
}
// 案由
if (ArrayUtil.contains(pageForm.getOrigins(), "brief")) {
for (String keyword : pageForm.getCommonKeyword().split(" ")) {
seg.orLike("info.brief", keyword);
}
}
// 提案人
if (ArrayUtil.contains(pageForm.getOrigins(), "createUserName")) {
for (String keyword : pageForm.getCommonKeyword().split(" ")) {
seg.orLike("info.createUserName", keyword);
seg.orLike("info.createUserLoginName", keyword);
}
}
// 答复内容
if (ArrayUtil.contains(pageForm.getOrigins(), "answer")) {
for (String keyword : pageForm.getCommonKeyword().split(" ")) {
seg.orLike("JSON_EXTRACT(replyTask.variable,'$.tf_opinion')", keyword);
}
}
if (!seg.isEmpty()) {
cnd.and(seg);
}
}
if (StrUtil.isNotBlank(pageForm.getUnderTakeUnitId())) {
cnd.andEX("pru.unitId", "=", pageForm.getUnderTakeUnitId());
}
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
cnd.groupBy("info.id");
// 综合查询包含多表字段及聚合别名,只允许白名单字段进入排序条件。
proposalCommonService.applySafePageOrder(cnd, pageForm, COMPREHENSIVE_ORDER_COLUMNS);
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
/**
* 按综合查询页面当前筛选条件和列设置导出 Excel。
*
* @param pageForm 页面筛选及排序参数
* @param tableColumns 列设置中当前可见的字段,顺序即 Excel 列顺序
* @param response HTTP 响应,返回 XSSF 格式的 Excel 文件
*/
@At
@Ok("void")
@SaCheckPermission("proposal.query.comprehensive")
@ApiOperation(value = "自定义导出综合查询列表")
public void exportCustomAsExcel(@Valid @Param("pageForm") ProposalQueryComprehensiveParam pageForm,
@Param("tableColumns") ExportTableColumns[] tableColumns,
HttpServletResponse response) {
proposalCommonService.exportComprehensiveAsExcel(pageForm, tableColumns, response);
}
@At
@@ -1,14 +1,11 @@
package com.budwk.app.zhgh.democratic.proposal.controller.query;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.sys.models.Sys_dict;
import com.budwk.app.sys.services.SysDictService;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSearchParam;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.Api;
@@ -21,10 +18,10 @@ import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
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.HashMap;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.util.Map;
@IocBean
@@ -34,15 +31,6 @@ import java.util.Map;
@Api(tags = "代表团提案统计")
public class ProposalQueryDelegationController {
/**
* 代表团汇总表固定列的安全排序映射,动态立案结果列在查询时追加。
*/
private static final Map<String, String> DELEGATION_SUMMARY_ORDER_COLUMNS = Map.of(
"dbtName", "dbt.name",
"TOTAL", "TOTAL",
"caseRate", "caseRateOrder"
);
/**
* 代表团提案明细表允许排序的字段映射。
*/
@@ -62,10 +50,6 @@ public class ProposalQueryDelegationController {
@Inject
private ProposalCommonService proposalCommonService;
@Inject
private SysDictService sysDictService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/query/delegation/index.html")
@SaCheckPermission("proposal.query.delegation")
@@ -77,48 +61,26 @@ 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
dbt.id,
dbt.`name` AS dbtName,
(select count(1) from proposal_info info where info.delegationId = dbt.id and info.sessionId = dbt.sessionId ) as TOTAL,
(select count(1) from proposal_info info where info.delegationId = dbt.id and info.sessionId = dbt.sessionId and info.isSubmit = 1 ) as SUBMIT_COUNT
$resultSql,
(select count(1) from proposal_info info where info.delegationId = dbt.id and info.sessionId = dbt.sessionId and info.caseFilingResult = @confirmFiling)
/ NULLIF((select count(1) from proposal_info info where info.delegationId = dbt.id and info.sessionId = dbt.sessionId), 0) AS caseRateOrder
FROM
teacher_congress_delegation dbt
$condition
""");
Map<String, String> summaryOrderColumns = new HashMap<>(DELEGATION_SUMMARY_ORDER_COLUMNS);
List<String> resultSqlParts = new ArrayList<>();
for (Sys_dict dict : dictList) {
String resultCode = dict.getCode();
// 动态别名和立案结果编码仅允许字母、数字和下划线;变量替换后新增的 @ 参数不会被 Nutz 再次解析,
// 因此这里使用已校验的字典编码字面量,确保统计条件能正确传递给数据库。
if (StrUtil.isBlank(resultCode) || !resultCode.matches("[A-Za-z0-9_]+")) {
continue;
}
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));
sql.setParam("confirmFiling", "CONFIRM_FILING");
Cnd cnd = Cnd.NEW();
cnd.andEX("dbt.sessionId", "=", sessionId);
if (!AuthUtil.hasRoleOr("SYSADMIN", "SCHOOL_UNION_ADMIN")) {
}
// 未选择排序时保留原有代表团编码升序,选择后仅应用白名单字段。
if (!proposalCommonService.applySafePageOrder(cnd, pageForm, summaryOrderColumns)) {
cnd.asc("dbt.`code`");
}
sql.setCondition(cnd);
return Result.success(baseService.listMap(sql));
return Result.success(proposalCommonService.queryDelegationStatistics(pageForm, sessionId));
}
/**
* 按当前届次、排序及页面可见字段导出代表团统计。
*
* @param pageForm 页面排序参数
* @param sessionId 教代会届次ID
* @param tableColumns 当前可见字段
* @param response HTTP 响应,返回 XSSF Excel
*/
@At
@Ok("void")
@ApiOperation("自定义导出代表团提案统计")
@SaCheckPermission("proposal.query.delegation")
public void exportCustomAsExcel(@Valid @Param("pageForm") PageForm pageForm,
@Param("sessionId") String sessionId,
@Param("tableColumns") ExportTableColumns[] tableColumns,
HttpServletResponse response) {
proposalCommonService.exportDelegationStatisticsAsExcel(pageForm, sessionId, tableColumns, response);
}
@@ -1,28 +1,19 @@
package com.budwk.app.zhgh.democratic.proposal.controller.query;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.flow.engine.util.FlowUtil;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalConfig;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalReplyUnit;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
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.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.*;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
/**
* 承办单位办理统计
@@ -33,16 +24,6 @@ import java.util.stream.Collectors;
@Ok("json:full")
public class ProposalQueryUnitReplyController {
/**
* 承办单位办理统计允许排序的字段白名单。
*/
private static final Set<String> UNIT_REPLY_ORDER_COLUMNS = Set.of(
"unitName", "sum", "masterSum", "masterReplySum", "masterNoReplySum",
"slaveSum", "slaveReplySum", "slaveNoReplySum"
);
@Inject
private Dao dao;
@Inject
private ProposalCommonService proposalCommonService;
@@ -64,94 +45,30 @@ public class ProposalQueryUnitReplyController {
* caseFilingResult:立案结果字典值,对应 proposal_info.caseFilingResult,为空时不限制立案结果。
* 返回值:Result.data 中包含 tableData 统计行列表,以及 slaveNeedReply 协办单位是否需要答复配置。
*/
// 提案配置 协办是否需要答复
ProposalConfig proposalConfig = dao.fetch(ProposalConfig.class, Cnd.NEW());
boolean slaveNeedReply = proposalConfig.getSlaveUnitNeedReply();
return Result.success().addData(proposalCommonService.queryUnitReplyStatistics(
pageForm, sessionId, undertakeUnitId, caseFilingResult));
}
// 当前届次所有的提案ID
Cnd proposalCnd = Cnd.where(ProposalInfo::getSessionId, "=", sessionId);
proposalCnd.andEX(ProposalInfo::getCaseFilingResult, "=", caseFilingResult);
List<String> proposalIds = dao.query(ProposalInfo.class, proposalCnd)
.stream()
.map(ProposalInfo::getId)
.collect(Collectors.toList());
if(proposalIds.isEmpty()){
return Result.success().addData(Map.of("tableData", Collections.EMPTY_LIST, "slaveNeedReply", slaveNeedReply));
}
// 关联所有的实例ID
Sql instSql = Sqls.create("select id from wf_process_instance where businessNo in (@proposalIds)");
instSql.setParam("proposalIds", proposalIds);
instSql.setCallback(Sqls.callback.strList());
dao.execute(instSql);
List<String> instIds = instSql.getList(String.class);
// 提案委员会立案分配的承办单位
Cnd cnd = Cnd.where(ProposalReplyUnit::getProposalId, "in", proposalIds);
cnd.andEX(ProposalReplyUnit::getUnitId, "=", undertakeUnitId);
List<ProposalReplyUnit> replyUnits = dao.query(ProposalReplyUnit.class, cnd);
// 承办单位答复记录
List<ProcessTask> replyTasks = dao.query(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "in", instIds).and(ProcessTask::getTaskName, "in", List.of("two_unit_reply","unit_reply","master_reply", "slave_reply", "opinion_master_reply")).and(ProcessTask::getTaskState, "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.DOING.getCode())));
// 承办单位
List<NutMap> tableData = new ArrayList<>();
replyUnits.stream().collect(Collectors.groupingBy(ProposalReplyUnit::getUnitCode)).forEach((k, v) -> {
NutMap tableRow = NutMap.NEW();
tableRow.put("id", k);
v.stream().findAny().ifPresent(item -> {
tableRow.put("unitName", item.getUnitName());
tableRow.put("unitCode", item.getUnitCode());
});
// 提案总数
tableRow.put("sum", v.size());
// 主办提案数
tableRow.put("masterSum", v.stream().filter(unit -> Objects.equals(unit.getUnitCode(), k) && unit.getIsMaster()).count());
// 协办提案数
tableRow.put("slaveSum", v.stream().filter(unit -> Objects.equals(unit.getUnitCode(), k) && !unit.getIsMaster()).count());
// 主办提案已答复数
long masterReplySum = replyTasks.stream().filter(x ->
FlowUtil.variableToNutMap(x.getVariable()).getString("underTakeId", "").equals(k)
&& FlowUtil.variableToNutMap(x.getVariable()).getBoolean("underTakeIsMaster")
&& x.getTaskState().equals(ProcessTaskStateEnum.FINISHED.getCode())
).map(ProcessTask::getProcessInstanceId).distinct().count();
// 主办提案未答复数
long masterNoReplySum = replyTasks.stream().filter(x ->
FlowUtil.variableToNutMap(x.getVariable()).getString("underTakeId", "").equals(k)
&& FlowUtil.variableToNutMap(x.getVariable()).getBoolean("underTakeIsMaster")
&& x.getTaskState().equals(ProcessTaskStateEnum.DOING.getCode())
).map(ProcessTask::getProcessInstanceId).distinct().count();
// 协办提案已答复数
long slaveReplySum = replyTasks.stream().filter(x ->
FlowUtil.variableToNutMap(x.getVariable()).getString("underTakeId", "").equals(k)
&& !FlowUtil.variableToNutMap(x.getVariable()).getBoolean("underTakeIsMaster")
&& x.getTaskState().equals(ProcessTaskStateEnum.FINISHED.getCode())
).map(ProcessTask::getProcessInstanceId).distinct().count();
// 协办提案未答复数
long slaveNoReplySum = replyTasks.stream().filter(x ->
FlowUtil.variableToNutMap(x.getVariable()).getString("underTakeId", "").equals(k)
&& !FlowUtil.variableToNutMap(x.getVariable()).getBoolean("underTakeIsMaster")
&& x.getTaskState().equals(ProcessTaskStateEnum.DOING.getCode())
).map(ProcessTask::getProcessInstanceId).distinct().count();
tableRow.put("masterReplySum", masterReplySum);
tableRow.put("masterNoReplySum", masterNoReplySum);
tableRow.put("slaveReplySum", slaveReplySum);
tableRow.put("slaveNoReplySum", slaveNoReplySum);
tableData.add(tableRow);
});
// 统计结果生成后在服务层按白名单字段排序,避免前端字段参与任意业务处理。
proposalCommonService.sortStatisticsRows(tableData, pageForm, UNIT_REPLY_ORDER_COLUMNS);
return Result.success().addData(Map.of("tableData", tableData, "slaveNeedReply", slaveNeedReply));
/**
* 按当前筛选条件及页面可见字段导出承办单位办理统计。
*
* @param pageForm 页面排序参数
* @param sessionId 教代会届次ID
* @param undertakeUnitId 承办单位ID
* @param caseFilingResult 立案结果编码
* @param tableColumns 当前可见字段
* @param response HTTP 响应,返回 XSSF Excel
*/
@At
@Ok("void")
@SaCheckPermission("proposal.query.unitReply")
public void exportCustomAsExcel(@Valid @Param("pageForm") PageForm pageForm,
@Param("sessionId") String sessionId,
@Param("undertakeUnitId") String undertakeUnitId,
@Param("caseFilingResult") String caseFilingResult,
@Param("tableColumns") ExportTableColumns[] tableColumns,
HttpServletResponse response) {
proposalCommonService.exportUnitReplyStatisticsAsExcel(pageForm, sessionId, undertakeUnitId,
caseFilingResult, tableColumns, response);
}
}
@@ -35,6 +35,7 @@ public class ProposalQueryYearReportController {
*/
private static final Map<String, String> YEAR_REPORT_ORDER_COLUMNS = Map.ofEntries(
Map.entry("code", "info.code"),
Map.entry("caseFilingCode", "info.caseFilingCode"),
Map.entry("name", "info.name"),
Map.entry("createUserName", "info.createUserName"),
Map.entry("typeName", "typeName"),
@@ -1,31 +1,19 @@
package com.budwk.app.zhgh.democratic.proposal.controller.query;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.flow.engine.util.FlowUtil;
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.ProposalConfig;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalReplyUnit;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.Api;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
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.*;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
@IocBean
@At("/platform/proposal/query/underTake/satisfaction")
@@ -33,17 +21,6 @@ import java.util.stream.Collectors;
@Api(tags = "提案承办单位满意度")
public class ProposalUnderTakeSatisfactionController {
/**
* 满意度统计固定列的排序白名单,动态满意度列在查询时追加。
*/
private static final Set<String> SATISFACTION_ORDER_COLUMNS = Set.of(
"unitName", "sum", "masterSum", "slaveSum"
);
@Inject
private Dao dao;
@Inject
private SysDictService sysDictService;
@Inject
private ProposalCommonService proposalCommonService;
@@ -65,80 +42,28 @@ public class ProposalUnderTakeSatisfactionController {
@At
@SaCheckPermission("proposal.query.underTake.satisfaction")
public Result data(PageForm pageForm, String sessionId, String undertakeUnitId) {
List<Sys_dict> feedbackCodes = sysDictService.getSubListByCode("PROPOSAL_FEEDBACK");
Set<String> orderColumns = new HashSet<>(SATISFACTION_ORDER_COLUMNS);
// 动态满意度列仅从后端字典加入白名单,不接受前端自行扩展字段。
feedbackCodes.stream()
.map(Sys_dict::getCode)
.filter(StrUtil::isNotBlank)
.forEach(orderColumns::add);
// 当前届次所有的提案ID
Sql sql = Sqls.create("select id from proposal_info where sessionId = @sessionId").setParam("sessionId", sessionId);
sql.setCallback(Sqls.callback.strList());
dao.execute(sql);
List<String> proposalIds = sql.getList(String.class);
if (proposalIds.isEmpty()) {
return Result.success(Collections.EMPTY_LIST);
}
// 关联所有的实例ID
Sql instSql = Sqls.create("select id from wf_process_instance where businessNo in (@proposalIds)");
instSql.setParam("proposalIds", proposalIds);
instSql.setCallback(Sqls.callback.strList());
dao.execute(instSql);
List<String> instIds = instSql.getList(String.class);
// 提案委员会立案分配的承办单位
Cnd cnd = Cnd.where(ProposalReplyUnit::getProposalId, "in", proposalIds);
cnd.andEX(ProposalReplyUnit::getUnitId, "=", undertakeUnitId);
List<ProposalReplyUnit> replyUnits = dao.query(ProposalReplyUnit.class, cnd);
// 反馈评分记录
// List<ProcessTask> feedbackTasks = dao.query(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "in", instIds)
// .and(ProcessTask::getTaskName, "=", "feedback")
// .and(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.FINISHED.getCode()));
// 承办单位
List<NutMap> tableData = new ArrayList<>();
replyUnits.stream().collect(Collectors.groupingBy(ProposalReplyUnit::getUnitCode)).forEach((k, v) -> {
NutMap tableRow = NutMap.NEW();
tableRow.put("id", k);
v.stream().findAny().ifPresent(item -> {
tableRow.put("unitName", item.getUnitName());
tableRow.put("unitCode", item.getUnitCode());
});
// 提案总数
tableRow.put("sum", v.size());
// 主办提案数
tableRow.put("masterSum", v.stream().filter(unit -> Objects.equals(unit.getUnitCode(), k) && unit.getIsMaster()).count());
// 协办提案数
tableRow.put("slaveSum", v.stream().filter(unit -> Objects.equals(unit.getUnitCode(), k) && !unit.getIsMaster()).count());
// 该单位负责的提案
List<String> thisUnitProposalIds = v.stream().map(ProposalReplyUnit::getProposalId).toList();
List<ProcessInstance> instances = dao.query(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "in", thisUnitProposalIds));
List<Long> thisUnitInstIds = instances.stream().map(ProcessInstance::getId).toList();
// 反馈评分记录
List<ProcessTask> feedbackTasks = dao.query(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "in", thisUnitInstIds)
.and(ProcessTask::getTaskName, "=", "feedback")
.and(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.FINISHED.getCode()));
for (Sys_dict code : feedbackCodes) {
long count = feedbackTasks.stream().filter(task -> FlowUtil.variableToNutMap(task.getVariable()).getString("tf_feedback", "").equals(code.getCode())).map(ProcessTask::getProcessInstanceId).distinct().count();
tableRow.put(code.getCode(), count);
}
tableData.add(tableRow);
});
// 汇总完成后在服务层执行白名单排序,数字列按数值顺序处理。
proposalCommonService.sortStatisticsRows(tableData, pageForm, orderColumns);
return Result.success(tableData);
return Result.success(proposalCommonService.queryUnderTakeSatisfactionStatistics(
pageForm, sessionId, undertakeUnitId));
}
/**
* 按当前届次、承办单位和页面可见字段导出满意度统计。
*
* @param pageForm 页面排序参数
* @param sessionId 教代会届次ID
* @param undertakeUnitId 承办单位ID
* @param tableColumns 当前可见字段
* @param response HTTP 响应,返回 XSSF Excel
*/
@At
@Ok("void")
@SaCheckPermission("proposal.query.underTake.satisfaction")
public void exportCustomAsExcel(@Valid @Param("pageForm") PageForm pageForm,
@Param("sessionId") String sessionId,
@Param("undertakeUnitId") String undertakeUnitId,
@Param("tableColumns") ExportTableColumns[] tableColumns,
HttpServletResponse response) {
proposalCommonService.exportUnderTakeSatisfactionAsExcel(
pageForm, sessionId, undertakeUnitId, tableColumns, response);
}
}
@@ -125,6 +125,17 @@ public class ProposalCommitteeFilingController {
proposalCommonService.exportWorkflowList("committeeFiling", pageForm, approval, tableColumns, response);
}
@At
@SaCheckPermission("proposal.committeeFiling")
@ApiOperation("执行单案立案审核任务")
@Aop(TransAop.READ_COMMITTED)
public Result executeTask(@Param("data") String data) {
// data 包含流程任务ID、提交类型、立案结果及主协办单位;Service 校验负责人后执行 WF,成功返回统一 Result。
Dict args = Json.fromJson(Dict.class, data);
proposalCommitteeFilingService.executeFlowTask(args);
return Result.success();
}
@At
@SaCheckPermission("proposal.committeeFiling")
@ApiOperation("并案审核")
@@ -4,7 +4,6 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.bpm.enums.BpmProcessTaskStatusEnum;
@@ -30,10 +29,8 @@ import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.validation.Valid;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
/**
@@ -129,13 +126,4 @@ public class ProposalDelegationController {
return Result.success(pagination);
}
@At
@Ok("void")
@SaCheckPermission("proposal.delegation")
@ApiOperation("导出列表")
public void exportList(@Valid @Param("pageForm") ProposalSearchParam pageForm, boolean approval,
@Param("tableColumns") ExportTableColumns[] tableColumns, HttpServletResponse response) {
proposalCommonService.exportWorkflowList("delegation", pageForm, approval, tableColumns, response);
}
}
@@ -5,7 +5,6 @@ import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.base.result.Result;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
@@ -27,7 +26,6 @@ import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.validation.Valid;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
@IocBean
@@ -118,15 +116,6 @@ public class ProposalFeedbackEvaluationController {
return Result.success(pagination);
}
@At
@Ok("void")
@SaCheckPermission("proposal.feedbackEvaluation")
@ApiOperation("导出列表")
public void exportList(@Valid @Param("pageForm") ProposalSearchParam pageForm, boolean approval,
@Param("tableColumns") ExportTableColumns[] tableColumns, HttpServletResponse response) {
proposalCommonService.exportWorkflowList("feedbackEvaluation", pageForm, approval, tableColumns, response);
}
@At
@SaCheckPermission("proposal.feedbackEvaluation")
@ApiOperation("获取立案信息")
@@ -134,6 +134,9 @@ public class ProposalSecondedController {
LEFT JOIN wf_process_instance ins ON ins.id = t.processInstanceId
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id
LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10
LEFT JOIN wf_process_task dt ON dt.processInstanceId = ins.id
AND dt.taskName = 'delegation' AND dt.taskState = 20
AND (dt.variable ->> '$.submitType') IN ('1', '2')
LEFT JOIN wf_process_task rt ON rt.processInstanceId = ins.id AND rt.taskParentId = t.id AND rt.taskState = 10
LEFT JOIN proposal_info info ON info.id = ins.businessNo
LEFT JOIN proposal_second ps ON ps.proposalId = info.id AND ps.seconderId = @seconderId
@@ -157,6 +160,8 @@ public class ProposalSecondedController {
} else {
// 未附议页签只展示当前登录附议人尚未处理的记录。
cnd.and("ps.isAgree", "is", null);
// 团长选择同意或不同意后均截止附议;退回提案人不视为最终审核结果。
cnd.and("dt.id", "is", null);
// 待附议列表只展示两类任务:
// 1. invite / second 节点任务
// 2. delegation / committee 节点,且任务状态必须是已废弃(99)
@@ -216,6 +221,9 @@ public class ProposalSecondedController {
if (Strings.isBlank(proposalId) || Strings.isBlank(taskActorUserId) || taskId == null || submitType == null) {
return Result.error("参数错误");
}
if (proposalSecondedService.isDelegationReviewed(proposalId)) {
return Result.error("团长已完成审核,当前提案不能继续附议");
}
// 先定位 wf_process_task,确保本次请求能准确命中需要完成的附议任务,再更新两张业务表。
ProcessTask processTask = this.getSecondTask(taskId, taskActorUserId);
@@ -294,6 +302,9 @@ public class ProposalSecondedController {
if (args.getLong("processTaskId") == null || Strings.isBlank(proposalId) || Strings.isBlank(taskActorUserId) || submitType == null) {
return Result.error("参数错误");
}
if (proposalSecondedService.isDelegationReviewed(proposalId)) {
return Result.error("团长已完成审核,当前提案不能继续附议");
}
flowCommonService.executeTask(args);
proposalSecondedService.updateSecondRecord(proposalId, taskActorUserId, opinion, submitType);
return Result.success();
@@ -5,7 +5,6 @@ import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjectUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.base.result.Result;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
@@ -38,7 +37,6 @@ import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.validation.Valid;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.List;
@@ -172,15 +170,6 @@ public class ProposalUnderTakeReplyController {
return Result.success(pagination);
}
@At
@Ok("void")
@SaCheckPermission("proposal.unitReply")
@ApiOperation("导出列表")
public void exportList(@Valid @Param("pageForm") ProposalSearchParam pageForm, boolean approval,
@Param("tableColumns") ExportTableColumns[] tableColumns, HttpServletResponse response) {
proposalCommonService.exportWorkflowList("unitReply", pageForm, approval, tableColumns, response);
}
/**
* 承办单位答复节点撤回需要基于整组当前节点判断。
* 只有“当前流程正在办理的节点”和“这条列表记录所属节点”完全一致时,才允许撤回;
@@ -73,6 +73,10 @@ public class ProposalCaseFilingInterceptor implements FlowInterceptor {
Dict dict = JSONUtil.toBean(processTask.getVariable(), Dict.class);
dict.set(FlowConst.TASK_FORM_DATA_PREFIX + "helpunitreply", helpUnitReply);
processTask.setVariable(JSONUtil.toJsonStr(dict));
dao.update(processTask);
/*
* 当前任务对象是在任务完成前加载的,其中 finishTime、operator 仍为空。
* 这里只更新拦截器实际修改的流程变量,避免整行更新覆盖 WF 已写入的办理时间和办理人。
*/
dao.update(processTask, "variable");
}
}
@@ -12,6 +12,9 @@ import org.nutz.dao.interceptor.annotation.PrevInsert;
@Accessors(chain = true)
@Table("proposal_reply_unit")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@TableIndexes({
@Index(name = "INDEX_PROPOSAL_REPLY_UNIT_PROPOSAL_UNIT", fields = {"proposalId", "unitId"}, unique = false)
})
@Comment("提案信息")
public class ProposalReplyUnit extends BaseModel {
@@ -19,6 +19,8 @@ public class ProposalQueryComprehensiveParam extends PageForm {
private String name;
@ApiModelProperty(name = "提案编号")
private String code;
@ApiModelProperty(name = "立案编号")
private String caseFilingCode;
@ApiModelProperty(name = "教代会ID")
private String sessionId;
@ApiModelProperty(name = "search session id")
@@ -61,15 +63,22 @@ public class ProposalQueryComprehensiveParam extends PageForm {
@ApiModelProperty(name = "通用关键字")
private String commonKeyword;
@ApiModelProperty(name = "提案名称或编号搜索字段,仅支持name、code、caseFilingCode")
private String proposalSearchField;
@ApiModelProperty(name = "提案名称或编号搜索内容")
private String proposalKeyword;
@ApiModelProperty(name = "搜索关键字")
private String createUserKeyword;
/**
* 构建通用查询参数
* 将综合查询参数追加到查询条件中,无返回值,处理结果直接写入 cnd。
*
* @param cnd cnd
* @param searchParam searchParam
* @param cnd 查询条件容器,用于接收届次、名称/编号、提案人、组织及立案条件
* @param searchParam 页面查询参数;proposalSearchField 传 name、code 或 caseFilingCode
* proposalKeyword 传对应字段的模糊搜索内容
*/
public static void buildSearch(Cnd cnd, ProposalQueryComprehensiveParam searchParam) {
if (cnd == null) {
@@ -84,6 +93,19 @@ public class ProposalQueryComprehensiveParam extends PageForm {
// Isolated single-session filter for pages that should not reuse sessionId semantics.
cnd.andEX("info.sessionId", "=", searchParam.getSearchSessionId());
// 名称/编号组合搜索仅允许三个固定字段,前端提交值不能直接作为SQL字段使用。
if (StrUtil.isNotBlank(searchParam.getProposalKeyword())) {
String proposalSearchColumn = switch (StrUtil.blankToDefault(searchParam.getProposalSearchField(), "name")) {
case "name" -> "info.name";
case "code" -> "info.code";
case "caseFilingCode" -> "info.caseFilingCode";
default -> null;
};
if (proposalSearchColumn != null) {
cnd.where().andLike(proposalSearchColumn, searchParam.getProposalKeyword());
}
}
//提案名称
if (StrUtil.isNotBlank(searchParam.getName())) {
cnd.where().andLike("info.name", searchParam.getName());
@@ -92,6 +114,10 @@ public class ProposalQueryComprehensiveParam extends PageForm {
if (StrUtil.isNotBlank(searchParam.getCode())) {
cnd.where().andLike("info.code", searchParam.getCode());
}
// 立案编号由立案环节生成,支持按已生成编号进行模糊检索。
if (StrUtil.isNotBlank(searchParam.getCaseFilingCode())) {
cnd.where().andLike("info.caseFilingCode", searchParam.getCaseFilingCode());
}
//代表团ID
cnd.andEX("info.delegationId", "=", searchParam.getDelegationId());
if (StrUtil.isNotBlank(searchParam.getCreateUserName())) {
@@ -24,17 +24,34 @@ public class ProposalSearchParam extends PageForm {
private String createUserLoginName;
private String createUserKeyword;
private String caseFilingResult;
/** 名称/编号组合搜索字段,仅支持 name、code、caseFilingCode。 */
private String proposalSearchField;
/** 名称/编号组合搜索内容。 */
private String proposalKeyword;
/**
* 构建通用查询参数
* 将提案列表参数追加到查询条件中,无返回值,处理结果直接写入 cnd。
*
* @param cnd cnd
* @param searchParam searchParam
* @param cnd 查询条件容器,用于接收名称/编号、届次、提案人及立案结果条件
* @param searchParam 页面查询参数;proposalSearchField 传 name、code 或 caseFilingCode
* proposalKeyword 传对应字段的模糊搜索内容
*/
public static void buildSearch(Cnd cnd, ProposalSearchParam searchParam) {
if (cnd == null) {
cnd = Cnd.NEW();
}
// 搜索字段通过固定白名单转换,禁止把前端字段名直接用于SQL条件。
if (StrUtil.isNotBlank(searchParam.getProposalKeyword())) {
String proposalSearchColumn = switch (StrUtil.blankToDefault(searchParam.getProposalSearchField(), "name")) {
case "name" -> "info.name";
case "code" -> "info.code";
case "caseFilingCode" -> "info.caseFilingCode";
default -> null;
};
if (proposalSearchColumn != null) {
cnd.where().andLike(proposalSearchColumn, searchParam.getProposalKeyword());
}
}
if (StrUtil.isNotBlank(searchParam.getName())) {
cnd.where().andLike("info.name", searchParam.getName());
}
@@ -6,6 +6,15 @@ import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo;
public interface ProposalCommitteeFilingService extends BaseService<ProposalInfo> {
/**
* 提案委员会单案立案审核。
* 正常提交确认立案或工作建议时,先校验所选承办单位是否均已配置负责人,再执行 WF 流程任务。
*
* @param args 页面提交的流程参数,包括 processTaskId、submitType、tf_caseFilingResult、
* tf_masterUnitIds、tf_slaveUnitIds 等字段;方法无返回值,校验失败时抛出业务异常
*/
void executeFlowTask(Dict args);
/**
* 提案委员会并案审核。
*
@@ -5,6 +5,14 @@ import com.budwk.app.zhgh.democratic.proposal.models.ProposalSecond;
public interface ProposalSecondedService extends BaseService<ProposalSecond> {
/**
* 判断团长是否已经对当前提案作出同意或不同意的审核结果。
*
* @param proposalId 提案ID,用于定位当前提案对应的最新工作流实例
* @return true 表示团长已同意或不同意,不能继续附议;false 表示仍可按原流程附议
*/
boolean isDelegationReviewed(String proposalId);
/**
* 仅更新附议表 proposal_second 中当前附议人的附议结果。
* 参数说明:
@@ -174,6 +174,25 @@ public interface ProposalCommonService extends BaseService<ProposalInfo> {
*/
void updateStateCode(String id, Integer stateCode);
/**
* 分页查询提案综合列表。
*
* @param pageForm 综合查询页面提交的筛选、排序和分页参数;origins 指定通用关键字的检索范围,
* commonKeyword 为通用关键字,其余字段对应页面各独立筛选项
* @return Pagination 分页结果,list 中每项包含提案、流程状态、当前节点及承办单位汇总字段
*/
Pagination queryComprehensive(ProposalQueryComprehensiveParam pageForm);
/**
* 按综合查询页面当前筛选条件和可见字段导出 Excel。
*
* @param pageForm 综合查询页面提交的筛选和排序参数;导出全部匹配记录,不受当前分页限制
* @param tableColumns 列设置中当前可见的字段,数组顺序即 Excel 列顺序
* @param response HTTP 响应,方法直接输出 XSSF 格式的 Excel 文件
*/
void exportComprehensiveAsExcel(ProposalQueryComprehensiveParam pageForm,
ExportTableColumns[] tableColumns, HttpServletResponse response);
/**
* 查询提案征集进度列表。
* 查询结果与页面既有筛选、排序语义保持一致,内部使用聚合关联避免逐行执行统计子查询。
@@ -183,6 +202,85 @@ public interface ProposalCommonService extends BaseService<ProposalInfo> {
*/
Pagination queryCollectProgress(ProposalQueryComprehensiveParam pageForm);
/**
* 按征集进度页面当前筛选条件和可见字段导出 Excel。
*
* @param pageForm 征集进度页面筛选及排序参数,导出全部匹配记录
* @param tableColumns 列设置中当前可见字段,数组顺序即 Excel 列顺序
* @param response HTTP 响应,方法直接输出 XSSF Excel
*/
void exportCollectProgressAsExcel(ProposalQueryComprehensiveParam pageForm,
ExportTableColumns[] tableColumns, HttpServletResponse response);
/**
* 查询指定届次的代表团提案统计。
*
* @param pageForm 排序参数
* @param sessionId 教代会届次ID
* @return 代表团统计行,包含动态立案结果数量和立案率计算字段
*/
List<NutMap> queryDelegationStatistics(PageForm pageForm, String sessionId);
/**
* 导出代表团提案统计。
*
* @param pageForm 当前排序参数
* @param sessionId 教代会届次ID
* @param tableColumns 当前可见统计字段
* @param response HTTP 响应,方法直接输出 XSSF Excel
*/
void exportDelegationStatisticsAsExcel(PageForm pageForm, String sessionId,
ExportTableColumns[] tableColumns, HttpServletResponse response);
/**
* 查询承办单位办理统计。
*
* @param pageForm 排序参数
* @param sessionId 教代会届次ID
* @param undertakeUnitId 承办单位ID,为空时统计全部单位
* @param caseFilingResult 立案结果编码,为空时不限制
* @return NutMaptableData 为统计行,slaveNeedReply 表示协办单位是否需要答复
*/
NutMap queryUnitReplyStatistics(PageForm pageForm, String sessionId,
String undertakeUnitId, String caseFilingResult);
/**
* 导出承办单位办理统计。
*
* @param pageForm 当前排序参数
* @param sessionId 教代会届次ID
* @param undertakeUnitId 承办单位ID
* @param caseFilingResult 立案结果编码
* @param tableColumns 当前可见统计字段
* @param response HTTP 响应,方法直接输出 XSSF Excel
*/
void exportUnitReplyStatisticsAsExcel(PageForm pageForm, String sessionId, String undertakeUnitId,
String caseFilingResult, ExportTableColumns[] tableColumns,
HttpServletResponse response);
/**
* 查询承办单位满意度统计。
*
* @param pageForm 排序参数
* @param sessionId 教代会届次ID
* @param undertakeUnitId 承办单位ID,为空时统计全部单位
* @return 满意度统计行,动态满意度字段使用字典编码作为键
*/
List<NutMap> queryUnderTakeSatisfactionStatistics(PageForm pageForm, String sessionId,
String undertakeUnitId);
/**
* 导出承办单位满意度统计。
*
* @param pageForm 当前排序参数
* @param sessionId 教代会届次ID
* @param undertakeUnitId 承办单位ID
* @param tableColumns 当前可见统计字段
* @param response HTTP 响应,方法直接输出 XSSF Excel
*/
void exportUnderTakeSatisfactionAsExcel(PageForm pageForm, String sessionId, String undertakeUnitId,
ExportTableColumns[] tableColumns, HttpServletResponse response);
/**
* 按提案去重统计指定届次各流程节点的待办和已办数量。
*
@@ -82,6 +82,18 @@ import java.util.stream.Collectors;
@IocBean(args = {"refer:dao"})
public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> implements ProposalCommonService {
/** 确定立案提案的 Word 模板编码。 */
private static final String PROPOSAL_CONFIRM_FILING_TEMPLATE_CODE = "proposal_confirm_filing";
/** 工作建议提案的 Word 模板编码。 */
private static final String PROPOSAL_SUGGESTION_TEMPLATE_CODE = "proposal_suggestion";
/** 不予立案提案的 Word 模板编码。 */
private static final String PROPOSAL_NOT_TEMPLATE_CODE = "proposal_not";
/** 历史提案立案结果为空或无法识别时使用的原 Word 模板编码。 */
private static final String PROPOSAL_DEFAULT_TEMPLATE_CODE = "proposal";
/** PC端提案列表页面的排序字段白名单,页面代码由对应Controller固定传入。 */
private static final Map<String, Map<String, String>> PROPOSAL_LIST_ORDER_COLUMNS = createProposalListOrderColumns();
@@ -91,6 +103,7 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
/** 征集进度页面允许排序的字段,避免排序参数直接拼接到SQL。 */
private static final Map<String, String> COLLECT_PROGRESS_ORDER_COLUMNS = Map.ofEntries(
Map.entry("code", "info.code"),
Map.entry("caseFilingCode", "info.caseFilingCode"),
Map.entry("name", "info.name"),
Map.entry("createUserName", "info.createUserName"),
Map.entry("delegationName", "delegationName"),
@@ -100,6 +113,68 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
Map.entry("instanceState", "instanceState")
);
/** 征集进度页面允许自定义导出的字段及固定表头。 */
private static final Map<String, String> COLLECT_PROGRESS_EXPORT_COLUMNS = Map.ofEntries(
Map.entry("code", "提案编号"),
Map.entry("caseFilingCode", "立案编号"),
Map.entry("name", "提案名称"),
Map.entry("createUserName", "提案人"),
Map.entry("delegationName", "代表团"),
Map.entry("inviteCount", "邀请附议人数"),
Map.entry("finishCount", "已附议人数"),
Map.entry("curTaskName", "当前节点"),
Map.entry("instanceState", "流程状态")
);
/** 代表团统计固定字段的安全排序映射,动态立案结果字段由后端字典追加。 */
private static final Map<String, String> DELEGATION_SUMMARY_ORDER_COLUMNS = Map.of(
"dbtName", "dbt.name",
"TOTAL", "TOTAL",
"caseRate", "caseRateOrder"
);
/** 承办单位办理统计允许排序的字段白名单。 */
private static final Set<String> UNIT_REPLY_ORDER_COLUMNS = Set.of(
"unitName", "sum", "masterSum", "masterReplySum", "masterNoReplySum",
"slaveSum", "slaveReplySum", "slaveNoReplySum"
);
/** 满意度统计固定字段的排序白名单,动态满意度字段由后端字典追加。 */
private static final Set<String> SATISFACTION_ORDER_COLUMNS = Set.of(
"unitName", "sum", "masterSum", "slaveSum"
);
/** 综合查询页面允许排序的字段,避免前端排序参数直接进入SQL。 */
private static final Map<String, String> COMPREHENSIVE_ORDER_COLUMNS = Map.ofEntries(
Map.entry("code", "info.code"),
Map.entry("caseFilingCode", "info.caseFilingCode"),
Map.entry("name", "info.name"),
Map.entry("createUserName", "info.createUserName"),
Map.entry("typeName", "typeName"),
Map.entry("delegationName", "delegationName"),
Map.entry("caseFilingResult", "info.caseFilingResult"),
Map.entry("caseFilingType", "info.caseFilingType"),
Map.entry("merge", "merge"),
Map.entry("undertakeUnits", "undertakeUnitsOrder"),
Map.entry("curTaskName", "curTaskName"),
Map.entry("instanceState", "instanceState")
);
/** 综合查询页面允许自定义导出的字段及固定表头,防止请求伪造页面外字段。 */
private static final Map<String, String> COMPREHENSIVE_EXPORT_COLUMNS = Map.ofEntries(
Map.entry("code", "提案编号"),
Map.entry("caseFilingCode", "立案编号"),
Map.entry("name", "提案名称"),
Map.entry("createUserName", "提案人"),
Map.entry("typeName", "提案类别"),
Map.entry("delegationName", "代表团"),
Map.entry("caseFilingResult", "立案结果"),
Map.entry("merge", "是否并案"),
Map.entry("undertakeUnits", "承办单位"),
Map.entry("curTaskName", "当前节点"),
Map.entry("instanceState", "流程状态")
);
//找出富文本里面上传的图片
static String IMG_SRC_REGEX = "<img\\s+[^>]*src\\s*=\\s*([\"'])(/platform/sys/file/download\\?id=.*?)\\1[^>]*>";
@@ -122,6 +197,223 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
super(dao);
}
/**
* 分页查询提案综合列表。
* 普通查询不关联历史任务表;仅在 origins 包含 answer 且 commonKeyword 非空时,
* 才关联答复任务变量,避免每条提案与全部历史任务产生大量中间结果。
*
* @param pageForm 综合查询页面的筛选、排序和分页参数
* @return 包含提案基本信息、流程状态和承办单位汇总字段的分页结果
*/
@Override
public Pagination queryComprehensive(ProposalQueryComprehensiveParam pageForm) {
boolean searchAnswer = ArrayUtil.contains(pageForm.getOrigins(), "answer")
&& StrUtil.isNotBlank(pageForm.getCommonKeyword());
String answerTaskJoin = searchAnswer
? "LEFT JOIN wf_process_task replyTask ON replyTask.processInstanceId = ins.id"
: "";
Sql sql = Sqls.create("""
SELECT
info.*,
IF(mer.proposalId IS NOT NULL, 1, 0) AS merge,
type.name AS typeName,
tcs.fullName AS sessionName,
tcd.`name` AS delegationName,
ins.id AS instanceId,
ins.businessNo,
ins.state AS instanceState,
ins.variable AS instanceVariable,
ins.processDefineId AS instanceProcessDefineId,
t.displayName AS curTaskName,
GROUP_CONCAT(DISTINCT CASE WHEN pru.isMaster = 1 THEN pru.unitName END) AS masterUnitName,
GROUP_CONCAT(DISTINCT CASE WHEN pru.isMaster = 0 THEN pru.unitName END) AS slaveUnitNames,
GROUP_CONCAT(DISTINCT pru.unitName ORDER BY pru.isMaster DESC, pru.unitName) AS undertakeUnitsOrder
FROM proposal_info info
LEFT JOIN proposal_type type ON type.id = info.typeId
LEFT JOIN proposal_merge mer ON mer.proposalId = info.id
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 AND tcde.sessionId = info.sessionId
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 proposal_reply_unit pru ON pru.proposalId = info.id
%s
$condition
""".formatted(answerTaskJoin));
Cnd dataCnd = buildComprehensiveCondition(pageForm, searchAnswer);
dataCnd.groupBy("info.id");
applySafePageOrder(dataCnd, pageForm, COMPREHENSIVE_ORDER_COLUMNS);
sql.setCondition(dataCnd);
// 总数查询只保留筛选需要的表,不重复执行列表字段聚合和排序。
String countDelegateJoin = StrUtil.isNotBlank(pageForm.getUnionId()) || StrUtil.isNotBlank(pageForm.getUnitId())
? "LEFT JOIN teacher_congress_delegate tcde ON tcde.loginName = info.createUserLoginName AND tcde.sessionId = info.sessionId"
: "";
String countInstanceJoin = searchAnswer
? "LEFT JOIN wf_process_instance ins ON ins.businessNo = info.id"
: "";
String countReplyUnitJoin = StrUtil.isNotBlank(pageForm.getUnderTakeUnitId())
? "LEFT JOIN proposal_reply_unit pru ON pru.proposalId = info.id"
: "";
Sql countSql = Sqls.create("""
SELECT COUNT(DISTINCT info.id)
FROM proposal_info info
%s
%s
%s
%s
$condition
""".formatted(countDelegateJoin, countInstanceJoin, countReplyUnitJoin, answerTaskJoin));
countSql.setCondition(buildComprehensiveCondition(pageForm, searchAnswer));
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql, countSql);
}
/**
* 按综合查询页面当前筛选条件和列设置导出 Excel。
* 查询条件与分页列表共用 buildComprehensiveCondition,确保届次、组织及万能搜索口径一致;
* tableColumns 只决定导出字段和顺序,不参与 SQL 字段拼接。
*
* @param pageForm 综合查询页面筛选及排序参数,导出不受分页参数限制
* @param tableColumns 列设置中当前可见字段,数组顺序即 Excel 列顺序
* @param response HTTP 响应,返回 XSSF 格式的 Excel 文件
*/
@Override
public void exportComprehensiveAsExcel(ProposalQueryComprehensiveParam pageForm,
ExportTableColumns[] tableColumns, HttpServletResponse response) {
List<ExportTableColumns> exportColumns = getComprehensiveExportColumns(tableColumns);
if (exportColumns.isEmpty()) {
throw new BaseException("请至少选择一个导出字段");
}
boolean searchAnswer = ArrayUtil.contains(pageForm.getOrigins(), "answer")
&& StrUtil.isNotBlank(pageForm.getCommonKeyword());
String answerTaskJoin = searchAnswer
? "LEFT JOIN wf_process_task replyTask ON replyTask.processInstanceId = ins.id"
: "";
Sql sql = Sqls.create("""
SELECT
info.*,
IF(mer.proposalId IS NOT NULL, 1, 0) AS merge,
type.name AS typeName,
tcs.fullName AS sessionName,
tcd.`name` AS delegationName,
ins.state AS instanceState,
t.displayName AS curTaskName,
GROUP_CONCAT(DISTINCT CASE WHEN pru.isMaster = 1 THEN pru.unitName END) AS masterUnitName,
GROUP_CONCAT(DISTINCT CASE WHEN pru.isMaster = 0 THEN pru.unitName END) AS slaveUnitNames,
GROUP_CONCAT(DISTINCT pru.unitName ORDER BY pru.isMaster DESC, pru.unitName) AS undertakeUnitsOrder
FROM proposal_info info
LEFT JOIN proposal_type type ON type.id = info.typeId
LEFT JOIN proposal_merge mer ON mer.proposalId = info.id
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 AND tcde.sessionId = info.sessionId
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 proposal_reply_unit pru ON pru.proposalId = info.id
%s
$condition
""".formatted(answerTaskJoin));
Cnd cnd = buildComprehensiveCondition(pageForm, searchAnswer);
cnd.groupBy("info.id");
applySafePageOrder(cnd, pageForm, COMPREHENSIVE_ORDER_COLUMNS);
sql.setCondition(cnd);
List<NutMap> rows = listMap(sql);
if (rows.isEmpty()) {
throw new BaseException("当前筛选条件下没有可导出的提案数据");
}
convertComprehensiveExportValues(rows);
List<ExcelExportEntity> excelColumns = exportColumns.stream()
.map(column -> new ExcelExportEntity(column.getLabel(), column.getProp(), 20))
.collect(Collectors.toList());
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
exportParams.setTitle("提案综合查询");
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, excelColumns, rows);
CommonDownloadUtil.download("提案综合查询.xlsx", workbook, response);
}
/**
* 按页面字段白名单过滤导出列,并使用后端固定表头,避免伪造字段或重复字段进入 Excel。
*
* @param tableColumns 页面提交的当前可见列
* @return 校验后的导出列,保持页面列设置顺序
*/
private List<ExportTableColumns> getComprehensiveExportColumns(ExportTableColumns[] tableColumns) {
if (ArrayUtil.isEmpty(tableColumns)) {
return List.of();
}
List<ExportTableColumns> exportColumns = new ArrayList<>();
Set<String> selectedProps = new HashSet<>();
for (ExportTableColumns column : tableColumns) {
if (column == null || !COMPREHENSIVE_EXPORT_COLUMNS.containsKey(column.getProp())
|| !selectedProps.add(column.getProp())) {
continue;
}
ExportTableColumns safeColumn = new ExportTableColumns();
safeColumn.setProp(column.getProp());
safeColumn.setLabel(COMPREHENSIVE_EXPORT_COLUMNS.get(column.getProp()));
exportColumns.add(safeColumn);
}
return exportColumns;
}
/** 将字典、枚举及组合字段转换为与综合查询列表一致的展示文本。 */
private void convertComprehensiveExportValues(List<NutMap> rows) {
Map<String, String> caseFilingResultMap = sysDictService.getSubListByCode("PROPOSAL_CASE_FILING_RESULT")
.stream().collect(Collectors.toMap(Sys_dict::getCode, Sys_dict::getName));
Map<Integer, String> processStateMap = Arrays.stream(ProcessInstanceStateEnum.values())
.collect(Collectors.toMap(ProcessInstanceStateEnum::getCode, ProcessInstanceStateEnum::getMessage));
for (NutMap row : rows) {
// 承办单位必须在立案结果转换为中文前拼接,否则无法判断立案与建议两种展示口径。
row.put("undertakeUnits", formatUndertakeUnits(row));
row.put("merge", row.getInt("merge", 0) == 1 ? "" : "");
String caseFilingResult = row.getString("caseFilingResult");
row.put("caseFilingResult", caseFilingResultMap.getOrDefault(caseFilingResult, caseFilingResult));
Integer instanceState = row.getInt("instanceState");
row.put("instanceState", processStateMap.getOrDefault(instanceState, row.getString("instanceState")));
}
}
/**
* 构建综合列表和总数查询共用的筛选条件,保证分页总数与列表数据范围一致。
* 通用关键字中的多个词沿用原页面的 OR 关系;未选择检索范围或关键字为空时不追加条件。
*
* @param pageForm 综合查询页面筛选参数
* @param searchAnswer 是否需要检索工作流任务变量中的答复内容
* @return 不包含分组和排序的查询条件
*/
private Cnd buildComprehensiveCondition(ProposalQueryComprehensiveParam pageForm, boolean searchAnswer) {
Cnd cnd = Cnd.NEW();
if (ArrayUtil.isNotEmpty(pageForm.getOrigins()) && StrUtil.isNotBlank(pageForm.getCommonKeyword())) {
SqlExpressionGroup keywordCondition = new SqlExpressionGroup();
for (String keyword : pageForm.getCommonKeyword().split(" ")) {
if (ArrayUtil.contains(pageForm.getOrigins(), "name")) {
keywordCondition.orLike("info.name", keyword);
}
if (ArrayUtil.contains(pageForm.getOrigins(), "brief")) {
keywordCondition.orLike("info.brief", keyword);
}
if (ArrayUtil.contains(pageForm.getOrigins(), "createUserName")) {
keywordCondition.orLike("info.createUserName", keyword);
keywordCondition.orLike("info.createUserLoginName", keyword);
}
if (searchAnswer) {
keywordCondition.orLike("JSON_EXTRACT(replyTask.variable,'$.tf_opinion')", keyword);
}
}
if (!keywordCondition.isEmpty()) {
cnd.and(keywordCondition);
}
}
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
return cnd;
}
/**
* 分页查询提案征集进度。
* 附议人数和已附议人数使用分组结果关联,避免原查询对每一条提案重复执行统计子查询;
@@ -132,11 +424,48 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
*/
@Override
public Pagination queryCollectProgress(ProposalQueryComprehensiveParam pageForm) {
Sql sql = buildCollectProgressListSql(pageForm);
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);
}
/**
* 按征集进度页面当前筛选条件和可见字段导出全部匹配数据。
*
* @param pageForm 页面筛选及排序参数
* @param tableColumns 当前可见字段,顺序即 Excel 列顺序
* @param response HTTP 响应,直接输出 XSSF Excel
*/
@Override
public void exportCollectProgressAsExcel(ProposalQueryComprehensiveParam pageForm,
ExportTableColumns[] tableColumns, HttpServletResponse response) {
List<NutMap> rows = listMap(buildCollectProgressListSql(pageForm));
Map<Integer, String> processStateMap = Arrays.stream(ProcessInstanceStateEnum.values())
.collect(Collectors.toMap(ProcessInstanceStateEnum::getCode, ProcessInstanceStateEnum::getMessage));
for (NutMap row : rows) {
Integer instanceState = row.getInt("instanceState");
row.put("instanceState", processStateMap.getOrDefault(instanceState, row.getString("instanceState")));
}
exportRowsAsExcel("提案征集进度", rows, tableColumns, COLLECT_PROGRESS_EXPORT_COLUMNS, response);
}
/** 构建征集进度分页和导出共用的数据查询,保证两处筛选、分组及排序口径一致。 */
private Sql buildCollectProgressListSql(ProposalQueryComprehensiveParam pageForm) {
Sql sql = Sqls.create("""
SELECT
info.id,
info.name,
info.code,
info.caseFilingCode,
info.createUserName,
type.NAME AS typeName,
tcs.fullName AS sessionName,
@@ -170,17 +499,7 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
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);
return sql;
}
/**
@@ -244,6 +563,301 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
return cnd;
}
/**
* 查询指定届次的代表团提案统计,动态立案结果字段仅使用已校验的后端字典编码。
*
* @param pageForm 排序参数
* @param sessionId 教代会届次ID;为空时返回空列表,避免跨届次统计
* @return 代表团统计行,包含各立案结果数量和立案率排序值
*/
@Override
public List<NutMap> queryDelegationStatistics(PageForm pageForm, String sessionId) {
if (StrUtil.isBlank(sessionId)) {
return List.of();
}
List<Sys_dict> dictList = sysDictService.getSubListByCode("PROPOSAL_CASE_FILING_RESULT");
Sql sql = Sqls.create("""
SELECT
dbt.id,
dbt.`name` AS dbtName,
(SELECT COUNT(1) FROM proposal_info info WHERE info.delegationId = dbt.id AND info.sessionId = dbt.sessionId) AS TOTAL,
(SELECT COUNT(1) FROM proposal_info info WHERE info.delegationId = dbt.id AND info.sessionId = dbt.sessionId AND info.isSubmit = 1) AS SUBMIT_COUNT
$resultSql,
(SELECT COUNT(1) FROM proposal_info info WHERE info.delegationId = dbt.id AND info.sessionId = dbt.sessionId AND info.caseFilingResult = @confirmFiling)
/ NULLIF((SELECT COUNT(1) FROM proposal_info info WHERE info.delegationId = dbt.id AND info.sessionId = dbt.sessionId), 0) AS caseRateOrder
FROM teacher_congress_delegation dbt
$condition
""");
Map<String, String> orderColumns = new HashMap<>(DELEGATION_SUMMARY_ORDER_COLUMNS);
List<String> resultSqlParts = new ArrayList<>();
for (Sys_dict dict : dictList) {
String resultCode = dict.getCode();
// 动态 SQL 别名和字面量只接受字母、数字、下划线,避免字典异常值进入 SQL 结构。
if (StrUtil.isBlank(resultCode) || !resultCode.matches("[A-Za-z0-9_]+")) {
continue;
}
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 + "`");
orderColumns.put(resultCode, "`" + resultCode + "`");
}
sql.setVar("resultSql", String.join("", resultSqlParts));
sql.setParam("confirmFiling", "CONFIRM_FILING");
Cnd cnd = Cnd.where("dbt.sessionId", "=", sessionId);
if (!applySafePageOrder(cnd, pageForm, orderColumns)) {
cnd.asc("dbt.`code`");
}
sql.setCondition(cnd);
return listMap(sql);
}
/**
* 导出代表团统计,导出字段由页面当前可见列决定,字段名称以服务端字典为准。
*
* @param pageForm 排序参数
* @param sessionId 教代会届次ID
* @param tableColumns 当前可见字段
* @param response HTTP 响应,直接输出 XSSF Excel
*/
@Override
public void exportDelegationStatisticsAsExcel(PageForm pageForm, String sessionId,
ExportTableColumns[] tableColumns, HttpServletResponse response) {
List<NutMap> rows = queryDelegationStatistics(pageForm, sessionId);
for (NutMap row : rows) {
int total = row.getInt("TOTAL", 0);
int confirmFiling = row.getInt("CONFIRM_FILING", 0);
row.put("caseRate", total == 0 ? "" : String.format(Locale.ROOT, "%.2f%%", confirmFiling * 100.0 / total));
}
Map<String, String> allowedColumns = new LinkedHashMap<>();
allowedColumns.put("dbtName", "代表团名称");
allowedColumns.put("TOTAL", "提案总数");
for (Sys_dict dict : sysDictService.getSubListByCode("PROPOSAL_CASE_FILING_RESULT")) {
if (StrUtil.isNotBlank(dict.getCode()) && dict.getCode().matches("[A-Za-z0-9_]+")) {
allowedColumns.put(dict.getCode(), dict.getName());
}
}
allowedColumns.put("caseRate", "立案率");
exportRowsAsExcel("代表团提案统计", rows, tableColumns, allowedColumns, response);
}
/**
* 查询承办单位办理统计,返回统计行及协办单位是否需要答复的配置。
*
* @param pageForm 排序参数
* @param sessionId 教代会届次ID
* @param undertakeUnitId 承办单位ID,为空时统计全部单位
* @param caseFilingResult 立案结果编码,为空时不限制
* @return tableData 为统计行,slaveNeedReply 为协办答复配置
*/
@Override
public NutMap queryUnitReplyStatistics(PageForm pageForm, String sessionId,
String undertakeUnitId, String caseFilingResult) {
ProposalConfig proposalConfig = dao().fetch(ProposalConfig.class, Cnd.NEW());
boolean slaveNeedReply = proposalConfig != null && Boolean.TRUE.equals(proposalConfig.getSlaveUnitNeedReply());
Cnd proposalCnd = Cnd.where(ProposalInfo::getSessionId, "=", sessionId);
proposalCnd.andEX(ProposalInfo::getCaseFilingResult, "=", caseFilingResult);
List<String> proposalIds = dao().query(ProposalInfo.class, proposalCnd).stream()
.map(ProposalInfo::getId).collect(Collectors.toList());
if (proposalIds.isEmpty()) {
return NutMap.NEW().addv("tableData", List.of()).addv("slaveNeedReply", slaveNeedReply);
}
Sql instanceSql = Sqls.create("SELECT id FROM wf_process_instance WHERE businessNo IN (@proposalIds)");
instanceSql.setParam("proposalIds", proposalIds);
instanceSql.setCallback(Sqls.callback.strList());
dao().execute(instanceSql);
List<String> instanceIds = instanceSql.getList(String.class);
Cnd replyUnitCnd = Cnd.where(ProposalReplyUnit::getProposalId, "in", proposalIds);
replyUnitCnd.andEX(ProposalReplyUnit::getUnitId, "=", undertakeUnitId);
List<ProposalReplyUnit> replyUnits = dao().query(ProposalReplyUnit.class, replyUnitCnd);
List<ProcessTask> replyTasks = instanceIds.isEmpty() ? List.of() : dao().query(ProcessTask.class,
Cnd.where(ProcessTask::getProcessInstanceId, "in", instanceIds)
.and(ProcessTask::getTaskName, "in", List.of("two_unit_reply", "unit_reply", "master_reply", "slave_reply", "opinion_master_reply"))
.and(ProcessTask::getTaskState, "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.DOING.getCode())));
List<NutMap> tableData = new ArrayList<>();
replyUnits.stream().collect(Collectors.groupingBy(ProposalReplyUnit::getUnitCode)).forEach((unitCode, units) -> {
NutMap tableRow = NutMap.NEW().addv("id", unitCode);
units.stream().findAny().ifPresent(item -> {
tableRow.put("unitName", item.getUnitName());
tableRow.put("unitCode", item.getUnitCode());
});
tableRow.put("sum", units.size());
tableRow.put("masterSum", units.stream().filter(unit -> Boolean.TRUE.equals(unit.getIsMaster())).count());
tableRow.put("slaveSum", units.stream().filter(unit -> !Boolean.TRUE.equals(unit.getIsMaster())).count());
long masterReplySum = countUnitReplyTasks(replyTasks, unitCode, true, ProcessTaskStateEnum.FINISHED.getCode());
long masterNoReplySum = countUnitReplyTasks(replyTasks, unitCode, true, ProcessTaskStateEnum.DOING.getCode());
long slaveReplySum = countUnitReplyTasks(replyTasks, unitCode, false, ProcessTaskStateEnum.FINISHED.getCode());
long slaveNoReplySum = countUnitReplyTasks(replyTasks, unitCode, false, ProcessTaskStateEnum.DOING.getCode());
tableRow.put("masterReplySum", masterReplySum);
tableRow.put("masterNoReplySum", masterNoReplySum);
tableRow.put("slaveReplySum", slaveReplySum);
tableRow.put("slaveNoReplySum", slaveNoReplySum);
tableData.add(tableRow);
});
sortStatisticsRows(tableData, pageForm, UNIT_REPLY_ORDER_COLUMNS);
return NutMap.NEW().addv("tableData", tableData).addv("slaveNeedReply", slaveNeedReply);
}
/** 按承办单位、主协办标识和任务状态,对流程实例去重统计答复数量。 */
private long countUnitReplyTasks(List<ProcessTask> replyTasks, String unitCode, boolean master, int taskState) {
return replyTasks.stream().filter(task -> {
NutMap variables = FlowUtil.variableToNutMap(task.getVariable());
return Objects.equals(unitCode, variables.getString("underTakeId", ""))
&& master == Boolean.TRUE.equals(variables.getBoolean("underTakeIsMaster"))
&& Objects.equals(task.getTaskState(), taskState);
}).map(ProcessTask::getProcessInstanceId).distinct().count();
}
/** 按承办单位办理统计页面当前可见字段导出全部统计行。 */
@Override
public void exportUnitReplyStatisticsAsExcel(PageForm pageForm, String sessionId, String undertakeUnitId,
String caseFilingResult, ExportTableColumns[] tableColumns,
HttpServletResponse response) {
NutMap statistics = queryUnitReplyStatistics(pageForm, sessionId, undertakeUnitId, caseFilingResult);
List<NutMap> rows = statistics.getList("tableData", NutMap.class);
Map<String, String> allowedColumns = new LinkedHashMap<>();
allowedColumns.put("unitName", "单位名称");
allowedColumns.put("sum", "提案总数");
allowedColumns.put("masterSum", "主办提案数量");
allowedColumns.put("masterReplySum", "主办已答复");
allowedColumns.put("masterNoReplySum", "主办未答复");
allowedColumns.put("slaveSum", "协办提案数量");
if (statistics.getBoolean("slaveNeedReply", false)) {
allowedColumns.put("slaveReplySum", "协办已答复");
allowedColumns.put("slaveNoReplySum", "协办未答复");
}
exportRowsAsExcel("承办单位办理统计", rows, tableColumns, allowedColumns, response);
}
/**
* 查询承办单位满意度统计,动态满意度字段来自 PROPOSAL_FEEDBACK 字典。
*
* @param pageForm 排序参数
* @param sessionId 教代会届次ID
* @param undertakeUnitId 承办单位ID,为空时统计全部单位
* @return 各承办单位的主协办数量及满意度计数
*/
@Override
public List<NutMap> queryUnderTakeSatisfactionStatistics(PageForm pageForm, String sessionId,
String undertakeUnitId) {
List<Sys_dict> feedbackCodes = sysDictService.getSubListByCode("PROPOSAL_FEEDBACK");
Set<String> orderColumns = new HashSet<>(SATISFACTION_ORDER_COLUMNS);
feedbackCodes.stream().map(Sys_dict::getCode).filter(StrUtil::isNotBlank).forEach(orderColumns::add);
Sql proposalSql = Sqls.create("SELECT id FROM proposal_info WHERE sessionId = @sessionId");
proposalSql.setParam("sessionId", sessionId);
proposalSql.setCallback(Sqls.callback.strList());
dao().execute(proposalSql);
List<String> proposalIds = proposalSql.getList(String.class);
if (proposalIds.isEmpty()) {
return List.of();
}
Cnd replyUnitCnd = Cnd.where(ProposalReplyUnit::getProposalId, "in", proposalIds);
replyUnitCnd.andEX(ProposalReplyUnit::getUnitId, "=", undertakeUnitId);
List<ProposalReplyUnit> replyUnits = dao().query(ProposalReplyUnit.class, replyUnitCnd);
List<NutMap> tableData = new ArrayList<>();
replyUnits.stream().collect(Collectors.groupingBy(ProposalReplyUnit::getUnitCode)).forEach((unitCode, units) -> {
NutMap tableRow = NutMap.NEW().addv("id", unitCode);
units.stream().findAny().ifPresent(item -> {
tableRow.put("unitName", item.getUnitName());
tableRow.put("unitCode", item.getUnitCode());
});
tableRow.put("sum", units.size());
tableRow.put("masterSum", units.stream().filter(unit -> Boolean.TRUE.equals(unit.getIsMaster())).count());
tableRow.put("slaveSum", units.stream().filter(unit -> !Boolean.TRUE.equals(unit.getIsMaster())).count());
List<String> unitProposalIds = units.stream().map(ProposalReplyUnit::getProposalId).toList();
List<Long> unitInstanceIds = dao().query(ProcessInstance.class,
Cnd.where(ProcessInstance::getBusinessNo, "in", unitProposalIds)).stream()
.map(ProcessInstance::getId).toList();
List<ProcessTask> feedbackTasks = unitInstanceIds.isEmpty() ? List.of() : dao().query(ProcessTask.class,
Cnd.where(ProcessTask::getProcessInstanceId, "in", unitInstanceIds)
.and(ProcessTask::getTaskName, "=", "feedback")
.and(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.FINISHED.getCode()));
for (Sys_dict feedbackCode : feedbackCodes) {
if (StrUtil.isBlank(feedbackCode.getCode())) {
continue;
}
long count = feedbackTasks.stream().filter(task -> Objects.equals(feedbackCode.getCode(),
FlowUtil.variableToNutMap(task.getVariable()).getString("tf_feedback", "")))
.map(ProcessTask::getProcessInstanceId).distinct().count();
tableRow.put(feedbackCode.getCode(), count);
}
tableData.add(tableRow);
});
sortStatisticsRows(tableData, pageForm, orderColumns);
return tableData;
}
/** 按满意度统计页面当前可见字段导出全部统计行。 */
@Override
public void exportUnderTakeSatisfactionAsExcel(PageForm pageForm, String sessionId, String undertakeUnitId,
ExportTableColumns[] tableColumns, HttpServletResponse response) {
List<NutMap> rows = queryUnderTakeSatisfactionStatistics(pageForm, sessionId, undertakeUnitId);
Map<String, String> allowedColumns = new LinkedHashMap<>();
allowedColumns.put("unitName", "单位名称");
allowedColumns.put("sum", "提案总数");
allowedColumns.put("masterSum", "主办提案数");
allowedColumns.put("slaveSum", "协办提案数");
for (Sys_dict feedbackCode : sysDictService.getSubListByCode("PROPOSAL_FEEDBACK")) {
if (StrUtil.isNotBlank(feedbackCode.getCode())) {
allowedColumns.put(feedbackCode.getCode(), feedbackCode.getName());
}
}
exportRowsAsExcel("承办单位满意度统计", rows, tableColumns, allowedColumns, response);
}
/**
* 按页面字段白名单校验列定义并输出 Excel,所有统计导出统一使用 XSSF 格式。
*
* @param title 文件名及工作表标题
* @param rows 待导出的全部匹配数据
* @param tableColumns 页面当前可见字段及顺序
* @param allowedColumns 后端允许字段及规范表头
* @param response HTTP 响应
*/
private void exportRowsAsExcel(String title, List<NutMap> rows, ExportTableColumns[] tableColumns,
Map<String, String> allowedColumns, HttpServletResponse response) {
List<ExportTableColumns> exportColumns = getSafeExportColumns(tableColumns, allowedColumns);
if (exportColumns.isEmpty()) {
throw new BaseException("请至少选择一个导出字段");
}
if (rows == null || rows.isEmpty()) {
throw new BaseException("当前筛选条件下没有可导出的数据");
}
List<ExcelExportEntity> excelColumns = exportColumns.stream()
.map(column -> new ExcelExportEntity(column.getLabel(), column.getProp(), 20))
.collect(Collectors.toList());
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
exportParams.setTitle(title);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, excelColumns, rows);
CommonDownloadUtil.download(title + ".xlsx", workbook, response);
}
/** 按后端白名单过滤重复或伪造字段,并保留页面列设置顺序。 */
private List<ExportTableColumns> getSafeExportColumns(ExportTableColumns[] tableColumns,
Map<String, String> allowedColumns) {
if (ArrayUtil.isEmpty(tableColumns)) {
return List.of();
}
List<ExportTableColumns> exportColumns = new ArrayList<>();
Set<String> selectedProps = new HashSet<>();
for (ExportTableColumns column : tableColumns) {
if (column == null || !allowedColumns.containsKey(column.getProp()) || !selectedProps.add(column.getProp())) {
continue;
}
ExportTableColumns safeColumn = new ExportTableColumns();
safeColumn.setProp(column.getProp());
safeColumn.setLabel(allowedColumns.get(column.getProp()));
exportColumns.add(safeColumn);
}
return exportColumns;
}
/**
* 使用业务页面提供的字段白名单构建排序条件,非法字段或非法排序方向将被忽略。
*
@@ -306,7 +920,7 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
pages.put("commissioner", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "CONFIRM_FILING_COUNT", "CONFIRM_FILING_COUNT", "SUGGESTION_COUNT", "SUGGESTION_COUNT", "NOT_COUNT", "NOT_COUNT", "taskName", "taskName", "instanceState", "ins.state"));
pages.put("feedbackEvaluation", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "merge", "merge", "curTaskName", "curTaskName", "instanceState", "ins.state"));
pages.put("delegation", standardOrderColumns());
pages.put("control", standardOrderColumns());
pages.put("control", mergeOrderColumns(standardOrderColumns(), orderColumns("caseFilingCode", "info.caseFilingCode")));
pages.put("schoolLeaderApproval", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "undertakeUnits", "masterUnitName", "merge", "merge", "curTaskName", "curTaskName", "auditUser", "auditUser", "instanceState", "ins.state"));
pages.put("committeeFilingUnit", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "sessionName", "tcs.fullName", "caseFilingResult", "info.caseFilingResult", "delegationName", "tcd.name", "curTaskName", "curTaskName", "instanceState", "ins.state"));
pages.put("allFinished", orderColumns("code", "info.code", "name", "info.name", "createUserName", "info.createUserName", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "instanceState", "ins.state"));
@@ -318,7 +932,7 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
pages.put("configUnit", orderColumns("name", "t1.name", "code", "t1.code", "unitLeader", "u2.username"));
pages.put("configType", orderColumns("name", "name", "code", "code"));
pages.put("seniorBasic", seniorOrderColumns());
pages.put("expediting", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "delegationName", "tcd.name", "caseFilingResult", "info.caseFilingResult", "curTaskName", "curTaskName", "underTakeName", "underTakeName", "taskName", "t.displayName", "actorName", "ta.actorName", "instanceState", "ins.state"));
pages.put("expediting", orderColumns("code", "info.code", "caseFilingCode", "info.caseFilingCode", "name", "info.name", "typeName", "type.name", "delegationName", "tcd.name", "caseFilingResult", "info.caseFilingResult", "curTaskName", "curTaskName", "underTakeName", "underTakeName", "taskName", "t.displayName", "actorName", "ta.actorName", "instanceState", "ins.state"));
pages.put("seniorDelete", seniorOrderColumns());
pages.put("seniorFeedback", mergeOrderColumns(seniorOrderColumns(), orderColumns("tf_feedback", "tf_feedback")));
pages.put("seniorType", seniorOrderColumns());
@@ -332,7 +946,7 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
/** 创建高级管理列表共用的字段映射。 */
private static Map<String, String> seniorOrderColumns() {
return orderColumns("code", "info.code", "name", "info.name", "createUserName", "info.createUserName", "typeName", "type.name", "delegationName", "tcd.name", "caseFilingResult", "info.caseFilingResult", "isConsolidation", "merge", "curTaskName", "curTaskName", "instanceState", "ins.state");
return orderColumns("code", "info.code", "caseFilingCode", "info.caseFilingCode", "name", "info.name", "createUserName", "info.createUserName", "typeName", "type.name", "delegationName", "tcd.name", "caseFilingResult", "info.caseFilingResult", "isConsolidation", "merge", "curTaskName", "curTaskName", "instanceState", "ins.state");
}
/**
@@ -732,6 +1346,7 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
SELECT
info.name,
info.code,
info.caseFilingResult,
info.brief,
info.measures,
info.createUserName,
@@ -862,13 +1477,29 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
docData.put("secondedList", secondInfos);
docData.put("delegationAuditList", delegationAuditList);
String templateCode = getProposalTemplateCode(docData.getString("caseFilingResult"));
try {
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate("proposal"), config).render(docData).writeAndClose(byteArrayOutputStream);
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate(templateCode), config).render(docData).writeAndClose(byteArrayOutputStream);
} catch (IOException e) {
log.error("提案导出失败{},提案id:{}", e.getMessage(), id);
}
}
/**
* 根据立案结果选择数据库办公模板编码。
*
* @param caseFilingResult 立案结果字典编码:CONFIRM_FILING、SUGGESTION 或 NOT
* @return 对应的 Word 模板编码;历史空值或未知编码返回 proposal
*/
private String getProposalTemplateCode(String caseFilingResult) {
return switch (StrUtil.nullToEmpty(caseFilingResult)) {
case "CONFIRM_FILING" -> PROPOSAL_CONFIRM_FILING_TEMPLATE_CODE;
case "SUGGESTION" -> PROPOSAL_SUGGESTION_TEMPLATE_CODE;
case "NOT" -> PROPOSAL_NOT_TEMPLATE_CODE;
default -> PROPOSAL_DEFAULT_TEMPLATE_CODE;
};
}
/**
* 将现行工作流节点映射到历史提案表模板使用的区块名称,并按主协办单位拆分办理答复。
* 模板仍使用旧节点名称时,避免已完成的意见和答复因键名不一致而无法渲染。
@@ -879,28 +1510,52 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
*/
private void putProposalExportTaskAliases(String proposalId, Map<String, List<ProcessTaskVO>> taskGroups,
NutMap docData) {
putTaskAlias(docData, "提案委主任审核", taskGroups.get("校工会预审核"));
/* putTaskAlias(docData, "提案委主任审核", taskGroups.get("校工会预审核"));
putTaskAlias(docData, "提案委主任意见", taskGroups.get("提案委员会立案"));
putTaskAlias(docData, "提案委委员审议", taskGroups.get("委员会确认承办单位"));
putTaskAlias(docData, "提案委委员审议", taskGroups.get("委员会确认承办单位"));*/
putProposalExportFeedbackName(taskGroups.get("反馈评价"));
List<ProcessTaskVO> replyTasks = taskGroups.get("承办单位答复");
if (Lang.isEmpty(replyTasks)) {
return;
}
Sql masterUnitSql = Sqls.create("SELECT unitName FROM proposal_reply_unit WHERE proposalId = @proposalId AND isMaster = 1")
Sql replyUnitSql = Sqls.create("SELECT unitId, unitName, isMaster FROM proposal_reply_unit WHERE proposalId = @proposalId")
.setParam("proposalId", proposalId);
List<NutMap> masterUnitRows = listMap(masterUnitSql);
Set<String> masterUnitNames = masterUnitRows.stream()
.map(row -> row.getString("unitName"))
List<NutMap> replyUnitRows = listMap(replyUnitSql);
Set<String> masterUnitIds = replyUnitRows.stream()
.filter(row -> row.getBoolean("isMaster", false))
.map(row -> row.getString("unitId"))
.filter(StrUtil::isNotBlank)
.collect(Collectors.toSet());
Map<String, String> replyUnitNameMap = replyUnitRows.stream()
.filter(row -> StrUtil.isNotBlank(row.getString("unitId")))
.collect(Collectors.toMap(row -> row.getString("unitId"), row -> row.getString("unitName", ""),
(left, right) -> left));
List<ProcessTaskVO> masterReplyTasks = new ArrayList<>();
List<ProcessTaskVO> slaveReplyTasks = new ArrayList<>();
for (ProcessTaskVO replyTask : replyTasks) {
Dict taskExt = replyTask.getExt();
Dict taskFormData = replyTask.getTaskFormData();
String unitName = taskFormData == null ? null : taskFormData.getStr("unitName");
if (masterUnitNames.contains(unitName)) {
String underTakeId = taskExt == null ? null : taskExt.getStr("underTakeId");
if (StrUtil.isBlank(underTakeId) && taskFormData != null) {
underTakeId = taskFormData.getStr("unitId");
}
/*
* 承办单位名称保存在任务根变量 underTakeName 中,而模板读取 taskFormData.underTakeName。
* 在本次导出对象中补充 tf_ 前缀字段,由 ProcessTaskVO 转换后即可保持现有模板不变。
*/
if (taskExt != null) {
String underTakeName = taskExt.getStr("underTakeName");
if (StrUtil.isBlank(underTakeName)) {
underTakeName = replyUnitNameMap.getOrDefault(underTakeId, "");
}
taskExt.put("tf_underTakeName", underTakeName);
}
// 主协办关系按单位ID匹配,避免把任务中的单位ID误当成单位名称后全部归入协办答复。
if (masterUnitIds.contains(underTakeId)) {
masterReplyTasks.add(replyTask);
} else {
slaveReplyTasks.add(replyTask);
@@ -910,6 +1565,28 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
putTaskAlias(docData, "协办单位答复", slaveReplyTasks);
}
/**
* 将反馈评价字典编码转换为模板使用的中文名称,仅补充本次导出的任务变量。
*
* @param feedbackTasks 已完成的反馈评价任务;为空时无需处理
*/
private void putProposalExportFeedbackName(List<ProcessTaskVO> feedbackTasks) {
if (Lang.isEmpty(feedbackTasks)) {
return;
}
Map<String, String> feedbackNameMap = sysDictService.getSubListByCode("PROPOSAL_FEEDBACK").stream()
.filter(item -> StrUtil.isNotBlank(item.getCode()))
.collect(Collectors.toMap(Sys_dict::getCode, Sys_dict::getName, (left, right) -> left));
for (ProcessTaskVO feedbackTask : feedbackTasks) {
Dict taskExt = feedbackTask.getExt();
if (taskExt == null) {
continue;
}
String feedbackCode = taskExt.getStr("tf_feedback");
taskExt.put("tf_feedback_name", feedbackNameMap.getOrDefault(feedbackCode, feedbackCode));
}
}
/**
* 仅在存在已完成任务时写入模板区块,空任务不触发 Word 条件标签。
*
@@ -980,7 +1657,7 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
);
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_feedback_name", 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"));
@@ -2,14 +2,23 @@ package com.budwk.app.zhgh.democratic.proposal.service.impl;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessSubmitTypeEnum;
import com.budwk.app.flow.service.FlowCommonService;
import com.budwk.app.flow.service.ProcessTaskService;
import com.budwk.app.sys.models.Sys_role;
import com.budwk.app.sys.models.Sys_user_role;
import com.budwk.app.sys.services.SysRoleService;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalConsolidation;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalMerge;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake;
import com.budwk.app.zhgh.democratic.proposal.service.ProposalCommitteeFilingService;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import org.nutz.aop.interceptor.ioc.TransAop;
@@ -20,7 +29,14 @@ import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.random.R;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@IocBean(args = {"refer:dao"})
public class ProposalCommitteeFilingServiceImpl extends BaseServiceImpl<ProposalInfo> implements ProposalCommitteeFilingService {
@@ -31,14 +47,26 @@ public class ProposalCommitteeFilingServiceImpl extends BaseServiceImpl<Proposal
private FlowCommonService flowCommonService;
@Inject
private ProcessTaskService processTaskService;
@Inject
private SysRoleService sysRoleService;
public ProposalCommitteeFilingServiceImpl(Dao dao) {
super(dao);
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void executeFlowTask(Dict args) {
validateSelectedUnitLeaders(args);
flowCommonService.executeTask(args);
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void merge(Dict args) {
// 并案必须在创建并案关系和执行任一流程任务前完成负责人校验,保证失败时整组数据不发生变化。
validateSelectedUnitLeaders(args);
List<Integer> taskIds = (List<Integer>) args.get("processTaskIds");
List<String> proposalIds = (List<String>) args.get("proposalIds");
@@ -65,6 +93,101 @@ public class ProposalCommitteeFilingServiceImpl extends BaseServiceImpl<Proposal
}
}
/**
* 校验正常提交所选承办单位的负责人配置。
* 确认立案检查全部主办、协办单位;工作建议只检查办理单位;不予立案和退回操作不检查。
* 负责人沿用 WF 承办单位分派口径:用户必须绑定“提案承办单位领导”角色及对应 underTakeId。
*
* @param args 页面流程参数,submitType=1 时根据 tf_caseFilingResult、tf_masterUnitIds、tf_slaveUnitIds 检查
*/
private void validateSelectedUnitLeaders(Dict args) {
Integer submitType = args.getInt(FlowConst.SUBMIT_TYPE);
if (!ProcessSubmitTypeEnum.AGREE.getCode().equals(submitType)) {
return;
}
String caseFilingResult = args.getStr(FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingResult");
if (!List.of("CONFIRM_FILING", "SUGGESTION").contains(caseFilingResult)) {
return;
}
LinkedHashSet<String> selectedUnitIds = new LinkedHashSet<>(
getUnitIdList(args, FlowConst.TASK_FORM_DATA_PREFIX + "masterUnitIds"));
if ("CONFIRM_FILING".equals(caseFilingResult)) {
selectedUnitIds.addAll(getUnitIdList(args, FlowConst.TASK_FORM_DATA_PREFIX + "slaveUnitIds"));
}
if (selectedUnitIds.isEmpty()) {
return;
}
Sys_role unitLeaderRole = sysRoleService.getByCode(RoleConstant.PROPOSAL_UNIT_LEADER);
if (unitLeaderRole == null) {
throw new BaseException("提案承办单位领导角色不存在");
}
Map<String, ProposalUndertake> unitMap = dao().query(ProposalUndertake.class,
Cnd.where(ProposalUndertake::getId, "in", selectedUnitIds))
.stream()
.collect(Collectors.toMap(ProposalUndertake::getId, unit -> unit));
if (unitMap.size() != selectedUnitIds.size()) {
throw new BaseException("选择的承办单位不存在,请重新选择");
}
Set<String> configuredUnitIds = dao().query(Sys_user_role.class,
Cnd.where(Sys_user_role::getRoleId, "=", unitLeaderRole.getId())
.and(Sys_user_role::getUnderTakeId, "in", selectedUnitIds))
.stream()
.map(Sys_user_role::getUnderTakeId)
.filter(StrUtil::isNotBlank)
.collect(Collectors.toSet());
List<String> missingUnitNames = selectedUnitIds.stream()
.filter(unitId -> !configuredUnitIds.contains(unitId))
.map(unitMap::get)
.map(ProposalUndertake::getName)
.toList();
if (!missingUnitNames.isEmpty()) {
throw new BaseException(String.join("", missingUnitNames) + "没有设置负责人!");
}
}
/**
* 将页面单位字段统一转换为去空、去重后的单位ID列表,兼容数组、JSON数组字符串和逗号分隔字符串。
*
* @param args 页面流程参数
* @param key 需要读取的单位字段名
* @return 规范化后的单位ID列表
*/
private List<String> getUnitIdList(Dict args, String key) {
Object value = args.get(key);
if (ObjectUtil.isEmpty(value)) {
return List.of();
}
List<String> unitIds = new ArrayList<>();
if (value instanceof Collection<?> collection) {
collection.forEach(item -> {
if (ObjectUtil.isNotEmpty(item)) {
unitIds.add(String.valueOf(item));
}
});
} else if (value instanceof String stringValue) {
if (StrUtil.isBlank(stringValue)) {
return List.of();
}
if (JSONUtil.isTypeJSONArray(stringValue)) {
unitIds.addAll(JSONUtil.parseArray(stringValue).toList(String.class));
} else {
Arrays.stream(stringValue.split(","))
.map(String::trim)
.filter(StrUtil::isNotBlank)
.forEach(unitIds::add);
}
} else {
unitIds.add(String.valueOf(value));
}
return new ArrayList<>(new LinkedHashSet<>(unitIds));
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void revokeTask(Long taskId, String proposalId) {
@@ -16,6 +16,7 @@ import com.budwk.app.bpm.models.BpmProcessTask;
import com.budwk.app.bpm.service.BpmService;
import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessSubmitTypeEnum;
import com.budwk.app.flow.service.FlowCommonService;
import com.budwk.app.flow.service.ProcessTaskService;
import com.budwk.app.sys.models.Sys_user_role;
@@ -34,6 +35,8 @@ import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
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.Static;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
@@ -87,7 +90,7 @@ public class ProposalCommitteeFilingUnitServiceImpl extends BaseServiceImpl<Prop
if (ObjectUtil.isEmpty(proposalIds)) {
proposalIds = List.of(proposalId);
}
validateAndNormalizeCaseFilingCode(args, proposalIds);
generateAndNormalizeCaseFilingCode(args, proposalIds);
/*
* 先同步 proposal_info 和承办单位,再执行 WF 流转,使查看页面及下一节点读取到的都是本次最新数据。
@@ -111,40 +114,114 @@ public class ProposalCommitteeFilingUnitServiceImpl extends BaseServiceImpl<Prop
}
/**
* 校验并规范化立案编号。确定立案时必须传入不超过 20 个字符且不与历史立案编号重复的编号
* 当前提案及并案提案不参与重复校验,保证流程回退后可按原编号重新提交
* 其他立案结果不保存编号,避免切换选项后把历史输入带入业务表和流程变量。
* 按立案结果和服务器当前年份生成立案编号,并覆盖页面可能传入的编号
* 正常提交时生成编号;退回等非同意操作不占用流水号。并案提案共用同一个编号
*
* @param args 流程提交参数,其中 tf_caseFilingResult 为立案结果,tf_caseFilingCode 为手工填写的立案编号
* @param excludedProposalIds 当前办理的提案及并案提案主键,不参与历史重复判断
* @param args 流程提交参数,tf_caseFilingResult 分别支持 CONFIRM_FILING、SUGGESTION、NOT
* @param proposalIds 当前办理的提案主键集合,并案时包含同组全部提案
*/
private void validateAndNormalizeCaseFilingCode(Dict args, List<String> excludedProposalIds) {
private void generateAndNormalizeCaseFilingCode(Dict args, List<String> proposalIds) {
String caseFilingResultKey = FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingResult";
String caseFilingCodeKey = FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingCode";
String caseFilingTypeKey = FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingType";
if (!"CONFIRM_FILING".equals(args.getStr(caseFilingResultKey))) {
args.put(caseFilingCodeKey, "");
String caseFilingResult = args.getStr(caseFilingResultKey);
if (!"CONFIRM_FILING".equals(caseFilingResult)) {
// 立案类型仅适用于确定立案,切换为其他结果时同步清理历史值。
args.put(caseFilingTypeKey, "");
}
Integer submitType = args.getInt(FlowConst.SUBMIT_TYPE);
if (submitType == null) {
submitType = ProcessSubmitTypeEnum.AGREE.getCode();
args.put(FlowConst.SUBMIT_TYPE, submitType);
}
if (!ProcessSubmitTypeEnum.AGREE.getCode().equals(submitType)) {
args.put(caseFilingCodeKey, "");
return;
}
String caseFilingCode = StrUtil.trim(args.getStr(caseFilingCodeKey));
if (StrUtil.isBlank(caseFilingCode)) {
throw new BaseException("立案编号不能为空");
String codePrefix = resolveCaseFilingCodePrefix(caseFilingResult);
int currentYear = java.time.LocalDate.now().getYear();
String codeHead = currentYear + codePrefix;
/*
* proposal_config 当前是单行配置表,锁定该行可让所有应用实例串行生成流水号。
* 行锁一直持有到本次 WF 提交事务结束,避免两个审核请求同时读到相同最大值。
*/
Sql lockSql = Sqls.create("SELECT id FROM proposal_config ORDER BY id LIMIT 1 FOR UPDATE");
lockSql.setCallback(Sqls.callback.map());
dao().execute(lockSql);
Map<?, ?> configRow = lockSql.getObject(Map.class);
if (ObjectUtil.isEmpty(configRow)) {
throw new BaseException("提案配置不存在,无法生成立案编号");
}
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("立案编号已存在,请重新填写");
String reusableCode = findReusableCaseFilingCode(proposalIds, caseFilingResult, codeHead);
if (StrUtil.isNotBlank(reusableCode)) {
args.put(caseFilingCodeKey, reusableCode);
return;
}
Sql maxSequenceSql = Sqls.create("""
SELECT COALESCE(MAX(CAST(SUBSTRING(caseFilingCode, 6) AS UNSIGNED)), 0)
FROM proposal_info
WHERE caseFilingCode REGEXP @codePattern
""");
maxSequenceSql.setParam("codePattern", "^" + codeHead + "[0-9]{3,}$");
maxSequenceSql.setCallback(Sqls.callback.integer());
dao().execute(maxSequenceSql);
int nextSequence = maxSequenceSql.getInt() + 1;
String caseFilingCode = codeHead + String.format("%03d", nextSequence);
args.put(caseFilingCodeKey, caseFilingCode);
}
/**
* 将立案结果转换成编号前缀,未知结果直接阻止流程提交,避免生成无业务含义的编号。
*
* @param caseFilingResult 立案结果编码
* @return 确定立案返回 A、工作建议返回 B、不予立案返回 C
*/
private String resolveCaseFilingCodePrefix(String caseFilingResult) {
return switch (StrUtil.nullToEmpty(caseFilingResult)) {
case "CONFIRM_FILING" -> "A";
case "SUGGESTION" -> "B";
case "NOT" -> "C";
default -> throw new BaseException("立案结果不正确,无法生成立案编号");
};
}
/**
* 流程撤回或重新提交时,复用同一批提案已有的本年同类型编号,避免流水号无意义增长。
* 只有全部提案的结果和编号完全一致且符合新编号格式时才允许复用。
*
* @param proposalIds 当前单案或并案提案主键集合
* @param caseFilingResult 本次选择的立案结果
* @param codeHead 年份和编号前缀,例如 2026A
* @return 可复用的编号;不存在时返回空字符串
*/
private String findReusableCaseFilingCode(List<String> proposalIds, String caseFilingResult, String codeHead) {
List<ProposalInfo> proposalInfos = dao().query(ProposalInfo.class,
Cnd.where(ProposalInfo::getId, "in", proposalIds));
if (proposalInfos.size() != new LinkedHashSet<>(proposalIds).size()) {
return "";
}
boolean allProposalsMatch = proposalInfos.stream().allMatch(proposalInfo ->
Objects.equals(caseFilingResult, proposalInfo.getCaseFilingResult())
&& StrUtil.isNotBlank(proposalInfo.getCaseFilingCode()));
if (!allProposalsMatch) {
return "";
}
LinkedHashSet<String> existingCodes = proposalInfos.stream()
.map(ProposalInfo::getCaseFilingCode)
.map(StrUtil::trim)
.collect(Collectors.toCollection(LinkedHashSet::new));
if (existingCodes.size() != 1) {
return "";
}
String existingCode = existingCodes.iterator().next();
return existingCode.matches(codeHead + "\\d{3,}") ? existingCode : "";
}
/**
* old-flow 页面提交后,需要把立案编号、立案结果以及主协办单位同步落业务表。
* 若这次改成“不予立案”等无需承办单位的结果,也要先清掉旧的承办单位记录,避免脏数据残留。
@@ -74,7 +74,7 @@ import java.util.zip.ZipOutputStream;
public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> implements ProposalExportService {
private static final String WORK_SUGGESTION_SECTION_MARKER = "__WORK_SUGGESTION_SECTION__";
private static final int UNDERTAKE_UNIT_EXPORT_COLUMN_COUNT = 8;
private static final int UNDERTAKE_UNIT_EXPORT_COLUMN_COUNT = 6;
@Inject
private ProposalCommonService proposalCommonService;
@@ -411,14 +411,14 @@ public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> imp
LinkedHashSet<String> masterUnitNames = getUnitNameSet(proposalRow, "masterUnitNames");
LinkedHashSet<String> slaveUnitNames = getUnitNameSet(proposalRow, "slaveUnitNames");
NutMap excelRow = NutMap.NEW()
.addv("code", proposalRow.getString("code"))
.addv("caseFilingCode", proposalRow.getString("caseFilingCode"))
// 承办单位表统一展示为“立案编号”;未生成立案编号的工作建议回退显示提案编号。
.addv("caseFilingCode", StrUtil.blankToDefault(proposalRow.getString("caseFilingCode"),
proposalRow.getString("code")))
.addv("name", proposalRow.getString("name"))
.addv("createUserName", proposalRow.getString("createUserName"))
.addv("masterUnitNames", "")
.addv("slaveUnitNames", "")
.addv("workSuggestionUnitNames", "")
.addv("remark", "");
.addv("workSuggestionUnitNames", "");
if ("CONFIRM_FILING".equals(proposalRow.getString("caseFilingResult"))) {
excelRow.put("masterUnitNames", String.join("", masterUnitNames));
excelRow.put("slaveUnitNames", String.join("", slaveUnitNames));
@@ -432,14 +432,12 @@ public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> imp
}
if (!suggestionRows.isEmpty()) {
filingRows.add(NutMap.NEW()
.addv("code", WORK_SUGGESTION_SECTION_MARKER)
.addv("caseFilingCode", "")
.addv("caseFilingCode", WORK_SUGGESTION_SECTION_MARKER)
.addv("name", "")
.addv("createUserName", "")
.addv("masterUnitNames", "")
.addv("slaveUnitNames", "")
.addv("workSuggestionUnitNames", "")
.addv("remark", ""));
.addv("workSuggestionUnitNames", ""));
filingRows.addAll(suggestionRows);
}
return filingRows;
@@ -450,14 +448,12 @@ public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> imp
*/
private Workbook buildUndertakeUnitWorkbook(String title, String unitName, List<NutMap> excelRows) {
List<ExcelExportEntity> exportEntities = new ArrayList<>();
exportEntities.add(new ExcelExportEntity("提案编号", "code", 18));
exportEntities.add(new ExcelExportEntity("立案编号", "caseFilingCode", 18));
exportEntities.add(new ExcelExportEntity("提案名称", "name", 45));
exportEntities.add(new ExcelExportEntity("提案名称", "name", 65));
exportEntities.add(new ExcelExportEntity("提案人", "createUserName", 15));
exportEntities.add(new ExcelExportEntity("主办单位", "masterUnitNames", 28));
exportEntities.add(new ExcelExportEntity("协办单位", "slaveUnitNames", 28));
exportEntities.add(new ExcelExportEntity("工作建议送达单位", "workSuggestionUnitNames", 32));
exportEntities.add(new ExcelExportEntity("备注", "remark", 20));
for (ExcelExportEntity exportEntity : exportEntities) {
exportEntity.setWrap(true);
}
@@ -468,14 +464,14 @@ public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> imp
exportParams.setSecondTitle("" + unitName + "");
exportParams.setSheetName("提案承办单位表");
exportParams.setHeaderHeight(30);
exportParams.setHeight((short) 24);
exportParams.setHeight((short) 13);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, exportEntities, excelRows);
mergeWorkSuggestionSection(workbook);
return workbook;
}
/**
* 查找工作建议标记行,将八个单元格合并并设置为居中的分组标题。
* 查找工作建议标记行,将承办单位表的全部单元格合并并设置为居中的分组标题。
*/
private void mergeWorkSuggestionSection(Workbook workbook) {
Sheet sheet = workbook.getSheetAt(0);
@@ -577,47 +573,37 @@ public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> imp
@Override
public void exportProposalStatisticsAsZip(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();
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
sql.setCondition(cnd);
List<NutMap> list = listMap(sql);
List<NutMap> list = queryProposalBaseList(pageForm);
if (Lang.isEmpty(list)) {
throw new BaseException("当前筛选条件下没有可导出的提案数据");
}
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ZipOutputStream zipOutputStream = new ZipOutputStream(bos);
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.exportProposalAsDocx(proposalRow.getString("id"), docxByteArrayOutputStream);
ZipEntry zipEntry = new ZipEntry(proposalRow.getString("code") + "-" + sanitizeZipFileName(proposalRow.getString("name")) + ".docx");
if (docxByteArrayOutputStream.size() == 0) {
throw new BaseException("提案【{}】Word生成失败", proposalRow.getString("code"));
}
ZipEntry zipEntry = new ZipEntry(createUniqueZipDocxEntryName("", proposalRow, entryNameCounts));
zipOutputStream.putNextEntry(zipEntry);
docxByteArrayOutputStream.writeTo(zipOutputStream);
zipOutputStream.closeEntry();
} catch (IOException e) {
// 处理单个文件生成失败的情况
throw new RuntimeException("zip entry write failed, proposalId=" + proposalRow.getString("id"), e);
throw new BaseException("提案【{}】写入压缩包失败", proposalRow.getString("code"));
}
}
zipOutputStream.finish();
zipOutputStream.close();
CommonDownloadUtil.download("提案汇总压缩包.zip", bos.toByteArray(), response);
} catch (Exception e) {
e.printStackTrace();
log.error("批量导出提案表压缩包失败", e);
if (e instanceof BaseException) {
throw (BaseException) e;
}
throw new BaseException("批量导出提案表压缩包失败:{}", e.getMessage());
}
}
@@ -673,7 +659,8 @@ public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> imp
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 teacher_congress_delegate tcde
ON tcde.loginName = info.createUserLoginName AND tcde.sessionId = info.sessionId
LEFT JOIN proposal_reply_unit pru ON pru.proposalId = info.id
$condition
""");
@@ -713,13 +700,14 @@ public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> imp
/**
* 生成 ZIP 内唯一且合法的 Word 文件名,避免提案名称中的非法字符或重名条目导致整个压缩包缺文件。
*
* @param prefix 文档类型前缀
* @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", "未命名提案"));
String prefixText = StrUtil.isBlank(prefix) ? "" : prefix + "-";
String baseName = sanitizeZipFileName(prefixText + proposalRow.getString("code", "未编号") + "-" + proposalRow.getString("name", "未命名提案"));
int sameNameCount = entryNameCounts.merge(baseName, 1, Integer::sum);
return baseName + (sameNameCount > 1 ? "-" + sameNameCount : "") + ".docx";
}
@@ -3,11 +3,17 @@ package com.budwk.app.zhgh.democratic.proposal.service.impl;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.engine.util.FlowUtil;
import com.budwk.app.flow.enums.ProcessSubmitTypeEnum;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalSecond;
import com.budwk.app.zhgh.democratic.proposal.service.ProposalSecondedService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Strings;
@@ -19,6 +25,31 @@ public class ProposalSecondedServiceImpl extends BaseServiceImpl<ProposalSecond>
}
@Override
public boolean isDelegationReviewed(String proposalId) {
if (Strings.isBlank(proposalId)) {
return false;
}
// 以提案最新一次流程实例为准;团长同意或不同意均截止附议,退回提案人则允许后续重新邀请附议。
ProcessInstance processInstance = this.dao().fetch(ProcessInstance.class,
Cnd.where(ProcessInstance::getBusinessNo, "=", proposalId)
.desc(ProcessInstance::getCreatedAt));
if (processInstance == null) {
return false;
}
return this.dao().query(ProcessTask.class,
Cnd.where(ProcessTask::getProcessInstanceId, "=", processInstance.getId())
.and(ProcessTask::getTaskName, "=", "delegation")
.and(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.FINISHED.getCode()))
.stream()
.anyMatch(task -> {
Integer submitType = FlowUtil.variableToDict(task.getVariable()).getInt("submitType");
return ObjectUtil.equals(submitType, ProcessSubmitTypeEnum.AGREE.getCode())
|| ObjectUtil.equals(submitType, ProcessSubmitTypeEnum.REJECT.getCode());
});
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void updateSecondRecord(String proposalId, String taskActorUserId, String opinion, Integer submitType) {
if (Strings.isBlank(proposalId) || Strings.isBlank(taskActorUserId) || submitType == null) {
return;
@@ -12,6 +12,9 @@ import org.nutz.dao.interceptor.annotation.PrevInsert;
@EqualsAndHashCode(callSuper = true)
@Table("teacher_congress_delegate")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@TableIndexes({
@Index(name = "INDEX_TEACHER_CONGRESS_DELEGATE_LOGIN_SESSION", fields = {"loginName", "sessionId"}, unique = false)
})
@Comment("教代会代表")
public class Teacher_congress_delegate extends BaseModel {
@@ -100,6 +100,10 @@ public class TeacherCongressSessionServiceImpl extends BaseServiceImpl<Teacher_c
session.setFullName("" + session.getJ() + "" + session.getC());
insert(session);
// 新届次创建后立即清除角色缓存,使旧届次角色停止参与首页展示和权限判断。
sysUserService.clearCache();
sysRoleService.clearCache();
// 继承上届信息
if (isExtend) {
//代表团角色ID