Compare commits

...
15 Commits
70 changed files with 2831 additions and 567 deletions
@@ -73,6 +73,8 @@ public interface FlowConst {
String COUNTERSIGN_OPERATOR_LIST = "operatorList"; String COUNTERSIGN_OPERATOR_LIST = "operatorList";
// 会签类型 PARALLEL表示并行会签,SEQUENTIAL表示串行会签 // 会签类型 PARALLEL表示并行会签,SEQUENTIAL表示串行会签
String COUNTERSIGN_TYPE = "countersignType"; String COUNTERSIGN_TYPE = "countersignType";
// 分组会签任务的服务端快照,包含本轮批次、组ID、组成员及创建时的会签规则。
String COUNTERSIGN_GROUP = "wf_countersignGroup";
// 会签不同意标识 // 会签不同意标识
String COUNTERSIGN_DISAGREE_FLAG = "countersignDisagreeFlag"; String COUNTERSIGN_DISAGREE_FLAG = "countersignDisagreeFlag";
String ACTOR_IDS_KEY = "actorIds"; 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.base.vo.LabelValueVO;
import com.budwk.app.flow.constant.FlowConst; import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.engine.FlowEngine; 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.ProcessModel;
import com.budwk.app.flow.engine.model.TaskModel; import com.budwk.app.flow.engine.model.TaskModel;
import com.budwk.app.flow.entity.*; 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.enums.ProcessTaskStateEnum;
import com.budwk.app.flow.service.FlowCommonService; import com.budwk.app.flow.service.FlowCommonService;
import com.budwk.app.flow.vo.HighLightVO; import com.budwk.app.flow.vo.HighLightVO;
import com.budwk.app.flow.vo.ProcessInstanceVO; import com.budwk.app.flow.vo.ProcessInstanceVO;
import com.budwk.app.flow.vo.ProcessTaskVO; import com.budwk.app.flow.vo.ProcessTaskVO;
import com.budwk.app.todo.service.TodoService;
import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop; import org.nutz.ioc.aop.Aop;
@@ -53,8 +46,6 @@ public class FlowCommonController {
private Dao dao; private Dao dao;
@Inject @Inject
private FlowCommonService flowCommonService; private FlowCommonService flowCommonService;
@Inject
private TodoService todoService;
@At("/approval/form") @At("/approval/form")
@SaCheckLogin @SaCheckLogin
@@ -250,44 +241,17 @@ public class FlowCommonController {
return Result.success(); return Result.success();
} }
/**
* @param taskId 需要撤回的 WF 已办任务 ID,办理人由服务端登录上下文取得
* @return Result 成功响应;资格及流程状态校验交由 service,失败不修改任务
*/
@At @At
@SaCheckLogin @SaCheckLogin
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
@ApiOperation("撤销任务") @ApiOperation("撤销任务")
public Result revokeTask(@Param("taskId") Long taskId) { public Result revokeTask(@Param("taskId") Long taskId) {
// 自己任务 if (taskId == null) return Result.error("任务ID不能为空");
ProcessTask selfTask = dao.fetch(ProcessTask.class, taskId); return flowCommonService.revokeTask(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();
} }
@At("/candidate") @At("/candidate")
@@ -5,6 +5,8 @@ import com.budwk.app.flow.engine.core.Execution;
import com.budwk.app.flow.engine.model.TaskModel; import com.budwk.app.flow.engine.model.TaskModel;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Collections;
/** /**
* *
@@ -20,6 +22,17 @@ public interface AssignmentHandler {
* @return Object 参与者对象 * @return Object 参与者对象
*/ */
List<String> assign(TaskModel model, Execution execution); 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() { default String getMessage() {
return this.getClass().getSimpleName(); 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.ProcessDefineService;
import com.budwk.app.flow.service.ProcessInstanceService; import com.budwk.app.flow.service.ProcessInstanceService;
import com.budwk.app.flow.service.ProcessTaskService; import com.budwk.app.flow.service.ProcessTaskService;
import com.budwk.app.flow.service.CountersignGroupService;
import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.ioc.aop.Aop; import org.nutz.ioc.aop.Aop;
@@ -106,6 +107,9 @@ public class FlowEngineImpl implements FlowEngine {
@Override @Override
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public List<ProcessTask> executeProcessTask(Long processTaskId, String operator, Dict args) { 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); Execution execution = execute(processTaskId, operator, args);
if (execution == null) return Collections.emptyList(); if (execution == null) return Collections.emptyList();
ProcessModel processModel = execution.getProcessModel(); ProcessModel processModel = execution.getProcessModel();
@@ -119,8 +123,12 @@ public class FlowEngineImpl implements FlowEngine {
@Override @Override
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public List<ProcessTask> executeAndJumpTask(Long processTaskId, String operator, Dict args, String nodeName) { 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); Execution execution = execute(processTaskId, operator, args);
if (execution == null) return Collections.emptyList(); if (execution == null) return Collections.emptyList();
cancelGroupedBatch(execution);
ProcessModel model = execution.getProcessModel(); ProcessModel model = execution.getProcessModel();
if (StrUtil.isEmpty(nodeName)) { if (StrUtil.isEmpty(nodeName)) {
ProcessTask newTask = processTaskService.rejectTask(model, execution.getProcessTask()); ProcessTask newTask = processTaskService.rejectTask(model, execution.getProcessTask());
@@ -150,8 +158,12 @@ public class FlowEngineImpl implements FlowEngine {
@Override @Override
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public List<ProcessTask> executeAndJumpToEnd(Long processTaskId, String operator, Dict args) { 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); Execution execution = execute(processTaskId, operator, args);
if (execution == null) return Collections.emptyList(); if (execution == null) return Collections.emptyList();
cancelGroupedBatch(execution);
ProcessModel model = execution.getProcessModel(); ProcessModel model = execution.getProcessModel();
List<EndModel> endModelList = model.getModels(EndModel.class); List<EndModel> endModelList = model.getModels(EndModel.class);
endModelList.forEach(endModel -> { endModelList.forEach(endModel -> {
@@ -164,9 +176,14 @@ public class FlowEngineImpl implements FlowEngine {
} }
@Override @Override
@Aop(TransAop.READ_COMMITTED)
public List<ProcessTask> executeAndJumpToFirstTaskNode(Long processTaskId, String operator, Dict args) { 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); Execution execution = execute(processTaskId, operator, args);
if (execution == null) return Collections.emptyList(); if (execution == null) return Collections.emptyList();
cancelGroupedBatch(execution);
ProcessModel model = execution.getProcessModel(); ProcessModel model = execution.getProcessModel();
StartModel startModel = model.getStart(); StartModel startModel = model.getStart();
startModel.getOutputs().forEach(transitionModel -> { startModel.getOutputs().forEach(transitionModel -> {
@@ -192,6 +209,18 @@ public class FlowEngineImpl implements FlowEngine {
private Execution execute(Long processTaskId, String operator, Dict args) { private Execution execute(Long processTaskId, String operator, Dict args) {
// 1.1 根据id查询正在进行中的流程任务 // 1.1 根据id查询正在进行中的流程任务
ProcessTask processTask = processTaskService.getById(processTaskId); 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())) { if (processTask == null || !ProcessTaskStateEnum.DOING.getCode().equals(processTask.getTaskState())) {
throw new BaseException("没有进行中的流程任务"); throw new BaseException("没有进行中的流程任务");
} }
@@ -236,5 +265,19 @@ public class FlowEngineImpl implements FlowEngine {
return execution; 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 @Override
public void handle(Execution execution) { 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; boolean isMerged = false;
String countersignType = taskModel.getExt().get("countersignType", "PARALLEL"); String countersignType = taskModel.getExt().get("countersignType", "PARALLEL");
String countersignCompletionCondition = taskModel.getExt().get("countersignCompletionCondition", ""); String countersignCompletionCondition = taskModel.getExt().get("countersignCompletionCondition", "");
@@ -41,13 +41,17 @@ public class TaskModel extends NodeModel {
*/ */
private String countersignCompletionCondition; private String countersignCompletionCondition;
/** 会签办理维度:PERSON 按人员(兼容旧定义)、UNIT 所属单位、UNION 工会、UNDERTAKE 承办单位。 */
private String countersignGroupBy = "PERSON";
private String h5form; private String h5form;
@Override @Override
public void exec(Execution execution) { public void exec(Execution execution) {
// 执行任务节点自定义执行逻辑 // 执行任务节点自定义执行逻辑
System.out.println(super.toString()); 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); fire(new CountersignHandler(this), execution);
if (execution.isMerged()) { if (execution.isMerged()) {
@@ -36,6 +36,7 @@ public interface NodeParser {
String EXT_FIELD_CANDIDATE_GROUPS_KEY = "candidateGroups"; String EXT_FIELD_CANDIDATE_GROUPS_KEY = "candidateGroups";
String EXT_FIELD_CANDIDATE_HANDLER_KEY = "candidateHandler"; String EXT_FIELD_CANDIDATE_HANDLER_KEY = "candidateHandler";
String EXT_FIELD_COUNTERSIGN_TYPE_KEY = "countersignType"; // 会签类型 String EXT_FIELD_COUNTERSIGN_TYPE_KEY = "countersignType"; // 会签类型
String EXT_FIELD_COUNTERSIGN_GROUP_BY_KEY = "countersignGroupBy"; // 会签办理维度
String EXT_FIELD_COUNTERSIGN_COMPLETION_CONDITION_KEY = "countersignCompletionCondition"; // 会签完成条件 String EXT_FIELD_COUNTERSIGN_COMPLETION_CONDITION_KEY = "countersignCompletionCondition"; // 会签完成条件
String CLASS_KEY = "clazz"; // 类路径 String CLASS_KEY = "clazz"; // 类路径
String METHOD_NAME_KEY = "methodName"; // 方法名 String METHOD_NAME_KEY = "methodName"; // 方法名
@@ -62,6 +62,9 @@ public class TaskParser extends AbstractNodeParser {
taskModel.setExt(Dict.create()); taskModel.setExt(Dict.create());
} }
// 将其他properties添加到ext中 // 将其他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)->{ properties.forEach((k,v)->{
if(!ReflectUtil.hasField(TaskModel.class,k)){ if(!ReflectUtil.hasField(TaskModel.class,k)){
taskModel.getExt().set(k,v); 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.lang.Dict;
import cn.hutool.core.util.ObjectUtil; 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.base.result.Result;
import com.budwk.app.flow.constant.FlowConst; import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.engine.FlowEngine; 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.entity.ProcessTask;
import com.budwk.app.flow.enums.*; import com.budwk.app.flow.enums.*;
import com.budwk.app.web.commons.auth.utils.SecurityUtil; 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.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain; import org.nutz.dao.Chain;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
@@ -20,6 +23,7 @@ import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import java.util.List; import java.util.List;
import java.util.Objects;
@IocBean @IocBean
public class FlowCommonService { public class FlowCommonService {
@@ -28,11 +32,16 @@ public class FlowCommonService {
private FlowEngine flowEngine; private FlowEngine flowEngine;
@Inject @Inject
private Dao dao; private Dao dao;
@Inject
private CountersignGroupService countersignGroupService;
@Inject
private TodoService todoService;
/** /**
* 执行任务 * 执行任务
* @param args * @param args
*/ */
@Aop(TransAop.READ_COMMITTED)
public void executeTask(Dict args){ public void executeTask(Dict args){
Long processTaskId = args.getLong(FlowConst.PROCESS_TASK_ID_KEY); Long processTaskId = args.getLong(FlowConst.PROCESS_TASK_ID_KEY);
String operator = SecurityUtil.getUserId(); 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) @Aop(TransAop.READ_COMMITTED)
public Result revokeTask(Long taskId) { public Result revokeTask(Long taskId) {
// 自己任务 if (taskId == null) throw new BaseException("任务ID不能为空");
ProcessTask selfTask = dao.fetch(ProcessTask.class, taskId); 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.setTaskState(ProcessTaskStateEnum.DOING.getCode());
selfTask.setOperator(null);
selfTask.setFinishTime(null);
// 撤销任务 // 撤销任务
List<ProcessTask> taskList = dao.query(ProcessTask.class, Cnd.where(ProcessTask::getTaskParentId, "=", taskId)); List<ProcessTask> taskList = dao.query(ProcessTask.class, Cnd.where(ProcessTask::getTaskParentId, "=", taskId));
@@ -112,6 +180,8 @@ public class FlowCommonService {
// 发送任务撤回事件 确保上面执行成功 // 发送任务撤回事件 确保上面执行成功
for (ProcessTask task : taskList) { for (ProcessTask task : taskList) {
ProcessPublisher.notify(ProcessEvent.builder().eventType(ProcessEventTypeEnum.PROCESS_TASK_REVOKE).sourceId(task.getId()).build()); ProcessPublisher.notify(ProcessEvent.builder().eventType(ProcessEventTypeEnum.PROCESS_TASK_REVOKE).sourceId(task.getId()).build());
// 保留原 controller 撤回入口对外部待办的终止处理。
todoService.taskTerminate(task);
} }
return Result.success(); 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.ProcessDefineService;
import com.budwk.app.flow.service.ProcessInstanceService; import com.budwk.app.flow.service.ProcessInstanceService;
import com.budwk.app.flow.service.ProcessTaskService; 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.flow.vo.ProcessTaskVO;
import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.aop.interceptor.ioc.TransAop;
@@ -165,6 +166,8 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
@Override @Override
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public List<ProcessTask> createTask(TaskModel taskModel, Execution execution) { public List<ProcessTask> createTask(TaskModel taskModel, Execution execution) {
// 分组快照只属于其创建的会签任务,不传递给下一节点。
execution.getArgs().remove(FlowConst.COUNTERSIGN_GROUP);
List<ProcessTask> processTaskList = new ArrayList<>(); List<ProcessTask> processTaskList = new ArrayList<>();
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
ProcessTask processTask = new ProcessTask(); ProcessTask processTask = new ProcessTask();
@@ -236,6 +239,10 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
public void addCandidateActor(Long processTaskId, List<String> actors) { public void addCandidateActor(Long processTaskId, List<String> actors) {
if (CollectionUtil.isEmpty(actors)) return; if (CollectionUtil.isEmpty(actors)) return;
ProcessTask processTask = fetch(processTaskId); 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() + "_"; String prefix = FlowConst.COUNTERSIGN_VARIABLE_PREFIX + processTask.getTaskName() + "_";
// 主要调整流程变量中的参与者 // 主要调整流程变量中的参与者
ProcessInstance processInstance = dao().fetch(ProcessInstance.class, processTask.getProcessInstanceId()); 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) { public List<ProcessTask> createCountersignTask(TaskModel taskModel, Execution execution) {
List<ProcessTask> processTaskList = new ArrayList<>(); List<ProcessTask> processTaskList = new ArrayList<>();
List<String> taskActors = getTaskActors(taskModel, execution); 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<>(); List<String> createTaskActors = new ArrayList<>();
if (CountersignTypeEnum.PARALLEL.equals(taskModel.getCountersignType())) { if (CountersignTypeEnum.PARALLEL.equals(taskModel.getCountersignType())) {
// 并行:一个参与者一个任务,同时创建 // 并行:一个参与者一个任务,同时创建
@@ -485,6 +501,14 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl<ProcessTask> impleme
public void transfer(Long taskId, String transferUserId) { public void transfer(Long taskId, String transferUserId) {
// 设置原任务状态为已转办 // 设置原任务状态为已转办
ProcessTask task = fetch(taskId); 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()); task.setTaskState(ProcessTaskStateEnum.TRANSFER.getCode());
Dict variable = FlowUtil.variableToDict(task.getVariable()); Dict variable = FlowUtil.variableToDict(task.getVariable());
variable.put(FlowConst.TASK_FORM_DATA_PREFIX + "transferUserId", transferUserId); 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)); 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); List<ProcessTask> processTaskList = List.of(task);
processTaskList.forEach(processTask -> { processTaskList.forEach(processTask -> {
@@ -69,6 +69,17 @@ public class SysDataUserPullController {
return Result.success(); 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 @At
@SaCheckPermission("sys.data.user.pull") @SaCheckPermission("sys.data.user.pull")
@ApiOperation("删除用户数据") @ApiOperation("删除用户数据")
@@ -10,28 +10,24 @@ import cn.hutool.core.lang.tree.TreeUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog; import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.constant.RoleConstant; 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.page.Pagination;
import com.budwk.app.base.param.PageForm; import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result; 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_unit;
import com.budwk.app.sys.models.Sys_user; 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.SysUnitService;
import com.budwk.app.sys.services.SysUserService; import com.budwk.app.sys.services.SysUserService;
import com.budwk.app.sys.views.View_user; 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.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls; import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql; import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup; import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.loader.annotation.Inject; 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.ioc.loader.annotation.IocBean;
import org.nutz.lang.Lang; import org.nutz.lang.Lang;
import org.nutz.lang.Strings; import org.nutz.lang.Strings;
@@ -59,10 +55,6 @@ public class SysUnitController {
private SysUnitService sysUnitService; private SysUnitService sysUnitService;
@Inject @Inject
private SysUserService sysUserService; private SysUserService sysUserService;
@Inject
private SysDictService sysDictService;
@Inject
private SysRoleService sysRoleService;
@At("") @At("")
@Ok("beetl:/platform/sys/unit/index.html") @Ok("beetl:/platform/sys/unit/index.html")
@@ -107,41 +99,23 @@ public class SysUnitController {
return Result.success(sysUnitService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql)); return Result.success(sysUnitService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql));
} }
/** 无入参;返回 Result,data 为角色选项列表,每项 code 为编码、name 为名称。 */
@At
@Ok("json")
@SaCheckPermission("sys.manager.unit")
public Result unitRoleOptions() {
return Result.success(sysUnitService.getUnitRoleOptions());
}
/** pageForm 为搜索和分页参数,unitId 为系统单位IDResult.data 包含 list 人员角色列表、totalCount 总数。 */
@At("/leaderPageData") @At("/leaderPageData")
@Ok("json") @Ok("json")
@SaCheckLogin @SaCheckLogin
public Object leaderPageData(PageForm pageForm, String unitId) { public Object leaderPageData(PageForm pageForm, String unitId) {
List<Sys_dict> unitRoles = sysDictService.getSubListByCode("UNIT_ROLES"); if (StrUtil.isBlank(unitId)) {
if (Lang.isEmpty(unitRoles)) { return Result.error("请选择单位");
return Result.success();
} }
List<String> unitRoleCodes = unitRoles.stream().map(Sys_dict::getCode).toList(); return Result.success(sysUnitService.leaderPageData(pageForm, unitId));
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);
} }
@At @At
@@ -164,30 +138,29 @@ public class SysUnitController {
return Result.success(list); return Result.success(list);
} }
/** userId 为人员IDroleCode 为角色编码,unitId 为系统单位ID;返回 Resultcode=0 表示添加成功。 */
@At @At
@Ok("json") @Ok("json")
@SaCheckPermission("sys.manager.unit")
@Aop(TransAop.READ_COMMITTED)
public Result insertUnitUserRole(String userId, String roleCode, String unitId) { public Result insertUnitUserRole(String userId, String roleCode, String unitId) {
Sys_role role = sysRoleService.fetch(Cnd.where("code", "=", roleCode)); if (!StrUtil.isAllNotBlank(userId, roleCode, unitId)) {
if (Lang.isEmpty(role)) { return Result.error("请选择单位、角色和人员");
throw new BaseException("无法找到{}对应编码的角色", roleCode);
} }
sysRoleService.clear("sys_user_role", Cnd.where("roleId", "=", role.getId()).and("userId", "=", userId).and("unitId", "=", unitId)); sysUnitService.insertUnitUserRole(userId, roleCode, unitId);
sysRoleService.insert("sys_user_role", Chain.make("roleId", role.getId()).add("userId", userId).add("unitId", unitId));
sysRoleService.clearCache();
sysUserService.clearCache();
return Result.success(); return Result.success();
} }
/** userId 为人员IDroleCode 为角色编码,unitId 为系统单位ID;返回 Resultcode=0 表示删除成功。 */
@At @At
@Ok("json") @Ok("json")
@SaCheckPermission("sys.manager.unit")
@Aop(TransAop.READ_COMMITTED)
public Result deleteUnitUserRole(String userId, String roleCode, String unitId) { public Result deleteUnitUserRole(String userId, String roleCode, String unitId) {
Sys_role role = sysRoleService.fetch(Cnd.where("code", "=", roleCode)); if (!StrUtil.isAllNotBlank(userId, roleCode, unitId)) {
if (Lang.isEmpty(role)) { return Result.error("请选择单位、角色和人员");
throw new BaseException("无法找到{}对应编码的角色", roleCode);
} }
sysRoleService.clear("sys_user_role", Cnd.where("roleId", "=", role.getId()).and("userId", "=", userId).and("unitId", "=", unitId)); sysUnitService.deleteUnitUserRole(userId, roleCode, unitId);
sysRoleService.clearCache();
sysUserService.clearCache();
return Result.success(); return Result.success();
} }
@@ -391,4 +391,12 @@ public class Sys_user extends BaseModel implements Serializable {
@Comment("openId") @Comment("openId")
@ColDefine(type = ColType.VARCHAR, width = 50) @ColDefine(type = ColType.VARCHAR, width = 50)
private String openId; private String openId;
@Column
@Comment("编制信息")
@ColDefine(type = ColType.VARCHAR, width = 64)
@DataCenterColumn(name = "编制信息", key = "bzxx")
private String compilationInformation;
} }
@@ -38,4 +38,19 @@ public interface SysDataUserPullService extends BaseService<Sys_user_source> {
* @return * @return
*/ */
Map<String, NutMap> pullFinance(); Map<String, NutMap> pullFinance();
/**
* 从数据中心分页拉取教职工手机号,并按工号同步到 sys_user.mobile。
*
* @return 同步过程统计信息
*/
NutMap syncTeacherMobile();
/**
* 根据工号逐个调用数据中心手机号接口,并按工号同步到 sys_user.mobile。
*
* @param jobNos 工号集合
* @return 同步过程统计信息,包含工号手机号映射
*/
NutMap syncTeacherMobileByJobNos(List<String> jobNos);
} }
@@ -2,11 +2,27 @@ package com.budwk.app.sys.services;
import com.budwk.app.base.service.BaseService; import com.budwk.app.base.service.BaseService;
import com.budwk.app.sys.models.Sys_unit; 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. * Created by wizzer on 2016/12/22.
*/ */
public interface SysUnitService extends BaseService<Sys_unit> { public interface SysUnitService extends BaseService<Sys_unit> {
/** 无参数;返回单位干部角色选项,code 为角色编码,name 为显示名称,包含提案承办单位领导。 */
List<Sys_dict> getUnitRoleOptions();
/** pageForm 为姓名/工号搜索和分页参数,unitId 为系统单位ID;返回 list 和 totalCount 等分页数据。 */
Pagination leaderPageData(PageForm pageForm, String unitId);
/** userId 为人员IDroleCode 为角色编码,unitId 为系统单位ID;新增对应负责人关系,返回 void。 */
void insertUnitUserRole(String userId, String roleCode, String unitId);
/** userId 为人员IDroleCode 为角色编码,unitId 为系统单位ID;仅删除该单位对应角色关系,返回 void。 */
void deleteUnitUserRole(String userId, String roleCode, String unitId);
/** /**
* 保存单位 * 保存单位
* *
@@ -26,8 +26,10 @@ import com.budwk.app.sys.services.SysDataUnitPullService;
import com.budwk.app.sys.services.SysDataUserPullService; import com.budwk.app.sys.services.SysDataUserPullService;
import com.budwk.app.sys.services.SysDictService; import com.budwk.app.sys.services.SysDictService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.nutz.aop.interceptor.async.Async; import org.nutz.aop.interceptor.async.Async;
import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.dao.Sqls; import org.nutz.dao.Sqls;
@@ -47,6 +49,10 @@ import java.util.stream.Collectors;
@Slf4j @Slf4j
public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source> implements SysDataUserPullService { public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source> implements SysDataUserPullService {
private static final String TEACHER_MOBILE_CONFIG_KEY = "teacherMobile";
private static final int TEACHER_MOBILE_PAGE_SIZE = 10;
private static final int TEACHER_MOBILE_DB_BATCH_SIZE = 500;
@Inject @Inject
private SysDictService sysDictService; private SysDictService sysDictService;
@Inject @Inject
@@ -223,6 +229,8 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
// 插入到数据库 // 插入到数据库
dao().insert(latestSourceList); dao().insert(latestSourceList);
updateDictCompilationInformation(latestSourceList);
// 人员的单位数据和数据库的单位数据比较,如果人员里面有单位不存在,去更新单位数据 // 人员的单位数据和数据库的单位数据比较,如果人员里面有单位不存在,去更新单位数据
List<String> sourceUnitIds = latestSourceList.stream().map(Sys_user_source::getUnitId).distinct().toList(); List<String> sourceUnitIds = latestSourceList.stream().map(Sys_user_source::getUnitId).distinct().toList();
Sql sql = Sqls.create("select id from sys_unit group by id"); Sql sql = Sqls.create("select id from sys_unit group by id");
@@ -240,6 +248,36 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
} }
} }
/**
* 更新编制信息
* @param userSources
*/
private void updateDictCompilationInformation(List<Sys_user_source> userSources){
if(CollUtil.isNotEmpty(userSources)){
List<String> compilationInformationList = userSources.stream()
.map(Sys_user_source::getCompilationInformation)
.filter(StringUtils::isNotBlank)
.distinct()
.collect(Collectors.toList());
//系统在职状态字典
List<Sys_dict> sysUserStates = sysDictService.getSubListByCode("USER_COMPILATION_INFORMATION");
//系统中不存在的就插入
List<Sys_dict> insertUserStates = compilationInformationList.stream()
.filter(compilationInformation -> sysUserStates.stream().noneMatch(dict -> dict.getCode().equals(compilationInformation)))
.map(compilationInformation -> {
Sys_dict dict = new Sys_dict();
dict.setCode(compilationInformation);
dict.setName(compilationInformation);
return dict;
}).toList();
if(CollUtil.isNotEmpty(insertUserStates)){
for (Sys_dict dict : insertUserStates) {
sysDictService.saveByParentCode(dict, "USER_COMPILATION_INFORMATION");
}
}
}
}
@Async @Async
private void updateDict(List<Sys_user_source> userSources) { private void updateDict(List<Sys_user_source> userSources) {
//判断是否要更新在职状态字典 //判断是否要更新在职状态字典
@@ -400,6 +438,186 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
return listMap(sql); return listMap(sql);
} }
/**
* 从数据中心分页同步教职工手机号,并按工号更新 sys_user.mobile。
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public NutMap syncTeacherMobile() {
Sql sql = Sqls.create("SELECT loginname FROM sys_user WHERE loginname IS NOT NULL AND loginname <> ''");
sql.setCallback(Sqls.callback.strList());
dao().execute(sql);
List<String> jobNos = sql.getList(String.class);
return syncTeacherMobileByJobNos(jobNos);
}
/**
* 根据传入工号逐个调用手机号接口,接口分页参数保持原有 pageNum/pageSize,仅替换 gh 查询条件。
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public NutMap syncTeacherMobileByJobNos(List<String> jobNos) {
List<String> distinctJobNos = jobNos == null ? Collections.emptyList() : jobNos.stream()
.map(StrUtil::trimToEmpty)
.filter(StrUtil::isNotBlank)
.distinct()
.toList();
if (distinctJobNos.isEmpty()) {
return NutMap.NEW()
.addv("total", 0)
.addv("pages", 0)
.addv("requestUserCount", 0)
.addv("pulledCount", 0)
.addv("validMobileCount", 0)
.addv("matchedUserCount", 0)
.addv("updatedUserCount", 0)
.addv("mobileMap", Collections.emptyMap())
.addv("failedJobNos", Collections.emptyList());
}
DataCenterProperties.Credential credential = dataCenterProperties.credential(TEACHER_MOBILE_CONFIG_KEY);
String url = credential.getUrl();
String token = credential.getToken();
log.info("教职工手机号同步开始:url={}, tokenReady={}, requestUserCount={}", url, StrUtil.isNotBlank(token), distinctJobNos.size());
List<JSONObject> rawDataList = new ArrayList<>();
int pageSize = TEACHER_MOBILE_PAGE_SIZE;
List<String> failedJobNos = new ArrayList<>();
for (String jobNo : distinctJobNos) {
try {
rawDataList.addAll(requestTeacherMobileByJobNo(url, token, jobNo, pageSize));
} catch (Exception e) {
failedJobNos.add(jobNo);
log.warn("教职工手机号单人工号拉取失败:jobNo={}, msg={}", jobNo, e.getMessage());
}
}
Map<String, String> mobileMap = collectTeacherMobileMap(rawDataList);
NutMap updateResult = updateTeacherMobile(mobileMap);
NutMap result = NutMap.NEW()
.addv("total", distinctJobNos.size())
.addv("pages", distinctJobNos.size())
.addv("requestUserCount", distinctJobNos.size())
.addv("pulledCount", rawDataList.size())
.addv("validMobileCount", mobileMap.size())
.addv("matchedUserCount", updateResult.getInt("matchedUserCount", 0))
.addv("updatedUserCount", updateResult.getInt("updatedUserCount", 0))
.addv("mobileMap", mobileMap)
.addv("failedJobNos", failedJobNos);
log.info("教职工手机号同步完成:{}", result);
return result;
}
/**
* 按单个工号请求手机号接口,保留 pageNum/pageSize 分页参数,避免调用第三方全量分页。
*/
private List<JSONObject> requestTeacherMobileByJobNo(String url, String token, String jobNo, int pageSize) {
List<JSONObject> rawDataList = new ArrayList<>();
int pageNum = 1;
int pages = 1;
int total = 0;
do {
Map<String, Object> reqBody = buildTeacherMobileRequestBody(jobNo, pageNum, pageSize);
log.info("教职工手机号接口请求:jobNo={}, pageNum={}, pageSize={}, body={}", jobNo, pageNum, pageSize, reqBody);
HttpRequest httpRequest = HttpUtil.createPost(url);
httpRequest.header(Header.CONTENT_TYPE, "application/json");
httpRequest.header("X-H3C-TOKEN", token);
httpRequest.body(JSONUtil.toJsonStr(reqBody));
String resBody = httpRequest.execute().body();
log.info("教职工手机号接口返回:jobNo={}, pageNum={}, response={}", jobNo, pageNum, resBody);
JSONObject resp = JSONUtil.parseObj(resBody);
if (!"0".equals(resp.getStr("code"))) {
throw new BaseException("获取教职工手机号失败,错误码: " + resp.getStr("code") + ";错误原因:" + resp.getStr("msg"));
}
JSONObject data = resp.getJSONObject("data");
if (data != null) {
total = data.getInt("total", total);
pages = data.getInt("pages", pages);
if (pages <= 0 && total > 0) {
pages = (int) Math.ceil((double) total / pageSize);
}
}
JSONArray records = readTeacherMobileRecords(resp, data);
if (CollUtil.isNotEmpty(records)) {
rawDataList.addAll(records.stream().map(v -> (JSONObject) v).toList());
}
log.info("教职工手机号单人工号拉取进度:jobNo={}, pageNum={}, pages={}, total={}, pulled={}",
jobNo, pageNum, pages, total, rawDataList.size());
pageNum++;
} while (pageNum <= Math.max(pages, 1));
return rawDataList;
}
/**
* 组装手机号接口分页请求体,uid 和 sjh 保持空值,gh 使用当前待查询工号。
*/
private Map<String, Object> buildTeacherMobileRequestBody(String jobNo, int pageNum, int pageSize) {
Map<String, Object> reqBody = new LinkedHashMap<>();
reqBody.put("uid", null);
reqBody.put("gh", jobNo);
reqBody.put("sjh", null);
reqBody.put("pageNum", pageNum);
reqBody.put("pageSize", pageSize);
return reqBody;
}
/**
* 读取手机号接口明细数据,优先按文档读取顶层 records,同时兼容 data.records。
*/
private JSONArray readTeacherMobileRecords(JSONObject resp, JSONObject data) {
JSONArray records = resp.getJSONArray("records");
if (records == null && data != null) {
records = data.getJSONArray("records");
}
return records == null ? new JSONArray() : records;
}
/**
* 将接口记录转换为工号到手机号的映射,过滤缺少工号或手机号的无效数据。
*/
private Map<String, String> collectTeacherMobileMap(List<JSONObject> rawDataList) {
Map<String, String> mobileMap = new LinkedHashMap<>();
for (JSONObject rawData : rawDataList) {
String loginname = StrUtil.trimToEmpty(rawData.getStr("gh"));
String mobile = StrUtil.trimToEmpty(rawData.getStr("sjh"));
if (StrUtil.isAllNotBlank(loginname, mobile)) {
mobileMap.put(loginname, mobile);
}
}
log.info("教职工手机号有效数据整理完成:rawCount={}, validCount={}", rawDataList.size(), mobileMap.size());
return mobileMap;
}
/**
* 分批匹配 sys_user.loginname 并更新手机号,避免一次性 IN 条件过长。
*/
private NutMap updateTeacherMobile(Map<String, String> mobileMap) {
if (mobileMap == null || mobileMap.isEmpty()) {
return NutMap.NEW().addv("matchedUserCount", 0).addv("updatedUserCount", 0);
}
int matchedUserCount = 0;
int updatedUserCount = 0;
List<String> loginNames = new ArrayList<>(mobileMap.keySet());
for (int start = 0; start < loginNames.size(); start += TEACHER_MOBILE_DB_BATCH_SIZE) {
int end = Math.min(start + TEACHER_MOBILE_DB_BATCH_SIZE, loginNames.size());
List<String> batchLoginNames = loginNames.subList(start, end);
List<Sys_user> users = dao().query(Sys_user.class, Cnd.where(Sys_user::getLoginname, "in", batchLoginNames));
matchedUserCount += users.size();
for (Sys_user user : users) {
String newMobile = mobileMap.get(user.getLoginname());
if (StrUtil.isNotBlank(newMobile) && !Objects.equals(newMobile, user.getMobile())) {
updatedUserCount += dao().update(Sys_user.class, Chain.make("mobile", newMobile), Cnd.where(Sys_user::getId, "=", user.getId()));
}
}
log.info("教职工手机号批次更新完成:batchStart={}, batchEnd={}, batchMatched={}, updatedTotal={}",
start, end, users.size(), updatedUserCount);
}
return NutMap.NEW().addv("matchedUserCount", matchedUserCount).addv("updatedUserCount", updatedUserCount);
}
@Override @Override
public Map<String, NutMap> pullFinance() { public Map<String, NutMap> pullFinance() {
@@ -1,6 +1,21 @@
package com.budwk.app.sys.services.impl; package com.budwk.app.sys.services.impl;
import cn.hutool.core.util.StrUtil; 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.constant.RedisConstant;
import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_unit; import com.budwk.app.sys.models.Sys_unit;
@@ -26,6 +41,129 @@ public class SysUnitServiceImpl extends BaseServiceImpl<Sys_unit> implements Sys
super(dao); 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();
}
/** /**
* 新增单位 * 新增单位
* *
@@ -25,6 +25,7 @@ import com.budwk.app.base.exception.BaseException;
import io.minio.*; import io.minio.*;
import io.minio.http.Method; import io.minio.http.Method;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.nutz.boot.AppContext;
import org.nutz.ioc.Ioc; import org.nutz.ioc.Ioc;
import org.nutz.ioc.impl.PropertiesProxy; import org.nutz.ioc.impl.PropertiesProxy;
import org.nutz.mvc.Mvcs; import org.nutz.mvc.Mvcs;
@@ -49,7 +50,7 @@ public class SysFileMinIoUtil {
* 初始化操作的客户端 * 初始化操作的客户端
*/ */
private static void initClient() { private static void initClient() {
Ioc ioc = Mvcs.getIoc(); Ioc ioc = AppContext.getDefault().getIoc();
PropertiesProxy propertiesProxy = ioc.get(PropertiesProxy.class, "conf"); PropertiesProxy propertiesProxy = ioc.get(PropertiesProxy.class, "conf");
String accessKey = propertiesProxy.get("minio.accessKey"); String accessKey = propertiesProxy.get("minio.accessKey");
@@ -96,6 +96,9 @@ public class View_user {
@Column @Column
private String preparedBy; private String preparedBy;
@Column
private String compilationInformation;
@Column @Column
private String identityType; private String identityType;
@@ -19,6 +19,7 @@ import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
import com.budwk.app.sys.enums.SysFileEngineTypeEnum; import com.budwk.app.sys.enums.SysFileEngineTypeEnum;
import com.budwk.app.sys.models.Sys_file; import com.budwk.app.sys.models.Sys_file;
import com.budwk.app.sys.models.Sys_union; import com.budwk.app.sys.models.Sys_union;
import com.budwk.app.sys.services.SysDataUserPullService;
import com.budwk.app.sys.services.SysFileService; import com.budwk.app.sys.services.SysFileService;
import com.budwk.app.sys.utils.SysFileMinIoUtil; import com.budwk.app.sys.utils.SysFileMinIoUtil;
import com.budwk.app.sys.views.View_user; import com.budwk.app.sys.views.View_user;
@@ -91,8 +92,16 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
@@ -103,6 +112,11 @@ import java.util.zip.ZipOutputStream;
@Slf4j @Slf4j
public class ThirtyTeachTourLedgerController { public class ThirtyTeachTourLedgerController {
private static final int LEDGER_EXPORT_IMAGE_THREAD_COUNT = 3;
private static final int LEDGER_EXPORT_FILE_QUERY_BATCH_SIZE = 500;
private static final int LEDGER_EXPORT_IMAGE_TASK_TIMEOUT_SECONDS = 300;
private static final String LEDGER_EXPORT_ROW_IMAGES_KEY = "__ledgerExportImages";
@Inject @Inject
private ThirtyTeachTourLedgerService tourLedgerService; private ThirtyTeachTourLedgerService tourLedgerService;
@@ -118,6 +132,9 @@ public class ThirtyTeachTourLedgerController {
@Inject @Inject
private SysFileService sysFileService; private SysFileService sysFileService;
@Inject
private SysDataUserPullService sysUserPullService;
@At("") @At("")
@Ok("beetl:/platform/zhgh/dayofficework/ThirtyTeachTour/ledger/index.html") @Ok("beetl:/platform/zhgh/dayofficework/ThirtyTeachTour/ledger/index.html")
@SaCheckPermission("thirtyTeachTour.ledger") @SaCheckPermission("thirtyTeachTour.ledger")
@@ -284,6 +301,111 @@ public class ThirtyTeachTourLedgerController {
return Result.success(); return Result.success();
} }
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("thirtyTeachTour.ledger")
@SLog(type = "tour", tag = "疗休养台账", msg = "获取台账人员手机号")
public Result syncMobile(@Param("ids") String ids, Integer startYear, Integer endYear, String keyword, String unionId, String lineId,
String travelPeriod, String lineType, Boolean directFamilyOnly, Boolean overCostOnly) {
try {
List<String> idList = StrUtil.isBlank(ids) ? Collections.emptyList() : Json.fromJsonAsList(String.class, ids);
List<NutMap> ledgers = queryLedgerMobileTargets(idList, startYear, endYear, keyword, unionId, lineId, travelPeriod,
lineType, directFamilyOnly, overCostOnly);
List<String> jobNos = ledgers.stream()
.map(row -> row.getString("jobNo", ""))
.filter(StrUtil::isNotBlank)
.distinct()
.toList();
if (jobNos.isEmpty()) {
return Result.error("当前台账范围内没有可获取手机号的人员");
}
NutMap syncResult = sysUserPullService.syncTeacherMobileByJobNos(jobNos);
int ledgerUpdatedCount = updateLedgerMobileBySyncResult(ledgers, syncResult);
syncResult.put("ledgerCount", ledgers.size());
syncResult.put("ledgerUpdatedCount", ledgerUpdatedCount);
return Result.success(syncResult);
} catch (Exception e) {
return Result.error(e.getMessage());
}
}
/**
* 查询本次需要获取手机号的台账人员;勾选时按台账ID取数,未勾选时复用页面当前筛选条件取数。
*/
private List<NutMap> queryLedgerMobileTargets(List<String> idList, Integer startYear, Integer endYear, String keyword, String unionId,
String lineId, String travelPeriod, String lineType, Boolean directFamilyOnly,
Boolean overCostOnly) {
Cnd cnd;
if (idList != null && !idList.isEmpty()) {
cnd = Cnd.NEW();
cnd.and("t.delFlag", "=", false);
cnd.and("t.id", "in", idList);
cnd.andEX("t.unionId", "=", resolveLedgerUnionId(null));
appendApprovedWorkflowFilter(cnd);
} else {
cnd = buildQueryCnd(startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType, directFamilyOnly, overCostOnly);
}
Sql sql = Sqls.create("""
SELECT
t.id,
t.jobNo,
t.mobile
FROM thirty_teach_tour_ledger t
LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id
LEFT JOIN thirty_teach_tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN thirty_teach_tour_line l ON l.id = t.lineId AND l.delFlag = 0
LEFT JOIN thirty_teach_tour_ledger_direct_relative dr ON dr.ledgerId = t.id AND dr.delFlag = 0
LEFT JOIN vw_user vu ON vu.loginname = t.jobNo
LEFT JOIN (
SELECT
`year`,
lineId,
GROUP_CONCAT(
DISTINCT CASE
WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> ''
THEN CONCAT(travelStartTime, ' 至 ', travelEndTime)
ELSE NULL
END
ORDER BY travelStartTime ASC
SEPARATOR ''
) AS travelPeriod
FROM thirty_teach_tour_matter
WHERE delFlag = 0
AND lineId IS NOT NULL
AND lineId <> ''
GROUP BY `year`, lineId
) tp ON (t.matterId IS NULL OR t.matterId = '') AND tp.`year` = t.`year` AND tp.lineId = t.lineId
$condition
GROUP BY t.id
""");
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.maps());
tourLedgerService.dao().execute(sql);
return sql.getList(NutMap.class);
}
/**
* 将手机号同步结果回写到当前台账范围内,保证台账列表刷新后展示最新手机号。
*/
private int updateLedgerMobileBySyncResult(List<NutMap> ledgers, NutMap syncResult) {
if (ledgers == null || ledgers.isEmpty() || syncResult == null || !(syncResult.get("mobileMap") instanceof Map)) {
return 0;
}
Map<?, ?> mobileMap = (Map<?, ?>) syncResult.get("mobileMap");
int updatedCount = 0;
for (NutMap ledger : ledgers) {
String jobNo = ledger.getString("jobNo", "");
Object mobileValue = mobileMap.get(jobNo);
String newMobile = mobileValue == null ? "" : String.valueOf(mobileValue);
if (StrUtil.isBlank(newMobile) || StrUtil.equals(newMobile, ledger.getString("mobile", ""))) {
continue;
}
updatedCount += tourLedgerService.dao().update(ThirtyTeachTourLedger.class,
Chain.make("mobile", newMobile), Cnd.where("id", "=", ledger.getString("id", "")));
}
return updatedCount;
}
@At @At
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("thirtyTeachTour.ledger") @SaCheckPermission("thirtyTeachTour.ledger")
@@ -379,6 +501,7 @@ public class ThirtyTeachTourLedgerController {
entities.add(new ExcelExportEntity("年龄", "age", 10)); entities.add(new ExcelExportEntity("年龄", "age", 10));
entities.add(new ExcelExportEntity("身份证号", "idCard", 24)); entities.add(new ExcelExportEntity("身份证号", "idCard", 24));
entities.add(new ExcelExportEntity("手机号", "mobile", 18)); entities.add(new ExcelExportEntity("手机号", "mobile", 18));
entities.add(new ExcelExportEntity("所属工会", "unionName", 18));
entities.add(new ExcelExportEntity("所在单位", "unitName", 26)); entities.add(new ExcelExportEntity("所在单位", "unitName", 26));
entities.add(new ExcelExportEntity("乘车地点", "boardingPlace", 18)); entities.add(new ExcelExportEntity("乘车地点", "boardingPlace", 18));
entities.add(new ExcelExportEntity("报名线路", "lineName", 30)); entities.add(new ExcelExportEntity("报名线路", "lineName", 30));
@@ -405,12 +528,13 @@ public class ThirtyTeachTourLedgerController {
ExcelExportEntity imageEntity = new ExcelExportEntity("当前时间段图片" + i, "photoCell" + (maxPhotoCount + i), 18); ExcelExportEntity imageEntity = new ExcelExportEntity("当前时间段图片" + i, "photoCell" + (maxPhotoCount + i), 18);
entities.add(imageEntity); entities.add(imageEntity);
} }
Map<String, LedgerExportImage> imageMap = batchResolveLedgerImages(rows);
rows.forEach(row -> { rows.forEach(row -> {
Integer familyCount = row.getInt("familyCount"); Integer familyCount = row.getInt("familyCount");
row.put("familyCountText", (familyCount == null ? 0 : familyCount) + ""); row.put("familyCountText", (familyCount == null ? 0 : familyCount) + "");
row.put("joinedText", Boolean.TRUE.equals(row.getBoolean("joined")) ? "" : ""); row.put("joinedText", Boolean.TRUE.equals(row.getBoolean("joined")) ? "" : "");
row.put("overCostReimbursedText", Boolean.TRUE.equals(row.getBoolean("overCostReimbursed")) ? "" : ""); row.put("overCostReimbursedText", Boolean.TRUE.equals(row.getBoolean("overCostReimbursed")) ? "" : "");
fillLedgerPhotoRowImages(row, maxPhotoCount, maxCurrentPeriodPhotoCount); fillLedgerPhotoRowImages(row, maxPhotoCount, maxCurrentPeriodPhotoCount, imageMap);
}); });
List<List<LedgerExportImage>> rowImages = snapshotLedgerRowImages(rows); List<List<LedgerExportImage>> rowImages = snapshotLedgerRowImages(rows);
@@ -439,22 +563,23 @@ public class ThirtyTeachTourLedgerController {
/** /**
* 将每条台账的图片材料解析为可写入 Excel 的图片对象,图片列文本保持为空,后续由 POI 手动插入图片。 * 将每条台账的图片材料解析为可写入 Excel 的图片对象,图片列文本保持为空,后续由 POI 手动插入图片。
*/ */
private void fillLedgerPhotoRowImages(NutMap row, int maxPhotoCount, int maxCurrentPeriodPhotoCount) { private void fillLedgerPhotoRowImages(NutMap row, int maxPhotoCount, int maxCurrentPeriodPhotoCount,
Map<String, LedgerExportImage> imageMap) {
List<String> photoFiles = splitPhotoFiles(row.getString("photoFiles", "")); List<String> photoFiles = splitPhotoFiles(row.getString("photoFiles", ""));
List<String> currentPeriodPhotoFiles = splitPhotoFiles(row.getString("currentPeriodPhotoFiles", "")); List<String> currentPeriodPhotoFiles = splitPhotoFiles(row.getString("currentPeriodPhotoFiles", ""));
if (!photoFiles.isEmpty()) { if (!photoFiles.isEmpty()) {
log.info("三十年教龄疗休养台账30年前图片字段读取:jobNo={}, userName={}, photoCount={}, photoFiles={}", log.debug("三十年教龄疗休养台账30年前图片字段读取:jobNo={}, userName={}, photoCount={}, photoFiles={}",
row.getString("jobNo", ""), row.getString("userName", ""), photoFiles.size(), row.getString("photoFiles", "")); row.getString("jobNo", ""), row.getString("userName", ""), photoFiles.size(), row.getString("photoFiles", ""));
} }
if (!currentPeriodPhotoFiles.isEmpty()) { if (!currentPeriodPhotoFiles.isEmpty()) {
log.info("三十年教龄疗休养台账当前时间段图片字段读取:jobNo={}, userName={}, photoCount={}, photoFiles={}", log.debug("三十年教龄疗休养台账当前时间段图片字段读取:jobNo={}, userName={}, photoCount={}, photoFiles={}",
row.getString("jobNo", ""), row.getString("userName", ""), currentPeriodPhotoFiles.size(), row.getString("currentPeriodPhotoFiles", "")); row.getString("jobNo", ""), row.getString("userName", ""), currentPeriodPhotoFiles.size(), row.getString("currentPeriodPhotoFiles", ""));
} }
// 图片对象按导出列顺序组装:先 30 年前图片,再当前时间段图片,确保同一教职工图片仍在同一行。 // 图片对象按导出列顺序组装:先 30 年前图片,再当前时间段图片,确保同一教职工图片仍在同一行。
List<LedgerExportImage> photoImages = new ArrayList<>(); List<LedgerExportImage> photoImages = new ArrayList<>();
photoImages.addAll(resolveLedgerImages(row.getString("photoFiles", ""), 0)); photoImages.addAll(resolveLedgerImages(row.getString("photoFiles", ""), 0, imageMap));
photoImages.addAll(resolveLedgerImages(row.getString("currentPeriodPhotoFiles", ""), maxPhotoCount)); photoImages.addAll(resolveLedgerImages(row.getString("currentPeriodPhotoFiles", ""), maxPhotoCount, imageMap));
row.put("photoImages", photoImages); row.put(LEDGER_EXPORT_ROW_IMAGES_KEY, photoImages);
for (int i = 1; i <= maxPhotoCount + maxCurrentPeriodPhotoCount; i++) { for (int i = 1; i <= maxPhotoCount + maxCurrentPeriodPhotoCount; i++) {
row.put("photoCell" + i, ""); row.put("photoCell" + i, "");
} }
@@ -490,7 +615,7 @@ public class ThirtyTeachTourLedgerController {
} }
for (NutMap row : rows) { for (NutMap row : rows) {
List<LedgerExportImage> images = new ArrayList<>(); List<LedgerExportImage> images = new ArrayList<>();
Object photoImageData = row.get("photoImages"); Object photoImageData = row.get(LEDGER_EXPORT_ROW_IMAGES_KEY);
if (photoImageData instanceof List) { if (photoImageData instanceof List) {
for (Object image : (List<?>) photoImageData) { for (Object image : (List<?>) photoImageData) {
if (image instanceof LedgerExportImage) { if (image instanceof LedgerExportImage) {
@@ -532,7 +657,7 @@ public class ThirtyTeachTourLedgerController {
anchor.setRow2(excelRowIndex + 1); anchor.setRow2(excelRowIndex + 1);
drawing.createPicture(anchor, pictureIndex); drawing.createPicture(anchor, pictureIndex);
insertedPictureCount++; insertedPictureCount++;
log.info("三十年教龄疗休养台账图片写入Excel成功:rowIndex={}, columnOffset={}, fileId={}, bytes={}, pictureType={}", log.debug("三十年教龄疗休养台账图片写入Excel成功:rowIndex={}, columnOffset={}, fileId={}, bytes={}, pictureType={}",
excelRowIndex, image.columnOffset, image.fileId, image.bytes.length, image.pictureType); excelRowIndex, image.columnOffset, image.fileId, image.bytes.length, image.pictureType);
} }
} }
@@ -540,21 +665,193 @@ public class ThirtyTeachTourLedgerController {
} }
/** /**
* 将台账图片字段解析成可插入 Excel 的图片对象,当前优先支持系统文件下载路径 * 先收集本次导出涉及的全部图片,再批量查询 sys_file 并并行读取图片字节,避免逐行逐图查库
* 图片字节处理仅生成不可变结果,Excel Workbook 仍在后续单线程写入,避免 POI 对象并发访问。
*/ */
private List<LedgerExportImage> resolveLedgerImages(String photoFiles) { private Map<String, LedgerExportImage> batchResolveLedgerImages(List<NutMap> rows) {
return resolveLedgerImages(photoFiles, 0); Map<String, Set<String>> pathCandidateMap = collectLedgerPhotoPathCandidates(rows);
List<String> normalizedPaths = new ArrayList<>(pathCandidateMap.keySet());
if (normalizedPaths.isEmpty()) {
return Collections.emptyMap();
}
Map<String, Sys_file> sysFileMap = batchFetchSysFiles(pathCandidateMap);
Map<String, LedgerExportImage> imageMap = new LinkedHashMap<>();
ExecutorService executorService = Executors.newFixedThreadPool(Math.min(LEDGER_EXPORT_IMAGE_THREAD_COUNT, normalizedPaths.size()));
List<Future<LedgerExportImageResolveResult>> futures = new ArrayList<>();
for (String normalizedPath : normalizedPaths) {
Sys_file sysFile = sysFileMap.get(normalizedPath);
if (sysFile == null) {
sysFile = sysFileMap.get(extractIdParam(normalizedPath));
}
Sys_file resolvedSysFile = sysFile;
futures.add(executorService.submit(() -> new LedgerExportImageResolveResult(normalizedPath,
resolveLedgerImage(normalizedPath, resolvedSysFile))));
}
executorService.shutdown();
for (Future<LedgerExportImageResolveResult> future : futures) {
try {
LedgerExportImageResolveResult result = future.get(LEDGER_EXPORT_IMAGE_TASK_TIMEOUT_SECONDS, TimeUnit.SECONDS);
if (result != null && result.image != null) {
imageMap.put(result.normalizedPath, result.image);
if (StrUtil.isNotBlank(result.image.fileId)) {
imageMap.putIfAbsent(result.image.fileId, result.image);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("三十年教龄疗休养台账导出图片并行读取被中断:msg={}", e.getMessage());
break;
} catch (ExecutionException e) {
log.warn("三十年教龄疗休养台账导出图片并行读取失败:msg={}", e.getMessage());
} catch (TimeoutException e) {
future.cancel(true);
log.warn("三十年教龄疗休养台账导出图片并行读取超时:timeoutSeconds={}", LEDGER_EXPORT_IMAGE_TASK_TIMEOUT_SECONDS);
}
}
try {
if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
executorService.shutdownNow();
}
log.info("三十年教龄疗休养台账导出图片批量解析完成:pathCount={}, sysFileCount={}, imageCount={}",
normalizedPaths.size(), sysFileMap.size(), imageMap.size());
return imageMap;
} }
/** /**
* 台账图片字段解析成可插入 Excel 的图片对象,并记录图片列偏移,支持多组图片列分区写入 * 台账图片字段原始顺序收集并去重,同时保留原始路径候选值用于批量匹配历史 sys_file.downloadPath
*/ */
private List<LedgerExportImage> resolveLedgerImages(String photoFiles, int columnOffsetStart) { private Map<String, Set<String>> collectLedgerPhotoPathCandidates(List<NutMap> rows) {
if (rows == null || rows.isEmpty()) {
return Collections.emptyMap();
}
Map<String, Set<String>> pathCandidateMap = new LinkedHashMap<>();
for (NutMap row : rows) {
splitPhotoFiles(row.getString("photoFiles", "")).forEach(path -> addPhotoPathCandidates(pathCandidateMap, path));
splitPhotoFiles(row.getString("currentPeriodPhotoFiles", "")).forEach(path -> addPhotoPathCandidates(pathCandidateMap, path));
}
return pathCandidateMap;
}
/**
* 同一图片用规范化路径作为回填 key,并保留原始值、解码值和规范化值,避免批量查询漏掉历史保存格式。
*/
private void addPhotoPathCandidates(Map<String, Set<String>> pathCandidateMap, String photoPath) {
String originalPath = StrUtil.trimToEmpty(photoPath);
String normalizedPath = normalizePhotoPath(photoPath);
if (StrUtil.isNotBlank(normalizedPath)) {
Set<String> candidates = pathCandidateMap.computeIfAbsent(normalizedPath, key -> new LinkedHashSet<>());
addPhotoPathCandidate(candidates, originalPath);
addPhotoPathCandidate(candidates, decodePhotoPath(originalPath));
addPhotoPathCandidate(candidates, normalizedPath);
if (StrUtil.isNotBlank(Globals.AppDomain) && normalizedPath.startsWith("/")) {
addPhotoPathCandidate(candidates, Globals.AppDomain + normalizedPath);
}
}
}
/**
* 图片路径候选值统一入集合,过滤空值,避免 IN 条件中出现无效参数。
*/
private void addPhotoPathCandidate(Set<String> candidates, String candidate) {
if (StrUtil.isNotBlank(candidate)) {
candidates.add(candidate);
}
}
/**
* 批量查询本次导出涉及的 sys_file 记录,同时兼容按文件 ID、原始 downloadPath 和规范化 downloadPath 保存的历史图片路径。
*/
private Map<String, Sys_file> batchFetchSysFiles(Map<String, Set<String>> pathCandidateMap) {
if (pathCandidateMap == null || pathCandidateMap.isEmpty()) {
return Collections.emptyMap();
}
Set<String> fileIds = new LinkedHashSet<>();
Set<String> downloadPaths = new LinkedHashSet<>();
for (Map.Entry<String, Set<String>> entry : pathCandidateMap.entrySet()) {
String normalizedPath = entry.getKey();
String fileId = extractIdParam(normalizedPath);
if (StrUtil.isNotBlank(fileId)) {
fileIds.add(fileId);
}
for (String candidate : entry.getValue()) {
String candidateFileId = extractIdParam(candidate);
if (StrUtil.isNotBlank(candidateFileId)) {
fileIds.add(candidateFileId);
}
downloadPaths.add(candidate);
}
}
Map<String, Sys_file> result = new LinkedHashMap<>();
querySysFilesByIds(fileIds).forEach(file -> putSysFileIndexes(result, file));
querySysFilesByDownloadPaths(downloadPaths).forEach(file -> putSysFileIndexes(result, file));
return result;
}
/**
* sys_file 按文件 ID 批量查询,使用模型字段引用兼容 @Name 主键字段映射。
*/
private List<Sys_file> querySysFilesByIds(Set<String> values) {
if (values == null || values.isEmpty()) {
return Collections.emptyList();
}
List<String> valueList = new ArrayList<>(values);
List<Sys_file> files = new ArrayList<>();
for (int start = 0; start < valueList.size(); start += LEDGER_EXPORT_FILE_QUERY_BATCH_SIZE) {
int end = Math.min(start + LEDGER_EXPORT_FILE_QUERY_BATCH_SIZE, valueList.size());
files.addAll(sysFileService.query(Cnd.where(Sys_file::getId, "in", valueList.subList(start, end))));
}
return files;
}
/**
* sys_file 按下载路径批量查询,按固定大小分片避免超长 IN 条件影响数据库解析。
*/
private List<Sys_file> querySysFilesByDownloadPaths(Set<String> values) {
if (values == null || values.isEmpty()) {
return Collections.emptyList();
}
List<String> valueList = new ArrayList<>(values);
List<Sys_file> files = new ArrayList<>();
for (int start = 0; start < valueList.size(); start += LEDGER_EXPORT_FILE_QUERY_BATCH_SIZE) {
int end = Math.min(start + LEDGER_EXPORT_FILE_QUERY_BATCH_SIZE, valueList.size());
files.addAll(sysFileService.query(Cnd.where(Sys_file::getDownloadPath, "in", valueList.subList(start, end))));
}
return files;
}
/**
* 同一文件同时建立 id 和 downloadPath 索引,行内回填时统一用规范化路径查找。
*/
private void putSysFileIndexes(Map<String, Sys_file> result, Sys_file file) {
if (file == null) {
return;
}
if (StrUtil.isNotBlank(file.getId())) {
result.putIfAbsent(file.getId(), file);
}
if (StrUtil.isNotBlank(file.getDownloadPath())) {
result.putIfAbsent(normalizePhotoPath(file.getDownloadPath()), file);
}
}
/**
* 将台账图片字段解析成可插入 Excel 的图片对象,并记录图片列偏移,图片字节来自批量预解析结果。
*/
private List<LedgerExportImage> resolveLedgerImages(String photoFiles, int columnOffsetStart,
Map<String, LedgerExportImage> imageMap) {
List<LedgerExportImage> images = new ArrayList<>(); List<LedgerExportImage> images = new ArrayList<>();
List<String> photoPaths = splitPhotoFiles(photoFiles); List<String> photoPaths = splitPhotoFiles(photoFiles);
for (int photoIndex = 0; photoIndex < photoPaths.size(); photoIndex++) { for (int photoIndex = 0; photoIndex < photoPaths.size(); photoIndex++) {
String originalPath = photoPaths.get(photoIndex); String originalPath = photoPaths.get(photoIndex);
LedgerExportImage image = resolveLedgerImage(originalPath); String normalizedPath = normalizePhotoPath(originalPath);
LedgerExportImage image = imageMap == null ? null : imageMap.get(normalizedPath);
if (image == null && imageMap != null) {
image = imageMap.get(extractIdParam(normalizedPath));
}
if (image != null) { if (image != null) {
images.add(image.withColumnOffset(columnOffsetStart + photoIndex)); images.add(image.withColumnOffset(columnOffsetStart + photoIndex));
} else { } else {
@@ -567,13 +864,10 @@ public class ThirtyTeachTourLedgerController {
/** /**
* 根据后台保存的下载路径定位 sys_file,并读取真实图片字节及 POI 可识别的图片类型。 * 根据后台保存的下载路径定位 sys_file,并读取真实图片字节及 POI 可识别的图片类型。
*/ */
private LedgerExportImage resolveLedgerImage(String originalPath) { private LedgerExportImage resolveLedgerImage(String normalizedPath, Sys_file sysFile) {
String normalizedPath = normalizePhotoPath(originalPath);
String fileId = extractIdParam(normalizedPath); String fileId = extractIdParam(normalizedPath);
Sys_file sysFile = fetchSysFile(fileId, normalizedPath);
if (sysFile == null) { if (sysFile == null) {
log.warn("三十年教龄疗休养台账图片文件记录不存在:fileId={}, originalPath={}, normalizedPath={}", log.warn("三十年教龄疗休养台账图片文件记录不存在:fileId={}, normalizedPath={}", fileId, normalizedPath);
fileId, originalPath, normalizedPath);
return null; return null;
} }
byte[] bytes = readSysFileBytes(sysFile); byte[] bytes = readSysFileBytes(sysFile);
@@ -587,25 +881,11 @@ public class ThirtyTeachTourLedgerController {
sysFile.getId(), sysFile.getEngine(), sysFile.getStoragePath(), bytes == null ? 0 : bytes.length, pictureType); sysFile.getId(), sysFile.getEngine(), sysFile.getStoragePath(), bytes == null ? 0 : bytes.length, pictureType);
return null; return null;
} }
log.info("三十年教龄疗休养台账图片读取成功:fileId={}, engine={}, bucket={}, storagePath={}, bytes={}, pictureType={}", log.debug("三十年教龄疗休养台账图片读取成功:fileId={}, engine={}, bucket={}, storagePath={}, bytes={}, pictureType={}",
sysFile.getId(), sysFile.getEngine(), sysFile.getBucket(), sysFile.getStoragePath(), bytes.length, pictureType); sysFile.getId(), sysFile.getEngine(), sysFile.getBucket(), sysFile.getStoragePath(), bytes.length, pictureType);
return new LedgerExportImage(sysFile.getId(), normalizedPath, bytes, pictureType); return new LedgerExportImage(sysFile.getId(), normalizedPath, bytes, pictureType);
} }
/**
* 优先按文件ID查询,失败时按 downloadPath 查询,兼容后台保存的是完整下载路径的场景。
*/
private Sys_file fetchSysFile(String fileId, String normalizedPath) {
Sys_file sysFile = null;
if (StrUtil.isNotBlank(fileId)) {
sysFile = sysFileService.fetch(fileId);
}
if (sysFile == null && StrUtil.isNotBlank(normalizedPath)) {
sysFile = sysFileService.fetch(Cnd.where(Sys_file::getDownloadPath, "=", normalizedPath));
}
return sysFile;
}
/** /**
* 根据图片字节头判断 POI 图片类型,无法识别时尝试通过 ImageIO 转为 PNG。 * 根据图片字节头判断 POI 图片类型,无法识别时尝试通过 ImageIO 转为 PNG。
*/ */
@@ -703,14 +983,10 @@ public class ThirtyTeachTourLedgerController {
* 将完整URL规整为站内相对路径,避免导出时重复拼接系统域名。 * 将完整URL规整为站内相对路径,避免导出时重复拼接系统域名。
*/ */
private String normalizePhotoPath(String photoFile) { private String normalizePhotoPath(String photoFile) {
String path = StrUtil.trimToEmpty(photoFile); String path = decodePhotoPath(photoFile);
if (StrUtil.isBlank(path)) { if (StrUtil.isBlank(path)) {
return ""; return "";
} }
try {
path = URLDecoder.decode(path, StandardCharsets.UTF_8.toString());
} catch (Exception ignored) {
}
if (StrUtil.isNotBlank(Globals.AppDomain) && path.startsWith(Globals.AppDomain)) { if (StrUtil.isNotBlank(Globals.AppDomain) && path.startsWith(Globals.AppDomain)) {
path = path.substring(Globals.AppDomain.length()); path = path.substring(Globals.AppDomain.length());
} }
@@ -722,6 +998,21 @@ public class ThirtyTeachTourLedgerController {
return path; return path;
} }
/**
* 图片路径可能由前端 URL 编码后保存,统一解码后再参与匹配和规范化处理。
*/
private String decodePhotoPath(String photoFile) {
String path = StrUtil.trimToEmpty(photoFile);
if (StrUtil.isBlank(path)) {
return "";
}
try {
return URLDecoder.decode(path, StandardCharsets.UTF_8.toString());
} catch (Exception ignored) {
return path;
}
}
/** /**
* 台账导出图片对象,保存 POI 写图所需的图片字节、类型和来源文件信息。 * 台账导出图片对象,保存 POI 写图所需的图片字节、类型和来源文件信息。
*/ */
@@ -749,6 +1040,19 @@ public class ThirtyTeachTourLedgerController {
} }
} }
/**
* 图片并行读取任务的返回对象,保留规范化路径用于回填到导出行。
*/
private static class LedgerExportImageResolveResult {
private final String normalizedPath;
private final LedgerExportImage image;
private LedgerExportImageResolveResult(String normalizedPath, LedgerExportImage image) {
this.normalizedPath = normalizedPath;
this.image = image;
}
}
private List<NutMap> queryLedgerExportRows(Integer startYear, Integer endYear, String keyword, String unionId, String lineId, private List<NutMap> queryLedgerExportRows(Integer startYear, Integer endYear, String keyword, String unionId, String lineId,
String travelPeriod, String lineType, Boolean directFamilyOnly, Boolean overCostOnly) { String travelPeriod, String lineType, Boolean directFamilyOnly, Boolean overCostOnly) {
Cnd cnd = buildQueryCnd(startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType, directFamilyOnly, overCostOnly); Cnd cnd = buildQueryCnd(startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType, directFamilyOnly, overCostOnly);
@@ -761,6 +1065,7 @@ public class ThirtyTeachTourLedgerController {
COALESCE(t.age, vu.age) AS age, COALESCE(t.age, vu.age) AS age,
COALESCE(NULLIF(t.idCard, ''), NULLIF(vu.idCard, ''), '') AS idCard, COALESCE(NULLIF(t.idCard, ''), NULLIF(vu.idCard, ''), '') AS idCard,
COALESCE(NULLIF(t.mobile, ''), NULLIF(vu.mobile, ''), '') AS mobile, COALESCE(NULLIF(t.mobile, ''), NULLIF(vu.mobile, ''), '') AS mobile,
COALESCE(NULLIF(t.unionName, ''), vu.unionName, '') AS unionName,
t.unitName, t.unitName,
t.boardingPlace, t.boardingPlace,
COALESCE(NULLIF(l.lineName, ''), t.lineName, '') AS lineName, COALESCE(NULLIF(l.lineName, ''), t.lineName, '') AS lineName,
@@ -34,6 +34,7 @@ import com.budwk.app.zhgh.dayofficework.thirtyTeachTour.service.ThirtyTeachTourL
import com.budwk.app.zhgh.dayofficework.thirtyTeachTour.service.ThirtyTeachTourMatterService; import com.budwk.app.zhgh.dayofficework.thirtyTeachTour.service.ThirtyTeachTourMatterService;
import com.budwk.app.zhgh.dayofficework.thirtyTeachTour.service.ThirtyTeachTourUserAssignmentService; import com.budwk.app.zhgh.dayofficework.thirtyTeachTour.service.ThirtyTeachTourUserAssignmentService;
import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls; import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql; import org.nutz.dao.sql.Sql;
@@ -445,12 +446,14 @@ public class ThirtyTeachTourMySignupController {
* @param ledger 报名台账主信息 * @param ledger 报名台账主信息
* @param families 家属信息 JSON 数组 * @param families 家属信息 JSON 数组
* @param directRelative 直系亲属线路报名信息 JSON * @param directRelative 直系亲属线路报名信息 JSON
* @param boardingPlaceOnly H5 修改页只更新乘车地点时传 true,避免修改其他只读报名信息
* @return 修改结果;直系亲属线路、超额报销等场景会返回待审核提示 * @return 修改结果;直系亲属线路、超额报销等场景会返回待审核提示
*/ */
@At @At
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
@SaCheckPermission(value = {"thirtyTeachTour.mysignup", "h5.thirtyTeachTour.mysignup"}, mode = SaMode.OR) @SaCheckPermission(value = {"thirtyTeachTour.mysignup", "h5.thirtyTeachTour.mysignup"}, mode = SaMode.OR)
public Result doSignup(ThirtyTeachTourLedger ledger, @Param("families") String families, @Param("directRelative") String directRelative) { public Result doSignup(ThirtyTeachTourLedger ledger, @Param("families") String families, @Param("directRelative") String directRelative,
@Param("boardingPlaceOnly") Boolean boardingPlaceOnly) {
Result checkResult = checkSignup(ledger); Result checkResult = checkSignup(ledger);
if (checkResult != null) { if (checkResult != null) {
return checkResult; return checkResult;
@@ -462,6 +465,9 @@ public class ThirtyTeachTourMySignupController {
if (oldLedger == null) { if (oldLedger == null) {
return Result.error("报名记录不存在"); return Result.error("报名记录不存在");
} }
if (Boolean.TRUE.equals(boardingPlaceOnly)) {
return updateH5BoardingPlaceOnly(ledger, oldLedger);
}
List<ThirtyTeachTourLedgerFamily> familyList = parseFamilies(families); List<ThirtyTeachTourLedgerFamily> familyList = parseFamilies(families);
Result familyResult = checkFamilies(familyList); Result familyResult = checkFamilies(familyList);
if (familyResult != null) { if (familyResult != null) {
@@ -541,6 +547,41 @@ public class ThirtyTeachTourMySignupController {
return Result.success().addMsg(approvalRequired ? "修改已提交,等待审核" : "修改成功"); return Result.success().addMsg(approvalRequired ? "修改已提交,等待审核" : "修改成功");
} }
/**
* H5 我的疗休养修改页仅允许调整乘车地点,其他报名、家属和直系亲属信息均以原台账为准。
* 保存时同步当前人员分配记录中的乘车地点,保证移动端列表、人员分配和台账展示一致。
*
* @param submitLedger 页面提交的台账信息,仅读取 id 和 boardingPlace
* @param oldLedger 当前登录人的原台账记录
* @return 乘车地点保存结果
*/
private Result updateH5BoardingPlaceOnly(ThirtyTeachTourLedger submitLedger, ThirtyTeachTourLedger oldLedger) {
if (oldLedger == null || StrUtil.isBlank(oldLedger.getMatterId())) {
return Result.error("报名记录缺少事项信息,不能修改");
}
ThirtyTeachTourMatter matter = tourMatterService.fetch(oldLedger.getMatterId());
if (matter == null || Boolean.TRUE.equals(matter.getDelFlag()) || !Boolean.TRUE.equals(matter.getEnabled())) {
return Result.error("报名出行时段不存在或已停用");
}
if (isTravelEnded(matter.getTravelEndTime())) {
return Result.error("线路出行已结束,不能修改");
}
ThirtyTeachTourSetting setting = tourLedgerService.dao().fetch(ThirtyTeachTourSetting.class, matter.getSettingId());
oldLedger.setBoardingPlace(submitLedger == null ? "" : submitLedger.getBoardingPlace());
Result boardingPlaceResult = normalizeBoardingPlace(oldLedger, matter, setting);
if (boardingPlaceResult != null) {
return boardingPlaceResult;
}
// H5 修改页除乘车地点外全部只读,数据库更新也只落乘车地点字段,避免前端绕过只读限制。
tourLedgerService.update(Chain.make("boardingPlace", oldLedger.getBoardingPlace()),
Cnd.where(ThirtyTeachTourLedger::getId, "=", oldLedger.getId())
.and(ThirtyTeachTourLedger::getJobNo, "=", currentJobNo())
.and(ThirtyTeachTourLedger::getDelFlag, "=", false));
tourUserAssignmentService.backfillExistingAssignmentAfterSignup(matter.getSettingId(), matter.getId(), SecurityUtil.getUserId(),
oldLedger.getBoardingPlace(), oldLedger.getPhotoFiles(), oldLedger.getCurrentPeriodPhotoFiles());
return Result.success().addMsg("修改成功");
}
@At @At
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
@SaCheckPermission(value = {"thirtyTeachTour.mysignup", "h5.thirtyTeachTour.mysignup"}, mode = SaMode.OR) @SaCheckPermission(value = {"thirtyTeachTour.mysignup", "h5.thirtyTeachTour.mysignup"}, mode = SaMode.OR)
@@ -698,11 +698,13 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
sql.setParam("unionId", unionId); sql.setParam("unionId", unionId);
List<NutMap> rows = listMap(sql); List<NutMap> rows = listMap(sql);
NutMap quotaInfo = Lang.isEmpty(rows) ? emptyQuotaInfo() : rows.get(0); NutMap quotaInfo = Lang.isEmpty(rows) ? emptyQuotaInfo() : rows.get(0);
int branchTotalQuota = branchPeriodQuotaTotal(settingId, unionId);
int formalQuota = defaultInt(quotaInfo.getInt("formalQuota")); int formalQuota = defaultInt(quotaInfo.getInt("formalQuota"));
int backupQuota = defaultInt(quotaInfo.getInt("backupQuota")); int backupQuota = defaultInt(quotaInfo.getInt("backupQuota"));
int formalUsed = defaultInt(quotaInfo.getInt("formalUsed")); int formalUsed = defaultInt(quotaInfo.getInt("formalUsed"));
int backupUsed = defaultInt(quotaInfo.getInt("backupUsed")); int backupUsed = defaultInt(quotaInfo.getInt("backupUsed"));
return NutMap.NEW() return NutMap.NEW()
.addv("branchTotalQuota", branchTotalQuota)
.addv("formalQuota", formalQuota) .addv("formalQuota", formalQuota)
.addv("backupQuota", backupQuota) .addv("backupQuota", backupQuota)
.addv("formalUsed", formalUsed) .addv("formalUsed", formalUsed)
@@ -2072,6 +2074,7 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
private NutMap emptyQuotaInfo() { private NutMap emptyQuotaInfo() {
return NutMap.NEW() return NutMap.NEW()
.addv("branchTotalQuota", 0)
.addv("formalQuota", 0) .addv("formalQuota", 0)
.addv("backupQuota", 0) .addv("backupQuota", 0)
.addv("formalUsed", 0) .addv("formalUsed", 0)
@@ -2080,6 +2083,29 @@ public class ThirtyTeachTourUserAssignmentServiceImpl extends BaseServiceImpl<Th
.addv("backupRemaining", 0); .addv("backupRemaining", 0);
} }
/**
* 统计当前配置下当前分工会所有出行时间段分配人员名额的汇总数量,用于人员分配弹窗展示分工会总名额。
*/
private int branchPeriodQuotaTotal(String settingId, String unionId) {
if (StrUtil.isBlank(settingId) || StrUtil.isBlank(unionId)) {
return 0;
}
Sql sql = Sqls.create("""
SELECT COALESCE(SUM(formalQuota), 0) AS branchTotalQuota
FROM thirty_teach_tour_setting_period_quota
WHERE settingId = @settingId
AND unionId = @unionId
AND delFlag = 0
""");
sql.setParam("settingId", settingId);
sql.setParam("unionId", unionId);
List<NutMap> rows = listMap(sql);
if (Lang.isEmpty(rows)) {
return 0;
}
return defaultInt(rows.get(0).getInt("branchTotalQuota"));
}
private NutMap emptyPeriodQuotaInfo(String periodId) { private NutMap emptyPeriodQuotaInfo(String periodId) {
return NutMap.NEW() return NutMap.NEW()
.addv("periodId", periodId) .addv("periodId", periodId)
@@ -1,29 +1,19 @@
package com.budwk.app.zhgh.democratic.opinion.controller.transact; package com.budwk.app.zhgh.democratic.opinion.controller.transact;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result; import com.budwk.app.base.result.Result;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.opinion.param.OpinionSearchParam; import com.budwk.app.zhgh.democratic.opinion.param.OpinionSearchParam;
import com.budwk.app.zhgh.democratic.opinion.service.common.OpinionCommonService; import com.budwk.app.zhgh.democratic.opinion.service.common.OpinionCommonService;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop; 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.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.validation.Valid; import javax.validation.Valid;
import java.util.List;
/** /**
* @ClassName OpinionUnitReplyController * @ClassName OpinionUnitReplyController
@@ -39,8 +29,6 @@ import java.util.List;
@Api(tags = "意见管理-承办答复") @Api(tags = "意见管理-承办答复")
public class OpinionUnitReplyController { public class OpinionUnitReplyController {
@Inject
private Dao dao;
@Inject @Inject
private OpinionCommonService opinionCommonService; private OpinionCommonService opinionCommonService;
@@ -50,67 +38,15 @@ public class OpinionUnitReplyController {
public void index() { public void index() {
} }
/**
* 承办答复分页查询pageForm 接收届次 sessionId意见搜索条件分页及排序参数
* approval true 查询已审核 false 或未传时查询未审核
* 返回 Resultdata 为分页结果其中 list 为意见及任务信息totalCount 为匹配总数
*/
@At @At
@SaCheckPermission("opinion.unitReply") @SaCheckPermission("opinion.unitReply")
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public Result pageData(@Valid OpinionSearchParam pageForm, public Result pageData(@Valid OpinionSearchParam pageForm) {
@Param(value = "String") String sessionId, return Result.success(opinionCommonService.listUnitReplyPage(pageForm));
@Param(value = "approval") Boolean approval) {
Sql sql = Sqls.create("""
SELECT
info.*,
type.name AS typeName,
tcs.fullName AS sessionName,
tcd.`name` AS delegationName,
ins.id AS instanceId,
ins.businessNo,
ins.state instanceState,
ins.variable instanceVariable,
ins.processDefineId instanceProcessDefineId,
t.id taskId,
t.taskName AS taskKey,
t.displayName taskName,
t.taskType,
t.performType taskPerformType,
t.taskState,
t.finishTime,
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
wf_process_task t
LEFT JOIN wf_process_instance ins ON ins.id = t.processInstanceId
LEFT JOIN opinion_info info ON info.id = ins.businessNo
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id
LEFT JOIN opinion_type type on type.id = info.typeId
LEFT JOIN teacher_congress_session tcs on tcs.id = info.sessionId
LEFT JOIN teacher_congress_delegation tcd on tcd.id = info.delegationId
LEFT JOIN 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
""");
Cnd cnd = Cnd.NEW();
cnd.andEX("info.sessionId", "=", sessionId);
cnd.and("t.taskName", "in", List.of("master_reply", "slave_reply"));
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId()));
if (approval) {
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
} else {
cnd.and("pta.actorId", "in", List.of(SecurityUtil.getUserId()));
cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
}
OpinionSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
sql.setCondition(cnd);
Pagination<NutMap> pageVO = opinionCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pageVO);
} }
} }
@@ -1,83 +1,38 @@
package com.budwk.app.zhgh.democratic.opinion.handler; 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.AssignmentHandler;
import com.budwk.app.flow.engine.core.Execution; import com.budwk.app.flow.engine.core.Execution;
import com.budwk.app.flow.engine.core.ServiceContext; import com.budwk.app.flow.engine.core.ServiceContext;
import com.budwk.app.flow.engine.model.TaskModel; import com.budwk.app.flow.engine.model.TaskModel;
import com.budwk.app.sys.models.Sys_role; import com.budwk.app.zhgh.democratic.opinion.service.OpinionSchoolAuditService;
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 java.util.List; import java.util.List;
/** /** 意见主办单位处理人,由 service 统一校验多主办参数及负责人配置。 */
* @ClassName OpinionMasterUnitAssignmentHandler
* @Author JyuHsin
* @Date 2026/2/3 11:13
* @Version 1.0
* @Description TODO
*/
public class OpinionMasterUnitAssignmentHandler implements AssignmentHandler { public class OpinionMasterUnitAssignmentHandler implements AssignmentHandler {
@Override /**
public List<String> assign(TaskModel model, Execution execution) { * @param model 主办答复节点定义多个主办时应配置并行会签
Dao dao = ServiceContext.find(Dao.class); * @param execution 意见流程上下文包含主办 ID 数组或历史单主办 ID
SysRoleService sysRoleService = ServiceContext.find(SysRoleService.class); * @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)){ @Override
ProposalReplyUnit masterUnit = dao.fetch(ProposalReplyUnit.class, Cnd.where(ProposalReplyUnit::getProposalId, "=", opinionId).and(ProposalReplyUnit::getIsMaster, "=", 1)); public int getOrder() {
if (masterUnit == null) { return AssignmentHandler.super.getOrder();
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();
}
} }
@@ -24,6 +24,12 @@ import java.util.List;
* @Description TODO * @Description TODO
*/ */
public class OpinionSlaveUnitAssignmentHandler implements AssignmentHandler { 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 @Override
public List<String> assign(TaskModel model, Execution execution) { public List<String> assign(TaskModel model, Execution execution) {
Dao dao = ServiceContext.find(Dao.class); Dao dao = ServiceContext.find(Dao.class);
@@ -1,85 +1,21 @@
package com.budwk.app.zhgh.democratic.opinion.listenter; 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.FlowInterceptor;
import com.budwk.app.flow.engine.core.Execution; import com.budwk.app.flow.engine.core.Execution;
import com.budwk.app.flow.engine.core.ServiceContext; import com.budwk.app.flow.engine.core.ServiceContext;
import com.budwk.app.flow.engine.util.FlowUtil; import com.budwk.app.zhgh.democratic.opinion.service.OpinionSchoolAuditService;
import com.budwk.app.flow.entity.ProcessTask; import org.nutz.aop.interceptor.ioc.TransAop;
import com.budwk.app.flow.entity.ProcessTaskActor; import org.nutz.ioc.aop.Aop;
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 java.util.List; /** 审核完成后的承办单位持久化入口,业务处理和事务由 IOC 中的 service 承担。 */
/**
* @ClassName OpinionOfficeSuffixInterceptor
* @Author JyuHsin
* @Date 2026/2/3 14:04
* @Version 1.0
* @Description TODO
*/
public class OpinionOfficeSuffixInterceptor implements FlowInterceptor { public class OpinionOfficeSuffixInterceptor implements FlowInterceptor {
@Override /**
public void intercept(Execution execution) { * @param execution 意见流程上下文包含审核结果主协办单位和新生成的任务
Dao dao = ServiceContext.find(Dao.class); * @return 无返回值保存承办记录并补充答复任务的单位信息
*/
String opinionId = execution.getProcessInstance().getBusinessNo(); @Override
String officeResult = execution.getArgs().getStr(FlowConst.TASK_FORM_DATA_PREFIX + "schoolResult"); @Aop(TransAop.READ_COMMITTED)
public void intercept(Execution execution) {
if (StrUtil.isNotBlank(officeResult) && "YES".equals(officeResult)) { ServiceContext.find(OpinionSchoolAuditService.class).saveReplyUnits(execution);
// 删除原来的记录 }
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");
}
} }
@@ -31,9 +31,11 @@ public class OpinionSchoolAuditImportExcelMode extends ExcelImportError {
private String unitName; private String unitName;
@Excel(name = "主办") @Excel(name = "主办")
// 多个主办单位名称或编码仅使用中文分号分隔单位全称内部标点保留表头不变
private String masterUnitName; private String masterUnitName;
@Excel(name = "协办") @Excel(name = "协办")
// 多个协办单位与主办使用相同的中文分号分隔规则未指定协办时可留空
private String slaveUnitNames; private String slaveUnitNames;
@Excel(name = "审核结果") @Excel(name = "审核结果")
@@ -2,17 +2,43 @@ package com.budwk.app.zhgh.democratic.opinion.service;
import com.budwk.app.base.model.ExcelImportRes; import com.budwk.app.base.model.ExcelImportRes;
import com.budwk.app.base.service.BaseService; 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.mode.OpinionSchoolAuditImportExcelMode;
import com.budwk.app.zhgh.democratic.opinion.models.OpinionInfo; import com.budwk.app.zhgh.democratic.opinion.models.OpinionInfo;
import org.nutz.mvc.upload.TempFile; import org.nutz.mvc.upload.TempFile;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.util.List;
/** /**
* 校工会审核业务处理 * 校工会审核业务处理
*/ */
public interface OpinionSchoolAuditService extends BaseService<OpinionInfo> { public interface OpinionSchoolAuditService extends BaseService<OpinionInfo> {
/**
* 校验主协办单位并解析主办负责人兼容历史单主办流程变量
* @param model 主办答复节点定义多主办要求并行会签
* @param execution 流程上下文args 中传 tf_masterUnitIdstf_slaveUnitIds 等单位 ID 数组
* @return 去重后的主办负责人用户 ID 列表 WF 引擎创建任务
*/
List<String> assignMasterUnits(TaskModel model, Execution execution);
/**
* @param execution 意见审核上下文包含所选主办/协办单位 ID 数组
* @param master true 为主办false 为协办
* @param actors 本节点已授权负责人 ID不把未授权人员加入分组
* @return 承办单位 ID 到负责人 ID 列表 WF 生成每单位一份共享任务
*/
java.util.Map<String, List<String>> getUndertakeGroups(Execution execution, boolean master, List<String> actors);
/**
* 审核通过后保存主协办记录并按负责人配置关联新生成的答复任务
* @param execution 当前意见实例审核变量和新生成任务列表主办兼容旧 tf_masterUnitId
* @return 无返回值保存 opinion_reply_unit 及任务中的承办单位信息
*/
void saveReplyUnits(Execution execution);
/** /**
* 下载校工会审核结果导入模板 * 下载校工会审核结果导入模板
* *
@@ -1,7 +1,9 @@
package com.budwk.app.zhgh.democratic.opinion.service.common; package com.budwk.app.zhgh.democratic.opinion.service.common;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.BaseService; import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.democratic.opinion.models.OpinionInfo; import com.budwk.app.zhgh.democratic.opinion.models.OpinionInfo;
import com.budwk.app.zhgh.democratic.opinion.param.OpinionSearchParam;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo; import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake; import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
@@ -12,6 +14,14 @@ import java.util.List;
public interface OpinionCommonService extends BaseService<OpinionInfo> { public interface OpinionCommonService extends BaseService<OpinionInfo> {
/**
* 查询当前登录人的意见承办答复任务仅包含 JDHYJ 流程且业务意见存在的记录
*
* @param pageForm 届次意见搜索条件分页及排序参数approval true 查询已审核否则查询未审核
* @return 分页结果list 包含意见及任务信息totalCount 为匹配总数
*/
Pagination<NutMap> listUnitReplyPage(OpinionSearchParam pageForm);
/** /**
* 查询意见信息 * 查询意见信息
*/ */
@@ -7,11 +7,13 @@ import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject; import cn.hutool.json.JSONObject;
import com.budwk.app.base.constant.RoleConstant; import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException; import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.CommonDownloadUtil; import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.SysOfficeTemplateUtil; import com.budwk.app.base.utils.SysOfficeTemplateUtil;
import com.budwk.app.flow.constant.FlowConst; import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.engine.FlowEngine; 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.engine.util.FlowUtil;
import com.budwk.app.flow.entity.ProcessInstance; import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask; import com.budwk.app.flow.entity.ProcessTask;
@@ -26,6 +28,7 @@ import com.budwk.app.sys.services.SysRoleService;
import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.web.commons.base.Globals; import com.budwk.app.web.commons.base.Globals;
import com.budwk.app.zhgh.democratic.opinion.models.OpinionInfo; import com.budwk.app.zhgh.democratic.opinion.models.OpinionInfo;
import com.budwk.app.zhgh.democratic.opinion.param.OpinionSearchParam;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalConsolidation; 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.ProposalInfo;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalMerge; import com.budwk.app.zhgh.democratic.proposal.models.ProposalMerge;
@@ -70,11 +73,81 @@ public class OpinionCommonServiceImpl extends BaseServiceImpl<OpinionInfo> imple
private FlowEngine flowEngine; private FlowEngine flowEngine;
@Inject @Inject
private SysDictService sysDictService; private SysDictService sysDictService;
@Inject
private FlowCommonService flowCommonService;
public OpinionCommonServiceImpl(Dao dao) { public OpinionCommonServiceImpl(Dao dao) {
super(dao); super(dao);
} }
/**
* 按当前登录人和审核状态查询意见承办任务避免其他流程的同名节点混入列表
*
* @param pageForm 届次搜索分页及排序参数approval 为空时按未审核处理
* @return 包含 list totalCount 的分页结果保留页面所需的意见及任务字段
*/
@Override
public Pagination<NutMap> listUnitReplyPage(OpinionSearchParam pageForm) {
// 提案与意见存在同名答复节点必须同时校验流程标识及意见业务记录
Sql sql = Sqls.create("""
SELECT
info.*,
type.name AS typeName,
tcs.fullName AS sessionName,
tcd.`name` AS delegationName,
ins.id AS instanceId,
ins.businessNo,
ins.state instanceState,
ins.variable instanceVariable,
ins.processDefineId instanceProcessDefineId,
t.id taskId,
t.taskName AS taskKey,
t.displayName taskName,
t.taskType,
t.performType taskPerformType,
t.taskState,
t.finishTime,
t.taskParentId,
t.variable taskVariable,
IFNULL(GROUP_CONCAT(DISTINCT nt.displayName),'结束') curTaskName,
t.variable->>'$.underTakeName' AS underTakeName,
IF(JSON_EXTRACT(t.variable, '$.underTakeIsMaster') in (true, 1), 1, 0) AS underTakeIsMaster
FROM
wf_process_task t
INNER JOIN wf_process_instance ins ON ins.id = t.processInstanceId
INNER JOIN wf_process_define def ON def.id = ins.processDefineId
INNER JOIN opinion_info info ON info.id = ins.businessNo
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id
LEFT JOIN opinion_type type on type.id = info.typeId
LEFT JOIN teacher_congress_session tcs on tcs.id = info.sessionId
LEFT JOIN teacher_congress_delegation tcd on tcd.id = info.delegationId
LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10
LEFT JOIN wf_process_task_actor pta ON pta.processTaskId = nt.id
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("def.name", "=", "JDHYJ");
cnd.and("t.taskName", "in", List.of("master_reply", "slave_reply"));
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId()));
if (Boolean.TRUE.equals(pageForm.getApproval())) {
cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
} else {
cnd.and("pta.actorId", "in", List.of(SecurityUtil.getUserId()));
cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
}
OpinionSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
sql.setCondition(cnd);
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 @Override
public NutMap info(String id) { public NutMap info(String id) {
if (StrUtil.isBlank(id)) { if (StrUtil.isBlank(id)) {
@@ -9,6 +9,17 @@ import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.hutool.core.lang.Dict; import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.model.ExcelImportRes; 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.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.CommonDownloadUtil; import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.flow.constant.FlowConst; 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.opinion.service.OpinionSchoolAuditService;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake; import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake;
import org.apache.poi.ss.usermodel.Workbook; 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.Chain;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
@@ -39,6 +53,8 @@ import org.nutz.mvc.upload.TempFile;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@@ -54,11 +70,174 @@ public class OpinionSchoolAuditServiceImpl extends BaseServiceImpl<OpinionInfo>
private FlowEngine flowEngine; private FlowEngine flowEngine;
@Inject @Inject
private FlowCommonService flowCommonService; private FlowCommonService flowCommonService;
@Inject
private SysRoleService sysRoleService;
public OpinionSchoolAuditServiceImpl(Dao dao) { public OpinionSchoolAuditServiceImpl(Dao dao) {
super(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 @Override
public void downloadAuditImportTemplate(HttpServletResponse response) { public void downloadAuditImportTemplate(HttpServletResponse response) {
List<ExcelExportEntity> entities = new ArrayList<>(); List<ExcelExportEntity> entities = new ArrayList<>();
@@ -81,6 +260,7 @@ public class OpinionSchoolAuditServiceImpl extends BaseServiceImpl<OpinionInfo>
} }
@Override @Override
@Aop(TransAop.READ_COMMITTED)
public ExcelImportRes<OpinionSchoolAuditImportExcelMode> importAuditResult(TempFile file, String sessionId) { public ExcelImportRes<OpinionSchoolAuditImportExcelMode> importAuditResult(TempFile file, String sessionId) {
ExcelImportRes<OpinionSchoolAuditImportExcelMode> excelImportRes = new ExcelImportRes<>(); ExcelImportRes<OpinionSchoolAuditImportExcelMode> excelImportRes = new ExcelImportRes<>();
List<OpinionSchoolAuditImportExcelMode> importRows; List<OpinionSchoolAuditImportExcelMode> importRows;
@@ -125,6 +305,7 @@ public class OpinionSchoolAuditServiceImpl extends BaseServiceImpl<OpinionInfo>
return excelImportRes; return excelImportRes;
} }
@Aop(TransAop.READ_COMMITTED)
private void handleAuditImportRow(OpinionSchoolAuditImportExcelMode row, int rowIndex, String sessionId, Set<String> importedCodes) { private void handleAuditImportRow(OpinionSchoolAuditImportExcelMode row, int rowIndex, String sessionId, Set<String> importedCodes) {
List<String> errors = new ArrayList<>(); List<String> errors = new ArrayList<>();
String code = StrUtil.trim(row.getCode()); String code = StrUtil.trim(row.getCode());
@@ -145,7 +326,7 @@ public class OpinionSchoolAuditServiceImpl extends BaseServiceImpl<OpinionInfo>
} }
OpinionType type = null; OpinionType type = null;
ProposalUndertake masterUnit = null; List<ProposalUndertake> masterUnits = new ArrayList<>();
List<ProposalUndertake> slaveUnits = new ArrayList<>(); List<ProposalUndertake> slaveUnits = new ArrayList<>();
if (auditResult == AuditResult.PASS) { if (auditResult == AuditResult.PASS) {
// 审核通过时与页面必填规则保持一致意见类别主办单位审核意见必须填写 // 审核通过时与页面必填规则保持一致意见类别主办单位审核意见必须填写
@@ -161,18 +342,16 @@ public class OpinionSchoolAuditServiceImpl extends BaseServiceImpl<OpinionInfo>
if (StrUtil.isBlank(row.getMasterUnitName())) { if (StrUtil.isBlank(row.getMasterUnitName())) {
errors.add("主办单位不能为空"); errors.add("主办单位不能为空");
} else { } else {
masterUnit = fetchUndertake(row.getMasterUnitName()); masterUnits = parseUnits(row.getMasterUnitName(), "主办", errors);
} }
if (StrUtil.isNotBlank(row.getMasterUnitName()) && masterUnit == null) { if (StrUtil.isNotBlank(row.getMasterUnitName()) && masterUnits.isEmpty() && errors.isEmpty()) {
errors.add("主办单位不存在"); errors.add("主办单位不能为空");
} }
slaveUnits = parseSlaveUnits(row.getSlaveUnitNames(), errors); slaveUnits = parseUnits(row.getSlaveUnitNames(), "协办", errors);
if (masterUnit != null) { Set<String> masterIds = masterUnits.stream().map(ProposalUndertake::getId).collect(Collectors.toSet());
String masterUnitId = masterUnit.getId(); if (slaveUnits.stream().anyMatch(unit -> masterIds.contains(unit.getId()))) {
if (slaveUnits.stream().anyMatch(unit -> StrUtil.equals(unit.getId(), masterUnitId))) { errors.add("协办单位不能包含主办单位");
errors.add("协办单位不能包含主办单位");
}
} }
} }
@@ -182,11 +361,12 @@ public class OpinionSchoolAuditServiceImpl extends BaseServiceImpl<OpinionInfo>
return; 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, 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"); String opinionId = taskInfo.getString("opinionId");
Long taskId = taskInfo.getLong("taskId"); Long taskId = taskInfo.getLong("taskId");
String taskName = taskInfo.getString("taskName"); String taskName = taskInfo.getString("taskName");
@@ -207,8 +387,11 @@ public class OpinionSchoolAuditServiceImpl extends BaseServiceImpl<OpinionInfo>
// 通过审核时与页面单条审核保持一致先保存意见类别再提交主办协办等流程表单字段 // 通过审核时与页面单条审核保持一致先保存意见类别再提交主办协办等流程表单字段
updateOpinionType(opinionId, type.getId()); updateOpinionType(opinionId, type.getId());
taskData.set("typeId", 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> slaveUnitIds = slaveUnits.stream().map(ProposalUndertake::getId).collect(Collectors.toList());
List<String> slaveUnitNames = slaveUnits.stream().map(ProposalUndertake::getName).collect(Collectors.toList()); List<String> slaveUnitNames = slaveUnits.stream().map(ProposalUndertake::getName).collect(Collectors.toList());
taskData.set("tf_slaveUnitIds", slaveUnitIds); taskData.set("tf_slaveUnitIds", slaveUnitIds);
@@ -243,6 +426,7 @@ public class OpinionSchoolAuditServiceImpl extends BaseServiceImpl<OpinionInfo>
return sql.getObject(Record.class); return sql.getObject(Record.class);
} }
@Aop(TransAop.READ_COMMITTED)
private void updateOpinionType(String opinionId, Integer typeId) { private void updateOpinionType(String opinionId, Integer typeId) {
dao().update(OpinionInfo.class, Chain.make("typeId", typeId), Cnd.where("id", "=", opinionId)); 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; 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<>(); List<ProposalUndertake> units = new ArrayList<>();
if (StrUtil.isBlank(slaveUnitText)) { if (StrUtil.isBlank(slaveUnitText)) {
return units; return units;
} }
String[] items = slaveUnitText.split("[,,、;\\n\\r]+"); String[] items = slaveUnitText.split("");
Set<String> names = new HashSet<>(); Set<String> names = new HashSet<>();
for (String item : items) { for (String item : items) {
String unitName = StrUtil.trim(item); String unitName = StrUtil.trim(item);
if (StrUtil.isBlank(unitName)) { if (StrUtil.isBlank(unitName)) {
continue; continue;
} }
if (!names.add(unitName)) {
errors.add("协办单位重复:" + unitName);
continue;
}
ProposalUndertake undertake = fetchUndertake(unitName); ProposalUndertake undertake = fetchUndertake(unitName);
if (undertake == null) { if (undertake == null) {
errors.add("协办单位不存在:" + unitName); errors.add(label + "单位不存在:" + unitName);
continue;
}
if (!names.add(undertake.getId())) {
errors.add(label + "单位重复:" + unitName);
continue; continue;
} }
units.add(undertake); units.add(undertake);
@@ -16,19 +16,17 @@ import com.budwk.app.base.service.BaseService;
import com.budwk.app.flow.enums.ProcessTaskStateEnum; import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.flow.service.ProcessTaskService; import com.budwk.app.flow.service.ProcessTaskService;
import com.budwk.app.sys.models.Sys_role; 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.models.Sys_user_role;
import com.budwk.app.sys.services.SysRoleService; import com.budwk.app.sys.services.SysRoleService;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalConfig; 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.models.ProposalUndertake;
import com.budwk.app.zhgh.democratic.proposal.service.ProposalUndertakeService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.dao.Sqls; import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql; import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.aop.Aop; import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
@@ -61,6 +59,8 @@ public class ProposalConfigUnitController {
private SysRoleService sysRoleService; private SysRoleService sysRoleService;
@Inject @Inject
private ProcessTaskService processTaskService; private ProcessTaskService processTaskService;
@Inject
private ProposalUndertakeService proposalUndertakeService;
@At("") @At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/config/unit/index.html") @Ok("beetl:/platform/zhgh/democratic/proposal/config/unit/index.html")
@@ -115,39 +115,17 @@ public class ProposalConfigUnitController {
} }
/** /**
* 同步系统单位 * 同步系统单位
* *
* @return * <p>无入参同步系统部门单位的编码名称和启用状态
* 返回值为 Resultcode 0 表示同步成功msg 为操作结果说明</p>
*/ */
@At @At
@SaCheckPermission("proposal.config.unit") @SaCheckPermission("proposal.config.unit")
@SLog(tag = "提案", msg = "承办单位同步系统单位") @SLog(tag = "提案", msg = "承办单位同步系统单位")
@Aop(TransAop.READ_COMMITTED)
public Result syncSysUnit() { public Result syncSysUnit() {
Sql sql = Sqls.create("select code from proposal_undertake"); proposalUndertakeService.syncSysUnits();
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);
}
return Result.success(); return Result.success();
} }
@@ -2,14 +2,21 @@ package com.budwk.app.zhgh.democratic.proposal.controller.query;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result; import com.budwk.app.base.result.Result;
import com.budwk.app.flow.engine.util.FlowUtil; import com.budwk.app.flow.engine.util.FlowUtil;
import com.budwk.app.flow.entity.ProcessTask; import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessTaskStateEnum; import com.budwk.app.flow.enums.ProcessTaskStateEnum;
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.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.proposal.controller.vo.ProposalUnitReplyProposalVO; import com.budwk.app.zhgh.democratic.proposal.controller.vo.ProposalUnitReplyProposalVO;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalConfig; import com.budwk.app.zhgh.democratic.proposal.models.ProposalConfig;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalReplyUnit; import com.budwk.app.zhgh.democratic.proposal.models.ProposalReplyUnit;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSearchParam; import com.budwk.app.zhgh.democratic.proposal.param.ProposalSearchParam;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService; import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
@@ -40,6 +47,8 @@ public class ProposalQueryUnitReplyController {
private Dao dao; private Dao dao;
@Inject @Inject
private ProposalCommonService proposalCommonService; private ProposalCommonService proposalCommonService;
@Inject
private SysRoleService sysRoleService;
@At("") @At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/query/underTakeReply/index.html") @Ok("beetl:/platform/zhgh/democratic/proposal/query/underTakeReply/index.html")
@@ -54,6 +63,19 @@ public class ProposalQueryUnitReplyController {
// 提案配置 协办是否需要答复 // 提案配置 协办是否需要答复
ProposalConfig proposalConfig = dao.fetch(ProposalConfig.class, Cnd.NEW()); ProposalConfig proposalConfig = dao.fetch(ProposalConfig.class, Cnd.NEW());
boolean slaveNeedReply = proposalConfig.getSlaveUnitNeedReply(); boolean slaveNeedReply = proposalConfig.getSlaveUnitNeedReply();
boolean proposalAdmin = isProposalAdmin();
if (!proposalAdmin) {
List<ProposalUndertake> manageUndertakes = getManageUndertakes();
List<String> manageUndertakeIds = manageUndertakes.stream().map(ProposalUndertake::getId).toList();
if (manageUndertakeIds.isEmpty()) {
return Result.success().addData(Map.of("tableData", Collections.EMPTY_LIST, "slaveNeedReply", slaveNeedReply));
}
// 非超级管理员提案管理员时统计范围固定为当前用户角色绑定的承办单位避免前端传其他单位越权查询
if (undertakeUnitId != null && !undertakeUnitId.isBlank() && !manageUndertakeIds.contains(undertakeUnitId)) {
return Result.success().addData(Map.of("tableData", Collections.EMPTY_LIST, "slaveNeedReply", slaveNeedReply));
}
}
// 当前届次所有的提案ID // 当前届次所有的提案ID
Sql sql = Sqls.create("select id from proposal_info where sessionId = @sessionId").setParam("sessionId", sessionId); Sql sql = Sqls.create("select id from proposal_info where sessionId = @sessionId").setParam("sessionId", sessionId);
@@ -75,7 +97,16 @@ public class ProposalQueryUnitReplyController {
// 提案委员会立案分配的承办单位 // 提案委员会立案分配的承办单位
Cnd cnd = Cnd.where(ProposalReplyUnit::getProposalId, "in", proposalIds); Cnd cnd = Cnd.where(ProposalReplyUnit::getProposalId, "in", proposalIds);
cnd.andEX(ProposalReplyUnit::getUnitId, "=", undertakeUnitId); if (proposalAdmin) {
cnd.andEX(ProposalReplyUnit::getUnitId, "=", undertakeUnitId);
} else {
List<String> manageUndertakeIds = getManageUndertakes().stream().map(ProposalUndertake::getId).toList();
if (undertakeUnitId != null && !undertakeUnitId.isBlank()) {
cnd.and(ProposalReplyUnit::getUnitId, "=", undertakeUnitId);
} else {
cnd.and(ProposalReplyUnit::getUnitId, "in", manageUndertakeIds);
}
}
List<ProposalReplyUnit> replyUnits = dao.query(ProposalReplyUnit.class, cnd); List<ProposalReplyUnit> replyUnits = dao.query(ProposalReplyUnit.class, cnd);
// 承办单位答复记录 // 承办单位答复记录
@@ -142,6 +173,14 @@ public class ProposalQueryUnitReplyController {
@SaCheckPermission("proposal.query.unitReply") @SaCheckPermission("proposal.query.unitReply")
@ApiOperation("承办单位提案明细") @ApiOperation("承办单位提案明细")
public Result getProposalsByUnit(@Valid ProposalSearchParam pageForm, String unitCode) { public Result getProposalsByUnit(@Valid ProposalSearchParam pageForm, String unitCode) {
if (!isProposalAdmin()) {
List<ProposalUndertake> manageUndertakes = getManageUndertakes();
List<String> manageUnitCodes = manageUndertakes.stream().map(ProposalUndertake::getCode).toList();
if (manageUnitCodes.isEmpty() || !manageUnitCodes.contains(unitCode)) {
return Result.success(new Pagination(pageForm.getPageNumber(), pageForm.getPageSize(), 0, Collections.emptyList()));
}
}
Sql sql = Sqls.create(""" Sql sql = Sqls.create("""
SELECT SELECT
info.id, info.id,
@@ -167,6 +206,10 @@ public class ProposalQueryUnitReplyController {
Cnd cnd = Cnd.NEW(); Cnd cnd = Cnd.NEW();
// 明细弹窗按当前届次和点击的承办单位编码查询返回该单位主办协办的提案列表 // 明细弹窗按当前届次和点击的承办单位编码查询返回该单位主办协办的提案列表
cnd.andEX("pru.unitCode", "=", unitCode); cnd.andEX("pru.unitCode", "=", unitCode);
if (!isProposalAdmin()) {
List<String> manageUndertakeIds = getManageUndertakes().stream().map(ProposalUndertake::getId).toList();
cnd.and("pru.unitId", "in", manageUndertakeIds);
}
ProposalSearchParam.buildSearch(cnd, pageForm); ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("info.id", "pru.unitName", "pru.isMaster"); cnd.groupBy("info.id", "pru.unitName", "pru.isMaster");
sql.setCondition(cnd); sql.setCondition(cnd);
@@ -176,4 +219,45 @@ public class ProposalQueryUnitReplyController {
pagination.setList(list); pagination.setList(list);
return Result.success(pagination); return Result.success(pagination);
} }
/**
* 判断当前用户是否拥有本页面全量统计权限
*
* 角色说明
* 1. SYSADMIN 为超级管理员可查看全部承办单位
* 2. SCHOOL_UNION_PROPOSAL_ADMIN 为校工会提案管理员可查看全部承办单位
* 3. 其他用户只能查看自己角色绑定的承办单位
*
* @return booleantrue 表示可查看全部承办单位false 表示需要限制到当前用户分管承办单位
*/
private boolean isProposalAdmin() {
return AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_PROPOSAL_ADMIN.name());
}
/**
* 获取当前用户角色绑定的提案承办单位
*
* 参数无参数方法内部根据当前登录用户和 PROPOSAL_UNIT_LEADER 角色查询 sys_user_role.underTakeId
* 处理逻辑用户角色表中的 underTakeId 表示该用户负责的提案承办单位不按教代会届次过滤
* 返回值List<ProposalUndertake>表示当前用户角色绑定的承办单位列表没有配置时返回空列表
*/
private List<ProposalUndertake> getManageUndertakes() {
Sys_role role = sysRoleService.getByCode(RoleConstant.PROPOSAL_UNIT_LEADER.name());
if (role == null) {
return Collections.emptyList();
}
List<Sys_user_role> userRoles = dao.query(Sys_user_role.class, Cnd.where(Sys_user_role::getRoleId, "=", role.getId())
.and(Sys_user_role::getUserId, "=", SecurityUtil.getUserId())
.and(Sys_user_role::getUnderTakeId, "is not", null));
List<String> undertakeIds = userRoles.stream()
.map(Sys_user_role::getUnderTakeId)
.filter(Objects::nonNull)
.filter(id -> !id.isBlank())
.distinct()
.toList();
if (undertakeIds.isEmpty()) {
return Collections.emptyList();
}
return dao.query(ProposalUndertake.class, Cnd.where(ProposalUndertake::getId, "in", undertakeIds));
}
} }
@@ -201,6 +201,7 @@ public class ProposalUnderTakeReplyController {
@At @At
@SaCheckPermission("proposal.unitReply") @SaCheckPermission("proposal.unitReply")
@ApiOperation("执行任务") @ApiOperation("执行任务")
@Aop(TransAop.READ_COMMITTED)
public Result executeTask(@Param("data") String data) { public Result executeTask(@Param("data") String data) {
Dict args = Json.fromJson(Dict.class, data); Dict args = Json.fromJson(Dict.class, data);
String proposalId = args.getStr("proposalId"); String proposalId = args.getStr("proposalId");
@@ -0,0 +1,41 @@
package com.budwk.app.zhgh.democratic.proposal.controller.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 承办单位办理统计中的提案明细
*/
@Data
public class ProposalUnitReplyProposalVO {
@ApiModelProperty("主键")
private String id;
@ApiModelProperty("提案编号")
private String code;
@ApiModelProperty("提案名称")
private String name;
@ApiModelProperty("提案人姓名")
private String createUserName;
@ApiModelProperty("提案类型名称")
private String typeName;
@ApiModelProperty("届次")
private String sessionName;
@ApiModelProperty("代表团")
private String delegationName;
@ApiModelProperty("承办单位名称")
private String underTakeUnitName;
@ApiModelProperty("承办类型,主办或协办")
private String underTakeType;
@ApiModelProperty("当前流程节点")
private String curTaskName;
}
@@ -3,7 +3,6 @@ package com.budwk.app.zhgh.democratic.proposal.interceptor;
import cn.hutool.core.lang.Dict; import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; 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.constant.FlowConst;
import com.budwk.app.flow.engine.FlowInterceptor; import com.budwk.app.flow.engine.FlowInterceptor;
import com.budwk.app.flow.engine.core.Execution; 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.engine.util.FlowUtil;
import com.budwk.app.flow.entity.ProcessInstance; import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask; 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.ProposalInfo;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalReplyUnit; 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.models.ProposalUndertake;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain; import org.nutz.dao.Chain;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop; import org.nutz.ioc.aop.Aop;
import org.nutz.json.Json; import org.nutz.json.Json;
import org.nutz.lang.util.NutMap;
import java.util.List; import java.util.List;
@@ -36,6 +32,7 @@ public class ProposalCaseFilingInterceptor implements FlowInterceptor {
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public void intercept(Execution execution) { public void intercept(Execution execution) {
Dao dao = ServiceContext.find(Dao.class); Dao dao = ServiceContext.find(Dao.class);
ProposalCommonService proposalCommonService = ServiceContext.find(ProposalCommonService.class);
// 提案ID // 提案ID
String proposalId = execution.getProcessInstance().getBusinessNo(); String proposalId = execution.getProcessInstance().getBusinessNo();
@@ -87,10 +84,8 @@ public class ProposalCaseFilingInterceptor implements FlowInterceptor {
List<ProcessTask> processTaskList = execution.getProcessTaskList(); List<ProcessTask> processTaskList = execution.getProcessTaskList();
for (ProcessTask task : processTaskList) { for (ProcessTask task : processTaskList) {
// 办理人 // 办理人的角色承办单位匹配系统所属单位可能与提案承办单位不同
ProcessTaskActor taskActor = dao.fetch(ProcessTaskActor.class, Cnd.where(ProcessTaskActor::getProcessTaskId, "=", task.getId())); ProposalReplyUnit replyUnit = proposalCommonService.getReplyUnitByTaskActorRole(proposalId, task.getId(), task.getTaskName());
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()); Dict variable = FlowUtil.variableToDict(task.getVariable());
variable.set("underTakeName", replyUnit.getUnitName()); variable.set("underTakeName", replyUnit.getUnitName());
@@ -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.core.ServiceContext;
import com.budwk.app.flow.engine.util.FlowUtil; import com.budwk.app.flow.engine.util.FlowUtil;
import com.budwk.app.flow.entity.ProcessTask; import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.entity.ProcessTaskActor;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalReplyUnit; 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.dao.Dao;
import org.nutz.ioc.aop.Aop;
import org.nutz.json.Json; import org.nutz.json.Json;
import java.util.List; import java.util.List;
@@ -20,26 +21,43 @@ import java.util.List;
*/ */
public class ProposalUnderTakeInfoInterceptor implements FlowInterceptor { public class ProposalUnderTakeInfoInterceptor implements FlowInterceptor {
@Override @Override
@Aop(TransAop.READ_COMMITTED)
public void intercept(Execution execution) { public void intercept(Execution execution) {
Dao dao = ServiceContext.find(Dao.class); Dao dao = ServiceContext.find(Dao.class);
ProposalCommonService proposalCommonService = ServiceContext.find(ProposalCommonService.class);
// 提案ID // 提案ID
String proposalId = execution.getProcessInstance().getBusinessNo(); 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(); List<ProcessTask> processTaskList = execution.getProcessTaskList();
for (ProcessTask task : processTaskList) { for (ProcessTask task : processTaskList) {
// 办理人 setReplyUnitVariable(proposalCommonService, proposalId, task);
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));
} }
dao.update(processTaskList,"variable"); 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));
}
} }
@@ -7,8 +7,10 @@ import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask; import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessEventTypeEnum; import com.budwk.app.flow.enums.ProcessEventTypeEnum;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalReplyUnit; 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.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
@@ -22,10 +24,21 @@ public class ProposalCaseTaskRevokeEventListener implements ProcessEventListener
private Dao dao; private Dao dao;
@Override @Override
@Aop(TransAop.READ_COMMITTED)
public void onEvent(ProcessEvent event) { public void onEvent(ProcessEvent event) {
if (event.getEventType() == ProcessEventTypeEnum.PROCESS_TASK_REVOKE) { if (event.getEventType() == ProcessEventTypeEnum.PROCESS_TASK_REVOKE) {
Long taskId = event.getSourceId(); Long taskId = event.getSourceId();
ProcessTask task = dao.fetch(ProcessTask.class, taskId); 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()); ProcessInstance instance = dao.fetch(ProcessInstance.class, task.getProcessInstanceId());
String businessNo = instance.getBusinessNo(); String businessNo = instance.getBusinessNo();
dao.clear(ProposalReplyUnit.class, Cnd.where(ProposalReplyUnit::getProposalId, "=", businessNo)); dao.clear(ProposalReplyUnit.class, Cnd.where(ProposalReplyUnit::getProposalId, "=", businessNo));
@@ -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();
}
@@ -2,6 +2,7 @@ package com.budwk.app.zhgh.democratic.proposal.service.common;
import com.budwk.app.base.service.BaseService; import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo; 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.models.ProposalUndertake;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
@@ -11,6 +12,16 @@ import java.util.List;
public interface ProposalCommonService extends BaseService<ProposalInfo> { public interface ProposalCommonService extends BaseService<ProposalInfo> {
/**
* 根据任务办理人的提案承办单位领导角色匹配提案承办单位
*
* @param proposalId 提案 ID对应 proposal_reply_unit.proposalId
* @param taskId 流程任务 ID用于获取该任务的全部办理人
* @param taskName 任务节点名称master_replyopinion_master_reply 匹配主办单位slave_reply 匹配协办单位
* @return 当前任务对应的 ProposalReplyUnit包含承办单位 ID名称及主协办类型
*/
ProposalReplyUnit getReplyUnitByTaskActorRole(String proposalId, Long taskId, String taskName);
/** /**
* 查询提案信息 * 查询提案信息
*/ */
@@ -15,6 +15,7 @@ import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.flow.engine.util.FlowUtil; import com.budwk.app.flow.engine.util.FlowUtil;
import com.budwk.app.flow.entity.ProcessInstance; import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask; 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.enums.ProcessTaskStateEnum;
import com.budwk.app.flow.vo.ProcessTaskVO; import com.budwk.app.flow.vo.ProcessTaskVO;
import com.budwk.app.sys.models.Sys_dict; 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.ProposalConsolidation;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo; 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.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.proposal.models.ProposalUndertake;
import com.budwk.app.zhgh.democratic.teachercongress.prepare.models.Teacher_congress_session; import com.budwk.app.zhgh.democratic.teachercongress.prepare.models.Teacher_congress_session;
import com.deepoove.poi.XWPFTemplate; import com.deepoove.poi.XWPFTemplate;
@@ -74,6 +76,76 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
super(dao); super(dao);
} }
/**
* 根据任务办理人的承办单位角色确定当前提案的主办或协办单位
*
* @param proposalId 提案 ID不能为空
* @param taskId 流程任务 ID不能为空且任务必须存在办理人
* @param taskName master_replyopinion_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 @Override
public NutMap info(String id) { public NutMap info(String id) {
if (StrUtil.isBlank(id)) { if (StrUtil.isBlank(id)) {
@@ -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);
}
}
}
@@ -1,17 +1,24 @@
package com.budwk.app.zhgh.staffmanage.member.controller.change; package com.budwk.app.zhgh.staffmanage.member.controller.change;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result; import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.zhgh.staffmanage.member.param.pageform.MemberManagePageForm; import com.budwk.app.zhgh.staffmanage.member.param.pageform.MemberManagePageForm;
import com.budwk.app.zhgh.staffmanage.member.service.MemberCommonService; import com.budwk.app.zhgh.staffmanage.member.service.MemberCommonService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Workbook;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Ok;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid; import javax.validation.Valid;
import java.util.List; import java.util.List;
@@ -44,6 +51,29 @@ public class MemberChangeRecordController {
return Result.success(pagination); return Result.success(pagination);
} }
/**
* 导出变更记录列表导出的字段和数据范围与当前页面查询结果保持一致
*
* @param pageForm 当前页面查询条件和显示字段
* @param response 文件下载响应
*/
@At
@Ok("void")
@SaCheckPermission("member.change.record")
public void doExport(MemberManagePageForm pageForm, HttpServletResponse response) {
try {
// 导出列由前端当前表格列配置决定额外补充页面固定展示的异动信息列
List<ExcelExportEntity> exportEntities = memberCommonService.buildMemberHistoryRecordExportEntities(pageForm.getColumns());
List<NutMap> list = memberCommonService.memberHistoryRecordExportData(pageForm);
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, exportEntities, list);
CommonDownloadUtil.download("会员变更记录.xlsx", workbook, response);
} catch (Exception e) {
log.error("导出会员变更记录失败", e);
}
}
@At @At
@SaCheckPermission("member.change.record") @SaCheckPermission("member.change.record")
@@ -5,8 +5,11 @@ import cn.dev33.satoken.annotation.SaMode;
import cn.hutool.core.util.DesensitizedUtil; import cn.hutool.core.util.DesensitizedUtil;
import com.alibaba.excel.EasyExcel; import com.alibaba.excel.EasyExcel;
import com.budwk.app.base.annotation.SLog; 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.page.Pagination;
import com.budwk.app.base.result.Result; 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.models.MemberHistory;
import com.budwk.app.zhgh.staffmanage.member.param.pageform.MemberInfoPageForm; import com.budwk.app.zhgh.staffmanage.member.param.pageform.MemberInfoPageForm;
import com.budwk.app.zhgh.staffmanage.member.service.MemberInfoService; import com.budwk.app.zhgh.staffmanage.member.service.MemberInfoService;
@@ -30,6 +33,7 @@ import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid; import javax.validation.Valid;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
/** /**
* @version 1.0 * @version 1.0
@@ -47,6 +51,9 @@ public class MemberInfoGroupController {
@Inject @Inject
private MemberInfoService memberInfoService; private MemberInfoService memberInfoService;
@Inject
private SysDataUserPullService sysUserPullService;
@At("") @At("")
@Ok("beetl:/platform/zhgh/staffmanage/member/info/group/index.html") @Ok("beetl:/platform/zhgh/staffmanage/member/info/group/index.html")
@SaCheckPermission("member.info.group") @SaCheckPermission("member.info.group")
@@ -79,6 +86,28 @@ public class MemberInfoGroupController {
return Result.success(memberInfoService.dao().count(MemberHistory.class, Cnd.where("year", "=", year))); 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 @At
@SaCheckPermission("member.info.group") @SaCheckPermission("member.info.group")
@@ -14,6 +14,7 @@ import org.nutz.dao.util.cri.Static;
import org.nutz.lang.Lang; import org.nutz.lang.Lang;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* @version 1.0 * @version 1.0
@@ -59,6 +60,11 @@ public class MemberManagePageForm extends PageForm {
//变更来源 //变更来源
private List<Integer> changeOrigins; private List<Integer> changeOrigins;
/**
* 页面当前显示的列表字段导出时按照该字段顺序动态生成表头
*/
private List<Map<String, String>> columns;
public static void buildSearch(Cnd cnd, MemberManagePageForm pageForm) { public static void buildSearch(Cnd cnd, MemberManagePageForm pageForm) {
if (cnd == null) { if (cnd == null) {
cnd = Cnd.NEW(); cnd = Cnd.NEW();
@@ -44,6 +44,9 @@ public class MemberStatisticsPageForm extends PageForm {
private List<String> preparedBys; private List<String> preparedBys;
@ApiModelProperty("编制信息")
private List<String> compilationInformations;
private List<String> userStates; private List<String> userStates;
private List<String> memberStatus; private List<String> memberStatus;
@@ -1,5 +1,6 @@
package com.budwk.app.zhgh.staffmanage.member.service; package com.budwk.app.zhgh.staffmanage.member.service;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.BaseService; import com.budwk.app.base.service.BaseService;
import com.budwk.app.sys.models.Sys_user; import com.budwk.app.sys.models.Sys_user;
@@ -43,6 +44,22 @@ public interface MemberCommonService extends BaseService<Sys_user> {
*/ */
Pagination memberHistoryRecords(MemberManagePageForm pageForm); Pagination memberHistoryRecords(MemberManagePageForm pageForm);
/**
* 根据变更记录页面查询条件获取全部导出数据导出数据与列表查询口径保持一致
*
* @param pageForm 变更记录查询参数
* @return 变更记录导出数据
*/
List<NutMap> memberHistoryRecordExportData(MemberManagePageForm pageForm);
/**
* 根据前端当前列表列配置生成导出列确保页面展示什么字段就导出什么字段
*
* @param columns 页面列配置
* @return Excel 导出列配置
*/
List<ExcelExportEntity> buildMemberHistoryRecordExportEntities(List<Map<String, String>> columns);
/** /**
* 获取可变更字段 * 获取可变更字段
@@ -68,4 +68,12 @@ public interface MemberInfoService extends BaseService<Sys_user> {
* @param isFlag 是否清空更新 * @param isFlag 是否清空更新
*/ */
NutMap handlingMemberImport(TempFile file, Boolean isFlag); NutMap handlingMemberImport(TempFile file, Boolean isFlag);
/**
* 随机查询当前权限范围内手机号为空的会员工号
*
* @param limit 查询人数上限
* @return 会员工号集合
*/
List<String> queryRandomBlankMobileMemberJobNos(int limit);
} }
@@ -113,6 +113,34 @@ public class MemberInfoServiceImpl extends BaseServiceImpl<Sys_user> implements
return sql; return sql;
} }
/**
* 随机查询当前权限范围内手机号为空的会员工号供批量补手机号功能使用
*/
@Override
public List<String> queryRandomBlankMobileMemberJobNos(int limit) {
Cnd cnd = Cnd.NEW();
cnd.and("u.member", "=", true);
cnd.and("u.loginname", "<>", "");
cnd.and(new SqlExpressionGroup()
.or("u.mobile", "IS", null)
.or("u.mobile", "=", ""));
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
cnd.and("u.unionId", "=", SecurityUtil.getUnionId());
}
Sql sql = Sqls.create("""
SELECT DISTINCT u.loginname
FROM vw_user u
$condition
ORDER BY RAND()
LIMIT @limit
""");
sql.setCondition(cnd);
sql.setParam("limit", limit);
sql.setCallback(Sqls.callback.strList());
dao().execute(sql);
return sql.getList(String.class);
}
@Override @Override
public Sql getMemberHistorySql(MemberInfoPageForm pageForm) { public Sql getMemberHistorySql(MemberInfoPageForm pageForm) {
@@ -1,11 +1,13 @@
package com.budwk.app.zhgh.staffmanage.member.service.impl; package com.budwk.app.zhgh.staffmanage.member.service.impl;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil; import cn.hutool.http.HtmlUtil;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.constant.RoleConstant; import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException; import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
@@ -139,6 +141,54 @@ public class MemberManageServiceImpl extends BaseServiceImpl<Sys_user> implement
@Override @Override
public Pagination memberHistoryRecords(MemberManagePageForm pageForm) { public Pagination memberHistoryRecords(MemberManagePageForm pageForm) {
Sql sql = getMemberHistoryRecordSql(pageForm);
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
}
/**
* 根据变更记录查询条件获取全部数据用于页面导出保证导出数据与列表查询口径一致
*
* @param pageForm 变更记录查询参数
* @return 已转换异动类型显示文本的导出数据
*/
@Override
public List<NutMap> memberHistoryRecordExportData(MemberManagePageForm pageForm) {
List<NutMap> list = listMap(getMemberHistoryRecordSql(pageForm));
Map<String, String> changeTypeNameMap = Arrays.stream(MemberChangeType.values())
.collect(Collectors.toMap(MemberChangeType::getCode, MemberChangeType::getChangeTypeName));
int[] no = {1};
list.forEach(row -> {
row.put("no", no[0]++);
row.put("changeTypesText", getChangeTypesText(row.get("changeTypes"), changeTypeNameMap));
});
return list;
}
/**
* 根据页面表格列生成导出列并补充页面分组展示的更新时间异动类型两列
*
* @param columns 页面当前显示的基础列
* @return Excel 导出列配置
*/
@Override
public List<ExcelExportEntity> buildMemberHistoryRecordExportEntities(List<Map<String, String>> columns) {
List<ExcelExportEntity> exportEntities = new ArrayList<>();
exportEntities.add(new ExcelExportEntity("序号", "no", 10));
if (Lang.isNotEmpty(columns)) {
columns.forEach(column -> exportEntities.add(new ExcelExportEntity(column.get("label"), column.get("prop"), 20)));
}
exportEntities.add(new ExcelExportEntity("更新时间", "changeTime", 25));
exportEntities.add(new ExcelExportEntity("异动类型", "changeTypesText", 30));
return exportEntities;
}
/**
* 构建变更记录列表 SQL分页查询和导出查询共用该方法避免筛选条件不一致
*
* @param pageForm 变更记录查询参数
* @return 变更记录查询 SQL
*/
private Sql getMemberHistoryRecordSql(MemberManagePageForm pageForm) {
Sql sql = Sqls.create(""" Sql sql = Sqls.create("""
SELECT SELECT
his.id, his.id,
@@ -206,7 +256,31 @@ public class MemberManageServiceImpl extends BaseServiceImpl<Sys_user> implement
cnd.andEX("his.unionId", "=", SecurityUtil.getUnionId()); cnd.andEX("his.unionId", "=", SecurityUtil.getUnionId());
} }
sql.setCondition(cnd); sql.setCondition(cnd);
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql); return sql;
}
/**
* 将数据库保存的异动类型 JSON 数组转换为页面展示的中文名称供导出 Excel 使用
*
* @param changeTypes 数据库中的异动类型值
* @param changeTypeNameMap 异动类型枚举名称映射
* @return 逗号分隔的异动类型名称
*/
private String getChangeTypesText(Object changeTypes, Map<String, String> changeTypeNameMap) {
if (changeTypes == null) {
return null;
}
List<String> changeTypeList;
if (changeTypes instanceof Collection<?> collection) {
changeTypeList = collection.stream().map(String::valueOf).toList();
} else if (StrUtil.isNotBlank(String.valueOf(changeTypes))) {
changeTypeList = JSONUtil.parseArray(String.valueOf(changeTypes)).toList(String.class);
} else {
return null;
}
return changeTypeList.stream()
.map(type -> changeTypeNameMap.getOrDefault(type, type))
.collect(Collectors.joining(","));
} }
@@ -90,6 +90,8 @@ public class MemberStatisticsServiceImpl extends BaseServiceImpl<Sys_user> imple
handleField("unitId", pageForm.getUnitId(), reverseSelection, IN_OR_NIN_OP, cnd); handleField("unitId", pageForm.getUnitId(), reverseSelection, IN_OR_NIN_OP, cnd);
handleField("personType", pageForm.getPersonTypes(), reverseSelection, IN_OR_NIN_OP, cnd); handleField("personType", pageForm.getPersonTypes(), reverseSelection, IN_OR_NIN_OP, cnd);
handleField("preparedBy", pageForm.getPreparedBys(), reverseSelection, IN_OR_NIN_OP, cnd); handleField("preparedBy", pageForm.getPreparedBys(), reverseSelection, IN_OR_NIN_OP, cnd);
// 编制信息前端使用独立字典传值实际对应用户表/视图中的 compilationInformation 字段
handleField("compilationInformation", pageForm.getCompilationInformations(), reverseSelection, IN_OR_NIN_OP, cnd);
handleField("userState", pageForm.getUserStates(), reverseSelection, IN_OR_NIN_OP, cnd); handleField("userState", pageForm.getUserStates(), reverseSelection, IN_OR_NIN_OP, cnd);
handleField("sex", pageForm.getSexTypes(), reverseSelection, IN_OR_NIN_OP, cnd); handleField("sex", pageForm.getSexTypes(), reverseSelection, IN_OR_NIN_OP, cnd);
cnd.andEX("sur.roleId", IN_OR_NIN_OP, pageForm.getRoleIds()); cnd.andEX("sur.roleId", IN_OR_NIN_OP, pageForm.getRoleIds());
@@ -145,6 +147,7 @@ public class MemberStatisticsServiceImpl extends BaseServiceImpl<Sys_user> imple
u.arrivalAtSchoolDate, u.arrivalAtSchoolDate,
u.personType, u.personType,
u.preparedBy, u.preparedBy,
u.compilationInformation,
u.identityType, u.identityType,
u.userState, u.userState,
u.unionName, u.unionName,
@@ -0,0 +1,6 @@
-- 30年教龄疗休养当前时间段图片材料字段。
ALTER TABLE `thirty_teach_tour_user_assignment`
ADD COLUMN `currentPeriodPhotoFiles` text DEFAULT NULL COMMENT '当前时间段图片材料' AFTER `photoFiles`;
ALTER TABLE `thirty_teach_tour_ledger`
ADD COLUMN `currentPeriodPhotoFiles` text DEFAULT NULL COMMENT '当前时间段图片材料' AFTER `photoFiles`;
@@ -40,6 +40,7 @@
'assigneeMode', 'assigneeMode',
'buttonConfig', 'buttonConfig',
'enableNextOperator', 'enableNextOperator',
'countersignGroupBy',
'h5form' 'h5form'
]" ]"
> >
@@ -83,6 +84,19 @@
</template> </template>
<template v-slot="{model,field}"> <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-form-item label="指定下一步处理人">
<el-checkbox v-model="model['enableNextOperator']">指定下一步处理人</el-checkbox> <el-checkbox v-model="model['enableNextOperator']">指定下一步处理人</el-checkbox>
</el-form-item> </el-form-item>
@@ -114,6 +128,7 @@
id: "${id!}", id: "${id!}",
designerData: {}, designerData: {},
flowData: {}, flowData: {},
formLoading: false,
assignmentHandlerClassOptions: [], assignmentHandlerClassOptions: [],
candidateHandlerClassOptions: [] candidateHandlerClassOptions: []
} }
@@ -135,7 +150,16 @@
} }
}, },
methods: { methods: {
// model 为当前节点的插槽表单,value 为所选办理维度;在实例方法内响应式赋值,避免插槽 this 指向错误。
handleCountersignGroupChange(model, value) {
this.$set(model, 'countersignGroupBy', value)
},
handleSave(val) { handleSave(val) {
// 避免连点重复保存;请求结束后统一恢复保存状态。
if (this.formLoading) {
return
}
this.formLoading = true
const design = { const design = {
...this.designerData, ...this.designerData,
content: val.json content: val.json
@@ -146,6 +170,8 @@
} else { } else {
window.parent.postMessage("error") window.parent.postMessage("error")
} }
}).always(() => {
this.formLoading = false
}) })
}, },
getDetail() { getDetail() {
@@ -63,6 +63,7 @@ layout("/layouts/platform.html"){
<el-card shadow="never"> <el-card shadow="never">
<table-tool :label="pageForm.pullTime ? pageForm.pullTime + '数据' : '全部数据'"> <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="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> <el-button type="danger" size="mini" @click="openDelete" icon="el-icon-delete">删除本地数据源</el-button>
</table-tool> </table-tool>
<el-table :key="tableKey" :data="tableData" @sort-change="pageOrder" header-align="center" v-loading="tableLoading"> <el-table :key="tableKey" :data="tableData" @sort-change="pageOrder" header-align="center" v-loading="tableLoading">
@@ -120,7 +121,8 @@ layout("/layouts/platform.html"){
pullTimeOptions: false, pullTimeOptions: false,
sourceTime: [], sourceTime: [],
pullTimeDialogVisible: false, pullTimeDialogVisible: false,
pullLoading: false pullLoading: false,
syncTeacherMobileLoading: false
} }
}, },
methods: { methods: {
@@ -148,6 +150,42 @@ layout("/layouts/platform.html"){
.catch(() => {}) .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() { getSearchOptions() {
this.$axios.post("/platform/sys/data/user/pull/searchOptions").then((resp) => { this.$axios.post("/platform/sys/data/user/pull/searchOptions").then((resp) => {
if (resp.code === 0) { if (resp.code === 0) {
@@ -18,7 +18,7 @@ const UNIT_LEADER_MANAGE_TEMPLATE = {
<table-tool label="人员列表"> <table-tool label="人员列表">
<el-button @click="openAdd" type="primary" size="small">新增</el-button> <el-button @click="openAdd" type="primary" size="small">新增</el-button>
</table-tool> </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"> <el-table-column label="序号" type="index" width="60">
<template v-slot="scope">{{scope.$index + (pageForm.pageNumber - 1) * pageForm.pageSize + 1}}</template> <template v-slot="scope">{{scope.$index + (pageForm.pageNumber - 1) * pageForm.pageSize + 1}}</template>
</el-table-column> </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-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 :model="formData" ref="form" size="small" label-width="60px">
<el-form-item prop="roleCode" label="角色"> <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>
<el-form-item prop="userId" label="人员"> <el-form-item prop="userId" label="人员">
<user-select <user-select
placeholder="请选择人员" placeholder="请选择人员"
style="width: 100%" style="width: 100%"
v-model="formData.userId" :value="formData.userId"
@change="(value) => this.$set(this.formData, 'userId', value)"
api="/platform/sys/unit/listUserSelect" api="/platform/sys/unit/listUserSelect"
:api_params="{ unitId: currentData?.id }" :api_params="{ unitId: currentData?.id }"
:option_label_func=" :option_label_func="
@@ -54,10 +57,11 @@ const UNIT_LEADER_MANAGE_TEMPLATE = {
</el-form-item> </el-form-item>
</el-form> </el-form>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false"> </el-button> <el-button @click="this.$set(this, 'dialogFormVisible', false)" :disabled="formLoading"> </el-button>
<el-button type="primary" @click="doSubmit"> </el-button> <el-button type="primary" :loading="formLoading" @click="doSubmit"> </el-button>
</div> </div>
</el-dialog> </el-dialog>
<slot></slot>
</div> </div>
`, `,
props: { props: {
@@ -70,6 +74,9 @@ const UNIT_LEADER_MANAGE_TEMPLATE = {
return { return {
dialogFormVisible: false, dialogFormVisible: false,
formData: {}, formData: {},
roleOptions: [],
tableLoading: false,
formLoading: false,
pageForm: { pageForm: {
unitName: "", unitName: "",
searchName: "username", searchName: "username",
@@ -87,20 +94,26 @@ const UNIT_LEADER_MANAGE_TEMPLATE = {
computed: {}, computed: {},
methods: { methods: {
doDelete({ userId, roleCode }) { doDelete({ userId, roleCode }) {
this.$confirm("您确定要删除吗?", "提示", { // 删除提案领导会同步取消提案配置中的负责人,操作前明确提示。
const message = roleCode === "PROPOSAL_UNIT_LEADER" ? "删除后将同时取消提案承办单位配置中的该负责人,确定删除吗?" : "您确定要删除吗?"
const unitId = this.currentData.id
this.$confirm(message, "提示", {
confirmButtonText: "确定", confirmButtonText: "确定",
cancelButtonText: "取消", cancelButtonText: "取消",
type: "info" type: "info"
}).then((res) => { }).then((res) => {
this.tableLoading = true
$.post("/platform/sys/unit/deleteUnitUserRole", { $.post("/platform/sys/unit/deleteUnitUserRole", {
userId, userId,
roleCode, roleCode,
unitId: this.currentData.id unitId
}).then((res) => { }).then((res) => {
if (res.code === 0) { if (res.code === 0) {
this.$message.success(res.msg) this.$message.success(res.msg)
this.doSearch() this.doSearch()
} }
}).always(() => {
this.tableLoading = false
}) })
}) })
}, },
@@ -111,6 +124,13 @@ const UNIT_LEADER_MANAGE_TEMPLATE = {
}) })
}, },
doSubmit() { 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", { $.post("/platform/sys/unit/insertUnitUserRole", {
...this.formData, ...this.formData,
unitId: this.currentData.id unitId: this.currentData.id
@@ -120,6 +140,8 @@ const UNIT_LEADER_MANAGE_TEMPLATE = {
this.$message.success(res.msg) this.$message.success(res.msg)
this.doSearch() this.doSearch()
} }
}).always(() => {
this.formLoading = false
}) })
}, },
doSearch() { doSearch() {
@@ -134,17 +156,28 @@ const UNIT_LEADER_MANAGE_TEMPLATE = {
this.pageForm.pageSize = val this.pageForm.pageSize = val
this.pageData() this.pageData()
}, },
// 角色选项由当前业务接口提供,保留字典角色并补充提案领导。
loadRoleOptions() {
this.$axios.post("/platform/sys/unit/unitRoleOptions").then((res) => {
if (res.code === 0) this.roleOptions = res.data
})
},
pageData() { pageData() {
if (!this.currentData?.id) return
this.tableLoading = true
this.pageForm.unitId = this.currentData.id this.pageForm.unitId = this.currentData.id
this.$axios.post(loc() + "/leaderPageData", this.pageForm).then((res) => { this.$axios.post(loc() + "/leaderPageData", this.pageForm).then((res) => {
if (res.code === 0) { if (res.code === 0) {
this.tableData = res.data.list this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount this.pageForm.totalCount = res.data.totalCount
} }
}).finally(() => {
this.tableLoading = false
}) })
} }
}, },
created() { created() {
this.loadRoleOptions()
this.pageData() this.pageData()
} }
} }
@@ -41,7 +41,6 @@ const basicForm = {
<el-col :span="12"> <el-col :span="12">
<el-form-item :rules="{required:true,message: '请选择活动时间', trigger: 'blur'}" label="活动时间" prop="activityTime"> <el-form-item :rules="{required:true,message: '请选择活动时间', trigger: 'blur'}" label="活动时间" prop="activityTime">
<el-date-picker <el-date-picker
:picker-options="pickerOptions"
end-placeholder="请选择活动结束日期" end-placeholder="请选择活动结束日期"
range-separator="至" range-separator="至"
start-placeholder="请选择活动开始日期" start-placeholder="请选择活动开始日期"
@@ -41,7 +41,6 @@ const basicForm = {
<el-col :span="12"> <el-col :span="12">
<el-form-item :rules="{required:true,message: '请选择活动时间', trigger: 'blur'}" label="活动时间" prop="activityTime"> <el-form-item :rules="{required:true,message: '请选择活动时间', trigger: 'blur'}" label="活动时间" prop="activityTime">
<el-date-picker <el-date-picker
:picker-options="pickerOptions"
end-placeholder="请选择活动结束日期" end-placeholder="请选择活动结束日期"
range-separator="至" range-separator="至"
start-placeholder="请选择活动开始日期" start-placeholder="请选择活动开始日期"
@@ -73,7 +73,7 @@ layout("/layouts/platform.html"){
<el-table-column label="工号" prop="loginName" width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column> <el-table-column label="工号" prop="loginName" width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所在单位" prop="unitName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column> <el-table-column label="所在单位" prop="unitName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="线路" prop="lineName" min-width="200" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column> <el-table-column label="线路" prop="lineName" min-width="200" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时间" prop="travelPeriod" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column> <el-table-column label="出行时间" prop="travelPeriod" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时段" prop="periodName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column> <el-table-column label="出行时段" prop="periodName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="乘车地点" prop="boardingPlace" min-width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column> <el-table-column label="乘车地点" prop="boardingPlace" min-width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="已上传图片数量" width="140" align="center" header-align="center"> <el-table-column label="已上传图片数量" width="140" align="center" header-align="center">
@@ -159,7 +159,7 @@ layout("/layouts/platform.html"){
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="18" v-if="assignForm.personType === 'FORMAL'"> <el-col :span="18" v-if="assignForm.personType === 'FORMAL'">
<el-form-item label="出行时间"> <el-form-item label="出行时间">
<div class="period-button-list"> <div class="period-button-list">
<el-button <el-button
v-for="item in assignPeriodOptions" v-for="item in assignPeriodOptions"
@@ -177,6 +177,10 @@ layout("/layouts/platform.html"){
</el-form> </el-form>
<div class="quota-bar"> <div class="quota-bar">
<div class="quota-item quota-item-total">
<span class="quota-label">分工会总名额</span>
<span>{{ quotaInfo.branchTotalQuota }}</span>
</div>
<div class="quota-item"> <div class="quota-item">
<span class="quota-label">正式名额</span> <span class="quota-label">正式名额</span>
<span>{{ quotaInfo.formalQuota }}</span> <span>{{ quotaInfo.formalQuota }}</span>
@@ -424,6 +428,9 @@ layout("/layouts/platform.html"){
color: #303133; color: #303133;
background: #fafafa; background: #fafafa;
} }
.quota-item-total {
min-width: 150px;
}
.quota-label { .quota-label {
font-weight: 600; font-weight: 600;
margin-right: 10px; margin-right: 10px;
@@ -503,6 +510,7 @@ layout("/layouts/platform.html"){
assignMatterOptions: [], assignMatterOptions: [],
assignPeriodOptions: [], assignPeriodOptions: [],
quotaInfo: { quotaInfo: {
branchTotalQuota: 0,
formalQuota: 0, formalQuota: 0,
backupQuota: 0, backupQuota: 0,
formalUsed: 0, formalUsed: 0,
@@ -511,6 +519,7 @@ layout("/layouts/platform.html"){
backupRemaining: 0 backupRemaining: 0
}, },
assignDialogVisible: false, assignDialogVisible: false,
assignOpenToken: 0,
candidateLoading: false, candidateLoading: false,
candidateSelectLoading: false, candidateSelectLoading: false,
candidateData: [], candidateData: [],
@@ -617,6 +626,7 @@ layout("/layouts/platform.html"){
}, },
defaultQuotaInfo() { defaultQuotaInfo() {
return { return {
branchTotalQuota: 0,
formalQuota: 0, formalQuota: 0,
backupQuota: 0, backupQuota: 0,
formalUsed: 0, formalUsed: 0,
@@ -726,21 +736,11 @@ layout("/layouts/platform.html"){
window.location.href = loc() + "/exportData?" + params.toString() window.location.href = loc() + "/exportData?" + params.toString()
}, },
openAssign() { openAssign() {
const token = this.nextAssignOpenToken()
const year = this.pageForm.year || moment().format("YYYY") const year = this.pageForm.year || moment().format("YYYY")
this.assignForm = this.defaultAssignForm(year) this.assignForm = this.defaultAssignForm(year)
this.candidateForm = this.defaultCandidateForm() this.candidateForm = this.defaultCandidateForm()
this.assignPeriodOptions = [] this.assignSettingOptions = []
this.quotaInfo = this.defaultQuotaInfo()
this.candidateData = []
this.selectedCandidates = []
this.selectedCandidateIds = []
this.candidateUserOptions = []
this.assignDialogVisible = true
this.loadAssignSettingOptions()
},
resetAssignDialog() {
this.assignForm = this.defaultAssignForm(moment().format("YYYY"))
this.candidateForm = this.defaultCandidateForm()
this.assignMatterOptions = [] this.assignMatterOptions = []
this.assignPeriodOptions = [] this.assignPeriodOptions = []
this.quotaInfo = this.defaultQuotaInfo() this.quotaInfo = this.defaultQuotaInfo()
@@ -749,8 +749,37 @@ layout("/layouts/platform.html"){
this.selectedCandidateIds = [] this.selectedCandidateIds = []
this.candidateUserOptions = [] this.candidateUserOptions = []
this.assignSubmitting = false this.assignSubmitting = false
this.candidateLoading = false
this.candidateSelectLoading = false
this.assignDialogVisible = true
this.loadAssignSettingOptions(token)
},
resetAssignDialog() {
this.assignOpenToken += 1
this.assignForm = this.defaultAssignForm(moment().format("YYYY"))
this.candidateForm = this.defaultCandidateForm()
this.assignSettingOptions = []
this.assignMatterOptions = []
this.assignPeriodOptions = []
this.quotaInfo = this.defaultQuotaInfo()
this.candidateData = []
this.selectedCandidates = []
this.selectedCandidateIds = []
this.candidateUserOptions = []
this.assignSubmitting = false
this.candidateLoading = false
this.candidateSelectLoading = false
},
// 人员分配弹窗每次打开都生成新的批次标记,旧请求返回时不再覆盖新弹窗数据。
nextAssignOpenToken() {
this.assignOpenToken += 1
return this.assignOpenToken
},
isCurrentAssignOpen(token) {
return this.assignDialogVisible && token === this.assignOpenToken
}, },
assignYearChange() { assignYearChange() {
const token = this.assignOpenToken
this.assignForm.settingId = "" this.assignForm.settingId = ""
this.assignForm.matterId = "" this.assignForm.matterId = ""
this.assignForm.periodId = "" this.assignForm.periodId = ""
@@ -761,9 +790,10 @@ layout("/layouts/platform.html"){
this.candidateUserOptions = [] this.candidateUserOptions = []
this.candidateForm.keyword = "" this.candidateForm.keyword = ""
this.clearCandidateSelection() this.clearCandidateSelection()
this.loadAssignSettingOptions() this.loadAssignSettingOptions(token)
}, },
assignSettingChange() { assignSettingChange() {
const token = this.assignOpenToken
this.assignForm.matterId = "" this.assignForm.matterId = ""
this.assignForm.periodId = "" this.assignForm.periodId = ""
this.assignMatterOptions = [] this.assignMatterOptions = []
@@ -772,30 +802,32 @@ layout("/layouts/platform.html"){
this.candidateUserOptions = [] this.candidateUserOptions = []
this.candidateForm.keyword = "" this.candidateForm.keyword = ""
this.clearCandidateSelection() this.clearCandidateSelection()
this.loadAssignMatterOptions() this.loadAssignMatterOptions(token)
this.loadAssignTravelPeriods(true) this.loadAssignTravelPeriods(token)
this.loadCandidatePageData() this.loadCandidatePageData(token)
}, },
assignPersonTypeChange() { assignPersonTypeChange() {
if (this.assignForm.personType === "BACKUP") { if (this.assignForm.personType === "BACKUP") {
this.assignForm.matterId = "" this.assignForm.matterId = ""
} }
this.loadQuotaInfo() this.loadQuotaInfo(this.assignOpenToken)
}, },
loadAssignSettingOptions() { loadAssignSettingOptions(token) {
token = token || this.assignOpenToken
this.$axios.post(loc() + "/settingOptions", {year: this.assignForm.year}).then((res) => { this.$axios.post(loc() + "/settingOptions", {year: this.assignForm.year}).then((res) => {
if (!this.isCurrentAssignOpen(token)) return
if (res.code === 0) { if (res.code === 0) {
this.assignSettingOptions = res.data || [] this.assignSettingOptions = res.data || []
// 人员分配弹窗独立默认取当前年度第一条配置,再加载名额、线路和候选人员。 // 人员分配弹窗独立默认取当前年度第一条配置,再加载名额、线路和候选人员。
if (!this.assignForm.settingId && this.assignSettingOptions.length > 0) { if (!this.assignForm.settingId && this.assignSettingOptions.length > 0) {
this.assignForm.settingId = this.assignSettingOptions[0].id this.assignForm.settingId = this.assignSettingOptions[0].id
this.loadAssignMatterOptions() this.loadAssignMatterOptions(token)
this.loadAssignTravelPeriods(true) this.loadAssignTravelPeriods(token)
this.loadCandidatePageData() this.loadCandidatePageData(token)
} else if (this.assignForm.settingId) { } else if (this.assignForm.settingId) {
this.loadAssignMatterOptions() this.loadAssignMatterOptions(token)
this.loadAssignTravelPeriods(true) this.loadAssignTravelPeriods(token)
this.loadCandidatePageData() this.loadCandidatePageData(token)
} else { } else {
this.assignMatterOptions = [] this.assignMatterOptions = []
this.assignPeriodOptions = [] this.assignPeriodOptions = []
@@ -806,46 +838,50 @@ layout("/layouts/platform.html"){
} }
}) })
}, },
loadAssignTravelPeriods(defaultFirst) { loadAssignTravelPeriods(token) {
token = token || this.assignOpenToken
if (!this.assignForm.settingId) { if (!this.assignForm.settingId) {
this.assignPeriodOptions = [] this.assignPeriodOptions = []
this.assignForm.periodId = "" this.assignForm.periodId = ""
this.loadQuotaInfo() this.loadQuotaInfo(token)
return return
} }
this.$axios.post(loc() + "/travelPeriodOptions", {settingId: this.assignForm.settingId}).then((res) => { this.$axios.post(loc() + "/travelPeriodOptions", {settingId: this.assignForm.settingId}).then((res) => {
if (!this.isCurrentAssignOpen(token)) return
if (res.code === 0) { if (res.code === 0) {
this.assignPeriodOptions = res.data || [] this.assignPeriodOptions = res.data || []
const hasCurrent = this.assignPeriodOptions.some(item => item.id === this.assignForm.periodId) const hasCurrent = this.assignPeriodOptions.some(item => item.id === this.assignForm.periodId)
if ((defaultFirst || !hasCurrent) && this.assignPeriodOptions.length > 0) { if (!hasCurrent) {
this.assignForm.periodId = this.assignPeriodOptions[0].id
} else if (!hasCurrent) {
this.assignForm.periodId = "" this.assignForm.periodId = ""
} }
} else { } else {
this.assignPeriodOptions = [] this.assignPeriodOptions = []
this.assignForm.periodId = "" this.assignForm.periodId = ""
} }
this.loadQuotaInfo() this.loadQuotaInfo(token)
}) })
}, },
loadAssignMatterOptions() { loadAssignMatterOptions(token) {
token = token || this.assignOpenToken
if (!this.assignForm.settingId) { if (!this.assignForm.settingId) {
this.assignMatterOptions = [] this.assignMatterOptions = []
return return
} }
this.$axios.post(loc() + "/matterOptions", {settingId: this.assignForm.settingId}).then((res) => { this.$axios.post(loc() + "/matterOptions", {settingId: this.assignForm.settingId}).then((res) => {
if (!this.isCurrentAssignOpen(token)) return
if (res.code === 0) { if (res.code === 0) {
this.assignMatterOptions = res.data || [] this.assignMatterOptions = res.data || []
} }
}) })
}, },
loadQuotaInfo() { loadQuotaInfo(token) {
token = token || this.assignOpenToken
if (!this.assignForm.settingId) { if (!this.assignForm.settingId) {
this.quotaInfo = this.defaultQuotaInfo() this.quotaInfo = this.defaultQuotaInfo()
return return
} }
this.$axios.post(loc() + "/quotaInfo", {settingId: this.assignForm.settingId}).then((res) => { this.$axios.post(loc() + "/quotaInfo", {settingId: this.assignForm.settingId}).then((res) => {
if (!this.isCurrentAssignOpen(token)) return
if (res.code === 0) { if (res.code === 0) {
const baseQuota = Object.assign(this.defaultQuotaInfo(), res.data || {}) const baseQuota = Object.assign(this.defaultQuotaInfo(), res.data || {})
if (this.assignForm.personType === "FORMAL") { if (this.assignForm.personType === "FORMAL") {
@@ -855,16 +891,18 @@ layout("/layouts/platform.html"){
} }
this.quotaInfo = baseQuota this.quotaInfo = baseQuota
if (this.assignForm.personType === "FORMAL" && this.assignForm.periodId) { if (this.assignForm.personType === "FORMAL" && this.assignForm.periodId) {
this.loadPeriodQuotaInfo() this.loadPeriodQuotaInfo(token)
} }
} }
}) })
}, },
loadPeriodQuotaInfo() { loadPeriodQuotaInfo(token) {
token = token || this.assignOpenToken
this.$axios.post(loc() + "/periodQuotaInfo", { this.$axios.post(loc() + "/periodQuotaInfo", {
settingId: this.assignForm.settingId, settingId: this.assignForm.settingId,
periodId: this.assignForm.periodId periodId: this.assignForm.periodId
}).then((res) => { }).then((res) => {
if (!this.isCurrentAssignOpen(token)) return
if (res.code === 0) { if (res.code === 0) {
const periodQuota = res.data || {} const periodQuota = res.data || {}
this.quotaInfo = Object.assign({}, this.quotaInfo, { this.quotaInfo = Object.assign({}, this.quotaInfo, {
@@ -876,13 +914,29 @@ layout("/layouts/platform.html"){
}) })
}, },
toggleAssignPeriod(item) { toggleAssignPeriod(item) {
const token = this.assignOpenToken
this.assignForm.periodId = this.assignForm.periodId === item.id ? "" : item.id this.assignForm.periodId = this.assignForm.periodId === item.id ? "" : item.id
// 选择出行时间段时提醒先核对分配批次,避免批次线路时间与时间段不一致。
if (this.assignForm.periodId) {
this.$message({
type: "warning",
message: this.assignPeriodCheckMessage(),
duration: 5000
})
}
this.candidateForm.pageNumber = 1 this.candidateForm.pageNumber = 1
this.clearCandidateSelection() this.clearCandidateSelection()
this.loadQuotaInfo() this.loadQuotaInfo(token)
this.loadCandidatePageData() this.loadCandidatePageData(token)
}, },
loadCandidatePageData() { assignPeriodCheckMessage() {
if (!this.assignForm.matterId) {
return "请选择分配批次,并确认选中的 “分配批次” 与 “出行时间段” 出行时间是否一致"
}
return "请确认选中的 “分配批次” 与 “出行时间段” 出行时间是否一致"
},
loadCandidatePageData(token) {
token = token || this.assignOpenToken
if (!this.assignForm.settingId) { if (!this.assignForm.settingId) {
this.candidateData = [] this.candidateData = []
this.candidateForm.totalCount = 0 this.candidateForm.totalCount = 0
@@ -897,6 +951,7 @@ layout("/layouts/platform.html"){
keyword: (this.selectedCandidateIds || []).length > 0 ? "" : this.candidateForm.keyword, keyword: (this.selectedCandidateIds || []).length > 0 ? "" : this.candidateForm.keyword,
userIds: JSON.stringify(this.selectedCandidateIds || []) userIds: JSON.stringify(this.selectedCandidateIds || [])
}).then((res) => { }).then((res) => {
if (!this.isCurrentAssignOpen(token)) return
if (res.code === 0) { if (res.code === 0) {
const data = res.data || {} const data = res.data || {}
this.candidateData = (data.list || []).map(item => Object.assign({}, item, { this.candidateData = (data.list || []).map(item => Object.assign({}, item, {
@@ -909,7 +964,9 @@ layout("/layouts/platform.html"){
this.$message.warning(res.msg || "候选人员查询失败") this.$message.warning(res.msg || "候选人员查询失败")
} }
}).finally(() => { }).finally(() => {
this.candidateLoading = false if (this.isCurrentAssignOpen(token)) {
this.candidateLoading = false
}
}) })
}, },
candidateSearch() { candidateSearch() {
@@ -948,6 +1005,7 @@ layout("/layouts/platform.html"){
// 人员选择器复用候选人员接口,后端会限定为当前登录人所在分工会会员。 // 人员选择器复用候选人员接口,后端会限定为当前登录人所在分工会会员。
this.candidateForm.keyword = keyword || "" this.candidateForm.keyword = keyword || ""
this.candidateSelectLoading = true this.candidateSelectLoading = true
const token = this.assignOpenToken
this.$axios.post(loc() + "/candidatePageData", { this.$axios.post(loc() + "/candidatePageData", {
pageNumber: 1, pageNumber: 1,
pageSize: 20, pageSize: 20,
@@ -955,12 +1013,15 @@ layout("/layouts/platform.html"){
retiringSoon: this.normalizeCandidateFilterValue(this.candidateForm.retiringSoon), retiringSoon: this.normalizeCandidateFilterValue(this.candidateForm.retiringSoon),
keyword: keyword || "" keyword: keyword || ""
}).then((res) => { }).then((res) => {
if (!this.isCurrentAssignOpen(token)) return
if (res.code === 0) { if (res.code === 0) {
const data = res.data || {} const data = res.data || {}
this.candidateUserOptions = this.mergeOptionLists(this.selectedCandidates, data.list || []) this.candidateUserOptions = this.mergeOptionLists(this.selectedCandidates, data.list || [])
} }
}).finally(() => { }).finally(() => {
this.candidateSelectLoading = false if (this.isCurrentAssignOpen(token)) {
this.candidateSelectLoading = false
}
}) })
}, },
candidateUserSelectChange(userIds) { candidateUserSelectChange(userIds) {
@@ -1149,7 +1210,7 @@ layout("/layouts/platform.html"){
return return
} }
if (this.assignForm.personType === "FORMAL" && !this.assignForm.periodId) { if (this.assignForm.personType === "FORMAL" && !this.assignForm.periodId) {
this.$message.warning("请选择出行时间") this.$message.warning("请选择出行时间")
return return
} }
this.$confirm("确定保存当前人员分配吗?", "提示", { this.$confirm("确定保存当前人员分配吗?", "提示", {
@@ -71,6 +71,7 @@ layout("/layouts/platform.html"){
<div class="tour-ledger-actions"> <div class="tour-ledger-actions">
<el-button size="medium" type="primary" icon="el-icon-upload2" @click="showImportDialog = true">参加人员导入</el-button> <el-button size="medium" type="primary" icon="el-icon-upload2" @click="showImportDialog = true">参加人员导入</el-button>
<el-button size="medium" type="primary" icon="el-icon-check" @click="setParticipants">设置参加人员</el-button> <el-button size="medium" type="primary" icon="el-icon-check" @click="setParticipants">设置参加人员</el-button>
<el-button size="medium" type="primary" icon="el-icon-mobile-phone" :loading="syncMobileLoading" @click="syncMobile">获取手机号</el-button>
<el-button size="medium" type="primary" icon="el-icon-download" @click="exportLedgerData">导出台账数据</el-button> <el-button size="medium" type="primary" icon="el-icon-download" @click="exportLedgerData">导出台账数据</el-button>
<el-button size="medium" type="primary" icon="el-icon-download" @click="exportUnionSignupZip">导出分工会报名压缩包</el-button> <el-button size="medium" type="primary" icon="el-icon-download" @click="exportUnionSignupZip">导出分工会报名压缩包</el-button>
</div> </div>
@@ -109,6 +110,7 @@ layout("/layouts/platform.html"){
<el-table-column label="年龄" prop="age" width="90" align="center" header-align="center" show-overflow-tooltip></el-table-column> <el-table-column label="年龄" prop="age" width="90" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="身份证号" prop="idCard" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column> <el-table-column label="身份证号" prop="idCard" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="手机号" prop="mobile" width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column> <el-table-column label="手机号" prop="mobile" width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所属工会" prop="unionName" min-width="150" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所在单位" prop="unitName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column> <el-table-column label="所在单位" prop="unitName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="乘车地点" prop="boardingPlace" min-width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column> <el-table-column label="乘车地点" prop="boardingPlace" min-width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="报名线路" prop="lineName" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column> <el-table-column label="报名线路" prop="lineName" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
@@ -365,6 +367,7 @@ layout("/layouts/platform.html"){
detailRow: {}, detailRow: {},
doneTasks: [], doneTasks: [],
showImportDialog: false, showImportDialog: false,
syncMobileLoading: false,
multipleSelection: [], multipleSelection: [],
filterOptionsTimer: null, filterOptionsTimer: null,
currentUnionId: "", currentUnionId: "",
@@ -439,6 +442,56 @@ layout("/layouts/platform.html"){
}) })
}) })
}, },
syncMobile() {
const selectedCount = this.multipleSelection.length
const confirmText = selectedCount > 0
? "确定获取已勾选的" + selectedCount + "名台账人员手机号吗?"
: "未勾选人员,将获取当前查询条件下全部台账人员手机号,确定继续吗?"
this.$confirm(confirmText, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
const params = {
ids: selectedCount > 0 ? JSON.stringify(this.multipleSelection.map(item => item.id)) : "",
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear,
keyword: this.pageForm.keyword,
unionId: this.pageForm.unionId,
lineId: this.pageForm.lineId,
travelPeriod: this.pageForm.travelPeriod,
lineType: this.pageForm.lineType,
directFamilyOnly: this.pageForm.directFamilyOnly,
overCostOnly: this.pageForm.overCostOnly
}
this.syncMobileLoading = true
this.$axios.post(loc() + "/syncMobile", params).then((res) => {
if (res.code === 0) {
const data = res.data || {}
const failedJobNos = data.failedJobNos || []
this.$alert(
"请求人数:" + (data.requestUserCount || 0) +
"<br>接口返回数量:" + (data.pulledCount || 0) +
"<br>有效手机号数量:" + (data.validMobileCount || 0) +
"<br>系统用户更新数量:" + (data.updatedUserCount || 0) +
"<br>台账更新数量:" + (data.ledgerUpdatedCount || 0) +
"<br>失败工号数量:" + failedJobNos.length,
"手机号获取完成",
{
dangerouslyUseHTMLString: true,
confirmButtonText: "确定"
}
)
this.clearTableSelection()
this.pageData()
} else {
this.$message.warning(res.msg || "手机号获取失败")
}
}).finally(() => {
this.syncMobileLoading = false
})
}).catch(() => {})
},
exportUnionSignupZip() { exportUnionSignupZip() {
const params = new URLSearchParams() const params = new URLSearchParams()
;["startYear", "endYear", "keyword", "unionId", "lineId", "travelPeriod", "lineType"].forEach(key => { ;["startYear", "endYear", "keyword", "unionId", "lineId", "travelPeriod", "lineType"].forEach(key => {
@@ -91,7 +91,7 @@ layout("/layouts/platform.html"){
<el-table-column label="所属分工会" prop="unionName" min-width="160" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column> <el-table-column label="所属分工会" prop="unionName" min-width="160" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="所在单位" prop="unitName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column> <el-table-column label="所在单位" prop="unitName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="线路" prop="lineName" min-width="200" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column> <el-table-column label="线路" prop="lineName" min-width="200" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时间" prop="travelPeriod" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column> <el-table-column label="出行时间" prop="travelPeriod" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="出行时段" prop="periodName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column> <el-table-column label="出行时段" prop="periodName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="乘车地点" prop="boardingPlace" min-width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column> <el-table-column label="乘车地点" prop="boardingPlace" min-width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="已上传图片数量" width="140" align="center" header-align="center"> <el-table-column label="已上传图片数量" width="140" align="center" header-align="center">
@@ -475,6 +475,7 @@ layout("/layouts/platform.html"){
assignMatterOptions: [], assignMatterOptions: [],
unionOptions: [], unionOptions: [],
assignDialogVisible: false, assignDialogVisible: false,
assignOpenToken: 0,
remindDialogVisible: false, remindDialogVisible: false,
remindSubmitting: false, remindSubmitting: false,
candidateLoading: false, candidateLoading: false,
@@ -760,28 +761,47 @@ layout("/layouts/platform.html"){
}) })
}, },
openAssign() { openAssign() {
const token = this.nextAssignOpenToken()
const year = this.pageForm.year || moment().format("YYYY") const year = this.pageForm.year || moment().format("YYYY")
this.assignForm = this.defaultAssignForm(year) this.assignForm = this.defaultAssignForm(year)
this.assignForm.matterId = "" this.assignForm.matterId = ""
this.candidateForm = this.defaultCandidateForm() this.candidateForm = this.defaultCandidateForm()
this.candidateData = [] this.assignSettingOptions = []
this.selectedCandidates = []
this.selectedCandidateIds = []
this.candidateUserOptions = []
this.assignDialogVisible = true
this.loadAssignSettingOptions()
},
resetAssignDialog() {
this.assignForm = this.defaultAssignForm(moment().format("YYYY"))
this.candidateForm = this.defaultCandidateForm()
this.assignMatterOptions = [] this.assignMatterOptions = []
this.candidateData = [] this.candidateData = []
this.selectedCandidates = [] this.selectedCandidates = []
this.selectedCandidateIds = [] this.selectedCandidateIds = []
this.candidateUserOptions = [] this.candidateUserOptions = []
this.assignSubmitting = false this.assignSubmitting = false
this.candidateLoading = false
this.candidateSelectLoading = false
this.assignDialogVisible = true
this.loadAssignSettingOptions(token)
},
resetAssignDialog() {
this.assignOpenToken += 1
this.assignForm = this.defaultAssignForm(moment().format("YYYY"))
this.candidateForm = this.defaultCandidateForm()
this.assignSettingOptions = []
this.assignMatterOptions = []
this.candidateData = []
this.selectedCandidates = []
this.selectedCandidateIds = []
this.candidateUserOptions = []
this.assignSubmitting = false
this.candidateLoading = false
this.candidateSelectLoading = false
},
// 人员分配弹窗每次打开都生成新的批次标记,旧请求返回时不再覆盖新弹窗数据。
nextAssignOpenToken() {
this.assignOpenToken += 1
return this.assignOpenToken
},
isCurrentAssignOpen(token) {
return this.assignDialogVisible && token === this.assignOpenToken
}, },
assignYearChange() { assignYearChange() {
const token = this.assignOpenToken
this.assignForm.settingId = "" this.assignForm.settingId = ""
this.assignForm.matterId = "" this.assignForm.matterId = ""
this.assignMatterOptions = [] this.assignMatterOptions = []
@@ -789,31 +809,34 @@ layout("/layouts/platform.html"){
this.candidateUserOptions = [] this.candidateUserOptions = []
this.candidateForm.keyword = "" this.candidateForm.keyword = ""
this.clearCandidateSelection() this.clearCandidateSelection()
this.loadAssignSettingOptions() this.loadAssignSettingOptions(token)
this.loadCandidatePageData() this.loadCandidatePageData(token)
}, },
assignSettingChange() { assignSettingChange() {
const token = this.assignOpenToken
this.assignForm.matterId = "" this.assignForm.matterId = ""
this.assignMatterOptions = [] this.assignMatterOptions = []
this.selectedCandidateIds = [] this.selectedCandidateIds = []
this.candidateUserOptions = [] this.candidateUserOptions = []
this.candidateForm.keyword = "" this.candidateForm.keyword = ""
this.clearCandidateSelection() this.clearCandidateSelection()
this.loadAssignMatterOptions() this.loadAssignMatterOptions(token)
this.loadCandidatePageData() this.loadCandidatePageData(token)
}, },
loadAssignSettingOptions() { loadAssignSettingOptions(token) {
token = token || this.assignOpenToken
this.$axios.post(loc() + "/settingOptions", {year: this.assignForm.year}).then((res) => { this.$axios.post(loc() + "/settingOptions", {year: this.assignForm.year}).then((res) => {
if (!this.isCurrentAssignOpen(token)) return
if (res.code === 0) { if (res.code === 0) {
this.assignSettingOptions = res.data || [] this.assignSettingOptions = res.data || []
// 人员分配弹窗独立于主列表筛选,默认取当前年度第一条可用配置。 // 人员分配弹窗独立于主列表筛选,默认取当前年度第一条可用配置。
if (!this.assignForm.settingId && this.assignSettingOptions.length > 0) { if (!this.assignForm.settingId && this.assignSettingOptions.length > 0) {
this.assignForm.settingId = this.assignSettingOptions[0].id this.assignForm.settingId = this.assignSettingOptions[0].id
this.loadAssignMatterOptions() this.loadAssignMatterOptions(token)
this.loadCandidatePageData() this.loadCandidatePageData(token)
} else if (this.assignForm.settingId) { } else if (this.assignForm.settingId) {
this.loadAssignMatterOptions() this.loadAssignMatterOptions(token)
this.loadCandidatePageData() this.loadCandidatePageData(token)
} else { } else {
this.assignMatterOptions = [] this.assignMatterOptions = []
this.candidateData = [] this.candidateData = []
@@ -822,18 +845,21 @@ layout("/layouts/platform.html"){
} }
}) })
}, },
loadAssignMatterOptions() { loadAssignMatterOptions(token) {
token = token || this.assignOpenToken
if (!this.assignForm.settingId) { if (!this.assignForm.settingId) {
this.assignMatterOptions = [] this.assignMatterOptions = []
return return
} }
this.$axios.post(loc() + "/matterOptions", {settingId: this.assignForm.settingId}).then((res) => { this.$axios.post(loc() + "/matterOptions", {settingId: this.assignForm.settingId}).then((res) => {
if (!this.isCurrentAssignOpen(token)) return
if (res.code === 0) { if (res.code === 0) {
this.assignMatterOptions = res.data || [] this.assignMatterOptions = res.data || []
} }
}) })
}, },
loadCandidatePageData() { loadCandidatePageData(token) {
token = token || this.assignOpenToken
if (!this.assignForm.settingId) { if (!this.assignForm.settingId) {
this.candidateData = [] this.candidateData = []
this.candidateForm.totalCount = 0 this.candidateForm.totalCount = 0
@@ -851,6 +877,7 @@ layout("/layouts/platform.html"){
keyword: (this.selectedCandidateIds || []).length > 0 ? "" : this.candidateForm.keyword, keyword: (this.selectedCandidateIds || []).length > 0 ? "" : this.candidateForm.keyword,
userIds: JSON.stringify(this.selectedCandidateIds || []) userIds: JSON.stringify(this.selectedCandidateIds || [])
}).then((res) => { }).then((res) => {
if (!this.isCurrentAssignOpen(token)) return
if (res.code === 0) { if (res.code === 0) {
const data = res.data || {} const data = res.data || {}
this.candidateData = (data.list || []).map(item => Object.assign({}, item, { this.candidateData = (data.list || []).map(item => Object.assign({}, item, {
@@ -864,7 +891,9 @@ layout("/layouts/platform.html"){
this.$message.warning(res.msg || "候选人员查询失败") this.$message.warning(res.msg || "候选人员查询失败")
} }
}).finally(() => { }).finally(() => {
this.candidateLoading = false if (this.isCurrentAssignOpen(token)) {
this.candidateLoading = false
}
}) })
}, },
assignMatterChange(value) { assignMatterChange(value) {
@@ -922,6 +951,7 @@ layout("/layouts/platform.html"){
// 人员选择器复用候选人员接口,校工会可按分工会过滤,也可不选分工会查询全部会员。 // 人员选择器复用候选人员接口,校工会可按分工会过滤,也可不选分工会查询全部会员。
this.candidateForm.keyword = keyword || "" this.candidateForm.keyword = keyword || ""
this.candidateSelectLoading = true this.candidateSelectLoading = true
const token = this.assignOpenToken
this.$axios.post(loc() + "/candidatePageData", { this.$axios.post(loc() + "/candidatePageData", {
pageNumber: 1, pageNumber: 1,
pageSize: 20, pageSize: 20,
@@ -932,12 +962,15 @@ layout("/layouts/platform.html"){
lastThirtyJoined: this.normalizeCandidateFilterValue(this.candidateForm.lastThirtyJoined), lastThirtyJoined: this.normalizeCandidateFilterValue(this.candidateForm.lastThirtyJoined),
keyword: keyword || "" keyword: keyword || ""
}).then((res) => { }).then((res) => {
if (!this.isCurrentAssignOpen(token)) return
if (res.code === 0) { if (res.code === 0) {
const data = res.data || {} const data = res.data || {}
this.candidateUserOptions = this.mergeOptionLists(this.selectedCandidates, data.list || []) this.candidateUserOptions = this.mergeOptionLists(this.selectedCandidates, data.list || [])
} }
}).finally(() => { }).finally(() => {
this.candidateSelectLoading = false if (this.isCurrentAssignOpen(token)) {
this.candidateSelectLoading = false
}
}) })
}, },
candidateUserSelectChange(userIds) { candidateUserSelectChange(userIds) {
@@ -81,7 +81,7 @@ const OPINION_INFO = {
<template v-if="['schoolAudit', 'office'].includes(task.taskName)"> <template v-if="['schoolAudit', 'office'].includes(task.taskName)">
<el-descriptions-item label="主办单位" :span="3"> <el-descriptions-item label="主办单位" :span="3">
{{task.ext.tf_masterUnitName}} {{task.ext.tf_masterUnitNameStr || task.ext.tf_masterUnitName}}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="协办单位" :span="3"> <el-descriptions-item label="协办单位" :span="3">
{{task.ext.tf_slaveUnitNameStr}} {{task.ext.tf_slaveUnitNameStr}}
@@ -91,10 +91,10 @@ layout("/layouts/platform.html"){
<el-form-item <el-form-item
label="主办单位" label="主办单位"
v-if="isSchoolPass()" v-if="isSchoolPass()"
:rules="[{required:true, message:'必填',trigger:['change','blur']}]" :rules="[{type:'array',required:true,min:1,message:'请选择主办单位',trigger:['change','blur']}]"
prop="tf_masterUnitId" 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 <el-option
v-for="item in underTakeOptions" v-for="item in underTakeOptions"
:label="item.name" :label="item.name"
@@ -112,7 +112,7 @@ layout("/layouts/platform.html"){
:label="item.name" :label="item.name"
:value="item.id" :value="item.id"
:key="item.id" :key="item.id"
:disabled="item.id===formData.tf_masterUnitId" :disabled="formData.tf_masterUnitIds.includes(item.id)"
></el-option> ></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -123,7 +123,7 @@ layout("/layouts/platform.html"){
</el-form> </el-form>
<el-row type="flex" justify="end"> <el-row type="flex" justify="end">
<el-button @click="$refs.guava.index()" size="small">取消</el-button> <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> </el-row>
</div> </div>
</opinion-info> </opinion-info>
@@ -182,7 +182,7 @@ layout("/layouts/platform.html"){
approval: false approval: false
}, },
formData: { formData: {
tf_masterUnitId: null, tf_masterUnitIds: [],
tf_slaveUnitIds: [] tf_slaveUnitIds: []
}, },
showApprovalForm: false, showApprovalForm: false,
@@ -212,7 +212,7 @@ layout("/layouts/platform.html"){
return return
} }
this.$nextTick(() => { 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) { openView(row) {
@@ -229,7 +229,7 @@ layout("/layouts/platform.html"){
processTaskId: row.taskId, processTaskId: row.taskId,
taskName: row.curTaskName, taskName: row.curTaskName,
typeId: row.typeId, typeId: row.typeId,
tf_masterUnitId: null, tf_masterUnitIds: [],
tf_slaveUnitIds: [], tf_slaveUnitIds: [],
tf_schoolResult: 'YES', tf_schoolResult: 'YES',
roleCode: 'OFFICE_MANAGER', roleCode: 'OFFICE_MANAGER',
@@ -239,6 +239,9 @@ layout("/layouts/platform.html"){
}, },
handleTaskAction() { handleTaskAction() {
if (this.formLoading) {
return
}
this.schoolResultChange() this.schoolResultChange()
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.formRef.validate((valid) => { this.$refs.formRef.validate((valid) => {
@@ -248,6 +251,7 @@ layout("/layouts/platform.html"){
cancelButtonText: "取消", cancelButtonText: "取消",
type: "warning" type: "warning"
}).then(() => { }).then(() => {
this.formLoading = true
const loading = createLoading('提交中') const loading = createLoading('提交中')
const submitType = this.formData.tf_schoolResult === 'ROLLBACK' ? 6 : 1 const submitType = this.formData.tf_schoolResult === 'ROLLBACK' ? 6 : 1
const executeTask = () => { const executeTask = () => {
@@ -259,11 +263,13 @@ layout("/layouts/platform.html"){
tf_opinion: this.formData.tf_opinion, tf_opinion: this.formData.tf_opinion,
} : { } : {
...this.formData, ...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_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(','), 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({ data: JSON.stringify({
...taskData, ...taskData,
submitType: submitType, submitType: submitType,
@@ -274,27 +280,26 @@ layout("/layouts/platform.html"){
this.$message.success(res.msg) this.$message.success(res.msg)
this.doSearch() this.doSearch()
} }
}).finally(() => {
loading.close()
}) })
} }
if (submitType === 6 || !this.isSchoolPass()) { if (submitType === 6 || !this.isSchoolPass()) {
executeTask() return executeTask().finally(() => {
return this.formLoading = false
loading.close()
})
} }
// 通过审核时校工会可调整意见类别,需要先保存 typeId,再按流程表 YES 分支继续流转 // 通过审核先保存类别,返回流程提交 Promise,让失败和成功都统一释放 loading
this.$axios.post("/platform/opinion/schoolAudit/edit", { return this.$axios.post("/platform/opinion/schoolAudit/edit", {
info: JSON.stringify({ info: JSON.stringify({
id: this.formData.id, id: this.formData.id,
typeId: this.formData.typeId typeId: this.formData.typeId
}) })
}).then((editRes) => { }).then((editRes) => {
if (editRes.code === 0) { if (editRes.code === 0) {
executeTask() return executeTask()
return
} }
loading.close() }).finally(() => {
}).catch(() => { this.formLoading = false
loading.close() loading.close()
}) })
}) })
@@ -202,21 +202,21 @@ layout("/layouts/platform.html"){
this.doSearch() this.doSearch()
}, },
// 查询开启的教代会 // 默认届次确定后再查询列表,避免无届次请求覆盖带届次的查询结果。
listOpenSession() { listOpenSession() {
this.$axios.post("/platform/opinion/common/listOpenSession").then((res) => { return this.$axios.post("/platform/opinion/common/listOpenSession").then((res) => {
if (res.code === 0) { if (res.code === 0) {
this.sessionOptions = res.data this.$set(this, "sessionOptions", res.data || [])
if (this.sessionOptions) { if (this.sessionOptions.length > 0) {
this.$set(this.pageForm, "sessionId", this.sessionOptions[0].id) this.$set(this.pageForm, "sessionId", this.sessionOptions[0].id)
this.pageData()
} }
// 无开启届次时仍按当前用户查询,由后端限定为有效的意见任务。
this.pageData()
} }
}) })
}, },
}, },
created() { created() {
this.pageData()
this.listOpenSession() this.listOpenSession()
} }
}) })
@@ -39,7 +39,9 @@ layout("/layouts/platform.html"){
</el-card> </el-card>
<el-card shadow="never" class="mt10"> <el-card shadow="never" class="mt10">
<table-tool label="变更列表" :app="this"></table-tool> <table-tool label="变更列表" :app="this">
<el-button icon="el-icon-printer" type="primary" size="small" @click="doExport">导出</el-button>
</table-tool>
<el-table :data="tableData" style="width: 100%" row-key="id" <el-table :data="tableData" style="width: 100%" row-key="id"
@sort-change="pageOrder" @sort-change="pageOrder"
resizable resizable
@@ -189,6 +191,19 @@ layout("/layouts/platform.html"){
}) })
}, },
doExport() {
const pageForm = clone(this.pageForm)
pageForm.userStates = JSON.stringify(pageForm.userStates)
pageForm.personTypes = JSON.stringify(pageForm.personTypes)
pageForm.changeTypes = JSON.stringify(pageForm.changeTypes)
pageForm.changeOrigins = JSON.stringify(pageForm.changeOrigins)
// 导出字段取当前页面列表列,并补充后端固定处理的异动信息列。
pageForm.columns = JSON.stringify(this.tableColumns.map((item) => {
return {prop: item.prop, label: item.label}
}))
this.$downLoad(loc() + "/doExport", pageForm)
},
}, },
async created() { async created() {
@@ -72,6 +72,7 @@
<el-descriptions-item label="在职状态">{{ viewData.userState }}</el-descriptions-item> <el-descriptions-item label="在职状态">{{ viewData.userState }}</el-descriptions-item>
<el-descriptions-item label="教职工类别">{{ viewData.personType }}</el-descriptions-item> <el-descriptions-item label="教职工类别">{{ viewData.personType }}</el-descriptions-item>
<el-descriptions-item label="编制类别">{{ viewData.preparedBy }}</el-descriptions-item> <el-descriptions-item label="编制类别">{{ viewData.preparedBy }}</el-descriptions-item>
<el-descriptions-item label="编制信息">{{ viewData.compilationInformation }}</el-descriptions-item>
<!-- 4 --> <!-- 4 -->
<el-descriptions-item label="职称">{{ viewData.professionalTitle }}</el-descriptions-item> <el-descriptions-item label="职称">{{ viewData.professionalTitle }}</el-descriptions-item>
@@ -82,6 +83,8 @@
<el-descriptions-item label="行政级别(管理岗位)">{{ viewData.administrativeLevel }}</el-descriptions-item> <el-descriptions-item label="行政级别(管理岗位)">{{ viewData.administrativeLevel }}</el-descriptions-item>
<el-descriptions-item label="岗级">{{ viewData.positionLevel }}</el-descriptions-item> <el-descriptions-item label="岗级">{{ viewData.positionLevel }}</el-descriptions-item>
<el-descriptions-item label="行政岗级">{{ viewData.administrativePositionLevel }}</el-descriptions-item> <el-descriptions-item label="行政岗级">{{ viewData.administrativePositionLevel }}</el-descriptions-item>
<el-descriptions-item></el-descriptions-item>
<el-descriptions-item></el-descriptions-item>
</el-descriptions> </el-descriptions>
</div> </div>
`, `,
@@ -20,6 +20,14 @@ layout("/layouts/platform.html"){
> >
备份历史会员 备份历史会员
</el-button> </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-button type="primary" size="small" icon="el-icon-download" @click="exportMember">会员档案导出</el-button>
<el-popover <el-popover
placement="bottom" placement="bottom"
@@ -149,7 +157,8 @@ layout("/layouts/platform.html"){
], ],
checkedFields: [], checkedFields: [],
importDialog: false importDialog: false,
syncMobileLoading: false
} }
}, },
components: { components: {
@@ -180,6 +189,39 @@ layout("/layouts/platform.html"){
openArchive() { openArchive() {
this.$refs.archiveMemberRef.archiveDialogVisible = true 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) { openView(userId) {
this.$refs.guava.view(() => { this.$refs.guava.view(() => {
this.$refs.infoRef.onOpen(userId) this.$refs.infoRef.onOpen(userId)
@@ -160,6 +160,7 @@ layout("/layouts/platform.html"){
{ prop: "userState", label: "在职状态", width: 120, sortable: true }, { prop: "userState", label: "在职状态", width: 120, sortable: true },
{ prop: "personType", label: "教职工类别", width: 120, sortable: true }, { prop: "personType", label: "教职工类别", width: 120, sortable: true },
{ prop: "preparedBy", label: "编制类别", width: 120, sortable: true }, { prop: "preparedBy", label: "编制类别", width: 120, sortable: true },
{ prop: "compilationInformation", label: "编制信息", width: 120, sortable: true },
{ prop: "postDoctoralJoinDate", label: "进站时间", sortable: true, checked: 0 }, { prop: "postDoctoralJoinDate", label: "进站时间", sortable: true, checked: 0 },
{ prop: "unionName", label: "所属工会", width: 120, sortable: true }, { prop: "unionName", label: "所属工会", width: 120, sortable: true },
{ prop: "unitName", label: "所属单位", width: 120, sortable: true }, { prop: "unitName", label: "所属单位", width: 120, sortable: true },
@@ -201,6 +202,8 @@ layout("/layouts/platform.html"){
pageForm.sexTypes = JSON.stringify(this.pageForm.sexTypes) pageForm.sexTypes = JSON.stringify(this.pageForm.sexTypes)
pageForm.memberTypes = JSON.stringify(this.pageForm.memberTypes) pageForm.memberTypes = JSON.stringify(this.pageForm.memberTypes)
pageForm.personTypes = JSON.stringify(this.pageForm.personTypes) pageForm.personTypes = JSON.stringify(this.pageForm.personTypes)
pageForm.preparedBys = JSON.stringify(this.pageForm.preparedBys)
pageForm.compilationInformations = JSON.stringify(this.pageForm.compilationInformations)
pageForm.userStates = JSON.stringify(this.pageForm.userStates) pageForm.userStates = JSON.stringify(this.pageForm.userStates)
const data = this.tableColumns.filter(item => this.checkedFields.includes(item.prop)).map(item => { const data = this.tableColumns.filter(item => this.checkedFields.includes(item.prop)).map(item => {
return {prop: item.prop, label: item.label} return {prop: item.prop, label: item.label}
@@ -225,6 +228,7 @@ layout("/layouts/platform.html"){
pageForm.memberTypes = JSON.stringify(this.pageForm.memberTypes) pageForm.memberTypes = JSON.stringify(this.pageForm.memberTypes)
pageForm.personTypes = JSON.stringify(this.pageForm.personTypes) pageForm.personTypes = JSON.stringify(this.pageForm.personTypes)
pageForm.preparedBys = JSON.stringify(this.pageForm.preparedBys) pageForm.preparedBys = JSON.stringify(this.pageForm.preparedBys)
pageForm.compilationInformations = JSON.stringify(this.pageForm.compilationInformations)
pageForm.memberStatus = JSON.stringify(this.pageForm.memberStatus) pageForm.memberStatus = JSON.stringify(this.pageForm.memberStatus)
pageForm.userStates = JSON.stringify(this.pageForm.userStates) pageForm.userStates = JSON.stringify(this.pageForm.userStates)
if (pageForm.birthdayRange && pageForm.birthdayRange.length > 0) { if (pageForm.birthdayRange && pageForm.birthdayRange.length > 0) {
@@ -112,6 +112,31 @@ const MEMBER_STATISTICS_COMPREHENSIVE_QUERY_FORM = {
</el-col> </el-col>
</el-row> </el-row>
<el-row type="flex" align="middle" class="query-row query-row-tag">
<el-col class="query-row-title">编制信息</el-col>
<el-col class="query-row-content query-row-content-tag">
<el-tag
style="margin-right: 10px;cursor: pointer"
v-for="item in compilationInformationOptions"
:key="item.code"
:type="item.name"
:effect="pageForm.compilationInformations.includes(item.name)?'dark':'plain'"
@click="tagClick('compilationInformations',item.name)">
{{ item.name }}
</el-tag>
<el-link type="danger"
v-if="compilationInformationOptions.length&&pageForm.compilationInformations.length"
:underline="false"
@click="pageForm.compilationInformations=[];doSearch()">清空
</el-link>
<el-link :underline="false"
@click="pageForm.compilationInformations=compilationInformationOptions.map(p=>p.code);doSearch()"
type="success">全部
</el-link>
</el-col>
</el-row>
<el-row type="flex" align="middle" class="query-row"> <el-row type="flex" align="middle" class="query-row">
<el-col class="query-row-content"> <el-col class="query-row-content">
<el-row> <el-row>
@@ -208,6 +233,7 @@ const MEMBER_STATISTICS_COMPREHENSIVE_QUERY_FORM = {
searchName: "username", searchName: "username",
personTypes: [], personTypes: [],
preparedBys: [], preparedBys: [],
compilationInformations: [],
userStates: [], userStates: [],
memberStatus: [], memberStatus: [],
year: moment().format("YYYY"), year: moment().format("YYYY"),
@@ -224,6 +250,7 @@ const MEMBER_STATISTICS_COMPREHENSIVE_QUERY_FORM = {
}, },
personTypeOptions: [], personTypeOptions: [],
preparedByOptions: [], preparedByOptions: [],
compilationInformationOptions: [],
userStateOptions: [], userStateOptions: [],
memberStatusOptions: [], memberStatusOptions: [],
unions: [], unions: [],
@@ -315,6 +342,7 @@ const MEMBER_STATISTICS_COMPREHENSIVE_QUERY_FORM = {
this.pageForm.sexTypes = [] this.pageForm.sexTypes = []
this.pageForm.personTypes = [] this.pageForm.personTypes = []
this.pageForm.preparedBys = [] this.pageForm.preparedBys = []
this.pageForm.compilationInformations = []
this.pageForm.memberStatus = [] this.pageForm.memberStatus = []
this.pageForm.userStates = [] this.pageForm.userStates = []
this.pageForm.unionId = [] this.pageForm.unionId = []
@@ -352,6 +380,10 @@ const MEMBER_STATISTICS_COMPREHENSIVE_QUERY_FORM = {
this.$businessTool.getDictOptions("USER_PREPARED_BY_TYPE").then((data) => { this.$businessTool.getDictOptions("USER_PREPARED_BY_TYPE").then((data) => {
this.preparedByOptions = data this.preparedByOptions = data
}) })
// 编制信息使用独立字典,后端按 sys_user.compilationInformation 字段进行筛选。
this.$businessTool.getDictOptions("USER_COMPILATION_INFORMATION").then((data) => {
this.compilationInformationOptions = data
})
this.$businessTool.getDictOptions("USER_STATE").then((data) => { this.$businessTool.getDictOptions("USER_STATE").then((data) => {
this.userStateOptions = data this.userStateOptions = data
}) })
@@ -379,7 +379,6 @@ layout("/layouts/platform_h5.html"){
<van-button v-if="canModify(row) || isTravelEnded(row)" size="small" type="info" :disabled="isTravelEnded(row)" @click="openSignup(row)">修改</van-button> <van-button v-if="canModify(row) || isTravelEnded(row)" size="small" type="info" :disabled="isTravelEnded(row)" @click="openSignup(row)">修改</van-button>
<van-button v-if="canRevoke(row)" size="small" type="danger" plain @click="revokeSignup(row)">撤回</van-button> <van-button v-if="canRevoke(row)" size="small" type="danger" plain @click="revokeSignup(row)">撤回</van-button>
<van-button v-if="showLeaveThirtyTeachTour(row)" size="small" type="danger" plain :loading="leaveLoading" @click="leaveThirtyTeachTour(row)">退出疗休养报名</van-button> <van-button v-if="showLeaveThirtyTeachTour(row)" size="small" type="danger" plain :loading="leaveLoading" @click="leaveThirtyTeachTour(row)">退出疗休养报名</van-button>
<van-button v-if="showCancel(row)" size="small" type="danger" plain :disabled="isTravelEnded(row)" @click="cancelByRow(row)">取消报名</van-button>
<van-button v-if="showExport(row)" size="small" type="info" plain :disabled="!canExport(row)" @click="downloadExport(row, 'cost')">报销申请</van-button> <van-button v-if="showExport(row)" size="small" type="info" plain :disabled="!canExport(row)" @click="downloadExport(row, 'cost')">报销申请</van-button>
<van-button v-if="showDirectRelativeExport(row)" size="small" type="info" plain :disabled="!canDirectRelativeExport(row)" @click="downloadExport(row, 'direct')">亲属线路申请</van-button> <van-button v-if="showDirectRelativeExport(row)" size="small" type="info" plain :disabled="!canDirectRelativeExport(row)" @click="downloadExport(row, 'direct')">亲属线路申请</van-button>
</div> </div>
@@ -470,8 +469,9 @@ layout("/layouts/platform_h5.html"){
<div class="tour-section-title">教职工信息</div> <div class="tour-section-title">教职工信息</div>
<van-field label="姓名" readonly v-model="signupForm.userName"></van-field> <van-field label="姓名" readonly v-model="signupForm.userName"></van-field>
<van-field label="工号" readonly v-model="signupForm.jobNo"></van-field> <van-field label="工号" readonly v-model="signupForm.jobNo"></van-field>
<van-field label="身份证号" v-model="signupForm.idCard" maxlength="30" placeholder="请填写身份证号"></van-field> <van-field label="身份证号" readonly v-model="signupForm.idCard"></van-field>
<van-field label="年龄" readonly :value="staffAge(signupForm)"></van-field> <van-field label="年龄" readonly :value="staffAge(signupForm)"></van-field>
<van-field label="手机号" readonly v-model="signupForm.mobile"></van-field>
<van-field label="所在工会" readonly v-model="signupForm.unionName"></van-field> <van-field label="所在工会" readonly v-model="signupForm.unionName"></van-field>
</div> </div>
@@ -480,13 +480,13 @@ layout("/layouts/platform_h5.html"){
<van-field label="报名线路" readonly v-model="signupForm.lineName"></van-field> <van-field label="报名线路" readonly v-model="signupForm.lineName"></van-field>
<van-field label="线路类型" readonly v-model="signupForm.lineType"></van-field> <van-field label="线路类型" readonly v-model="signupForm.lineType"></van-field>
<van-field label="出行时间" readonly v-model="signupForm.travelPeriod"></van-field> <van-field label="出行时间" readonly v-model="signupForm.travelPeriod"></van-field>
<van-field readonly clickable is-link label="乘车地点" v-model="signupForm.boardingPlace" placeholder="请选择乘车地点" @click="openBoardingPlacePicker"></van-field> <van-field required readonly clickable is-link label="乘车地点" v-model="signupForm.boardingPlace" placeholder="请选择乘车地点" @click="openBoardingPlacePicker"></van-field>
<van-field v-if="fillBedInfo" readonly clickable is-link label="床型" v-model="signupForm.bedType" placeholder="请选择床型" @click="openBedPicker('self')"></van-field> <van-field v-if="fillBedInfo" label="床型" readonly v-model="signupForm.bedType"></van-field>
<van-field v-if="fillBedInfo" label="床位信息" v-model="signupForm.bedInfo" maxlength="100" placeholder="请输入床位信息"></van-field> <van-field v-if="fillBedInfo" label="床位信息" readonly v-model="signupForm.bedInfo"></van-field>
<van-field v-if="fillBedInfo" label="意向拼床人" v-model="signupForm.intendedRoommate" maxlength="100" placeholder="请输入意向拼床人"></van-field> <van-field v-if="fillBedInfo" label="意向拼床人" readonly v-model="signupForm.intendedRoommate"></van-field>
<van-field v-if="canApplyOverReimbursement(signupForm)" class="tour-over-cost" label="报销超出费用"> <van-field v-if="canApplyOverReimbursement(signupForm)" class="tour-over-cost" label="报销超出费用">
<template #input> <template #input>
<van-radio-group v-model="signupForm.overCostReimbursed" direction="horizontal"> <van-radio-group v-model="signupForm.overCostReimbursed" direction="horizontal" disabled>
<van-radio :name="true"></van-radio> <van-radio :name="true"></van-radio>
<van-radio :name="false"></van-radio> <van-radio :name="false"></van-radio>
</van-radio-group> </van-radio-group>
@@ -499,12 +499,12 @@ layout("/layouts/platform_h5.html"){
<div v-if="isDirectFamilyLine(signupForm)" class="tour-section"> <div v-if="isDirectFamilyLine(signupForm)" class="tour-section">
<div class="tour-section-title">直系亲属线路</div> <div class="tour-section-title">直系亲属线路</div>
<van-field label="亲属姓名" v-model="directRelativeForm.relativeName" maxlength="100" placeholder="请输入亲属姓名"></van-field> <van-field label="亲属姓名" readonly v-model="directRelativeForm.relativeName"></van-field>
<van-field label="所在单位" v-model="directRelativeForm.unitName" maxlength="100" placeholder="请输入所在单位"></van-field> <van-field label="所在单位" readonly v-model="directRelativeForm.unitName"></van-field>
<van-field readonly clickable is-link label="亲属关系" v-model="directRelativeForm.relationshipName" placeholder="请选择亲属关系" @click="openDirectRelativePicker"></van-field> <van-field label="亲属关系" readonly v-model="directRelativeForm.relationshipName"></van-field>
<van-field label="线路名称" v-model="directRelativeForm.lineName" maxlength="100" placeholder="请输入线路名称"></van-field> <van-field label="线路名称" readonly v-model="directRelativeForm.lineName"></van-field>
<van-field label="出行开始" v-model="directRelativeForm.travelStartTime" type="date" placeholder="请选择出行开始日期"></van-field> <van-field label="出行开始" readonly v-model="directRelativeForm.travelStartTime"></van-field>
<van-field label="出行结束" v-model="directRelativeForm.travelEndTime" type="date" placeholder="请选择出行结束日期"></van-field> <van-field label="出行结束" readonly v-model="directRelativeForm.travelEndTime"></van-field>
</div> </div>
<div v-if="allowFamily && !isDirectFamilyLine(signupForm)" class="tour-section"> <div v-if="allowFamily && !isDirectFamilyLine(signupForm)" class="tour-section">
@@ -512,29 +512,26 @@ layout("/layouts/platform_h5.html"){
<div v-for="(item,index) in familyData" :key="index" class="tour-family-card"> <div v-for="(item,index) in familyData" :key="index" class="tour-family-card">
<div class="tour-family-head"> <div class="tour-family-head">
<span>家属{{ index + 1 }}</span> <span>家属{{ index + 1 }}</span>
<van-button size="mini" type="danger" plain @click="removeFamily(index)">删除</van-button>
</div> </div>
<van-field label="姓名" v-model="item.familyName" maxlength="100" placeholder="请填写姓名"></van-field> <van-field label="姓名" readonly v-model="item.familyName"></van-field>
<van-field label="年龄" v-model.number="item.age" type="digit" placeholder="请填写年龄"></van-field> <van-field label="年龄" readonly v-model="item.age"></van-field>
<van-field label="性别"> <van-field label="性别">
<template #input> <template #input>
<van-radio-group v-model="item.gender" direction="horizontal"> <van-radio-group v-model="item.gender" direction="horizontal" disabled>
<van-radio name="男"></van-radio> <van-radio name="男"></van-radio>
<van-radio name="女"></van-radio> <van-radio name="女"></van-radio>
</van-radio-group> </van-radio-group>
</template> </template>
</van-field> </van-field>
<van-field label="身份证号" v-model="item.idCard" maxlength="18" placeholder="请填写身份证号" @blur="normalizeFamilyIdCard(item)"></van-field> <van-field label="身份证号" readonly v-model="item.idCard"></van-field>
<van-field readonly clickable is-link label="关系" v-model="item.relationship" placeholder="请选择关系" @click="openRelationshipPicker(index)"></van-field> <van-field label="关系" readonly v-model="item.relationship"></van-field>
<van-field v-if="fillBedInfo" readonly clickable is-link label="床型" v-model="item.bedType" placeholder="请选择床型" @click="openBedPicker('family', index)"></van-field> <van-field v-if="fillBedInfo" label="床型" readonly v-model="item.bedType"></van-field>
<van-field v-if="fillBedInfo" label="床位" v-model="item.bedInfo" maxlength="100" placeholder="请输入床位信息"></van-field> <van-field v-if="fillBedInfo" label="床位" readonly v-model="item.bedInfo"></van-field>
<van-field v-if="fillBedInfo" label="意向拼床人" v-model="item.intendedRoommate" maxlength="100" placeholder="请输入意向拼床人"></van-field> <van-field v-if="fillBedInfo" label="意向拼床人" readonly v-model="item.intendedRoommate"></van-field>
</div> </div>
<van-cell title="添加家属" is-link @click="addFamily"></van-cell>
</div> </div>
</div> </div>
<div class="tour-bottom-bar"> <div class="tour-bottom-bar">
<van-button v-if="showCancel(signupForm)" type="danger" plain block round :loading="cancelLoading" @click="cancelSignup">取消报名</van-button>
<van-button type="info" block round :loading="signupLoading" @click="submitSignup">提交</van-button> <van-button type="info" block round :loading="signupLoading" @click="submitSignup">提交</van-button>
</div> </div>
</van-popup> </van-popup>
@@ -623,7 +620,7 @@ layout("/layouts/platform_h5.html"){
return this.directRelativeOptions.map(item => ({ text: item.name || item.code, item })) return this.directRelativeOptions.map(item => ({ text: item.name || item.code, item }))
}, },
boardingPlaceColumns() { boardingPlaceColumns() {
return this.boardingPlaceOptions return [""].concat(this.boardingPlaceOptions || [])
} }
}, },
methods: { methods: {
@@ -986,7 +983,7 @@ layout("/layouts/platform_h5.html"){
this.boardingPlacePickerVisible = true this.boardingPlacePickerVisible = true
}, },
confirmBoardingPlace(value) { confirmBoardingPlace(value) {
this.signupForm.boardingPlace = value this.signupForm.boardingPlace = value || ""
this.boardingPlacePickerVisible = false this.boardingPlacePickerVisible = false
}, },
normalizeFamilyIdCard(row) { normalizeFamilyIdCard(row) {
@@ -1032,11 +1029,11 @@ layout("/layouts/platform_h5.html"){
vant.Toast("请选择乘车地点") vant.Toast("请选择乘车地点")
return return
} }
if (!this.validateFamilies() || !this.validateDirectRelative()) return // H5 修改页当前只允许调整乘车地点,家属、直系亲属等历史报名信息仅展示不参与提交校验。
const families = this.allowFamily && this.signupForm.hasFamily ? this.familyData : []
const form = Object.assign({}, this.signupForm, { const form = Object.assign({}, this.signupForm, {
families: JSON.stringify(families), boardingPlaceOnly: true,
directRelative: this.isDirectFamilyLine(this.signupForm) ? JSON.stringify(this.directRelativeForm) : "" families: "[]",
directRelative: ""
}) })
this.signupLoading = true this.signupLoading = true
this.$axios.post("/platform/thirtyTeachTour/mysignup/doSignup", form).then((res) => { this.$axios.post("/platform/thirtyTeachTour/mysignup/doSignup", form).then((res) => {