|
@@ -61,12 +61,12 @@ public class WorkHoursCalcService {
|
|
|
// prd 应填报人显示名称文本,用于报表按姓名筛选;姓名取自人员档案成员组件显示值
|
|
// prd 应填报人显示名称文本,用于报表按姓名筛选;姓名取自人员档案成员组件显示值
|
|
|
private static final String REQUIRED_HOURS_EMPLOYEE_NAME = "textField_ms4bzjkm";
|
|
private static final String REQUIRED_HOURS_EMPLOYEE_NAME = "textField_ms4bzjkm";
|
|
|
|
|
|
|
|
- // prd 全员项目参与关系 + 外部员工项目经理:项目档案字段
|
|
|
|
|
|
|
+ // prd 外部员工项目参与关系 + 项目经理:项目档案字段
|
|
|
private static final String PROJECT_SUB_TABLE = "tableField_mkowyn6d"; // 成员子表(人员明细)
|
|
private static final String PROJECT_SUB_TABLE = "tableField_mkowyn6d"; // 成员子表(人员明细)
|
|
|
private static final String PROJECT_SUB_MEMBER = "employeeField_mmbfe0ij"; // 子表-员工(成员)
|
|
private static final String PROJECT_SUB_MEMBER = "employeeField_mmbfe0ij"; // 子表-员工(成员)
|
|
|
private static final String PROJECT_SUB_PM = "employeeField_mkoxpswf"; // 子表-项目经理
|
|
private static final String PROJECT_SUB_PM = "employeeField_mkoxpswf"; // 子表-项目经理
|
|
|
private static final String PROJECT_SUB_OFFLINE = "dateField_mo6s11tc"; // 子表-下线时间
|
|
private static final String PROJECT_SUB_OFFLINE = "dateField_mo6s11tc"; // 子表-下线时间
|
|
|
- // prd 仅启用项目参与人员需要统计应填报工时
|
|
|
|
|
|
|
+ // prd 外部员工仅在参与启用项目且存在有效 PM 时统计应填报工时
|
|
|
private static final String PROJECT_STATUS = "textField_mpwc5r0q"; // 主表-项目状态
|
|
private static final String PROJECT_STATUS = "textField_mpwc5r0q"; // 主表-项目状态
|
|
|
private static final String PROJECT_STATUS_ACTIVE = "启用";
|
|
private static final String PROJECT_STATUS_ACTIVE = "启用";
|
|
|
|
|
|
|
@@ -96,11 +96,10 @@ public class WorkHoursCalcService {
|
|
|
return stats;
|
|
return stats;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 2. 预取 Manager + 启用项目参与关系;所有员工均须命中当天参与项目
|
|
|
|
|
|
|
+ // 2. 预取 Manager:内部员工取直属主管,外部员工取启用项目 assignments
|
|
|
ManagerData managerData = queryManagerData(personnelMap);
|
|
ManagerData managerData = queryManagerData(personnelMap);
|
|
|
stats.put("internalMgrCount", managerData.internal.size());
|
|
stats.put("internalMgrCount", managerData.internal.size());
|
|
|
stats.put("externalWithProjects", managerData.external.size());
|
|
stats.put("externalWithProjects", managerData.external.size());
|
|
|
- stats.put("projectParticipantCount", managerData.projects.size());
|
|
|
|
|
|
|
|
|
|
// 3. 查询节假日规则
|
|
// 3. 查询节假日规则
|
|
|
Map<LocalDate, String> holidayRules = queryHolidayRules(String.valueOf(year));
|
|
Map<LocalDate, String> holidayRules = queryHolidayRules(String.valueOf(year));
|
|
@@ -148,7 +147,7 @@ public class WorkHoursCalcService {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 2. 预取 Manager + 全员启用项目 assignments
|
|
|
|
|
|
|
+ // 2. 预取 Manager:内部员工取直属主管,外部员工取启用项目 assignments
|
|
|
ManagerData managerData = queryManagerData(personnelMap);
|
|
ManagerData managerData = queryManagerData(personnelMap);
|
|
|
|
|
|
|
|
// 3~4. 节假日 + 收窄到窗口内工作日 (跨月边界: 若窗口跨 6-30/7-1, 分别按各自月份的节假日规则)
|
|
// 3~4. 节假日 + 收窄到窗口内工作日 (跨月边界: 若窗口跨 6-30/7-1, 分别按各自月份的节假日规则)
|
|
@@ -239,15 +238,7 @@ public class WorkHoursCalcService {
|
|
|
return result;
|
|
return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 2. 所有员工都必须参与启用项目;内部取直属主管,外部取当天参与项目的 PM 合并
|
|
|
|
|
- Map<String, List<Assignment>> projectMap = queryProjectAssignments(Collections.singleton(userId));
|
|
|
|
|
- List<Assignment> assignments = projectMap.get(userId);
|
|
|
|
|
- if (!hasActiveProject(assignments, workDay)) {
|
|
|
|
|
- result.put("success", false);
|
|
|
|
|
- result.put("error", "员工当日未参与启用项目, 按业务规则跳过写入");
|
|
|
|
|
- return result;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ // 2. 内部员工不校验项目,外部员工取当天参与启用项目的 PM 合并
|
|
|
boolean isInternal = "内部".equals(String.valueOf(info.get("radioField_mkow4ydo")));
|
|
boolean isInternal = "内部".equals(String.valueOf(info.get("radioField_mkow4ydo")));
|
|
|
List<String> managerIds;
|
|
List<String> managerIds;
|
|
|
if (isInternal) {
|
|
if (isInternal) {
|
|
@@ -266,6 +257,13 @@ public class WorkHoursCalcService {
|
|
|
}
|
|
}
|
|
|
managerIds = mgrId == null ? Collections.emptyList() : Collections.singletonList(mgrId);
|
|
managerIds = mgrId == null ? Collections.emptyList() : Collections.singletonList(mgrId);
|
|
|
} else {
|
|
} else {
|
|
|
|
|
+ Map<String, List<Assignment>> projectMap = queryProjectAssignments(Collections.singleton(userId));
|
|
|
|
|
+ List<Assignment> assignments = projectMap.get(userId);
|
|
|
|
|
+ if (!hasActiveProject(assignments, workDay)) {
|
|
|
|
|
+ result.put("success", false);
|
|
|
|
|
+ result.put("error", "外部员工当日未参与启用项目, 按业务规则跳过写入");
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
// 外部员工: 按 workDay 算当天活跃项目 PM; 全空则按业务规则不写入
|
|
// 外部员工: 按 workDay 算当天活跃项目 PM; 全空则按业务规则不写入
|
|
|
managerIds = computeDailyPms(assignments, workDay);
|
|
managerIds = computeDailyPms(assignments, workDay);
|
|
|
if (managerIds.isEmpty()) {
|
|
if (managerIds.isEmpty()) {
|
|
@@ -339,7 +337,6 @@ public class WorkHoursCalcService {
|
|
|
ManagerData managerData = queryManagerData(subset);
|
|
ManagerData managerData = queryManagerData(subset);
|
|
|
stats.put("internalMgrCount", managerData.internal.size());
|
|
stats.put("internalMgrCount", managerData.internal.size());
|
|
|
stats.put("externalWithProjects", managerData.external.size());
|
|
stats.put("externalWithProjects", managerData.external.size());
|
|
|
- stats.put("projectParticipantCount", managerData.projects.size());
|
|
|
|
|
|
|
|
|
|
// 4. 节假日 + 工作日
|
|
// 4. 节假日 + 工作日
|
|
|
Map<LocalDate, String> rules = queryHolidayRules(String.valueOf(year));
|
|
Map<LocalDate, String> rules = queryHolidayRules(String.valueOf(year));
|
|
@@ -858,9 +855,9 @@ public class WorkHoursCalcService {
|
|
|
String empId = entry.getKey();
|
|
String empId = entry.getKey();
|
|
|
Map<String, Object> info = entry.getValue();
|
|
Map<String, Object> info = entry.getValue();
|
|
|
boolean isInternal = "内部".equals(String.valueOf(info.get("radioField_mkow4ydo")));
|
|
boolean isInternal = "内部".equals(String.valueOf(info.get("radioField_mkow4ydo")));
|
|
|
- // 内部: 单值直属主管; 外部: assignments 交给内层按 workDay 算 PM
|
|
|
|
|
|
|
+ // 内部: 单值直属主管且不校验项目; 外部: assignments 交给内层按 workDay 算 PM
|
|
|
String internalMgrId = isInternal ? managerData.internal.get(empId) : null;
|
|
String internalMgrId = isInternal ? managerData.internal.get(empId) : null;
|
|
|
- List<Assignment> projectAssignments = managerData.projects.get(empId);
|
|
|
|
|
|
|
+ List<Assignment> externalAssignments = isInternal ? null : managerData.external.get(empId);
|
|
|
// prd 离职时间: 若有离职时间, 该日之后的应报工时不再生成
|
|
// prd 离职时间: 若有离职时间, 该日之后的应报工时不再生成
|
|
|
LocalDate offlineDate = parseToLocalDate(info.get(PERSONNEL_OFFLINE_DATE));
|
|
LocalDate offlineDate = parseToLocalDate(info.get(PERSONNEL_OFFLINE_DATE));
|
|
|
// prd 入职时间: 入职日前不生成, 入职当天保留
|
|
// prd 入职时间: 入职日前不生成, 入职当天保留
|
|
@@ -894,8 +891,8 @@ public class WorkHoursCalcService {
|
|
|
skippedBeforeHired.incrementAndGet();
|
|
skippedBeforeHired.incrementAndGet();
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
- // prd 所有员工均须在启用项目中且当日未下线;无项目不统计应填报工时
|
|
|
|
|
- if (!hasActiveProject(projectAssignments, workDay)) {
|
|
|
|
|
|
|
+ // prd 外部员工须在启用项目中且当日未下线;内部员工不校验项目
|
|
|
|
|
+ if (!isInternal && !hasActiveProject(externalAssignments, workDay)) {
|
|
|
skippedNoProject.incrementAndGet();
|
|
skippedNoProject.incrementAndGet();
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
@@ -906,7 +903,7 @@ public class WorkHoursCalcService {
|
|
|
? Collections.emptyList()
|
|
? Collections.emptyList()
|
|
|
: Collections.singletonList(internalMgrId);
|
|
: Collections.singletonList(internalMgrId);
|
|
|
} else {
|
|
} else {
|
|
|
- managerIds = computeDailyPms(projectAssignments, workDay);
|
|
|
|
|
|
|
+ managerIds = computeDailyPms(externalAssignments, workDay);
|
|
|
if (managerIds.isEmpty()) {
|
|
if (managerIds.isEmpty()) {
|
|
|
skippedNoPm.incrementAndGet();
|
|
skippedNoPm.incrementAndGet();
|
|
|
continue;
|
|
continue;
|
|
@@ -962,7 +959,7 @@ public class WorkHoursCalcService {
|
|
|
log.info("入职日期过滤: 跳过{}条 workDay < hiredDate 的记录", skippedBeforeHired.get());
|
|
log.info("入职日期过滤: 跳过{}条 workDay < hiredDate 的记录", skippedBeforeHired.get());
|
|
|
}
|
|
}
|
|
|
if (skippedNoProject.get() > 0) {
|
|
if (skippedNoProject.get() > 0) {
|
|
|
- log.info("启用项目参与过滤: 跳过{}条 当天未参与启用项目的记录", skippedNoProject.get());
|
|
|
|
|
|
|
+ log.info("外部员工启用项目参与过滤: 跳过{}条 当天未参与启用项目的记录", skippedNoProject.get());
|
|
|
}
|
|
}
|
|
|
if (skippedNoPm.get() > 0) {
|
|
if (skippedNoPm.get() > 0) {
|
|
|
log.info("外部员工无 PM 过滤: 跳过{}条 当天无活跃项目 PM 的记录", skippedNoPm.get());
|
|
log.info("外部员工无 PM 过滤: 跳过{}条 当天无活跃项目 PM 的记录", skippedNoPm.get());
|
|
@@ -1697,12 +1694,12 @@ public class WorkHoursCalcService {
|
|
|
/**
|
|
/**
|
|
|
* 预取 Manager 数据:
|
|
* 预取 Manager 数据:
|
|
|
* - 内部员工: 钉钉 API 取 manager_userid (直属主管, 单值)
|
|
* - 内部员工: 钉钉 API 取 manager_userid (直属主管, 单值)
|
|
|
- * - 全体员工: 匹配启用项目 assignments,作为应填报工时准入条件
|
|
|
|
|
- * - 外部员工: 再按天从 assignments 计算 PM,多项目合并去重
|
|
|
|
|
|
|
+ * - 外部员工: 匹配启用项目 assignments,再按天计算 PM,多项目合并去重
|
|
|
|
|
+ * - 内部员工: 不以项目参与关系作为应填报工时准入条件
|
|
|
* ppExt: 与前端 TimeCard 刻意不同 - 不做 PM 离职探活、不兜底 Raymond (后端只记数据, 非审批找活人)
|
|
* ppExt: 与前端 TimeCard 刻意不同 - 不做 PM 离职探活、不兜底 Raymond (后端只记数据, 非审批找活人)
|
|
|
*
|
|
*
|
|
|
* @param personnelMap 全量人员档案
|
|
* @param personnelMap 全量人员档案
|
|
|
- * @return ManagerData: internal=内部主管, projects=全员启用项目参与关系, external=外部员工项目参与关系
|
|
|
|
|
|
|
+ * @return ManagerData: internal=内部主管, external=外部员工启用项目参与关系
|
|
|
*/
|
|
*/
|
|
|
@SuppressWarnings("unchecked")
|
|
@SuppressWarnings("unchecked")
|
|
|
private ManagerData queryManagerData(Map<String, Map<String, Object>> personnelMap) {
|
|
private ManagerData queryManagerData(Map<String, Map<String, Object>> personnelMap) {
|
|
@@ -1734,17 +1731,13 @@ public class WorkHoursCalcService {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // prd 全体员工都必须匹配启用项目;外部员工另保留子集用于兼容现有统计字段
|
|
|
|
|
- md.projects = queryProjectAssignments(personnelMap.keySet());
|
|
|
|
|
- for (String externalId : externalIds) {
|
|
|
|
|
- List<Assignment> assignments = md.projects.get(externalId);
|
|
|
|
|
- if (assignments != null && !assignments.isEmpty()) {
|
|
|
|
|
- md.external.put(externalId, assignments);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // prd 仅外部员工匹配启用项目;内部员工只按人员在职及日期边界生成应填报工时
|
|
|
|
|
+ if (!externalIds.isEmpty()) {
|
|
|
|
|
+ md.external = queryProjectAssignments(externalIds);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- log.info("Manager 预取完成: 内部{}人取直属主管落地{}, 全员启用项目参与者{}, 外部{}人中{}人有启用项目",
|
|
|
|
|
- internalCount, md.internal.size(), md.projects.size(), externalIds.size(), md.external.size());
|
|
|
|
|
|
|
+ log.info("Manager 预取完成: 内部{}人取直属主管落地{}, 外部{}人中{}人有启用项目",
|
|
|
|
|
+ internalCount, md.internal.size(), externalIds.size(), md.external.size());
|
|
|
return md;
|
|
return md;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1842,10 +1835,9 @@ public class WorkHoursCalcService {
|
|
|
return new ArrayList<>(pms);
|
|
return new ArrayList<>(pms);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 内部员工 → 直属主管;projects → 全员启用项目;external → 外部员工启用项目子集。 */
|
|
|
|
|
|
|
+ /** 内部员工 → 直属主管;外部员工 → 启用项目 assignments。 */
|
|
|
private static class ManagerData {
|
|
private static class ManagerData {
|
|
|
Map<String, String> internal = new HashMap<>();
|
|
Map<String, String> internal = new HashMap<>();
|
|
|
- Map<String, List<Assignment>> projects = new HashMap<>();
|
|
|
|
|
Map<String, List<Assignment>> external = new HashMap<>();
|
|
Map<String, List<Assignment>> external = new HashMap<>();
|
|
|
}
|
|
}
|
|
|
|
|
|