diff --git a/src/main/java/com/budwk/app/flow/constant/FlowConst.java b/src/main/java/com/budwk/app/flow/constant/FlowConst.java index 5bff559a..a475df33 100644 --- a/src/main/java/com/budwk/app/flow/constant/FlowConst.java +++ b/src/main/java/com/budwk/app/flow/constant/FlowConst.java @@ -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"; diff --git a/src/main/java/com/budwk/app/flow/controller/FlowCommonController.java b/src/main/java/com/budwk/app/flow/controller/FlowCommonController.java index c3b12329..ad978b97 100644 --- a/src/main/java/com/budwk/app/flow/controller/FlowCommonController.java +++ b/src/main/java/com/budwk/app/flow/controller/FlowCommonController.java @@ -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 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 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") diff --git a/src/main/java/com/budwk/app/flow/engine/AssignmentHandler.java b/src/main/java/com/budwk/app/flow/engine/AssignmentHandler.java index 2990fd39..e7362184 100644 --- a/src/main/java/com/budwk/app/flow/engine/AssignmentHandler.java +++ b/src/main/java/com/budwk/app/flow/engine/AssignmentHandler.java @@ -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 assign(TaskModel model, Execution execution); + + /** + * 提供业务承办单位分组;不支持此维度的处理器返回空映射,由引擎给出配置提示。 + * @param model 当前会签节点 + * @param execution 含业务单据和承办单位选择的流程上下文 + * @param actors 已解析的参与者用户 ID,不能通过分组额外扩大办理权限 + * @return 承办单位 ID 到用户 ID 列表的映射;一人可负责多个单位,分别参与各组任务 + */ + default Map> getUndertakeGroups(TaskModel model, Execution execution, List actors) { + return Collections.emptyMap(); + } default String getMessage() { return this.getClass().getSimpleName(); } diff --git a/src/main/java/com/budwk/app/flow/engine/core/FlowEngineImpl.java b/src/main/java/com/budwk/app/flow/engine/core/FlowEngineImpl.java index c70a34ce..00b32c86 100644 --- a/src/main/java/com/budwk/app/flow/engine/core/FlowEngineImpl.java +++ b/src/main/java/com/budwk/app/flow/engine/core/FlowEngineImpl.java @@ -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 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 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 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 endModelList = model.getModels(EndModel.class); endModelList.forEach(endModel -> { @@ -164,9 +176,14 @@ public class FlowEngineImpl implements FlowEngine { } @Override + @Aop(TransAop.READ_COMMITTED) public List 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); + } + } + } diff --git a/src/main/java/com/budwk/app/flow/engine/handlers/impl/CountersignHandler.java b/src/main/java/com/budwk/app/flow/engine/handlers/impl/CountersignHandler.java index a340c671..6af76286 100644 --- a/src/main/java/com/budwk/app/flow/engine/handlers/impl/CountersignHandler.java +++ b/src/main/java/com/budwk/app/flow/engine/handlers/impl/CountersignHandler.java @@ -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", ""); diff --git a/src/main/java/com/budwk/app/flow/engine/model/TaskModel.java b/src/main/java/com/budwk/app/flow/engine/model/TaskModel.java index 821b54c0..0fc216d1 100644 --- a/src/main/java/com/budwk/app/flow/engine/model/TaskModel.java +++ b/src/main/java/com/budwk/app/flow/engine/model/TaskModel.java @@ -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()) { diff --git a/src/main/java/com/budwk/app/flow/engine/parser/NodeParser.java b/src/main/java/com/budwk/app/flow/engine/parser/NodeParser.java index 3167413c..d483f39e 100644 --- a/src/main/java/com/budwk/app/flow/engine/parser/NodeParser.java +++ b/src/main/java/com/budwk/app/flow/engine/parser/NodeParser.java @@ -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"; // 方法名 diff --git a/src/main/java/com/budwk/app/flow/engine/parser/impl/TaskParser.java b/src/main/java/com/budwk/app/flow/engine/parser/impl/TaskParser.java index 9424d27a..6ec6d4d5 100644 --- a/src/main/java/com/budwk/app/flow/engine/parser/impl/TaskParser.java +++ b/src/main/java/com/budwk/app/flow/engine/parser/impl/TaskParser.java @@ -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); diff --git a/src/main/java/com/budwk/app/flow/service/CountersignGroupService.java b/src/main/java/com/budwk/app/flow/service/CountersignGroupService.java new file mode 100644 index 00000000..19b69e9d --- /dev/null +++ b/src/main/java/com/budwk/app/flow/service/CountersignGroupService.java @@ -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 { + public CountersignGroupService(Dao dao) { + super(dao); + } + + /** @param action 一次完整的分组审批操作。@return 该操作生成的后续任务列表;异常整体回滚。 */ + @Aop(TransAop.READ_COMMITTED) + public List inTransaction(java.util.function.Supplier> action) { + java.util.concurrent.atomic.AtomicReference> 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> resolveGroups(TaskModel model, Execution execution, List actors) { + if (!enabled(model)) throw new BaseException("按人员会签不需要组织分组"); + List users = actors.stream().filter(StrUtil::isNotBlank).distinct().toList(); + if (users.isEmpty()) return new LinkedHashMap<>(); + Map> 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 records = listMap(sql); + String field = "UNIT".equals(model.getCountersignGroupBy()) ? "unitId" : "unionId"; + for (String user : users) { + List 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 covered = new HashSet<>(); + for (Map.Entry> 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 createTasks(TaskModel model, Execution execution, List actors) { + Map> 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 keys = new ArrayList<>(groups.keySet()); + List first = CountersignTypeEnum.PARALLEL.equals(model.getCountersignType()) ? keys : keys.subList(0, 1); + List 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 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 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> members(Dict state) { + Map raw = JSONUtil.toBean(JSONUtil.toJsonStr(state.get("groups")), Dict.class); + Map> 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 completed, int active) { + String prefix = FlowConst.COUNTERSIGN_VARIABLE_PREFIX + state.getStr("node") + "_"; + List keys = new ArrayList<>(members(state).keySet()); + Set done = completed.stream().map(task -> snapshot(task).getStr("group")).collect(Collectors.toSet()); + Set 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 batch = batchTasks(execution.getProcessTask()); + List 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 doneKeys = completed.stream().map(task -> snapshot(task).getStr("group")).collect(Collectors.toSet()); + Map> 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 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 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> 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 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()); + } +} diff --git a/src/main/java/com/budwk/app/flow/service/FlowCommonService.java b/src/main/java/com/budwk/app/flow/service/FlowCommonService.java index 31f866bb..20e476fb 100644 --- a/src/main/java/com/budwk/app/flow/service/FlowCommonService.java +++ b/src/main/java/com/budwk/app/flow/service/FlowCommonService.java @@ -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 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 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 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(); diff --git a/src/main/java/com/budwk/app/flow/service/impl/ProcessTaskServiceImpl.java b/src/main/java/com/budwk/app/flow/service/impl/ProcessTaskServiceImpl.java index bb542295..85833887 100644 --- a/src/main/java/com/budwk/app/flow/service/impl/ProcessTaskServiceImpl.java +++ b/src/main/java/com/budwk/app/flow/service/impl/ProcessTaskServiceImpl.java @@ -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 impleme @Override @Aop(TransAop.READ_COMMITTED) public List createTask(TaskModel taskModel, Execution execution) { + // 分组快照只属于其创建的会签任务,不传递给下一节点。 + execution.getArgs().remove(FlowConst.COUNTERSIGN_GROUP); List processTaskList = new ArrayList<>(); long now = System.currentTimeMillis(); ProcessTask processTask = new ProcessTask(); @@ -236,6 +239,10 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl impleme public void addCandidateActor(Long processTaskId, List 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 impleme public List createCountersignTask(TaskModel taskModel, Execution execution) { List processTaskList = new ArrayList<>(); List 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 createTaskActors = new ArrayList<>(); if (CountersignTypeEnum.PARALLEL.equals(taskModel.getCountersignType())) { // 并行:一个参与者一个任务,同时创建 @@ -485,6 +501,14 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl 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 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 processTaskList = List.of(task); processTaskList.forEach(processTask -> { diff --git a/src/main/java/com/budwk/app/sys/controller/SysUnitController.java b/src/main/java/com/budwk/app/sys/controller/SysUnitController.java index 1de58ea7..d3b1a4eb 100644 --- a/src/main/java/com/budwk/app/sys/controller/SysUnitController.java +++ b/src/main/java/com/budwk/app/sys/controller/SysUnitController.java @@ -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 为系统单位ID;Result.data 包含 list 人员角色列表、totalCount 总数。 */ @At("/leaderPageData") @Ok("json") @SaCheckLogin public Object leaderPageData(PageForm pageForm, String unitId) { - List unitRoles = sysDictService.getSubListByCode("UNIT_ROLES"); - if (Lang.isEmpty(unitRoles)) { - return Result.success(); + if (StrUtil.isBlank(unitId)) { + return Result.error("请选择单位"); } - List 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 为人员ID,roleCode 为角色编码,unitId 为系统单位ID;返回 Result,code=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 为人员ID,roleCode 为角色编码,unitId 为系统单位ID;返回 Result,code=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(); } diff --git a/src/main/java/com/budwk/app/sys/services/SysUnitService.java b/src/main/java/com/budwk/app/sys/services/SysUnitService.java index 257cd73c..cd1f596a 100644 --- a/src/main/java/com/budwk/app/sys/services/SysUnitService.java +++ b/src/main/java/com/budwk/app/sys/services/SysUnitService.java @@ -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 { + /** 无参数;返回单位干部角色选项,code 为角色编码,name 为显示名称,包含提案承办单位领导。 */ + List getUnitRoleOptions(); + + /** pageForm 为姓名/工号搜索和分页参数,unitId 为系统单位ID;返回 list 和 totalCount 等分页数据。 */ + Pagination leaderPageData(PageForm pageForm, String unitId); + + /** userId 为人员ID,roleCode 为角色编码,unitId 为系统单位ID;新增对应负责人关系,返回 void。 */ + void insertUnitUserRole(String userId, String roleCode, String unitId); + + /** userId 为人员ID,roleCode 为角色编码,unitId 为系统单位ID;仅删除该单位对应角色关系,返回 void。 */ + void deleteUnitUserRole(String userId, String roleCode, String unitId); + /** * 保存单位 * diff --git a/src/main/java/com/budwk/app/sys/services/impl/SysUnitServiceImpl.java b/src/main/java/com/budwk/app/sys/services/impl/SysUnitServiceImpl.java index 953e0142..40b9c8a2 100644 --- a/src/main/java/com/budwk/app/sys/services/impl/SysUnitServiceImpl.java +++ b/src/main/java/com/budwk/app/sys/services/impl/SysUnitServiceImpl.java @@ -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 implements Sys super(dao); } + @Inject + private SysDictService sysDictService; + @Inject + private SysRoleService sysRoleService; + @Inject + private SysUserService sysUserService; + + /** 复制字典选项后补充提案角色,避免修改字典缓存或生成重复选项。 */ + @Override + public List getUnitRoleOptions() { + List 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 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(); + } + /** * 新增单位 * diff --git a/src/main/java/com/budwk/app/zhgh/democratic/opinion/handler/OpinionMasterUnitAssignmentHandler.java b/src/main/java/com/budwk/app/zhgh/democratic/opinion/handler/OpinionMasterUnitAssignmentHandler.java index e3ddc7cd..548bc4d5 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/opinion/handler/OpinionMasterUnitAssignmentHandler.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/opinion/handler/OpinionMasterUnitAssignmentHandler.java @@ -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 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 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> getUndertakeGroups(TaskModel model, Execution execution, List 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 sysUserRoles = dao.query(Sys_user_role.class, Cnd.where(Sys_user_role::getRoleId, "=", sysRole.getId()).and(Sys_user_role::getUnderTakeId, "=", undertake.getId())); - List 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(); + } } diff --git a/src/main/java/com/budwk/app/zhgh/democratic/opinion/handler/OpinionSlaveUnitAssignmentHandler.java b/src/main/java/com/budwk/app/zhgh/democratic/opinion/handler/OpinionSlaveUnitAssignmentHandler.java index fcca0cd6..82305718 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/opinion/handler/OpinionSlaveUnitAssignmentHandler.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/opinion/handler/OpinionSlaveUnitAssignmentHandler.java @@ -24,6 +24,12 @@ import java.util.List; * @Description TODO */ public class OpinionSlaveUnitAssignmentHandler implements AssignmentHandler { + /** 按所选协办单位返回已授权成员,供分组会签生成共享答复任务。 */ + @Override + public java.util.Map> getUndertakeGroups(TaskModel model, Execution execution, List actors) { + return ServiceContext.find(com.budwk.app.zhgh.democratic.opinion.service.OpinionSchoolAuditService.class) + .getUndertakeGroups(execution, false, actors); + } @Override public List assign(TaskModel model, Execution execution) { Dao dao = ServiceContext.find(Dao.class); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/opinion/listenter/OpinionOfficeSuffixInterceptor.java b/src/main/java/com/budwk/app/zhgh/democratic/opinion/listenter/OpinionOfficeSuffixInterceptor.java index 8a0f523b..52757311 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/opinion/listenter/OpinionOfficeSuffixInterceptor.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/opinion/listenter/OpinionOfficeSuffixInterceptor.java @@ -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 helpUnitIds = (List) 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 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); + } } diff --git a/src/main/java/com/budwk/app/zhgh/democratic/opinion/mode/OpinionSchoolAuditImportExcelMode.java b/src/main/java/com/budwk/app/zhgh/democratic/opinion/mode/OpinionSchoolAuditImportExcelMode.java index e0eb4960..4a00f498 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/opinion/mode/OpinionSchoolAuditImportExcelMode.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/opinion/mode/OpinionSchoolAuditImportExcelMode.java @@ -31,9 +31,11 @@ public class OpinionSchoolAuditImportExcelMode extends ExcelImportError { private String unitName; @Excel(name = "主办") + // 多个主办单位名称或编码仅使用中文分号“;”分隔,单位全称内部标点保留,表头不变。 private String masterUnitName; @Excel(name = "协办") + // 多个协办单位与主办使用相同的中文分号“;”分隔规则,未指定协办时可留空。 private String slaveUnitNames; @Excel(name = "审核结果") diff --git a/src/main/java/com/budwk/app/zhgh/democratic/opinion/service/OpinionSchoolAuditService.java b/src/main/java/com/budwk/app/zhgh/democratic/opinion/service/OpinionSchoolAuditService.java index af9ba234..4b1f0f42 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/opinion/service/OpinionSchoolAuditService.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/opinion/service/OpinionSchoolAuditService.java @@ -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 { + /** + * 校验主协办单位并解析主办负责人,兼容历史单主办流程变量。 + * @param model 主办答复节点定义,多主办要求并行会签 + * @param execution 流程上下文,args 中传 tf_masterUnitIds、tf_slaveUnitIds 等单位 ID 数组 + * @return 去重后的主办负责人用户 ID 列表,供 WF 引擎创建任务 + */ + List assignMasterUnits(TaskModel model, Execution execution); + + /** + * @param execution 意见审核上下文,包含所选主办/协办单位 ID 数组 + * @param master true 为主办,false 为协办 + * @param actors 本节点已授权负责人 ID,不把未授权人员加入分组 + * @return 承办单位 ID 到负责人 ID 列表,供 WF 生成每单位一份共享任务 + */ + java.util.Map> getUndertakeGroups(Execution execution, boolean master, List actors); + + /** + * 审核通过后保存主协办记录,并按负责人配置关联新生成的答复任务。 + * @param execution 当前意见实例、审核变量和新生成任务列表;主办兼容旧 tf_masterUnitId + * @return 无返回值,保存 opinion_reply_unit 及任务中的承办单位信息 + */ + void saveReplyUnits(Execution execution); + /** * 下载校工会审核结果导入模板。 * diff --git a/src/main/java/com/budwk/app/zhgh/democratic/opinion/service/common/OpinionCommonServiceImpl.java b/src/main/java/com/budwk/app/zhgh/democratic/opinion/service/common/OpinionCommonServiceImpl.java index b9178de7..dfb82a91 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/opinion/service/common/OpinionCommonServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/opinion/service/common/OpinionCommonServiceImpl.java @@ -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 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 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 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 imple OpinionSearchParam.buildSearch(cnd, pageForm); cnd.groupBy("t.id"); sql.setCondition(cnd); - return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql); + Pagination page = listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql); + // 单位成员仍可查看共享已办记录;撤回按钮由实际办理人及当前 WF 状态共同决定。 + for (NutMap row : page.getList()) { + row.put("canRevoke", flowCommonService.canRevokeTask(row.getLong("taskId"))); + } + return page; } @Override diff --git a/src/main/java/com/budwk/app/zhgh/democratic/opinion/service/impl/OpinionSchoolAuditServiceImpl.java b/src/main/java/com/budwk/app/zhgh/democratic/opinion/service/impl/OpinionSchoolAuditServiceImpl.java index cf50f85e..3de03f49 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/opinion/service/impl/OpinionSchoolAuditServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/opinion/service/impl/OpinionSchoolAuditServiceImpl.java @@ -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 private FlowEngine flowEngine; @Inject private FlowCommonService flowCommonService; + @Inject + private SysRoleService sysRoleService; public OpinionSchoolAuditServiceImpl(Dao dao) { super(dao); } + @Override + public List assignMasterUnits(TaskModel model, Execution execution) { + List 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 users = new LinkedHashSet<>(); + Sys_role role = sysRoleService.getByCode(RoleConstant.PROPOSAL_UNIT_LEADER); + if (role == null) { + throw new BaseException("未配置承办单位负责人角色"); + } + for (ProposalUndertake unit : masters) { + List 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> getUndertakeGroups(Execution execution, boolean master, List actors) { + List masters = resolveMasterUnits(execution); + List slaves = validateSlaveUnits(execution.getArgs(), masters); + Sys_role role = sysRoleService.getByCode(RoleConstant.PROPOSAL_UNIT_LEADER); + if (role == null) throw new BaseException("未配置承办单位负责人角色"); + java.util.Map> groups = new java.util.LinkedHashMap<>(); + for (ProposalUndertake unit : master ? masters : slaves) { + // 按业务选择的承办单位分组,同一负责人管理多个单位时仍生成多份独立答复。 + List 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 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 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 selected = new HashSet<>(); + List 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 validateSlaveUnits(Dict args, List masters) { + List slaves = resolveUnits(args.get("tf_slaveUnitIds"), "协办", false); + Set 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 masters = resolveMasterUnits(execution); + List slaves = validateSlaveUnits(execution.getArgs(), masters); + // 全部校验通过后替换承办记录,撤回重审时保留本次选择的完整主协办集合。 + List 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 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 actorIds = dao().query(ProcessTaskActor.class, Cnd.where("processTaskId", "=", task.getId())) + .stream().map(ProcessTaskActor::getActorId).toList(); + List 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 matchedUnitIds = group != null && "UNDERTAKE".equals(group.getStr("mode")) + ? List.of(group.getStr("group")) : unitIds; + boolean master = "master_reply".equals(task.getTaskName()); + List 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 entities = new ArrayList<>(); @@ -81,6 +260,7 @@ public class OpinionSchoolAuditServiceImpl extends BaseServiceImpl } @Override + @Aop(TransAop.READ_COMMITTED) public ExcelImportRes importAuditResult(TempFile file, String sessionId) { ExcelImportRes excelImportRes = new ExcelImportRes<>(); List importRows; @@ -125,6 +305,7 @@ public class OpinionSchoolAuditServiceImpl extends BaseServiceImpl return excelImportRes; } + @Aop(TransAop.READ_COMMITTED) private void handleAuditImportRow(OpinionSchoolAuditImportExcelMode row, int rowIndex, String sessionId, Set importedCodes) { List errors = new ArrayList<>(); String code = StrUtil.trim(row.getCode()); @@ -145,7 +326,7 @@ public class OpinionSchoolAuditServiceImpl extends BaseServiceImpl } OpinionType type = null; - ProposalUndertake masterUnit = null; + List masterUnits = new ArrayList<>(); List slaveUnits = new ArrayList<>(); if (auditResult == AuditResult.PASS) { // 审核通过时与页面必填规则保持一致:意见类别、主办单位、审核意见必须填写。 @@ -161,18 +342,16 @@ public class OpinionSchoolAuditServiceImpl extends BaseServiceImpl 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 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 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 slaveUnits) { + OpinionType type, List masterUnits, List 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 // 通过审核时与页面单条审核保持一致:先保存意见类别,再提交主办、协办等流程表单字段。 updateOpinionType(opinionId, type.getId()); taskData.set("typeId", type.getId()); - taskData.set("tf_masterUnitId", masterUnit.getId()); - taskData.set("tf_masterUnitName", masterUnit.getName()); + // 主办与协办均使用数组传递,名称字符串供审批记录直接展示。 + List 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 slaveUnitIds = slaveUnits.stream().map(ProposalUndertake::getId).collect(Collectors.toList()); List slaveUnitNames = slaveUnits.stream().map(ProposalUndertake::getName).collect(Collectors.toList()); taskData.set("tf_slaveUnitIds", slaveUnitIds); @@ -243,6 +426,7 @@ public class OpinionSchoolAuditServiceImpl extends BaseServiceImpl 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 return undertakes.size() == 1 ? undertakes.get(0) : null; } - private List parseSlaveUnits(String slaveUnitText, List errors) { + /** + * 解析导入行的主办或协办单位,仅以中文分号“;”分隔,避免拆开单位全称内部的标点。 + * @param slaveUnitText 主办或协办名称/编码文本,多单位使用中文分号连接;空白文本返回空列表 + * @param label 错误提示中的业务角色,传“主办”或“协办” + * @param errors 接收不存在、重复等校验错误的列表,交由调用方汇总到当前 Excel 行 + * @return 按输入顺序解析的有效承办单位列表,按单位 ID 查重;空项忽略,错误项不加入 + */ + private List parseUnits(String slaveUnitText, String label, List errors) { List units = new ArrayList<>(); if (StrUtil.isBlank(slaveUnitText)) { return units; } - String[] items = slaveUnitText.split("[,,、;;\\n\\r]+"); + String[] items = slaveUnitText.split(";"); Set 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); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/config/ProposalConfigUnitController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/config/ProposalConfigUnitController.java index b3a67ed7..09d4e349 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/config/ProposalConfigUnitController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/config/ProposalConfigUnitController.java @@ -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 + *

