|
@@ -0,0 +1,515 @@
|
|
|
|
|
+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.dingtalk.DDR_New;
|
|
|
|
|
+import com.malk.server.workhours.MonthClosingCheckRequest;
|
|
|
|
|
+import com.malk.server.workhours.MonthClosingConf;
|
|
|
|
|
+import com.malk.server.workhours.MonthClosingStatusResult;
|
|
|
|
|
+import com.malk.server.workhours.MonthClosingValidationResult;
|
|
|
|
|
+import com.malk.server.workhours.WHConf;
|
|
|
|
|
+import com.malk.service.aliwork.YDClient;
|
|
|
|
|
+import org.apache.commons.lang3.StringUtils;
|
|
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+
|
|
|
|
|
+import java.time.YearMonth;
|
|
|
|
|
+import java.time.Instant;
|
|
|
|
|
+import java.time.ZoneId;
|
|
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
|
|
+import java.time.format.DateTimeParseException;
|
|
|
|
|
+import java.time.format.ResolverStyle;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.HashMap;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.Locale;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 月度封账统一校验服务。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>TimeCard 与封账流程提交前均调用本服务,避免页面各自维护不同口径。</p>
|
|
|
|
|
+ */
|
|
|
|
|
+@Service
|
|
|
|
|
+public class MonthClosingService {
|
|
|
|
|
+
|
|
|
|
|
+ private static final int PAGE_SIZE = 100;
|
|
|
|
|
+ private static final int MAX_PAGES = 1000;
|
|
|
|
|
+ private static final String APPROVAL_MONTH_FIELD = "textField_mmd2lv0y";
|
|
|
|
|
+ private static final String SUMMARY_MONTH_FIELD = "textField_mmbffvda";
|
|
|
|
|
+ private static final String SUMMARY_STATUS_IN_APPROVAL = "审批中";
|
|
|
|
|
+ private static final String WRITE_BACK_SUCCESS = "全部成功";
|
|
|
|
|
+ private static final String STATUS_CLOSED = "封账";
|
|
|
|
|
+ private static final String STATUS_REOPENED = "启封";
|
|
|
|
|
+ private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai");
|
|
|
|
|
+ private static final DateTimeFormatter MONTH_FORMATTER =
|
|
|
|
|
+ DateTimeFormatter.ofPattern("uuuuMM").withResolverStyle(ResolverStyle.STRICT);
|
|
|
|
|
+
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private YDClient ydClient;
|
|
|
|
|
+
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private WHConf whConf;
|
|
|
|
|
+
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private MonthClosingConf closingConf;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 查询目标月份是否已封账。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param request 月份请求
|
|
|
|
|
+ * @return 已审批封账与进行中封账数量
|
|
|
|
|
+ */
|
|
|
|
|
+ public MonthClosingStatusResult status(MonthClosingCheckRequest request) {
|
|
|
|
|
+ String monthText = validateRequest(request);
|
|
|
|
|
+ return queryClosingStatus(monthText);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 校验目标月份是否允许发起封账。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param request 月份请求
|
|
|
|
|
+ * @return 封账许可、阻塞数量及提示
|
|
|
|
|
+ */
|
|
|
|
|
+ public MonthClosingValidationResult validate(MonthClosingCheckRequest request) {
|
|
|
|
|
+ String monthText = validateRequest(request);
|
|
|
|
|
+ String requestedStatus = validateClosingStatus(request.getClosingStatus());
|
|
|
|
|
+ MonthClosingStatusResult closingStatus = queryClosingStatus(monthText);
|
|
|
|
|
+
|
|
|
|
|
+ if (STATUS_REOPENED.equals(requestedStatus)) {
|
|
|
|
|
+ return validateReopening(monthText, closingStatus);
|
|
|
|
|
+ }
|
|
|
|
|
+ return validateClosing(monthText, closingStatus);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private MonthClosingValidationResult validateClosing(
|
|
|
|
|
+ String monthText,
|
|
|
|
|
+ MonthClosingStatusResult closingStatus) {
|
|
|
|
|
+ if (closingStatus.isClosed() || closingStatus.getRunningClosingCount() > 0) {
|
|
|
|
|
+ return buildValidationResult(monthText, STATUS_CLOSED, closingStatus, 0, 0, 0, 0);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<Map> projectRunning = queryProcesses(
|
|
|
|
|
+ whConf.getFormUuidApproval(), APPROVAL_MONTH_FIELD, monthText, "RUNNING");
|
|
|
|
|
+ List<Map> otherRunning = queryProcesses(
|
|
|
|
|
+ whConf.getFormUuidOtherApproval(), APPROVAL_MONTH_FIELD, monthText, "RUNNING");
|
|
|
|
|
+ List<Map> projectCompleted = queryProcesses(
|
|
|
|
|
+ whConf.getFormUuidApproval(), APPROVAL_MONTH_FIELD, monthText, "COMPLETED");
|
|
|
|
|
+ List<Map> otherCompleted = queryProcesses(
|
|
|
|
|
+ whConf.getFormUuidOtherApproval(), APPROVAL_MONTH_FIELD, monthText, "COMPLETED");
|
|
|
|
|
+
|
|
|
|
|
+ int writeBackPendingCount = countWriteBackPending(
|
|
|
|
|
+ projectCompleted, whConf.getApprovalSyncStatusField())
|
|
|
|
|
+ + countWriteBackPending(
|
|
|
|
|
+ otherCompleted, whConf.getOtherApprovalSyncStatusField());
|
|
|
|
|
+ int summaryPendingCount = querySummaryPendingCount(monthText);
|
|
|
|
|
+ return buildValidationResult(
|
|
|
|
|
+ monthText,
|
|
|
|
|
+ STATUS_CLOSED,
|
|
|
|
|
+ closingStatus,
|
|
|
|
|
+ projectRunning.size(),
|
|
|
|
|
+ otherRunning.size(),
|
|
|
|
|
+ writeBackPendingCount,
|
|
|
|
|
+ summaryPendingCount);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private MonthClosingValidationResult validateReopening(
|
|
|
|
|
+ String monthText,
|
|
|
|
|
+ MonthClosingStatusResult closingStatus) {
|
|
|
|
|
+ return buildValidationResult(monthText, STATUS_REOPENED, closingStatus, 0, 0, 0, 0);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static MonthClosingValidationResult buildValidationResult(
|
|
|
|
|
+ String monthText,
|
|
|
|
|
+ String requestedStatus,
|
|
|
|
|
+ MonthClosingStatusResult closingStatus,
|
|
|
|
|
+ int projectRunningCount,
|
|
|
|
|
+ int otherRunningCount,
|
|
|
|
|
+ int writeBackPendingCount,
|
|
|
|
|
+ int summaryPendingCount) {
|
|
|
|
|
+ boolean requestedClosing = STATUS_CLOSED.equals(requestedStatus);
|
|
|
|
|
+ boolean stateAllowed = requestedClosing ? !closingStatus.isClosed() : closingStatus.isClosed();
|
|
|
|
|
+ boolean allowed = stateAllowed
|
|
|
|
|
+ && closingStatus.getRunningClosingCount() == 0
|
|
|
|
|
+ && projectRunningCount == 0
|
|
|
|
|
+ && otherRunningCount == 0
|
|
|
|
|
+ && writeBackPendingCount == 0
|
|
|
|
|
+ && summaryPendingCount == 0;
|
|
|
|
|
+
|
|
|
|
|
+ return MonthClosingValidationResult.builder()
|
|
|
|
|
+ .monthText(monthText)
|
|
|
|
|
+ .allowed(allowed)
|
|
|
|
|
+ .requestedStatus(requestedStatus)
|
|
|
|
|
+ .currentStatus(closingStatus.getCurrentStatus())
|
|
|
|
|
+ .alreadyClosed(closingStatus.isClosed())
|
|
|
|
|
+ .closingRunningCount(closingStatus.getRunningClosingCount())
|
|
|
|
|
+ .projectRunningCount(projectRunningCount)
|
|
|
|
|
+ .otherRunningCount(otherRunningCount)
|
|
|
|
|
+ .writeBackPendingCount(writeBackPendingCount)
|
|
|
|
|
+ .summaryPendingCount(summaryPendingCount)
|
|
|
|
|
+ .message(buildMessage(
|
|
|
|
|
+ monthText,
|
|
|
|
|
+ requestedStatus,
|
|
|
|
|
+ closingStatus,
|
|
|
|
|
+ projectRunningCount,
|
|
|
|
|
+ otherRunningCount,
|
|
|
|
|
+ writeBackPendingCount,
|
|
|
|
|
+ summaryPendingCount))
|
|
|
|
|
+ .build();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private MonthClosingStatusResult queryClosingStatus(String monthText) {
|
|
|
|
|
+ List<Map> completed = filterClosingMonth(
|
|
|
|
|
+ queryProcesses(closingConf.getFormUuid(), null, null, "COMPLETED"), monthText);
|
|
|
|
|
+ List<Map> running = filterClosingMonth(
|
|
|
|
|
+ queryProcesses(closingConf.getFormUuid(), null, null, "RUNNING"), monthText);
|
|
|
|
|
+ int approvedClosingCount = 0;
|
|
|
|
|
+ int approvedReopeningCount = 0;
|
|
|
|
|
+ String currentStatus = "";
|
|
|
|
|
+ Long latestTime = null;
|
|
|
|
|
+ for (Map item : completed) {
|
|
|
|
|
+ if (!isApproved(item)) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ String itemStatus = text(extractFormData(item).get(closingConf.getStatusField()));
|
|
|
|
|
+ if (STATUS_CLOSED.equals(itemStatus)) {
|
|
|
|
|
+ approvedClosingCount++;
|
|
|
|
|
+ } else if (STATUS_REOPENED.equals(itemStatus)) {
|
|
|
|
|
+ approvedReopeningCount++;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ Long itemTime = processEventTime(item);
|
|
|
|
|
+ if (StringUtils.isBlank(currentStatus)
|
|
|
|
|
+ || (latestTime == null && itemTime != null)
|
|
|
|
|
+ || (latestTime != null && itemTime != null && itemTime > latestTime)) {
|
|
|
|
|
+ currentStatus = itemStatus;
|
|
|
|
|
+ latestTime = itemTime;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return MonthClosingStatusResult.builder()
|
|
|
|
|
+ .monthText(monthText)
|
|
|
|
|
+ .closed(STATUS_CLOSED.equals(currentStatus))
|
|
|
|
|
+ .currentStatus(currentStatus)
|
|
|
|
|
+ .approvedClosingCount(approvedClosingCount)
|
|
|
|
|
+ .approvedReopeningCount(approvedReopeningCount)
|
|
|
|
|
+ .runningClosingCount(running.size())
|
|
|
|
|
+ .build();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<Map> filterClosingMonth(List<Map> processes, String monthText) {
|
|
|
|
|
+ List<Map> result = new ArrayList<>();
|
|
|
|
|
+ for (Map item : processes) {
|
|
|
|
|
+ Object monthValue = extractFormData(item).get(closingConf.getMonthDateField());
|
|
|
|
|
+ if (monthText.equals(monthFromDateValue(monthValue))) {
|
|
|
|
|
+ result.add(item);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private int countWriteBackPending(List<Map> completed, String syncStatusField) {
|
|
|
|
|
+ int pending = 0;
|
|
|
|
|
+ for (Map item : completed) {
|
|
|
|
|
+ if (!isApproved(item)) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ Map formData = extractFormData(item);
|
|
|
|
|
+ String syncStatus = text(formData.get(syncStatusField));
|
|
|
|
|
+ if (!WRITE_BACK_SUCCESS.equals(syncStatus)) {
|
|
|
|
|
+ pending++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return pending;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private int querySummaryPendingCount(String monthText) {
|
|
|
|
|
+ Map<String, Object> search = new HashMap<>();
|
|
|
|
|
+ search.put(SUMMARY_MONTH_FIELD, monthText);
|
|
|
|
|
+ search.put(whConf.getSummaryApprovalStatusField(), SUMMARY_STATUS_IN_APPROVAL);
|
|
|
|
|
+ List<Map> rows = queryForms(whConf.getFormUuidWorkHoursSummary(), search);
|
|
|
|
|
+ int count = 0;
|
|
|
|
|
+ for (Map item : rows) {
|
|
|
|
|
+ Map formData = extractFormData(item);
|
|
|
|
|
+ if (monthText.equals(text(formData.get(SUMMARY_MONTH_FIELD)))
|
|
|
|
|
+ && SUMMARY_STATUS_IN_APPROVAL.equals(
|
|
|
|
|
+ text(formData.get(whConf.getSummaryApprovalStatusField())))) {
|
|
|
|
|
+ count++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return count;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<Map> queryProcesses(
|
|
|
|
|
+ String formUuid,
|
|
|
|
|
+ String monthField,
|
|
|
|
|
+ String monthText,
|
|
|
|
|
+ String instanceStatus) {
|
|
|
|
|
+ McException.assertAccessException(
|
|
|
|
|
+ StringUtils.isBlank(formUuid),
|
|
|
|
|
+ "月度封账流程查询配置不完整");
|
|
|
|
|
+ Map<String, Object> search = new HashMap<>();
|
|
|
|
|
+ if (StringUtils.isNotBlank(monthField) && StringUtils.isNotBlank(monthText)) {
|
|
|
|
|
+ search.put(monthField, monthText);
|
|
|
|
|
+ }
|
|
|
|
|
+ List<Map> result = new ArrayList<>();
|
|
|
|
|
+ int page = 1;
|
|
|
|
|
+ while (page <= MAX_PAGES) {
|
|
|
|
|
+ DDR_New response = ydClient.queryData(YDParam.builder()
|
|
|
|
|
+ .appType(whConf.getYidaAppType())
|
|
|
|
|
+ .systemToken(whConf.getYidaSystemToken())
|
|
|
|
|
+ .formUuid(formUuid)
|
|
|
|
|
+ .searchFieldJson(search.isEmpty() ? null : JSON.toJSONString(search))
|
|
|
|
|
+ .instanceStatus(instanceStatus)
|
|
|
|
|
+ .pageNumber(page)
|
|
|
|
|
+ .pageSize(PAGE_SIZE)
|
|
|
|
|
+ .build(), YDConf.FORM_QUERY.retrieve_search_process);
|
|
|
|
|
+ List<Map> data = responseData(response);
|
|
|
|
|
+ result.addAll(data);
|
|
|
|
|
+ if (!hasNextPage(page, response.getTotalCount(), data.size(), result.size())) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ page++;
|
|
|
|
|
+ }
|
|
|
|
|
+ McException.assertAccessException(page > MAX_PAGES, "月度封账流程查询超过分页上限");
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<Map> queryForms(String formUuid, Map<String, Object> search) {
|
|
|
|
|
+ McException.assertAccessException(StringUtils.isBlank(formUuid), "工时汇总表配置为空");
|
|
|
|
|
+ List<Map> result = new ArrayList<>();
|
|
|
|
|
+ int page = 1;
|
|
|
|
|
+ while (page <= MAX_PAGES) {
|
|
|
|
|
+ DDR_New response = ydClient.queryData(YDParam.builder()
|
|
|
|
|
+ .appType(whConf.getYidaAppType())
|
|
|
|
|
+ .systemToken(whConf.getYidaSystemToken())
|
|
|
|
|
+ .formUuid(formUuid)
|
|
|
|
|
+ .searchFieldJson(JSON.toJSONString(search))
|
|
|
|
|
+ .currentPage(page)
|
|
|
|
|
+ .pageSize(PAGE_SIZE)
|
|
|
|
|
+ .build(), YDConf.FORM_QUERY.retrieve_search_form);
|
|
|
|
|
+ List<Map> data = responseData(response);
|
|
|
|
|
+ result.addAll(data);
|
|
|
|
|
+ if (!hasNextPage(page, response.getTotalCount(), data.size(), result.size())) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ page++;
|
|
|
|
|
+ }
|
|
|
|
|
+ McException.assertAccessException(page > MAX_PAGES, "工时汇总查询超过分页上限");
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static boolean hasNextPage(
|
|
|
|
|
+ int page,
|
|
|
|
|
+ long totalCount,
|
|
|
|
|
+ int returnedSize,
|
|
|
|
|
+ int collectedSize) {
|
|
|
|
|
+ if (returnedSize < PAGE_SIZE) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ return totalCount <= 0 || collectedSize < totalCount || (long) page * PAGE_SIZE < totalCount;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
|
|
+ private static List<Map> responseData(DDR_New response) {
|
|
|
|
|
+ McException.assertAccessException(response == null, "宜搭查询未返回结果");
|
|
|
|
|
+ Object data = response.getData();
|
|
|
|
|
+ if (data == null) {
|
|
|
|
|
+ return new ArrayList<>();
|
|
|
|
|
+ }
|
|
|
|
|
+ McException.assertAccessException(!(data instanceof List), "宜搭查询返回数据格式异常");
|
|
|
|
|
+ return (List<Map>) data;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
|
|
+ static Map extractFormData(Map item) {
|
|
|
|
|
+ if (item == null) {
|
|
|
|
|
+ return new HashMap();
|
|
|
|
|
+ }
|
|
|
|
|
+ Object formData = item.get("formData");
|
|
|
|
|
+ if (formData instanceof Map) {
|
|
|
|
|
+ return (Map) formData;
|
|
|
|
|
+ }
|
|
|
|
|
+ Object data = item.get("data");
|
|
|
|
|
+ if (!(data instanceof Map)) {
|
|
|
|
|
+ return new HashMap();
|
|
|
|
|
+ }
|
|
|
|
|
+ Map dataMap = (Map) data;
|
|
|
|
|
+ Object nested = dataMap.get("formData");
|
|
|
|
|
+ return nested instanceof Map ? (Map) nested : dataMap;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ static String validateMonthText(String monthText) {
|
|
|
|
|
+ String value = StringUtils.trimToEmpty(monthText);
|
|
|
|
|
+ McException.assertParamException(!value.matches("\\d{6}"), "月份必须为yyyyMM格式");
|
|
|
|
|
+ try {
|
|
|
|
|
+ YearMonth.parse(value, MONTH_FORMATTER);
|
|
|
|
|
+ } catch (DateTimeParseException ex) {
|
|
|
|
|
+ throw McException.builder()
|
|
|
|
|
+ .code("validated_param")
|
|
|
|
|
+ .message("月份必须为有效的yyyyMM格式")
|
|
|
|
|
+ .build();
|
|
|
|
|
+ }
|
|
|
|
|
+ return value;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ static String monthFromDateValue(Object value) {
|
|
|
|
|
+ if (value == null) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (value instanceof Number) {
|
|
|
|
|
+ return monthFromEpoch(((Number) value).longValue());
|
|
|
|
|
+ }
|
|
|
|
|
+ String raw = text(value);
|
|
|
|
|
+ if (raw.matches("\\d{6}")) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return validateMonthText(raw);
|
|
|
|
|
+ } catch (McException ignored) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (raw.matches("\\d{8}")) {
|
|
|
|
|
+ return normalizeYearMonth(raw.substring(0, 4), raw.substring(4, 6));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (raw.matches("\\d{10}|\\d{13}")) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return monthFromEpoch(Long.parseLong(raw));
|
|
|
|
|
+ } catch (NumberFormatException ignored) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ return YearMonth.from(Instant.parse(raw).atZone(BUSINESS_ZONE)).format(MONTH_FORMATTER);
|
|
|
|
|
+ } catch (DateTimeParseException ignored) {
|
|
|
|
|
+ // 普通日期字符串继续按 yyyy-MM 前缀处理。
|
|
|
|
|
+ }
|
|
|
|
|
+ if (raw.matches("^\\d{4}[-/]\\d{2}.*")) {
|
|
|
|
|
+ return normalizeYearMonth(raw.substring(0, 4), raw.substring(5, 7));
|
|
|
|
|
+ }
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String monthFromEpoch(long rawTimestamp) {
|
|
|
|
|
+ String digits = String.valueOf(Math.abs(rawTimestamp));
|
|
|
|
|
+ if (digits.length() == 8) {
|
|
|
|
|
+ return normalizeYearMonth(digits.substring(0, 4), digits.substring(4, 6));
|
|
|
|
|
+ }
|
|
|
|
|
+ long milliseconds = digits.length() <= 10 ? rawTimestamp * 1000L : rawTimestamp;
|
|
|
|
|
+ try {
|
|
|
|
|
+ return YearMonth.from(Instant.ofEpochMilli(milliseconds).atZone(BUSINESS_ZONE))
|
|
|
|
|
+ .format(MONTH_FORMATTER);
|
|
|
|
|
+ } catch (RuntimeException ignored) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String normalizeYearMonth(String year, String month) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return YearMonth.of(Integer.parseInt(year), Integer.parseInt(month))
|
|
|
|
|
+ .format(MONTH_FORMATTER);
|
|
|
|
|
+ } catch (RuntimeException ignored) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static Long processEventTime(Map item) {
|
|
|
|
|
+ if (item == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ String[] fields = {
|
|
|
|
|
+ "modifiedTimeGMT", "gmtModified", "finishTimeGMT", "finishTime",
|
|
|
|
|
+ "modifiedTime", "createTimeGMT", "createTime"
|
|
|
|
|
+ };
|
|
|
|
|
+ for (String field : fields) {
|
|
|
|
|
+ Long parsed = parseTimestamp(item.get(field));
|
|
|
|
|
+ if (parsed != null) {
|
|
|
|
|
+ return parsed;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static Long parseTimestamp(Object value) {
|
|
|
|
|
+ if (value instanceof Number) {
|
|
|
|
|
+ long number = ((Number) value).longValue();
|
|
|
|
|
+ return String.valueOf(Math.abs(number)).length() <= 10 ? number * 1000L : number;
|
|
|
|
|
+ }
|
|
|
|
|
+ String raw = text(value);
|
|
|
|
|
+ if (raw.matches("\\d{10}|\\d{13}")) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ long number = Long.parseLong(raw);
|
|
|
|
|
+ return raw.length() == 10 ? number * 1000L : number;
|
|
|
|
|
+ } catch (NumberFormatException ignored) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ return Instant.parse(raw).toEpochMilli();
|
|
|
|
|
+ } catch (DateTimeParseException ignored) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String validateClosingStatus(String closingStatus) {
|
|
|
|
|
+ String value = text(closingStatus);
|
|
|
|
|
+ McException.assertParamException(
|
|
|
|
|
+ !STATUS_CLOSED.equals(value) && !STATUS_REOPENED.equals(value),
|
|
|
|
|
+ "封账状态必须为封账或启封");
|
|
|
|
|
+ return value;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String validateRequest(MonthClosingCheckRequest request) {
|
|
|
|
|
+ McException.assertParamException(request == null, "请求不能为空");
|
|
|
|
|
+ McException.assertAccessException(
|
|
|
|
|
+ StringUtils.isAnyBlank(
|
|
|
|
|
+ whConf.getYidaAppType(),
|
|
|
|
|
+ whConf.getYidaSystemToken(),
|
|
|
|
|
+ closingConf.getFormUuid(),
|
|
|
|
|
+ closingConf.getMonthDateField(),
|
|
|
|
|
+ closingConf.getStatusField()),
|
|
|
|
|
+ "月度封账配置不完整");
|
|
|
|
|
+ return validateMonthText(request.getMonthText());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static boolean isApproved(Map item) {
|
|
|
|
|
+ return "agree".equals(text(item.get("approvedResult")).toLowerCase(Locale.ROOT));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String buildMessage(
|
|
|
|
|
+ String monthText,
|
|
|
|
|
+ String requestedStatus,
|
|
|
|
|
+ MonthClosingStatusResult closingStatus,
|
|
|
|
|
+ int projectRunningCount,
|
|
|
|
|
+ int otherRunningCount,
|
|
|
|
|
+ int writeBackPendingCount,
|
|
|
|
|
+ int summaryPendingCount) {
|
|
|
|
|
+ if (closingStatus.getRunningClosingCount() > 0) {
|
|
|
|
|
+ return monthText + " 已有封账或启封流程审批中,不可重复提交";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (STATUS_REOPENED.equals(requestedStatus)) {
|
|
|
|
|
+ return closingStatus.isClosed()
|
|
|
|
|
+ ? monthText + " 当前已封账,可以提交启封"
|
|
|
|
|
+ : monthText + " 当前未封账,不可重复启封";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (closingStatus.isClosed()) {
|
|
|
|
|
+ return monthText + " 已完成封账,不可重复提交";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (projectRunningCount > 0 || otherRunningCount > 0) {
|
|
|
|
|
+ return monthText + " 仍有工时审批单处理中:项目工时 "
|
|
|
|
|
+ + projectRunningCount + " 张,其他工时 " + otherRunningCount + " 张";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (writeBackPendingCount > 0) {
|
|
|
|
|
+ return monthText + " 有 " + writeBackPendingCount + " 张审批单尚未成功回写工时汇总";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (summaryPendingCount > 0) {
|
|
|
|
|
+ return monthText + " 仍有 " + summaryPendingCount + " 条工时汇总处于审批中";
|
|
|
|
|
+ }
|
|
|
|
|
+ return monthText + " 工时审批已完成,可以提交封账";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String text(Object value) {
|
|
|
|
|
+ return value == null ? "" : String.valueOf(value).trim();
|
|
|
|
|
+ }
|
|
|
|
|
+}
|