Compare commits
10
Commits
c18579fe40
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77da090407 | ||
|
|
15d298c301 | ||
|
|
705fe4a086 | ||
|
|
1151e9d586 | ||
|
|
be9ba54b58 | ||
|
|
8301342598 | ||
|
|
4373cc0095 | ||
|
|
1faebf3bc2 | ||
|
|
a67f80bec9 | ||
|
|
f2b02f8b49 |
@@ -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")
|
||||
|
||||
@@ -230,7 +230,7 @@ public class FlowTodoCenterController {
|
||||
dao.execute(todoSql);
|
||||
int todoCount = todoSql.getInt();
|
||||
|
||||
// 查询我的已办任务
|
||||
// 已办统计排除发起节点 startTask,与已办列表保持一致,避免将本人发起流程计入已办数量。
|
||||
Sql doneSql = Sqls.create("""
|
||||
SELECT
|
||||
count(1)
|
||||
@@ -240,6 +240,7 @@ public class FlowTodoCenterController {
|
||||
WHERE
|
||||
ta.actorId = @userId
|
||||
AND t.taskState = 20
|
||||
AND t.taskName != 'startTask'
|
||||
""");
|
||||
doneSql.setParam("userId", SecurityUtil.getUserId());
|
||||
doneSql.setCallback(Sqls.callback.integer());
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -69,17 +69,6 @@ public class SysDataUserPullController {
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@At
|
||||
@SaCheckPermission("sys.data.user.pull")
|
||||
@ApiOperation("同步教职工手机号")
|
||||
public Result syncTeacherMobile() {
|
||||
try {
|
||||
return Result.success(sysUserPullService.syncTeacherMobile());
|
||||
} catch (Exception e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@At
|
||||
@SaCheckPermission("sys.data.user.pull")
|
||||
@ApiOperation("删除用户数据")
|
||||
|
||||
@@ -17,6 +17,8 @@ import com.budwk.app.sys.services.*;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
@@ -47,6 +49,8 @@ public class SysDataUserUpdateController {
|
||||
@Inject
|
||||
private SysUserService sysUserService;
|
||||
@Inject
|
||||
private SysUserMobileSyncService sysUserMobileSyncService;
|
||||
@Inject
|
||||
private SysDataUserPullService sysDataUserPullService;
|
||||
@Inject
|
||||
private SysDataUnitPullService sysDataUnitPullService;
|
||||
@@ -62,7 +66,8 @@ public class SysDataUserUpdateController {
|
||||
@At
|
||||
@SaCheckPermission("sys.data.user.pull")
|
||||
@ApiOperation("分页数据")
|
||||
@Ok("json:{locked:'password|idCard|mobile'}")
|
||||
// 手机号用于列表核对拉取结果;密码和身份证号仍不返回。
|
||||
@Ok("json:{locked:'password|idCard'}")
|
||||
public Result pageData(@Valid SysDataUserUpdatePageForm pageForm) {
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT
|
||||
@@ -136,4 +141,46 @@ public class SysDataUserUpdateController {
|
||||
sysDataDictPullService.pullDataDict();
|
||||
return Result.success();
|
||||
}
|
||||
/**
|
||||
* 预览:scope传ALL/MEMBER/FILTER,mode传FILL/REFRESH,pageForm传列表筛选条件。
|
||||
* 返回NutMap,scopeCount/existingCount/cooldownCount/total分别为范围、已有号、冷却及待请求工号数。
|
||||
*/
|
||||
@At
|
||||
@SaCheckPermission("sys.data.user.update")
|
||||
public Result mobilePreview(String scope, String mode, SysDataUserUpdatePageForm pageForm) {
|
||||
return Result.success(sysUserMobileSyncService.preview(scope, mode, pageForm));
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动:scope/mode及pageForm同预览;action传NEW/RESUME/RETRY,sourceTaskId仅继续或重试时传原任务编号。
|
||||
* 返回NutMap进度:taskId任务编号、status状态及total/processed/updated/empty/failed/skipped各项人数。
|
||||
*/
|
||||
@At
|
||||
@SaCheckPermission("sys.data.user.update")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
@SLog(tag = "手机号拉取", msg = "启动手机号后台拉取任务")
|
||||
public Result mobileStart(String scope, String mode, SysDataUserUpdatePageForm pageForm, String sourceTaskId, String action) {
|
||||
return Result.success(sysUserMobileSyncService.start(scope, mode, pageForm, sourceTaskId, action));
|
||||
}
|
||||
|
||||
/**
|
||||
* 进度:taskId可为空(最近任务)或传启动接口返回的编号;返回与启动一致的NutMap,不返回手机号与工号队列。
|
||||
*/
|
||||
@At
|
||||
@SaCheckPermission("sys.data.user.update")
|
||||
public Result mobileStatus(String taskId) {
|
||||
return Result.success(sysUserMobileSyncService.status(taskId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止:taskId必须传当前任务编号;返回NutMap最新进度,当前请求结束后停止且保留已完成结果。
|
||||
*/
|
||||
@At
|
||||
@SaCheckPermission("sys.data.user.update")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
@SLog(tag = "手机号拉取", msg = "停止手机号后台拉取任务")
|
||||
public Result mobileStop(String taskId) {
|
||||
return Result.success(sysUserMobileSyncService.stop(taskId));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<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 为人员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();
|
||||
}
|
||||
|
||||
|
||||
@@ -53,4 +53,20 @@ public interface SysDataUserPullService extends BaseService<Sys_user_source> {
|
||||
* @return 同步过程统计信息,包含工号手机号映射
|
||||
*/
|
||||
NutMap syncTeacherMobileByJobNos(List<String> jobNos);
|
||||
/**
|
||||
* 按单个工号读取数据中心手机号,不修改数据库;空字符串表示接口明确没有号码。
|
||||
* @param jobNo 非空教职工工号
|
||||
* @return 已校验的手机号;接口异常、工号不匹配或号码格式异常时抛出异常
|
||||
*/
|
||||
String fetchTeacherMobile(String jobNo);
|
||||
|
||||
/**
|
||||
* 保存单人工号的有效号码,使用请求前快照避免覆盖期间的人工修改。
|
||||
* @param jobNo 数据中心查询工号
|
||||
* @param mobile 已校验的非空手机号
|
||||
* @param originalMobiles 请求前的用户ID到原手机号映射
|
||||
* @param fillOnly true仅补空号,false核对更新已有号码
|
||||
* @return 实际更新的系统用户行数
|
||||
*/
|
||||
int saveTeacherMobile(String jobNo, String mobile, Map<String, String> originalMobiles, boolean fillOnly);
|
||||
}
|
||||
|
||||
@@ -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 为人员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);
|
||||
|
||||
/**
|
||||
* 保存单位
|
||||
*
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
package com.budwk.app.sys.services;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.budwk.app.base.constant.RedisConstant;
|
||||
import com.budwk.app.base.constant.RoleConstant;
|
||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||
import com.budwk.app.base.exception.BaseException;
|
||||
import com.budwk.app.sys.models.Sys_user;
|
||||
import com.budwk.app.sys.param.SysDataUserUpdatePageForm;
|
||||
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.integration.jedis.RedisService;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.json.Json;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 手机号补齐任务:工号去重、会员优先、单并发限速,Redis保存队列和检查点。
|
||||
* 编排线程不持有数据库事务;每人工号通过注入的拉取service以独立短事务写入。
|
||||
*/
|
||||
@IocBean
|
||||
public class SysUserMobileSyncService {
|
||||
// 同一hash tag使任务原子脚本兼容Redis Cluster;记录保留七天,不保存手机号。
|
||||
private static final String PREFIX = RedisConstant.PLATFORM_REDIS_PREFIX + "{userMobileSync}:";
|
||||
private static final String LOCK = PREFIX + "lock";
|
||||
private static final String LATEST = PREFIX + "latest";
|
||||
private static final String EMPTY = PREFIX + "empty";
|
||||
private static final int RETAIN_SECONDS = 7 * 24 * 3600;
|
||||
private static final int LEASE_SECONDS = 120;
|
||||
@Inject private Dao dao;
|
||||
@Inject private RedisService redisService;
|
||||
@Inject private SysDataUserPullService sysDataUserPullService;
|
||||
@Inject private ThreadPoolTaskExecutor threadPoolTaskExecutor;
|
||||
|
||||
/**
|
||||
* 预览待请求工号范围,不调用外部接口。
|
||||
* @param scope ALL全部、MEMBER仅会员、FILTER当前筛选的全部结果
|
||||
* @param mode FILL仅补空号、REFRESH核对范围内所有人员
|
||||
* @param form 姓名、工号、单位、在职状态、编制类别、教职工类别;不使用分页参数
|
||||
* @return scopeCount范围工号数、existingCount已有号码数、cooldownCount冷却数、total待请求数
|
||||
*/
|
||||
public NutMap preview(String scope, String mode, SysDataUserUpdatePageForm form) {
|
||||
checkAccess();
|
||||
return select(scope, mode, form, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动后台任务,返回脱敏进度;同一时间只允许一个本功能任务。
|
||||
* @param scope ALL/MEMBER/FILTER
|
||||
* @param mode FILL/REFRESH
|
||||
* @param form 当前列表筛选条件(仅FILTER使用)
|
||||
* @param sourceTaskId 继续或重试的原任务编号;新任务传空
|
||||
* @param action NEW新任务、RESUME继续未处理部分、RETRY仅失败部分
|
||||
* @return taskId、status及total/processed/updated/empty/failed/skipped统计
|
||||
*/
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public NutMap start(String scope, String mode, SysDataUserUpdatePageForm form, String sourceTaskId, String action) {
|
||||
checkAccess();
|
||||
List<String> queue;
|
||||
if ("NEW".equals(action)) {
|
||||
NutMap selection = select(scope, mode, form, true);
|
||||
queue = (List<String>) selection.get("queue");
|
||||
} else if ("RESUME".equals(action) || "RETRY".equals(action)) {
|
||||
NutMap source = read(sourceTaskId);
|
||||
if (source.isEmpty()) throw new BaseException("任务记录已过期,请重新预览");
|
||||
if (sourceTaskId.equals(redisService.get(LOCK))) throw new BaseException("原任务仍在执行");
|
||||
mode = source.getString("mode");
|
||||
scope = source.getString("scope");
|
||||
if ("RETRY".equals(action)) {
|
||||
queue = strings(source.get("failedJobs"));
|
||||
} else {
|
||||
List<String> original = queue(sourceTaskId);
|
||||
queue = new ArrayList<>(original.subList(Math.min(source.getInt("processed", 0), original.size()), original.size()));
|
||||
// 继续时将原任务失败项排在未处理项之后,避免切换最近任务后遗失失败重试入口。
|
||||
queue.addAll(strings(source.get("failedJobs")));
|
||||
queue = new ArrayList<>(new LinkedHashSet<>(queue));
|
||||
}
|
||||
} else {
|
||||
throw new BaseException("不支持的任务操作");
|
||||
}
|
||||
if (queue.isEmpty()) throw new BaseException("没有需要处理的人员");
|
||||
String id = UUID.randomUUID().toString();
|
||||
NutMap state = NutMap.NEW().addv("taskId", id).addv("status", "RUNNING")
|
||||
.addv("scope", scope).addv("mode", mode).addv("total", queue.size())
|
||||
.addv("processed", 0).addv("updated", 0).addv("empty", 0).addv("failed", 0)
|
||||
.addv("skipped", 0).addv("failedJobs", new ArrayList<String>())
|
||||
.addv("message", "后台拉取中,每秒最多一个工号请求")
|
||||
.addv("startedAt", System.currentTimeMillis());
|
||||
// 锁、队列、初始进度同时写入,防止重复点击及进程中途退出留下半个任务。
|
||||
String script = "if redis.call('exists',KEYS[1]) == 1 then return 0 end "
|
||||
+ "redis.call('setex',KEYS[1],ARGV[2],ARGV[1]); "
|
||||
+ "redis.call('setex',KEYS[2],ARGV[3],ARGV[1]); "
|
||||
+ "redis.call('setex',KEYS[3],ARGV[3],ARGV[4]); "
|
||||
+ "redis.call('setex',KEYS[4],ARGV[3],ARGV[5]); return 1";
|
||||
Object acquired = redisService.eval(script, Arrays.asList(LOCK, LATEST, stateKey(id), PREFIX + "queue:" + id),
|
||||
Arrays.asList(id, String.valueOf(LEASE_SECONDS), String.valueOf(RETAIN_SECONDS), Json.toJson(state), Json.toJson(queue)));
|
||||
if (!Long.valueOf(1).equals(acquired)) throw new BaseException("已有手机号任务执行中,请查看当前进度");
|
||||
NutMap initial = publicState(state);
|
||||
final List<String> jobs = queue;
|
||||
try {
|
||||
threadPoolTaskExecutor.execute(() -> run(state, jobs));
|
||||
} catch (RuntimeException e) {
|
||||
state.put("status", "INTERRUPTED");
|
||||
state.put("message", "后台执行资源繁忙,请稍后继续任务");
|
||||
checkpoint(state, true);
|
||||
throw new BaseException("后台执行资源繁忙,请稍后继续任务");
|
||||
}
|
||||
return initial;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定任务;taskId为空读取最近任务。锁过期的运行任务显示中断,可手动继续。
|
||||
* @param taskId 后端返回的UUID任务编号或空值
|
||||
* @return 进度对象,不含工号队列、手机号或失败人员明细
|
||||
*/
|
||||
public NutMap status(String taskId) {
|
||||
checkAccess();
|
||||
String id = StrUtil.isBlank(taskId) ? redisService.get(LATEST) : taskId;
|
||||
NutMap state = read(id);
|
||||
if (state.isEmpty()) return state;
|
||||
if ("RUNNING".equals(state.getString("status")) && !id.equals(redisService.get(LOCK))) {
|
||||
state.put("status", "INTERRUPTED");
|
||||
state.put("message", "任务心跳已中断,可继续未完成部分;已保存的号码不会丢失");
|
||||
} else if ("RUNNING".equals(state.getString("status")) && "1".equals(redisService.get(PREFIX + "stop:" + id))) {
|
||||
state.put("message", "正在停止,等待当前工号处理结束");
|
||||
}
|
||||
return publicState(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求停止指定任务;当前工号处理结束后保存检查点,已完成结果不回滚。
|
||||
* @param taskId 当前任务UUID
|
||||
* @return 最新脱敏进度对象
|
||||
*/
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public NutMap stop(String taskId) {
|
||||
checkAccess();
|
||||
validateId(taskId);
|
||||
if (taskId.equals(redisService.get(LOCK))) redisService.setex(PREFIX + "stop:" + taskId, RETAIN_SECONDS, "1");
|
||||
return status(taskId);
|
||||
}
|
||||
|
||||
/** 按工号去重并优先会员;读取冷却时间,不在预览中发起远程调用。 */
|
||||
private NutMap select(String scope, String mode, SysDataUserUpdatePageForm form, boolean includeQueue) {
|
||||
if (!Arrays.asList("ALL", "MEMBER", "FILTER").contains(scope)
|
||||
|| !Arrays.asList("FILL", "REFRESH").contains(mode)) throw new BaseException("手机号拉取范围或模式无效");
|
||||
Cnd cnd = Cnd.where("u.loginname", "IS NOT", null).and("TRIM(u.loginname)", "<>", "");
|
||||
if ("MEMBER".equals(scope)) cnd.and("u.member", "=", true);
|
||||
if ("FILTER".equals(scope)) {
|
||||
if (form == null) throw new BaseException("缺少列表筛选条件");
|
||||
cnd.and(Cnd.likeEX("u.username", form.getUserName()));
|
||||
cnd.and(Cnd.likeEX("u.loginname", form.getLoginName()));
|
||||
cnd.andEX("u.unitId", "=", form.getUnitId());
|
||||
cnd.andEX("u.userState", "=", form.getUserState());
|
||||
cnd.andEX("u.preparedBy", "=", form.getPreparedBy());
|
||||
cnd.andEX("u.personType", "=", form.getPersonType());
|
||||
}
|
||||
Sql sql = Sqls.create("SELECT u.loginname, MAX(COALESCE(u.member,0)) AS memberFirst, "
|
||||
+ "MIN(CASE WHEN u.mobile IS NULL OR TRIM(u.mobile) = '' THEN 0 ELSE 1 END) AS hasMobile "
|
||||
+ "FROM vw_user u $condition GROUP BY u.loginname ORDER BY memberFirst DESC,u.loginname");
|
||||
sql.setCondition(cnd);
|
||||
sql.setCallback(Sqls.callback.maps());
|
||||
dao.execute(sql);
|
||||
Map<String, String> emptyTimes = redisService.hgetAll(EMPTY);
|
||||
int existing = 0, cooldown = 0;
|
||||
List<String> jobs = new ArrayList<>();
|
||||
List<NutMap> rows = sql.getList(NutMap.class);
|
||||
for (NutMap row : rows) {
|
||||
String jobNo = row.getString("loginname");
|
||||
boolean hasMobile = row.getInt("hasMobile", 0) == 1;
|
||||
if (hasMobile) existing++;
|
||||
if ("FILL".equals(mode) && hasMobile) continue;
|
||||
if (inCooldown(emptyTimes.get(jobNo))) { cooldown++; continue; }
|
||||
jobs.add(jobNo);
|
||||
}
|
||||
NutMap result = NutMap.NEW().addv("scopeCount", rows.size()).addv("existingCount", existing)
|
||||
.addv("cooldownCount", cooldown).addv("total", jobs.size());
|
||||
if (includeQueue) result.put("queue", jobs);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 编排只负责远程读取与检查点;每人独立提交,连续五次失败暂停,支持手动继续和失败重试。 */
|
||||
private void run(NutMap state, List<String> jobs) {
|
||||
String id = state.getString("taskId");
|
||||
boolean fillOnly = "FILL".equals(state.getString("mode"));
|
||||
int consecutiveFailures = 0;
|
||||
try {
|
||||
for (String jobNo : jobs) {
|
||||
if (!checkpoint(state, false)) return;
|
||||
if ("1".equals(redisService.get(PREFIX + "stop:" + id))) {
|
||||
state.put("status", "STOPPED");
|
||||
state.put("message", "已停止,可继续未完成部分");
|
||||
break;
|
||||
}
|
||||
if (inCooldown(redisService.hget(EMPTY, jobNo))) {
|
||||
increment(state, "skipped");
|
||||
} else {
|
||||
List<Sys_user> users = dao.query(Sys_user.class, Cnd.where(Sys_user::getLoginname, "=", jobNo));
|
||||
Map<String, String> originals = new LinkedHashMap<>();
|
||||
for (Sys_user user : users) {
|
||||
if (!fillOnly || StrUtil.isBlank(user.getMobile())) originals.put(user.getId(), user.getMobile());
|
||||
}
|
||||
if (originals.isEmpty()) {
|
||||
increment(state, "skipped");
|
||||
} else {
|
||||
try {
|
||||
String mobile = sysDataUserPullService.fetchTeacherMobile(jobNo);
|
||||
// 外部请求返回后再次核对租约,过期的旧worker不得继续落库或提交检查点。
|
||||
if (!checkpoint(state, false)) return;
|
||||
if (StrUtil.isBlank(mobile)) {
|
||||
redisService.hset(EMPTY, jobNo, String.valueOf(System.currentTimeMillis()));
|
||||
redisService.expire(EMPTY, RETAIN_SECONDS);
|
||||
increment(state, "empty");
|
||||
} else {
|
||||
int changed = sysDataUserPullService.saveTeacherMobile(jobNo, mobile, originals, fillOnly);
|
||||
increment(state, changed > 0 ? "updated" : "skipped");
|
||||
}
|
||||
consecutiveFailures = 0;
|
||||
} catch (Exception e) {
|
||||
// 不将第三方异常原文返回前端或写日志,避免泄露响应中的手机号和凭据。
|
||||
increment(state, "failed");
|
||||
((List<String>) state.get("failedJobs")).add(jobNo);
|
||||
consecutiveFailures++;
|
||||
}
|
||||
// 每人工号请求结束后至少等待一秒;失败逐步退避,五连败后暂停。
|
||||
if (consecutiveFailures < 5) Thread.sleep(Math.max(1000, consecutiveFailures * 2000L));
|
||||
}
|
||||
}
|
||||
increment(state, "processed");
|
||||
if (consecutiveFailures >= 5) {
|
||||
state.put("status", "PAUSED");
|
||||
state.put("message", "连续五次拉取失败,已暂停;请检查接口后继续或重试失败项");
|
||||
break;
|
||||
}
|
||||
if (!checkpoint(state, false)) return;
|
||||
}
|
||||
if ("RUNNING".equals(state.getString("status"))) {
|
||||
state.put("status", "COMPLETED");
|
||||
state.put("message", state.getInt("failed", 0) > 0 ? "拉取结束,部分失败可单独重试" : "拉取完成");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (e instanceof InterruptedException) Thread.currentThread().interrupt();
|
||||
state.put("status", "INTERRUPTED");
|
||||
state.put("message", "任务执行中断,可继续未完成部分");
|
||||
} finally {
|
||||
checkpoint(state, true);
|
||||
}
|
||||
}
|
||||
|
||||
/** 原子校验锁持有者后保存进度及续期;旧任务不能覆盖新任务,也不能释放别人的锁。 */
|
||||
private boolean checkpoint(NutMap state, boolean finish) {
|
||||
String id = state.getString("taskId");
|
||||
state.put("updatedAt", System.currentTimeMillis());
|
||||
String script = "if redis.call('get',KEYS[1]) ~= ARGV[1] then return 0 end "
|
||||
+ "redis.call('setex',KEYS[2],ARGV[2],ARGV[3]); "
|
||||
+ "redis.call('expire',KEYS[3],ARGV[2]); redis.call('expire',KEYS[4],ARGV[2]); "
|
||||
+ (finish ? "redis.call('del',KEYS[1]); " : "redis.call('expire',KEYS[1],ARGV[4]); ") + "return 1";
|
||||
return Long.valueOf(1).equals(redisService.eval(script,
|
||||
Arrays.asList(LOCK, stateKey(id), PREFIX + "queue:" + id, LATEST),
|
||||
Arrays.asList(id, String.valueOf(RETAIN_SECONDS), Json.toJson(state), String.valueOf(LEASE_SECONDS))));
|
||||
}
|
||||
|
||||
/** 全局人员拉取仅开放给系统管理员和校工会管理员,沿用现有会员手机号入口的角色限制。 */
|
||||
private void checkAccess() {
|
||||
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
|
||||
throw new BaseException("当前用户无权拉取系统人员手机号");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean inCooldown(String time) {
|
||||
if (time == null) return false;
|
||||
try { return System.currentTimeMillis() - Long.parseLong(time) < RETAIN_SECONDS * 1000L; }
|
||||
catch (NumberFormatException e) { return false; }
|
||||
}
|
||||
|
||||
private void increment(NutMap state, String field) { state.put(field, state.getInt(field, 0) + 1); }
|
||||
private String stateKey(String id) { return PREFIX + "state:" + id; }
|
||||
private void validateId(String id) {
|
||||
if (id == null || !id.matches("[0-9a-fA-F-]{36}")) throw new BaseException("任务编号无效");
|
||||
}
|
||||
private NutMap read(String id) {
|
||||
if (StrUtil.isBlank(id)) return NutMap.NEW();
|
||||
validateId(id);
|
||||
String json = redisService.get(stateKey(id));
|
||||
return json == null ? NutMap.NEW() : Json.fromJson(NutMap.class, json);
|
||||
}
|
||||
private List<String> queue(String id) {
|
||||
String json = redisService.get(PREFIX + "queue:" + id);
|
||||
if (json == null) throw new BaseException("任务队列已过期,请重新预览");
|
||||
return JSONUtil.toList(JSONUtil.parseArray(json), String.class);
|
||||
}
|
||||
private List<String> strings(Object value) {
|
||||
return value == null ? new ArrayList<>() : JSONUtil.toList(JSONUtil.parseArray(value), String.class);
|
||||
}
|
||||
private NutMap publicState(NutMap state) {
|
||||
NutMap result = NutMap.NEW();
|
||||
result.putAll(state);
|
||||
result.remove("failedJobs");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -438,6 +438,101 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
|
||||
return listMap(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台任务逐工号读取号码:每个分页请求均设置超时,且不输出号码或接口原文。
|
||||
* 分页间等待一秒,避免单人工号多页响应绕过任务限速。
|
||||
*/
|
||||
@Override
|
||||
public String fetchTeacherMobile(String jobNo) {
|
||||
DataCenterProperties.Credential credential = dataCenterProperties.credential(TEACHER_MOBILE_CONFIG_KEY);
|
||||
Set<String> numbers = new LinkedHashSet<>();
|
||||
int pages = 1;
|
||||
for (int page = 1; page <= pages; page++) {
|
||||
if (page > 1) {
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new BaseException("手机号拉取已中断");
|
||||
}
|
||||
}
|
||||
String body;
|
||||
try (cn.hutool.http.HttpResponse response = HttpUtil.createPost(credential.getUrl())
|
||||
.header(Header.CONTENT_TYPE, "application/json")
|
||||
.header("X-H3C-TOKEN", credential.getToken())
|
||||
.timeout(10000)
|
||||
.body(JSONUtil.toJsonStr(buildTeacherMobileRequestBody(jobNo, page, TEACHER_MOBILE_PAGE_SIZE)))
|
||||
.execute()) {
|
||||
if (response.getStatus() != 200) {
|
||||
throw new BaseException("手机号接口HTTP异常,请稍后重试");
|
||||
}
|
||||
body = response.body();
|
||||
}
|
||||
pages = collectTeacherMobilePage(body, jobNo, numbers);
|
||||
}
|
||||
return numbers.isEmpty() ? "" : numbers.iterator().next();
|
||||
}
|
||||
|
||||
/** 校验单工号响应结构、页数与号码一致性;只有明确的空records或空号码才视为无号码。 */
|
||||
private int collectTeacherMobilePage(String body, String jobNo, Set<String> numbers) {
|
||||
JSONObject response = JSONUtil.parseObj(body);
|
||||
if (!"0".equals(response.getStr("code"))) {
|
||||
throw new BaseException("手机号接口未成功返回,请检查接口授权或稍后重试");
|
||||
}
|
||||
JSONObject data = response.getJSONObject("data");
|
||||
JSONObject pagination = data == null ? response : data;
|
||||
int pages = Math.max(pagination.getInt("pages", 1),
|
||||
(int) Math.ceil(pagination.getInt("total", 0) / (double) TEACHER_MOBILE_PAGE_SIZE));
|
||||
// 单工号异常大分页通常意味着过滤条件失效,禁止继续当作全量接口拉取。
|
||||
if (pages > 5) {
|
||||
throw new BaseException("单工号返回数据量异常");
|
||||
}
|
||||
JSONArray records = response.getJSONArray("records");
|
||||
if (records == null && data != null) records = data.getJSONArray("records");
|
||||
if (records == null) {
|
||||
throw new BaseException("手机号接口缺少records字段");
|
||||
}
|
||||
for (Object record : records) {
|
||||
JSONObject row = JSONUtil.parseObj(record);
|
||||
if (!jobNo.equals(StrUtil.trimToEmpty(row.getStr("gh")))) {
|
||||
throw new BaseException("手机号接口返回工号不匹配");
|
||||
}
|
||||
String mobile = StrUtil.trimToEmpty(row.getStr("sjh"));
|
||||
if (mobile.startsWith("+86")) mobile = mobile.substring(3);
|
||||
if (mobile.startsWith("0086")) mobile = mobile.substring(4);
|
||||
if (StrUtil.isNotBlank(mobile)) {
|
||||
if (!mobile.matches("1[3-9][0-9]{9}")) {
|
||||
throw new BaseException("接口手机号格式异常");
|
||||
}
|
||||
numbers.add(mobile);
|
||||
}
|
||||
}
|
||||
if (numbers.size() > 1) {
|
||||
throw new BaseException("同一工号返回多个不同手机号,需人工核实");
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅在原手机号未被他人修改时落库;空返回值不清空原号,补齐模式不覆盖已有号码。
|
||||
*/
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public int saveTeacherMobile(String jobNo, String mobile, Map<String, String> originalMobiles, boolean fillOnly) {
|
||||
if (StrUtil.isBlank(mobile) || !mobile.matches("1[3-9][0-9]{9}")) return 0;
|
||||
int updated = 0;
|
||||
for (Map.Entry<String, String> entry : originalMobiles.entrySet()) {
|
||||
String oldMobile = entry.getValue();
|
||||
if (Objects.equals(oldMobile, mobile) || (fillOnly && StrUtil.isNotBlank(oldMobile))) continue;
|
||||
Cnd cnd = Cnd.where(Sys_user::getId, "=", entry.getKey())
|
||||
.and(Sys_user::getLoginname, "=", jobNo);
|
||||
if (oldMobile == null) cnd.and("mobile", "IS", null);
|
||||
else cnd.and("mobile", "=", oldMobile);
|
||||
updated += dao().update(Sys_user.class, Chain.make("mobile", mobile), cnd);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据中心分页同步教职工手机号,并按工号更新 sys_user.mobile。
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增单位
|
||||
*
|
||||
|
||||
+3
-4
@@ -54,8 +54,7 @@ public class HonorBoardController {
|
||||
}
|
||||
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT YEAR
|
||||
( h.grantDate ) as year,
|
||||
SELECT CAST(YEAR(h.grantDate) AS UNSIGNED) as year,
|
||||
hbs.`name`
|
||||
FROM
|
||||
honor h
|
||||
@@ -82,7 +81,7 @@ public class HonorBoardController {
|
||||
SELECT
|
||||
hbs.`name`,
|
||||
count( 1 ) AS num,
|
||||
YEAR ( h.grantDate ) as year
|
||||
CAST(YEAR(h.grantDate) AS UNSIGNED) as year
|
||||
FROM
|
||||
honor h
|
||||
LEFT JOIN honor_basic_settings hbs ON hbs.id = h.prize
|
||||
@@ -106,7 +105,7 @@ public class HonorBoardController {
|
||||
SELECT
|
||||
hbs.`name`,
|
||||
count( 1 ) AS num,
|
||||
YEAR ( h.grantDate ) AS YEAR
|
||||
CAST(YEAR(h.grantDate) AS UNSIGNED) AS YEAR
|
||||
FROM
|
||||
honor h
|
||||
LEFT JOIN honor_basic_settings hbs ON hbs.id = h.honorLevel
|
||||
|
||||
+1
-1
@@ -117,7 +117,7 @@ public class OutlayReimburseApplyServiceImpl extends BaseServiceImpl<OutlayReimb
|
||||
|
||||
if (outlayManageSource.equals("ACTIVITY_BUDGET_TYPE_ONE")) {
|
||||
OutlayManageSchool school = dao().fetch(OutlayManageSchool.class, Cnd.where("year", "=", DateUtil.thisYear()));
|
||||
map.put("budgetMoney", school.getTotalQuota().subtract(school.getUsedQuota()));
|
||||
map.put("budgetMoney", ObjectUtil.isNotEmpty(school) ? school.getTotalQuota().subtract(school.getUsedQuota()) : 0);
|
||||
} else if (outlayManageSource.equals("ACTIVITY_BUDGET_TYPE_TWO")) {
|
||||
OutlayManageUnion union = dao().fetch(OutlayManageUnion.class, Cnd.where("year", "=", DateUtil.thisYear())
|
||||
.and("unionId", "=", StrUtil.isNotBlank(unionId) ? unionId : SecurityUtil.getUnionId()));
|
||||
|
||||
+24
-69
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -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);
|
||||
|
||||
+13
-77
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -31,9 +31,11 @@ public class OpinionSchoolAuditImportExcelMode extends ExcelImportError {
|
||||
private String unitName;
|
||||
|
||||
@Excel(name = "主办")
|
||||
// 多个主办单位名称或编码仅使用中文分号“;”分隔,单位全称内部标点保留,表头不变。
|
||||
private String masterUnitName;
|
||||
|
||||
@Excel(name = "协办")
|
||||
// 多个协办单位与主办使用相同的中文分号“;”分隔规则,未指定协办时可留空。
|
||||
private String slaveUnitNames;
|
||||
|
||||
@Excel(name = "审核结果")
|
||||
|
||||
+26
@@ -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_masterUnitIds、tf_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);
|
||||
|
||||
/**
|
||||
* 下载校工会审核结果导入模板。
|
||||
*
|
||||
|
||||
+9
-3
@@ -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
|
||||
|
||||
+212
-21
@@ -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);
|
||||
|
||||
+8
-30
@@ -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>无入参;同步系统部门单位的编码、名称和启用状态;
|
||||
* 返回值为 Result,code 为 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();
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -201,6 +201,7 @@ public class ProposalUnderTakeReplyController {
|
||||
@At
|
||||
@SaCheckPermission("proposal.unitReply")
|
||||
@ApiOperation("执行任务")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result executeTask(@Param("data") String data) {
|
||||
Dict args = Json.fromJson(Dict.class, data);
|
||||
String proposalId = args.getStr("proposalId");
|
||||
|
||||
+4
-9
@@ -3,7 +3,6 @@ package com.budwk.app.zhgh.democratic.proposal.interceptor;
|
||||
import cn.hutool.core.lang.Dict;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.budwk.app.base.constant.RoleConstant;
|
||||
import com.budwk.app.flow.constant.FlowConst;
|
||||
import com.budwk.app.flow.engine.FlowInterceptor;
|
||||
import com.budwk.app.flow.engine.core.Execution;
|
||||
@@ -11,19 +10,16 @@ import com.budwk.app.flow.engine.core.ServiceContext;
|
||||
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.entity.ProcessTaskActor;
|
||||
import com.budwk.app.sys.models.Sys_role;
|
||||
import com.budwk.app.sys.services.SysRoleService;
|
||||
import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo;
|
||||
import com.budwk.app.zhgh.democratic.proposal.models.ProposalReplyUnit;
|
||||
import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake;
|
||||
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
|
||||
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.json.Json;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -36,6 +32,7 @@ public class ProposalCaseFilingInterceptor implements FlowInterceptor {
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void intercept(Execution execution) {
|
||||
Dao dao = ServiceContext.find(Dao.class);
|
||||
ProposalCommonService proposalCommonService = ServiceContext.find(ProposalCommonService.class);
|
||||
// 提案ID
|
||||
String proposalId = execution.getProcessInstance().getBusinessNo();
|
||||
|
||||
@@ -87,10 +84,8 @@ public class ProposalCaseFilingInterceptor implements FlowInterceptor {
|
||||
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();
|
||||
ProposalReplyUnit replyUnit = dao.fetch(ProposalReplyUnit.class, Cnd.where(ProposalReplyUnit::getProposalId, "=", proposalId).and(ProposalReplyUnit::getUnitId, "=", actorUnitId));
|
||||
// 按办理人的角色承办单位匹配,系统所属单位可能与提案承办单位不同。
|
||||
ProposalReplyUnit replyUnit = proposalCommonService.getReplyUnitByTaskActorRole(proposalId, task.getId(), task.getTaskName());
|
||||
|
||||
Dict variable = FlowUtil.variableToDict(task.getVariable());
|
||||
variable.set("underTakeName", replyUnit.getUnitName());
|
||||
|
||||
+31
-13
@@ -6,10 +6,11 @@ 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.proposal.models.ProposalReplyUnit;
|
||||
import org.nutz.dao.Cnd;
|
||||
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.json.Json;
|
||||
|
||||
import java.util.List;
|
||||
@@ -20,26 +21,43 @@ import java.util.List;
|
||||
*/
|
||||
public class ProposalUnderTakeInfoInterceptor implements FlowInterceptor {
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void intercept(Execution execution) {
|
||||
Dao dao = ServiceContext.find(Dao.class);
|
||||
ProposalCommonService proposalCommonService = ServiceContext.find(ProposalCommonService.class);
|
||||
|
||||
// 提案ID
|
||||
String proposalId = execution.getProcessInstance().getBusinessNo();
|
||||
// 当前任务
|
||||
// 创建后续任务时 execution.processTask 会被替换,processTaskId 才始终指向本次办结的原任务。
|
||||
ProcessTask finishedTask = dao.fetch(ProcessTask.class, execution.getProcessTaskId());
|
||||
if (finishedTask != null && "slave_reply".equals(finishedTask.getTaskName())) {
|
||||
setReplyUnitVariable(proposalCommonService, proposalId, finishedTask);
|
||||
dao.update(finishedTask, "variable");
|
||||
}
|
||||
|
||||
// 为本次流转新生成的主办答复任务设置承办单位。
|
||||
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();
|
||||
ProposalReplyUnit replyUnit = dao.fetch(ProposalReplyUnit.class, Cnd.where(ProposalReplyUnit::getProposalId, "=", proposalId).and(ProposalReplyUnit::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));
|
||||
setReplyUnitVariable(proposalCommonService, proposalId, task);
|
||||
}
|
||||
dao.update(processTaskList,"variable");
|
||||
}
|
||||
|
||||
/**
|
||||
* 按任务全部办理人的负责人角色承办单位,补充任务显示所需的承办单位信息。
|
||||
*
|
||||
* @param proposalCommonService 提案公共服务
|
||||
* @param proposalId 当前流程对应的提案 ID
|
||||
* @param task 当前办结或本次新生成的承办答复任务
|
||||
*/
|
||||
private void setReplyUnitVariable(ProposalCommonService proposalCommonService, String proposalId, ProcessTask task) {
|
||||
ProposalReplyUnit replyUnit = proposalCommonService.getReplyUnitByTaskActorRole(
|
||||
proposalId, task.getId(), task.getTaskName());
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -7,8 +7,10 @@ import com.budwk.app.flow.entity.ProcessInstance;
|
||||
import com.budwk.app.flow.entity.ProcessTask;
|
||||
import com.budwk.app.flow.enums.ProcessEventTypeEnum;
|
||||
import com.budwk.app.zhgh.democratic.proposal.models.ProposalReplyUnit;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
|
||||
@@ -22,10 +24,21 @@ public class ProposalCaseTaskRevokeEventListener implements ProcessEventListener
|
||||
private Dao dao;
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void onEvent(ProcessEvent event) {
|
||||
if (event.getEventType() == ProcessEventTypeEnum.PROCESS_TASK_REVOKE) {
|
||||
Long taskId = event.getSourceId();
|
||||
ProcessTask task = dao.fetch(ProcessTask.class, taskId);
|
||||
if (task == null || task.getTaskParentId() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 通用撤回事件传入的是被撤销的后续任务,只有其父任务为委员会立案时才清理承办单位。
|
||||
ProcessTask parentTask = dao.fetch(ProcessTask.class, task.getTaskParentId());
|
||||
if (parentTask == null || !"committeeFiling".equals(parentTask.getTaskName())) {
|
||||
return;
|
||||
}
|
||||
|
||||
ProcessInstance instance = dao.fetch(ProcessInstance.class, task.getProcessInstanceId());
|
||||
String businessNo = instance.getBusinessNo();
|
||||
dao.clear(ProposalReplyUnit.class, Cnd.where(ProposalReplyUnit::getProposalId, "=", businessNo));
|
||||
|
||||
+18
@@ -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();
|
||||
}
|
||||
+11
@@ -2,6 +2,7 @@ package com.budwk.app.zhgh.democratic.proposal.service.common;
|
||||
|
||||
import com.budwk.app.base.service.BaseService;
|
||||
import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo;
|
||||
import com.budwk.app.zhgh.democratic.proposal.models.ProposalReplyUnit;
|
||||
import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
|
||||
@@ -11,6 +12,16 @@ import java.util.List;
|
||||
|
||||
public interface ProposalCommonService extends BaseService<ProposalInfo> {
|
||||
|
||||
/**
|
||||
* 根据任务办理人的“提案承办单位领导”角色匹配提案承办单位。
|
||||
*
|
||||
* @param proposalId 提案 ID,对应 proposal_reply_unit.proposalId
|
||||
* @param taskId 流程任务 ID,用于获取该任务的全部办理人
|
||||
* @param taskName 任务节点名称;master_reply、opinion_master_reply 匹配主办单位,slave_reply 匹配协办单位
|
||||
* @return 当前任务对应的 ProposalReplyUnit,包含承办单位 ID、名称及主协办类型
|
||||
*/
|
||||
ProposalReplyUnit getReplyUnitByTaskActorRole(String proposalId, Long taskId, String taskName);
|
||||
|
||||
/**
|
||||
* 查询提案信息
|
||||
*/
|
||||
|
||||
+72
@@ -15,6 +15,7 @@ import com.budwk.app.flow.engine.FlowEngine;
|
||||
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.entity.ProcessTaskActor;
|
||||
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
|
||||
import com.budwk.app.flow.vo.ProcessTaskVO;
|
||||
import com.budwk.app.sys.models.Sys_dict;
|
||||
@@ -28,6 +29,7 @@ import com.budwk.app.web.commons.base.Globals;
|
||||
import com.budwk.app.zhgh.democratic.proposal.models.ProposalConsolidation;
|
||||
import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo;
|
||||
import com.budwk.app.zhgh.democratic.proposal.models.ProposalMerge;
|
||||
import com.budwk.app.zhgh.democratic.proposal.models.ProposalReplyUnit;
|
||||
import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake;
|
||||
import com.budwk.app.zhgh.democratic.teachercongress.prepare.models.Teacher_congress_session;
|
||||
import com.deepoove.poi.XWPFTemplate;
|
||||
@@ -74,6 +76,76 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
|
||||
super(dao);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据任务办理人的承办单位角色确定当前提案的主办或协办单位。
|
||||
*
|
||||
* @param proposalId 提案 ID,不能为空
|
||||
* @param taskId 流程任务 ID,不能为空且任务必须存在办理人
|
||||
* @param taskName master_reply、opinion_master_reply 或 slave_reply
|
||||
* @return 唯一匹配的提案承办单位记录
|
||||
*/
|
||||
@Override
|
||||
public ProposalReplyUnit getReplyUnitByTaskActorRole(String proposalId, Long taskId, String taskName) {
|
||||
if (StrUtil.isBlank(proposalId) || taskId == null || StrUtil.isBlank(taskName)) {
|
||||
throw new BaseException("提案承办单位匹配参数不完整");
|
||||
}
|
||||
|
||||
boolean isMaster;
|
||||
if (List.of("master_reply", "opinion_master_reply").contains(taskName)) {
|
||||
isMaster = true;
|
||||
} else if ("slave_reply".equals(taskName)) {
|
||||
isMaster = false;
|
||||
} else {
|
||||
throw new BaseException("当前任务不是提案承办答复节点,任务ID:" + taskId);
|
||||
}
|
||||
|
||||
List<ProcessTaskActor> taskActors = dao().query(ProcessTaskActor.class,
|
||||
Cnd.where(ProcessTaskActor::getProcessTaskId, "=", taskId));
|
||||
if (taskActors.isEmpty()) {
|
||||
throw new BaseException("提案承办答复任务没有配置办理人,任务ID:" + taskId);
|
||||
}
|
||||
|
||||
Sys_role leaderRole = sysRoleService.getByCode(RoleConstant.PROPOSAL_UNIT_LEADER.name());
|
||||
if (leaderRole == null) {
|
||||
throw new BaseException("系统未配置提案承办单位领导角色");
|
||||
}
|
||||
|
||||
List<String> actorIds = taskActors.stream().map(ProcessTaskActor::getActorId).distinct().toList();
|
||||
List<Sys_user_role> actorRoles = dao().query(Sys_user_role.class,
|
||||
Cnd.where(Sys_user_role::getRoleId, "=", leaderRole.getId())
|
||||
.and(Sys_user_role::getUserId, "in", actorIds));
|
||||
Map<String, Set<String>> actorUnitIds = actorRoles.stream()
|
||||
.filter(role -> StrUtil.isNotBlank(role.getUnderTakeId()))
|
||||
.collect(Collectors.groupingBy(Sys_user_role::getUserId,
|
||||
Collectors.mapping(Sys_user_role::getUnderTakeId, Collectors.toSet())));
|
||||
|
||||
// 同一流程任务的办理人应归属同一个角色承办单位,取全部办理人的共同承办单位避免误匹配。
|
||||
Set<String> commonUnitIds = null;
|
||||
for (String actorId : actorIds) {
|
||||
Set<String> unitIds = actorUnitIds.getOrDefault(actorId, Collections.emptySet());
|
||||
if (commonUnitIds == null) {
|
||||
commonUnitIds = new HashSet<>(unitIds);
|
||||
} else {
|
||||
commonUnitIds.retainAll(unitIds);
|
||||
}
|
||||
}
|
||||
if (commonUnitIds == null || commonUnitIds.isEmpty()) {
|
||||
throw new BaseException("任务办理人的提案承办单位角色与当前任务不匹配,任务ID:" + taskId);
|
||||
}
|
||||
|
||||
List<ProposalReplyUnit> replyUnits = dao().query(ProposalReplyUnit.class,
|
||||
Cnd.where(ProposalReplyUnit::getProposalId, "=", proposalId)
|
||||
.and(ProposalReplyUnit::getUnitId, "in", commonUnitIds)
|
||||
.and(ProposalReplyUnit::getIsMaster, "=", isMaster));
|
||||
if (replyUnits.isEmpty()) {
|
||||
throw new BaseException("任务办理人的角色承办单位未包含在当前提案承办单位中,任务ID:" + taskId);
|
||||
}
|
||||
if (replyUnits.size() > 1) {
|
||||
throw new BaseException("任务办理人匹配到多个提案承办单位,请检查角色配置,任务ID:" + taskId);
|
||||
}
|
||||
return replyUnits.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NutMap info(String id) {
|
||||
if (StrUtil.isBlank(id)) {
|
||||
|
||||
+86
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-29
@@ -5,11 +5,8 @@ import cn.dev33.satoken.annotation.SaMode;
|
||||
import cn.hutool.core.util.DesensitizedUtil;
|
||||
import com.alibaba.excel.EasyExcel;
|
||||
import com.budwk.app.base.annotation.SLog;
|
||||
import com.budwk.app.base.constant.RoleConstant;
|
||||
import com.budwk.app.base.page.Pagination;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.sys.services.SysDataUserPullService;
|
||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||
import com.budwk.app.zhgh.staffmanage.member.models.MemberHistory;
|
||||
import com.budwk.app.zhgh.staffmanage.member.param.pageform.MemberInfoPageForm;
|
||||
import com.budwk.app.zhgh.staffmanage.member.service.MemberInfoService;
|
||||
@@ -51,9 +48,6 @@ public class MemberInfoGroupController {
|
||||
@Inject
|
||||
private MemberInfoService memberInfoService;
|
||||
|
||||
@Inject
|
||||
private SysDataUserPullService sysUserPullService;
|
||||
|
||||
@At("")
|
||||
@Ok("beetl:/platform/zhgh/staffmanage/member/info/group/index.html")
|
||||
@SaCheckPermission("member.info.group")
|
||||
@@ -86,29 +80,6 @@ public class MemberInfoGroupController {
|
||||
return Result.success(memberInfoService.dao().count(MemberHistory.class, Cnd.where("year", "=", year)));
|
||||
}
|
||||
|
||||
@At
|
||||
@SaCheckPermission("member.info.group")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
@SLog(tag = "会员档案管理", msg = "获取会员手机号")
|
||||
public Result syncMobile() {
|
||||
try {
|
||||
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
|
||||
return Result.error("当前用户无权获取会员手机号");
|
||||
}
|
||||
List<String> jobNos = memberInfoService.queryRandomBlankMobileMemberJobNos(500);
|
||||
if (jobNos.isEmpty()) {
|
||||
return Result.error("当前没有手机号为空的会员");
|
||||
}
|
||||
NutMap result = sysUserPullService.syncTeacherMobileByJobNos(jobNos);
|
||||
result.put("sampleCount", jobNos.size());
|
||||
return Result.success(result);
|
||||
} catch (Exception e) {
|
||||
log.error("会员手机号获取失败", e);
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@At
|
||||
@SaCheckPermission("member.info.group")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
|
||||
@@ -69,11 +69,13 @@ const initTableMixins = {
|
||||
const address = this.pageDataUrl ? this.pageDataUrl : loc() + "/pageData"
|
||||
this.tableLoading = true
|
||||
this.$axios.post(address, data ? data : this.pageForm).then((res) => {
|
||||
this.tableLoading = false
|
||||
if (res.code === 0) {
|
||||
this.tableData = res.data.list
|
||||
this.pageForm.totalCount = res.data.totalCount
|
||||
}
|
||||
}).finally(() => {
|
||||
// 列表请求成功或失败后均关闭加载遮罩,避免异常时持续转圈。
|
||||
this.tableLoading = false
|
||||
})
|
||||
},
|
||||
notifySuccess(msg) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -63,7 +63,6 @@ layout("/layouts/platform.html"){
|
||||
<el-card shadow="never">
|
||||
<table-tool :label="pageForm.pullTime ? pageForm.pullTime + '数据' : '全部数据'">
|
||||
<el-button type="primary" size="mini" :loading="pullLoading" @click="pull" icon="el-icon-download">拉取信息中心数据</el-button>
|
||||
<el-button type="primary" size="mini" :loading="syncTeacherMobileLoading" @click="syncTeacherMobile" icon="el-icon-mobile-phone">同步教职工手机号</el-button>
|
||||
<el-button type="danger" size="mini" @click="openDelete" icon="el-icon-delete">删除本地数据源</el-button>
|
||||
</table-tool>
|
||||
<el-table :key="tableKey" :data="tableData" @sort-change="pageOrder" header-align="center" v-loading="tableLoading">
|
||||
@@ -121,8 +120,7 @@ layout("/layouts/platform.html"){
|
||||
pullTimeOptions: false,
|
||||
sourceTime: [],
|
||||
pullTimeDialogVisible: false,
|
||||
pullLoading: false,
|
||||
syncTeacherMobileLoading: false
|
||||
pullLoading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -150,42 +148,6 @@ layout("/layouts/platform.html"){
|
||||
.catch(() => {})
|
||||
},
|
||||
|
||||
syncTeacherMobile() {
|
||||
this.$confirm("确定要同步教职工手机号吗?", "提示", {
|
||||
type: "warning",
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消"
|
||||
})
|
||||
.then(() => {
|
||||
this.syncTeacherMobileLoading = true
|
||||
this.$axios
|
||||
.post("/platform/sys/data/user/pull/syncTeacherMobile")
|
||||
.then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
const data = resp.data || {}
|
||||
this.$alert(
|
||||
"接口总数:" + (data.total || 0) +
|
||||
"<br>总页数:" + (data.pages || 0) +
|
||||
"<br>拉取数量:" + (data.pulledCount || 0) +
|
||||
"<br>有效手机号数量:" + (data.validMobileCount || 0) +
|
||||
"<br>匹配用户数量:" + (data.matchedUserCount || 0) +
|
||||
"<br>更新用户数量:" + (data.updatedUserCount || 0),
|
||||
"同步完成",
|
||||
{
|
||||
dangerouslyUseHTMLString: true,
|
||||
confirmButtonText: "确定"
|
||||
}
|
||||
)
|
||||
this.pageData()
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.syncTeacherMobileLoading = false
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
},
|
||||
|
||||
getSearchOptions() {
|
||||
this.$axios.post("/platform/sys/data/user/pull/searchOptions").then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
|
||||
@@ -51,10 +51,11 @@ layout("/layouts/platform.html"){
|
||||
<el-card shadow="never">
|
||||
<table-tool label="系统当前用户数据">
|
||||
<el-button type="primary" size="mini" @click="updateDataDict" icon="el-icon-refresh">更新【码表】数据</el-button>
|
||||
<el-button v-if="$auth.hasRoleOr(['SYSADMIN', 'SCHOOL_UNION_ADMIN'])" type="primary" size="mini" @click="openMobileSync" icon="el-icon-phone">拉取【手机号】</el-button>
|
||||
<el-button type="primary" size="mini" @click="updateUnits" icon="el-icon-refresh">更新【单位】数据</el-button>
|
||||
<el-button type="primary" size="mini" @click="openUpdate" icon="el-icon-refresh">更新【人员】数据</el-button>
|
||||
</table-tool>
|
||||
<el-table :key="tableKey" :data="tableData" @sort-change="pageOrder" header-align="center">
|
||||
<el-table v-loading="tableLoading" :key="tableKey" :data="tableData" @sort-change="pageOrder" header-align="center">
|
||||
<el-table-column type="index" width="70" label="序号" fixed="left"></el-table-column>
|
||||
<el-table-column prop="loginname" label="工号" width="100" fixed="left"></el-table-column>
|
||||
<el-table-column prop="username" label="姓名" fixed="left" width="120" show-overflow-tooltip></el-table-column>
|
||||
@@ -67,7 +68,8 @@ layout("/layouts/platform.html"){
|
||||
</el-table-column>
|
||||
<el-table-column prop="changeTime" label="变更时间" sortable width="150"></el-table-column>
|
||||
<el-table-column prop="sex" label="性别" sortable></el-table-column>
|
||||
<!-- <el-table-column prop="mobile" label="手机号" width="120"></el-table-column>-->
|
||||
<!-- 展示系统当前手机号,便于核对后台拉取结果。 -->
|
||||
<el-table-column prop="mobile" label="手机号" width="130"></el-table-column>
|
||||
<el-table-column prop="birthday" label="生日" sortable width="120">
|
||||
<template slot-scope="{row}">{{$moment(row.birthday).format('YYYY-MM-DD')}}</template>
|
||||
</el-table-column>
|
||||
@@ -75,14 +77,8 @@ layout("/layouts/platform.html"){
|
||||
<el-table-column prop="preparedBy" label="编制类别" sortable width="120"></el-table-column>
|
||||
<el-table-column prop="personType" label="教职工类别" sortable width="120"></el-table-column>
|
||||
<el-table-column prop="arrivalAtSchoolDate" label="来校年月" sortable width="120"></el-table-column>
|
||||
<el-table-column prop="technicalTitle" label="技术职称" sortable width="120"></el-table-column>
|
||||
<el-table-column prop="position" label="职务" sortable width="120" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="education" label="学历" show-overflow-tooltip sortable width="120"></el-table-column>
|
||||
<el-table-column prop="academicDegree" label="学位" show-overflow-tooltip sortable width="120"></el-table-column>
|
||||
<el-table-column prop="unitName" label="单位" show-overflow-tooltip sortable width="120"></el-table-column>
|
||||
<el-table-column prop="unitId" label="单位编码" sortable width="120"></el-table-column>
|
||||
<el-table-column prop="nationality" label="国籍" sortable></el-table-column>
|
||||
<el-table-column prop="nation" label="民族" sortable></el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
@@ -129,6 +125,51 @@ layout("/layouts/platform.html"){
|
||||
<el-button type="primary" @click="execUpdate" :disabled="updateLoading">确 定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
<el-dialog title="拉取手机号" :visible.sync="mobileDialog" width="680px" @closed="closeMobilePolling">
|
||||
<el-alert title="后台逐人拉取,会员优先。关闭弹框不停止任务;无号码结果七天内跳过。" type="info" :closable="false"></el-alert>
|
||||
<el-form label-width="110px" style="margin-top: 16px" :disabled="mobileRunning || formLoading">
|
||||
<el-form-item label="人员范围">
|
||||
<el-select v-model="mobileScope" @change="invalidateMobilePreview">
|
||||
<el-option label="全部系统人员" value="ALL"></el-option>
|
||||
<el-option label="仅会员" value="MEMBER"></el-option>
|
||||
<el-option label="当前筛选的全部结果" value="FILTER"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="拉取模式">
|
||||
<el-radio-group v-model="mobileMode" @change="invalidateMobilePreview">
|
||||
<el-radio label="FILL">只补手机号为空的人员</el-radio>
|
||||
<el-radio label="REFRESH">重新核对(含已有号码)</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button :loading="mobilePreviewLoading" @click="previewMobileSync">预览本次人数</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<p v-if="mobilePreview">
|
||||
范围 {{mobilePreview.scopeCount}} 人,已有号码 {{mobilePreview.existingCount}} 人,
|
||||
冷却跳过 {{mobilePreview.cooldownCount}} 人,本次待请求 {{mobilePreview.total}} 人(按工号去重)。
|
||||
</p>
|
||||
<p v-if="mobilePreview && mobilePreview.total">按每秒最多 1 次请求执行,实际耗时还取决于接口响应速度。</p>
|
||||
<div v-if="mobileTask.taskId" v-loading="mobileStatusLoading && !mobileTask.status">
|
||||
<el-divider>最近任务</el-divider>
|
||||
<p>{{mobileStatusLabel}} · {{mobileTask.message}}</p>
|
||||
<p>任务范围:{{mobileTask.scope === 'MEMBER' ? '仅会员' : (mobileTask.scope === 'FILTER' ? '筛选结果' : '全部系统人员')}};
|
||||
模式:{{mobileTask.mode === 'REFRESH' ? '重新核对' : '只补空号'}}</p>
|
||||
<el-progress :percentage="mobileProgress"></el-progress>
|
||||
<p>已处理 {{mobileTask.processed || 0}} / {{mobileTask.total || 0}};
|
||||
已更新 {{mobileTask.updated || 0}};无号码 {{mobileTask.empty || 0}};
|
||||
失败 {{mobileTask.failed || 0}};跳过或未变更 {{mobileTask.skipped || 0}}。</p>
|
||||
<p v-if="!mobileRunning">任务与续传记录保留七天。继续任务会先处理剩余人员,再重试原失败项。</p>
|
||||
</div>
|
||||
<span slot="footer">
|
||||
<el-button @click="closeMobileSync">关闭</el-button>
|
||||
<el-button v-if="mobileRunning" type="warning" :loading="formLoading" @click="stopMobileSync">停止任务</el-button>
|
||||
<el-button v-if="!mobileRunning && mobileTask.taskId && mobileTask.processed < mobileTask.total" :loading="formLoading" @click="startMobileSync('RESUME')">继续任务</el-button>
|
||||
<el-button v-if="!mobileRunning && mobileTask.failed > 0" :loading="formLoading" @click="startMobileSync('RETRY')">仅重试失败项</el-button>
|
||||
<el-button type="primary" :loading="formLoading" :disabled="mobileRunning || mobileStatusLoading || mobilePreviewLoading || !mobilePreview || !mobilePreview.total" @click="startMobileSync('NEW')">开始拉取</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -139,6 +180,17 @@ layout("/layouts/platform.html"){
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
return {
|
||||
mobileDialog: false,
|
||||
mobileScope: "ALL",
|
||||
mobileMode: "FILL",
|
||||
mobileFilters: {},
|
||||
mobilePreview: null,
|
||||
mobilePreviewVersion: 0,
|
||||
mobilePreviewLoading: false,
|
||||
mobileStatusLoading: false,
|
||||
mobileTask: {},
|
||||
mobilePollTimer: null,
|
||||
mobileDisposed: false,
|
||||
updateDialog: false,
|
||||
pullTimeOptions: false,
|
||||
updateFromData: {
|
||||
@@ -176,7 +228,114 @@ layout("/layouts/platform.html"){
|
||||
components: {
|
||||
"member-all-change-info": MEMBER_ALL_CHANGE_INFO
|
||||
},
|
||||
computed: {
|
||||
mobileRunning() { return this.mobileTask.status === "RUNNING" },
|
||||
mobileProgress() {
|
||||
return this.mobileTask.total ? Math.min(100, Math.floor((this.mobileTask.processed || 0) * 100 / this.mobileTask.total)) : 0
|
||||
},
|
||||
mobileStatusLabel() {
|
||||
const labels = { RUNNING: "执行中", STOPPED: "已停止", PAUSED: "已暂停", INTERRUPTED: "已中断", COMPLETED: "已完成" }
|
||||
return labels[this.mobileTask.status] || "读取中"
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.mobileDisposed = true
|
||||
this.closeMobilePolling()
|
||||
},
|
||||
methods: {
|
||||
// 打开时固定本次列表筛选条件;只读取进度,外部手机号请求必须由开始按钮触发。
|
||||
openMobileSync() {
|
||||
this.mobileFilters = Object.assign({}, this.pageForm)
|
||||
this.mobileDialog = true
|
||||
this.invalidateMobilePreview()
|
||||
this.loadMobileStatus()
|
||||
},
|
||||
invalidateMobilePreview() {
|
||||
this.mobilePreviewVersion++
|
||||
this.mobilePreview = null
|
||||
},
|
||||
// scope为ALL/MEMBER/FILTER,mode为FILL/REFRESH;筛选参数只参与FILTER,不传分页含义。
|
||||
mobileParams() {
|
||||
return Object.assign({}, this.mobileFilters, { scope: this.mobileScope, mode: this.mobileMode })
|
||||
},
|
||||
previewMobileSync() {
|
||||
if (this.mobilePreviewLoading) return
|
||||
const version = ++this.mobilePreviewVersion
|
||||
this.mobilePreview = null
|
||||
this.mobilePreviewLoading = true
|
||||
this.$axios.post("/platform/sys/data/user/update/mobilePreview", this.mobileParams())
|
||||
.then((res) => {
|
||||
// 选项改变后不显示旧请求的预览,避免按旧人数开始新范围任务。
|
||||
if (res.code === 0 && version === this.mobilePreviewVersion) this.mobilePreview = res.data
|
||||
})
|
||||
.catch(() => { this.$message.error("预览失败,请稍后重试") })
|
||||
.finally(() => { this.mobilePreviewLoading = false })
|
||||
},
|
||||
// NEW使用预览范围;RESUME/RETRY由后端按sourceTaskId恢复原任务范围及模式,不能被当前选项替换。
|
||||
startMobileSync(action) {
|
||||
if (this.formLoading || this.mobileRunning) return
|
||||
const params = Object.assign(this.mobileParams(), { action: action, sourceTaskId: this.mobileTask.taskId || "" })
|
||||
const refresh = action === "NEW" ? this.mobileMode === "REFRESH" : this.mobileTask.mode === "REFRESH"
|
||||
const message = refresh ? "本次会核对并更新已有手机号,确定开始吗?" : "确定在后台拉取手机号吗?"
|
||||
this.$confirm(message, "提示", { type: "warning" }).then(() => {
|
||||
this.formLoading = true
|
||||
return this.$axios.post("/platform/sys/data/user/update/mobileStart", params)
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.mobileTask = res.data || {}
|
||||
this.invalidateMobilePreview()
|
||||
this.$message.success("任务已提交,可关闭弹框稍后查看")
|
||||
}
|
||||
})
|
||||
.catch(() => { this.$message.error("启动未成功,请查看最新任务状态") })
|
||||
.finally(() => {
|
||||
this.formLoading = false
|
||||
this.loadMobileStatus()
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
loadMobileStatus() {
|
||||
if (this.mobileStatusLoading || this.mobileDisposed || !this.mobileDialog) return
|
||||
this.closeMobilePolling()
|
||||
this.mobileStatusLoading = true
|
||||
this.$axios.post("/platform/sys/data/user/update/mobileStatus", {})
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
const wasRunning = this.mobileRunning
|
||||
this.mobileTask = res.data || {}
|
||||
if (wasRunning && !this.mobileRunning) this.pageData()
|
||||
}
|
||||
})
|
||||
.catch(() => { this.$message.error("读取任务进度失败,将稍后重试") })
|
||||
.finally(() => {
|
||||
this.mobileStatusLoading = false
|
||||
// 使用单次计时器串行轮询;弹框关闭或页面销毁后不再注册,避免重复绑定与泄漏。
|
||||
if (this.mobileDialog && !this.mobileDisposed) {
|
||||
this.mobilePollTimer = setTimeout(() => { this.loadMobileStatus() }, 3000)
|
||||
}
|
||||
})
|
||||
},
|
||||
stopMobileSync() {
|
||||
if (this.formLoading) return
|
||||
this.formLoading = true
|
||||
this.$axios.post("/platform/sys/data/user/update/mobileStop", { taskId: this.mobileTask.taskId })
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.mobileTask = res.data || {}
|
||||
this.$message.success("停止请求已提交,等待当前工号处理结束")
|
||||
}
|
||||
})
|
||||
.catch(() => { this.$message.error("停止请求失败,请稍后重试") })
|
||||
.finally(() => { this.formLoading = false })
|
||||
},
|
||||
closeMobilePolling() {
|
||||
if (this.mobilePollTimer) clearTimeout(this.mobilePollTimer)
|
||||
this.mobilePollTimer = null
|
||||
},
|
||||
closeMobileSync() {
|
||||
this.$set(this, "mobileDialog", false)
|
||||
this.closeMobilePolling()
|
||||
},
|
||||
// 更新单位数据
|
||||
updateUnits() {
|
||||
this.$confirm("确定要更新单位数据吗?", "提示", {
|
||||
|
||||
@@ -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}}
|
||||
|
||||
+25
-20
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,14 +20,6 @@ layout("/layouts/platform.html"){
|
||||
>
|
||||
备份历史会员
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN'])"
|
||||
type="primary"
|
||||
size="small"
|
||||
icon="el-icon-mobile-phone"
|
||||
:loading="syncMobileLoading"
|
||||
@click="syncMobile"
|
||||
>获取手机号</el-button>
|
||||
<el-button type="primary" size="small" icon="el-icon-download" @click="exportMember">会员档案导出</el-button>
|
||||
<el-popover
|
||||
placement="bottom"
|
||||
@@ -157,8 +149,7 @@ layout("/layouts/platform.html"){
|
||||
],
|
||||
checkedFields: [],
|
||||
|
||||
importDialog: false,
|
||||
syncMobileLoading: false
|
||||
importDialog: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
@@ -189,39 +180,6 @@ layout("/layouts/platform.html"){
|
||||
openArchive() {
|
||||
this.$refs.archiveMemberRef.archiveDialogVisible = true
|
||||
},
|
||||
syncMobile() {
|
||||
this.$confirm("将从手机号为空的会员中随机取500人获取手机号,确定继续吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
this.syncMobileLoading = true
|
||||
this.$axios.post(loc() + "/syncMobile").then((res) => {
|
||||
if (res.code === 0) {
|
||||
const data = res.data || {}
|
||||
const failedJobNos = data.failedJobNos || []
|
||||
this.$alert(
|
||||
"抽取人数:" + (data.sampleCount || 0) +
|
||||
"<br>请求人数:" + (data.requestUserCount || 0) +
|
||||
"<br>接口返回数量:" + (data.pulledCount || 0) +
|
||||
"<br>有效手机号数量:" + (data.validMobileCount || 0) +
|
||||
"<br>系统用户更新数量:" + (data.updatedUserCount || 0) +
|
||||
"<br>失败工号数量:" + failedJobNos.length,
|
||||
"手机号获取完成",
|
||||
{
|
||||
dangerouslyUseHTMLString: true,
|
||||
confirmButtonText: "确定"
|
||||
}
|
||||
)
|
||||
this.pageData()
|
||||
} else {
|
||||
this.$message.warning(res.msg || "手机号获取失败")
|
||||
}
|
||||
}).finally(() => {
|
||||
this.syncMobileLoading = false
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
openView(userId) {
|
||||
this.$refs.guava.view(() => {
|
||||
this.$refs.infoRef.onOpen(userId)
|
||||
|
||||
@@ -2,10 +2,50 @@
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<style>
|
||||
/* 扣除平台顶部导航 64px 和内容区上下间距 20px,让列表使用剩余可视高度。 */
|
||||
.thirty-teach-summary-page {
|
||||
height: calc(100vh - 84px);
|
||||
}
|
||||
|
||||
.thirty-teach-summary-page > .guava-main-content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.thirty-teach-summary-page > .guava-main-content > .transition-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.thirty-teach-summary-page .summary-search-card,
|
||||
.thirty-teach-summary-page .summary-list-card > .el-card__body > .ele-table-tool {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 列表卡片允许收缩,避免数据行撑高外层页面;表头和合计由 el-table 固定。 */
|
||||
.thirty-teach-summary-page .summary-list-card {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.thirty-teach-summary-page .summary-list-card > .el-card__body {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.thirty-teach-summary-page .summary-table-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" class="thirty-teach-summary-page" v-cloak>
|
||||
<guava ref="guava">
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<el-card shadow="never" class="summary-search-card">
|
||||
<search @search="doSearch">
|
||||
<search-item label="所属工会">
|
||||
<el-select v-model="pageForm.unionId" placeholder="请选择所属工会"
|
||||
@@ -29,29 +69,31 @@ layout("/layouts/platform.html"){
|
||||
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="mt10">
|
||||
<el-card shadow="never" class="mt10 summary-list-card">
|
||||
<table-tool label="教职工汇总">
|
||||
<el-button size="small" @click="doExportSummary" type="primary">
|
||||
导出汇总表
|
||||
</el-button>
|
||||
</table-tool>
|
||||
<el-table ref="table" :data="tableData" style="width: 100%" row-key="id"
|
||||
@sort-change="pageOrder" show-summary
|
||||
v-loading="tableLoading" :size="tableSize" class="vi-table">
|
||||
<el-table-column align="center" header-align="center" type="index"
|
||||
:index="indexMethod" label="序号"
|
||||
width="80px"></el-table-column>
|
||||
<el-table-column
|
||||
align="center"
|
||||
header-align="center"
|
||||
v-for="column in tableColumns"
|
||||
show-overflow-tooltip
|
||||
:label="column.label"
|
||||
:prop="column.prop"
|
||||
:sortable="column.sortable"
|
||||
>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="summary-table-container">
|
||||
<el-table ref="table" :data="tableData" style="width: 100%" row-key="id"
|
||||
@sort-change="pageOrder" show-summary height="100%"
|
||||
v-loading="tableLoading" :size="tableSize" class="vi-table">
|
||||
<el-table-column align="center" header-align="center" type="index"
|
||||
:index="indexMethod" label="序号"
|
||||
width="80px"></el-table-column>
|
||||
<el-table-column
|
||||
align="center"
|
||||
header-align="center"
|
||||
v-for="column in tableColumns"
|
||||
show-overflow-tooltip
|
||||
:label="column.label"
|
||||
:prop="column.prop"
|
||||
:sortable="column.sortable"
|
||||
>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
</el-card>
|
||||
</template>
|
||||
@@ -71,10 +113,10 @@ layout("/layouts/platform.html"){
|
||||
},
|
||||
tableColumns: [
|
||||
{prop: 'name', label: '分工会名称', sortable: true},
|
||||
{prop: 'userNum', label: '人数', sortable: true},
|
||||
{prop: 'willThirtyNum', label: '即将满30年', sortable: true},
|
||||
{prop: 'anniversaryThirtyNum', label: '周年满30年', sortable: true},
|
||||
{prop: 'overThirtyNum', label: '超过30年', sortable: true},
|
||||
{prop: 'userNum', label: '人数', sortable: true},
|
||||
],
|
||||
unionOptions: [],
|
||||
unitOptions: [],
|
||||
|
||||
@@ -2,10 +2,50 @@
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<style>
|
||||
/* 扣除平台顶部导航 64px 和内容区上下间距 20px,让列表使用剩余可视高度。 */
|
||||
.welfare-statistics-page {
|
||||
height: calc(100vh - 84px);
|
||||
}
|
||||
|
||||
.welfare-statistics-page > .guava-main-content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.welfare-statistics-page > .guava-main-content > .transition-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.welfare-statistics-page .statistics-search-card,
|
||||
.welfare-statistics-page .statistics-list-card > .el-card__body > .ele-table-tool {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 列表卡片允许收缩,避免数据行撑高外层页面;表头和合计由 el-table 固定。 */
|
||||
.welfare-statistics-page .statistics-list-card {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.welfare-statistics-page .statistics-list-card > .el-card__body {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.welfare-statistics-page .statistics-table-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" class="welfare-statistics-page" v-cloak>
|
||||
<guava ref="guava">
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<el-card class="statistics-search-card" shadow="never">
|
||||
<search @search="doSearch">
|
||||
<search-item label="年度">
|
||||
<el-date-picker
|
||||
@@ -31,7 +71,7 @@ layout("/layouts/platform.html"){
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt10" shadow="never" v-loading="tableLoading">
|
||||
<el-card class="mt10 statistics-list-card" shadow="never" v-loading="tableLoading">
|
||||
<table-tool label="工会列表">
|
||||
<el-button @click="exportByWelfareOptions" icon="el-icon-printer" size="small" type="primary">按福利选项导出</el-button>
|
||||
<template v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN','SCHOOL_UNION_WELFARE_ADMIN'])">
|
||||
@@ -40,38 +80,41 @@ layout("/layouts/platform.html"){
|
||||
</el-button>
|
||||
</template>
|
||||
</table-tool>
|
||||
<el-table
|
||||
:data="tableData"
|
||||
:size="tableSize"
|
||||
@sort-change="pageOrder"
|
||||
class="vi-table"
|
||||
lazy
|
||||
ref="table"
|
||||
row-key="id"
|
||||
show-summary
|
||||
style="width: 100%"
|
||||
v-if="tableData&&tableData.length>0"
|
||||
>
|
||||
<el-table-column type="index" width="50"></el-table-column>
|
||||
<el-table-column
|
||||
:key="column.prop"
|
||||
:label="column.label"
|
||||
:prop="column.prop"
|
||||
:sortable="column.sortable"
|
||||
:width="column.width"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
v-for="column in tableColumns"
|
||||
<div class="statistics-table-container">
|
||||
<el-table
|
||||
:data="tableData"
|
||||
:size="tableSize"
|
||||
@sort-change="pageOrder"
|
||||
class="vi-table"
|
||||
height="100%"
|
||||
lazy
|
||||
ref="table"
|
||||
row-key="id"
|
||||
show-summary
|
||||
style="width: 100%"
|
||||
v-if="tableData&&tableData.length>0"
|
||||
>
|
||||
<template scope="{row}" v-if="column.prop=='welfareUnitName'">{{row.welfareUnitName}}</template>
|
||||
<template scope="{row}" v-else-if="column.prop=='selectedNum'">
|
||||
<el-link @click="openSelected(row)" type="primary">{{row.selectedNum}}</el-link>
|
||||
</template>
|
||||
<template scope="{row}" v-else-if="column.prop=='unSelectedNum'">
|
||||
<el-link @click="openUnSelected(row)" type="primary">{{row.unSelectedNum}}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-table-column type="index" width="50"></el-table-column>
|
||||
<el-table-column
|
||||
:key="column.prop"
|
||||
:label="column.label"
|
||||
:prop="column.prop"
|
||||
:sortable="column.sortable"
|
||||
:width="column.width"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
v-for="column in tableColumns"
|
||||
>
|
||||
<template scope="{row}" v-if="column.prop=='welfareUnitName'">{{row.welfareUnitName}}</template>
|
||||
<template scope="{row}" v-else-if="column.prop=='selectedNum'">
|
||||
<el-link @click="openSelected(row)" type="primary">{{row.selectedNum}}</el-link>
|
||||
</template>
|
||||
<template scope="{row}" v-else-if="column.prop=='unSelectedNum'">
|
||||
<el-link @click="openUnSelected(row)" type="primary">{{row.unSelectedNum}}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
</guava>
|
||||
|
||||
Reference in New Issue
Block a user