无入参;同步系统部门单位的编码、名称和启用状态; + * 返回值为 Result,code 为 0 表示同步成功,msg 为操作结果说明。

*/ @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 sysUnits = dao.query(Sys_unit.class, Cnd.where(Sys_unit::getUnitcode, "not in", sql).and(Sys_unit::getUnitTypeCode, "=", 1)); - List 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 delFlagUnit = dao.query(Sys_unit.class, cnd); - if(CollectionUtil.isNotEmpty(delFlagUnit)){ - List 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(); } diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/ProposalUndertakeService.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/ProposalUndertakeService.java new file mode 100644 index 00000000..80c2bf63 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/ProposalUndertakeService.java @@ -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 { + + /** + * 将系统部门单位同步到提案承办单位。 + * + *

无入参;以单位编码作为匹配依据,新增缺失单位、更新已有单位名称, + * 并根据系统单位删除标记同步承办单位启用状态。返回值为 void。

+ */ + void syncSysUnits(); +} diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/impl/ProposalUndertakeServiceImpl.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/impl/ProposalUndertakeServiceImpl.java new file mode 100644 index 00000000..1b82b0df --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/impl/ProposalUndertakeServiceImpl.java @@ -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 implements ProposalUndertakeService { + + public ProposalUndertakeServiceImpl(Dao dao) { + super(dao); + } + + /** + * 将系统部门单位同步到提案承办单位。 + * + *

无入参;系统单位编码对应承办单位编码,系统单位名称对应承办单位名称; + * 新单位会被新增,已有单位会同步最新名称,已删除的系统单位会被停用;返回值为 void。

+ */ + @Override + @Aop(TransAop.READ_COMMITTED) + public void syncSysUnits() { + List sysUnits = dao().query(Sys_unit.class, Cnd.where(Sys_unit::getUnitTypeCode, "=", 1)); + Map 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 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 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); + } + } +} diff --git a/src/main/resources/views/platform/flow/design/designer.html b/src/main/resources/views/platform/flow/design/designer.html index e72586b3..f43d8f2d 100644 --- a/src/main/resources/views/platform/flow/design/designer.html +++ b/src/main/resources/views/platform/flow/design/designer.html @@ -40,6 +40,7 @@ 'assigneeMode', 'buttonConfig', 'enableNextOperator', + 'countersignGroupBy', 'h5form' ]" > @@ -83,6 +84,19 @@