泛微OA-离职人员流程转移接口
前言:对于离职人员,走了离职交接流程可以执行 action 接口自动创建流程代理,进行代理的流程只能代理未来到达的流程,但是已到达的流程如果离职人在在职期间不处理,将一直卡在离职人那里。
一、突破点
可利用 hrmresource 表的 enddate 字段,筛选当日的离职人员。
- 1、根据离职人员去查询他的离职交接流程,找到交接人。
- 2、查询此人的待办流程(转发、传阅、申请节点、归档节点的流程可过滤)。
- 3、通过 /api/hrm/permissiontoadjust/processData 接口批量转移流程至交接人。
tips:enddate 字段代表人员离职后 hr 系统关闭账号的日期,因此用该字段来作为筛选条件是安全的。
二、流程转移代码
ProcessTransfer.java
public class ProcessTransfer extends BaseCronJob {
private static Logger logger = LoggerFactory.getLogger(ProcessTransfer.class);
private static final String TABLE_NAME = "formtable_main_62";
private static final String LOG_TABLE_NAME = "transfer_log"; // 日志表名
private String workflowid; // 过滤的流程(某类流程不进行代理)
@Override
public void execute() {
LocalDate currentDate = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String todayDate = currentDate.format(formatter);
RecordSetDataSource rs = new RecordSetDataSource();
// 1.查询今日离职人
List<Integer> userids = this.queryResign(todayDate, rs);
if (userids.isEmpty()) {
logger.info("今日【" + todayDate + "】没有离职人,接口退出。");
return;
}
JSONObject jsonObject = this.getToken(); // 优先获取token
for (int i = 0; i < userids.size(); i++) {
int userid = userids.get(i);
// 2.查询离职人对应交接人
int jjr = this.queryHanderOver(userid, rs);
if (jjr == -1) {
continue;
}
// 3.查询当前离职人待办流程(转发、传阅、创建节点、归档节点等已过滤)
List<Integer> requestids = this.queryPendProcess(userid, rs);
if (requestids.isEmpty()) {
logger.info("用户【" + userid + "】名下暂无待办流程。");
continue;
}
String requestidStr = requestids.toString().replaceAll("^\\[|\\]$", "");
// 4.流程转移
processTransfe(userid, jjr, requestidStr, jsonObject);
}
}
/**
* 汇总当日离职人id
*
* @return
*/
private List<Integer> queryResign(String todayDate, RecordSetDataSource rs) {
List<Integer> userids = new ArrayList<>();
String sql = " select id from hrmresource where status in(4,5,6) and enddate ='" + todayDate + "'";
try {
rs.executeSql(sql);
while (rs.next()) {
userids.add(rs.getInt("id"));
}
} catch (Exception e) {
logger.error("sql执行错误. sql: " + sql);
throw new RuntimeException("processTransfer(流程转移)接口,查询当日离职人id异常:" + e.getMessage());
}
return userids;
}
/**
* 待办流程
*
* @param userid
* @return
*/
private List<Integer> queryPendProcess(int userid, RecordSetDataSource rs) {
List<Integer> requestids = new ArrayList<>();
StringBuffer sql = new StringBuffer(" select t1.requestid from workflow_requestbase t1 ")
.append(" inner join workflow_currentoperator t2 on t1.requestid = t2.requestid ")
.append(" inner join workflow_base t3 on t1.workflowid = t3.id ")
.append(" where (t1.deleted <> 1 or t1.deleted is null or t1.deleted='') ")
.append(" and t2.userid= " + userid + " and t2.usertype=0 ")
.append(" and ((t2.isremark=0 and (t2.takisremark is null or t2.takisremark=0 )) or t2.isremark =0) ")
.append(" and (t1.deleted=0 or t1.deleted is null) and t2.islasttimes=1 and (isnull(t1.currentstatus,-1) = -1 or (isnull(t1.currentstatus,-1)=0 and t1.creater=" + userid + " )) ")
.append(" and (t3.isvalid='1' or t3.isvalid='3') ") // 有效流程
.append(" and t1.currentnodetype in(1,2) ") // 过滤创建、归档节点
.append(" and t3.id not in(" +workflowid+ ")"); // 过滤的流程(如留言、系统异常工作流,可自定义传参)
try {
rs.executeSql(sql.toString());
while (rs.next()) {
requestids.add(rs.getInt("requestid"));
}
} catch (Exception e) {
logger.error("sql执行错误. sql: " + sql);
throw new RuntimeException("processTransfer(流程转移)接口,查询离职人待办流程异常:" + e.getMessage());
}
return requestids;
}
/**
* 查询离职人对应交接人
*
* @param userid
* @return
*/
private int queryHanderOver(int userid, RecordSetDataSource rs) {
String sql = " select top 1 jiaojr from " + TABLE_NAME + " where shenqr= " + userid + " order by shenqrq desc";
int jjr = -1;
try {
rs.executeSql(sql);
while (rs.next()) {
String jiaojrArr[] = rs.getString("jiaojr").split(",");
jjr = Integer.valueOf(jiaojrArr[0]);
}
} catch (Exception e) {
logger.error("sql执行错误. sql: " + sql);
throw new RuntimeException("processTransfer(流程转移)接口,查询离职人对应交接人异常:" + e.getMessage());
}
return jjr; // 交接人
}
/**
* 流程转移接口
*
* @param userid 转移人
* @param jjr 交接人
* @param requests 转移的流程
* @param jsonObject token 信息
*/
private void processTransfe(int userid, int jjr, String requests, JSONObject jsonObject) {
final String api = "/api/hrm/permissiontoadjust/processData";
Map<String, Object> map = new HashMap<>();
map.put("transferType", "resource");
map.put("authorityTag", "transfer");
map.put("fromid", userid);
map.put("toid", jjr);
map.put("T133IdStr", requests); // 转移的流程,requestid以逗号隔开
String ipAddress = "";
try {
InetAddress localHost = InetAddress.getLocalHost();
ipAddress = localHost.getHostAddress();
} catch (Exception e) {
logger.error("processTransfer(流程转移)接口,获取本机ip失败:" + e.getMessage());
throw new RuntimeException("processTransfer(流程转移)接口,获取本机ip失败:" + e.getMessage());
}
if (ipAddress.equals("")) {
ipAddress = "192.168.8.62"; // 正式环境可以替换为主节点:192.168.8.105
}
String encryptUserId = (String) jsonObject.get("encryptUserId");
String token = (String) jsonObject.get("token");
String appid = (String) jsonObject.get("appid");
// 将 map 转换为 x-www-form-urlencoded 格式的字符串
StringBuilder formData = new StringBuilder();
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (formData.length() > 0) {
formData.append("&");
}
formData.append(entry.getKey()).append("=").append(entry.getValue());
}
boolean isFinished = false;
try {
String resultJson = HttpRequest.post("http://" + ipAddress + ":8080" + api)
.header("Content-Type", "application/x-www-form-urlencoded")
.header("token", token)
.header("appid", appid)
.header("userid", encryptUserId)
.body(formData.toString()) // 使用 x-www-form-urlencoded 格式的字符串作为请求体
.execute().body();
JSONObject rootObject = JSONObject.parseObject(resultJson);
JSONObject messageObject = rootObject.getJSONObject("message"); // 嵌套的json
String fromName = "";
String toName = "";
int succCount = -1;
if (messageObject != null || !messageObject.isEmpty()) {
isFinished = messageObject.getBooleanValue("isFinished"); // 获取isFinished
fromName = messageObject.getString("from");
toName = messageObject.getString("to");
succCount = messageObject.getIntValue("succCount");
}
if (isFinished && !fromName.equals("") && !toName.equals("") && succCount != -1) {
logger.info("流程转移成功: 【from:" + fromName + ",to:" + toName + "】,共:" + succCount + "条流程。");
}
} catch (Exception e) {
logger.error("processTransfer(流程转移)接口,processTransfe失败!请联系管理员。 ");
throw new RuntimeException("processTransfer(流程转移)接口,processTransfe失败!请联系管理员:" + e.getMessage());
} finally {
saveLog(requests, userid, jjr, isFinished);
}
}
/**
* 获取一次即可,接口执行就获取
*
* @return
*/
private JSONObject getToken() {
// 获取token
LoginRegist loginRegist = new LoginRegist();
Map<String, Object> tokenMap = new HashMap<>();
tokenMap.put("workcode", "E1009132");
return JSONObject.parseObject(loginRegist.ecPost(tokenMap));
}
private void saveLog(String requestids, int fromid, int toid, boolean isFinished) {
requestids = requestids.replaceAll("\\s", ""); // 去空格
BatchRecordSet brs = new BatchRecordSet();
String[] dictKey = {"requestid", "from_id", "to_id", "operator", "transfer_time", "interface", "is_finished", "log"};
List<String> keys = Arrays.asList(dictKey);
String saveLogSql = DBUtil.makePrepareInsertSql(LOG_TABLE_NAME, keys); // 预编译sql
List<Map<String, Object>> workflowList = new ArrayList<>();
Map<String, Object> rowMap = null;
String[] requestidArray = requestids.split(",");
int index = 0;
while (index < requestidArray.length) {
rowMap = new HashMap<>();
rowMap.put("requestid", requestidArray[index]); // requestid
rowMap.put("from_id", String.valueOf(fromid)); // 流程原始审批人
rowMap.put("to_id", String.valueOf(toid)); // 转移的人
rowMap.put("operator", "schedule_interface"); // 默认传值计划任务接口
LocalDateTime currentDateTime = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String dateTime = currentDateTime.format(formatter);
rowMap.put("transfer_time", dateTime);
rowMap.put("interface", "com.comen.schedule.workflow.ProcessTransfer"); // 当前类路径
rowMap.put("is_finished", String.valueOf(isFinished));
rowMap.put("log", "人员离职后,已到达未处理流程转移。"); // 其他备注
workflowList.add(rowMap);
index++;
}
DBUtil.insertList2Table(brs, saveLogSql, keys, workflowList);
}
}
DBUtil.java
public class DBUtil extends BaseBean {
public static Log logger = LogFactory.getLog(DBUtil.class);
/**
* 获取自定义datasource的connection
*
* @param datasourceName
* 数据源id,为"datasource"+数据源配置里面的数据源名称,
* 比如配置了一个数据源名称为local,那么这个数据源id为:datasource.local
* @return
*/
public static Connection getConnection(String datasourceName) {
Connection conn = null;
try {
DataSource datasource = (DataSource) StaticObj.getServiceByFullname("datasource."+datasourceName, DataSource.class); //获取数据源的信息
conn = datasource.getConnection(); //和数据源取得连接
} catch (Exception e) {
e.printStackTrace();
logger.error(e);
}
return conn;
}
/**
* 获取自定义datasource的connection
* OA的数据源
* @return
*/
public static Connection getConnection() {
Connection conn = null;
try {
DataSource datasource = (DataSource) StaticObj.getServiceByFullname("datasource."+FinalString.DATASOURCE_OA, DataSource.class); //获取数据源的信息
conn = datasource.getConnection(); //和数据源取得连接
} catch (Exception e) {
e.printStackTrace();
logger.error(e);
}
return conn;
}
/**
* 关闭Connection
*
* @param conn
*/
public static void closeConnection(Connection conn) {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
logger.error(e);
}
}
}
/**
* 匹配sql
* @param sql 注意字符串要带''
* @param targets sql中对应的值
* @return 完整的sql
* @deprecated 该方法实际上只是把?替换成参数组装sql,既没有事务批处理的高效,也不能防止sql注入,建议使用void weaver.conn.BatchRecordSet.executeBatchSql(String arg0, List<List<Object>> arg1)替代。
*/
public static String prepareSQL(String sql, String... targets){
if(null == targets || targets.length==0){
return sql;
}
String resSql = sql;
for(int i = 0; i < targets.length; i++){
if(null == targets[i]){ //去掉null
targets[i] = "NULL";
}
if(targets[i].contains("'")){ //转换'
targets[i] = targets[i].replace("'", "''");
}
resSql = StringUtils.replaceOnce(resSql, "?", targets[i]);
}
return resSql;
}
/**
* 组装一个预编译的insert sql
* @param tableName
* @param keys
* @return 预编译的insert sql
*/
public static String makePrepareInsertSql(String tableName, List<String> keys) {
if(keys == null || keys.isEmpty()) {
return "";
}
StringBuffer insertSqlBuffer = new StringBuffer("insert into ").append(tableName).append("(");
StringBuffer valueSqlBuffer = new StringBuffer("values(");
for(String key: keys) {
insertSqlBuffer.append(key).append(",");
valueSqlBuffer.append("?,");
}
insertSqlBuffer.deleteCharAt(insertSqlBuffer.length()-1).append(")");
valueSqlBuffer.deleteCharAt(valueSqlBuffer.length()-1).append(")");
return insertSqlBuffer.append(" ").append(valueSqlBuffer).toString();
}
/**
* 插入数据到指定表中
* @param brs
* @param prepareSql
* @param keys
* @param data
*/
public static void insertList2Table(BatchRecordSet brs, String prepareSql, List<String> keys, List<Map<String,Object>> data) {
if(brs == null || StringUtils.isBlank(prepareSql) || keys == null || data == null) {
return;
}
if(prepareSql.replaceAll("[^\\?]", "").length() != keys.size()) {
return;
}
List<List<Object>> tableData = new ArrayList<List<Object>>();
List<Object> rowData = null;
for(Map<String,Object> map: data) {
rowData = new ArrayList<Object>();
for(String key: keys) {
rowData.add(map.get(key));
}
tableData.add(rowData);
}
brs.executeBatchSql(prepareSql, tableData);
}
}
三、日志记录
对于接口批量转移,没有任何记录,鉴于流程转移比较敏感,难免有业务人员追溯,因此可将每一次转移的流程进行日志记录。
表结构:
CREATE TABLE [ecology].[dbo].[transfer_log] (
[requestid] varchar(20) COLLATE Chinese_PRC_CI_AS,
[from_id] varchar(100) COLLATE Chinese_PRC_CI_AS, -- 离职人
[to_id] varchar(100) COLLATE Chinese_PRC_CI_AS, -- 转移的人
[operator] varchar(100) COLLATE Chinese_PRC_CI_AS, -- 操作者,写死interface
[transfer_time] varchar(100) COLLATE Chinese_PRC_CI_AS, -- 转移时间
[interface] varchar(500) COLLATE Chinese_PRC_CI_AS, -- 接口名
[is_finished] varchar(20) COLLATE Chinese_PRC_CI_AS, -- 是否完成,调用 /api/hrm/permissiontoadjust/processData 接口会返回
[log] varchar(999) COLLATE Chinese_PRC_CI_AS -- 其他日志
);
四、定时任务
该接口可设计成计划任务,每天晚上转移当日的离职人员流程,没有则跳过。