| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859 |
- 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.nio.charset.StandardCharsets;
- import java.security.MessageDigest;
- import java.security.NoSuchAlgorithmException;
- import java.time.LocalDate;
- import java.time.ZoneId;
- import java.time.format.DateTimeFormatter;
- import java.time.YearMonth;
- import java.util.*;
- import java.util.concurrent.ConcurrentHashMap;
- /** 补填 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_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";
- private static final String R_DAILY_DRAFT = "numberField_mmd1smel";
- private static final String R_DAILY_SUBMITTED = "numberField_mmd1smem";
- private static final String R_DAILY_APPROVED = "numberField_mmeakgi4";
- private static final String R_DAILY_CHANGE = "numberField_mmeakgi5";
- private static final long SUBMIT_GUARD_TTL_MILLIS = 30_000L;
- private static final ConcurrentHashMap<String, SubmitGuard> SUBMIT_GUARDS = new ConcurrentHashMap<>();
- private static final List<String> R_TOTAL_FIELDS = Arrays.asList(
- "numberField_mmacxew9", "numberField_mmacxewa", "numberField_mmacxewg", "numberField_mmad6jbc",
- "numberField_mmad6jbx", "numberField_mmad6jbz", "numberField_mmad6jc1", "numberField_mmeakgia",
- "numberField_mmd1smeo", "numberField_mmd1smeq", "numberField_mmd1smes", "numberField_mmeakgic",
- R_DAILY_DRAFT, R_DAILY_SUBMITTED, R_DAILY_APPROVED, R_DAILY_CHANGE);
- @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(StringUtils.isBlank(whConf.getRepairSummarySyncStatusField()), "补填汇总状态字段未配置");
- McException.assertAccessException(request.getFormData() == null, "补填工时数据不能为空");
- assertSourceDateAvailable(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(whConf.getRepairSummarySyncStatusField(), "暂存");
- 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(false).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()), "补填只能提交已封账月份");
- Map<String, Object> projectGroups = request.getProjectGroups() == null ? Collections.emptyMap() : request.getProjectGroups();
- Map other = request.getOtherGroup() == null ? Collections.emptyMap() : request.getOtherGroup();
- String guardKey = buildSubmitGuardKey(request, projectGroups, other);
- SubmitGuard guard = acquireSubmitGuard(guardKey);
- Map<String, Object> cachedResult = guard.cachedResult();
- if (cachedResult != null) return cachedResult;
- try {
- String batch = UUID.randomUUID().toString();
- long submitDate = LocalDate.now(ZONE).atStartOfDay(ZONE).toInstant().toEpochMilli();
- 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);
- Map<String, Object> result = mapOf("batch", batch, "submitDate", submitDate, "status", "审批中");
- guard.complete(result);
- return result;
- } catch (RuntimeException ex) {
- SUBMIT_GUARDS.remove(guardKey, guard);
- throw ex;
- }
- }
- private String buildSubmitGuardKey(TimeCardRepairSubmitRequest request,
- Map<String, Object> projectGroups, Map other) {
- String payload = request.getSubmitterUserId() + "|" + request.getSourceMonthText()
- + "|" + JSON.toJSONString(projectGroups) + "|" + JSON.toJSONString(other);
- try {
- byte[] digest = MessageDigest.getInstance("SHA-256")
- .digest(payload.getBytes(StandardCharsets.UTF_8));
- StringBuilder hex = new StringBuilder(digest.length * 2);
- for (byte value : digest) hex.append(String.format("%02x", value));
- return hex.toString();
- } catch (NoSuchAlgorithmException ex) {
- throw new IllegalStateException("提交幂等键计算失败", ex);
- }
- }
- private SubmitGuard acquireSubmitGuard(String key) {
- long now = System.currentTimeMillis();
- synchronized (SUBMIT_GUARDS) {
- SUBMIT_GUARDS.entrySet().removeIf(entry -> entry.getValue().expiresAt <= now);
- SubmitGuard existing = SUBMIT_GUARDS.get(key);
- if (existing != null && existing.expiresAt > now) {
- if (existing.result != null) return existing;
- McException.assertAccessException(true, "补填提交正在处理中,请勿重复提交");
- }
- SubmitGuard guard = new SubmitGuard(now + SUBMIT_GUARD_TTL_MILLIS);
- SUBMIT_GUARDS.put(key, guard);
- return guard;
- }
- }
- private static final class SubmitGuard {
- private final long expiresAt;
- private Map<String, Object> result;
- private SubmitGuard(long expiresAt) {
- this.expiresAt = expiresAt;
- }
- private synchronized void complete(Map<String, Object> value) {
- this.result = new LinkedHashMap<>(value);
- }
- private synchronized Map<String, Object> cachedResult() {
- return result == null ? null : new LinkedHashMap<>(result);
- }
- }
- /**
- * 补填审批补发起:只扫描近期修改的补填汇总,按员工、月份和补填提交日期补齐缺失明细。
- * 正常 TimeCard 补发逻辑仍由 ReSubmitApprovalService 独立处理。
- *
- * @param lookbackDays 最近修改自然日数量
- * @return 本次补发的审批分组数量及补试后失败批次数
- */
- public RepairResubmitResult resubmitRecentlyModifiedPending(int lookbackDays) {
- String[] window = modifiedDateWindow(lookbackDays);
- List<Map> records = queryModified(
- whConf.getFormUuidRepairWorkHoursSummary(), window[0], window[1]);
- Map<String, RepairBatch> batches = buildPendingBatches(records);
- Map<String, Set<String>> coverageByMonth = new HashMap<>();
- int started = 0;
- int failed = 0;
- List<RepairBatch> retryQueue = new ArrayList<>();
- for (RepairBatch batch : batches.values()) {
- try {
- Set<String> covered = coverageByMonth.computeIfAbsent(
- batch.monthText, this::loadRepairApprovalDetailCoverage);
- started += submitMissingRepairApprovals(batch, covered);
- } catch (Exception ex) {
- retryQueue.add(batch);
- log.warn("[补填审批补发] 首次处理失败,待本轮扫描结束补试 batch={}", batch.batchId, ex);
- }
- }
- for (RepairBatch batch : retryQueue) {
- try {
- started += submitMissingRepairApprovals(
- batch, loadRepairApprovalDetailCoverage(batch.monthText));
- } catch (Exception ex) {
- failed++;
- log.error("[补填审批补发] 本轮补试失败 batch={}", batch.batchId, ex);
- }
- }
- log.info("[补填审批补发] from={} to={} records={} batches={} started={} failed={}",
- window[0], window[1], records.size(), batches.size(), started, failed);
- return new RepairResubmitResult(started, failed);
- }
- public static final class RepairResubmitResult {
- private final int started;
- private final int failed;
- public RepairResubmitResult(int started, int failed) {
- this.started = started;
- this.failed = failed;
- }
- public int getStarted() {
- return started;
- }
- public int getFailed() {
- return failed;
- }
- }
- private int submitMissingRepairApprovals(RepairBatch batch, Set<String> covered) {
- removeCoveredRows(batch, covered);
- Map<String, Object> missingProjects = batch.projectGroups;
- Map<String, Object> missingOther = batch.otherGroup;
- if (missingProjects.isEmpty() && missingOther.isEmpty()) return 0;
- 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);
- return missingProjects.size() + (missingOther.isEmpty() ? 0 : 1);
- }
- 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;
- List<Map> found = query(whConf.getFormUuidRepairWorkHoursSummary(),
- mapOf(R_SUBMITTER, request.getSubmitterUserId(), R_DAY, key));
- McException.assertAccessException(found.size() != 1, "补填汇总完整记录不存在或重复,请先保存后提交 userId="
- + request.getSubmitterUserId() + ", day=" + key);
- String instanceId = text(found.get(0).get("formInstId"));
- if (StringUtils.isBlank(instanceId)) instanceId = text(found.get(0).get("formInstanceId"));
- McException.assertAccessException(StringUtils.isBlank(instanceId), "补填汇总实例ID为空");
- Map existing = detail(instanceId);
- 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(whConf.getRepairSummarySyncStatusField(), "审批中");
- 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 = copyRows(existing.get(tableField));
- String submittedField = "billableRows".equals(category)
- ? "numberField_mmczo636"
- : ("nonBillableRows".equals(category) ? "numberField_mmczo63c" : "numberField_mmeakgij");
- // prd 与原 TimeCard 一致:提交值覆盖 submitted,draft 保留供拒绝后恢复编辑。
- mergeSubmittedRow(current, child, keyField, submittedField, hours);
- data.put(tableField, current);
- Map snapshot = new LinkedHashMap(existing);
- snapshot.put(tableField, current);
- applySummaryTotals(data, snapshot);
- final String targetInstanceId = instanceId;
- operateAndVerify(
- () -> ydClient.operateData(YDParam.builder().appType(whConf.getYidaAppType())
- .systemToken(whConf.getYidaSystemToken()).formInstanceId(targetInstanceId)
- .updateFormDataJson(JSON.toJSONString(data)).ignoreEmpty(false).useLatestVersion(true).build(),
- YDConf.FORM_OPERATION.update),
- () -> verifySubmittedRepairRow(targetInstanceId, tableField, keyField,
- text(child.get(keyField)), submittedField, hours, data));
- }
- private void verifySubmittedRepairRow(String instanceId, String tableField, String keyField,
- String rowKey, String submittedField, double hours,
- Map<String, Object> expected) {
- Map actual = detail(instanceId);
- Map actualRow = null;
- for (Map row : list(actual.get(tableField))) {
- if (rowKey.equals(text(row.get(keyField)))) {
- actualRow = row;
- break;
- }
- }
- McException.assertAccessException(actualRow == null
- || !sameHours(num(actualRow.get(submittedField)), hours),
- "补填汇总提交子表回读不一致 instanceId=" + instanceId + ", key=" + rowKey);
- for (String field : R_TOTAL_FIELDS) {
- McException.assertAccessException(!sameHours(num(actual.get(field)), num(expected.get(field))),
- "补填汇总主表合计回读不一致 instanceId=" + instanceId + ", field=" + field);
- }
- String statusField = whConf.getRepairSummarySyncStatusField();
- McException.assertAccessException(!text(expected.get(statusField)).equals(text(actual.get(statusField))),
- "补填汇总审批状态回读不一致 instanceId=" + instanceId);
- }
- private void operateAndVerify(Runnable operation, Runnable verification) {
- try {
- operation.run();
- } catch (RuntimeException operationException) {
- try {
- verification.run();
- return;
- } catch (RuntimeException verificationException) {
- operationException.addSuppressed(verificationException);
- throw operationException;
- }
- }
- verification.run();
- }
- private static boolean sameHours(double actual, double expected) {
- return Math.abs(actual - expected) <= 1e-6;
- }
- 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.getApprovalRepairSourceMonthField(), 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.getOtherApprovalRepairSubmitDateField(), submitDate);
- fd.put(whConf.getOtherApprovalRepairBatchField(), batch);
- fd.put(whConf.getOtherApprovalRepairSourceMonthField(), 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 category) {
- 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小时");
- if (!"otherRows".equals(category)) {
- McException.assertAccessException(StringUtils.isBlank(text(row.get("projectCode"))),
- ("billableRows".equals(category) ? "Billable" : "Non Billable") + "工时必须填写项目编号");
- }
- assertSourceDateAvailable(request.getSubmitterUserId(), day);
- assertRepairRowMutable(request.getSubmitterUserId(), day, row, category);
- }
- 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, category);
- count++;
- }
- }
- }
- for (Object rawRow : list(otherGroup.get("otherRows"))) {
- if (!(rawRow instanceof Map)) continue;
- validateRepairRow(request, (Map) rawRow, "otherRows");
- count++;
- }
- return count;
- }
- private void assertSourceDateAvailable(String userId, String day) {
- List<Map> original = query(whConf.getFormUuidWorkHoursSummary(), mapOf(R_SUBMITTER, userId, R_DAY, day));
- boolean hasApprovedOrChangedHours = original.stream()
- .anyMatch(TimeCardRepairService::hasApprovedOrChangedHours);
- McException.assertAccessException(hasApprovedOrChangedHours,
- "来源日期已有已审批或变更工时,不能补填 userId=" + userId + ", day=" + day);
- }
- /** 仅原汇总当天已审批与变更工时的实际合计大于 0 时禁止补填;暂存草稿不占用补填资格。 */
- static boolean hasApprovedOrChangedHours(Map record) {
- Object nested = record == null ? null : record.get("formData");
- Map formData = nested instanceof Map ? (Map) nested : record;
- if (formData == null) return false;
- return num(formData.get(R_DAILY_APPROVED)) + num(formData.get(R_DAILY_CHANGE)) > 1e-6;
- }
- private void assertRepairRowMutable(String userId, String day, Map row, String category) {
- 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;
- String id = text(existing.get(0).get("formInstId"));
- if (StringUtils.isBlank(id)) id = text(existing.get(0).get("formInstanceId"));
- Map fd = detail(id);
- String projectCode = text(row.get("projectCode"));
- String activityName = text(row.get("activityName"));
- boolean other = "otherRows".equals(category);
- String table = other ? R_OTHER_TABLE
- : ("billableRows".equals(category) ? R_BIL_TABLE : R_NON_TABLE);
- String keyField = other ? "selectField_mmeakgie"
- : (R_BIL_TABLE.equals(table) ? "textField_mmacxew3" : "textField_mmczo639");
- String submittedField = other ? "numberField_mmeakgij"
- : (R_BIL_TABLE.equals(table) ? "numberField_mmczo636" : "numberField_mmczo63c");
- String approvedField = other ? "numberField_mmeakgii"
- : (R_BIL_TABLE.equals(table) ? "numberField_mmczo637" : "numberField_mmczo63d");
- String key = other ? activityName : projectCode;
- for (Map child : list(fd.get(table))) {
- if (key.equals(text(child.get(keyField)))) {
- McException.assertAccessException(num(child.get(submittedField)) > 1e-6
- || num(child.get(approvedField)) > 1e-6,
- "该补填明细已提交或审批完成,不能再次补填 day=" + day + ", key=" + key);
- }
- }
- }
- 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(whConf.getRepairSummarySyncStatusField())))) continue;
- String userId = text(fd.get(R_SUBMITTER));
- String monthText = text(fd.get(R_MONTH));
- String batchId = text(fd.get(whConf.getRepairSummaryBatchField()));
- long submitDate = (long) num(fd.get(whConf.getRepairSummarySubmitDateField()));
- if (StringUtils.isAnyBlank(userId, monthText, batchId) || submitDate <= 0) continue;
- String submissionKey = userId + "|" + monthText + "|" + batchId;
- RepairBatch batch = batches.computeIfAbsent(submissionKey, key -> {
- RepairBatch value = new RepairBatch();
- value.batchId = batchId;
- value.userId = userId;
- value.monthText = monthText;
- value.submitDate = submitDate;
- 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> loadRepairApprovalDetailCoverage(String monthText) {
- Set<String> covered = new HashSet<>();
- Map<String, Object> search = mapOf(
- whConf.getApprovalFillTypeField(), "补填",
- "textField_mmd2lv0y", monthText);
- for (Map item : queryProcesses(whConf.getFormUuidApproval(), search)) {
- collectRepairApprovalCoverage(item, false, covered);
- }
- for (Map item : queryProcesses(whConf.getFormUuidOtherApproval(), search)) {
- collectRepairApprovalCoverage(item, true, covered);
- }
- return covered;
- }
- private void collectRepairApprovalCoverage(Map item, boolean other, Set<String> covered) {
- String status = text(item.get("instanceStatus")).toUpperCase(Locale.ROOT);
- String approvedResult = text(item.get("approvedResult")).toLowerCase(Locale.ROOT);
- if (!"RUNNING".equals(status)
- && !("COMPLETED".equals(status) && "agree".equals(approvedResult))) {
- return;
- }
- Map fd = formData(item);
- if (other) {
- addCoverageRows(covered, list(fd.get("tableField_mmd1wu9h")), "other",
- "textField_mmd1wu9f", "textField_mmd8g655", "selectField_mmeakgie",
- "radioField_mpwhk294", status);
- return;
- }
- addCoverageRows(covered, list(fd.get("tableField_mmae8t99")), "billable",
- "textField_mmd1ozk4", "textField_mmd8g654", "textField_mmacxew3",
- "radioField_mpwhk294", status);
- addCoverageRows(covered, list(fd.get("tableField_mmd1wu9h")), "nonBillable",
- "textField_mmd1wu9f", "textField_mmd8g655", "textField_mmd1wu9d",
- "radioField_mpwhk295", status);
- }
- private void addCoverageRows(Set<String> covered, List<Map> rows, String category,
- String userField, String dayField, String keyField,
- String opinionField, String status) {
- for (Map row : rows) {
- if ("COMPLETED".equals(status)
- && row.containsKey(opinionField)
- && !"同意".equals(text(row.get(opinionField)))) {
- continue;
- }
- covered.add(coverageKey(category, text(row.get(userField)),
- text(row.get(dayField)), text(row.get(keyField))));
- }
- }
- private void removeCoveredRows(RepairBatch batch, Set<String> covered) {
- Iterator<Map.Entry<String, Object>> iterator = batch.projectGroups.entrySet().iterator();
- while (iterator.hasNext()) {
- Map group = (Map) iterator.next().getValue();
- removeCoveredProjectRows(batch.userId, list(group.get("billableRows")),
- "billable", covered);
- removeCoveredProjectRows(batch.userId, list(group.get("nonBillableRows")),
- "nonBillable", covered);
- if (list(group.get("billableRows")).isEmpty()
- && list(group.get("nonBillableRows")).isEmpty()) {
- iterator.remove();
- }
- }
- List<Map> otherRows = list(batch.otherGroup.get("otherRows"));
- otherRows.removeIf(row -> covered.contains(coverageKey("other", batch.userId,
- text(row.get("dateStr")), text(row.get("activityName")))));
- if (otherRows.isEmpty()) batch.otherGroup.clear();
- }
- private void removeCoveredProjectRows(String userId, List<Map> rows,
- String category, Set<String> covered) {
- rows.removeIf(row -> covered.contains(coverageKey(category, userId,
- text(row.get("dateStr")), text(row.get("projectCode")))));
- }
- static String coverageKey(String category, String userId, String day, String detailKey) {
- return category + "|" + userId + "|" + day + "|" + detailKey;
- }
- 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);
- }
- static void applySummaryTotals(Map<String, Object> update, Map snapshot) {
- putCategoryTotals(update, snapshot, R_BIL_TABLE,
- "numberField_mmczo635", "numberField_mmczo636", "numberField_mmczo637", "numberField_mmd80zae",
- "numberField_mmacxew9", "numberField_mmacxewa", "numberField_mmacxewg", "numberField_mmad6jbc");
- putCategoryTotals(update, snapshot, R_NON_TABLE,
- "numberField_mmczo63b", "numberField_mmczo63c", "numberField_mmczo63d", "numberField_mmeakgi9",
- "numberField_mmad6jbx", "numberField_mmad6jbz", "numberField_mmad6jc1", "numberField_mmeakgia");
- putCategoryTotals(update, snapshot, R_OTHER_TABLE,
- "numberField_mmeakgik", "numberField_mmeakgij", "numberField_mmeakgii", "numberField_mmeakgim",
- "numberField_mmd1smeo", "numberField_mmd1smeq", "numberField_mmd1smes", "numberField_mmeakgic");
- update.put(R_DAILY_DRAFT, categoryTotal(snapshot, R_BIL_TABLE, "numberField_mmczo635")
- + categoryTotal(snapshot, R_NON_TABLE, "numberField_mmczo63b")
- + categoryTotal(snapshot, R_OTHER_TABLE, "numberField_mmeakgik"));
- update.put(R_DAILY_SUBMITTED, categoryTotal(snapshot, R_BIL_TABLE, "numberField_mmczo636")
- + categoryTotal(snapshot, R_NON_TABLE, "numberField_mmczo63c")
- + categoryTotal(snapshot, R_OTHER_TABLE, "numberField_mmeakgij"));
- update.put(R_DAILY_APPROVED, categoryTotal(snapshot, R_BIL_TABLE, "numberField_mmczo637")
- + categoryTotal(snapshot, R_NON_TABLE, "numberField_mmczo63d")
- + categoryTotal(snapshot, R_OTHER_TABLE, "numberField_mmeakgii"));
- update.put(R_DAILY_CHANGE, categoryTotal(snapshot, R_BIL_TABLE, "numberField_mmd80zae")
- + categoryTotal(snapshot, R_NON_TABLE, "numberField_mmeakgi9")
- + categoryTotal(snapshot, R_OTHER_TABLE, "numberField_mmeakgim"));
- }
- private static void putCategoryTotals(Map<String, Object> update, Map snapshot, String table,
- String draftChild, String submittedChild, String approvedChild, String changeChild,
- String draftMain, String submittedMain, String approvedMain, String changeMain) {
- update.put(draftMain, categoryTotal(snapshot, table, draftChild));
- update.put(submittedMain, categoryTotal(snapshot, table, submittedChild));
- update.put(approvedMain, categoryTotal(snapshot, table, approvedChild));
- update.put(changeMain, categoryTotal(snapshot, table, changeChild));
- }
- private static double categoryTotal(Map snapshot, String table, String field) {
- double total = 0d;
- for (Map row : list(snapshot.get(table))) total += num(row.get(field));
- return total;
- }
- private List<Map> copyRows(Object raw) {
- List<Map> rows = new ArrayList<>();
- for (Map row : list(raw)) rows.add(new LinkedHashMap(row));
- return rows;
- }
- 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; }
- }
|