教代会意见一个单位可以设置多个管理员,审核时只需要一个管理员审核即可完成节点

This commit is contained in:
2026-09-08 19:27:34 +08:00
parent c18579fe40
commit f2b02f8b49
28 changed files with 1233 additions and 325 deletions
@@ -73,6 +73,8 @@ public interface FlowConst {
String COUNTERSIGN_OPERATOR_LIST = "operatorList";
// 会签类型 PARALLEL表示并行会签,SEQUENTIAL表示串行会签
String COUNTERSIGN_TYPE = "countersignType";
// 分组会签任务的服务端快照,包含本轮批次、组ID、组成员及创建时的会签规则。
String COUNTERSIGN_GROUP = "wf_countersignGroup";
// 会签不同意标识
String COUNTERSIGN_DISAGREE_FLAG = "countersignDisagreeFlag";
String ACTOR_IDS_KEY = "actorIds";
@@ -10,25 +10,18 @@ import com.budwk.app.base.result.Result;
import com.budwk.app.base.vo.LabelValueVO;
import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.flow.engine.event.ProcessEvent;
import com.budwk.app.flow.engine.event.ProcessPublisher;
import com.budwk.app.flow.engine.model.ProcessModel;
import com.budwk.app.flow.engine.model.TaskModel;
import com.budwk.app.flow.entity.*;
import com.budwk.app.flow.enums.ProcessEventTypeEnum;
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
import com.budwk.app.flow.enums.ProcessTaskPerformTypeEnum;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.flow.service.FlowCommonService;
import com.budwk.app.flow.vo.HighLightVO;
import com.budwk.app.flow.vo.ProcessInstanceVO;
import com.budwk.app.flow.vo.ProcessTaskVO;
import com.budwk.app.todo.service.TodoService;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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.ioc.aop.Aop;
@@ -53,8 +46,6 @@ public class FlowCommonController {
private Dao dao;
@Inject
private FlowCommonService flowCommonService;
@Inject
private TodoService todoService;
@At("/approval/form")
@SaCheckLogin
@@ -250,44 +241,17 @@ public class FlowCommonController {
return Result.success();
}
/**
* @param taskId 需要撤回的 WF 已办任务 ID,办理人由服务端登录上下文取得
* @return Result 成功响应;资格及流程状态校验交由 service,失败不修改任务
*/
@At
@SaCheckLogin
@Aop(TransAop.READ_COMMITTED)
@ApiOperation("撤销任务")
public Result revokeTask(@Param("taskId") Long taskId) {
// 自己任务
ProcessTask selfTask = dao.fetch(ProcessTask.class, taskId);
selfTask.setTaskState(ProcessTaskStateEnum.DOING.getCode());
// 撤销任务
List<ProcessTask> taskList = dao.query(ProcessTask.class, Cnd.where(ProcessTask::getTaskParentId, "=", taskId));
for (ProcessTask task : taskList) {
task.setTaskState(ProcessTaskStateEnum.WITHDRAW.getCode());
dao.update(task);
}
// 会签并行任务 撤销后续任务
if (selfTask.getPerformType().equals(ProcessTaskPerformTypeEnum.COUNTERSIGN.getCode())) {
List<ProcessTask> doingTasks = dao.query(ProcessTask.class, Cnd.where(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.DOING.getCode())
.and(ProcessTask::getProcessInstanceId, "=", selfTask.getProcessInstanceId()));
for (ProcessTask doingTask : doingTasks) {
doingTask.setTaskState(ProcessTaskStateEnum.WITHDRAW.getCode());
dao.update(doingTask);
}
}
// 激活
dao.update(selfTask);
// 流程激活
dao.update(ProcessInstance.class, Chain.make("state", ProcessInstanceStateEnum.DOING.getCode()),Cnd.where(ProcessInstance::getId, "=", selfTask.getProcessInstanceId()));
// 发送任务撤回事件 确保上面执行成功
for (ProcessTask task : taskList) {
ProcessPublisher.notify(ProcessEvent.builder().eventType(ProcessEventTypeEnum.PROCESS_TASK_REVOKE).sourceId(task.getId()).build());
todoService.taskTerminate(task);
}
return Result.success();
if (taskId == null) return Result.error("任务ID不能为空");
return flowCommonService.revokeTask(taskId);
}
@At("/candidate")
@@ -5,6 +5,8 @@ import com.budwk.app.flow.engine.core.Execution;
import com.budwk.app.flow.engine.model.TaskModel;
import java.util.List;
import java.util.Map;
import java.util.Collections;
/**
*
@@ -20,6 +22,17 @@ public interface AssignmentHandler {
* @return Object 参与者对象
*/
List<String> assign(TaskModel model, Execution execution);
/**
* 提供业务承办单位分组;不支持此维度的处理器返回空映射,由引擎给出配置提示。
* @param model 当前会签节点
* @param execution 含业务单据和承办单位选择的流程上下文
* @param actors 已解析的参与者用户 ID,不能通过分组额外扩大办理权限
* @return 承办单位 ID 到用户 ID 列表的映射;一人可负责多个单位,分别参与各组任务
*/
default Map<String, List<String>> getUndertakeGroups(TaskModel model, Execution execution, List<String> actors) {
return Collections.emptyMap();
}
default String getMessage() {
return this.getClass().getSimpleName();
}
@@ -18,6 +18,7 @@ import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.flow.service.ProcessDefineService;
import com.budwk.app.flow.service.ProcessInstanceService;
import com.budwk.app.flow.service.ProcessTaskService;
import com.budwk.app.flow.service.CountersignGroupService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.ioc.aop.Aop;
@@ -106,6 +107,9 @@ public class FlowEngineImpl implements FlowEngine {
@Override
@Aop(TransAop.READ_COMMITTED)
public List<ProcessTask> executeProcessTask(Long processTaskId, String operator, Dict args) {
if (needsGroupTransaction(processTaskId)) {
return ServiceContext.find(CountersignGroupService.class).inTransaction(() -> executeProcessTask(processTaskId, operator, args));
}
Execution execution = execute(processTaskId, operator, args);
if (execution == null) return Collections.emptyList();
ProcessModel processModel = execution.getProcessModel();
@@ -119,8 +123,12 @@ public class FlowEngineImpl implements FlowEngine {
@Override
@Aop(TransAop.READ_COMMITTED)
public List<ProcessTask> executeAndJumpTask(Long processTaskId, String operator, Dict args, String nodeName) {
if (needsGroupTransaction(processTaskId)) {
return ServiceContext.find(CountersignGroupService.class).inTransaction(() -> executeAndJumpTask(processTaskId, operator, args, nodeName));
}
Execution execution = execute(processTaskId, operator, args);
if (execution == null) return Collections.emptyList();
cancelGroupedBatch(execution);
ProcessModel model = execution.getProcessModel();
if (StrUtil.isEmpty(nodeName)) {
ProcessTask newTask = processTaskService.rejectTask(model, execution.getProcessTask());
@@ -150,8 +158,12 @@ public class FlowEngineImpl implements FlowEngine {
@Override
@Aop(TransAop.READ_COMMITTED)
public List<ProcessTask> executeAndJumpToEnd(Long processTaskId, String operator, Dict args) {
if (needsGroupTransaction(processTaskId)) {
return ServiceContext.find(CountersignGroupService.class).inTransaction(() -> executeAndJumpToEnd(processTaskId, operator, args));
}
Execution execution = execute(processTaskId, operator, args);
if (execution == null) return Collections.emptyList();
cancelGroupedBatch(execution);
ProcessModel model = execution.getProcessModel();
List<EndModel> endModelList = model.getModels(EndModel.class);
endModelList.forEach(endModel -> {
@@ -164,9 +176,14 @@ public class FlowEngineImpl implements FlowEngine {
}
@Override
@Aop(TransAop.READ_COMMITTED)
public List<ProcessTask> executeAndJumpToFirstTaskNode(Long processTaskId, String operator, Dict args) {
if (needsGroupTransaction(processTaskId)) {
return ServiceContext.find(CountersignGroupService.class).inTransaction(() -> executeAndJumpToFirstTaskNode(processTaskId, operator, args));
}
Execution execution = execute(processTaskId, operator, args);
if (execution == null) return Collections.emptyList();
cancelGroupedBatch(execution);
ProcessModel model = execution.getProcessModel();
StartModel startModel = model.getStart();
startModel.getOutputs().forEach(transitionModel -> {
@@ -192,6 +209,18 @@ public class FlowEngineImpl implements FlowEngine {
private Execution execute(Long processTaskId, String operator, Dict args) {
// 1.1 根据id查询正在进行中的流程任务
ProcessTask processTask = processTaskService.getById(processTaskId);
Dict groupState = CountersignGroupService.snapshot(processTask);
if (groupState != null) {
processTask = ServiceContext.find(CountersignGroupService.class).lockTask(processTask);
groupState = CountersignGroupService.snapshot(processTask);
// 计数和分组只能由服务端生成,不能用表单参数伪造完成组数或覆盖所属组织。
args.keySet().removeIf(key -> key.startsWith(FlowConst.COUNTERSIGN_VARIABLE_PREFIX));
if (Integer.valueOf(20).equals(args.getInt(FlowConst.SUBMIT_TYPE))) {
args.set(FlowConst.COUNTERSIGN_DISAGREE_FLAG, 1);
}
}
args.remove(FlowConst.COUNTERSIGN_GROUP);
if (groupState != null) args.set(FlowConst.COUNTERSIGN_GROUP, groupState);
if (processTask == null || !ProcessTaskStateEnum.DOING.getCode().equals(processTask.getTaskState())) {
throw new BaseException("没有进行中的流程任务");
}
@@ -236,5 +265,19 @@ public class FlowEngineImpl implements FlowEngine {
return execution;
}
/** 引擎由工厂创建,不能单靠方法注解;分组提交显式建立覆盖校验、完成和流转的事务。 */
private boolean needsGroupTransaction(Long taskId) {
return org.nutz.trans.Trans.isTransactionNone()
&& CountersignGroupService.snapshot(processTaskService.getById(taskId)) != null;
}
/** 分组节点退回/跳转时关闭本轮剩余组,防止旧组在下游再次提交。 */
@Aop(TransAop.READ_COMMITTED)
private void cancelGroupedBatch(Execution execution) {
if (CountersignGroupService.snapshot(execution.getProcessTask()) != null) {
ServiceContext.find(CountersignGroupService.class).cancelBatch(execution);
}
}
}
@@ -26,6 +26,12 @@ public class CountersignHandler implements IHandler {
@Override
public void handle(Execution execution) {
// 已生成任务按服务器快照办理,旧任务不因最新流程增加配置而改变计数方式。
if (com.budwk.app.flow.service.CountersignGroupService.snapshot(execution.getProcessTask()) != null) {
com.budwk.app.flow.engine.core.ServiceContext.find(com.budwk.app.flow.service.CountersignGroupService.class)
.complete(taskModel, execution);
return;
}
boolean isMerged = false;
String countersignType = taskModel.getExt().get("countersignType", "PARALLEL");
String countersignCompletionCondition = taskModel.getExt().get("countersignCompletionCondition", "");
@@ -41,13 +41,17 @@ public class TaskModel extends NodeModel {
*/
private String countersignCompletionCondition;
/** 会签办理维度:PERSON 按人员(兼容旧定义)、UNIT 所属单位、UNION 工会、UNDERTAKE 承办单位。 */
private String countersignGroupBy = "PERSON";
private String h5form;
@Override
public void exec(Execution execution) {
// 执行任务节点自定义执行逻辑
System.out.println(super.toString());
if (ProcessTaskPerformTypeEnum.COUNTERSIGN.equals(performType)) {
if (ProcessTaskPerformTypeEnum.COUNTERSIGN.equals(performType)
|| com.budwk.app.flow.service.CountersignGroupService.snapshot(execution.getProcessTask()) != null) {
// 会签任务处理
fire(new CountersignHandler(this), execution);
if (execution.isMerged()) {
@@ -36,6 +36,7 @@ public interface NodeParser {
String EXT_FIELD_CANDIDATE_GROUPS_KEY = "candidateGroups";
String EXT_FIELD_CANDIDATE_HANDLER_KEY = "candidateHandler";
String EXT_FIELD_COUNTERSIGN_TYPE_KEY = "countersignType"; // 会签类型
String EXT_FIELD_COUNTERSIGN_GROUP_BY_KEY = "countersignGroupBy"; // 会签办理维度
String EXT_FIELD_COUNTERSIGN_COMPLETION_CONDITION_KEY = "countersignCompletionCondition"; // 会签完成条件
String CLASS_KEY = "clazz"; // 类路径
String METHOD_NAME_KEY = "methodName"; // 方法名
@@ -62,6 +62,9 @@ public class TaskParser extends AbstractNodeParser {
taskModel.setExt(Dict.create());
}
// 将其他properties添加到ext中
// 新定义优先读取顶层字段,兼容扩展属性;旧定义缺失时继续按人员会签。
taskModel.setCountersignGroupBy(properties.get(EXT_FIELD_COUNTERSIGN_GROUP_BY_KEY,
taskModel.getExt().getStr(EXT_FIELD_COUNTERSIGN_GROUP_BY_KEY)));
properties.forEach((k,v)->{
if(!ReflectUtil.hasField(TaskModel.class,k)){
taskModel.getExt().set(k,v);
@@ -0,0 +1,375 @@
package com.budwk.app.flow.service;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.extra.expression.ExpressionUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.json.JSONUtil;
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.engine.AssignmentHandler;
import com.budwk.app.flow.engine.core.Execution;
import com.budwk.app.flow.engine.core.ServiceContext;
import com.budwk.app.flow.engine.event.ProcessEvent;
import com.budwk.app.flow.engine.event.ProcessPublisher;
import com.budwk.app.flow.engine.model.TaskModel;
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.CountersignTypeEnum;
import com.budwk.app.flow.enums.ProcessEventTypeEnum;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.flow.enums.ProcessTaskPerformTypeEnum;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import org.nutz.aop.interceptor.ioc.TransAop;
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.aop.Aop;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import java.util.*;
import java.util.stream.Collectors;
/**
* WF 分组会签:每组一份共享任务,组内任一人办理,组间按创建时的会签规则流转。
* 规则、组成员和批次均保存到任务变量,不增加数据库字段;没有快照的旧任务继续走原逻辑。
*/
@IocBean(args = {"refer:dao"})
public class CountersignGroupService extends BaseServiceImpl<ProcessTask> {
public CountersignGroupService(Dao dao) {
super(dao);
}
/** @param action 一次完整的分组审批操作。@return 该操作生成的后续任务列表;异常整体回滚。 */
@Aop(TransAop.READ_COMMITTED)
public List<ProcessTask> inTransaction(java.util.function.Supplier<List<ProcessTask>> action) {
java.util.concurrent.atomic.AtomicReference<List<ProcessTask>> result = new java.util.concurrent.atomic.AtomicReference<>();
org.nutz.trans.Trans.exec(java.sql.Connection.TRANSACTION_READ_COMMITTED, () -> result.set(action.get()));
return result.get();
}
/** @param model 节点定义;缺省 PERSON 保持原行为。@return 是否启用组织分组,非法配置直接报错。 */
public static boolean enabled(TaskModel model) {
String mode = StrUtil.blankToDefault(model.getCountersignGroupBy(), "PERSON");
if (!List.of("PERSON", "UNIT", "UNION", "UNDERTAKE").contains(mode)) {
throw new BaseException("不支持的会签办理维度:" + mode);
}
return !"PERSON".equals(mode);
}
/** @param task 已存储任务。@return 本任务的分组快照;排除从上一节点继承的快照,旧任务返回空。 */
public static Dict snapshot(ProcessTask task) {
if (task == null || StrUtil.isBlank(task.getVariable())) return null;
Object value = FlowUtil.variableToDict(task.getVariable()).get(FlowConst.COUNTERSIGN_GROUP);
if (value == null) return null;
Dict state = JSONUtil.toBean(JSONUtil.toJsonStr(value), Dict.class);
return StrUtil.equals(task.getTaskName(), state.getStr("node")) && StrUtil.isNotBlank(state.getStr("batch"))
? state : null;
}
/**
* 从人员资料或业务处理器取得组织分组,严格校验缺失/歧义,禁止把空单位的人合为一组。
* @param model 节点办理维度及参与者处理类
* @param execution 业务上下文,承办单位由对应业务 service 解析
* @param actors 已有参与者 ID 集合
* @return 有序的组织 ID 到人员 ID 列表,保持首位参与者所在组的顺序
*/
public Map<String, List<String>> resolveGroups(TaskModel model, Execution execution, List<String> actors) {
if (!enabled(model)) throw new BaseException("按人员会签不需要组织分组");
List<String> users = actors.stream().filter(StrUtil::isNotBlank).distinct().toList();
if (users.isEmpty()) return new LinkedHashMap<>();
Map<String, List<String>> groups = new LinkedHashMap<>();
if ("UNDERTAKE".equals(model.getCountersignGroupBy())) {
if (StrUtil.isBlank(model.getAssignmentHandler())) {
throw new BaseException("按承办单位会签需要配置支持承办分组的参与者处理类");
}
AssignmentHandler handler = ReflectUtil.newInstance(model.getAssignmentHandler());
groups.putAll(handler.getUndertakeGroups(model, execution, users));
} else {
// vw_user 中的单位、工会是人员的所属组织,不等于承办单位负责人角色的管理范围。
Sql sql = Sqls.create("SELECT id,unitId,unionId FROM vw_user WHERE id IN (@ids)");
sql.setParam("ids", users);
List<NutMap> records = listMap(sql);
String field = "UNIT".equals(model.getCountersignGroupBy()) ? "unitId" : "unionId";
for (String user : users) {
List<String> keys = records.stream().filter(row -> user.equals(row.getString("id")))
.map(row -> row.getString(field)).filter(StrUtil::isNotBlank).distinct().toList();
if (keys.size() != 1) throw new BaseException("会签人员所属组织缺失或不唯一:" + user);
groups.computeIfAbsent(keys.get(0), key -> new ArrayList<>()).add(user);
}
}
Set<String> covered = new HashSet<>();
for (Map.Entry<String, List<String>> entry : groups.entrySet()) {
if (StrUtil.isBlank(entry.getKey()) || entry.getValue() == null || entry.getValue().isEmpty()
|| !users.containsAll(entry.getValue())) {
throw new BaseException("会签分组缺少组织、成员或包含未授权人员");
}
entry.setValue(entry.getValue().stream().distinct().toList());
covered.addAll(entry.getValue());
}
if (!covered.containsAll(users)) throw new BaseException("部分会签人员无法匹配承办单位,请检查负责人配置");
return groups;
}
/**
* @param model 新进入的会签节点
* @param execution 当前流程上下文
* @param actors 已解析的参与者 ID
* @return 新创建的组任务;并行创建全部组,串行仅创建首组,每组关联全部可办理人员
*/
@Aop(TransAop.READ_COMMITTED)
public List<ProcessTask> createTasks(TaskModel model, Execution execution, List<String> actors) {
Map<String, List<String>> groups = resolveGroups(model, execution, actors);
if (groups.isEmpty()) return new ArrayList<>();
Dict state = Dict.create().set("batch", UUID.randomUUID().toString()).set("node", model.getName())
.set("mode", model.getCountersignGroupBy()).set("groups", groups)
.set("type", model.getCountersignType().name())
.set("condition", StrUtil.nullToEmpty(model.getCountersignCompletionCondition()));
List<String> keys = new ArrayList<>(groups.keySet());
List<String> first = CountersignTypeEnum.PARALLEL.equals(model.getCountersignType()) ? keys : keys.subList(0, 1);
List<ProcessTask> tasks = new ArrayList<>();
for (String key : first) tasks.add(createGroupTask(model, execution, state, key, groups.get(key), null));
updateCounts(execution, state, Collections.emptyList(), tasks.size());
return tasks;
}
/** 创建一组共享任务;串行后续组复用首组的表单和业务快照,避免丢失承办信息。 */
@Aop(TransAop.READ_COMMITTED)
protected ProcessTask createGroupTask(TaskModel model, Execution execution, Dict state, String key,
List<String> members, ProcessTask previous) {
ProcessTask task = previous == null ? new ProcessTask() : cn.hutool.core.bean.BeanUtil.copyProperties(previous, ProcessTask.class);
task.setId(null);
task.setOperator(null);
task.setFinishTime(null);
task.setCreatedBy(null);
task.setUpdatedBy(null);
task.setTaskName(model.getName());
task.setDisplayName(model.getDisplayName());
task.setFormKey(model.getForm());
task.setH5FormKey(model.getH5form());
task.setProcessInstanceId(execution.getProcessInstanceId());
task.setPerformType(ProcessTaskPerformTypeEnum.COUNTERSIGN.getCode());
task.setTaskType(model.getTaskType().getCode());
task.setTaskState(ProcessTaskStateEnum.DOING.getCode());
task.setTaskParentId(execution.getProcessTaskId() == null ? 0L : execution.getProcessTaskId());
task.setCreatedAt(System.currentTimeMillis());
task.setUpdatedAt(System.currentTimeMillis());
Dict variable = previous == null ? Dict.create() : FlowUtil.variableToDict(previous.getVariable());
variable.putAll(execution.getArgs());
Dict ownState = JSONUtil.toBean(JSONUtil.toJsonStr(state), Dict.class);
ownState.set("group", key);
variable.set(FlowConst.COUNTERSIGN_GROUP, ownState);
variable.set(FlowConst.COUNTERSIGN_VARIABLE_PREFIX + FlowConst.COUNTERSIGN_TYPE, state.getStr("type"));
variable.set(FlowConst.IS_FIRST_TASK_NODE, FlowUtil.isFistTaskName(execution.getProcessModel(), model.getName()));
if (StrUtil.isNotBlank(model.getExpireTime())) task.setExpireTime(FlowUtil.processTime(model.getExpireTime(), variable));
task.setVariable(JSONUtil.toJsonStr(variable));
insert(task);
execution.getEngine().processTaskService().addTaskActor(task.getId(), members);
return task;
}
/**
* 对同一流程实例加行锁,序列化同组重复提交及不同组同时完成,锁由外层流程事务持有到提交。
* @param task 分组任务
* @return 加锁后重新读取的任务(调用方必须再次校验状态和权限)
*/
@Aop(TransAop.READ_COMMITTED)
public ProcessTask lockTask(ProcessTask task) {
Sql sql = Sqls.create("SELECT id FROM wf_process_instance WHERE id=@id FOR UPDATE");
sql.setParam("id", task.getProcessInstanceId());
dao().execute(sql);
return fetch(task.getId());
}
/** @param task 本轮任一分组任务。@return 同实例同节点且同批次的任务,排除退回重走产生的旧轮次。 */
protected List<ProcessTask> batchTasks(ProcessTask task) {
String batch = snapshot(task).getStr("batch");
return query(Cnd.where("processInstanceId", "=", task.getProcessInstanceId()).and("taskName", "=", task.getTaskName()))
.stream().filter(item -> snapshot(item) != null && batch.equals(snapshot(item).getStr("batch"))).toList();
}
/** 读取分组快照时重新反序列化,避免在共享定义对象或其他任务的成员列表上原地修改。 */
private Map<String, List<String>> members(Dict state) {
Map<String, Object> raw = JSONUtil.toBean(JSONUtil.toJsonStr(state.get("groups")), Dict.class);
Map<String, List<String>> result = new LinkedHashMap<>();
raw.forEach((key, value) -> result.put(key, Convert.toList(String.class, value)));
return result;
}
/** 按已完成组去重计数;转办任务沿用组 ID,不会多算一次。 */
@Aop(TransAop.READ_COMMITTED)
protected void updateCounts(Execution execution, Dict state, List<ProcessTask> completed, int active) {
String prefix = FlowConst.COUNTERSIGN_VARIABLE_PREFIX + state.getStr("node") + "_";
List<String> keys = new ArrayList<>(members(state).keySet());
Set<String> done = completed.stream().map(task -> snapshot(task).getStr("group")).collect(Collectors.toSet());
Set<String> agree = completed.stream().filter(task -> !Integer.valueOf(20).equals(
FlowUtil.variableToDict(task.getVariable()).getInt(FlowConst.SUBMIT_TYPE)))
.map(task -> snapshot(task).getStr("group")).collect(Collectors.toSet());
Dict counts = Dict.create().set(prefix + FlowConst.NR_OF_INSTANCES, keys.size())
.set(prefix + FlowConst.NR_OF_COMPLETED_INSTANCES, done.size())
.set(prefix + FlowConst.NR_OF_COMPLETED_AGREE_INSTANCES, agree.size())
.set(prefix + FlowConst.NR_OF_ACTIVATE_INSTANCES, active)
.set(prefix + FlowConst.COUNTERSIGN_OPERATOR_LIST, keys)
.set(prefix + FlowConst.LOOP_COUNTER, keys.indexOf(state.getStr("group")));
execution.getArgs().putAll(counts);
execution.getEngine().processInstanceService().addVariable(execution.getProcessInstanceId(), counts);
}
/**
* @param model 当前节点模型,用于路由,不用最新定义覆盖已生成任务的分组规则
* @param execution 当前完成的任务和执行人;调用前任务已在持有实例锁的事务中置为完成
* @return 无返回值,通过 execution.merged 决定是否进入下一节点
*/
@Aop(TransAop.READ_COMMITTED)
public void complete(TaskModel model, Execution execution) {
Dict state = snapshot(execution.getProcessTask());
List<ProcessTask> batch = batchTasks(execution.getProcessTask());
List<ProcessTask> completed = batch.stream().filter(task -> task.getTaskState().equals(ProcessTaskStateEnum.FINISHED.getCode())).toList();
int active = (int) batch.stream().filter(task -> task.getTaskState().equals(ProcessTaskStateEnum.DOING.getCode())).count();
updateCounts(execution, state, completed, active);
String prefix = FlowConst.COUNTERSIGN_VARIABLE_PREFIX + model.getName() + "_";
int done = execution.getArgs().getInt(prefix + FlowConst.NR_OF_COMPLETED_INSTANCES);
int total = members(state).size();
String condition = state.getStr("condition");
boolean veto = "ONE_VOTE_VETO".equalsIgnoreCase(condition);
boolean merged = veto && execution.getArgs().containsKey(FlowConst.COUNTERSIGN_DISAGREE_FLAG);
if (!veto && StrUtil.isNotBlank(condition)) {
Dict values = Dict.create();
execution.getArgs().forEach((key, value) -> values.set(key.startsWith(prefix) ? key.substring(prefix.length()) : key, value));
merged = Convert.toBool(ExpressionUtil.eval(condition, values));
}
if (!merged && done == total) {
if (!veto && StrUtil.isNotBlank(condition)) {
// 不允许留下“全部组已完成但无人可办”的僵死节点,回滚本次提交并提示修改条件。
throw new BaseException("所有组办理后仍不满足会签完成条件,请联系流程管理员检查条件");
} else {
merged = true;
}
}
if (!merged && done < total && "SEQUENTIAL".equals(state.getStr("type")) && active == 0) {
Set<String> doneKeys = completed.stream().map(task -> snapshot(task).getStr("group")).collect(Collectors.toSet());
Map<String, List<String>> groups = members(state);
String next = groups.keySet().stream().filter(key -> !doneKeys.contains(key)).findFirst().orElseThrow();
ProcessTask task = createGroupTask(model, execution, state, next, groups.get(next), execution.getProcessTask());
execution.addTask(task);
updateCounts(execution, state, completed, 1);
com.budwk.app.flow.engine.core.ServiceContext.findList(com.budwk.app.flow.engine.FlowInterceptor.class)
.forEach(interceptor -> interceptor.intercept(execution));
// 沿用 WF 创建事件,使下一组所有成员收到同一份待办。
ProcessPublisher.notify(ProcessEvent.builder().eventType(ProcessEventTypeEnum.PROCESS_TASK_START).sourceId(task.getId()).build());
}
if (merged) {
cancelBatch(execution);
}
execution.setMerged(merged);
}
/** @param execution 需要结束的本轮分组会签。@return 无返回值,关闭剩余组并清理本节点计数。 */
@Aop(TransAop.READ_COMMITTED)
public void cancelBatch(Execution execution) {
for (ProcessTask task : batchTasks(execution.getProcessTask())) {
if (ProcessTaskStateEnum.DOING.getCode().equals(task.getTaskState())) {
// 不把已完成组的快照覆盖到其他组。
Dict args = Dict.create();
args.putAll(execution.getArgs());
args.remove(FlowConst.COUNTERSIGN_GROUP);
execution.getEngine().processTaskService().abandonProcessTask(task.getId(), FlowConst.AUTO_ID, args);
}
}
String prefix = FlowConst.COUNTERSIGN_VARIABLE_PREFIX + execution.getProcessTask().getTaskName() + "_";
List<String> keys = execution.getArgs().keySet().stream().filter(key -> key.startsWith(prefix)).collect(Collectors.toList());
keys.add(FlowConst.COUNTERSIGN_DISAGREE_FLAG);
execution.getEngine().processInstanceService().removeVariable(execution.getProcessInstanceId(), keys.toArray(new String[0]));
keys.forEach(execution.getArgs()::remove);
execution.getArgs().remove(FlowConst.COUNTERSIGN_GROUP);
}
/**
* 分组加签只增加当前组的可办理人,不增加会签组数;跨组织加签必须重新配置并发起节点。
* @param taskId 当前共享任务 ID
* @param actors 新增用户 ID 集合
* @return 无返回值,新增任务参与者
*/
@Aop(TransAop.READ_COMMITTED)
public void addMembers(Long taskId, List<String> actors) {
ProcessTask task = lockTask(fetch(taskId));
if (!ProcessTaskStateEnum.DOING.getCode().equals(task.getTaskState())) throw new BaseException("任务已办理,无法加签");
Dict state = snapshot(task);
if ("UNDERTAKE".equals(state.getStr("mode"))) {
// 承办单位只能增加该单位当前配置的负责人,不能把其他单位混进本组。
Sql sql = Sqls.create("SELECT DISTINCT ur.userId AS id FROM sys_user_role ur JOIN sys_role r ON r.id=ur.roleId WHERE r.code='PROPOSAL_UNIT_LEADER' AND ur.underTakeId=@unitId AND ur.userId IN (@ids)");
sql.setParam("unitId", state.getStr("group")).setParam("ids", actors);
if (!listMap(sql).stream().map(row -> row.getString("id")).toList().containsAll(actors)) {
throw new BaseException("分组加签只允许当前承办单位的负责人");
}
} else {
TaskModel model = new TaskModel();
model.setCountersignGroupBy(state.getStr("mode"));
Map<String, List<String>> groups = resolveGroups(model, null, actors);
if (groups.size() != 1 || !groups.containsKey(state.getStr("group"))) throw new BaseException("分组加签只允许同一单位或工会");
}
ServiceContext.find(ProcessTaskService.class).addTaskActor(taskId, actors);
}
/**
* 判断本组是否仍允许由实际办理人局部撤回,不产生数据库写入。
* @param task 已存储的任务;列表读取快照,写操作必须先加实例锁并重新读取
* @param operator 当前登录用户 ID,必须由服务端登录上下文取得
* @return null 表示允许;非空为拒绝原因,供按钮判断和撤回接口共用
*/
public String revokeUnavailableReason(ProcessTask task, String operator) {
if (task == null) return "任务不存在";
if (!ProcessTaskStateEnum.FINISHED.getCode().equals(task.getTaskState())
|| StrUtil.isBlank(operator) || !Objects.equals(task.getOperator(), operator)) {
return "只能撤回本人已办理的任务";
}
Dict state = snapshot(task);
if (state == null) return "任务不是有效的分组会签任务";
if (!"PARALLEL".equals(state.getStr("type"))
|| batchTasks(task).stream().noneMatch(item -> ProcessTaskStateEnum.DOING.getCode().equals(item.getTaskState()))) {
return "该会签已流转或为串行会签,不能局部撤回,请通过流程退回处理";
}
ProcessInstance instance = dao().fetch(ProcessInstance.class, task.getProcessInstanceId());
if (instance == null || !Integer.valueOf(10).equals(instance.getState())) {
return "流程不在办理中,无法撤回";
}
String prefix = FlowConst.COUNTERSIGN_VARIABLE_PREFIX + task.getTaskName() + "_";
if (!FlowUtil.variableToDict(instance.getVariable()).containsKey(prefix + FlowConst.NR_OF_INSTANCES)) {
return "会签节点已经结束,不能局部撤回";
}
return null;
}
/**
* 仅在本轮会签尚有其他组待办时允许撤回本组;节点流转后拒绝局部撤回,避免破坏下游会签。
* @param task 已完成的本组任务
* @return 无返回值,恢复原共享任务并重算本轮组数,不撤销其他单位的待办
*/
@Aop(TransAop.READ_COMMITTED)
public void revoke(ProcessTask task) {
if (task == null) throw new BaseException("任务不存在");
task = lockTask(task);
// 列表仅提示是否可撤回,提交时必须在实例锁内按最新状态重新检查。
String reason = revokeUnavailableReason(task, SecurityUtil.getUserId());
if (reason != null) throw new BaseException(reason);
Dict state = snapshot(task);
ProcessInstance instance = dao().fetch(ProcessInstance.class, task.getProcessInstanceId());
task.setTaskState(ProcessTaskStateEnum.DOING.getCode());
task.setOperator(null);
task.setFinishTime(null);
dao().update(task, "taskState|operator|finishTime");
Execution execution = new Execution();
execution.setProcessInstanceId(instance.getId());
execution.setArgs(FlowUtil.variableToDict(instance.getVariable()));
execution.setEngine(ServiceContext.find(com.budwk.app.flow.engine.FlowEngine.class));
List<ProcessTask> refreshed = batchTasks(task);
updateCounts(execution, state, refreshed.stream().filter(item -> item.getTaskState().equals(20)).toList(),
(int) refreshed.stream().filter(item -> item.getTaskState().equals(10)).count());
ProcessPublisher.notify(ProcessEvent.builder().eventType(ProcessEventTypeEnum.PROCESS_TASK_START).sourceId(task.getId()).build());
}
}
@@ -2,6 +2,8 @@ package com.budwk.app.flow.service;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.result.Result;
import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.engine.FlowEngine;
@@ -11,6 +13,7 @@ import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.*;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.todo.service.TodoService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
@@ -20,6 +23,7 @@ import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import java.util.List;
import java.util.Objects;
@IocBean
public class FlowCommonService {
@@ -28,11 +32,16 @@ public class FlowCommonService {
private FlowEngine flowEngine;
@Inject
private Dao dao;
@Inject
private CountersignGroupService countersignGroupService;
@Inject
private TodoService todoService;
/**
* 执行任务
* @param args
*/
@Aop(TransAop.READ_COMMITTED)
public void executeTask(Dict args){
Long processTaskId = args.getLong(FlowConst.PROCESS_TASK_ID_KEY);
String operator = SecurityUtil.getUserId();
@@ -81,11 +90,70 @@ public class FlowCommonService {
/**
* @param taskId 页面返回的真实 WF 任务 ID;为空或不存在时不允许撤回
* @return 当前登录人能否撤回,只读结果供按钮展示,实际提交仍会加锁复查
*/
public boolean canRevokeTask(Long taskId) {
return taskId != null && revokeUnavailableReason(dao.fetch(ProcessTask.class, taskId), SecurityUtil.getUserId()) == null;
}
/**
* 统一列表和操作入口的撤回资格,参与者身份不等于实际办理人身份。
* @param task 数据库任务记录
* @param operator 服务端取得的当前登录用户 ID,不接受前端指定的操作人
* @return null 表示允许撤回,非空为拒绝原因;分组任务复用分组服务的规则
*/
public String revokeUnavailableReason(ProcessTask task, String operator) {
if (task == null) return "任务不存在";
if (!ProcessTaskStateEnum.FINISHED.getCode().equals(task.getTaskState())
|| StrUtil.isBlank(operator) || !Objects.equals(task.getOperator(), operator)) {
return "只能撤回本人已办理的任务";
}
if (CountersignGroupService.snapshot(task) != null) {
return countersignGroupService.revokeUnavailableReason(task, operator);
}
ProcessInstance instance = dao.fetch(ProcessInstance.class, task.getProcessInstanceId());
if (instance == null || !ProcessInstanceStateEnum.DOING.getCode().equals(instance.getState())) {
return "流程不在办理中,无法撤回";
}
List<ProcessTask> children = dao.query(ProcessTask.class, Cnd.where("taskParentId", "=", task.getId())
.and("processInstanceId", "=", task.getProcessInstanceId()));
// 普通任务仅在直接后续任务尚未办理时保留原撤回能力,不抹掉下游已办记录。
if (children.isEmpty() || children.stream().anyMatch(child -> !ProcessTaskStateEnum.DOING.getCode().equals(child.getTaskState()))) {
return "后续任务已办理或不存在,不能直接撤回";
}
if (ProcessTaskPerformTypeEnum.COUNTERSIGN.getCode().equals(task.getPerformType())) {
List<ProcessTask> active = dao.query(ProcessTask.class, Cnd.where("processInstanceId", "=", task.getProcessInstanceId())
.and("taskState", "=", ProcessTaskStateEnum.DOING.getCode()));
// 无分组快照的旧会签不能通过旧撤回逻辑关闭其他单位或其他分支的待办。
if (active.stream().anyMatch(item -> !Objects.equals(item.getTaskParentId(), task.getId()))) {
return "旧会签仍有其他任务办理中,不能直接撤回";
}
}
return null;
}
/**
* @param taskId 待撤回的已办任务 ID,仅允许实际办理人操作
* @return 成功 Result;无权限或状态变化抛出业务异常,事务整体回滚
*/
@Aop(TransAop.READ_COMMITTED)
public Result revokeTask(Long taskId) {
// 自己任务
if (taskId == null) throw new BaseException("任务ID不能为空");
ProcessTask selfTask = dao.fetch(ProcessTask.class, taskId);
if (selfTask == null) throw new BaseException("任务不存在");
// 与分组办理持有同一实例行锁;锁内重读避免按钮显示后流程已继续流转。
selfTask = countersignGroupService.lockTask(selfTask);
String reason = revokeUnavailableReason(selfTask, SecurityUtil.getUserId());
if (reason != null) throw new BaseException(reason);
if (CountersignGroupService.snapshot(selfTask) != null) {
countersignGroupService.revoke(selfTask);
return Result.success();
}
selfTask.setTaskState(ProcessTaskStateEnum.DOING.getCode());
selfTask.setOperator(null);
selfTask.setFinishTime(null);
// 撤销任务
List<ProcessTask> taskList = dao.query(ProcessTask.class, Cnd.where(ProcessTask::getTaskParentId, "=", taskId));
@@ -112,6 +180,8 @@ public class FlowCommonService {
// 发送任务撤回事件 确保上面执行成功
for (ProcessTask task : taskList) {
ProcessPublisher.notify(ProcessEvent.builder().eventType(ProcessEventTypeEnum.PROCESS_TASK_REVOKE).sourceId(task.getId()).build());
// 保留原 controller 撤回入口对外部待办的终止处理。
todoService.taskTerminate(task);
}
return Result.success();
@@ -29,6 +29,7 @@ import com.budwk.app.flow.enums.*;
import com.budwk.app.flow.service.ProcessDefineService;
import com.budwk.app.flow.service.ProcessInstanceService;
import com.budwk.app.flow.service.ProcessTaskService;
import com.budwk.app.flow.service.CountersignGroupService;
import com.budwk.app.flow.vo.ProcessTaskVO;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import org.nutz.aop.interceptor.ioc.TransAop;
@@ -165,6 +166,8 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
@Override
@Aop(TransAop.READ_COMMITTED)
public List<ProcessTask> createTask(TaskModel taskModel, Execution execution) {
// 分组快照只属于其创建的会签任务,不传递给下一节点。
execution.getArgs().remove(FlowConst.COUNTERSIGN_GROUP);
List<ProcessTask> processTaskList = new ArrayList<>();
long now = System.currentTimeMillis();
ProcessTask processTask = new ProcessTask();
@@ -236,6 +239,10 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
public void addCandidateActor(Long processTaskId, List<String> actors) {
if (CollectionUtil.isEmpty(actors)) return;
ProcessTask processTask = fetch(processTaskId);
if (CountersignGroupService.snapshot(processTask) != null) {
ServiceContext.find(CountersignGroupService.class).addMembers(processTaskId, actors);
return;
}
String prefix = FlowConst.COUNTERSIGN_VARIABLE_PREFIX + processTask.getTaskName() + "_";
// 主要调整流程变量中的参与者
ProcessInstance processInstance = dao().fetch(ProcessInstance.class, processTask.getProcessInstanceId());
@@ -387,6 +394,15 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
public List<ProcessTask> createCountersignTask(TaskModel taskModel, Execution execution) {
List<ProcessTask> processTaskList = new ArrayList<>();
List<String> taskActors = getTaskActors(taskModel, execution);
// 旧串行任务续办仍按人员创建;只有新进入节点才读取分组配置。
boolean legacyContinuation = execution.getProcessTask() != null
&& taskModel.getName().equals(execution.getProcessTask().getTaskName())
&& execution.getArgs().containsKey(FlowConst.COUNTERSIGN_VARIABLE_PREFIX + taskModel.getName() + "_" + FlowConst.NR_OF_INSTANCES)
&& CountersignGroupService.snapshot(execution.getProcessTask()) == null;
execution.getArgs().remove(FlowConst.COUNTERSIGN_GROUP);
if (!legacyContinuation && CountersignGroupService.enabled(taskModel)) {
return ServiceContext.find(CountersignGroupService.class).createTasks(taskModel, execution, taskActors);
}
List<String> createTaskActors = new ArrayList<>();
if (CountersignTypeEnum.PARALLEL.equals(taskModel.getCountersignType())) {
// 并行:一个参与者一个任务,同时创建
@@ -485,6 +501,14 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
public void transfer(Long taskId, String transferUserId) {
// 设置原任务状态为已转办
ProcessTask task = fetch(taskId);
boolean grouped = CountersignGroupService.snapshot(task) != null;
if (grouped) {
task = ServiceContext.find(CountersignGroupService.class).lockTask(task);
if (!ProcessTaskStateEnum.DOING.getCode().equals(task.getTaskState()) || !isAllowed(task, SecurityUtil.getUserId())) {
throw new BaseException("任务已办理或当前用户无权转办");
}
if (StrUtil.isBlank(transferUserId)) throw new BaseException("请选择转办人");
}
task.setTaskState(ProcessTaskStateEnum.TRANSFER.getCode());
Dict variable = FlowUtil.variableToDict(task.getVariable());
variable.put(FlowConst.TASK_FORM_DATA_PREFIX + "transferUserId", transferUserId);
@@ -507,6 +531,14 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
// 创建转办任务参与者
addTaskActor(newTask.getId(), List.of(transferUserId));
// 分组转办保留组标识,只替换共享任务;关闭原待办后通知新的实际办理人。
if (grouped) {
if (getTaskActors(newTask.getId()).isEmpty()) throw new BaseException("转办人不存在");
ProcessPublisher.notify(ProcessEvent.builder().eventType(ProcessEventTypeEnum.PROCESS_TASK_END).sourceId(taskId).build());
ProcessPublisher.notify(ProcessEvent.builder().eventType(ProcessEventTypeEnum.PROCESS_TASK_START).sourceId(newTask.getId()).build());
return;
}
// 发布流程任务开始事件
List<ProcessTask> processTaskList = List.of(task);
processTaskList.forEach(processTask -> {
@@ -10,28 +10,24 @@ import cn.hutool.core.lang.tree.TreeUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.sys.models.Sys_dict;
import com.budwk.app.sys.models.Sys_role;
import com.budwk.app.sys.models.Sys_unit;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.sys.services.SysDictService;
import com.budwk.app.sys.services.SysRoleService;
import com.budwk.app.sys.services.SysUnitService;
import com.budwk.app.sys.services.SysUserService;
import com.budwk.app.sys.views.View_user;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import io.swagger.annotations.ApiOperation;
import org.nutz.dao.Chain;
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.aop.Aop;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Lang;
import org.nutz.lang.Strings;
@@ -59,10 +55,6 @@ public class SysUnitController {
private SysUnitService sysUnitService;
@Inject
private SysUserService sysUserService;
@Inject
private SysDictService sysDictService;
@Inject
private SysRoleService sysRoleService;
@At("")
@Ok("beetl:/platform/sys/unit/index.html")
@@ -107,41 +99,23 @@ public class SysUnitController {
return Result.success(sysUnitService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql));
}
/** 无入参;返回 Result,data 为角色选项列表,每项 code 为编码、name 为名称。 */
@At
@Ok("json")
@SaCheckPermission("sys.manager.unit")
public Result unitRoleOptions() {
return Result.success(sysUnitService.getUnitRoleOptions());
}
/** pageForm 为搜索和分页参数,unitId 为系统单位IDResult.data 包含 list 人员角色列表、totalCount 总数。 */
@At("/leaderPageData")
@Ok("json")
@SaCheckLogin
public Object leaderPageData(PageForm pageForm, String unitId) {
List<Sys_dict> unitRoles = sysDictService.getSubListByCode("UNIT_ROLES");
if (Lang.isEmpty(unitRoles)) {
return Result.success();
if (StrUtil.isBlank(unitId)) {
return Result.error("请选择单位");
}
List<String> unitRoleCodes = unitRoles.stream().map(Sys_dict::getCode).toList();
Sql sql = Sqls.create("""
SELECT
t1.userId,
t2.username,
t2.loginname,
t3.`code` AS roleCode,
t3.`name` AS roleName
FROM
sys_user_role t1
LEFT JOIN sys_user t2 ON t2.id = t1.userId
LEFT JOIN sys_role t3 ON t3.id = t1.roleId
$condition
""");
Cnd cnd = Cnd.where("t3.`code`", "in", unitRoleCodes);
cnd.and("t1.unitId", "=", unitId);
if (StrUtil.isAllNotBlank(pageForm.getSearchName(), pageForm.getSearchKeyword())) {
switch (pageForm.getSearchName()) {
case "loginname" -> cnd.where().andLike("t2.loginname", pageForm.getSearchKeyword());
case "username" -> cnd.where().andLike("t2.username", pageForm.getSearchKeyword());
case "roleName" -> cnd.where().andLike("t3.`name`", pageForm.getSearchKeyword());
}
}
cnd.asc("t2.username");
sql.setCondition(cnd);
Pagination pagination = sysUserService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
return Result.success(sysUnitService.leaderPageData(pageForm, unitId));
}
@At
@@ -164,30 +138,29 @@ public class SysUnitController {
return Result.success(list);
}
/** userId 为人员IDroleCode 为角色编码,unitId 为系统单位ID;返回 Resultcode=0 表示添加成功。 */
@At
@Ok("json")
@SaCheckPermission("sys.manager.unit")
@Aop(TransAop.READ_COMMITTED)
public Result insertUnitUserRole(String userId, String roleCode, String unitId) {
Sys_role role = sysRoleService.fetch(Cnd.where("code", "=", roleCode));
if (Lang.isEmpty(role)) {
throw new BaseException("无法找到{}对应编码的角色", roleCode);
if (!StrUtil.isAllNotBlank(userId, roleCode, unitId)) {
return Result.error("请选择单位、角色和人员");
}
sysRoleService.clear("sys_user_role", Cnd.where("roleId", "=", role.getId()).and("userId", "=", userId).and("unitId", "=", unitId));
sysRoleService.insert("sys_user_role", Chain.make("roleId", role.getId()).add("userId", userId).add("unitId", unitId));
sysRoleService.clearCache();
sysUserService.clearCache();
sysUnitService.insertUnitUserRole(userId, roleCode, unitId);
return Result.success();
}
/** userId 为人员IDroleCode 为角色编码,unitId 为系统单位ID;返回 Resultcode=0 表示删除成功。 */
@At
@Ok("json")
@SaCheckPermission("sys.manager.unit")
@Aop(TransAop.READ_COMMITTED)
public Result deleteUnitUserRole(String userId, String roleCode, String unitId) {
Sys_role role = sysRoleService.fetch(Cnd.where("code", "=", roleCode));
if (Lang.isEmpty(role)) {
throw new BaseException("无法找到{}对应编码的角色", roleCode);
if (!StrUtil.isAllNotBlank(userId, roleCode, unitId)) {
return Result.error("请选择单位、角色和人员");
}
sysRoleService.clear("sys_user_role", Cnd.where("roleId", "=", role.getId()).and("userId", "=", userId).and("unitId", "=", unitId));
sysRoleService.clearCache();
sysUserService.clearCache();
sysUnitService.deleteUnitUserRole(userId, roleCode, unitId);
return Result.success();
}
@@ -2,11 +2,27 @@ package com.budwk.app.sys.services;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.sys.models.Sys_unit;
import com.budwk.app.sys.models.Sys_dict;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import java.util.List;
/**
* Created by wizzer on 2016/12/22.
*/
public interface SysUnitService extends BaseService<Sys_unit> {
/** 无参数;返回单位干部角色选项,code 为角色编码,name 为显示名称,包含提案承办单位领导。 */
List<Sys_dict> getUnitRoleOptions();
/** pageForm 为姓名/工号搜索和分页参数,unitId 为系统单位ID;返回 list 和 totalCount 等分页数据。 */
Pagination leaderPageData(PageForm pageForm, String unitId);
/** userId 为人员IDroleCode 为角色编码,unitId 为系统单位ID;新增对应负责人关系,返回 void。 */
void insertUnitUserRole(String userId, String roleCode, String unitId);
/** userId 为人员IDroleCode 为角色编码,unitId 为系统单位ID;仅删除该单位对应角色关系,返回 void。 */
void deleteUnitUserRole(String userId, String roleCode, String unitId);
/**
* 保存单位
*
@@ -1,6 +1,21 @@
package com.budwk.app.sys.services.impl;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.sys.models.Sys_dict;
import com.budwk.app.sys.models.Sys_role;
import com.budwk.app.sys.models.Sys_user_role;
import com.budwk.app.sys.services.SysDictService;
import com.budwk.app.sys.services.SysRoleService;
import com.budwk.app.sys.services.SysUserService;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.Inject;
import java.util.ArrayList;
import java.util.List;
import com.budwk.app.base.constant.RedisConstant;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_unit;
@@ -26,6 +41,129 @@ public class SysUnitServiceImpl extends BaseServiceImpl<Sys_unit> implements Sys
super(dao);
}
@Inject
private SysDictService sysDictService;
@Inject
private SysRoleService sysRoleService;
@Inject
private SysUserService sysUserService;
/** 复制字典选项后补充提案角色,避免修改字典缓存或生成重复选项。 */
@Override
public List<Sys_dict> getUnitRoleOptions() {
List<Sys_dict> options = new ArrayList<>(sysDictService.getSubListByCode("UNIT_ROLES"));
if (options.stream().noneMatch(item -> RoleConstant.PROPOSAL_UNIT_LEADER.name().equals(item.getCode()))) {
Sys_dict option = new Sys_dict();
option.setCode(RoleConstant.PROPOSAL_UNIT_LEADER.name());
option.setName("提案承办单位领导");
options.add(option);
}
return options;
}
/** 按系统单位编码匹配承办单位;历史承办单位ID可能不同,缺失时查询允许为空,写入必须明确报错。 */
private ProposalUndertake findUndertake(String unitId, boolean required) {
Sys_unit unit = fetch(unitId);
if (unit == null || StrUtil.isBlank(unit.getUnitcode())) {
throw new BaseException("系统单位不存在或单位编码为空");
}
List<ProposalUndertake> matches = dao().query(ProposalUndertake.class,
Cnd.where("code", "=", StrUtil.trim(unit.getUnitcode())));
if (matches.size() > 1) {
throw new BaseException("单位编码对应多个提案承办单位,请先检查承办单位配置");
}
if (matches.isEmpty()) {
if (required) {
throw new BaseException("该单位尚未同步为提案承办单位,请先在提案承办单位配置中同步系统单位");
}
return null;
}
return matches.get(0);
}
/** pageForm 指定搜索和分页,unitId 为系统单位ID;返回人员、角色和分页信息,提案角色读取原有 underTakeId 关系。 */
@Override
public Pagination leaderPageData(PageForm pageForm, String unitId) {
ProposalUndertake undertake = findUndertake(unitId, false);
Sql sql = Sqls.create("""
SELECT t1.userId, t2.username, t2.loginname, t3.code AS roleCode, t3.name AS roleName
FROM sys_user_role t1
LEFT JOIN sys_user t2 ON t2.id = t1.userId
LEFT JOIN sys_role t3 ON t3.id = t1.roleId
$condition
""");
Cnd cnd = Cnd.where("t3.code", "in", getUnitRoleOptions().stream().map(Sys_dict::getCode).toList());
// 普通干部按系统单位关联;提案领导只按承办单位关联,避免另一入口更新后残留或串单位。
Cnd ordinary = Cnd.where("t3.code", "<>", RoleConstant.PROPOSAL_UNIT_LEADER.name())
.and("t1.unitId", "=", unitId);
if (undertake != null) {
ordinary.or(Cnd.where("t3.code", "=", RoleConstant.PROPOSAL_UNIT_LEADER.name())
.and("t1.underTakeId", "=", undertake.getId()).where());
}
cnd.and(ordinary.where());
if (StrUtil.isAllNotBlank(pageForm.getSearchName(), pageForm.getSearchKeyword())) {
switch (pageForm.getSearchName()) {
case "loginname" -> cnd.where().andLike("t2.loginname", pageForm.getSearchKeyword());
case "username" -> cnd.where().andLike("t2.username", pageForm.getSearchKeyword());
case "roleName" -> cnd.where().andLike("t3.name", pageForm.getSearchKeyword());
}
}
cnd.asc("t2.username");
sql.setCondition(cnd);
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
}
/** 将角色限定到当前页面支持的角色,防止通过参数指定其他系统角色。 */
private Sys_role requireUnitRole(String roleCode) {
if (getUnitRoleOptions().stream().noneMatch(item -> roleCode.equals(item.getCode()))) {
throw new BaseException("不支持的单位干部角色");
}
return sysRoleService.getByCode(roleCode);
}
/** 参数为人员ID、角色编码、系统单位ID;提案角色写入共享承办单位关系,同一人员重复添加不产生重复记录。 */
@Override
@Aop(TransAop.READ_COMMITTED)
public void insertUnitUserRole(String userId, String roleCode, String unitId) {
Sys_role role = requireUnitRole(roleCode);
boolean proposal = RoleConstant.PROPOSAL_UNIT_LEADER.name().equals(roleCode);
ProposalUndertake undertake = proposal ? findUndertake(unitId, true) : null;
if (proposal && !Boolean.TRUE.equals(undertake.getEnable())) {
throw new BaseException("该提案承办单位已停用,不能设置负责人");
}
if (fetch(unitId) == null || sysUserService.fetch(userId) == null) {
throw new BaseException("单位或人员不存在");
}
String field = proposal ? "underTakeId" : "unitId";
String targetId = proposal ? undertake.getId() : unitId;
dao().clear(Sys_user_role.class, Cnd.where("roleId", "=", role.getId())
.and("userId", "=", userId).and(field, "=", targetId));
Sys_user_role relation = new Sys_user_role();
relation.setRoleId(role.getId());
relation.setUserId(userId);
if (proposal) {
relation.setUnderTakeId(targetId);
} else {
relation.setUnitId(targetId);
}
dao().insert(relation);
sysRoleService.clearCache();
sysUserService.clearCache();
}
/** 参数为人员ID、角色编码、系统单位ID;仅删除当前单位对应关系,提案配置菜单同时生效。 */
@Override
@Aop(TransAop.READ_COMMITTED)
public void deleteUnitUserRole(String userId, String roleCode, String unitId) {
Sys_role role = requireUnitRole(roleCode);
boolean proposal = RoleConstant.PROPOSAL_UNIT_LEADER.name().equals(roleCode);
String targetId = proposal ? findUndertake(unitId, true).getId() : unitId;
dao().clear(Sys_user_role.class, Cnd.where("roleId", "=", role.getId())
.and("userId", "=", userId).and(proposal ? "underTakeId" : "unitId", "=", targetId));
sysRoleService.clearCache();
sysUserService.clearCache();
}
/**
* 新增单位
*
@@ -1,83 +1,38 @@
package com.budwk.app.zhgh.democratic.opinion.handler;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.flow.engine.AssignmentHandler;
import com.budwk.app.flow.engine.core.Execution;
import com.budwk.app.flow.engine.core.ServiceContext;
import com.budwk.app.flow.engine.model.TaskModel;
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.ProposalReplyUnit;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import com.budwk.app.zhgh.democratic.opinion.service.OpinionSchoolAuditService;
import java.util.List;
/**
* @ClassName OpinionMasterUnitAssignmentHandler
* @Author JyuHsin
* @Date 2026/2/3 11:13
* @Version 1.0
* @Description TODO
*/
/** 意见主办单位处理人,由 service 统一校验多主办参数及负责人配置。 */
public class OpinionMasterUnitAssignmentHandler implements AssignmentHandler {
@Override
public List<String> assign(TaskModel model, Execution execution) {
Dao dao = ServiceContext.find(Dao.class);
SysRoleService sysRoleService = ServiceContext.find(SysRoleService.class);
/**
* @param model 主办答复节点定义,多个主办时应配置并行会签
* @param execution 意见流程上下文,包含主办 ID 数组或历史单主办 ID
* @return 负责人用户 ID 列表,供 WF 引擎创建答复任务
*/
@Override
public List<String> assign(TaskModel model, Execution execution) {
return ServiceContext.find(OpinionSchoolAuditService.class).assignMasterUnits(model, execution);
}
String opinionId = execution.getProcessInstance().getBusinessNo();
@Override
public String getMessage() {
return "意见主办单位处理人";
}
String masterUnitId = execution.getArgs().getStr("tf_masterUnitId");
/** 按所选主办单位返回已授权成员,成员可以同时属于多个承办组。 */
@Override
public java.util.Map<String, List<String>> getUndertakeGroups(TaskModel model, Execution execution, List<String> actors) {
return ServiceContext.find(OpinionSchoolAuditService.class).getUndertakeGroups(execution, true, actors);
}
if(StrUtil.isBlank(masterUnitId)){
ProposalReplyUnit masterUnit = dao.fetch(ProposalReplyUnit.class, Cnd.where(ProposalReplyUnit::getProposalId, "=", opinionId).and(ProposalReplyUnit::getIsMaster, "=", 1));
if (masterUnit == null) {
throw new BaseException("系统异常");
}
masterUnitId = masterUnit.getUnitId();
}
if(StrUtil.isBlank(masterUnitId)){
throw new BaseException("提案没有主办单位");
}
ProposalUndertake undertake = dao.fetch(ProposalUndertake.class, masterUnitId);
if (undertake == null) {
throw new BaseException("主办单位不存在");
}
Sys_role sysRole = sysRoleService.getByCode(RoleConstant.PROPOSAL_UNIT_LEADER);
List<Sys_user_role> sysUserRoles = dao.query(Sys_user_role.class, Cnd.where(Sys_user_role::getRoleId, "=", sysRole.getId()).and(Sys_user_role::getUnderTakeId, "=", undertake.getId()));
List<String> selectUserIds = sysUserRoles.stream().map(Sys_user_role::getUserId).toList();
if (selectUserIds.isEmpty()) {
throw new BaseException("主办单位没有负责人");
}
// 参数
Dict args = execution.getArgs();
args.set("underTakeName", undertake.getName());
args.set("underTakeId", undertake.getId());
args.set("underTakeIsMaster",true);
return selectUserIds;
}
@Override
public String getMessage() {
return "意见主办单位处理人";
}
@Override
public int getOrder() {
return AssignmentHandler.super.getOrder();
}
@Override
public int getOrder() {
return AssignmentHandler.super.getOrder();
}
}
@@ -24,6 +24,12 @@ import java.util.List;
* @Description TODO
*/
public class OpinionSlaveUnitAssignmentHandler implements AssignmentHandler {
/** 按所选协办单位返回已授权成员,供分组会签生成共享答复任务。 */
@Override
public java.util.Map<String, List<String>> getUndertakeGroups(TaskModel model, Execution execution, List<String> actors) {
return ServiceContext.find(com.budwk.app.zhgh.democratic.opinion.service.OpinionSchoolAuditService.class)
.getUndertakeGroups(execution, false, actors);
}
@Override
public List<String> assign(TaskModel model, Execution execution) {
Dao dao = ServiceContext.find(Dao.class);
@@ -1,85 +1,21 @@
package com.budwk.app.zhgh.democratic.opinion.listenter;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.engine.FlowInterceptor;
import com.budwk.app.flow.engine.core.Execution;
import com.budwk.app.flow.engine.core.ServiceContext;
import com.budwk.app.flow.engine.util.FlowUtil;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.entity.ProcessTaskActor;
import com.budwk.app.zhgh.democratic.opinion.models.OpinionReplyUnit;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalReplyUnit;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.json.Json;
import com.budwk.app.zhgh.democratic.opinion.service.OpinionSchoolAuditService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
import java.util.List;
/**
* @ClassName OpinionOfficeSuffixInterceptor
* @Author JyuHsin
* @Date 2026/2/3 14:04
* @Version 1.0
* @Description TODO
*/
/** 审核完成后的承办单位持久化入口,业务处理和事务由 IOC 中的 service 承担。 */
public class OpinionOfficeSuffixInterceptor implements FlowInterceptor {
@Override
public void intercept(Execution execution) {
Dao dao = ServiceContext.find(Dao.class);
String opinionId = execution.getProcessInstance().getBusinessNo();
String officeResult = execution.getArgs().getStr(FlowConst.TASK_FORM_DATA_PREFIX + "schoolResult");
if (StrUtil.isNotBlank(officeResult) && "YES".equals(officeResult)) {
// 删除原来的记录
dao.clear(OpinionReplyUnit.class, Cnd.where(OpinionReplyUnit::getOpinionId, "=", opinionId));
// 主办单位
String masterUnitId = execution.getArgs().getStr(FlowConst.TASK_FORM_DATA_PREFIX + "masterUnitId");
ProposalUndertake masterUnit = dao.fetch(ProposalUndertake.class, masterUnitId);
OpinionReplyUnit masterReplyUnit = new OpinionReplyUnit();
masterReplyUnit.setOpinionId(opinionId);
masterReplyUnit.setUnitId(masterUnit.getId());
masterReplyUnit.setUnitCode(masterUnit.getCode());
masterReplyUnit.setUnitName(masterUnit.getName());
masterReplyUnit.setIsMaster(true);
dao.insert(masterReplyUnit);
// 协办单位
List<String> helpUnitIds = (List<String>) execution.getArgs().getObj(FlowConst.TASK_FORM_DATA_PREFIX + "slaveUnitIds");
if(ObjectUtil.isNotEmpty(helpUnitIds)){
for (String helpUnitId : helpUnitIds) {
ProposalUndertake helpUnit = dao.fetch(ProposalUndertake.class, helpUnitId);
OpinionReplyUnit helpReplyUnit = new OpinionReplyUnit();
helpReplyUnit.setOpinionId(opinionId);
helpReplyUnit.setUnitId(helpUnit.getId());
helpReplyUnit.setUnitCode(helpUnit.getCode());
helpReplyUnit.setUnitName(helpUnit.getName());
helpReplyUnit.setIsMaster(false);
dao.insert(helpReplyUnit);
}
}
}
// 当前任务
List<ProcessTask> processTaskList = execution.getProcessTaskList();
for (ProcessTask task : processTaskList) {
// 办理人
ProcessTaskActor taskActor = dao.fetch(ProcessTaskActor.class, Cnd.where(ProcessTaskActor::getProcessTaskId, "=", task.getId()));
String actorUnitId = taskActor.getActorUnitId();
OpinionReplyUnit replyUnit = dao.fetch(OpinionReplyUnit.class, Cnd.where(OpinionReplyUnit::getOpinionId, "=", opinionId).and(OpinionReplyUnit::getUnitId, "=", actorUnitId));
Dict variable = FlowUtil.variableToDict(task.getVariable());
variable.set("underTakeName", replyUnit.getUnitName());
variable.set("underTakeId", replyUnit.getUnitId());
variable.set("underTakeIsMaster", replyUnit.getIsMaster());
task.setVariable(Json.toJson(variable));
}
dao.update(processTaskList,"variable");
}
/**
* @param execution 意见流程上下文包含审核结果主协办单位和新生成的任务
* @return 无返回值保存承办记录并补充答复任务的单位信息
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public void intercept(Execution execution) {
ServiceContext.find(OpinionSchoolAuditService.class).saveReplyUnits(execution);
}
}
@@ -31,9 +31,11 @@ public class OpinionSchoolAuditImportExcelMode extends ExcelImportError {
private String unitName;
@Excel(name = "主办")
// 多个主办单位名称或编码仅使用中文分号分隔单位全称内部标点保留表头不变
private String masterUnitName;
@Excel(name = "协办")
// 多个协办单位与主办使用相同的中文分号分隔规则未指定协办时可留空
private String slaveUnitNames;
@Excel(name = "审核结果")
@@ -2,17 +2,43 @@ package com.budwk.app.zhgh.democratic.opinion.service;
import com.budwk.app.base.model.ExcelImportRes;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.flow.engine.core.Execution;
import com.budwk.app.flow.engine.model.TaskModel;
import com.budwk.app.zhgh.democratic.opinion.mode.OpinionSchoolAuditImportExcelMode;
import com.budwk.app.zhgh.democratic.opinion.models.OpinionInfo;
import org.nutz.mvc.upload.TempFile;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
/**
* 校工会审核业务处理
*/
public interface OpinionSchoolAuditService extends BaseService<OpinionInfo> {
/**
* 校验主协办单位并解析主办负责人兼容历史单主办流程变量
* @param model 主办答复节点定义多主办要求并行会签
* @param execution 流程上下文args 中传 tf_masterUnitIdstf_slaveUnitIds 等单位 ID 数组
* @return 去重后的主办负责人用户 ID 列表 WF 引擎创建任务
*/
List<String> assignMasterUnits(TaskModel model, Execution execution);
/**
* @param execution 意见审核上下文包含所选主办/协办单位 ID 数组
* @param master true 为主办false 为协办
* @param actors 本节点已授权负责人 ID不把未授权人员加入分组
* @return 承办单位 ID 到负责人 ID 列表 WF 生成每单位一份共享任务
*/
java.util.Map<String, List<String>> getUndertakeGroups(Execution execution, boolean master, List<String> actors);
/**
* 审核通过后保存主协办记录并按负责人配置关联新生成的答复任务
* @param execution 当前意见实例审核变量和新生成任务列表主办兼容旧 tf_masterUnitId
* @return 无返回值保存 opinion_reply_unit 及任务中的承办单位信息
*/
void saveReplyUnits(Execution execution);
/**
* 下载校工会审核结果导入模板
*
@@ -13,6 +13,7 @@ import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.SysOfficeTemplateUtil;
import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.flow.service.FlowCommonService;
import com.budwk.app.flow.engine.util.FlowUtil;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask;
@@ -72,6 +73,8 @@ public class OpinionCommonServiceImpl extends BaseServiceImpl<OpinionInfo> imple
private FlowEngine flowEngine;
@Inject
private SysDictService sysDictService;
@Inject
private FlowCommonService flowCommonService;
public OpinionCommonServiceImpl(Dao dao) {
super(dao);
@@ -107,7 +110,6 @@ public class OpinionCommonServiceImpl extends BaseServiceImpl<OpinionInfo> imple
t.taskParentId,
t.variable taskVariable,
IFNULL(GROUP_CONCAT(DISTINCT nt.displayName),'结束') curTaskName,
IF(rt.id IS NOT NULL, 1, 0) AS canRevoke,
t.variable->>'$.underTakeName' AS underTakeName,
IF(JSON_EXTRACT(t.variable, '$.underTakeIsMaster') in (true, 1), 1, 0) AS underTakeIsMaster
FROM
@@ -120,7 +122,6 @@ public class OpinionCommonServiceImpl extends BaseServiceImpl<OpinionInfo> imple
LEFT JOIN teacher_congress_session tcs on tcs.id = info.sessionId
LEFT JOIN teacher_congress_delegation tcd on tcd.id = info.delegationId
LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10
LEFT JOIN wf_process_task rt ON rt.processInstanceId = ins.id AND rt.taskParentId = t.id AND rt.taskState = 10
LEFT JOIN wf_process_task_actor pta ON pta.processTaskId = nt.id
$condition
""");
@@ -139,7 +140,12 @@ public class OpinionCommonServiceImpl extends BaseServiceImpl<OpinionInfo> imple
OpinionSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
sql.setCondition(cnd);
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
Pagination<NutMap> page = listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
// 单位成员仍可查看共享已办记录撤回按钮由实际办理人及当前 WF 状态共同决定
for (NutMap row : page.getList()) {
row.put("canRevoke", flowCommonService.canRevokeTask(row.getLong("taskId")));
}
return page;
}
@Override
@@ -9,6 +9,17 @@ import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.model.ExcelImportRes;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.flow.engine.core.Execution;
import com.budwk.app.flow.engine.model.TaskModel;
import com.budwk.app.flow.enums.CountersignTypeEnum;
import com.budwk.app.flow.enums.ProcessTaskPerformTypeEnum;
import com.budwk.app.flow.entity.ProcessTaskActor;
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.opinion.models.OpinionReplyUnit;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.flow.constant.FlowConst;
@@ -26,6 +37,9 @@ import com.budwk.app.zhgh.democratic.opinion.models.OpinionType;
import com.budwk.app.zhgh.democratic.opinion.service.OpinionSchoolAuditService;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake;
import org.apache.poi.ss.usermodel.Workbook;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
import org.nutz.json.Json;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
@@ -39,6 +53,8 @@ import org.nutz.mvc.upload.TempFile;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -54,11 +70,174 @@ public class OpinionSchoolAuditServiceImpl extends BaseServiceImpl<OpinionInfo>
private FlowEngine flowEngine;
@Inject
private FlowCommonService flowCommonService;
@Inject
private SysRoleService sysRoleService;
public OpinionSchoolAuditServiceImpl(Dao dao) {
super(dao);
}
@Override
public List<String> assignMasterUnits(TaskModel model, Execution execution) {
List<ProposalUndertake> masters = resolveMasterUnits(execution);
validateSlaveUnits(execution.getArgs(), masters);
// 普通任务会让不同主办共用一份答复多主办必须由流程定义启用并行会签
if (masters.size() > 1 && (!ProcessTaskPerformTypeEnum.COUNTERSIGN.equals(model.getPerformType())
|| !CountersignTypeEnum.PARALLEL.equals(model.getCountersignType()))) {
throw new BaseException("多主办单位需要将主办答复节点配置为并行会签,请联系流程管理员");
}
Set<String> users = new LinkedHashSet<>();
Sys_role role = sysRoleService.getByCode(RoleConstant.PROPOSAL_UNIT_LEADER);
if (role == null) {
throw new BaseException("未配置承办单位负责人角色");
}
for (ProposalUndertake unit : masters) {
List<String> unitUsers = dao().query(Sys_user_role.class,
Cnd.where("roleId", "=", role.getId()).and("underTakeId", "=", unit.getId()))
.stream().map(Sys_user_role::getUserId).filter(StrUtil::isNotBlank).distinct().toList();
if (unitUsers.isEmpty()) {
throw new BaseException(unit.getName() + "主办单位没有负责人");
}
// WF 按用户创建会签任务同一人负责多个主办时无法唯一确定本次答复所属单位
if (!"UNDERTAKE".equals(model.getCountersignGroupBy()) && unitUsers.stream().anyMatch(users::contains)) {
throw new BaseException("多个主办单位存在相同负责人,无法区分答复任务,请调整负责人配置");
}
users.addAll(unitUsers);
}
return new ArrayList<>(users);
}
@Override
public java.util.Map<String, List<String>> getUndertakeGroups(Execution execution, boolean master, List<String> actors) {
List<ProposalUndertake> masters = resolveMasterUnits(execution);
List<ProposalUndertake> slaves = validateSlaveUnits(execution.getArgs(), masters);
Sys_role role = sysRoleService.getByCode(RoleConstant.PROPOSAL_UNIT_LEADER);
if (role == null) throw new BaseException("未配置承办单位负责人角色");
java.util.Map<String, List<String>> groups = new java.util.LinkedHashMap<>();
for (ProposalUndertake unit : master ? masters : slaves) {
// 按业务选择的承办单位分组同一负责人管理多个单位时仍生成多份独立答复
List<String> members = dao().query(Sys_user_role.class,
Cnd.where("roleId", "=", role.getId()).and("underTakeId", "=", unit.getId()))
.stream().map(Sys_user_role::getUserId).filter(actors::contains).distinct().toList();
if (members.isEmpty()) throw new BaseException(unit.getName() + "没有可办理的负责人");
groups.put(unit.getId(), members);
}
return groups;
}
/** 以新数组字段为准;只有新字段缺失时读取历史单值或意见承办记录,空数组不得回退。 */
private List<ProposalUndertake> resolveMasterUnits(Execution execution) {
Dict args = execution.getArgs();
Object value = args.get("tf_masterUnitIds");
if (!args.containsKey("tf_masterUnitIds")) {
String legacyId = args.getStr("tf_masterUnitId");
value = StrUtil.isNotBlank(legacyId) ? List.of(legacyId)
: dao().query(OpinionReplyUnit.class, Cnd.where("opinionId", "=",
execution.getProcessInstance().getBusinessNo()).and("isMaster", "=", true))
.stream().map(OpinionReplyUnit::getUnitId).toList();
}
return resolveUnits(value, "主办", true);
}
/** 单位 ID 必须为数组且不能重复;查询数据库验证有效性,不信任前端传入的名称。 */
private List<ProposalUndertake> resolveUnits(Object value, String label, boolean required) {
if (value == null && !required) {
return new ArrayList<>();
}
if (!(value instanceof Collection<?> ids)) {
throw new BaseException(label + "单位参数必须为ID数组");
}
if (required && ids.isEmpty()) {
throw new BaseException(label + "单位不能为空");
}
Set<String> selected = new HashSet<>();
List<ProposalUndertake> units = new ArrayList<>();
for (Object item : ids) {
if (!(item instanceof String id) || StrUtil.isBlank(id)) {
throw new BaseException(label + "单位ID不能为空");
}
if (!selected.add(id)) {
throw new BaseException(label + "单位重复");
}
ProposalUndertake unit = dao().fetch(ProposalUndertake.class,
Cnd.where("id", "=", id).and("delFlag", "=", false));
if (unit == null) {
throw new BaseException(label + "单位不存在:" + id);
}
units.add(unit);
}
return units;
}
/** 校验主协办集合互斥,供流程分配和承办记录保存共同使用。 */
private List<ProposalUndertake> validateSlaveUnits(Dict args, List<ProposalUndertake> masters) {
List<ProposalUndertake> slaves = resolveUnits(args.get("tf_slaveUnitIds"), "协办", false);
Set<String> masterIds = masters.stream().map(ProposalUndertake::getId).collect(Collectors.toSet());
if (slaves.stream().anyMatch(unit -> masterIds.contains(unit.getId()))) {
throw new BaseException("协办单位不能包含主办单位");
}
return slaves;
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void saveReplyUnits(Execution execution) {
String opinionId = execution.getProcessInstance().getBusinessNo();
if ("YES".equals(execution.getArgs().getStr("tf_schoolResult"))) {
List<ProposalUndertake> masters = resolveMasterUnits(execution);
List<ProposalUndertake> slaves = validateSlaveUnits(execution.getArgs(), masters);
// 全部校验通过后替换承办记录撤回重审时保留本次选择的完整主协办集合
List<OpinionReplyUnit> records = new ArrayList<>();
for (ProposalUndertake unit : masters) {
records.add(toReplyUnit(opinionId, unit, true));
}
for (ProposalUndertake unit : slaves) {
records.add(toReplyUnit(opinionId, unit, false));
}
dao().clear(OpinionReplyUnit.class, Cnd.where("opinionId", "=", opinionId));
dao().insert(records);
}
List<OpinionReplyUnit> records = dao().query(OpinionReplyUnit.class, Cnd.where("opinionId", "=", opinionId));
Sys_role role = sysRoleService.getByCode(RoleConstant.PROPOSAL_UNIT_LEADER);
for (ProcessTask task : execution.getProcessTaskList()) {
// 本拦截器只关联承办答复任务其他节点不读取承办单位
if (!List.of("master_reply", "slave_reply").contains(task.getTaskName())) {
continue;
}
if (role == null) {
throw new BaseException("未配置承办单位负责人角色");
}
List<String> actorIds = dao().query(ProcessTaskActor.class, Cnd.where("processTaskId", "=", task.getId()))
.stream().map(ProcessTaskActor::getActorId).toList();
List<String> unitIds = actorIds.isEmpty() ? Collections.emptyList()
: dao().query(Sys_user_role.class, Cnd.where("roleId", "=", role.getId()).and("userId", "in", actorIds))
.stream().map(Sys_user_role::getUnderTakeId).distinct().toList();
Dict group = com.budwk.app.flow.service.CountersignGroupService.snapshot(task);
// 优先使用创建时的承办组避免一个负责人同时管理多个单位导致任务归属歧义
List<String> matchedUnitIds = group != null && "UNDERTAKE".equals(group.getStr("mode"))
? List.of(group.getStr("group")) : unitIds;
boolean master = "master_reply".equals(task.getTaskName());
List<OpinionReplyUnit> matches = records.stream()
.filter(unit -> Boolean.valueOf(master).equals(unit.getIsMaster()) && matchedUnitIds.contains(unit.getUnitId())).toList();
if (matches.size() != 1) {
throw new BaseException("答复任务无法唯一匹配承办单位,请检查负责人及并行会签配置");
}
OpinionReplyUnit unit = matches.get(0);
Dict variable = FlowUtil.variableToDict(task.getVariable());
variable.set("underTakeName", unit.getUnitName());
variable.set("underTakeId", unit.getUnitId());
variable.set("underTakeIsMaster", unit.getIsMaster());
task.setVariable(Json.toJson(variable));
dao().update(task, "variable");
}
}
/** 每个承办单位生成独立记录,主办和协办共用同一实体且不增加数据库字段。 */
private OpinionReplyUnit toReplyUnit(String opinionId, ProposalUndertake unit, boolean master) {
return new OpinionReplyUnit().setOpinionId(opinionId).setUnitId(unit.getId())
.setUnitCode(unit.getCode()).setUnitName(unit.getName()).setIsMaster(master);
}
@Override
public void downloadAuditImportTemplate(HttpServletResponse response) {
List<ExcelExportEntity> entities = new ArrayList<>();
@@ -81,6 +260,7 @@ public class OpinionSchoolAuditServiceImpl extends BaseServiceImpl<OpinionInfo>
}
@Override
@Aop(TransAop.READ_COMMITTED)
public ExcelImportRes<OpinionSchoolAuditImportExcelMode> importAuditResult(TempFile file, String sessionId) {
ExcelImportRes<OpinionSchoolAuditImportExcelMode> excelImportRes = new ExcelImportRes<>();
List<OpinionSchoolAuditImportExcelMode> importRows;
@@ -125,6 +305,7 @@ public class OpinionSchoolAuditServiceImpl extends BaseServiceImpl<OpinionInfo>
return excelImportRes;
}
@Aop(TransAop.READ_COMMITTED)
private void handleAuditImportRow(OpinionSchoolAuditImportExcelMode row, int rowIndex, String sessionId, Set<String> importedCodes) {
List<String> errors = new ArrayList<>();
String code = StrUtil.trim(row.getCode());
@@ -145,7 +326,7 @@ public class OpinionSchoolAuditServiceImpl extends BaseServiceImpl<OpinionInfo>
}
OpinionType type = null;
ProposalUndertake masterUnit = null;
List<ProposalUndertake> masterUnits = new ArrayList<>();
List<ProposalUndertake> slaveUnits = new ArrayList<>();
if (auditResult == AuditResult.PASS) {
// 审核通过时与页面必填规则保持一致意见类别主办单位审核意见必须填写
@@ -161,18 +342,16 @@ public class OpinionSchoolAuditServiceImpl extends BaseServiceImpl<OpinionInfo>
if (StrUtil.isBlank(row.getMasterUnitName())) {
errors.add("主办单位不能为空");
} else {
masterUnit = fetchUndertake(row.getMasterUnitName());
masterUnits = parseUnits(row.getMasterUnitName(), "主办", errors);
}
if (StrUtil.isNotBlank(row.getMasterUnitName()) && masterUnit == null) {
errors.add("主办单位不存在");
if (StrUtil.isNotBlank(row.getMasterUnitName()) && masterUnits.isEmpty() && errors.isEmpty()) {
errors.add("主办单位不能为空");
}
slaveUnits = parseSlaveUnits(row.getSlaveUnitNames(), errors);
if (masterUnit != null) {
String masterUnitId = masterUnit.getId();
if (slaveUnits.stream().anyMatch(unit -> StrUtil.equals(unit.getId(), masterUnitId))) {
errors.add("协办单位不能包含主办单位");
}
slaveUnits = parseUnits(row.getSlaveUnitNames(), "协办", errors);
Set<String> masterIds = masterUnits.stream().map(ProposalUndertake::getId).collect(Collectors.toSet());
if (slaveUnits.stream().anyMatch(unit -> masterIds.contains(unit.getId()))) {
errors.add("协办单位不能包含主办单位");
}
}
@@ -182,11 +361,12 @@ public class OpinionSchoolAuditServiceImpl extends BaseServiceImpl<OpinionInfo>
return;
}
executeSchoolAuditTask(row, taskInfo, auditResult, type, masterUnit, slaveUnits);
executeSchoolAuditTask(row, taskInfo, auditResult, type, masterUnits, slaveUnits);
}
@Aop(TransAop.READ_COMMITTED)
private void executeSchoolAuditTask(OpinionSchoolAuditImportExcelMode row, Record taskInfo, AuditResult auditResult,
OpinionType type, ProposalUndertake masterUnit, List<ProposalUndertake> slaveUnits) {
OpinionType type, List<ProposalUndertake> masterUnits, List<ProposalUndertake> slaveUnits) {
String opinionId = taskInfo.getString("opinionId");
Long taskId = taskInfo.getLong("taskId");
String taskName = taskInfo.getString("taskName");
@@ -207,8 +387,11 @@ public class OpinionSchoolAuditServiceImpl extends BaseServiceImpl<OpinionInfo>
// 通过审核时与页面单条审核保持一致先保存意见类别再提交主办协办等流程表单字段
updateOpinionType(opinionId, type.getId());
taskData.set("typeId", type.getId());
taskData.set("tf_masterUnitId", masterUnit.getId());
taskData.set("tf_masterUnitName", masterUnit.getName());
// 主办与协办均使用数组传递名称字符串供审批记录直接展示
List<String> masterUnitNames = masterUnits.stream().map(ProposalUndertake::getName).toList();
taskData.set("tf_masterUnitIds", masterUnits.stream().map(ProposalUndertake::getId).toList());
taskData.set("tf_masterUnitNames", masterUnitNames);
taskData.set("tf_masterUnitNameStr", StrUtil.join(",", masterUnitNames));
List<String> slaveUnitIds = slaveUnits.stream().map(ProposalUndertake::getId).collect(Collectors.toList());
List<String> slaveUnitNames = slaveUnits.stream().map(ProposalUndertake::getName).collect(Collectors.toList());
taskData.set("tf_slaveUnitIds", slaveUnitIds);
@@ -243,6 +426,7 @@ public class OpinionSchoolAuditServiceImpl extends BaseServiceImpl<OpinionInfo>
return sql.getObject(Record.class);
}
@Aop(TransAop.READ_COMMITTED)
private void updateOpinionType(String opinionId, Integer typeId) {
dao().update(OpinionInfo.class, Chain.make("typeId", typeId), Cnd.where("id", "=", opinionId));
@@ -287,25 +471,32 @@ public class OpinionSchoolAuditServiceImpl extends BaseServiceImpl<OpinionInfo>
return undertakes.size() == 1 ? undertakes.get(0) : null;
}
private List<ProposalUndertake> parseSlaveUnits(String slaveUnitText, List<String> errors) {
/**
* 解析导入行的主办或协办单位仅以中文分号分隔避免拆开单位全称内部的标点
* @param slaveUnitText 主办或协办名称/编码文本多单位使用中文分号连接空白文本返回空列表
* @param label 错误提示中的业务角色主办协办
* @param errors 接收不存在重复等校验错误的列表交由调用方汇总到当前 Excel
* @return 按输入顺序解析的有效承办单位列表按单位 ID 查重空项忽略错误项不加入
*/
private List<ProposalUndertake> parseUnits(String slaveUnitText, String label, List<String> errors) {
List<ProposalUndertake> units = new ArrayList<>();
if (StrUtil.isBlank(slaveUnitText)) {
return units;
}
String[] items = slaveUnitText.split("[,,、;\\n\\r]+");
String[] items = slaveUnitText.split("");
Set<String> names = new HashSet<>();
for (String item : items) {
String unitName = StrUtil.trim(item);
if (StrUtil.isBlank(unitName)) {
continue;
}
if (!names.add(unitName)) {
errors.add("协办单位重复:" + unitName);
continue;
}
ProposalUndertake undertake = fetchUndertake(unitName);
if (undertake == null) {
errors.add("协办单位不存在:" + unitName);
errors.add(label + "单位不存在:" + unitName);
continue;
}
if (!names.add(undertake.getId())) {
errors.add(label + "单位重复:" + unitName);
continue;
}
units.add(undertake);
@@ -16,19 +16,17 @@ import com.budwk.app.base.service.BaseService;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.flow.service.ProcessTaskService;
import com.budwk.app.sys.models.Sys_role;
import com.budwk.app.sys.models.Sys_unit;
import com.budwk.app.sys.models.Sys_user_role;
import com.budwk.app.sys.services.SysRoleService;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalConfig;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake;
import com.budwk.app.zhgh.democratic.proposal.service.ProposalUndertakeService;
import lombok.extern.slf4j.Slf4j;
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.SqlExpressionGroup;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
@@ -61,6 +59,8 @@ public class ProposalConfigUnitController {
private SysRoleService sysRoleService;
@Inject
private ProcessTaskService processTaskService;
@Inject
private ProposalUndertakeService proposalUndertakeService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/config/unit/index.html")
@@ -115,39 +115,17 @@ public class ProposalConfigUnitController {
}
/**
* 同步系统单位
* 同步系统单位
*
* @return
* <p>无入参同步系统部门单位的编码名称和启用状态
* 返回值为 Resultcode 0 表示同步成功msg 为操作结果说明</p>
*/
@At
@SaCheckPermission("proposal.config.unit")
@SLog(tag = "提案", msg = "承办单位同步系统单位")
@Aop(TransAop.READ_COMMITTED)
public Result syncSysUnit() {
Sql sql = Sqls.create("select code from proposal_undertake");
List<Sys_unit> sysUnits = dao.query(Sys_unit.class, Cnd.where(Sys_unit::getUnitcode, "not in", sql).and(Sys_unit::getUnitTypeCode, "=", 1));
List<ProposalUndertake> proposalUndertakes = sysUnits.stream().map(unit -> {
ProposalUndertake proposalUndertake = new ProposalUndertake();
proposalUndertake.setId(unit.getId());
proposalUndertake.setCode(unit.getUnitcode());
proposalUndertake.setEnable(true);
proposalUndertake.setName(unit.getName());
return proposalUndertake;
}).toList();
dao.fastInsert(proposalUndertakes);
dao.update(ProposalUndertake.class, Chain.make("enable",true), Cnd.where("1","=","1"));
Cnd cnd = Cnd.NEW();
SqlExpressionGroup group = Cnd.NEW().where();
group.and("delFlag", "=", true);
cnd.and(group);
List<Sys_unit> delFlagUnit = dao.query(Sys_unit.class, cnd);
if(CollectionUtil.isNotEmpty(delFlagUnit)){
List<String> unitCodeList = delFlagUnit.stream().map(Sys_unit::getUnitcode).collect(Collectors.toList());
Cnd cndUnit = Cnd.NEW();
cndUnit.where().andInStrList("code", unitCodeList);
dao.update(ProposalUndertake.class, Chain.make("enable",false), cndUnit);
}
proposalUndertakeService.syncSysUnits();
return Result.success();
}
@@ -0,0 +1,18 @@
package com.budwk.app.zhgh.democratic.proposal.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake;
/**
* 提案承办单位配置服务
*/
public interface ProposalUndertakeService extends BaseService<ProposalUndertake> {
/**
* 将系统部门单位同步到提案承办单位
*
* <p>无入参以单位编码作为匹配依据新增缺失单位更新已有单位名称
* 并根据系统单位删除标记同步承办单位启用状态返回值为 void</p>
*/
void syncSysUnits();
}
@@ -0,0 +1,86 @@
package com.budwk.app.zhgh.democratic.proposal.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_unit;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake;
import com.budwk.app.zhgh.democratic.proposal.service.ProposalUndertakeService;
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.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.IocBean;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 提案承办单位配置服务实现
*/
@IocBean(args = {"refer:dao"})
public class ProposalUndertakeServiceImpl extends BaseServiceImpl<ProposalUndertake> implements ProposalUndertakeService {
public ProposalUndertakeServiceImpl(Dao dao) {
super(dao);
}
/**
* 将系统部门单位同步到提案承办单位
*
* <p>无入参系统单位编码对应承办单位编码系统单位名称对应承办单位名称
* 新单位会被新增已有单位会同步最新名称已删除的系统单位会被停用返回值为 void</p>
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public void syncSysUnits() {
List<Sys_unit> sysUnits = dao().query(Sys_unit.class, Cnd.where(Sys_unit::getUnitTypeCode, "=", 1));
Map<String, ProposalUndertake> undertakeMap = dao().query(ProposalUndertake.class, Cnd.NEW()).stream()
.filter(undertake -> StrUtil.isNotBlank(undertake.getCode()))
.collect(Collectors.toMap(ProposalUndertake::getCode, Function.identity(), (first, second) -> first));
List<ProposalUndertake> insertList = new ArrayList<>();
for (Sys_unit sysUnit : sysUnits) {
String unitCode = StrUtil.trim(sysUnit.getUnitcode());
if (StrUtil.isBlank(unitCode)) {
continue;
}
String unitName = StrUtil.trim(sysUnit.getName());
ProposalUndertake undertake = undertakeMap.get(unitCode);
if (undertake == null) {
ProposalUndertake insert = new ProposalUndertake();
insert.setId(sysUnit.getId());
insert.setCode(unitCode);
insert.setName(unitName);
insert.setEnable(true);
insertList.add(insert);
} else if (!Objects.equals(undertake.getName(), unitName)) {
// 单位编码保持不变时名称仍可能由数据中心调整需要覆盖承办单位中的旧名称
dao().update(ProposalUndertake.class, Chain.make("name", unitName), Cnd.where(ProposalUndertake::getCode, "=", unitCode));
}
}
if (CollectionUtil.isNotEmpty(insertList)) {
dao().fastInsert(insertList);
}
// 延续原同步规则先启用全部承办单位再停用系统中已标记删除的单位
dao().update(ProposalUndertake.class, Chain.make("enable", true), Cnd.where("1", "=", "1"));
List<String> disabledUnitCodes = dao().query(Sys_unit.class, Cnd.where(Sys_unit::getDelFlag, "=", true)).stream()
.map(Sys_unit::getUnitcode)
.filter(StrUtil::isNotBlank)
.map(StrUtil::trim)
.toList();
if (CollectionUtil.isNotEmpty(disabledUnitCodes)) {
Cnd disabledCnd = Cnd.NEW();
disabledCnd.where().andInStrList("code", disabledUnitCodes);
dao().update(ProposalUndertake.class, Chain.make("enable", false), disabledCnd);
}
}
}
@@ -40,6 +40,7 @@
'assigneeMode',
'buttonConfig',
'enableNextOperator',
'countersignGroupBy',
'h5form'
]"
>
@@ -83,6 +84,19 @@
</template>
<template v-slot="{model,field}">
<el-form-item label="会签办理维度" v-if="['ALL', 'COUNTERSIGN', 'countersign', 1].includes(model.performType)">
<el-select :value="model.countersignGroupBy || 'PERSON'"
@input="handleCountersignGroupChange(model, $event)" style="width: 100%">
<el-option label="按人员(每人分别办理)" value="PERSON"></el-option>
<el-option label="按所属单位(同单位任一人办理)" value="UNIT"></el-option>
<el-option label="按工会(同工会任一人办理)" value="UNION"></el-option>
<el-option label="按承办单位(同承办单位任一人办理)" value="UNDERTAKE"></el-option>
</el-select>
<div style="color: #909399; line-height: 20px; margin-top: 6px;">
分组后,同组任一人办理即完成本组;会签数量和比例按组计算。
意见答复请选择“按承办单位”。配置在新生成的任务中生效,已有待办保持原规则。
</div>
</el-form-item>
<el-form-item label="指定下一步处理人">
<el-checkbox v-model="model['enableNextOperator']">指定下一步处理人</el-checkbox>
</el-form-item>
@@ -114,6 +128,7 @@
id: "${id!}",
designerData: {},
flowData: {},
formLoading: false,
assignmentHandlerClassOptions: [],
candidateHandlerClassOptions: []
}
@@ -135,7 +150,16 @@
}
},
methods: {
// model 为当前节点的插槽表单,value 为所选办理维度;在实例方法内响应式赋值,避免插槽 this 指向错误。
handleCountersignGroupChange(model, value) {
this.$set(model, 'countersignGroupBy', value)
},
handleSave(val) {
// 避免连点重复保存;请求结束后统一恢复保存状态。
if (this.formLoading) {
return
}
this.formLoading = true
const design = {
...this.designerData,
content: val.json
@@ -146,6 +170,8 @@
} else {
window.parent.postMessage("error")
}
}).always(() => {
this.formLoading = false
})
},
getDetail() {
@@ -18,7 +18,7 @@ const UNIT_LEADER_MANAGE_TEMPLATE = {
<table-tool label="人员列表">
<el-button @click="openAdd" type="primary" size="small">新增</el-button>
</table-tool>
<el-table :data="tableData" size="mini">
<el-table :data="tableData" v-loading="tableLoading" size="mini">
<el-table-column label="序号" type="index" width="60">
<template v-slot="scope">{{scope.$index + (pageForm.pageNumber - 1) * pageForm.pageSize + 1}}</template>
</el-table-column>
@@ -36,13 +36,16 @@ const UNIT_LEADER_MANAGE_TEMPLATE = {
<el-dialog title="添加" :visible.sync="dialogFormVisible" width="600px" :close-on-click-modal="false">
<el-form :model="formData" ref="form" size="small" label-width="60px">
<el-form-item prop="roleCode" label="角色">
<dict-select placeholder="请选择角色" v-model="formData.roleCode" code="UNIT_ROLES"></dict-select>
<el-select placeholder="请选择角色" :value="formData.roleCode" @change="(value) => this.$set(this.formData, 'roleCode', value)" style="width: 100%">
<el-option v-for="item in roleOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
</el-form-item>
<el-form-item prop="userId" label="人员">
<user-select
placeholder="请选择人员"
style="width: 100%"
v-model="formData.userId"
:value="formData.userId"
@change="(value) => this.$set(this.formData, 'userId', value)"
api="/platform/sys/unit/listUserSelect"
:api_params="{ unitId: currentData?.id }"
:option_label_func="
@@ -54,10 +57,11 @@ const UNIT_LEADER_MANAGE_TEMPLATE = {
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false"> </el-button>
<el-button type="primary" @click="doSubmit"> </el-button>
<el-button @click="this.$set(this, 'dialogFormVisible', false)" :disabled="formLoading"> </el-button>
<el-button type="primary" :loading="formLoading" @click="doSubmit"> </el-button>
</div>
</el-dialog>
<slot></slot>
</div>
`,
props: {
@@ -70,6 +74,9 @@ const UNIT_LEADER_MANAGE_TEMPLATE = {
return {
dialogFormVisible: false,
formData: {},
roleOptions: [],
tableLoading: false,
formLoading: false,
pageForm: {
unitName: "",
searchName: "username",
@@ -87,20 +94,26 @@ const UNIT_LEADER_MANAGE_TEMPLATE = {
computed: {},
methods: {
doDelete({ userId, roleCode }) {
this.$confirm("您确定要删除吗?", "提示", {
// 删除提案领导会同步取消提案配置中的负责人,操作前明确提示。
const message = roleCode === "PROPOSAL_UNIT_LEADER" ? "删除后将同时取消提案承办单位配置中的该负责人,确定删除吗?" : "您确定要删除吗?"
const unitId = this.currentData.id
this.$confirm(message, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "info"
}).then((res) => {
this.tableLoading = true
$.post("/platform/sys/unit/deleteUnitUserRole", {
userId,
roleCode,
unitId: this.currentData.id
unitId
}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.doSearch()
}
}).always(() => {
this.tableLoading = false
})
})
},
@@ -111,6 +124,13 @@ const UNIT_LEADER_MANAGE_TEMPLATE = {
})
},
doSubmit() {
// 单位、角色和人员必须完整,提交期间阻止重复点击。
if (this.formLoading) return
if (!this.currentData?.id || !this.formData.roleCode || !this.formData.userId) {
this.$message.warning("请选择单位、角色和人员")
return
}
this.formLoading = true
$.post("/platform/sys/unit/insertUnitUserRole", {
...this.formData,
unitId: this.currentData.id
@@ -120,6 +140,8 @@ const UNIT_LEADER_MANAGE_TEMPLATE = {
this.$message.success(res.msg)
this.doSearch()
}
}).always(() => {
this.formLoading = false
})
},
doSearch() {
@@ -134,17 +156,28 @@ const UNIT_LEADER_MANAGE_TEMPLATE = {
this.pageForm.pageSize = val
this.pageData()
},
// 角色选项由当前业务接口提供,保留字典角色并补充提案领导。
loadRoleOptions() {
this.$axios.post("/platform/sys/unit/unitRoleOptions").then((res) => {
if (res.code === 0) this.roleOptions = res.data
})
},
pageData() {
if (!this.currentData?.id) return
this.tableLoading = true
this.pageForm.unitId = this.currentData.id
this.$axios.post(loc() + "/leaderPageData", this.pageForm).then((res) => {
if (res.code === 0) {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
}
}).finally(() => {
this.tableLoading = false
})
}
},
created() {
this.loadRoleOptions()
this.pageData()
}
}
@@ -81,7 +81,7 @@ const OPINION_INFO = {
<template v-if="['schoolAudit', 'office'].includes(task.taskName)">
<el-descriptions-item label="主办单位" :span="3">
{{task.ext.tf_masterUnitName}}
{{task.ext.tf_masterUnitNameStr || task.ext.tf_masterUnitName}}
</el-descriptions-item>
<el-descriptions-item label="协办单位" :span="3">
{{task.ext.tf_slaveUnitNameStr}}
@@ -91,10 +91,10 @@ layout("/layouts/platform.html"){
<el-form-item
label="主办单位"
v-if="isSchoolPass()"
:rules="[{required:true, message:'必填',trigger:['change','blur']}]"
prop="tf_masterUnitId"
:rules="[{type:'array',required:true,min:1,message:'请选择主办单位',trigger:['change','blur']}]"
prop="tf_masterUnitIds"
>
<el-select v-model="formData.tf_masterUnitId" filterable clearable style="width: 100%" placeholder="请选择主办单位">
<el-select v-model="formData.tf_masterUnitIds" multiple filterable clearable style="width: 100%" placeholder="请选择主办单位">
<el-option
v-for="item in underTakeOptions"
:label="item.name"
@@ -112,7 +112,7 @@ layout("/layouts/platform.html"){
:label="item.name"
:value="item.id"
:key="item.id"
:disabled="item.id===formData.tf_masterUnitId"
:disabled="formData.tf_masterUnitIds.includes(item.id)"
></el-option>
</el-select>
</el-form-item>
@@ -123,7 +123,7 @@ layout("/layouts/platform.html"){
</el-form>
<el-row type="flex" justify="end">
<el-button @click="$refs.guava.index()" size="small">取消</el-button>
<el-button @click="handleTaskAction" size="small" type="primary">提交</el-button>
<el-button @click="handleTaskAction" :loading="formLoading" size="small" type="primary">提交</el-button>
</el-row>
</div>
</opinion-info>
@@ -182,7 +182,7 @@ layout("/layouts/platform.html"){
approval: false
},
formData: {
tf_masterUnitId: null,
tf_masterUnitIds: [],
tf_slaveUnitIds: []
},
showApprovalForm: false,
@@ -212,7 +212,7 @@ layout("/layouts/platform.html"){
return
}
this.$nextTick(() => {
this.$refs.formRef && this.$refs.formRef.clearValidate(["typeId", "tf_masterUnitId", "tf_slaveUnitIds"])
this.$refs.formRef && this.$refs.formRef.clearValidate(["typeId", "tf_masterUnitIds", "tf_slaveUnitIds"])
})
},
openView(row) {
@@ -229,7 +229,7 @@ layout("/layouts/platform.html"){
processTaskId: row.taskId,
taskName: row.curTaskName,
typeId: row.typeId,
tf_masterUnitId: null,
tf_masterUnitIds: [],
tf_slaveUnitIds: [],
tf_schoolResult: 'YES',
roleCode: 'OFFICE_MANAGER',
@@ -239,6 +239,9 @@ layout("/layouts/platform.html"){
},
handleTaskAction() {
if (this.formLoading) {
return
}
this.schoolResultChange()
this.$nextTick(() => {
this.$refs.formRef.validate((valid) => {
@@ -248,6 +251,7 @@ layout("/layouts/platform.html"){
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.formLoading = true
const loading = createLoading('提交中')
const submitType = this.formData.tf_schoolResult === 'ROLLBACK' ? 6 : 1
const executeTask = () => {
@@ -259,11 +263,13 @@ layout("/layouts/platform.html"){
tf_opinion: this.formData.tf_opinion,
} : {
...this.formData,
tf_masterUnitName: this.formData.tf_masterUnitId ? this.underTakeOptions.find(v => v.id === this.formData.tf_masterUnitId)?.name : null,
// ID 数组供任务分配使用,名称数组和拼接文本供审批历史展示。
tf_masterUnitNames: this.formData.tf_masterUnitIds.map(v => this.underTakeOptions.find(v2 => v2.id === v)?.name),
tf_masterUnitNameStr: this.formData.tf_masterUnitIds.map(v => this.underTakeOptions.find(v2 => v2.id === v)?.name).join(','),
tf_slaveUnitNames: this.formData.tf_slaveUnitIds.map(v => this.underTakeOptions.find(v2 => v2.id === v)?.name),
tf_slaveUnitNameStr: this.formData.tf_slaveUnitIds.map(v => this.underTakeOptions.find(v2 => v2.id === v)?.name).join(','),
}
this.$axios.post("/flow/common/executeTask", {
return this.$axios.post("/flow/common/executeTask", {
data: JSON.stringify({
...taskData,
submitType: submitType,
@@ -274,27 +280,26 @@ layout("/layouts/platform.html"){
this.$message.success(res.msg)
this.doSearch()
}
}).finally(() => {
loading.close()
})
}
if (submitType === 6 || !this.isSchoolPass()) {
executeTask()
return
return executeTask().finally(() => {
this.formLoading = false
loading.close()
})
}
// 通过审核时校工会可调整意见类别,需要先保存 typeId,再按流程表 YES 分支继续流转
this.$axios.post("/platform/opinion/schoolAudit/edit", {
// 通过审核先保存类别,返回流程提交 Promise,让失败和成功都统一释放 loading
return this.$axios.post("/platform/opinion/schoolAudit/edit", {
info: JSON.stringify({
id: this.formData.id,
typeId: this.formData.typeId
})
}).then((editRes) => {
if (editRes.code === 0) {
executeTask()
return
return executeTask()
}
loading.close()
}).catch(() => {
}).finally(() => {
this.formLoading = false
loading.close()
})
})