| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081 |
- 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.MonthClosingCheckRequest;
- import com.malk.server.workhours.MonthClosingStatusResult;
- 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.CompletableFuture;
- import java.util.concurrent.CompletionException;
- import java.util.concurrent.ConcurrentHashMap;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.atomic.AtomicInteger;
- /** 补填 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 REPAIR_TYPE = "补填";
- 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 long CONTEXT_CACHE_TTL_MILLIS = 60_000L;
- private static final ConcurrentHashMap<String, SubmitGuard> SUBMIT_GUARDS = new ConcurrentHashMap<>();
- private static final AtomicInteger CONTEXT_THREAD_SEQUENCE = new AtomicInteger();
- private static final ExecutorService CONTEXT_QUERY_EXECUTOR = Executors.newFixedThreadPool(8, runnable -> {
- Thread thread = new Thread(runnable,
- "timecard-repair-context-" + CONTEXT_THREAD_SEQUENCE.incrementAndGet());
- thread.setDaemon(true);
- return thread;
- });
- private final ConcurrentHashMap<String, CompletableFuture<Map<String, Object>>> contextRequests =
- new ConcurrentHashMap<>();
- private final ConcurrentHashMap<String, ContextCacheEntry> contextCache = 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;
- @Autowired private TimeCardPersonnelPolicy personnelPolicy;
- public Map<String, Object> context(TimeCardRepairContextRequest request) {
- validateMonthRequest(request == null ? null : request.getSubmitterUserId(),
- request == null ? null : request.getSourceMonthText());
- String userId = request.getSubmitterUserId();
- String monthText = request.getSourceMonthText();
- String staffType = text(request.getStaffType());
- McException.assertAccessException(!staffType.isEmpty()
- && !"内部".equals(staffType) && !"外部".equals(staffType), "人员属性无效");
- String requestKey = userId + "|" + monthText + "|" + staffType;
- Map<String, Object> cachedResult = getCachedContext(requestKey);
- if (cachedResult != null) {
- CompletableFuture<Map<String, Object>> completedRequest = contextRequests.get(requestKey);
- if (completedRequest != null && completedRequest.isDone()) {
- contextRequests.remove(requestKey, completedRequest);
- }
- return cachedResult;
- }
- CompletableFuture<Map<String, Object>> contextFuture = contextRequests.computeIfAbsent(
- requestKey, ignored -> CompletableFuture.supplyAsync(
- () -> personnelPolicy.getStaffType(userId), CONTEXT_QUERY_EXECUTOR)
- .thenCompose(actualType -> {
- McException.assertAccessException(!staffType.isEmpty() && !staffType.equals(actualType),
- "人员属性已变化,请刷新后重试");
- return loadContextAsync(requestKey, userId, monthText, actualType);
- }));
- if (!contextFuture.isDone()) return pendingContext();
- contextRequests.remove(requestKey, contextFuture);
- return completedContext(contextFuture, userId, monthText);
- }
- private CompletableFuture<Map<String, Object>> loadContextAsync(String requestKey,
- String userId,
- String monthText,
- String staffType) {
- // prd 外部补填独立于已冲销但保留的原汇总;不发起原表查询。
- CompletableFuture<List<Map>> originalFuture = "外部".equals(staffType)
- ? CompletableFuture.completedFuture(Collections.emptyList())
- : CompletableFuture.supplyAsync(
- () -> query(whConf.getFormUuidWorkHoursSummary(),
- mapOf(R_SUBMITTER, userId, R_MONTH, monthText)), CONTEXT_QUERY_EXECUTOR);
- CompletableFuture<List<Map>> repairFuture = CompletableFuture.supplyAsync(
- () -> queryRepairSummaries(mapOf(R_SUBMITTER, userId, R_MONTH, monthText)),
- CONTEXT_QUERY_EXECUTOR);
- return originalFuture.thenCombine(repairFuture, (originalRecords, repairRecords) -> {
- Map<String, Object> result = new LinkedHashMap<>();
- result.put("ready", true);
- result.put("staffType", staffType);
- result.put("summaryReadMode", "外部".equals(staffType) ? "repair-only" : "original-and-repair");
- result.put("originalRecords", compactContextRecords(originalRecords));
- result.put("repairRecords", compactContextRecords(repairRecords));
- // 兼容页面状态合并接口的 records 约定;原始双表结果仍通过 repairRecords 返回。
- result.put("records", compactContextRecords(repairRecords));
- contextCache.put(requestKey,
- new ContextCacheEntry(result, System.currentTimeMillis() + CONTEXT_CACHE_TTL_MILLIS));
- return result;
- });
- }
- private Map<String, Object> completedContext(CompletableFuture<Map<String, Object>> contextFuture,
- String userId,
- String monthText) {
- try {
- return contextFuture.join();
- } catch (CompletionException error) {
- Throwable cause = error.getCause();
- if (cause instanceof RuntimeException) throw (RuntimeException) cause;
- log.error("补填上下文查询失败: userId={}, month={}", userId, monthText, cause);
- throw new McException("REPAIR_CONTEXT_QUERY_FAILED", "补填工时上下文查询失败");
- }
- }
- private static Map<String, Object> pendingContext() {
- Map<String, Object> result = new LinkedHashMap<>();
- result.put("ready", false);
- result.put("retryAfterMillis", 2_000L);
- return result;
- }
- private Map<String, Object> getCachedContext(String requestKey) {
- long now = System.currentTimeMillis();
- ContextCacheEntry cached = contextCache.get(requestKey);
- if (cached == null) return null;
- if (cached.expiresAt > now) return cached.result;
- contextCache.remove(requestKey, cached);
- return null;
- }
- private void invalidateContext(String userId, String monthText) {
- String prefix = userId + "|" + monthText + "|";
- contextCache.keySet().removeIf(key -> key.startsWith(prefix));
- }
- private static List<Map<String, Object>> compactContextRecords(List<Map> records) {
- List<Map<String, Object>> compact = new ArrayList<>();
- for (Map record : records) {
- Map<String, Object> item = new LinkedHashMap<>();
- item.put("formInstId", instanceId(record));
- item.put("formData", formData(record));
- compact.add(item);
- }
- return compact;
- }
- 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.isLocked(monthText), "补填只能保存已发起封账或已完成封账的月份");
- assertCurrentMonthOpen("保存");
- McException.assertAccessException(StringUtils.isBlank(whConf.getFormUuidWorkHoursSummary()), "原工时汇总表未配置");
- McException.assertAccessException(StringUtils.isBlank(whConf.getFormUuidRepairWorkHoursSummary()), "补填汇总表未配置");
- McException.assertAccessException(StringUtils.isBlank(whConf.getRepairSummaryFillTypeField()), "补填汇总类型字段未配置");
- McException.assertAccessException(StringUtils.isBlank(whConf.getRepairSummarySyncStatusField()), "补填汇总状态字段未配置");
- McException.assertAccessException(request.getFormData() == null, "补填工时数据不能为空");
- assertSourceDateAvailable(request.getSubmitterUserId(), request.getDayText(),
- "外部".equals(personnelPolicy.getStaffType(request.getSubmitterUserId())));
- List<Map> existing = queryRepairSummaries(
- mapOf(R_SUBMITTER, request.getSubmitterUserId(), R_DAY, request.getDayText()));
- McException.assertAccessException(existing.size() > 1,
- "补填汇总记录重复,已停止保存 userId=" + request.getSubmitterUserId() + ", day=" + request.getDayText());
- McException.assertAccessException(existing.stream().anyMatch(record ->
- TimeCardSummaryService.isRepairRecordLocked(record, whConf.getRepairSummarySyncStatusField())),
- "该日期补填工时已提交或已审批,不能再填写 userId=" + request.getSubmitterUserId()
- + ", day=" + request.getDayText());
- Map<String, Object> data = new LinkedHashMap<>(request.getFormData());
- // fixme 草稿仅接收明细暂存工时;汇总由服务端重算,不能信任页面传入的审批合计。
- for (String table : Arrays.asList(R_BIL_TABLE, R_NON_TABLE, R_OTHER_TABLE)) {
- for (Map child : list(data.get(table))) {
- McException.assertAccessException(
- TimeCardSummaryService.hasSubmittedOrApprovedHours(mapOf(table,
- Collections.singletonList(child))),
- "补填草稿不能包含已提交、已审批或变更工时");
- }
- }
- TimeCardRepairService.applySummaryTotals(data, data);
- data.put(R_SUBMITTER, request.getSubmitterUserId());
- data.put(R_DAY, request.getDayText());
- data.put(R_MONTH, request.getDayText().substring(0, 6));
- data.put(whConf.getRepairSummaryFillTypeField(), REPAIR_TYPE);
- data.put(whConf.getRepairSummarySyncStatusField(), "暂存");
- // prd 补填与变更草稿均记录保存当天日期;实际提交时更新日期并生成批次。
- McException.assertAccessException(StringUtils.isBlank(whConf.getRepairSummarySubmitDateField()),
- "补填/变更提交日期字段未配置");
- data.put(whConf.getRepairSummarySubmitDateField(),
- LocalDate.now(ZONE).atStartOfDay(ZONE).toInstant().toEpochMilli());
- if (StringUtils.isNotBlank(whConf.getRepairSummarySourceMonthField())) {
- data.put(whConf.getRepairSummarySourceMonthField(), monthText);
- }
- saveRepairDraft(existing, data, request.getSubmitterUserId(), request.getDayText());
- invalidateContext(request.getSubmitterUserId(), monthText);
- verifyRepairDraftDate(request.getSubmitterUserId(), request.getDayText(), data);
- return true;
- }
- /** 保存和提交均禁止当前月进入封账后的补填写入。 */
- private void assertCurrentMonthOpen(String action) {
- String monthText = LocalDate.now(ZONE).format(MONTH);
- MonthClosingStatusResult status = monthClosingService.status(
- MonthClosingCheckRequest.builder().monthText(monthText).build());
- McException.assertAccessException(status == null || status.isClosed()
- || status.getRunningClosingCount() > 0,
- "当前月份已封账或封账流程审批中,不可" + action + "补填");
- }
- /** 以实际实例回读确认草稿的提交日期已落库。 */
- private void verifyRepairDraftDate(String userId, String dayText, Map<String, Object> expected) {
- List<Map> found = queryRepairSummaries(mapOf(R_SUBMITTER, userId, R_DAY, dayText));
- McException.assertAccessException(found.size() != 1, "补填草稿未落库或记录重复,请勿提交");
- Map actual = detail(instanceId(found.get(0)));
- String field = whConf.getRepairSummarySubmitDateField();
- McException.assertAccessException(num(expected.get(field)) != num(actual.get(field)),
- "补填/变更提交日期回读不一致,请勿提交");
- }
- public Map<String, Object> submit(TimeCardRepairSubmitRequest request) {
- McException.assertAccessException(request == null, "请求不能为空");
- validateMonthRequest(request.getSubmitterUserId(), request.getSourceMonthText());
- McException.assertAccessException(!monthClosingService.isLocked(request.getSourceMonthText()), "补填只能提交已发起封账或已完成封账的月份");
- LocalDate submitDay = LocalDate.now(ZONE);
- // prd 补填写入当前月变更工时;当前月进入封账流程或已封账后禁止新增提交。
- assertCurrentMonthOpen("提交");
- 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 = submitDay.atStartOfDay(ZONE).toInstant().toEpochMilli();
- int rowCount = validateSubmitRows(request, projectGroups, other);
- McException.assertAccessException(rowCount == 0, "没有需要提交的补填工时");
- invalidateContext(request.getSubmitterUserId(), request.getSourceMonthText());
- 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);
- }
- }
- private static final class ContextCacheEntry {
- private final Map<String, Object> result;
- private final long expiresAt;
- private ContextCacheEntry(Map<String, Object> result, long expiresAt) {
- this.result = result;
- this.expiresAt = expiresAt;
- }
- }
- /**
- * 补填审批补发起:只扫描近期修改的补填汇总,按员工、月份和补填提交日期补齐缺失明细。
- * 正常 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 = queryRepairSummaries(
- 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) {
- return new WorkHoursFormDataReader(ydClient, whConf).form(id, whConf.getFormUuidRepairWorkHoursSummary());
- }
- 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<>();
- com.malk.server.dingtalk.DDR_New result = 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);
- McException.assertAccessException(result == null || !(result.getData() instanceof List),
- "查询现有工时失败,已停止保存");
- List<Map> records = (List<Map>) result.getData();
- McException.assertAccessException(records.size() >= 100, "现有工时记录过多,已停止保存");
- return records;
- }
- 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,
- boolean external) {
- 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, external);
- assertRepairRowMutable(request.getSubmitterUserId(), day, row, category);
- }
- private int validateSubmitRows(TimeCardRepairSubmitRequest request,
- Map<String, Object> projectGroups,
- Map<String, Object> otherGroup) {
- int count = 0;
- String staffType = null;
- 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;
- if (staffType == null) staffType = personnelPolicy.getStaffType(request.getSubmitterUserId());
- validateRepairRow(request, (Map) rawRow, category, "外部".equals(staffType));
- count++;
- }
- }
- }
- for (Object rawRow : list(otherGroup.get("otherRows"))) {
- if (!(rawRow instanceof Map)) continue;
- if (staffType == null) staffType = personnelPolicy.getStaffType(request.getSubmitterUserId());
- validateRepairRow(request, (Map) rawRow, "otherRows", "外部".equals(staffType));
- count++;
- }
- return count;
- }
- private void assertSourceDateAvailable(String userId, String day, boolean external) {
- // prd 外部来源日由补填记录保护;内部继续核实原表锁定。
- if (external) return;
- List<Map> original = query(whConf.getFormUuidWorkHoursSummary(), mapOf(R_SUBMITTER, userId, R_DAY, day));
- boolean hasApprovedOrChangedHours = original.stream()
- .anyMatch(TimeCardSummaryService::isFormalRecordLocked);
- McException.assertAccessException(hasApprovedOrChangedHours,
- "来源日期已有已提交、已审批或变更工时,不能补填 userId=" + userId + ", day=" + day);
- }
- /** 原汇总当天任一已审批或变更工时均禁止补填;暂存草稿不占用补填资格。 */
- static boolean hasApprovedOrChangedHours(Map record) {
- return TimeCardSummaryService.hasApprovedOrChangedHours(record);
- }
- private void assertRepairRowMutable(String userId, String day, Map row, String category) {
- List<Map> existing = queryRepairSummaries(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 = new WorkHoursFormDataReader(ydClient, whConf).complete(
- instanceId(item),
- whConf.getFormUuidRepairWorkHoursSummary(), formData(item));
- if (!isRepairSummaryRecord(fd, whConf.getRepairSummaryFillTypeField())) continue;
- 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;
- }
- String id = text(item.get("processInstanceId"));
- if (StringUtils.isBlank(id)) id = instanceId(item);
- Map fd = new WorkHoursFormDataReader(ydClient, whConf).complete(id,
- other ? whConf.getFormUuidOtherApproval() : whConf.getFormUuidApproval(), 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> queryRepairSummaries(Map<String, Object> search) {
- List<Map> records = new ArrayList<>(query(whConf.getFormUuidRepairWorkHoursSummary(), search));
- records.removeIf(record -> !isRepairSummaryRecord(record, whConf.getRepairSummaryFillTypeField()));
- return records;
- }
- private void saveRepairDraft(List<Map> existing, Map<String, Object> data,
- String userId, String dayText) {
- YDParam.YDParamBuilder builder = YDParam.builder()
- .appType(whConf.getYidaAppType())
- .systemToken(whConf.getYidaSystemToken());
- if (existing.isEmpty()) {
- Map<String, Object> search = mapOf(R_SUBMITTER, userId, R_DAY, dayText,
- whConf.getRepairSummaryFillTypeField(), REPAIR_TYPE);
- ydClient.operateData(builder.formUuid(whConf.getFormUuidRepairWorkHoursSummary())
- .searchCondition(JSON.toJSONString(search)).formDataJson(JSON.toJSONString(data))
- .noExecuteExpression(false).build(), YDConf.FORM_OPERATION.upsert);
- return;
- }
- String instanceId = instanceId(existing.get(0));
- McException.assertAccessException(StringUtils.isBlank(instanceId), "补填汇总实例ID为空");
- ydClient.operateData(builder.formInstanceId(instanceId)
- .updateFormDataJson(JSON.toJSONString(data)).ignoreEmpty(false).useLatestVersion(true).build(),
- YDConf.FORM_OPERATION.update);
- }
- 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)};
- }
- static boolean isRepairSummaryRecord(Map item, String fillTypeField) {
- if (StringUtils.isBlank(fillTypeField)) return true;
- Map data = formData(item);
- String fillType = text(data.get(fillTypeField));
- return StringUtils.isBlank(fillType) || REPAIR_TYPE.equals(fillType);
- }
- private static 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 static String instanceId(Map item) {
- String id = text(item == null ? null : item.get("formInstId"));
- return StringUtils.isBlank(id) ? text(item == null ? null : item.get("formInstanceId")) : id;
- }
- 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; }
- }
|