MonthClosingService.java 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753
  1. package com.malk.service.workhours;
  2. import com.alibaba.fastjson.JSON;
  3. import com.malk.server.aliwork.YDConf;
  4. import com.malk.server.aliwork.YDParam;
  5. import com.malk.server.common.McException;
  6. import com.malk.server.dingtalk.DDR_New;
  7. import com.malk.server.workhours.MonthClosingCheckRequest;
  8. import com.malk.server.workhours.MonthClosingConf;
  9. import com.malk.server.workhours.MonthClosingStatusResult;
  10. import com.malk.server.workhours.MonthClosingValidationResult;
  11. import com.malk.server.workhours.WHConf;
  12. import com.malk.service.aliwork.YDClient;
  13. import org.apache.commons.lang3.StringUtils;
  14. import org.springframework.beans.factory.annotation.Autowired;
  15. import org.springframework.stereotype.Service;
  16. import java.time.Instant;
  17. import java.time.YearMonth;
  18. import java.time.ZoneId;
  19. import java.time.format.DateTimeFormatter;
  20. import java.time.format.DateTimeParseException;
  21. import java.time.format.ResolverStyle;
  22. import java.util.ArrayList;
  23. import java.util.Arrays;
  24. import java.util.HashMap;
  25. import java.util.LinkedHashSet;
  26. import java.util.List;
  27. import java.util.Locale;
  28. import java.util.Map;
  29. import java.util.Set;
  30. import java.util.concurrent.ConcurrentHashMap;
  31. import java.util.concurrent.ConcurrentMap;
  32. import java.util.concurrent.ExecutionException;
  33. import java.util.concurrent.FutureTask;
  34. import java.util.function.LongSupplier;
  35. /**
  36. * 月度封账统一校验服务。
  37. *
  38. * <p>TimeCard、补填、变更与封账流程均调用本服务,避免各业务维护不同口径。</p>
  39. * <p>prd 2026-10-23:页面准入状态改为宜搭原生查询;本服务仍承担后端保存、
  40. * 提交、审批补发及封账校验,不能随旧页面查询接口作废而停用。</p>
  41. */
  42. @Service
  43. public class MonthClosingService {
  44. private static final int PAGE_SIZE = 100;
  45. private static final int MAX_PAGES = 1000;
  46. private static final String APPROVAL_MONTH_FIELD = "textField_mmd2lv0y";
  47. private static final String SUMMARY_MONTH_FIELD = "textField_mmbffvda";
  48. private static final String SUMMARY_STATUS_IN_APPROVAL = "审批中";
  49. private static final String REPAIR_SUMMARY_STATUS_PARTIAL = "部分成功";
  50. private static final String WRITE_BACK_SUCCESS = "全部成功";
  51. private static final String STATUS_CLOSED = "封账";
  52. private static final String STATUS_REOPENED = "启封";
  53. private static final long CLOSING_STATUS_CACHE_MILLIS = 5 * 60 * 1000L;
  54. private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai");
  55. private static final DateTimeFormatter MONTH_FORMATTER =
  56. DateTimeFormatter.ofPattern("uuuuMM").withResolverStyle(ResolverStyle.STRICT);
  57. private final ConcurrentMap<String, FutureTask<MonthClosingStatusResult>> closingStatusRequests =
  58. new ConcurrentHashMap<>();
  59. private final ConcurrentMap<String, CachedClosingStatus> closingStatusCache =
  60. new ConcurrentHashMap<>();
  61. private LongSupplier currentTimeMillis = System::currentTimeMillis;
  62. @Autowired
  63. private YDClient ydClient;
  64. @Autowired
  65. private WHConf whConf;
  66. @Autowired
  67. private MonthClosingConf closingConf;
  68. /**
  69. * 查询目标月份是否已封账。
  70. *
  71. * @param request 月份请求
  72. * @return 已审批封账与进行中封账数量
  73. */
  74. public MonthClosingStatusResult status(MonthClosingCheckRequest request) {
  75. String monthText = validateRequest(request);
  76. return queryClosingStatusCached(monthText);
  77. }
  78. /**
  79. * 一次读取全部封账流程,预热实际封账月份、当前月和上月的五分钟缓存。
  80. *
  81. * @deprecated prd 2026-10-23:旧页面查询预热功能作废,对应定时任务已关闭。
  82. * 仅保留历史方法兼容,不再自动调用;业务按需缓存及并发合并保持原样。
  83. * @return 本次写入缓存的月份数量
  84. */
  85. @Deprecated
  86. public int warmUpClosingStatusCache() {
  87. List<Map> processes = queryProcesses(closingConf.getFormUuid(), null, null, null);
  88. Set<String> months = new LinkedHashSet<>();
  89. for (Map item : processes) {
  90. String monthText = monthFromDateValue(
  91. extractFormData(item).get(closingConf.getMonthDateField()));
  92. if (StringUtils.isNotBlank(monthText)) {
  93. months.add(monthText);
  94. }
  95. }
  96. YearMonth currentMonth = YearMonth.now(BUSINESS_ZONE);
  97. months.add(currentMonth.format(MONTH_FORMATTER));
  98. months.add(currentMonth.minusMonths(1).format(MONTH_FORMATTER));
  99. long expiresAt = currentTimeMillis.getAsLong() + CLOSING_STATUS_CACHE_MILLIS;
  100. for (String monthText : months) {
  101. closingStatusCache.put(monthText, new CachedClosingStatus(
  102. summarizeClosingStatus(
  103. monthText, filterClosingMonth(processes, monthText)), expiresAt));
  104. }
  105. return months.size();
  106. }
  107. /**
  108. * 查询目标月份是否已封账;同月查询结果在五分钟内由后端复用。
  109. *
  110. * @param monthText 月份,格式 yyyyMM
  111. * @return true 表示最新已审批动作是封账
  112. */
  113. public boolean isClosed(String monthText) {
  114. MonthClosingCheckRequest request = MonthClosingCheckRequest.builder()
  115. .monthText(monthText)
  116. .build();
  117. return status(request).isClosed();
  118. }
  119. /**
  120. * 正常工时写入锁:封账流程中与已审批封账均禁止新增或追加审批。
  121. *
  122. * @param monthText 工时所属月份,格式 yyyyMM
  123. * @return true 表示当月已经锁定
  124. */
  125. public boolean isLocked(String monthText) {
  126. MonthClosingStatusResult result = status(MonthClosingCheckRequest.builder()
  127. .monthText(monthText).build());
  128. return result.isClosed() || result.getRunningClosingCount() > 0;
  129. }
  130. /**
  131. * 校验目标月份是否允许发起封账。
  132. *
  133. * @param request 月份请求
  134. * @return 封账许可、阻塞数量及提示
  135. */
  136. public MonthClosingValidationResult validate(MonthClosingCheckRequest request) {
  137. String monthText = validateRequest(request);
  138. String requestedStatus = validateClosingStatus(request.getClosingStatus());
  139. MonthClosingStatusResult closingStatus = queryClosingStatusCached(monthText);
  140. if (STATUS_REOPENED.equals(requestedStatus)) {
  141. return validateReopening(monthText, closingStatus);
  142. }
  143. return validateClosing(monthText, closingStatus);
  144. }
  145. private MonthClosingValidationResult validateClosing(
  146. String monthText,
  147. MonthClosingStatusResult closingStatus) {
  148. if (closingStatus.isClosed() || closingStatus.getRunningClosingCount() > 0) {
  149. return buildValidationResult(monthText, STATUS_CLOSED, closingStatus, 0, 0, 0, 0);
  150. }
  151. List<Map> projectRunning = normalApprovalProcesses(queryProcesses(
  152. whConf.getFormUuidApproval(), APPROVAL_MONTH_FIELD, monthText, "RUNNING"));
  153. List<Map> otherRunning = normalApprovalProcesses(queryProcesses(
  154. whConf.getFormUuidOtherApproval(), APPROVAL_MONTH_FIELD, monthText, "RUNNING"));
  155. List<Map> projectCompleted = normalApprovalProcesses(queryProcesses(
  156. whConf.getFormUuidApproval(), APPROVAL_MONTH_FIELD, monthText, "COMPLETED"));
  157. List<Map> otherCompleted = normalApprovalProcesses(queryProcesses(
  158. whConf.getFormUuidOtherApproval(), APPROVAL_MONTH_FIELD, monthText, "COMPLETED"));
  159. projectRunning.addAll(adjustmentApprovalProcesses(whConf.getFormUuidApproval(),
  160. whConf.getApprovalRepairSubmitDateField(), monthText, "RUNNING"));
  161. otherRunning.addAll(adjustmentApprovalProcesses(whConf.getFormUuidOtherApproval(),
  162. whConf.getOtherApprovalRepairSubmitDateField(), monthText, "RUNNING"));
  163. projectCompleted.addAll(adjustmentApprovalProcesses(whConf.getFormUuidApproval(),
  164. whConf.getApprovalRepairSubmitDateField(), monthText, "COMPLETED"));
  165. otherCompleted.addAll(adjustmentApprovalProcesses(whConf.getFormUuidOtherApproval(),
  166. whConf.getOtherApprovalRepairSubmitDateField(), monthText, "COMPLETED"));
  167. int writeBackPendingCount = countWriteBackPending(
  168. projectCompleted, whConf.getApprovalSyncStatusField())
  169. + countWriteBackPending(
  170. otherCompleted, whConf.getOtherApprovalSyncStatusField());
  171. int summaryPendingCount = querySummaryPendingCount(monthText)
  172. + queryRepairSummaryPendingCount(monthText);
  173. return buildValidationResult(
  174. monthText,
  175. STATUS_CLOSED,
  176. closingStatus,
  177. projectRunning.size(),
  178. otherRunning.size(),
  179. writeBackPendingCount,
  180. summaryPendingCount);
  181. }
  182. /** 正常填报沿用填报月份口径;历史空类型按正常填报处理。 */
  183. private List<Map> normalApprovalProcesses(List<Map> processes) {
  184. List<Map> result = new ArrayList<>();
  185. String fillTypeField = whConf.getApprovalFillTypeField();
  186. for (Map item : processes) {
  187. Map data = extractFormData(item);
  188. String fillType = text(data.get(fillTypeField));
  189. if (StringUtils.isBlank(fillTypeField)
  190. || (!"补填".equals(fillType) && !"变更".equals(fillType))) {
  191. result.add(item);
  192. }
  193. }
  194. return result;
  195. }
  196. /** 补填和变更审批按提交日期归属封账月份,目标月份只用于定位被调整的工时。 */
  197. private List<Map> adjustmentApprovalProcesses(String formUuid, String submitDateField,
  198. String monthText, String instanceStatus) {
  199. McException.assertAccessException(StringUtils.isAnyBlank(
  200. whConf.getApprovalFillTypeField(), submitDateField), "补填/变更审批查询配置不完整");
  201. List<Map> result = new ArrayList<>();
  202. for (Map item : queryProcesses(formUuid, null, null, instanceStatus)) {
  203. Map data = extractFormData(item);
  204. String fillType = text(data.get(whConf.getApprovalFillTypeField()));
  205. if (("补填".equals(fillType) || "变更".equals(fillType))
  206. && monthText.equals(monthFromDateValue(data.get(submitDateField)))) {
  207. result.add(item);
  208. }
  209. }
  210. return result;
  211. }
  212. private MonthClosingValidationResult validateReopening(
  213. String monthText,
  214. MonthClosingStatusResult closingStatus) {
  215. return buildValidationResult(monthText, STATUS_REOPENED, closingStatus, 0, 0, 0, 0);
  216. }
  217. private static MonthClosingValidationResult buildValidationResult(
  218. String monthText,
  219. String requestedStatus,
  220. MonthClosingStatusResult closingStatus,
  221. int projectRunningCount,
  222. int otherRunningCount,
  223. int writeBackPendingCount,
  224. int summaryPendingCount) {
  225. boolean requestedClosing = STATUS_CLOSED.equals(requestedStatus);
  226. boolean stateAllowed = requestedClosing ? !closingStatus.isClosed() : closingStatus.isClosed();
  227. boolean allowed = stateAllowed
  228. && closingStatus.getRunningClosingCount() == 0
  229. && projectRunningCount == 0
  230. && otherRunningCount == 0
  231. && writeBackPendingCount == 0
  232. && summaryPendingCount == 0;
  233. return MonthClosingValidationResult.builder()
  234. .monthText(monthText)
  235. .allowed(allowed)
  236. .requestedStatus(requestedStatus)
  237. .currentStatus(closingStatus.getCurrentStatus())
  238. .alreadyClosed(closingStatus.isClosed())
  239. .closingRunningCount(closingStatus.getRunningClosingCount())
  240. .projectRunningCount(projectRunningCount)
  241. .otherRunningCount(otherRunningCount)
  242. .writeBackPendingCount(writeBackPendingCount)
  243. .summaryPendingCount(summaryPendingCount)
  244. .message(buildMessage(
  245. monthText,
  246. requestedStatus,
  247. closingStatus,
  248. projectRunningCount,
  249. otherRunningCount,
  250. writeBackPendingCount,
  251. summaryPendingCount))
  252. .build();
  253. }
  254. private MonthClosingStatusResult queryClosingStatusCached(String monthText) {
  255. MonthClosingStatusResult cached = getCachedClosingStatus(monthText);
  256. if (cached != null) {
  257. return cached;
  258. }
  259. return queryClosingStatusCoalesced(monthText);
  260. }
  261. private MonthClosingStatusResult queryClosingStatusCoalesced(String monthText) {
  262. // fixme 缓存未命中时,同月并发请求共享一次宜搭查询。
  263. FutureTask<MonthClosingStatusResult> newRequest = new FutureTask<>(() -> {
  264. MonthClosingStatusResult cached = getCachedClosingStatus(monthText);
  265. if (cached != null) {
  266. return cached;
  267. }
  268. MonthClosingStatusResult result = queryClosingStatus(monthText);
  269. closingStatusCache.put(monthText, new CachedClosingStatus(
  270. result, currentTimeMillis.getAsLong() + CLOSING_STATUS_CACHE_MILLIS));
  271. return result;
  272. });
  273. FutureTask<MonthClosingStatusResult> activeRequest =
  274. closingStatusRequests.putIfAbsent(monthText, newRequest);
  275. boolean ownsRequest = activeRequest == null;
  276. if (ownsRequest) {
  277. activeRequest = newRequest;
  278. activeRequest.run();
  279. }
  280. try {
  281. return activeRequest.get();
  282. } catch (InterruptedException ex) {
  283. Thread.currentThread().interrupt();
  284. throw new IllegalStateException("月度封账状态查询被中断", ex);
  285. } catch (ExecutionException ex) {
  286. Throwable cause = ex.getCause();
  287. if (cause instanceof RuntimeException) {
  288. throw (RuntimeException) cause;
  289. }
  290. if (cause instanceof Error) {
  291. throw (Error) cause;
  292. }
  293. throw new IllegalStateException("月度封账状态查询失败", cause);
  294. } finally {
  295. if (ownsRequest) {
  296. closingStatusRequests.remove(monthText, newRequest);
  297. }
  298. }
  299. }
  300. private MonthClosingStatusResult getCachedClosingStatus(String monthText) {
  301. while (true) {
  302. CachedClosingStatus cached = closingStatusCache.get(monthText);
  303. if (cached == null) {
  304. return null;
  305. }
  306. if (currentTimeMillis.getAsLong() < cached.expiresAtMillis) {
  307. return cached.result;
  308. }
  309. if (closingStatusCache.remove(monthText, cached)) {
  310. return null;
  311. }
  312. }
  313. }
  314. private static final class CachedClosingStatus {
  315. private final MonthClosingStatusResult result;
  316. private final long expiresAtMillis;
  317. private CachedClosingStatus(MonthClosingStatusResult result, long expiresAtMillis) {
  318. this.result = result;
  319. this.expiresAtMillis = expiresAtMillis;
  320. }
  321. }
  322. private MonthClosingStatusResult queryClosingStatus(String monthText) {
  323. List<Long> monthRange = closingMonthRange(monthText);
  324. List<Map> processes = filterClosingMonth(
  325. queryProcesses(closingConf.getFormUuid(), closingConf.getMonthDateField(),
  326. monthRange, null), monthText);
  327. return summarizeClosingStatus(monthText, processes);
  328. }
  329. private MonthClosingStatusResult summarizeClosingStatus(String monthText, List<Map> processes) {
  330. int approvedClosingCount = 0;
  331. int approvedReopeningCount = 0;
  332. int runningClosingCount = 0;
  333. String currentStatus = "";
  334. Long latestTime = null;
  335. for (Map item : processes) {
  336. String instanceStatus = processInstanceStatus(item);
  337. if ("RUNNING".equals(instanceStatus)) {
  338. runningClosingCount++;
  339. continue;
  340. }
  341. if (!"COMPLETED".equals(instanceStatus)) {
  342. continue;
  343. }
  344. if (!isApproved(item)) {
  345. continue;
  346. }
  347. String itemStatus = text(extractFormData(item).get(closingConf.getStatusField()));
  348. if (STATUS_CLOSED.equals(itemStatus)) {
  349. approvedClosingCount++;
  350. } else if (STATUS_REOPENED.equals(itemStatus)) {
  351. approvedReopeningCount++;
  352. } else {
  353. continue;
  354. }
  355. Long itemTime = processEventTime(item);
  356. if (StringUtils.isBlank(currentStatus)
  357. || (latestTime == null && itemTime != null)
  358. || (latestTime != null && itemTime != null && itemTime > latestTime)) {
  359. currentStatus = itemStatus;
  360. latestTime = itemTime;
  361. }
  362. }
  363. return MonthClosingStatusResult.builder()
  364. .monthText(monthText)
  365. .closed(STATUS_CLOSED.equals(currentStatus))
  366. .currentStatus(currentStatus)
  367. .approvedClosingCount(approvedClosingCount)
  368. .approvedReopeningCount(approvedReopeningCount)
  369. .runningClosingCount(runningClosingCount)
  370. .build();
  371. }
  372. private List<Map> filterClosingMonth(List<Map> processes, String monthText) {
  373. List<Map> result = new ArrayList<>();
  374. for (Map item : processes) {
  375. Object monthValue = extractFormData(item).get(closingConf.getMonthDateField());
  376. if (monthText.equals(monthFromDateValue(monthValue))) {
  377. result.add(item);
  378. }
  379. }
  380. return result;
  381. }
  382. private int countWriteBackPending(List<Map> completed, String syncStatusField) {
  383. int pending = 0;
  384. for (Map item : completed) {
  385. if (!isApproved(item)) {
  386. continue;
  387. }
  388. Map formData = extractFormData(item);
  389. String syncStatus = text(formData.get(syncStatusField));
  390. if (!WRITE_BACK_SUCCESS.equals(syncStatus)) {
  391. pending++;
  392. }
  393. }
  394. return pending;
  395. }
  396. private int querySummaryPendingCount(String monthText) {
  397. Map<String, Object> search = new HashMap<>();
  398. search.put(SUMMARY_MONTH_FIELD, monthText);
  399. search.put(whConf.getSummaryApprovalStatusField(), SUMMARY_STATUS_IN_APPROVAL);
  400. List<Map> rows = queryForms(whConf.getFormUuidWorkHoursSummary(), search);
  401. int count = 0;
  402. for (Map item : rows) {
  403. Map formData = extractFormData(item);
  404. if (monthText.equals(text(formData.get(SUMMARY_MONTH_FIELD)))
  405. && SUMMARY_STATUS_IN_APPROVAL.equals(
  406. text(formData.get(whConf.getSummaryApprovalStatusField())))) {
  407. count++;
  408. }
  409. }
  410. return count;
  411. }
  412. private int queryRepairSummaryPendingCount(String monthText) {
  413. McException.assertAccessException(StringUtils.isAnyBlank(
  414. whConf.getFormUuidRepairWorkHoursSummary(),
  415. whConf.getRepairSummarySubmitDateField(),
  416. whConf.getRepairSummarySyncStatusField()), "补填汇总查询配置不完整");
  417. int count = 0;
  418. // fixme 补填提交日期是复制表新增字段;完整分页后本地匹配月份,避免查询索引漏掉待回写记录。
  419. for (Map item : queryForms(whConf.getFormUuidRepairWorkHoursSummary(), new HashMap<>())) {
  420. Map data = extractFormData(item);
  421. String status = text(data.get(whConf.getRepairSummarySyncStatusField()));
  422. if (monthText.equals(monthFromDateValue(data.get(whConf.getRepairSummarySubmitDateField())))
  423. && (SUMMARY_STATUS_IN_APPROVAL.equals(status)
  424. || REPAIR_SUMMARY_STATUS_PARTIAL.equals(status))) {
  425. count++;
  426. }
  427. }
  428. return count;
  429. }
  430. private List<Map> queryProcesses(
  431. String formUuid,
  432. String monthField,
  433. Object monthValue,
  434. String instanceStatus) {
  435. McException.assertAccessException(
  436. StringUtils.isBlank(formUuid),
  437. "月度封账流程查询配置不完整");
  438. Map<String, Object> search = new HashMap<>();
  439. if (StringUtils.isNotBlank(monthField) && monthValue != null) {
  440. search.put(monthField, monthValue);
  441. }
  442. List<Map> result = new ArrayList<>();
  443. int page = 1;
  444. while (page <= MAX_PAGES) {
  445. DDR_New response = ydClient.queryData(YDParam.builder()
  446. .appType(whConf.getYidaAppType())
  447. .systemToken(whConf.getYidaSystemToken())
  448. .formUuid(formUuid)
  449. .searchFieldJson(search.isEmpty() ? null : JSON.toJSONString(search))
  450. .instanceStatus(instanceStatus)
  451. .pageNumber(page)
  452. .pageSize(PAGE_SIZE)
  453. .build(), YDConf.FORM_QUERY.retrieve_search_process);
  454. List<Map> data = responseData(response);
  455. result.addAll(data);
  456. if (!hasNextPage(page, response.getTotalCount(), data.size(), result.size())) {
  457. break;
  458. }
  459. page++;
  460. }
  461. McException.assertAccessException(page > MAX_PAGES, "月度封账流程查询超过分页上限");
  462. return result;
  463. }
  464. private List<Map> queryForms(String formUuid, Map<String, Object> search) {
  465. McException.assertAccessException(StringUtils.isBlank(formUuid), "工时汇总表配置为空");
  466. List<Map> result = new ArrayList<>();
  467. int page = 1;
  468. while (page <= MAX_PAGES) {
  469. DDR_New response = ydClient.queryData(YDParam.builder()
  470. .appType(whConf.getYidaAppType())
  471. .systemToken(whConf.getYidaSystemToken())
  472. .formUuid(formUuid)
  473. .searchFieldJson(JSON.toJSONString(search))
  474. .currentPage(page)
  475. .pageSize(PAGE_SIZE)
  476. .build(), YDConf.FORM_QUERY.retrieve_search_form);
  477. List<Map> data = responseData(response);
  478. result.addAll(data);
  479. if (!hasNextPage(page, response.getTotalCount(), data.size(), result.size())) {
  480. break;
  481. }
  482. page++;
  483. }
  484. McException.assertAccessException(page > MAX_PAGES, "工时汇总查询超过分页上限");
  485. return result;
  486. }
  487. private static boolean hasNextPage(
  488. int page,
  489. long totalCount,
  490. int returnedSize,
  491. int collectedSize) {
  492. if (returnedSize < PAGE_SIZE) {
  493. return false;
  494. }
  495. return totalCount <= 0 || collectedSize < totalCount || (long) page * PAGE_SIZE < totalCount;
  496. }
  497. @SuppressWarnings("unchecked")
  498. private static List<Map> responseData(DDR_New response) {
  499. McException.assertAccessException(response == null, "宜搭查询未返回结果");
  500. Object data = response.getData();
  501. if (data == null) {
  502. return new ArrayList<>();
  503. }
  504. McException.assertAccessException(!(data instanceof List), "宜搭查询返回数据格式异常");
  505. return (List<Map>) data;
  506. }
  507. @SuppressWarnings("unchecked")
  508. static Map extractFormData(Map item) {
  509. if (item == null) {
  510. return new HashMap();
  511. }
  512. Object formData = item.get("formData");
  513. if (formData instanceof Map) {
  514. return (Map) formData;
  515. }
  516. Object data = item.get("data");
  517. if (!(data instanceof Map)) {
  518. return new HashMap();
  519. }
  520. Map dataMap = (Map) data;
  521. Object nested = dataMap.get("formData");
  522. return nested instanceof Map ? (Map) nested : dataMap;
  523. }
  524. static String validateMonthText(String monthText) {
  525. String value = StringUtils.trimToEmpty(monthText);
  526. McException.assertParamException(!value.matches("\\d{6}"), "月份必须为yyyyMM格式");
  527. try {
  528. YearMonth.parse(value, MONTH_FORMATTER);
  529. } catch (DateTimeParseException ex) {
  530. throw McException.builder()
  531. .code("validated_param")
  532. .message("月份必须为有效的yyyyMM格式")
  533. .build();
  534. }
  535. return value;
  536. }
  537. static List<Long> closingMonthRange(String monthText) {
  538. YearMonth month = YearMonth.parse(validateMonthText(monthText), MONTH_FORMATTER);
  539. long start = month.atDay(1).atStartOfDay(BUSINESS_ZONE).toInstant().toEpochMilli();
  540. long end = month.plusMonths(1).atDay(1).atStartOfDay(BUSINESS_ZONE)
  541. .toInstant().toEpochMilli() - 1L;
  542. return Arrays.asList(start, end);
  543. }
  544. static String monthFromDateValue(Object value) {
  545. if (value == null) {
  546. return "";
  547. }
  548. if (value instanceof Number) {
  549. return monthFromEpoch(((Number) value).longValue());
  550. }
  551. String raw = text(value);
  552. if (raw.matches("\\d{6}")) {
  553. try {
  554. return validateMonthText(raw);
  555. } catch (McException ignored) {
  556. return "";
  557. }
  558. }
  559. if (raw.matches("\\d{8}")) {
  560. return normalizeYearMonth(raw.substring(0, 4), raw.substring(4, 6));
  561. }
  562. if (raw.matches("\\d{10}|\\d{13}")) {
  563. try {
  564. return monthFromEpoch(Long.parseLong(raw));
  565. } catch (NumberFormatException ignored) {
  566. return "";
  567. }
  568. }
  569. try {
  570. return YearMonth.from(Instant.parse(raw).atZone(BUSINESS_ZONE)).format(MONTH_FORMATTER);
  571. } catch (DateTimeParseException ignored) {
  572. // 普通日期字符串继续按 yyyy-MM 前缀处理。
  573. }
  574. if (raw.matches("^\\d{4}[-/]\\d{2}.*")) {
  575. return normalizeYearMonth(raw.substring(0, 4), raw.substring(5, 7));
  576. }
  577. return "";
  578. }
  579. private static String monthFromEpoch(long rawTimestamp) {
  580. String digits = String.valueOf(Math.abs(rawTimestamp));
  581. if (digits.length() == 8) {
  582. return normalizeYearMonth(digits.substring(0, 4), digits.substring(4, 6));
  583. }
  584. long milliseconds = digits.length() <= 10 ? rawTimestamp * 1000L : rawTimestamp;
  585. try {
  586. return YearMonth.from(Instant.ofEpochMilli(milliseconds).atZone(BUSINESS_ZONE))
  587. .format(MONTH_FORMATTER);
  588. } catch (RuntimeException ignored) {
  589. return "";
  590. }
  591. }
  592. private static String normalizeYearMonth(String year, String month) {
  593. try {
  594. return YearMonth.of(Integer.parseInt(year), Integer.parseInt(month))
  595. .format(MONTH_FORMATTER);
  596. } catch (RuntimeException ignored) {
  597. return "";
  598. }
  599. }
  600. private static Long processEventTime(Map item) {
  601. if (item == null) {
  602. return null;
  603. }
  604. String[] fields = {
  605. "modifiedTimeGMT", "gmtModified", "finishTimeGMT", "finishTime",
  606. "modifiedTime", "createTimeGMT", "createTime"
  607. };
  608. for (String field : fields) {
  609. Long parsed = parseTimestamp(item.get(field));
  610. if (parsed != null) {
  611. return parsed;
  612. }
  613. }
  614. return null;
  615. }
  616. private static Long parseTimestamp(Object value) {
  617. if (value instanceof Number) {
  618. long number = ((Number) value).longValue();
  619. return String.valueOf(Math.abs(number)).length() <= 10 ? number * 1000L : number;
  620. }
  621. String raw = text(value);
  622. if (raw.matches("\\d{10}|\\d{13}")) {
  623. try {
  624. long number = Long.parseLong(raw);
  625. return raw.length() == 10 ? number * 1000L : number;
  626. } catch (NumberFormatException ignored) {
  627. return null;
  628. }
  629. }
  630. try {
  631. return Instant.parse(raw).toEpochMilli();
  632. } catch (DateTimeParseException ignored) {
  633. return null;
  634. }
  635. }
  636. private static String validateClosingStatus(String closingStatus) {
  637. String value = text(closingStatus);
  638. McException.assertParamException(
  639. !STATUS_CLOSED.equals(value) && !STATUS_REOPENED.equals(value),
  640. "封账状态必须为封账或启封");
  641. return value;
  642. }
  643. private String validateRequest(MonthClosingCheckRequest request) {
  644. McException.assertParamException(request == null, "请求不能为空");
  645. McException.assertAccessException(
  646. StringUtils.isAnyBlank(
  647. whConf.getYidaAppType(),
  648. whConf.getYidaSystemToken(),
  649. closingConf.getFormUuid(),
  650. closingConf.getMonthDateField(),
  651. closingConf.getStatusField()),
  652. "月度封账配置不完整");
  653. return validateMonthText(request.getMonthText());
  654. }
  655. private static boolean isApproved(Map item) {
  656. return "agree".equals(text(item.get("approvedResult")).toLowerCase(Locale.ROOT));
  657. }
  658. private static String processInstanceStatus(Map item) {
  659. String status = text(item == null ? null : item.get("instanceStatus"));
  660. if (StringUtils.isBlank(status)) {
  661. status = text(item == null ? null : item.get("processInstanceStatus"));
  662. }
  663. return status.toUpperCase(Locale.ROOT);
  664. }
  665. private static String buildMessage(
  666. String monthText,
  667. String requestedStatus,
  668. MonthClosingStatusResult closingStatus,
  669. int projectRunningCount,
  670. int otherRunningCount,
  671. int writeBackPendingCount,
  672. int summaryPendingCount) {
  673. if (closingStatus.getRunningClosingCount() > 0) {
  674. return monthText + " 已有封账或启封流程审批中,不可重复提交";
  675. }
  676. if (STATUS_REOPENED.equals(requestedStatus)) {
  677. return closingStatus.isClosed()
  678. ? monthText + " 当前已封账,可以提交启封"
  679. : monthText + " 当前未封账,不可重复启封";
  680. }
  681. if (closingStatus.isClosed()) {
  682. return monthText + " 已完成封账,不可重复提交";
  683. }
  684. if (projectRunningCount > 0 || otherRunningCount > 0) {
  685. return monthText + " 仍有工时审批单处理中:项目工时 "
  686. + projectRunningCount + " 张,其他工时 " + otherRunningCount + " 张";
  687. }
  688. if (writeBackPendingCount > 0) {
  689. return monthText + " 有 " + writeBackPendingCount + " 张审批单尚未成功回写工时汇总";
  690. }
  691. if (summaryPendingCount > 0) {
  692. return monthText + " 仍有 " + summaryPendingCount + " 条工时汇总尚未完成审批回写";
  693. }
  694. return monthText + " 工时审批已完成,可以提交封账";
  695. }
  696. private static String text(Object value) {
  697. return value == null ? "" : String.valueOf(value).trim();
  698. }
  699. }