|
|
@@ -27,8 +27,9 @@ import java.util.*;
|
|
|
* 即前端 submitApprovalProcess 链路失败(startInstance 挂了/静默丢),导致部分员工工时从未进入审批。
|
|
|
* <p>
|
|
|
* 逻辑完全参考前端 TimeCard 的 submitApprovalProcess:
|
|
|
- * 1. collectApprovalData — 遍历汇总表每天子表,delta = submitted - approved - change, delta>0 才 push,按 PM 分组
|
|
|
- * 2. processManagerApproval — queryProcess(month + PM + RUNNING) 有则 update / 无则 create
|
|
|
+ * 1. collectApprovalData — 遍历汇总表每天子表,delta = submitted - approved - change, delta>0 才 push;
|
|
|
+ * 普通 PM 按审批人分组,Raymond 兜底按项目拆组
|
|
|
+ * 2. processManagerApproval — queryProcess(month + PM + RUNNING) 有同项目可用分片则 update / 无则 create
|
|
|
* COMPLETED 明细先进入覆盖索引;汇总尚未落值时归回写处理,不重复发起
|
|
|
* 3. collectOtherApprovalData / processOtherApproval — 其他工时按当前员工的部门主管作 approver,同样 query→update/create
|
|
|
* <p>
|
|
|
@@ -205,10 +206,12 @@ public class ReSubmitApprovalService {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 某员工某月按 PM 聚合的项目工时组
|
|
|
+ * 某员工某月的项目工时组;Raymond 兜底组带项目标识做项目隔离
|
|
|
*/
|
|
|
private static class ManagerGroup {
|
|
|
String managerId;
|
|
|
+ String projectCode;
|
|
|
+ String projectName;
|
|
|
List<BillRow> billableRows = new ArrayList<>();
|
|
|
List<BillRow> nonBillableRows = new ArrayList<>();
|
|
|
double billableTotal = 0;
|
|
|
@@ -271,7 +274,7 @@ public class ReSubmitApprovalService {
|
|
|
skipped++;
|
|
|
continue;
|
|
|
}
|
|
|
- // 3a) 项目工时:每个 PM 一张单
|
|
|
+ // 3a) 项目工时:普通 PM 按人合单,Raymond 兜底按项目拆单
|
|
|
for (ManagerGroup mg : cr.managerGroups.values()) {
|
|
|
try {
|
|
|
List<String> actions = processManagerApproval(mg, monthText, uid);
|
|
|
@@ -284,7 +287,8 @@ public class ReSubmitApprovalService {
|
|
|
}
|
|
|
} catch (Exception ex) {
|
|
|
failed++;
|
|
|
- log.error("[补发起] 项目工时发起失败 uid={} pm={} month={}", uid, mg.managerId, monthText, ex);
|
|
|
+ log.error("[补发起] 项目工时发起失败 uid={} pm={} project={} month={}",
|
|
|
+ uid, mg.managerId, mg.projectCode, monthText, ex);
|
|
|
}
|
|
|
}
|
|
|
// 3b) 其他工时:一人一张单,approver 用该员工部门主管(记录中带),否则兜底 Raymond
|
|
|
@@ -415,7 +419,8 @@ public class ReSubmitApprovalService {
|
|
|
|
|
|
/**
|
|
|
* collectApprovalData + collectOtherApprovalData 的等价:
|
|
|
- * 遍历该员工该月每天记录,按项目工时 / 其他工时反构造 delta 行,按 PM 分组
|
|
|
+ * 遍历该员工该月每天记录,按项目工时 / 其他工时反构造 delta 行。
|
|
|
+ * 普通 PM 按审批人分组,Raymond 兜底按项目拆组;项目编号为空时使用项目名称。
|
|
|
*/
|
|
|
private static class CollectResult {
|
|
|
Map<String, ManagerGroup> managerGroups = new LinkedHashMap<>();
|
|
|
@@ -465,16 +470,21 @@ public class ReSubmitApprovalService {
|
|
|
String pmRaw = firstIdOrText(row, S_BIL_MANAGER);
|
|
|
final String pm = projectManagerCache.computeIfAbsent(
|
|
|
StringUtils.defaultString(pmRaw), this::resolveProjectManager);
|
|
|
- ManagerGroup mg = cr.managerGroups.computeIfAbsent(pm, k -> {
|
|
|
+ String projectName = firstNonBlank(
|
|
|
+ str(row, S_BIL_PROJECT_NAME_TEXT), str(row, S_BIL_PROJECT_NAME));
|
|
|
+ String groupKey = approvalGroupKey(pm, projectCode, projectName);
|
|
|
+ ManagerGroup mg = cr.managerGroups.computeIfAbsent(groupKey, k -> {
|
|
|
ManagerGroup m = new ManagerGroup();
|
|
|
m.managerId = pm;
|
|
|
+ m.projectCode = DEFAULT_MANAGER_ID.equals(pm) ? projectCode : "";
|
|
|
+ m.projectName = DEFAULT_MANAGER_ID.equals(pm) ? projectName : "";
|
|
|
return m;
|
|
|
});
|
|
|
BillRow br = new BillRow();
|
|
|
br.dayText = dayText;
|
|
|
br.dayTs = dayTs;
|
|
|
br.projectCode = projectCode;
|
|
|
- br.projectName = firstNonBlank(str(row, S_BIL_PROJECT_NAME_TEXT), str(row, S_BIL_PROJECT_NAME));
|
|
|
+ br.projectName = projectName;
|
|
|
br.hours = delta;
|
|
|
br.submitterUid = uid;
|
|
|
br.monthText = monthText;
|
|
|
@@ -500,16 +510,21 @@ public class ReSubmitApprovalService {
|
|
|
String pmRaw = firstIdOrText(row, S_NON_MANAGER);
|
|
|
final String pm = projectManagerCache.computeIfAbsent(
|
|
|
StringUtils.defaultString(pmRaw), this::resolveProjectManager);
|
|
|
- ManagerGroup mg = cr.managerGroups.computeIfAbsent(pm, k -> {
|
|
|
+ String projectName = firstNonBlank(
|
|
|
+ str(row, S_NON_PROJECT_NAME_TEXT), str(row, S_NON_PROJECT_NAME));
|
|
|
+ String groupKey = approvalGroupKey(pm, projectCode, projectName);
|
|
|
+ ManagerGroup mg = cr.managerGroups.computeIfAbsent(groupKey, k -> {
|
|
|
ManagerGroup m = new ManagerGroup();
|
|
|
m.managerId = pm;
|
|
|
+ m.projectCode = DEFAULT_MANAGER_ID.equals(pm) ? projectCode : "";
|
|
|
+ m.projectName = DEFAULT_MANAGER_ID.equals(pm) ? projectName : "";
|
|
|
return m;
|
|
|
});
|
|
|
BillRow br = new BillRow();
|
|
|
br.dayText = dayText;
|
|
|
br.dayTs = dayTs;
|
|
|
br.projectCode = projectCode;
|
|
|
- br.projectName = firstNonBlank(str(row, S_NON_PROJECT_NAME_TEXT), str(row, S_NON_PROJECT_NAME));
|
|
|
+ br.projectName = projectName;
|
|
|
br.hours = delta;
|
|
|
br.submitterUid = uid;
|
|
|
br.monthText = monthText;
|
|
|
@@ -548,6 +563,24 @@ public class ReSubmitApprovalService {
|
|
|
return cr;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 生成项目工时审批分组键。
|
|
|
+ *
|
|
|
+ * @param managerId 审批人 userId
|
|
|
+ * @param projectCode 项目编号
|
|
|
+ * @param projectName 项目名称
|
|
|
+ * @return 普通 PM 返回 managerId;Raymond 返回项目隔离键,编号为空时用名称
|
|
|
+ */
|
|
|
+ static String approvalGroupKey(String managerId, String projectCode, String projectName) {
|
|
|
+ if (DEFAULT_MANAGER_ID.equals(managerId)) {
|
|
|
+ String projectIdentity = StringUtils.isNotBlank(projectCode)
|
|
|
+ ? projectCode
|
|
|
+ : "name:" + StringUtils.defaultString(projectName);
|
|
|
+ return managerId + "|project|" + projectIdentity;
|
|
|
+ }
|
|
|
+ return managerId;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* prd 项目经理离职时,改由人员档案中的在职主管审批;仅在整条替补链路不可用时兜底 Raymond。
|
|
|
*/
|
|
|
@@ -873,6 +906,9 @@ public class ReSubmitApprovalService {
|
|
|
existBil, A_BIL_SUBMITTER_UID, A_BIL_DAY_TEXT, A_BIL_PROJECT_CODE);
|
|
|
int existingNonBillableCount = countUniqueRows(
|
|
|
existNon, A_NON_SUBMITTER_UID, A_NON_DAY_TEXT, A_NON_PROJECT_CODE);
|
|
|
+ if (!approvalMatchesProject(existBil, existNon, mg.projectCode, mg.projectName)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
int existingCount = existingBillableCount + existingNonBillableCount;
|
|
|
int mergedCount = billable.rows.size() + nonBillable.rows.size();
|
|
|
return canAppendToApprovalShard(existingCount, mergedCount);
|
|
|
@@ -915,8 +951,8 @@ public class ReSubmitApprovalService {
|
|
|
whConf.getApprovalProcessCode(),
|
|
|
formData,
|
|
|
whConf.getApprovalOriginatorDeptId());
|
|
|
- log.info("[补发起] 新建工时审批单 submitterUid={} pm={} month={} bilTotal={} nonBilTotal={} rows={}+{}",
|
|
|
- submitterUid, mg.managerId, monthText,
|
|
|
+ log.info("[补发起] 新建工时审批单 submitterUid={} pm={} project={} month={} bilTotal={} nonBilTotal={} rows={}+{}",
|
|
|
+ submitterUid, mg.managerId, mg.projectCode, monthText,
|
|
|
mg.billableTotal, mg.nonBillableTotal, billableSub.size(), nonBillableSub.size());
|
|
|
}
|
|
|
|
|
|
@@ -1003,11 +1039,55 @@ public class ReSubmitApprovalService {
|
|
|
.ignoreEmpty(false)
|
|
|
.useLatestVersion(true)
|
|
|
.build(), YDConf.FORM_OPERATION.update);
|
|
|
- log.info("[补发起] 累计更新工时审批单 formInstanceId={} pm={} addedBil={} addedNon={}",
|
|
|
- formInstanceId, mg.managerId, mrBil.addedHours, mrNon.addedHours);
|
|
|
+ log.info("[补发起] 累计更新工时审批单 formInstanceId={} pm={} project={} addedBil={} addedNon={}",
|
|
|
+ formInstanceId, mg.managerId, mg.projectCode, mrBil.addedHours, mrNon.addedHours);
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 判断已有 Raymond 审批单是否只包含目标项目。
|
|
|
+ *
|
|
|
+ * @param billableRows Billable 明细
|
|
|
+ * @param nonBillableRows Non Billable 明细
|
|
|
+ * @param projectCode Raymond 组的项目编号
|
|
|
+ * @param projectName Raymond 组的项目名称;编号为空时用于项目隔离,两者均空表示普通 PM 组
|
|
|
+ * @return 可否追加到该审批单
|
|
|
+ */
|
|
|
+ static boolean approvalMatchesProject(List<Map> billableRows,
|
|
|
+ List<Map> nonBillableRows,
|
|
|
+ String projectCode,
|
|
|
+ String projectName) {
|
|
|
+ if (StringUtils.isBlank(projectCode) && StringUtils.isBlank(projectName)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ int rowCount = 0;
|
|
|
+ for (Map row : billableRows) {
|
|
|
+ rowCount++;
|
|
|
+ if (!matchesProject(row, A_BIL_PROJECT_CODE, A_BIL_PROJECT_NAME, projectCode, projectName)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for (Map row : nonBillableRows) {
|
|
|
+ rowCount++;
|
|
|
+ if (!matchesProject(row, A_NON_PROJECT_CODE, A_NON_PROJECT_NAME, projectCode, projectName)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return rowCount > 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static boolean matchesProject(Map row,
|
|
|
+ String codeField,
|
|
|
+ String nameField,
|
|
|
+ String projectCode,
|
|
|
+ String projectName) {
|
|
|
+ String rowProjectCode = str(row, codeField);
|
|
|
+ if (StringUtils.isNotBlank(projectCode)) {
|
|
|
+ return projectCode.equals(rowProjectCode);
|
|
|
+ }
|
|
|
+ return StringUtils.isBlank(rowProjectCode) && StringUtils.equals(projectName, str(row, nameField));
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 其他工时审批处理(与 processManagerApproval 同构,但 approver 为员工的部门主管)
|
|
|
*/
|
|
|
@@ -1270,7 +1350,7 @@ public class ReSubmitApprovalService {
|
|
|
return Collections.singletonList(source);
|
|
|
}
|
|
|
List<ManagerGroup> shards = new ArrayList<>();
|
|
|
- shards.add(newManagerGroup(source.managerId));
|
|
|
+ shards.add(newManagerGroup(source.managerId, source.projectCode, source.projectName));
|
|
|
for (BillRow row : source.billableRows) {
|
|
|
appendManagerRow(shards, row, true);
|
|
|
}
|
|
|
@@ -1280,9 +1360,11 @@ public class ReSubmitApprovalService {
|
|
|
return shards;
|
|
|
}
|
|
|
|
|
|
- private ManagerGroup newManagerGroup(String managerId) {
|
|
|
+ private ManagerGroup newManagerGroup(String managerId, String projectCode, String projectName) {
|
|
|
ManagerGroup group = new ManagerGroup();
|
|
|
group.managerId = managerId;
|
|
|
+ group.projectCode = projectCode;
|
|
|
+ group.projectName = projectName;
|
|
|
return group;
|
|
|
}
|
|
|
|
|
|
@@ -1290,7 +1372,7 @@ public class ReSubmitApprovalService {
|
|
|
ManagerGroup current = shards.get(shards.size() - 1);
|
|
|
if (current.billableRows.size() + current.nonBillableRows.size()
|
|
|
>= MAX_APPROVAL_SUBTABLE_ROWS) {
|
|
|
- current = newManagerGroup(current.managerId);
|
|
|
+ current = newManagerGroup(current.managerId, current.projectCode, current.projectName);
|
|
|
shards.add(current);
|
|
|
}
|
|
|
if (billable) {
|
|
|
@@ -1528,7 +1610,7 @@ public class ReSubmitApprovalService {
|
|
|
return (long) num(m, k);
|
|
|
}
|
|
|
|
|
|
- private String str(Map m, String k) {
|
|
|
+ private static String str(Map m, String k) {
|
|
|
Object v = m == null ? null : m.get(k);
|
|
|
return v == null ? "" : String.valueOf(v);
|
|
|
}
|