| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516 |
- 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();
- }
- }
|