|
|
@@ -0,0 +1,568 @@
|
|
|
+package com.malk.service.workhours;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
+import com.malk.server.aliwork.YDConf;
|
|
|
+import com.malk.server.aliwork.YDParam;
|
|
|
+import com.malk.server.common.McException;
|
|
|
+import com.malk.server.workhours.TimeCardApprovalStartRequest;
|
|
|
+import com.malk.server.workhours.TimeCardRepairContextRequest;
|
|
|
+import com.malk.server.workhours.TimeCardRepairSubmitRequest;
|
|
|
+import com.malk.server.workhours.TimeCardRepairSummaryUpsertRequest;
|
|
|
+import com.malk.server.workhours.WHConf;
|
|
|
+import com.malk.service.aliwork.YDClient;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.apache.commons.lang3.StringUtils;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.time.ZoneId;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.time.YearMonth;
|
|
|
+import java.util.*;
|
|
|
+
|
|
|
+/** 补填 TimeCard 专用服务;不复用正常汇总表写入目标。 */
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+public class TimeCardRepairService {
|
|
|
+ private static final ZoneId ZONE = ZoneId.of("Asia/Shanghai");
|
|
|
+ private static final DateTimeFormatter MONTH = DateTimeFormatter.ofPattern("yyyyMM");
|
|
|
+ private static final String DEFAULT_MANAGER_ID = "634735129";
|
|
|
+ // 宜搭查询接口对复制表新增的 mokwy 字段返回 field is null;沿用复制表中可检索的原字段。
|
|
|
+ private static final String R_SUBMITTER = "textField_mmbffvd9";
|
|
|
+ private static final String R_DAY = "textField_mmbffvdb";
|
|
|
+ private static final String R_MONTH = "textField_mmbffvda";
|
|
|
+ private static final String R_STATUS = "selectField_mre1xz9g";
|
|
|
+ private static final String R_SUBMITTER_EMP = "employeeField_mmacxew4";
|
|
|
+ private static final String R_MONTH_DATE = "dateField_mmacxewf";
|
|
|
+ private static final String R_DAY_DATE = "dateField_mmbffvd8";
|
|
|
+ private static final String R_BIL_TABLE = "tableField_mmczo634";
|
|
|
+ private static final String R_NON_TABLE = "tableField_mmczo63h";
|
|
|
+ private static final String R_OTHER_TABLE = "tableField_mmeakgid";
|
|
|
+
|
|
|
+ @Autowired private YDClient ydClient;
|
|
|
+ @Autowired private WHConf whConf;
|
|
|
+ @Autowired private MonthClosingService monthClosingService;
|
|
|
+ @Autowired private ReSubmitApprovalService reSubmitApprovalService;
|
|
|
+
|
|
|
+ public Map<String, Object> context(TimeCardRepairContextRequest request) {
|
|
|
+ validateMonthRequest(request == null ? null : request.getSubmitterUserId(),
|
|
|
+ request == null ? null : request.getSourceMonthText());
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ result.put("closed", monthClosingService.isClosed(request.getSourceMonthText()));
|
|
|
+ result.put("originalRecords", query(whConf.getFormUuidWorkHoursSummary(),
|
|
|
+ mapOf("textField_mmbffvd9", request.getSubmitterUserId(), "textField_mmbffvda", request.getSourceMonthText())));
|
|
|
+ List<Map> repairRecords = query(whConf.getFormUuidRepairWorkHoursSummary(),
|
|
|
+ mapOf(R_SUBMITTER, request.getSubmitterUserId(), R_MONTH, request.getSourceMonthText()));
|
|
|
+ result.put("repairRecords", repairRecords);
|
|
|
+ // 兼容页面状态合并接口的 records 约定;原始双表结果仍通过 repairRecords 返回。
|
|
|
+ result.put("records", repairRecords);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ public boolean upsert(TimeCardRepairSummaryUpsertRequest request) {
|
|
|
+ McException.assertAccessException(request == null, "请求不能为空");
|
|
|
+ McException.assertAccessException(StringUtils.isBlank(request.getDayText())
|
|
|
+ || !request.getDayText().matches("\\d{8}"), "日期必须为yyyyMMdd");
|
|
|
+ try {
|
|
|
+ LocalDate.parse(request.getDayText(), DateTimeFormatter.ofPattern("yyyyMMdd"));
|
|
|
+ } catch (RuntimeException ex) {
|
|
|
+ McException.assertAccessException(true, "日期必须为有效的yyyyMMdd");
|
|
|
+ }
|
|
|
+ String monthText = request.getDayText().substring(0, 6);
|
|
|
+ validateMonthRequest(request.getSubmitterUserId(), monthText);
|
|
|
+ McException.assertAccessException(!monthClosingService.isClosed(monthText), "补填只能保存已封账月份");
|
|
|
+ McException.assertAccessException(StringUtils.isBlank(whConf.getFormUuidRepairWorkHoursSummary()), "补填汇总表未配置");
|
|
|
+ McException.assertAccessException(request.getFormData() == null, "补填工时数据不能为空");
|
|
|
+ assertSourceDateAvailable(request.getSubmitterUserId(), request.getDayText());
|
|
|
+ assertRepairRecordMutable(request.getSubmitterUserId(), request.getDayText());
|
|
|
+ Map<String, Object> data = new LinkedHashMap<>(request.getFormData());
|
|
|
+ data.put(R_SUBMITTER, request.getSubmitterUserId());
|
|
|
+ data.put(R_DAY, request.getDayText());
|
|
|
+ data.put(R_MONTH, request.getDayText().substring(0, 6));
|
|
|
+ data.put(R_STATUS, "暂存");
|
|
|
+ ydClient.operateData(YDParam.builder().appType(whConf.getYidaAppType())
|
|
|
+ .systemToken(whConf.getYidaSystemToken()).formUuid(whConf.getFormUuidRepairWorkHoursSummary())
|
|
|
+ .searchCondition(JSON.toJSONString(mapOf(R_SUBMITTER, request.getSubmitterUserId(), R_DAY, request.getDayText())))
|
|
|
+ .formDataJson(JSON.toJSONString(data)).noExecuteExpression(true).build(), YDConf.FORM_OPERATION.upsert);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Map<String, Object> submit(TimeCardRepairSubmitRequest request) {
|
|
|
+ McException.assertAccessException(request == null, "请求不能为空");
|
|
|
+ validateMonthRequest(request.getSubmitterUserId(), request.getSourceMonthText());
|
|
|
+ McException.assertAccessException(!monthClosingService.isClosed(request.getSourceMonthText()), "补填只能提交已封账月份");
|
|
|
+ String batch = UUID.randomUUID().toString();
|
|
|
+ long submitDate = LocalDate.now(ZONE).atStartOfDay(ZONE).toInstant().toEpochMilli();
|
|
|
+ Map<String, Object> projectGroups = request.getProjectGroups() == null ? Collections.emptyMap() : request.getProjectGroups();
|
|
|
+ Map other = request.getOtherGroup() == null ? Collections.emptyMap() : request.getOtherGroup();
|
|
|
+ int rowCount = validateSubmitRows(request, projectGroups, other);
|
|
|
+ McException.assertAccessException(rowCount == 0, "没有需要提交的补填工时");
|
|
|
+ for (Object rawGroup : projectGroups.values()) {
|
|
|
+ if (!(rawGroup instanceof Map)) continue;
|
|
|
+ Map group = (Map) rawGroup;
|
|
|
+ String managerId = normalizeManagerId(text(group.get("managerId")));
|
|
|
+ for (String category : new String[]{"billableRows", "nonBillableRows"}) {
|
|
|
+ for (Object rawRow : list(group.get(category))) {
|
|
|
+ if (rawRow instanceof Map) {
|
|
|
+ upsertRepairRow(request, (Map) rawRow, category, managerId, batch, submitDate);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ String otherManagerId = normalizeManagerId(text(other.get("managerId")));
|
|
|
+ for (Object rawRow : list(other.get("otherRows"))) {
|
|
|
+ if (rawRow instanceof Map) {
|
|
|
+ upsertRepairRow(request, (Map) rawRow, "otherRows", otherManagerId, batch, submitDate);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ startApprovals(request, projectGroups, other, batch, submitDate);
|
|
|
+ return mapOf("batch", batch, "submitDate", submitDate, "status", "审批中");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 补填审批补发起:只扫描近期修改的补填汇总,按批次补齐缺失的审批分组。
|
|
|
+ * 正常 TimeCard 补发逻辑仍由 ReSubmitApprovalService 独立处理。
|
|
|
+ *
|
|
|
+ * @param lookbackDays 最近修改自然日数量
|
|
|
+ * @return 本次补发的审批分组数量
|
|
|
+ */
|
|
|
+ public int resubmitRecentlyModifiedPending(int lookbackDays) {
|
|
|
+ String[] window = modifiedDateWindow(lookbackDays);
|
|
|
+ List<Map> records = queryModified(
|
|
|
+ whConf.getFormUuidRepairWorkHoursSummary(), window[0], window[1]);
|
|
|
+ Map<String, RepairBatch> batches = buildPendingBatches(records);
|
|
|
+ int started = 0;
|
|
|
+ for (RepairBatch batch : batches.values()) {
|
|
|
+ Set<String> covered = loadRepairApprovalGroupCoverage(batch.monthText, batch.batchId);
|
|
|
+ Map<String, Object> missingProjects = new LinkedHashMap<>();
|
|
|
+ for (Map.Entry<String, Object> entry : batch.projectGroups.entrySet()) {
|
|
|
+ if (!covered.contains("P|" + entry.getKey())) {
|
|
|
+ missingProjects.put(entry.getKey(), entry.getValue());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ Map<String, Object> missingOther = Collections.emptyMap();
|
|
|
+ if (!batch.otherGroup.isEmpty()) {
|
|
|
+ String managerId = text(batch.otherGroup.get("managerId"));
|
|
|
+ if (!covered.contains("O|" + managerId)) {
|
|
|
+ missingOther = batch.otherGroup;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (missingProjects.isEmpty() && missingOther.isEmpty()) continue;
|
|
|
+ TimeCardRepairSubmitRequest request = new TimeCardRepairSubmitRequest();
|
|
|
+ request.setSourceMonthText(batch.monthText);
|
|
|
+ request.setSubmitterUserId(batch.userId);
|
|
|
+ request.setSubmitterName(batch.submitterName);
|
|
|
+ request.setProjectGroups(missingProjects);
|
|
|
+ request.setOtherGroup(missingOther);
|
|
|
+ startApprovals(request, missingProjects, missingOther, batch.batchId, batch.submitDate);
|
|
|
+ started += missingProjects.size() + (missingOther.isEmpty() ? 0 : 1);
|
|
|
+ }
|
|
|
+ log.info("[补填审批补发] from={} to={} records={} batches={} started={}",
|
|
|
+ window[0], window[1], records.size(), batches.size(), started);
|
|
|
+ return started;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void upsertRepairRow(TimeCardRepairSubmitRequest request, Map row, String category,
|
|
|
+ String managerId,
|
|
|
+ String batch, long submitDate) {
|
|
|
+ String day = text(row.get("dateStr"));
|
|
|
+ if (StringUtils.isBlank(day)) day = text(row.get("dayText"));
|
|
|
+ String key = day;
|
|
|
+ Map<String, Object> data = new LinkedHashMap<>();
|
|
|
+ data.put(R_SUBMITTER, request.getSubmitterUserId());
|
|
|
+ data.put(R_DAY, day);
|
|
|
+ data.put(R_MONTH, request.getSourceMonthText());
|
|
|
+ data.put(R_STATUS, "审批中");
|
|
|
+ data.put(whConf.getRepairSummaryFillTypeField(), "补填");
|
|
|
+ data.put(whConf.getRepairSummarySubmitDateField(), submitDate);
|
|
|
+ data.put(whConf.getRepairSummaryBatchField(), batch);
|
|
|
+ data.put(whConf.getRepairSummarySourceMonthField(), request.getSourceMonthText());
|
|
|
+ data.put(R_SUBMITTER_EMP, Collections.singletonList(request.getSubmitterUserId()));
|
|
|
+ data.put(R_MONTH_DATE, LocalDate.parse(request.getSourceMonthText() + "01", DateTimeFormatter.ofPattern("yyyyMMdd"))
|
|
|
+ .atStartOfDay(ZONE).toInstant().toEpochMilli());
|
|
|
+ data.put(R_DAY_DATE, LocalDate.parse(day, DateTimeFormatter.ofPattern("yyyyMMdd"))
|
|
|
+ .atStartOfDay(ZONE).toInstant().toEpochMilli());
|
|
|
+ Map<String, Object> child = new LinkedHashMap<>();
|
|
|
+ double hours = num(row.get("hours"));
|
|
|
+ if ("billableRows".equals(category)) {
|
|
|
+ child.put("numberField_mmczo636", hours);
|
|
|
+ child.put("numberField_mmczo637", 0d);
|
|
|
+ } else if ("nonBillableRows".equals(category)) {
|
|
|
+ child.put("numberField_mmczo63c", hours);
|
|
|
+ child.put("numberField_mmczo63d", 0d);
|
|
|
+ } else {
|
|
|
+ child.put("numberField_mmeakgij", hours);
|
|
|
+ child.put("numberField_mmeakgii", 0d);
|
|
|
+ }
|
|
|
+ String tableField;
|
|
|
+ String keyField;
|
|
|
+ if ("billableRows".equals(category)) {
|
|
|
+ child.put("textField_mmacxew3", text(row.get("projectCode")));
|
|
|
+ child.put("textField_mmad6jcj", text(row.get("projectName")));
|
|
|
+ tableField = R_BIL_TABLE; keyField = "textField_mmacxew3";
|
|
|
+ } else if ("nonBillableRows".equals(category)) {
|
|
|
+ child.put("textField_mmczo639", text(row.get("projectCode")));
|
|
|
+ child.put("textField_mmczo63g", text(row.get("projectName")));
|
|
|
+ tableField = R_NON_TABLE; keyField = "textField_mmczo639";
|
|
|
+ } else {
|
|
|
+ child.put("selectField_mmeakgie", text(row.get("activityName")));
|
|
|
+ tableField = R_OTHER_TABLE; keyField = "selectField_mmeakgie";
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(managerId)) {
|
|
|
+ if ("billableRows".equals(category)) {
|
|
|
+ child.put("employeeField_mkoxpswf", Collections.singletonList(managerId));
|
|
|
+ } else if ("nonBillableRows".equals(category)) {
|
|
|
+ child.put("employeeField_mmczo63a", Collections.singletonList(managerId));
|
|
|
+ } else {
|
|
|
+ data.put("employeeField_mh8xhqc3", Collections.singletonList(managerId));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ List<Map> current = new ArrayList<>();
|
|
|
+ List<Map> found = query(whConf.getFormUuidRepairWorkHoursSummary(), mapOf(R_SUBMITTER, request.getSubmitterUserId(), R_DAY, key));
|
|
|
+ McException.assertAccessException(found.size() > 1, "补填汇总记录重复 userId="
|
|
|
+ + request.getSubmitterUserId() + ", day=" + key);
|
|
|
+ if (found.size() == 1) {
|
|
|
+ String id = text(found.get(0).get("formInstId"));
|
|
|
+ if (StringUtils.isBlank(id)) id = text(found.get(0).get("formInstanceId"));
|
|
|
+ current = new ArrayList<>(list(detail(id).get(tableField)));
|
|
|
+ }
|
|
|
+ String submittedField = "billableRows".equals(category)
|
|
|
+ ? "numberField_mmczo636"
|
|
|
+ : ("nonBillableRows".equals(category) ? "numberField_mmczo63c" : "numberField_mmeakgij");
|
|
|
+ mergeSubmittedRow(current, child, keyField, submittedField, hours);
|
|
|
+ data.put(tableField, current);
|
|
|
+ ydClient.operateData(YDParam.builder().appType(whConf.getYidaAppType()).systemToken(whConf.getYidaSystemToken())
|
|
|
+ .formUuid(whConf.getFormUuidRepairWorkHoursSummary())
|
|
|
+ .searchCondition(JSON.toJSONString(mapOf(R_SUBMITTER, request.getSubmitterUserId(), R_DAY, key)))
|
|
|
+ .formDataJson(JSON.toJSONString(data)).noExecuteExpression(true).build(), YDConf.FORM_OPERATION.upsert);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map detail(String id) {
|
|
|
+ if (StringUtils.isBlank(id)) return new HashMap();
|
|
|
+ com.malk.server.dingtalk.DDR_New result = ydClient.queryData(YDParam.builder()
|
|
|
+ .appType(whConf.getYidaAppType()).systemToken(whConf.getYidaSystemToken()).formInstanceId(id).build(), YDConf.FORM_QUERY.retrieve_id);
|
|
|
+ return result == null || result.getFormData() == null ? new HashMap() : result.getFormData();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void startApprovals(TimeCardRepairSubmitRequest request, Map<String, Object> groups,
|
|
|
+ Map<String, Object> other, String batch, long submitDate) {
|
|
|
+ for (Object raw : groups.values()) {
|
|
|
+ if (!(raw instanceof Map)) continue;
|
|
|
+ Map group = (Map) raw;
|
|
|
+ Map<String, Object> fd = new LinkedHashMap<>();
|
|
|
+ fd.put("dateField_mmacxewf", parseMonth(request.getSourceMonthText()));
|
|
|
+ fd.put("employeeField_moi9pvt1", Collections.singletonList(request.getSubmitterUserId()));
|
|
|
+ fd.put(whConf.getApprovalFillTypeField(), "补填");
|
|
|
+ fd.put(whConf.getApprovalRepairSubmitDateField(), submitDate);
|
|
|
+ fd.put(whConf.getApprovalRepairBatchField(), batch);
|
|
|
+ fd.put(whConf.getApprovalSourceMonthField(), request.getSourceMonthText());
|
|
|
+ fd.put("textField_mmd2lv0y", request.getSourceMonthText());
|
|
|
+ fd.put("numberField_mmd2z155", sumRows(group.get("billableRows")));
|
|
|
+ fd.put("numberField_mmd2z156", sumRows(group.get("nonBillableRows")));
|
|
|
+ fd.put("numberField_mmd5b5gl", sumRows(group.get("billableRows")) + sumRows(group.get("nonBillableRows")));
|
|
|
+ fd.put("employeeField_mkoxpswf", Collections.singletonList(
|
|
|
+ normalizeManagerId(text(group.get("managerId")))));
|
|
|
+ fd.put("tableField_mmae8t99", approvalRows(group.get("billableRows"), true, request));
|
|
|
+ fd.put("tableField_mmd1wu9h", approvalRows(group.get("nonBillableRows"), false, request));
|
|
|
+ start(request.getSubmitterUserId(), whConf.getFormUuidApproval(), whConf.getApprovalProcessCode(), fd);
|
|
|
+ }
|
|
|
+ if (other != null && !list(other.get("otherRows")).isEmpty()) {
|
|
|
+ Map<String, Object> fd = new LinkedHashMap<>();
|
|
|
+ fd.put("dateField_mmacxewf", parseMonth(request.getSourceMonthText()));
|
|
|
+ fd.put("employeeField_moi9pvt1", Collections.singletonList(request.getSubmitterUserId()));
|
|
|
+ fd.put(whConf.getApprovalFillTypeField(), "补填");
|
|
|
+ fd.put(whConf.getApprovalRepairSubmitDateField(), submitDate);
|
|
|
+ fd.put(whConf.getApprovalRepairBatchField(), batch);
|
|
|
+ fd.put(whConf.getApprovalSourceMonthField(), request.getSourceMonthText());
|
|
|
+ fd.put("textField_mmd2lv0y", request.getSourceMonthText());
|
|
|
+ fd.put("numberField_mmd5b5gl", sumRows(other.get("otherRows")));
|
|
|
+ fd.put("employeeField_mh8xhqc3", Collections.singletonList(
|
|
|
+ normalizeManagerId(text(other.get("managerId")))));
|
|
|
+ fd.put("tableField_mmd1wu9h", approvalRows(other.get("otherRows"), false, request));
|
|
|
+ start(request.getSubmitterUserId(), whConf.getFormUuidOtherApproval(), whConf.getOtherApprovalProcessCode(), fd);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void start(String userId, String formUuid, String processCode, Map<String, Object> data) {
|
|
|
+ reSubmitApprovalService.startApproval(new TimeCardApprovalStartRequest() {{
|
|
|
+ setFormUuid(formUuid); setProcessCode(processCode); setFormDataJson(JSON.toJSONString(data));
|
|
|
+ }});
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<Map<String, Object>> approvalRows(Object raw, boolean billable, TimeCardRepairSubmitRequest request) {
|
|
|
+ List<Map<String, Object>> result = new ArrayList<>();
|
|
|
+ for (Object item : list(raw)) {
|
|
|
+ if (!(item instanceof Map)) continue;
|
|
|
+ Map row = (Map) item;
|
|
|
+ String day = text(row.get("dateStr"));
|
|
|
+ Map<String, Object> out = new LinkedHashMap<>();
|
|
|
+ out.put(billable ? "dateField_mmbffvd8" : "dateField_mmd2z150", parseDay(day));
|
|
|
+ out.put(billable ? "textField_mmd8g654" : "textField_mmd8g655", day);
|
|
|
+ out.put(billable ? "textField_mmd1ozk4" : "textField_mmd1wu9f", request.getSubmitterUserId());
|
|
|
+ out.put(billable ? "employeeField_mmacxew4" : "employeeField_mmd1wu9b",
|
|
|
+ Collections.singletonList(request.getSubmitterUserId()));
|
|
|
+ out.put(billable ? "textField_mmdbi2n2" : "textField_mmdbi2n0", request.getSourceMonthText());
|
|
|
+ out.put(billable ? "numberField_mmacxew9" : "numberField_mmd1wu9e", num(row.get("hours")));
|
|
|
+ out.put(billable ? "textField_mmacxew3" : "textField_mmd1wu9d", text(row.get("projectCode")));
|
|
|
+ if (billable) out.put("textField_mkov5ozd", text(row.get("projectName")));
|
|
|
+ else if (!text(row.get("activityName")).isEmpty()) {
|
|
|
+ out.put("selectField_mmeakgie", text(row.get("activityName")));
|
|
|
+ out.remove("textField_mmd1wu9d");
|
|
|
+ } else out.put("textField_mmd1wu9c", text(row.get("projectName")));
|
|
|
+ // 其他工时审批沿用 radioField_mpwhk294;只有 Non Billable 使用 295。
|
|
|
+ String opinionField = billable || out.containsKey("selectField_mmeakgie")
|
|
|
+ ? "radioField_mpwhk294" : "radioField_mpwhk295";
|
|
|
+ out.put(opinionField, "同意");
|
|
|
+ result.add(out);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private long parseDay(String day) { return LocalDate.parse(day, DateTimeFormatter.ofPattern("yyyyMMdd")).atStartOfDay(ZONE).toInstant().toEpochMilli(); }
|
|
|
+ private List<Map> query(String formUuid, Map<String, Object> search) {
|
|
|
+ if (StringUtils.isBlank(formUuid)) return new ArrayList<>();
|
|
|
+ Object data = ydClient.queryData(YDParam.builder().appType(whConf.getYidaAppType()).systemToken(whConf.getYidaSystemToken())
|
|
|
+ .formUuid(formUuid).searchFieldJson(JSON.toJSONString(search)).currentPage(1).pageSize(100).build(), YDConf.FORM_QUERY.retrieve_search_form).getData();
|
|
|
+ return data instanceof List ? (List<Map>) data : new ArrayList<>();
|
|
|
+ }
|
|
|
+ private void validateMonthRequest(String userId, String month) {
|
|
|
+ McException.assertAccessException(StringUtils.isAnyBlank(userId, month)
|
|
|
+ || !month.matches("\\d{6}"), "员工和月份不能为空且月份必须为yyyyMM");
|
|
|
+ try {
|
|
|
+ YearMonth.parse(month, MONTH);
|
|
|
+ } catch (RuntimeException ex) {
|
|
|
+ McException.assertAccessException(true, "月份必须为有效的yyyyMM");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void validateRepairRow(TimeCardRepairSubmitRequest request, Map row) {
|
|
|
+ String day = text(row.get("dateStr"));
|
|
|
+ if (StringUtils.isBlank(day)) day = text(row.get("dayText"));
|
|
|
+ McException.assertAccessException(!day.matches("\\d{8}")
|
|
|
+ || !day.startsWith(request.getSourceMonthText()), "补填日期必须属于来源月份");
|
|
|
+ try {
|
|
|
+ LocalDate.parse(day, DateTimeFormatter.ofPattern("yyyyMMdd"));
|
|
|
+ } catch (RuntimeException ex) {
|
|
|
+ McException.assertAccessException(true, "补填日期必须为有效的yyyyMMdd");
|
|
|
+ }
|
|
|
+ double hours = num(row.get("hours"));
|
|
|
+ McException.assertAccessException(hours <= 0d || hours > 24d, "补填工时必须大于0且不超过24小时");
|
|
|
+ assertSourceDateAvailable(request.getSubmitterUserId(), day);
|
|
|
+ assertRepairRecordMutable(request.getSubmitterUserId(), day);
|
|
|
+ }
|
|
|
+
|
|
|
+ private int validateSubmitRows(TimeCardRepairSubmitRequest request,
|
|
|
+ Map<String, Object> projectGroups,
|
|
|
+ Map<String, Object> otherGroup) {
|
|
|
+ int count = 0;
|
|
|
+ for (Object rawGroup : projectGroups.values()) {
|
|
|
+ if (!(rawGroup instanceof Map)) continue;
|
|
|
+ Map group = (Map) rawGroup;
|
|
|
+ for (String category : new String[]{"billableRows", "nonBillableRows"}) {
|
|
|
+ for (Object rawRow : list(group.get(category))) {
|
|
|
+ if (!(rawRow instanceof Map)) continue;
|
|
|
+ validateRepairRow(request, (Map) rawRow);
|
|
|
+ count++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for (Object rawRow : list(otherGroup.get("otherRows"))) {
|
|
|
+ if (!(rawRow instanceof Map)) continue;
|
|
|
+ validateRepairRow(request, (Map) rawRow);
|
|
|
+ count++;
|
|
|
+ }
|
|
|
+ return count;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void assertSourceDateAvailable(String userId, String day) {
|
|
|
+ List<Map> original = query(whConf.getFormUuidWorkHoursSummary(), mapOf(R_SUBMITTER, userId, R_DAY, day));
|
|
|
+ McException.assertAccessException(original.size() > 0, "来源日期已有原工时记录,不能补填 userId=" + userId + ", day=" + day);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void assertRepairRecordMutable(String userId, String day) {
|
|
|
+ List<Map> existing = query(whConf.getFormUuidRepairWorkHoursSummary(), mapOf(R_SUBMITTER, userId, R_DAY, day));
|
|
|
+ McException.assertAccessException(existing.size() > 1, "补填汇总记录重复 userId=" + userId + ", day=" + day);
|
|
|
+ if (existing.size() != 1) return;
|
|
|
+ Map fd = (Map) existing.get(0).get("formData");
|
|
|
+ if (fd == null) fd = existing.get(0);
|
|
|
+ String status = text(fd.get(R_STATUS));
|
|
|
+ McException.assertAccessException("审批中".equals(status) || "已审批".equals(status),
|
|
|
+ "该日期已提交或审批完成,不能再次补填");
|
|
|
+ }
|
|
|
+
|
|
|
+ private long parseMonth(String month) {
|
|
|
+ return LocalDate.parse(month + "01", DateTimeFormatter.ofPattern("yyyyMMdd"))
|
|
|
+ .atStartOfDay(ZONE).toInstant().toEpochMilli();
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, RepairBatch> buildPendingBatches(List<Map> records) {
|
|
|
+ Map<String, RepairBatch> batches = new LinkedHashMap<>();
|
|
|
+ for (Map item : records) {
|
|
|
+ Map fd = formData(item);
|
|
|
+ if (!"审批中".equals(text(fd.get(R_STATUS)))) continue;
|
|
|
+ String batchId = text(fd.get(whConf.getRepairSummaryBatchField()));
|
|
|
+ String userId = text(fd.get(R_SUBMITTER));
|
|
|
+ String monthText = text(fd.get(R_MONTH));
|
|
|
+ if (StringUtils.isAnyBlank(batchId, userId, monthText)) continue;
|
|
|
+ RepairBatch batch = batches.computeIfAbsent(batchId, key -> {
|
|
|
+ RepairBatch value = new RepairBatch();
|
|
|
+ value.batchId = batchId;
|
|
|
+ value.userId = userId;
|
|
|
+ value.monthText = monthText;
|
|
|
+ value.submitDate = (long) num(fd.get(whConf.getRepairSummarySubmitDateField()));
|
|
|
+ if (value.submitDate <= 0) {
|
|
|
+ value.submitDate = LocalDate.now(ZONE).atStartOfDay(ZONE).toInstant().toEpochMilli();
|
|
|
+ }
|
|
|
+ value.submitterName = text(fd.get("textField_mmoazo1b"));
|
|
|
+ return value;
|
|
|
+ });
|
|
|
+ String dayText = text(fd.get(R_DAY));
|
|
|
+ collectPendingProjectRows(batch, fd, dayText, true);
|
|
|
+ collectPendingProjectRows(batch, fd, dayText, false);
|
|
|
+ collectPendingOtherRows(batch, fd, dayText);
|
|
|
+ }
|
|
|
+ return batches;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void collectPendingProjectRows(RepairBatch batch, Map fd, String dayText, boolean billable) {
|
|
|
+ String tableField = billable ? R_BIL_TABLE : R_NON_TABLE;
|
|
|
+ String managerField = billable ? "employeeField_mkoxpswf" : "employeeField_mmczo63a";
|
|
|
+ String submittedField = billable ? "numberField_mmczo636" : "numberField_mmczo63c";
|
|
|
+ String approvedField = billable ? "numberField_mmczo637" : "numberField_mmczo63d";
|
|
|
+ String projectCodeField = billable ? "textField_mmacxew3" : "textField_mmczo639";
|
|
|
+ String projectNameField = billable ? "textField_mmad6jcj" : "textField_mmczo63g";
|
|
|
+ for (Map child : list(fd.get(tableField))) {
|
|
|
+ double hours = num(child.get(submittedField)) - num(child.get(approvedField));
|
|
|
+ if (hours <= 1e-6) continue;
|
|
|
+ String managerId = firstUserId(child, managerField);
|
|
|
+ if (StringUtils.isBlank(managerId)) managerId = DEFAULT_MANAGER_ID;
|
|
|
+ final String resolvedManagerId = managerId;
|
|
|
+ Map<String, Object> group = (Map<String, Object>) batch.projectGroups.computeIfAbsent(resolvedManagerId, key -> {
|
|
|
+ Map<String, Object> value = new LinkedHashMap<>();
|
|
|
+ value.put("managerId", resolvedManagerId);
|
|
|
+ value.put("billableRows", new ArrayList<Map>());
|
|
|
+ value.put("nonBillableRows", new ArrayList<Map>());
|
|
|
+ return value;
|
|
|
+ });
|
|
|
+ Map<String, Object> row = mapOf(
|
|
|
+ "dateStr", dayText,
|
|
|
+ "projectCode", text(child.get(projectCodeField)),
|
|
|
+ "projectName", text(child.get(projectNameField)),
|
|
|
+ "hours", hours);
|
|
|
+ ((List<Map<String, Object>>) group.get(billable ? "billableRows" : "nonBillableRows")).add(row);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void collectPendingOtherRows(RepairBatch batch, Map fd, String dayText) {
|
|
|
+ for (Map child : list(fd.get(R_OTHER_TABLE))) {
|
|
|
+ double hours = num(child.get("numberField_mmeakgij")) - num(child.get("numberField_mmeakgii"));
|
|
|
+ if (hours <= 1e-6) continue;
|
|
|
+ String managerId = firstUserId(fd, "employeeField_mh8xhqc3");
|
|
|
+ if (StringUtils.isBlank(managerId)) managerId = DEFAULT_MANAGER_ID;
|
|
|
+ batch.otherGroup.put("managerId", managerId);
|
|
|
+ List<Map<String, Object>> rows = (List<Map<String, Object>>) batch.otherGroup.computeIfAbsent(
|
|
|
+ "otherRows", key -> new ArrayList<Map<String, Object>>());
|
|
|
+ rows.add(mapOf("dateStr", dayText,
|
|
|
+ "activityName", text(child.get("selectField_mmeakgie")), "hours", hours));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private Set<String> loadRepairApprovalGroupCoverage(String monthText, String batchId) {
|
|
|
+ Set<String> covered = new HashSet<>();
|
|
|
+ Map<String, Object> search = mapOf(
|
|
|
+ whConf.getApprovalFillTypeField(), "补填",
|
|
|
+ whConf.getApprovalRepairBatchField(), batchId,
|
|
|
+ whConf.getApprovalSourceMonthField(), monthText);
|
|
|
+ for (Map item : queryProcesses(whConf.getFormUuidApproval(), search)) {
|
|
|
+ Map fd = formData(item);
|
|
|
+ covered.add("P|" + firstUserId(fd, "employeeField_mkoxpswf"));
|
|
|
+ }
|
|
|
+ for (Map item : queryProcesses(whConf.getFormUuidOtherApproval(), search)) {
|
|
|
+ Map fd = formData(item);
|
|
|
+ covered.add("O|" + firstUserId(fd, "employeeField_mh8xhqc3"));
|
|
|
+ }
|
|
|
+ return covered;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<Map> queryModified(String formUuid, String fromDate, String toDate) {
|
|
|
+ List<Map> all = new ArrayList<>();
|
|
|
+ for (int page = 1; page <= 200; page++) {
|
|
|
+ Object raw = ydClient.queryData(YDParam.builder().appType(whConf.getYidaAppType())
|
|
|
+ .systemToken(whConf.getYidaSystemToken()).formUuid(formUuid)
|
|
|
+ .modifiedFromTimeGMT(fromDate).modifiedToTimeGMT(toDate)
|
|
|
+ .currentPage(page).pageSize(100).build(), YDConf.FORM_QUERY.retrieve_search_form).getData();
|
|
|
+ List<Map> data = raw instanceof List ? (List<Map>) raw : Collections.emptyList();
|
|
|
+ all.addAll(data);
|
|
|
+ if (data.size() < 100) break;
|
|
|
+ }
|
|
|
+ return all;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<Map> queryProcesses(String formUuid, Map<String, Object> search) {
|
|
|
+ List<Map> all = new ArrayList<>();
|
|
|
+ for (int page = 1; page <= 200; page++) {
|
|
|
+ Object raw = ydClient.queryData(YDParam.builder().appType(whConf.getYidaAppType())
|
|
|
+ .systemToken(whConf.getYidaSystemToken()).formUuid(formUuid)
|
|
|
+ .searchFieldJson(JSON.toJSONString(search)).pageNumber(page).pageSize(100)
|
|
|
+ .build(), YDConf.FORM_QUERY.retrieve_search_process).getData();
|
|
|
+ List<Map> data = raw instanceof List ? (List<Map>) raw : Collections.emptyList();
|
|
|
+ all.addAll(data);
|
|
|
+ if (data.size() < 100) break;
|
|
|
+ }
|
|
|
+ return all;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String[] modifiedDateWindow(int lookbackDays) {
|
|
|
+ int safeDays = Math.max(1, lookbackDays);
|
|
|
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
+ return new String[]{LocalDate.now(ZONE).minusDays(safeDays - 1L).format(formatter),
|
|
|
+ LocalDate.now(ZONE).plusDays(1L).format(formatter)};
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map formData(Map item) {
|
|
|
+ Object fd = item == null ? null : item.get("formData");
|
|
|
+ if (!(fd instanceof Map)) fd = item == null ? null : item.get("data");
|
|
|
+ return fd instanceof Map ? (Map) fd : (item == null ? Collections.emptyMap() : item);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String firstUserId(Map data, String field) {
|
|
|
+ Object value = data == null ? null : data.get(field + "_id");
|
|
|
+ if (!(value instanceof List) || ((List) value).isEmpty()) value = data == null ? null : data.get(field);
|
|
|
+ if (value instanceof List && !((List) value).isEmpty()) return text(((List) value).get(0));
|
|
|
+ return text(value);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String normalizeManagerId(String managerId) {
|
|
|
+ return StringUtils.isBlank(managerId) ? DEFAULT_MANAGER_ID : managerId;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 提交请求是页面当前值的完整快照;已有草稿应覆盖而不是再次累加。
|
|
|
+ */
|
|
|
+ static void mergeSubmittedRow(List<Map> current, Map child, String keyField,
|
|
|
+ String submittedField, double hours) {
|
|
|
+ for (Map existing : current) {
|
|
|
+ if (text(existing.get(keyField)).equals(text(child.get(keyField)))) {
|
|
|
+ existing.put(submittedField, hours);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ current.add(child);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class RepairBatch {
|
|
|
+ private String batchId;
|
|
|
+ private String userId;
|
|
|
+ private String submitterName;
|
|
|
+ private String monthText;
|
|
|
+ private long submitDate;
|
|
|
+ private final Map<String, Object> projectGroups = new LinkedHashMap<>();
|
|
|
+ private final Map<String, Object> otherGroup = new LinkedHashMap<>();
|
|
|
+ }
|
|
|
+ private static Map<String, Object> mapOf(Object... values) { Map<String, Object> m = new LinkedHashMap<>(); for (int i=0;i<values.length;i+=2) m.put(String.valueOf(values[i]), values[i+1]); return m; }
|
|
|
+ private static List<Map> list(Object raw) { return raw instanceof List ? (List<Map>) raw : new ArrayList<>(); }
|
|
|
+ private static String text(Object value) { return value == null ? "" : String.valueOf(value); }
|
|
|
+ private static double num(Object value) { try { return value == null ? 0d : Double.parseDouble(String.valueOf(value)); } catch (NumberFormatException e) { return 0d; } }
|
|
|
+ private static double sumRows(Object raw) { double total=0d; for (Object item:list(raw)) if (item instanceof Map) total += num(((Map)item).get("hours")); return total; }
|
|
|
+}
|