# 出勤二开钩子

# 前言：

- [**E10
  后端埋点开发文档**](https://weapp.eteams.cn/build/techdoc/wdoc/index.html#/online/951257517407772672/doc/951718040830124032)

- 二开钩子开发都会继承RegHookEvent类，从而实现handle的方法来开发具体的业务逻辑。

public abstract class RegHookEvent implements Serializable {  
protected static final Logger log =
LoggerFactory.getLogger(RegHookEvent.class);  
public int order;  
  
public RegHookEvent() {  
}  
  
public abstract WeaHookResponse handle(WeaHookRequest request,
WeaHookResponse response, AppInfo appInfo) throws Exception;  
  
public abstract void rollBack(WeaHookRequest request, WeaHookResponse
response, AppInfo appInfo) throws Exception;  
}

- 二开钩子的类都会加上钩子注解@WeaHookReg来注册是钩子身份。

@Retention(RetentionPolicy.RUNTIME)  
@Target(ElementType.TYPE)  
@Component  
public @interface WeaHookReg {  
  
/**  
* 埋点编码  
*/  
String code();  
  
int order() default 0;  
  
/**  
* 是否同步执行 默认 true  
*/  
boolean sync() default true;  
  
/* 状态 */  
boolean status() default true;  
  
}

# 1. 班次浏览按钮自定义SQL钩子

最低基线支持：0801

埋点编码：**code：ATTEND_SHIFT_BROWSER_CUSTOM_SQL_001**

## 1.1. 功能说明

该钩子可以满足客户不同场景下需要查询不同条件下的班次数据。

比如：

- 针对具体某个流程需要查询非工作日班次的数据。可以用此钩子进行二开。而无需改动源码。

## 1.2. 代码示例

该示例代码具体实现了通过入场的workflowId是否等于配置文件的shiftBrowserWorkflowId值，来控制查询到非工作日班次的浏览数据

@WeaHookReg(code = "ATTEND_SHIFT_BROWSER_CUSTOM_SQL_001")  
@Component  
public class ShiftBrowserCustomSqlHook extends RegHookEvent {  
  
protected final Logger logger = LoggerFactory.getLogger(getClass());  
  
@Value("\${attend.hook.shiftBrowser.workflowId}")  
private String shiftBrowserWorkflowId;  
  
  
@Override  
public WeaHookResponse handle(WeaHookRequest request, WeaHookResponse
response, AppInfo appInfo) throws Exception {  
Map\<String, Object\> params = request.getParams();  
if (params != null && params.size() \> 0) {  
if (params.containsKey("customParam")) {  
Map\<String, Object\> customParam = (Map) params.get("customParam");  
if (customParam.containsKey("workflowId")) {  
String workflowId = customParam.get("workflowId") + "";  
if (StringUtils.isBlank(shiftBrowserWorkflowId) &&
params.containsKey("attend.hook.shiftBrowser.workflowId")) {  
shiftBrowserWorkflowId = (String)
params.get("attend.hook.shiftBrowser.workflowId");  
}  
  
if (StringUtils.isNotBlank(shiftBrowserWorkflowId)) {  
String\[\] split = shiftBrowserWorkflowId.split(",");  
if (Arrays.asList(split).contains(workflowId)) {  
response.setResult("is_nonwork_shift = 1 ");  
}  
}  
}  
}  
}  
return response;  
}  
  
@Override  
public void rollBack(WeaHookRequest request, WeaHookResponse response,
AppInfo appInfo) throws Exception {  
// TODO  
logger.info("执行事件回滚机制 =\> {}, {}", DateUtil.now(),
appInfo.getAppName());  
}

## 1.3. 参数说明

-  Map\<String, Object\> params =
  request.getParams()可以获取埋点执行的入参数。根据入参的不通获取来控制条件筛选。

- shiftBrowserWorkflowId类参数可以通过配置文件配置具体的值，支持多个流程ID，用“,"间隔开。

- WeaHookResponse
  response中对response.setResult方法调用，可以把二开执行的结果返回给埋点中。用于埋点获取到执行结果的数据。比如示例代码中。返回的是“is_nonwork_shift
  = 1 ”这会把条件最终拼接到最终的 select 查询条件中。

# 2. 考勤规则浏览按钮自定义SQL钩子

最低基线支持：0801

埋点编码：**code：ATTEND_CONFIG_BROWSER_CUSTOM_SQL_001**

## 2.1. 功能说明

该钩子可以满足客户不同场景下需要查询不同条件下的考勤规则数据。

比如：

- 针对具体某个流程需要查询排班制度的数据。可以用此钩子进行二开。而无需改动源码。

## 2.2. 代码示例

@WeaHookReg(code = "ATTEND_CONFIG_BROWSER_CUSTOM_SQL_001")  
public class ConfigBrowserCustomSqlHook extends RegHookEvent {  
@Value("\${attend.hook.configBrowser.workflowId}")  
private String configBrowserWorkflowId;  
  
@Override  
public WeaHookResponse handle(WeaHookRequest request, WeaHookResponse
response, AppInfo appInfo) throws Exception {  
// TODO 具体的业务二开业务逻辑实现  
return response;  
}  
  
@Override  
public void rollBack(WeaHookRequest request, WeaHookResponse response,
AppInfo appInfo) throws Exception {  
// TODO  
log.info("执行事件回滚机制 =\> {}, {}", DateUtil.now(),
appInfo.getAppName());  
}  
}

## 2.3. 参数说明

-  Map\<String, Object\> params =
  request.getParams()可以获取埋点执行的入参数。根据入参的不通获取来控制条件筛选。

- configBrowserWorkflowId类参数可以通过配置文件配置具体的值，支持多个流程ID，用“,"间隔开。

- WeaHookResponse
  response中对response.setResult方法调用，可以把二开执行的结果返回给埋点中。用于埋点获取到执行结果的数据。最终的自定义
  SQL 赋值到setResult方法中。

# 3. 假期类型自定义SQL钩子

最低基线支持：0801

埋点编码：**code：ATTEND_VACATION_TYPE_BROWSER_CUSTOM_SQL_001**

## 3.1. 功能说明

该钩子可以满足客户不同场景下需要查询不同条件下的请假类型数据。

比如：

- 针对具体某个流程需要查询年假数据。可以用此钩子进行二开。而无需改动源码。

## 3.2. 代码示例

@WeaHookReg(code = "ATTEND_VACATION_TYPE_BROWSER_CUSTOM_SQL_001")  
public class VacationTypeBrowserCustomSqlHook extends RegHookEvent {  
  
@Value("\${attend.hook.vacationTypeBrowser.workflowId}")  
private String vacationTypeBrowserWorkflowId;  
  
@Override  
public WeaHookResponse handle(WeaHookRequest request, WeaHookResponse
response, AppInfo appInfo) throws Exception {  
// TODO  
return response;  
}  
  
@Override  
public void rollBack(WeaHookRequest request, WeaHookResponse response,
AppInfo appInfo) throws Exception {  
// TODO  
log.info("执行事件回滚机制 =\> {}, {}", DateUtil.now(),
appInfo.getAppName());  
}  
}

## 3.3. 参数说明

-  Map\<String, Object\> params =
  request.getParams()可以获取埋点执行的入参数。根据入参的不通获取来控制条件筛选。

- vacationTypeBrowserWorkflowId类参数可以通过配置文件配置具体的值，支持多个流程ID，用“,"间隔开。

- WeaHookResponse
  response中对response.setResult方法调用，可以把二开执行的结果返回给埋点中。用于埋点获取到执行结果的数据。最终的自定义
  SQL 赋值到setResult方法中。

# 4. 加班规则自定义SQL钩子

最低基线支持：0801

埋点编码：**code：ATTEND_OVERTIME_RULE_BROWSER_CUSTOM_SQL_001**

## 4.1. 功能说明

该钩子可以满足客户不同场景下需要查询不同条件下的加班规则数据。

比如：

- 针对具体某个流程需要查询不同加班规则数据。可以用此钩子进行二开。而无需改动源码。

## 4.2. 代码示例

@WeaHookReg(code = "ATTEND_OVERTIME_RULE_BROWSER_CUSTOM_SQL_001")  
public class OvertimeRuleBrowserCustomSqlHook extends RegHookEvent {  
  
@Value("\${attend.hook.overtimeRuleBrowser.workflowId}")  
private String overtimeRuleBrowserWorkflowId;  
  
@Override  
public WeaHookResponse handle(WeaHookRequest request, WeaHookResponse
response, AppInfo appInfo) throws Exception {  
// TODO  
return response;  
}  
  
@Override  
public void rollBack(WeaHookRequest request, WeaHookResponse response,
AppInfo appInfo) throws Exception {  
// TODO  
log.info("执行事件回滚机制 =\> {}, {}", DateUtil.now(),
appInfo.getAppName());  
}  
}

## 4.3. 参数说明

-  Map\<String, Object\> params =
  request.getParams()可以获取埋点执行的入参数。根据入参的不通获取来控制条件筛选。

- overtimeRuleBrowserWorkflowId类参数可以通过配置文件配置具体的值，支持多个流程ID，用“,"间隔开。

- WeaHookResponse
  response中对response.setResult方法调用，可以把二开执行的结果返回给埋点中。用于埋点获取到执行结果的数据。最终的自定义
  SQL 赋值到setResult方法中。

# 5. 出勤日历返回的打卡结果增加口子

埋点编码：**code：ATTEND_CALENDAR_LIST_SIGN_CUSTOM_RECORD_001**

## 5.1. 功能说明

该钩子可以满足不同客户在出勤日历场景下展示更多的打卡记录。

比如：

- 一天一个时段打卡记录只有 08:00 的签到卡和 18:00
  的签退卡，但是有些客户想把中午 12:00 的开始吃饭卡以及 13:00
  的吃完午饭的卡也记录上，因此可以用此钩子，客户自己二开来实现把 12:00
  以及 13:00 的卡展示到日历上。

## 5.2. 代码示例

@WeaHookReg(code = "ATTEND_CALENDAR_LIST_SIGN_CUSTOM_RECORD_001")  
@Component  
public class AttendCalendarListSignCustomRecord extends RegHookEvent {  
@Override  
public WeaHookResponse handle(WeaHookRequest request, WeaHookResponse
response, AppInfo appInfo) throws Exception {  
// 插入新打卡记录的结果集  
List\<Map\<String, Object\>\> newResult = new ArrayList\<\>();  
// 获取传入的参数信息  
Map\<String, Object\> params = request.getParams();  
// 之前打卡的结果集合  
List\<Map\<String, Object\>\> result = (List\<Map\<String, Object\>\>)
params.get("result");  
// 员工 Id  
Long id = (Long) params.get("empId");  
// 员工说对应的出勤日期  
String attendDate = params.get("attendDate") + "";  
// 该日期对应的考勤开始时段  
String periodStart = params.get("periodStart") + "";  
// 该日期对应的考勤的结束时段  
String periodEnd = params.get("periodEnd") + "";  
  
//
要先把原先的打卡记录原封不动增加到你新的新打卡记录的结果集newResult集合中，在把你要插入的打卡信息也要add到newResult中  
if (CollectionUtils.isNotEmpty(result)) {  
newResult.addAll(result);  
}  
// TODO
模拟插入其他打卡记录，（注意可能你插入的卡记录需要和原先的打卡记录做一下排序，比如原先打卡
08：00的签到，  
// 以及 18：00的签退，所以需要把 12：00的以及
13：00的插入到这两个卡之间，需要自己处理）  
/**  
* 比如你插入中午 12：00的开始吃法打卡记录，  
* 以及 13：00吃完饭的打卡记录这两条数据  
*/  
newResult.add(getEventMap("12:00:00 吃午饭开始卡", attendDate,
attendDate, "NORMAL"));  
newResult.add(getEventMap("13:00:00 吃午饭结束卡", attendDate,
attendDate, "NORMAL"));  
response.setResult(newResult);  
return response;  
}  
  
/**  
* 这是封装你要新增的新打卡记录map  
*  
* @param title 具体展示到日历时的内容展示（一般都是打卡时间点）  
* @param start 具体对应的日期字符串  
* @param end 具体对应的日期字符串  
* @param type
对应的这个卡的状态，不同状态会在日历上展示不同颜色，不如正常，迟到，早退等  
* @return  
*/  
private Map\<String, Object\> getEventMap(String title, String start,
String end, String type) {  
Map\<String, Object\> map = Maps.newHashMap();  
map.put("title", title);  
map.put("start", start);  
map.put("end", end);  
map.put("type", type);  
return map;  
}  
  
@Override  
public void rollBack(WeaHookRequest request, WeaHookResponse response,
AppInfo appInfo) throws Exception {  
  
}  
}

## 5.3. 参数说明

**具体的参数代码示例都很清楚的展示出来了**

获取Map\<String, Object\> params =
request.getParams()的对象是传给二开的参数集合，具体对应的字段解释如下：

|  |  |  |
|:--:|:--:|:--:|
| **key** | **含义** | **备注** |
| empId | 员工的 Id 值，Long 类型 |  |
| attendDate | 员工对应哪一天的出勤日，字符串 | 比如 2024-08-01 |
| periodStart |  对应这一天的考勤时段开始时间，字符串 | 比如 08:00 |
| periodEnd | 对应这一天的考勤时段结束时间，字符串 | 比如 18:00 |
| result | 对应的这天本身的打卡记录数据 List\<Map\<String, Object\>\>类型 | 比如会有两条记录，分别对应 08:00 的签到卡，以及 18:00 的签退卡 |

具体 result 的格式如下：

<table>
<colgroup>
<col style="width: 41%" />
<col style="width: 31%" />
<col style="width: 26%" />
</colgroup>
<tbody>
<tr>
<td style="text-align: center;">key</td>
<td style="text-align: center;">含义</td>
<td style="text-align: center;">备注</td>
</tr>
<tr>
<td style="text-align: left;">title</td>
<td style="text-align: left;">具体展示在日历的上的样式</td>
<td style="text-align: left;">比如 08:00:00 正常</td>
</tr>
<tr>
<td style="text-align: left;">start</td>
<td style="text-align: left;">开始日期，字符串</td>
<td style="text-align: left;">比比如 2024-08-01</td>
</tr>
<tr>
<td style="text-align: left;">end</td>
<td style="text-align: left;">结束日期，字符串，一般start和
end值是一样的</td>
<td style="text-align: left;">比如 2024-08-01</td>
</tr>
<tr>
<td style="text-align: left;">type</td>
<td
style="text-align: left;">对应的这个卡的状态，不同状态会在日历上展示不同颜色，不如正常，迟到，早退等</td>
<td style="text-align: left;"><p>NORMAL("正常")</p>
<p>SIGN_IN_NORMAL("正常签到"),</p>
<p>SIGN_IN_LATE("迟到"),</p>
<p>SIGN_IN_ABSENCE("未签到"),</p>
<p>SIGN_IN_ABSENTEEISM("旷工"),</p>
<p>SIGN_OUT_NORMAL("正常签退"),</p>
<p>SIGN_OUT_ABSENCE("未签退"),</p>
<p>SIGN_OUT_EARLY("早退"),</p>
<p>SIGN_OUT_ABSENTEEISM("旷工"),</p>
<p>SIGN_IN_ABSENCE_ONLY("未签到"),</p>
<p>OATTEND("外勤"),</p>
<p>OCHECKIN("外勤签到"),</p>
<p>OCHECKOUT("外勤签退"),</p>
<p>NO_SIGN("免打卡"),</p>
<p>SERIOUSLY_LATE("严重迟到"),</p>
<p>SERIOUSLY_EARLY("严重早退");</p></td>
</tr>
</tbody>
</table>

# 出勤日历单元格中加班信息自定义显示钩子

最低基线版本：10.0.2410.01

埋点编码：ATTEND_CALENDAR_LIST_OVERTIME_CUSTOM_RECORD_001

## 6.1.功能说明

该钩子可以满足客户需要根据打卡时间限制日历单元格中加班数据内容

eg:
9月15号改员工于09:00上班，22:00下班。加班流程中填写的时间为18:00-23:00。对超出下班卡外不算加班的公司来说，可以利用改钩子对标准功能中展示的加班时间进行修改

## 6.2.代码示例

package com.weaver.seconddev;  
  
import com.alibaba.fastjson.JSONObject;  
import com.alibaba.fastjson.TypeReference;  
import com.google.common.collect.Maps;  
import com.weaver.loom.context.annotation.WeaHookReg;  
import com.weaver.loom.context.domain.AppInfo;  
import com.weaver.loom.context.domain.WeaHookRequest;  
import com.weaver.loom.context.domain.WeaHookResponse;  
import com.weaver.loom.context.register.RegHookEvent;  
import org.springframework.stereotype.Component;  
  
import java.util.ArrayList;  
import java.util.List;  
import java.util.Map;  
  
@WeaHookReg(code = "ATTEND_CALENDAR_LIST_OVERTIME_CUSTOM_RECORD_001")  
@Component  
public class AttendCalendarListOverTimeRecordDev extends RegHookEvent
{  
  
@Override  
public WeaHookResponse handle(WeaHookRequest request, WeaHookResponse
response, AppInfo appInfo) throws Exception {  
Map\<String, Object\> params = request.getParams();  
String dateStr = (String) params.get("dateStr");  
String flowStart = (String) params.get("flowStart");  
String flowEnd = (String) params.get("flowEnd");  
String columnType = (String) params.get("columnType");  
List\<Map\<String, String\>\> signInfo =
JSONObject.parseObject(JSONObject.toJSONString(params.get("signInfo")),
new TypeReference\<List\<Map\<String, String\>\>\>() {  
});  
List\<Map\<String, Object\>\> oldData =
JSONObject.parseObject(JSONObject.toJSONString(params.get("data")), new
TypeReference\<List\<Map\<String, Object\>\>\>() {  
});  
List\<Map\<String, Object\>\> result = new ArrayList\<\>(oldData);  
// 只处理一个时段的班次  
if (signInfo == null \|\| signInfo.size() != 1) {  
response.setResult(result);  
return response;  
}  
String signInDate = signInfo.get(0).get("signInDateStr");  
String signOutDate = signInfo.get(0).get("signOutDateStr");  
  
String actualStart = flowStart.compareTo(signInDate) \< 0 ? signInDate :
flowStart;  
String actualEnd = flowEnd.compareTo(signOutDate) \> 0 ? signOutDate :
flowEnd;  
  
result.add(getEventMap(actualStart.split(" ")\[1\] + "-" +
actualEnd.split(" ")\[1\] + " " + columnType, dateStr, dateStr,
"OVERTIME"));  
response.setResult(result);  
return response;  
}  
  
private Map\<String, Object\> getEventMap(String title, String start,
String end, String type) {  
Map\<String, Object\> map = Maps.newHashMap();  
map.put("title", title);  
map.put("start", start);  
map.put("end", end);  
map.put("type", type);  
return map;  
}  
  
@Override  
public void rollBack(WeaHookRequest request, WeaHookResponse response,
AppInfo appInfo) throws Exception {  
  
}  
}

## 6.3.参数说明

入参：

<table>
<colgroup>
<col style="width: 30%" />
<col style="width: 27%" />
<col style="width: 42%" />
</colgroup>
<tbody>
<tr>
<td style="text-align: left;">字段名称</td>
<td style="text-align: left;">数据类型</td>
<td style="text-align: left;">备注</td>
</tr>
<tr>
<td style="text-align: left;">dateStr</td>
<td style="text-align: left;">String</td>
<td style="text-align: left;">考勤日期，格式：yyyy-MM-dd</td>
</tr>
<tr>
<td style="text-align: left;">flowStart</td>
<td style="text-align: left;">String</td>
<td style="text-align: left;">流程开始时间，格式：yyyy-MM-dd
HH:mm:ss</td>
</tr>
<tr>
<td style="text-align: left;">flowEnd</td>
<td style="text-align: left;">String</td>
<td style="text-align: left;">流程结束时间，格式：yyyy-MM-dd
HH:mm:ss</td>
</tr>
<tr>
<td style="text-align: left;">columnType</td>
<td style="text-align: left;">String</td>
<td style="text-align: left;">【加班】国际化翻译</td>
</tr>
<tr>
<td style="text-align: left;">signInfo</td>
<td style="text-align: left;">List&lt;Map&lt;String, String&gt;&gt;</td>
<td style="text-align: left;"><p>signInDateStr：签到卡时间</p>
<p>signOutDateStr：签退卡时间</p></td>
</tr>
<tr>
<td style="text-align: left;">data</td>
<td style="text-align: left;">List&lt;Map&lt;String, Object&gt;&gt;</td>
<td style="text-align: left;">口子返回的数据需要适配data的数据类型</td>
</tr>
</tbody>
</table>

# columnType角加班信息自定义显示钩子

最低基线版本：10.0.2410.01

埋点编码：ATTEND_CALENDAR_DETAIL_OVERTIME_CUSTOM_INFO_001

## 7.1.功能说明

该钩子可以满足客户需要根据打卡时间限制出勤明细左下角加班流程中流程时间的功能

eg:
9月15号改员工于09:00上班，22:00下班。加班流程中填写的时间为18:00-23:00。对超出下班卡外不算加班的公司来说，可以利用改钩子对标准功能中展示的加班时间进行修改

## 7.2.代码示例

package com.weaver.seconddev.attend.overtime;  
  
import com.alibaba.fastjson.JSONObject;  
import com.alibaba.fastjson.TypeReference;  
import com.weaver.loom.context.annotation.WeaHookReg;  
import com.weaver.loom.context.domain.AppInfo;  
import com.weaver.loom.context.domain.WeaHookRequest;  
import com.weaver.loom.context.domain.WeaHookResponse;  
import com.weaver.loom.context.register.RegHookEvent;  
import org.springframework.stereotype.Component;  
  
import java.util.ArrayList;  
import java.util.HashMap;  
import java.util.List;  
import java.util.Map;  
  
@WeaHookReg(code = "ATTEND_CALENDAR_DETAIL_OVERTIME_CUSTOM_INFO_001")  
@Component  
public class AttendCalendarDetailOverTimeInfoSecDev extends RegHookEvent
{  
@Override  
public WeaHookResponse handle(WeaHookRequest request, WeaHookResponse
response, AppInfo appInfo) throws Exception {  
List\<Map\<String, String\>\> result = new ArrayList\<\>();  
Map\<String, Object\> params = request.getParams();  
String startTimeLabel = (String) params.get("startTimeLabel");  
String startTime = (String) params.get("startTime");  
String endTimeLabel = (String) params.get("endTimeLabel");  
String endTime = (String) params.get("endTime");  
String typeLabel = (String) params.get("typeLabel");  
String type = (String) params.get("type");  
String timeLengthLabel = (String) params.get("timeLengthLabel");  
String timeLengthDay = (String) params.get("timeLengthDay");  
String timeLengthHour = (String) params.get("timeLengthHour");  
String dayUnitLabel = (String) params.get("dayUnitLabel");  
String hourUnitLabel = (String) params.get("hourUnitLabel");  
  
List\<Map\<String, String\>\> signInfo =
JSONObject.parseObject(JSONObject.toJSONString(params.get("signInfo")),
new TypeReference\<List\<Map\<String, String\>\>\>() {  
});  
  
// 只处理一个时段的班次  
if (signInfo == null \|\| signInfo.size() != 1) {  
response.setResult(result);  
return response;  
}  
String signInDate = signInfo.get(0).get("signInDateStr");  
String signOutDate = signInfo.get(0).get("signOutDateStr");  
  
String actualStart = startTime.compareTo(signInDate) \< 0 ? signInDate :
startTime;  
String actualEnd = endTime.compareTo(signOutDate) \> 0 ? signOutDate :
endTime;  
  
result.add(getTipMap(startTimeLabel + "：" + actualStart, "text",
""));  
result.add(getTipMap(endTimeLabel + "：" + actualEnd, "text", ""));  
result.add(getTipMap(typeLabel + "：" + type, "text", ""));  
result.add(getTipMap(timeLengthLabel + "：" + timeLengthHour + "(" +
hourUnitLabel + ")", "text", ""));  
response.setResult(result);  
return response;  
}  
  
private Map\<String, String\> getTipMap(String label, String type,
String id) {  
Map\<String, String\> tip = new HashMap\<\>();  
tip.put("label", label);  
tip.put("type", type);  
tip.put("id", id);  
return tip;  
}  
  
@Override  
public void rollBack(WeaHookRequest request, WeaHookResponse response,
AppInfo appInfo) throws Exception {  
  
}  
}

## 7.3.参数说明

入参：

<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<tbody>
<tr>
<td style="text-align: left;">字段名称</td>
<td style="text-align: left;">数据类型</td>
<td style="text-align: left;">备注</td>
</tr>
<tr>
<td style="text-align: left;">startTimeLabel</td>
<td style="text-align: left;">String</td>
<td style="text-align: left;">【开始时间】国际化翻译</td>
</tr>
<tr>
<td style="text-align: left;">startTime</td>
<td style="text-align: left;">String</td>
<td style="text-align: left;">流程开始时间：yyyy-MM-dd HH:mm:ss</td>
</tr>
<tr>
<td style="text-align: left;">endTimeLabel</td>
<td style="text-align: left;">String</td>
<td style="text-align: left;">【结束时间】国家化翻译</td>
</tr>
<tr>
<td style="text-align: left;">endTime</td>
<td style="text-align: left;">String</td>
<td style="text-align: left;">流程结束时间：yyyy-MM-dd HH:mm:ss</td>
</tr>
<tr>
<td style="text-align: left;">typeLabel</td>
<td style="text-align: left;">String</td>
<td style="text-align: left;">【类型】国际化翻译</td>
</tr>
<tr>
<td style="text-align: left;">type</td>
<td style="text-align: left;">String</td>
<td style="text-align: left;">【加班】国际化翻译</td>
</tr>
<tr>
<td style="text-align: left;">timeLengthLabel</td>
<td style="text-align: left;">String</td>
<td style="text-align: left;">【时长】国际化翻译</td>
</tr>
<tr>
<td style="text-align: left;">timeLengthDay</td>
<td style="text-align: left;">Double</td>
<td style="text-align: left;">加班时长（天）</td>
</tr>
<tr>
<td style="text-align: left;">timeLengthHour</td>
<td style="text-align: left;">Double</td>
<td style="text-align: left;">加班时长（小时）</td>
</tr>
<tr>
<td style="text-align: left;">dayUnitLabel</td>
<td style="text-align: left;">String</td>
<td style="text-align: left;">【天】国际化翻译</td>
</tr>
<tr>
<td style="text-align: left;">hourUnitLabel</td>
<td style="text-align: left;">String</td>
<td style="text-align: left;">【小时】国际化翻译</td>
</tr>
<tr>
<td style="text-align: left;">signInfo</td>
<td style="text-align: left;">List&lt;Map&lt;String, String&gt;&gt;</td>
<td style="text-align: left;"><p>signInDateStr：签到卡时间</p>
<p>signOutDateStr：签退卡时间</p></td>
</tr>
</tbody>
</table>

口子需要返回List\<Map\<String,
String\>\>结构数据，用于替换原有的左下角流程信息

# 8、打卡生成加班增加限定预留钩子

最低基线版本：10.0.2409.01

埋点编码：ATTEND_OVERTIME_CARDCHECK_001

## 8.1.功能说明

该钩子可以自定义加班打卡校验规则。

比如：

- 加班打卡需要满足必须有签到和签退才可以生成加班。可以用此钩子进行二开。而无需改动源码。

## 8.2.代码示例

@@WeaHookReg(code = "ATTEND_OVERTIME_CARDCHECK_001", order = 0)

public class WeaHookOvertimeCardCheckReg extends RegHookEvent {

protected static final Logger logger =
LoggerFactory.getLogger(WeaHookOvertimeCardCheckReg.class);

@Override

public WeaHookResponse handle(WeaHookRequest request, WeaHookResponse
response, AppInfo appInfo) throws Exception {

//入参，修改入参并不会影响到埋点的原始数据

Map\<String, Object\> params = request.getParams();

logger.info("getSignCardRange:"+params);

Map\<Long,Map\<String,Long\>\> signMaps = null;

if(params.containsKey("signMaps")){

signMaps = (Map\<Long,Map\<String,Long\>\>) params.get("signMaps");

if(MapUtils.isNotEmpty(signMaps)){

for (Map.Entry\<Long,Map\<String,Long\>\> me : signMaps.entrySet()){

Map\<String,Long\> signMap = me.getValue();

signMap.put("ok", 0L);

if(signMap.containsKey("in") && signMap.containsKey("out")){

signMap.put("ok", 1L);

}

}

}

response.setResult(signMaps);

}

return response;

}

@Override

public void rollBack(WeaHookRequest request, WeaHookResponse response,
AppInfo appInfo) throws Exception {

}

}

## 8.3.参数说明

获取Map\<String, Object\> params =
request.getParams()的对象是传给二开的参数集合，具体对应的字段解释如下：

|  |  |  |
|:--:|:--:|:--:|
| **key** | **含义** | **备注** |
| signMaps | 对应的这天本身的打卡记录数据Map\<Long,Map\<String,Long\>\>类型 | 外层map的key是时段的id，内部map中key是in表示签到，out表示签退 |

具体 result 的格式如下：

<table>
<colgroup>
<col style="width: 28%" />
<col style="width: 21%" />
<col style="width: 49%" />
</colgroup>
<tbody>
<tr>
<td style="text-align: center;">key</td>
<td style="text-align: center;">含义</td>
<td style="text-align: center;">备注</td>
</tr>
<tr>
<td style="text-align: left;">signMaps</td>
<td style="text-align: left;">返回的还是入参的signMaps</td>
<td
style="text-align: left;"><p>可以自定义过滤signMaps，内层的map可以如下过滤:</p>
<p>signMap.put("ok", 0L)表示这个打卡时段不生成加班</p>
<p>signMap.put("ok", 1L)表示这个打卡时段可以生成加班</p></td>
</tr>
</tbody>
</table>

# 9、考勤机打卡数据回写oa

最低基线版本：10.0.2409.01

埋点编码：ATTEND_MACHINE_CHECK_CUSTOM_001

## 9.1功能说明

根据传入口子中的数据来源（打卡方式），返回true或者false，处理打卡数据是否舍弃

## 9.2代码示例

package com.weaver.seconddev.attend.check;  
  
import com.weaver.loom.context.annotation.WeaHookReg;  
import com.weaver.loom.context.domain.AppInfo;  
import com.weaver.loom.context.domain.WeaHookRequest;  
import com.weaver.loom.context.domain.WeaHookResponse;  
import com.weaver.loom.context.register.RegHookEvent;  
import org.springframework.stereotype.Component;  
  
import java.util.Map;  
import java.util.Objects;  
  
@WeaHookReg(code = "ATTEND_MACHINE_CHECK_CUSTOM_001")  
@Component  
public class AttendMachineCheckSecDev extends RegHookEvent {  
  
@Override  
public WeaHookResponse handle(WeaHookRequest request, WeaHookResponse
response, AppInfo appInfo) throws Exception {  
Map\<String, Object\> params = request.getParams();  
String verifyMode = (String) params.get("verifyMode");  
String result = "false";  
if (Objects.equals("Fp", verifyMode)) {  
result = "true";  
}  
response.setResult(result);  
return response;  
}  
  
@Override  
public void rollBack(WeaHookRequest request, WeaHookResponse response,
AppInfo appInfo) throws Exception {  
  
}  
}

## 9.3参数说明

req：

<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<tbody>
<tr>
<td style="text-align: left;">参数名称</td>
<td style="text-align: left;">数据类型</td>
<td style="text-align: left;">说明</td>
</tr>
<tr>
<td style="text-align: left;">verifyMode</td>
<td style="text-align: left;">String</td>
<td style="text-align: left;"><p>Fp:指纹</p>
<p>Face:人脸</p>
<p>Pass:密码</p>
<p>Car:刷卡</p>
<p>Palm:掌纹</p></td>
</tr>
</tbody>
</table>

resp:

true:计入打卡数据

false:不计入打卡数据

# 出勤汇总报表日历口子

最低基线版本：10.0.2411.01

埋点编码：ATTEND_WORKTIME_SUMMARY_REPORT_DAILY_001

attend库attend_configuration配置表增加开关，开关必须打开，否则口子不生效（为了不影响标准功能的性能）

config_key:ATTEND_HOOKATTEND_WORKTIME_SUMMARY_REPORT_DAILY_001

value:true

tenant_key:allTenant

## 10.1 功能说明

根据传入的流程、班次、假期类型等数据，修改单元格内容

比如：迟到早退不算异常

## 10.2 代码示例

package com.weaver.seconddev;

import com.alibaba.fastjson.JSONObject;

import com.alibaba.fastjson.TypeReference;

import com.google.common.collect.HashBasedTable;

import com.google.common.collect.Maps;

import com.google.common.collect.Range;

import com.google.common.collect.Table;

import com.google.common.collect.TreeRangeSet;

import com.weaver.attend.common.utils.AttendCommonUtils;

import com.weaver.attendweb.util.Attendi18Util;

import com.weaver.common.i18n.label.SystemEnv;

import com.weaver.loom.context.annotation.WeaHookReg;

import com.weaver.loom.context.domain.AppInfo;

import com.weaver.loom.context.domain.WeaHookRequest;

import com.weaver.loom.context.domain.WeaHookResponse;

import com.weaver.loom.context.register.RegHookEvent;

import com.weaver.teams.attend.entity.AttendVacationType;

import com.weaver.teams.attend.enums.AttendAppealType;

import com.weaver.teams.attend.enums.AttendMinUnit;

import com.weaver.teams.attend.enums.HalfDayType;

import com.weaver.teams.attend.secdev.entity.AttendShiftSecDev;

import
com.weaver.teams.attend.secdev.entity.AttendStatusDetailLinkSecDev;

import com.weaver.teams.attend.secdev.entity.AttendStatusDetailSecDev;

import
com.weaver.teams.attend.secdev.entity.AttendStatusRestDetailSecDev;

import com.weaver.teams.attend.secdev.entity.DateRange;

import com.weaver.teams.security.context.TenantContext;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.collections.CollectionUtils;

import org.apache.commons.collections.MapUtils;

import org.apache.commons.compress.utils.Lists;

import org.apache.commons.lang3.BooleanUtils;

import org.apache.commons.lang3.StringUtils;

import org.apache.commons.lang3.time.DateFormatUtils;

import org.joda.time.LocalDateTime;

import org.joda.time.LocalTime;

import org.springframework.stereotype.Component;

import java.util.ArrayList;

import java.util.Arrays;

import java.util.Collection;

import java.util.Date;

import java.util.LinkedHashSet;

import java.util.List;

import java.util.Map;

import java.util.Objects;

import java.util.Optional;

import java.util.Set;

import java.util.concurrent.atomic.AtomicLong;

import java.util.concurrent.atomic.AtomicReference;

import java.util.function.Function;

import java.util.stream.Collectors;

@WeaHookReg(code = "ATTEND_WORKTIME_SUMMARY_REPORT_DAILY_001")

@Component()

@Slf4j

public class AttendWorkTimeSummaryDaily extends RegHookEvent {

private static final long serialVersionUID = -3591354030652081264L;

private static final ThreadLocal\<Map\<Long, AttendShiftSecDev\>\>
attendShiftMap = new ThreadLocal\<\>();

private static final ThreadLocal\<Map\<Long, AttendVacationType\>\>
attendVacationTypeMap = new ThreadLocal\<\>();

@Override

public WeaHookResponse handle(WeaHookRequest request, WeaHookResponse
response, AppInfo appInfo) throws Exception {

Map\<String, Object\> params = request.getParams();

// 原始数据

List\<Map\<String, Object\>\> data =
JSONObject.parseObject(JSONObject.toJSONString(params.get("data")), new
TypeReference\<List\<Map\<String, Object\>\>\>(){});

// 考勤规则相关信息

try {

List\<AttendStatusDetailSecDev\> attendstatusDetails =
JSONObject.parseObject(JSONObject.toJSONString(params.get("statusDetails")),
new TypeReference\<List\<AttendStatusDetailSecDev\>\>() {

});

Table\<String, String, List\<AttendStatusDetailSecDev\>\>
perDayStatusDetailTable = HashBasedTable.create();

Optional.ofNullable(attendstatusDetails).ifPresent(list -\>

list.forEach(detail -\> {

String empIdStr = String.valueOf(detail.getEmployee().getId());

String attendDateStr = DateFormatUtils.format(detail.getAttendDate(),
"yyyy-MM-dd");

List\<AttendStatusDetailSecDev\> statusDetailSecDevList =
Optional.ofNullable(perDayStatusDetailTable.get(empIdStr,
attendDateStr)).orElse(Lists.newArrayList());

statusDetailSecDevList.add(detail);

perDayStatusDetailTable.put(empIdStr, attendDateStr,
statusDetailSecDevList);

})

);

// 流程相关信息

List\<AttendStatusDetailLinkSecDev\> attendLinkDetails =
JSONObject.parseObject(JSONObject.toJSONString(params.get("statusDetailLinks")),
new TypeReference\<List\<AttendStatusDetailLinkSecDev\>\>() {

});

Table\<String, String, List\<AttendStatusDetailLinkSecDev\>\>
perDayLinkDetailTable = HashBasedTable.create();

Optional.ofNullable(attendLinkDetails).ifPresent(list -\>

list.forEach(detail -\> {

String empIdStr = String.valueOf(detail.getEmployee().getId());

String attendDateStr = DateFormatUtils.format(detail.getAttendDate(),
"yyyy-MM-dd");

List\<AttendStatusDetailLinkSecDev\> linkDetailSecDevList =
Optional.ofNullable(perDayLinkDetailTable.get(empIdStr,
attendDateStr)).orElse(Lists.newArrayList());

linkDetailSecDevList.add(detail);

perDayLinkDetailTable.put(empIdStr, attendDateStr,
linkDetailSecDevList);

})

);

// 班次配置

attendShiftMap.set(convert2Map(JSONObject.parseObject(JSONObject.toJSONString(params.get("shiftList")),
new TypeReference\<List\<AttendShiftSecDev\>\>() {

}), AttendShiftSecDev::getId));

// 流程配置

attendVacationTypeMap.set(convert2Map(JSONObject.parseObject(JSONObject.toJSONString(params.get("vacationSettings")),
new TypeReference\<List\<AttendVacationType\>\>() {

}), AttendVacationType::getId));

// 口子执行前后端返回的数据，口子处理完后要在response里返回

// data中attend_status属性说明：0:休息; 1:正常; 2:迟到; 3:严重迟到;
4:早退; 5:严重早退; 6:未签到; 7:旷工; 8-请假类型:请假; 9:出差; 10:公出;
11:加班; 12: 补签; 13: 补卡;14:未签退;15:未打卡(未签到&&未签退)

buildDayStatus(perDayLinkDetailTable, perDayStatusDetailTable, data);

} catch (Exception e) {

log.error("invoke secdev error.", e);

} finally {

attendShiftMap.remove();

attendVacationTypeMap.remove();

}

response.setResult(data);

return response;

}

private void buildDayStatus(Table\<String, String,
List\<AttendStatusDetailLinkSecDev\>\> perDayLinkDetailTable,

Table\<String, String, List\<AttendStatusDetailSecDev\>\>
perDayStatusDetailTable,

List\<Map\<String, Object\>\> data) {

Set\<Table.Cell\<String, String, List\<AttendStatusDetailSecDev\>\>\>
cells = perDayStatusDetailTable.cellSet();

cells.forEach(perDayDetail -\> {

String empIdStr = perDayDetail.getRowKey();

String attendDateStr = perDayDetail.getColumnKey();

List\<AttendStatusDetailSecDev\> perDayStatusValue =
perDayDetail.getValue();

Map\<String, Object\> statusMap = data.stream().filter(e -\>
Objects.equals(e.get("employee"), empIdStr) &&
Objects.equals(e.get("attend_date"),
attendDateStr)).findFirst().orElse(Maps.newHashMap());

String dayStatus = (String) statusMap.getOrDefault("day_status", "");

String\[\] statusArr = dayStatus.split(",");

List\<AttendStatusDetailLinkSecDev\> perDayLinkValue =
perDayLinkDetailTable.get(empIdStr, attendDateStr);

if (CollectionUtils.isNotEmpty(perDayStatusValue) &&
MapUtils.isNotEmpty(statusMap)) {

ArrayList\<String\> statusList = new
ArrayList\<\>(Arrays.asList(statusArr));

// 迟到早退不算异常

if ((statusList.contains("2") \|\| statusList.contains("4")) &&
!statusList.contains("1")) {

statusList.add(0, "1");

statusMap.put("day_status", String.join(",", statusList));

}

if (CollectionUtils.isNotEmpty(perDayLinkValue)) {

TreeRangeSet\<Date\> leaveFlowRangeSet = TreeRangeSet.create(); //
请假流程区间

TreeRangeSet\<Date\> outsideFlowRangeSet = TreeRangeSet.create(); //
公出流程区间

TreeRangeSet\<Date\> bussinessTripFlowRangeSet = TreeRangeSet.create();
// 出差流程区间

TreeRangeSet\<Date\> periodRangeSet = TreeRangeSet.create(); //
应出勤区间

TreeRangeSet\<Date\> restRangeSet = TreeRangeSet.create(); // 休息区间

Set\<String\> vacationNameList = new LinkedHashSet\<\>(); //
存放所有的请假类型名称

for (AttendStatusDetailSecDev value : perDayStatusValue) {

DateRange periodRange = value.getPeriodRange();

if (periodRange != null && periodRange.getStart() != null &&
periodRange.getEnd() != null) {

periodRangeSet.add(periodRange.asRange());

}

if (value.getRestRange() != null && value.getRestRange().asRange() !=
null) {

restRangeSet.add(value.getRestRange().asRange());

}

List\<Range\<Date\>\> mulRestRanges = Lists.newArrayList();

if (value.getAttendStatusRestDetail() != null) {

mulRestRanges = getMulRestDateRange(value.getAttendStatusRestDetail());

for (int j = 0; j \< mulRestRanges.size(); j++) {

Range\<Date\> mulRest = mulRestRanges.get(j);

restRangeSet.add(mulRest);

}

}

}

List\<AttendStatusDetailLinkSecDev\> leaveLinkValue =
perDayLinkValue.stream().filter(e -\> e.getAppealType() ==
AttendAppealType.leave).collect(Collectors.toList());

buildFlowRangeByLinkList(leaveFlowRangeSet, leaveLinkValue,
vacationNameList);

List\<AttendStatusDetailLinkSecDev\> businessTripLinkValue =
perDayLinkValue.stream().filter(e -\> e.getAppealType() ==
AttendAppealType.businessTrip).collect(Collectors.toList());

buildFlowRangeByLinkList(bussinessTripFlowRangeSet,
businessTripLinkValue, null);

List\<AttendStatusDetailLinkSecDev\> outSideLinkValue =
perDayLinkValue.stream().filter(e -\> e.getAppealType() ==
AttendAppealType.outSide).collect(Collectors.toList());

buildFlowRangeByLinkList(outsideFlowRangeSet, outSideLinkValue, null);

TreeRangeSet\<Date\> halfPeriodRangeSet =
TreeRangeSet.create(periodRangeSet);

// 全天请假判断 所有的考勤时段-休息时长-请假时长 == 0

boolean isAllDayLeave = isZero(periodRangeSet, restRangeSet,
leaveFlowRangeSet);

if (isAllDayLeave) {

dayStatus = "8-" + String.join(" ", vacationNameList);

statusMap.put("day_status", dayStatus);

}

if (!isAllDayLeave) {

if (isHalfDay(restRangeSet, leaveFlowRangeSet, halfPeriodRangeSet,
perDayStatusValue, perDayLinkValue,
AttendCommonUtils.parseDate(attendDateStr, "yyyy-MM-dd"))) {

for (int i = 0; i \< statusArr.length; i++) {

if (statusArr\[i\].startsWith("8-")) {

String s = statusArr\[i\] + " 0.5" + SystemEnv.getHtmlLabelName(1554,
"天");

statusArr\[i\] = s;

statusMap.put("day_status", String.join(",", statusArr));

}

}

}

}

// 半天出差 类似半天请假

// 半天公出 类似半天出差

}

}

});

}

public static boolean isHalfDay(TreeRangeSet\<Date\> restRangeSet,
TreeRangeSet\<Date\> leaveFlowRangeSet, TreeRangeSet\<Date\>
periodRangeSet, List\<AttendStatusDetailSecDev\> statusDetailSecDevList,
List\<AttendStatusDetailLinkSecDev\> flowLinkList, Date attendDate) {

TreeRangeSet\<Date\> backPeriodRangeSet =
TreeRangeSet.create(periodRangeSet);

if (leaveFlowRangeSet.isEmpty()) {

return false;

}

Date halfDayPoint = getHalfDayPoint(periodRangeSet,
statusDetailSecDevList, flowLinkList, attendDate);

if (halfDayPoint == null) {

return false;

}

Range\<Date\> span = leaveFlowRangeSet.span();

// 上午

if (halfDayPoint.compareTo(span.upperEndpoint()) \>= 0) {

TreeRangeSet\<Date\> morningPeriodRangeSet =
getMorningPeriodRangeSet(periodRangeSet, statusDetailSecDevList,
flowLinkList, attendDate);

return morningPeriodRangeSet != null && isZero(morningPeriodRangeSet,
restRangeSet, leaveFlowRangeSet);

}

// 下午

if (halfDayPoint.compareTo(span.lowerEndpoint()) \<= 0) {

TreeRangeSet\<Date\> afternoonPeriodRangeSet =
getAfternoonPeriodRangeSet(periodRangeSet, statusDetailSecDevList,
flowLinkList, attendDate);

return afternoonPeriodRangeSet != null &&
isZero(afternoonPeriodRangeSet, restRangeSet, leaveFlowRangeSet);

}

periodRangeSet = backPeriodRangeSet;

return false;

}

/**

* 一定时段范围内全部是流程

*

* @param periodRangeSet

* @param restRangeSet

* @param flowRangeSet

* @return

*/

public static boolean isZero(TreeRangeSet\<Date\> periodRangeSet,
TreeRangeSet\<Date\> restRangeSet, TreeRangeSet\<Date\> flowRangeSet) {

periodRangeSet.removeAll(restRangeSet);

periodRangeSet.removeAll(flowRangeSet);

return periodRangeSet.isEmpty();

}

public static void buildFlowRangeByLinkList(TreeRangeSet\<Date\>
rangeSet, List\<AttendStatusDetailLinkSecDev\> flowLinkList,
Set\<String\> vacationNameList) {

if (CollectionUtils.isNotEmpty(flowLinkList)) {

flowLinkList.forEach(link -\> {

DateRange appealRange = link.getAppealRange();

if (appealRange != null && appealRange.getStart() != null &&
appealRange.getEnd() != null) {

if (vacationNameList != null) {

vacationNameList.add(link.getVacationType().getName());

}

rangeSet.add(Range.closed(appealRange.getStart(),
appealRange.getEnd()));

}

});

}

}

// 班次规则-固定时间点，流程规则自行补充

public static Date getHalfDayPoint(TreeRangeSet\<Date\> periodRangeSet,
List\<AttendStatusDetailSecDev\> statusDetailSecDevList,
List\<AttendStatusDetailLinkSecDev\> flowLinkList, Date attendDate) {

try {

Date timePoint = null;

Long shiftId =
statusDetailSecDevList.get(0).getAttendPeriod().getAttendShift().getId();

List\<Long\> shiftIds =
flowLinkList.stream().map(AttendStatusDetailLinkSecDev::getShiftId).distinct().collect(Collectors.toList());

shiftIds.remove(0L);// 防止数据库存字段默认为0的情况

if (CollectionUtils.isNotEmpty(shiftIds)) {

shiftId = shiftIds.get(0);

flowLinkList = flowLinkList.stream().filter(flow -\>
Objects.equals(flow.getShiftId(),
shiftIds.get(0))).collect(Collectors.toList());

}

AttendVacationType vacationType =
attendVacationTypeMap.get().get(flowLinkList.get(0).getVacationType().getId());

AttendShiftSecDev shift = attendShiftMap.get().get(shiftId);

AttendMinUnit minUnit = (vacationType != null) ?
vacationType.getMinUnit() : AttendMinUnit.MIN;

if (shift != null && "shift".equals(shift.getHalfDayRule()) && minUnit
== AttendMinUnit.HALFDAY) {

if (shift.getHalfDayType() == HalfDayType.timeLength) {

long dWork = AttendCommonUtils.countPeriodsLength(periodRangeSet);

long halfDWork = dWork / 2;

AtomicLong periodLength = new AtomicLong(0L);

for (Range\<Date\> range : periodRangeSet.asRanges()) {

long tempLength =
periodLength.addAndGet(range.upperEndpoint().getTime() -
range.lowerEndpoint().getTime());

if (tempLength \>= halfDWork) {

long diff = halfDWork - tempLength;

timePoint =
LocalDateTime.fromDateFields(range.upperEndpoint()).plusMillis((int)
diff).toDate();

return timePoint;

}

}

} else if (shift.getHalfDayType() == HalfDayType.timePoint &&
StringUtils.isNotBlank(shift.getTimePoint())) {

LocalTime time = LocalTime.parse(shift.getTimePoint());

LocalDateTime dateTime = LocalDateTime.fromDateFields(attendDate);

if (BooleanUtils.isTrue(shift.getHalfNextDay())) dateTime =
dateTime.plusDays(1);

timePoint = dateTime.withFields(time).toDate();

return timePoint;

}

}

return null;

} catch (Exception e) {

log.error("method invoke getHalfDayPoint error.", e);

}

return null;

}

/**

*
先获取半天分界时间，分界时间处于period端之间的，取该period开始时间-分界时间及该period之前的所有的period

*

* @param periodRangeSet 应出勤时段

* @param statusDetailSecDevList 考勤规则数据

* @param flowLinkList 流程数据

* @param attendDate 考勤日期

* @return obj

*/

public static TreeRangeSet\<Date\>
getMorningPeriodRangeSet(TreeRangeSet\<Date\> periodRangeSet,
List\<AttendStatusDetailSecDev\> statusDetailSecDevList,
List\<AttendStatusDetailLinkSecDev\> flowLinkList, Date attendDate) {

Date timePoint = getHalfDayPoint(periodRangeSet, statusDetailSecDevList,
flowLinkList, attendDate);

if (timePoint == null) {

return null;

}

TreeRangeSet\<Date\> result = TreeRangeSet.create();

periodRangeSet.asRanges().forEach(range -\> {

if (range.upperEndpoint().compareTo(timePoint) \<= 0) {

result.add(range);

} else {

result.add(Range.closed(range.lowerEndpoint(), timePoint));

}

});

return result;

}

/**

*
先获取半天分界时间，分界时间处于period端之间的，取分界时间-改peirod的结束时间及该period后的所有的period

*

* @param periodRangeSet 应出勤时段

* @param statusDetailSecDevList 考勤规则数据

* @param flowLinkList 流程数据

* @param attendDate 考勤日期

* @return obj

*/

public static TreeRangeSet\<Date\>
getAfternoonPeriodRangeSet(TreeRangeSet\<Date\> periodRangeSet,
List\<AttendStatusDetailSecDev\> statusDetailSecDevList,
List\<AttendStatusDetailLinkSecDev\> flowLinkList, Date attendDate) {

Date timePoint = getHalfDayPoint(periodRangeSet, statusDetailSecDevList,
flowLinkList, attendDate);

if (timePoint == null) {

return null;

}

TreeRangeSet\<Date\> result = TreeRangeSet.create();

periodRangeSet.asRanges().forEach(range -\> {

if (timePoint.compareTo(range.lowerEndpoint()) \>= 0 &&
timePoint.compareTo(range.upperEndpoint()) \<= 0) {

result.add(Range.closed(timePoint, range.upperEndpoint()));

} else if (range.lowerEndpoint().compareTo(timePoint) \>= 0){

result.add(range);

}

});

return result;

}

public static \<R, T\> Map\<R, T\> convert2Map(Collection\<T\> objs,
Function\<T, R\> function) {

if (CollectionUtils.isEmpty(objs)) {

return Maps.newHashMap();

}

return objs.stream().collect(Collectors.toMap(function,
Function.identity(), (a, b) -\> a));

}

public static List\<Range\<Date\>\>
getMulRestDateRange(AttendStatusRestDetailSecDev attendStatusRestDetail)
{

List\<Range\<Date\>\> dateRanges =
com.google.common.collect.Lists.newArrayList();

DateRange restRange1 = attendStatusRestDetail.getRestRange1();

DateRange restRange2 = attendStatusRestDetail.getRestRange2();

DateRange restRange3 = attendStatusRestDetail.getRestRange3();

DateRange restRange4 = attendStatusRestDetail.getRestRange4();

DateRange restRange5 = attendStatusRestDetail.getRestRange5();

DateRange restRange6 = attendStatusRestDetail.getRestRange6();

DateRange restRange7 = attendStatusRestDetail.getRestRange7();

DateRange restRange8 = attendStatusRestDetail.getRestRange8();

DateRange restRange9 = attendStatusRestDetail.getRestRange9();

DateRange restRange10 = attendStatusRestDetail.getRestRange10();

if(restRange1 != null){

dateRanges.add(restRange1.asRange());

}

if(restRange2 != null){

dateRanges.add(restRange2.asRange());

}

if(restRange3 != null){

dateRanges.add(restRange3.asRange());

}

if(restRange4 != null){

dateRanges.add(restRange4.asRange());

}

if(restRange5 != null){

dateRanges.add(restRange5.asRange());

}

if(restRange6 != null){

dateRanges.add(restRange6.asRange());

}

if(restRange7 != null){

dateRanges.add(restRange7.asRange());

}

if(restRange8 != null){

dateRanges.add(restRange8.asRange());

}

if(restRange8 != null){

dateRanges.add(restRange8.asRange());

}

if(restRange9 != null){

dateRanges.add(restRange9.asRange());

}

if(restRange10 != null){

dateRanges.add(restRange10.asRange());

}

return dateRanges;

}

@Override

public void rollBack(WeaHookRequest request, WeaHookResponse response,
AppInfo appInfo) throws Exception {

}

}

## 10.3 参数说明

参考代码注释及系统请求接口返回的数据结构

# 出勤汇总报表日历明细口子

最低基线版本：10.0.2411.01

埋点编码：ATTEND_WORKTIME_SUMMARY_REPORT_DAILY_INFO_001

attend库attend_configuration配置表增加开关，开关必须打开，否则口子不生效（为了不影响标准功能的性能）

config_key:ATTEND_HOOKATTEND_WORKTIME_SUMMARY_REPORT_DAILY_INFO_001

value:true

## 11.1 功能说明

根据传入的数据自行处理日历明细中的上下午展示内容

## 11.2 代码示例

参考10号口子，类似

## 11.3 参数说明

|  |  |  |  |
|:---|:---|:---|:---|
| 参数名称 | 数据类型 | 备注 | 示例 |
| data | list\<Map\<String,Object\> | 变更link结尾的属性，要按照示例中的格式拼接对应的span结尾的属性值 | 见下面 |
| 其他参数 |  | 参考10号口子中params解析出来的数据，都有注释 |  |

data示例：

\[{

"attendDayspan": "2024-11-07 09:00-18:00",

"attendStatus": "未打卡",

"attendMsglink": \[{

"linktype": "flow",

"label": "相关流程：",

"linkname": "请假-请假主表+育儿假主表-王琦-2024-11-19",

"flowid": "1068510214571671564"

}, {

"linktype": "flow",

"label": "相关流程：",

"linkname":
"出差-日期/日期区间-王琦-2024-10-28(外勤打卡规则校验不通过，该流程未统计到报表)",

"flowid": "1060450480876822532"

}\],

"attendMsg": "",

"showMore": false,

"photo": \[\],

"attendMsgspan": "相关流程： \<a
onclick='flowFunc('workflow',1068510214571671564)'
href='javascript:void(0)'
\>请假-请假主表+育儿假主表-王琦-2024-11-19\</a\>、 \<a
onclick='flowFunc('workflow',1060450480876822532)'
href='javascript:void(0)'
\>出差-日期/日期区间-王琦-2024-10-28(外勤打卡规则校验不通过，该流程未统计到报表)\</a\>",

"attendDay": "2024-11-07 09:00-18:00"

}\]

# 12. 加班规则自定义SQL钩子

最低基线支持：1101

埋点编码：**code：ATTEND_HRM_AFTER_FLOW_CUSTOM_INFO_001**

## 12.1. 功能说明

该钩子可以满足客户考勤流程归档后触发口子的场景。

比如：

- 请假流程归档后要触发加班重算。

## 12.2. 代码示例

@WeaHookReg(code = "ATTEND_HRM_AFTER_FLOW_CUSTOM_INFO_001", order = 0)

@Component

public class AttendAfterFlowReg4Local extends RegHookEvent {

protected static final Logger logger =
LoggerFactory.getLogger(AttendAfterFlowReg4Local.class);

@Autowired

private AttendReCalOvertimeService attendReCalOvertimeService;

@Override

public WeaHookResponse handle(WeaHookRequest request, WeaHookResponse
response, AppInfo appInfo) throws Exception {

//入参，修改入参并不会影响到埋点的原始数据

Map\<String, Object\> params = request.getParams();

List\<AttendFlowDataBefore\> flowDataSecList = Lists.newArrayList();

if(params.containsKey("flowData")){

flowDataSecList = (List\<AttendFlowDataBefore\>) params.get("flowData");

}

logger.info("AttendAfterFlowReg4Local",flowDataSecList);

if(CollectionUtils.isNotEmpty(flowDataSecList)){

for (int i = 0; i \< flowDataSecList.size(); i++) {

AttendFlowDataBefore attendFlowDataBefore = flowDataSecList.get(i);

Long empId = attendFlowDataBefore.getApplyEmpId();

Date startTime = attendFlowDataBefore.getStartTime();

Date endTime = attendFlowDataBefore.getEndTime();

String tenantKey = attendFlowDataBefore.getTenantKey();

logger.info("AttendAfterFlowReg4Local_reCalOvertimeWithDate:empId:"+empId+":startTime:"+startTime+":endTime:"+endTime+":tenantKey:"+tenantKey);

attendReCalOvertimeService.reCalOvertimeWithDate(Lists.newArrayList(empId),startTime,endTime,tenantKey);

}

}

response.setResult("");

return response;

}

@Override

public void rollBack(WeaHookRequest request, WeaHookResponse response,
AppInfo appInfo) throws Exception {

}

}

## 12.3. 参数说明

|  |  |  |  |
|:---|:---|:---|:---|
| 参数名称 | 数据类型 | 备注 | 示例 |
| flowData | List\<AttendFlowDataBefore\> | 流程基本数据 |  |
| status | String | 流程状态(VALID:还原;INVALID:删除;DELETE:干预到发起;) |  |

# 请假变更人事工作状态的钩子

最低基线支持：1201

## 13.1. 功能说明

该钩子可以满足请假流程归档后变更人事状态时，控制人事状态的显示的内容，可以显示请假的时间范围。

## 13.2. 代码示例

@WeaHookReg(code = "ATTEND_HRM_WORK_CONTENT_ADD_CUSTOM_INFO_001")

@Component

public class AttendHrmWorkContentAddCustomeSecDev extends RegHookEvent {

@Override

public WeaHookResponse handle(WeaHookRequest request, WeaHookResponse
response, AppInfo appInfo) throws Exception {

Map\<String, Object\> params = request.getParams();

if (!params.isEmpty()) {

List\<Map\> data = (List\<Map\>) params.get("hrmWorkMaps");

List\<Map\> flowDataMaps = (List\<Map\>) params.get("flowDataMaps");

if (CollectionUtils.isNotEmpty(data) &&
CollectionUtils.isNotEmpty(flowDataMaps)) {

for (Map datum : data) {

String contents = datum.get("contents") + "";

if (StringUtils.isBlank(contents) \|\| datum.get("flowId") == null) {

continue;

}

String flowId = datum.get("flowId") + "";

Optional\<Map\> first = flowDataMaps.stream().filter(a -\>
flowId.equals(a.get("flowId").toString())).findFirst();

if (first.isPresent()) {

Map map = first.get();

datum.put("contents", contents + "(" + DateUtil.formatDateTime((Date)
map.get("startTime")) + "-" + DateUtil.formatDateTime((Date)
map.get("endTime")) + ")");

}

}

response.setResult(data);

}

}

return response;

}

@Override

public void rollBack(WeaHookRequest request, WeaHookResponse response,
AppInfo appInfo) throws Exception {

}

}

## 13.3. 参数说明

|              |                          |                             |      |
|:-------------|:-------------------------|:----------------------------|:-----|
| 参数名称     | 数据类型                 | 备注                        | 示例 |
| hrmWorkMaps  | List\<Map\> data         | 人事状态对象 map的集合      |      |
| flowDataMaps | List\<Map\> flowDataMaps | 流程的 flowdata 的 map 信息 |      |

# 自定义指定请假类型是需要算出勤工时的钩子

最低基线支持：1101

## 14.1. 功能说明

该钩子可以满哪些请假类型算实际出勤的需求。

## 14.2. 代码示例

package com.weaver.seconddev.attend.rule;

import com.google.common.collect.Lists;

import com.weaver.loom.context.annotation.WeaHookReg;

import com.weaver.loom.context.domain.AppInfo;

import com.weaver.loom.context.domain.WeaHookRequest;

import com.weaver.loom.context.domain.WeaHookResponse;

import com.weaver.loom.context.register.RegHookEvent;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import java.util.List;

import java.util.Map;

/**

* 请假时长也记为实际出勤

*/

@WeaHookReg(code = "ATTEND_LEAVE_WORKLENGTH_001", order = 0)

public class WeaHookLeaveWorkLengthReg extends RegHookEvent {

protected static final Logger logger =
LoggerFactory.getLogger(WeaHookLeaveWorkLengthReg.class);

@Override

public WeaHookResponse handle(WeaHookRequest request, WeaHookResponse
response, AppInfo appInfo) throws Exception {

//入参，修改入参并不会影响到埋点的原始数据

Map\<String, Object\> params = request.getParams();

logger.info("WeaHookLeave4WorkLengthReg:"+params);

List\<Long\> worklengthLeaveids = Lists.newArrayList();

//这里传入的请假类型的id，会计算到实际出勤里

worklengthLeaveids.add(1005992911325478912L);

worklengthLeaveids.add(6160176009735635121L);

worklengthLeaveids.add(6160176009752835136L);

worklengthLeaveids.add(6160176009752835140L);

response.setResult(worklengthLeaveids);

return response;

}

@Override

public void rollBack(WeaHookRequest request, WeaHookResponse response,
AppInfo appInfo) throws Exception {

}

}

## 14.3. 参数说明

|          |          |      |      |
|:---------|:---------|:-----|:-----|
| 参数名称 | 数据类型 | 备注 | 示例 |

返回结果WeaHookResponse说明

|  |  |  |  |
|:---|:---|:---|:---|
| 参数名称 | 数据类型 | 备注 | 示例 |
| worklengthLeaveids | Long | 请假类型的id，返回的请假类型id就是需要计入实际出勤的 |  |

# 15.出勤汇总报表日历明细导出口子

## 15.1功能说明

最低基线版本：10.0.2411.01

埋点编码：ATTEND_WORKTIME_SUMMARY_REPORT_DAILY_EXPORT_001

attend库attend_configuration配置表增加开关，开关必须打开，否则口子不生效（为了不影响标准功能的性能）

config_key:ATTEND_HOOKATTEND_WORKTIME_SUMMARY_REPORT_DAILY_EXPORT_001

value:true

## 15.2代码示例

package com.weaver.seconddev;

import com.alibaba.fastjson.JSONObject;

import com.alibaba.fastjson.TypeReference;

import com.google.common.collect.HashBasedTable;

import com.google.common.collect.Lists;

import com.google.common.collect.Maps;

import com.google.common.collect.Table;

import com.weaver.common.i18n.label.SystemEnv;

import com.weaver.common.i18n.tool.util.I18nLanguageUtil;

import com.weaver.loom.context.annotation.WeaHookReg;

import com.weaver.loom.context.domain.AppInfo;

import com.weaver.loom.context.domain.WeaHookRequest;

import com.weaver.loom.context.domain.WeaHookResponse;

import com.weaver.loom.context.register.RegHookEvent;

import com.weaver.teams.attend.entity.AttendVacationType;

import com.weaver.teams.attend.entity.TimeLength;

import com.weaver.teams.attend.entity.vo.AttendDetailVo;

import com.weaver.teams.attend.enums.AttendAppealType;

import com.weaver.teams.attend.enums.AttendDayType;

import com.weaver.teams.attend.secdev.entity.AttendShiftSecDev;

import
com.weaver.teams.attend.secdev.entity.AttendStatusDetailLinkSecDev;

import com.weaver.teams.attend.secdev.entity.AttendStatusDetailSecDev;

import com.weaver.teams.domain.user.SimpleEmployee;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.collections.CollectionUtils;

import org.apache.commons.collections.MapUtils;

import org.apache.commons.lang.StringUtils;

import org.apache.commons.lang3.time.DateFormatUtils;

import org.apache.commons.lang3.time.DateUtils;

import org.springframework.stereotype.Component;

import java.text.ParseException;

import java.text.SimpleDateFormat;

import java.util.List;

import java.util.Map;

import java.util.Optional;

@WeaHookReg(code = "ATTEND_WORKTIME_SUMMARY_REPORT_DAILY_EXPORT_001")

@Component()

@Slf4j

public class AttendWorkTimeSummaryDailyReport extends RegHookEvent {

private static final long serialVersionUID = -8825903435820809826L;

private static final ThreadLocal\<Map\<Long, AttendShiftSecDev\>\>
attendShiftMap = new ThreadLocal\<\>();

private static final ThreadLocal\<Map\<Long, AttendVacationType\>\>
attendVacationTypeMap = new ThreadLocal\<\>();

/**

*
由于原有的导出内容组装过程比较复杂且包含很多字符串判断内容，不利于口子开发，所以把标准的组装过程放进来了，根据业务代码理解自行修改

*/

@Override

public WeaHookResponse handle(WeaHookRequest request, WeaHookResponse
response, AppInfo appInfo) throws Exception {

Map\<String, Object\> params = request.getParams();

/************************标准业务入参数据*********************************/

boolean isAttendOsign = (boolean) params.get("isAttendOsign");

boolean signDateEnable = (boolean) params.get("signDateEnable");

boolean dailyStatusRefactor = (boolean)
params.get("dailyStatusRefactor");

Map\<String, String\> vacaNameMap =
JSONObject.parseObject(JSONObject.toJSONString(params.get("vacaNameMap")),
new TypeReference\<Map\<String, String\>\>(){});

Map\<String, String\> i18nMap =
JSONObject.parseObject(JSONObject.toJSONString(params.get("i18nMap")),
new TypeReference\<Map\<String, String\>\>(){});

SimpleEmployee currentUser =
JSONObject.parseObject(JSONObject.toJSONString(params.get("currentUser")),
new TypeReference\<SimpleEmployee\>(){});

// 导出内容基本数据

List\<AttendDetailVo\> tempDetails = JSONObject.parseObject((String)
params.get("attendDetailVoList"), new
TypeReference\<List\<AttendDetailVo\>\>(){});

/**********************************************************************/

try {

Map\<String, Object\> detailShow = getDetailShow(params, currentUser,
signDateEnable, isAttendOsign, dailyStatusRefactor, tempDetails,
vacaNameMap, i18nMap);

response.setResult(detailShow);

} catch (Exception e) {

log.error("invoke daily export error.", e);

} finally {

attendShiftMap.remove();

attendVacationTypeMap.remove();

}

return response;

}

public Map\<String, Object\> getDetailShow(Map\<String, Object\> params,
SimpleEmployee curUser, boolean signDateEnable, boolean isAttendOsign,
boolean dailyStatusRefactor, List\<AttendDetailVo\> tempDetails,
Map\<String, String\> vacaNameMap, Map\<String, String\> i18nMap) {

/**********************二开相关流程及配置数据**********************/

List\<AttendStatusDetailSecDev\> attendstatusDetails =
JSONObject.parseObject((String) params.get("statusDetails"), new
TypeReference\<List\<AttendStatusDetailSecDev\>\>() {

});

Table\<String, String, List\<AttendStatusDetailSecDev\>\>
perDayStatusDetailTable = HashBasedTable.create();

Optional.ofNullable(attendstatusDetails).ifPresent(list -\>

list.forEach(detail -\> {

String empIdStr = String.valueOf(detail.getEmployee().getId());

String attendDateStr = DateFormatUtils.format(detail.getAttendDate(),
"yyyy-MM-dd");

List\<AttendStatusDetailSecDev\> statusDetailSecDevList =
Optional.ofNullable(perDayStatusDetailTable.get(empIdStr,
attendDateStr)).orElse(org.apache.commons.compress.utils.Lists.newArrayList());

statusDetailSecDevList.add(detail);

perDayStatusDetailTable.put(empIdStr, attendDateStr,
statusDetailSecDevList);

})

);

// 流程相关信息

List\<AttendStatusDetailLinkSecDev\> attendLinkDetails =
JSONObject.parseObject((String) params.get("statusDetailLinks"), new
TypeReference\<List\<AttendStatusDetailLinkSecDev\>\>() {

});

Table\<String, String, List\<AttendStatusDetailLinkSecDev\>\>
perDayLinkDetailTable = HashBasedTable.create();

Optional.ofNullable(attendLinkDetails).ifPresent(list -\>

list.forEach(detail -\> {

String empIdStr = String.valueOf(detail.getEmployee().getId());

String attendDateStr = DateFormatUtils.format(detail.getAttendDate(),
"yyyy-MM-dd");

List\<AttendStatusDetailLinkSecDev\> linkDetailSecDevList =
Optional.ofNullable(perDayLinkDetailTable.get(empIdStr,
attendDateStr)).orElse(org.apache.commons.compress.utils.Lists.newArrayList());

linkDetailSecDevList.add(detail);

perDayLinkDetailTable.put(empIdStr, attendDateStr,
linkDetailSecDevList);

})

);

// 班次配置

attendShiftMap.set(AttendWorkTimeSummaryDaily.convert2Map(JSONObject.parseObject((String)
params.get("shiftList"), new
TypeReference\<List\<AttendShiftSecDev\>\>() {

}), AttendShiftSecDev::getId));

// 流程配置

attendVacationTypeMap.set(AttendWorkTimeSummaryDaily.convert2Map(JSONObject.parseObject((String)
params.get("vacationSettings"), new
TypeReference\<List\<AttendVacationType\>\>() {

}), AttendVacationType::getId));

/**************************************************************/

Map\<String,String\> detailMap = Maps.newHashMap(); //打卡及申诉

Map\<String,List\<String\>\> detailVacaMap = Maps.newHashMap();
//请假数据

List\<String\> tempVacaList = Lists.newArrayList();

String tenantKey = curUser.getTenantKey();

int langId = I18nLanguageUtil.getLangId(curUser.getId());

SimpleDateFormat sdf = new SimpleDateFormat("MM.dd HH:mm");

boolean hasFlow = false;//是否有相关审批：请假、出差、加班、外出

List\<String\> abnormalStatus =
Lists.newArrayList("SIGN_IN_LATE","SIGN_OUT_EARLY","SIGN_IN_ABSENCE","SIGN_IN_ABSENCE_ONLY","SIGN_OUT_ABSENCE",

"SIGN_IN_ABSENTEEISM","SIGN_OUT_ABSENTEEISM", "SERIOUSLY_LATE",
"SERIOUSLY_EARLY");

if(CollectionUtils.isNotEmpty(tempDetails)) {

String normalLabel = i18nMap.get("NORMAL");

for (AttendDetailVo detail : tempDetails) {

String date = detail.getAttendDay();

String status = detail.getAttendStatus();

String statusDetailType = detail.getStatusDetailType();

//异常考勤则取最终状态

if(StringUtils.isNotEmpty(status) && abnormalStatus.contains(status) &&
StringUtils.isNotEmpty(detail.getFinalStatus())) {

status = detail.getFinalStatus();

}

if(StringUtils.isNotEmpty(date) && StringUtils.isNotEmpty(status)) {

String tempStr = detailMap.get(date);

//excel展示内容

String content = "";

if (dailyStatusRefactor && (detail.getDayType() == AttendDayType.PLAYDAY
\|\| detail.getDayType() == AttendDayType.HOLIDAY)) {

if(StringUtils.isEmpty(tempStr) \|\|
!tempStr.contains(i18nMap.get("PLAYDAY"))) content =
i18nMap.get("PLAYDAY");

} else if(status.equals("SIGN_IN_NORMAL")) {

if (dailyStatusRefactor) {

if (detail.isOsignIn()) {

content = i18nMap.get("O_SIGN_IN");

} else if (StringUtils.isBlank(tempStr)) {

content = normalLabel;

}

} else {

content += i18nMap.get("SIGN_IN_NORMAL");

if(StringUtils.isNotEmpty(detail.getAttendTime()) && signDateEnable)
content += " " +detail.getAttendTime();

}

}else if(status.equals("SIGN_IN_LATE") \|\|
"SERIOUSLY_LATE".equals(status)) {

if (dailyStatusRefactor) {

content += i18nMap.get(status) + (langId != 7 ? " " : "");

TimeLength timeLength = detail.getLateLength() != null &&
detail.getLateLength().getMillisecond() \> 0 ? detail.getLateLength() :
detail.getSeriouslyLateLength();

if (timeLength != null && signDateEnable) content +=
convertDuration(timeLength.getMillisecond(), i18nMap);

} else {

content += i18nMap.get(status) + " " + (signDateEnable ?
detail.getAttendTime() : "");

}

}else if(status.equals("SIGN_IN_ABSENCE")) {

content += i18nMap.get("SIGN_IN_ABSENCE");

}else if(status.equals("SIGN_IN_ABSENCE_ONLY")) {

content += i18nMap.get("SIGN_IN_ABSENCE_ONLY");

}else if(status.equals("SIGN_OUT_NORMAL")) {

if (dailyStatusRefactor) {

if (detail.isOsignOut()) {

content = i18nMap.get("O_SIGN_OUT");

} else if (StringUtils.isBlank(tempStr)) {

content = normalLabel;

}

} else {

content += i18nMap.get("SIGN_OUT_NORMAL");

if(StringUtils.isNotEmpty(detail.getAttendTime()) && signDateEnable)
content += " " +detail.getAttendTime();

}

}else if(status.equals("SIGN_OUT_EARLY") \|\|
"SERIOUSLY_EARLY".equals(status)) {

if (dailyStatusRefactor) {

content += i18nMap.get(status) + (langId != 7 ? " " : "");

TimeLength timeLength = detail.getEarlyOutLength() != null &&
detail.getEarlyOutLength().getMillisecond() \> 0 ?
detail.getEarlyOutLength() : detail.getSeriouslyEarlyLength();

if (timeLength != null) content +=
convertDuration(timeLength.getMillisecond(), i18nMap);

} else {

content += i18nMap.get(status)+ " " + (signDateEnable ?
detail.getAttendTime() : "");

}

}else if(status.equals("SIGN_OUT_ABSENCE")) {

content += i18nMap.get("SIGN_OUT_ABSENCE");

}else if(status.equals("NO_SIGN")) {

if(StringUtils.isEmpty(tempStr) \|\|
!tempStr.contains(i18nMap.get("NO_SIGN"))) content +=
i18nMap.get("NO_SIGN");

}else if(status.equals("SIGN_IN_ABSENTEEISM") \|\|
status.equals("SIGN_OUT_ABSENTEEISM")) {

content += i18nMap.get("SIGN_IN_ABSENTEEISM");

if(StringUtils.isNotBlank(detail.getAttendTime()) && signDateEnable)
content += (" " + detail.getAttendTime());

}

if (dailyStatusRefactor && (abnormalStatus.contains(status) \|\|
status.equals("NO_SIGN") \|\| detail.isOsignIn() \|\|
detail.isOsignOut()) && StringUtils.isNotEmpty(tempStr)) {

// 如果有异常，就不显示正常了

tempStr = tempStr.replace(normalLabel + "\n", "").replace("\n" +
normalLabel, "").replace(normalLabel, "");

}

//申诉

if("NORMAL".equals(detail.getHandleResult())) {

content += !detail.isResultAfterAppeal() ? "
("+i18nMap.get("APPEAL_NORMAL")+")":"("+i18nMap.get("APPEAL_NORMAL")+"
"+ SystemEnv.getHtmlLabelName(36692, langId, tenantKey,"仍异常")+")";

}else if("RECALL".equals(detail.getHandleResult())) {

content += "("+i18nMap.get("APPEAL_RECALL")+")";

}else if("AGAINST".equals(detail.getHandleResult())) {

content += "("+i18nMap.get("APPEAL_AGAINST")+")";

}

if(isAttendOsign && status.equals("OCHECKIN")) {

content = i18nMap.get("OUT_SIGN_IN");

if(StringUtils.isNotEmpty(detail.getAttendTime()) && signDateEnable)
content +=" " +detail.getAttendDay() +" " +detail.getAttendTime();

if(detail.getOattendSginOut()!=null){

content += "\n" + i18nMap.get("OUT_SIGN_OUT");

if(signDateEnable) content += " " +
DateFormatUtils.format(detail.getOattendSginOut().getAddTime(),
"yyyy-MM-dd HH:mm:ss");

}

}

//打卡状态

if(StringUtils.isNotEmpty(tempStr)) {

if(StringUtils.isNotEmpty(content)) detailMap.put(date, tempStr + "\n"
+content);

}else {

detailMap.put(date, content);

}

tempStr = detailMap.get(date);

//有相关审批

try {

boolean hasOvertimeFlow = false;

if(CollectionUtils.isNotEmpty(detail.getFlowInfo())) {

for (Map\<String, String\> flows : detail.getFlowInfo()) {

if(MapUtils.isNotEmpty(flows)) {

//请假

hasFlow = false;

String vacaContent = "";

if("leave".equals(flows.get("type"))) {

hasFlow = true;

String vacationId = flows.get("vacationId");

String repeatLabelInfo = flows.get("repeatLabelInfo");

if(MapUtils.isNotEmpty(vacaNameMap) &&
StringUtils.isNotEmpty(vacationId) &&
StringUtils.isNotEmpty(vacaNameMap.get(vacationId))) {

vacaContent += vacaNameMap.get(vacationId);

}else {

vacaContent += i18nMap.get("LEAVE");

}

if(StringUtils.isNotEmpty(repeatLabelInfo)){

vacaContent += repeatLabelInfo;

}

}else if("businessTrip".equals(flows.get("type"))) { //出差

String name = flows.get("name") + "";

if (!name.contains(SystemEnv.getHtmlLabelName(255847,
"强关联外勤打卡不通过，该流程未统计到报表")) &&
!name.contains(SystemEnv.getHtmlLabelName(278658,
"外勤打卡规则校验不通过，该流程未统计到报表"))) {

hasFlow = true;

vacaContent += i18nMap.get("BUSINESS");

}

}else if("overTime".equals(flows.get("type"))) { //加班

hasOvertimeFlow = true;

hasFlow = true;

vacaContent += i18nMap.get("OVERTIME");

}else if("outSide".equals(flows.get("type"))) { //外出

String name = flows.get("name") + "";

if (!name.contains(SystemEnv.getHtmlLabelName(255847,
"强关联外勤打卡不通过，该流程未统计到报表")) &&
!name.contains(SystemEnv.getHtmlLabelName(278658,
"外勤打卡规则校验不通过，该流程未统计到报表"))) {

hasFlow = true;

vacaContent += i18nMap.get("OUTSIDE");

}

}

if(hasFlow) {

String beginDate = flows.get("beginDate"), endDate =
flows.get("endDate");

if(StringUtils.isNotEmpty(beginDate)) vacaContent += " " +
sdf.format(DateUtils.parseDate(beginDate, "yyyy-MM-dd HH:mm:ss"));

if(StringUtils.isNotEmpty(endDate)) vacaContent += "-" +
sdf.format(DateUtils.parseDate(endDate, "yyyy-MM-dd HH:mm:ss"));

/*********************************************************************/

//
此处参考AttendWorkTimeSummaryDaily类中的相关判断全天流程或者半天流程的处理方法处理，根据自己的需要处理vacaContent的拼接结果

/*********************************************************************/

//将该请假信息放入map

tempVacaList = detailVacaMap.get(date);

if(CollectionUtils.isNotEmpty(tempVacaList)) {

if(!tempVacaList.contains(vacaContent)) {

tempVacaList.add(vacaContent);

detailVacaMap.put(date, tempVacaList);

}

}else {

detailVacaMap.put(date, Lists.newArrayList(vacaContent));

}

}

}

}

if (dailyStatusRefactor &&
CollectionUtils.isNotEmpty(detailVacaMap.get(date))) {

// 如果有请假，就不显示正常了

tempStr = tempStr.replace(normalLabel + "\n", "").replace("\n" +
normalLabel, "").replace(normalLabel, "");

detailMap.put(date, tempStr);

}

}

if(statusDetailType != null &&
statusDetailType.equals(AttendAppealType.overTime.name()) &&
!hasOvertimeFlow) { // 加班

String overtimeContent = "";

String beginDate = detail.getOverRangeStart(), endDate =
detail.getOverRangeEnd();

if(StringUtils.isNotEmpty(beginDate)) overtimeContent += " " +
sdf.format(DateUtils.parseDate(beginDate, "yyyy-MM-dd HH:mm:ss"));

if(StringUtils.isNotEmpty(endDate)) overtimeContent += "-" +
sdf.format(DateUtils.parseDate(endDate, "yyyy-MM-dd HH:mm:ss"));

content = i18nMap.get("OVERTIME")+" "+overtimeContent;

if(StringUtils.isNotEmpty(content)) detailMap.put(date, tempStr + "\n"
+content);

}

} catch (ParseException e) {

log.info("导出月度汇总时，每日详情审批数据内容添加出错："+e.getMessage(),e);

}

}

}

}

Map\<String, Object\> rtnMap = Maps.newHashMap();

rtnMap.put("detail", detailMap); //打卡详情

rtnMap.put("detailVaca", detailVacaMap); //请假详情

return rtnMap;

}

public static String convertDuration(long millisecond, Map\<String,
String\> i18nMap) {

if (millisecond \< 1000) return "";

long seconds = millisecond / 1000;

if (seconds \< 60) {

return i18nMap.get("SECONDS").replace("{0}", seconds + "");

}

long minutes = seconds / 60;

if (minutes \< 60) {

return i18nMap.get("MINUTES").replace("{0}", minutes + "");

}

long hours = minutes / 60;

minutes = minutes % 60;

if (minutes == 0) {

return i18nMap.get("HOURS").replace("{0}", hours + "");

} else {

return i18nMap.get("HOURS_MINUTES").replace("{0}", hours +
"").replace("{1}", minutes + "");

}

}

@Override

public void rollBack(WeaHookRequest request, WeaHookResponse response,
AppInfo appInfo) throws Exception {

}

}

## 15.3参数说明

入参解析参考10号口子，代码中的注释

这里把组装过程都放到了口子中，代码中涉及到的状态字符串参考下面给出的：

SIGN_IN_NORMAL("正常签到"),

SIGN_IN_LATE("迟到"),

SIGN_IN_ABSENCE("未签到"),

SIGN_IN_ABSENTEEISM("旷工"),

SIGN_OUT_NORMAL("正常签退"),

SIGN_OUT_ABSENCE("未签退"),

SIGN_OUT_EARLY("早退"),

SIGN_OUT_ABSENTEEISM("旷工"),

SIGN_IN_ABSENCE_ONLY("未签到"),

OATTEND("外勤"),

OCHECKIN("外勤签到"),

OCHECKOUT("外勤签退"),

NO_SIGN("免打卡"),

SERIOUSLY_LATE("严重迟到"),

SERIOUSLY_EARLY("严重早退");

# 16.加班折算天数日折算时长口子

最低基线支持：1201

## 16.1. 功能说明

该钩子可以加班折算天数日折算时长口子的需求。

## 16.2. 代码示例

{

//入参，修改入参并不会影响到埋点的原始数据

Map\<String, Object\> params = request.getParams();

if(!params.containsKey("dayType")){

return response;

}

logger.info("overtimedayids:"+overtimedayids);

logger.info("overtimeday:"+overtimeday);

Long newDTime = null;

try{

// overtimedayids 用;区分大类型，用,区分小类型 这种 1,2,3;4,5,6

List\<List\<String\>\> overtimedayidList =
getOvertimedayidList(overtimedayids);

logger.info("overtimedayidList:"+overtimedayidList);

// overtimeday 用,区分类型 7.5,8

List\<String\> overtimedayList = getOvertimedayList(overtimeday);

logger.info("overtimedayList:"+overtimedayList);

if(CollectionUtils.isNotEmpty(overtimedayidList) &&
CollectionUtils.isNotEmpty(overtimedayList)){

//这俩是需要一一对应的

logger.info("overtimedayidList.size():"+overtimedayidList.size()+":overtimedayList.size():"+overtimedayList.size());

if(overtimedayidList.size() == overtimedayList.size()){

String dayType = String.valueOf(params.get("dayType"));

String overDetailId = String.valueOf(params.get("overDetailId"));

for (int i = 0; i \< overtimedayidList.size(); i++) {

List\<String\> ids = overtimedayidList.get(i);

String overtimeDay = overtimedayList.get(i);

logger.info("ids:"+ids+":overDetailId:"+overDetailId+":ids.contains(overDetailId):"+ids.contains(overDetailId)+":overtimeDay:"+overtimeDay);

if(ids.contains(overDetailId) && StringUtils.isNotBlank(overtimeDay)){

double d = Double.parseDouble(overtimeDay);

logger.info(":overDetailId:"+overDetailId+":d:"+d+":dayType:"+dayType);

if("HOLIDAY".equals(dayType)){

//节假日按照8小时一天

newDTime = (long) (d*60*60*1000L);

}else if("PLAYDAY".equals(dayType)){

//休息日按照8小时一天

newDTime = (long) (d*60*60*1000L);

}

logger.info(":overDetailId:"+overDetailId+":newDTime:"+newDTime);

break ;

}

}

}

}

}catch (Exception e){

logger.error("AttendOvertimeConversionReg_Exception:", e);

}

logger.info(":response:"+ JSON.toJSONString(response));

response.setResult(newDTime);

return response;

}

## 16.3. 参数说明

|              |          |                                  |      |
|:-------------|:---------|:---------------------------------|:-----|
| 参数名称     | 数据类型 | 备注                             | 示例 |
| dayType      | String   | 日期属性，工作量，休息日，节假日 |      |
| overId       | String   | 加班规则的主id                   |      |
| overDetailId | String   | 加班规则的明细id                 |      |
| tenantKey    | String   | 租户                             |      |

# 17.排班保存和导入后后预留口子

最低基线支持：1201

## 17.1 功能说明

该钩子排班保存和导入后后预留口子。（ATTEND_SAVE_SCHEDULING_AFTER_CUSTOM_INFO_001）

## 17.2 代码示例

<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<tbody>
<tr>
<td style="text-align: left;"><p>@WeaHookReg(code =
"ATTEND_SAVE_SCHEDULING_AFTER_CUSTOM_INFO_001")</p>
<p>@Component</p>
<p>public class AttendHrmWorkContentAddCustomeSecDev extends
RegHookEvent {</p>
<p>@Override</p>
<p>public WeaHookResponse handle(WeaHookRequest request, WeaHookResponse
response, AppInfo appInfo) throws Exception {</p>
<p>Map&lt;String, Object&gt; params = request.getParams();</p>
<p>if (!params.isEmpty()) {</p>
<p>List&lt;Map&gt; oldMapScheduling = (List&lt;Map&gt;)
params.get("oldScheduling");</p>
<p>List&lt;Map&gt; newScheduling = (List&lt;Map&gt;)
params.get("newScheduling");</p>
<p>}</p>
<p>return response;</p>
<p>}</p>
<p>@Override</p>
<p>public void rollBack(WeaHookRequest request, WeaHookResponse
response, AppInfo appInfo) throws Exception {</p>
<p>}</p>
<p>}</p></td>
</tr>
</tbody>
</table>

## 17.3. 参数说明

|               |             |                  |      |
|:--------------|:------------|:-----------------|:-----|
| 参数名称      | 数据类型    | 备注             | 示例 |
| oldScheduling | List\<Map\> | 修改前的班次数据 |      |
| newScheduling | List\<Map\> | 修改后的班次数据 |      |
