| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899 |
- 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.ReSubmitApprovalResult;
- import com.malk.server.workhours.TimeCardApprovalStartRequest;
- import com.malk.server.workhours.WHConf;
- import com.malk.service.aliwork.YDClient;
- import com.malk.service.aliwork.YDService;
- import com.malk.service.dingtalk.DDClient;
- import com.malk.service.dingtalk.DDClient_Contacts;
- import com.malk.utils.UtilMap;
- 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.time.LocalDate;
- import java.time.format.DateTimeFormatter;
- import java.util.*;
- /**
- * 补发起工时审批(bug 1 兜底)
- * <p>
- * 场景:汇总表 submitted > 0 但对应 (userId, day) 未出现在任何该月工时审批 / 其他工时审批的子表明细中,
- * 即前端 submitApprovalProcess 链路失败(startInstance 挂了/静默丢),导致部分员工工时从未进入审批。
- * <p>
- * 逻辑完全参考前端 TimeCard 的 submitApprovalProcess:
- * 1. collectApprovalData — 遍历汇总表每天子表,delta = submitted - approved - change, delta>0 才 push;
- * 普通 PM 按审批人分组,Raymond 兜底按项目拆组
- * 2. processManagerApproval — queryProcess(month + PM + RUNNING) 有同项目可用分片则 update / 无则 create
- * COMPLETED 明细先进入覆盖索引;汇总尚未落值时归回写处理,不重复发起
- * 3. collectOtherApprovalData / processOtherApproval — 其他工时按当前员工的部门主管作 approver,同样 query→update/create
- * <p>
- * 发起人 userId 必须用真人钉钉账号(建议 System),不能用 yida_pub_account(不触发钉钉待办通知)
- * <p>
- * 设计文档:后端/阿科德斯/审批链路补发起.md
- */
- @Slf4j
- @Service
- public class ReSubmitApprovalService {
- @Autowired
- private YDClient ydClient;
- @Autowired
- private YDService ydService;
- @Autowired
- private WHConf whConf;
- @Autowired
- private DDClient ddClient;
- @Autowired
- private DDClient_Contacts ddClient_contacts;
- /**
- * 前端 v2.35 常量:发起时明细「审批意见」默认「同意」
- */
- private static final String APPROVAL_OPINION_AGREE = "同意";
- private static final String SUMMARY_STATUS_IN_APPROVAL = "审批中";
- private static final String SUMMARY_STATUS_DRAFT = "暂存";
- private static final String SUMMARY_STATUS_APPROVED = "已审批";
- private static final String S_BIL_SUBMITTED_TOTAL = "numberField_mmacxewa";
- private static final String S_BIL_APPROVED_TOTAL = "numberField_mmacxewg";
- private static final String S_NON_SUBMITTED_TOTAL = "numberField_mmad6jbz";
- private static final String S_NON_APPROVED_TOTAL = "numberField_mmad6jc1";
- private static final String S_OTHER_SUBMITTED_TOTAL = "numberField_mmd1smeq";
- private static final String S_OTHER_APPROVED_TOTAL = "numberField_mmd1smes";
- /**
- * 宜搭单个子表组件可稳定查询和覆盖更新的最大行数。
- */
- private static final int MAX_APPROVAL_SUBTABLE_ROWS = 500;
- /**
- * 批量历史补偿的目标分片大小;以员工整组为边界,允许单个员工整组略超目标值。
- */
- private static final int TARGET_APPROVAL_SUBTABLE_ROWS = 300;
- /**
- * 兜底部门主管 userId(Raymond,前端 v2.42 保持一致)
- */
- private static final String DEFAULT_MANAGER_ID = "634735129";
- private static final String PERSONNEL_EMPLOYEE = "employeeField_mkow4ydp";
- private static final String PERSONNEL_MANAGER = "employeeField_mh8xhqc3";
- // ===== 汇总表字段(与 ApprovalWriteBackService 一致) =====
- private static final String S_SUBMITTER = "employeeField_mmacxew4";
- private static final String S_SUBMITTER_ID = "textField_mmbffvd9";
- private static final String S_MONTH_TEXT = "textField_mmbffvda";
- private static final String S_DAY_TEXT = "textField_mmbffvdb";
- private static final String S_DAY_DATE = "dateField_mmbffvd8";
- private static final String S_DEPT_MANAGER = "employeeField_mh8xhqc3";
- // 汇总表 · Billable 子表
- private static final String S_BIL_TABLE = "tableField_mmczo634";
- private static final String S_BIL_PROJECT_NAME = "selectField_mmad6jci";
- private static final String S_BIL_PROJECT_NAME_TEXT = "textField_mmad6jcj";
- private static final String S_BIL_PROJECT_CODE = "textField_mmacxew3";
- private static final String S_BIL_MANAGER = "employeeField_mkoxpswf";
- private static final String S_BIL_SUBMITTED = "numberField_mmczo636";
- private static final String S_BIL_APPROVED = "numberField_mmczo637";
- private static final String S_BIL_CHANGE = "numberField_mmd80zae";
- // 汇总表 · Non Billable 子表
- private static final String S_NON_TABLE = "tableField_mmczo63h";
- private static final String S_NON_PROJECT_NAME = "selectField_mmczo638";
- private static final String S_NON_PROJECT_NAME_TEXT = "textField_mmczo63g";
- private static final String S_NON_PROJECT_CODE = "textField_mmczo639";
- private static final String S_NON_MANAGER = "employeeField_mmczo63a";
- private static final String S_NON_SUBMITTED = "numberField_mmczo63c";
- private static final String S_NON_APPROVED = "numberField_mmczo63d";
- private static final String S_NON_CHANGE = "numberField_mmeakgi9";
- // 汇总表 · 其他工时子表
- private static final String S_OTH_TABLE = "tableField_mmeakgid";
- private static final String S_OTH_ACTIVITY = "selectField_mmeakgie";
- private static final String S_OTH_SUBMITTED = "numberField_mmeakgij";
- private static final String S_OTH_APPROVED = "numberField_mmeakgii";
- private static final String S_OTH_CHANGE = "numberField_mmeakgim";
- // ===== 工时审批(APPROVAL) 主表字段 =====
- private static final String A_MONTH_DATE = "dateField_mmacxewf";
- private static final String A_MONTH_TEXT = "textField_mmd2lv0y";
- private static final String A_MANAGER = "employeeField_mkoxpswf";
- private static final String A_PROJECT_HOURS = "numberField_mmd2z155";
- private static final String A_NON_PROJECT_HOURS = "numberField_mmd2z156";
- private static final String A_TOTAL_HOURS = "numberField_mmd5b5gl";
- private static final String A_VIEWERS = "employeeField_moi9pvt1";
- // 工时审批 · Billable 明细子表
- private static final String A_BIL_TABLE = "tableField_mmae8t99";
- private static final String A_BIL_WORK_DATE = "dateField_mmbffvd8";
- private static final String A_BIL_SUBMITTER = "employeeField_mmacxew4";
- private static final String A_BIL_SUBMITTER_UID = "textField_mmd1ozk4";
- private static final String A_BIL_PROJECT_NAME = "textField_mkov5ozd";
- private static final String A_BIL_PROJECT_CODE = "textField_mmacxew3";
- private static final String A_BIL_HOURS = "numberField_mmacxew9";
- private static final String A_BIL_MONTH_TEXT = "textField_mmdbi2n2";
- private static final String A_BIL_DAY_TEXT = "textField_mmd8g654";
- private static final String A_BIL_OPINION = "radioField_mpwhk294";
- // 工时审批 · Non Billable 明细子表
- private static final String A_NON_TABLE = "tableField_mmd1wu9h";
- private static final String A_NON_WORK_DATE = "dateField_mmd2z150";
- private static final String A_NON_SUBMITTER = "employeeField_mmd1wu9b";
- private static final String A_NON_SUBMITTER_UID = "textField_mmd1wu9f";
- private static final String A_NON_PROJECT_NAME = "textField_mmd1wu9c";
- private static final String A_NON_PROJECT_CODE = "textField_mmd1wu9d";
- private static final String A_NON_HOURS = "numberField_mmd1wu9e";
- private static final String A_NON_MONTH_TEXT = "textField_mmdbi2n0";
- private static final String A_NON_DAY_TEXT = "textField_mmd8g655";
- private static final String A_NON_OPINION = "radioField_mpwhk295";
- // ===== 其他工时审批(OTHER_APPROVAL) 主表字段 =====
- private static final String O_MONTH_DATE = "dateField_mmacxewf";
- private static final String O_MONTH_TEXT = "textField_mmd2lv0y";
- private static final String O_DEPT_MANAGER = "employeeField_mh8xhqc3";
- private static final String O_TOTAL_HOURS = "numberField_mmd5b5gl";
- private static final String O_VIEWERS = "employeeField_moi9pvt1";
- // 其他工时审批 · 明细子表(与 Non Billable 同 tableField,但 activity 语义)
- private static final String O_SUB_TABLE = "tableField_mmd1wu9h";
- private static final String O_SUB_WORK_DATE = "dateField_mmd2z150";
- private static final String O_SUB_SUBMITTER = "employeeField_mmd1wu9b";
- private static final String O_SUB_SUBMITTER_UID = "textField_mmd1wu9f";
- private static final String O_SUB_ACTIVITY = "selectField_mmeakgie";
- private static final String O_SUB_HOURS = "numberField_mmd1wu9e";
- private static final String O_SUB_MONTH_TEXT = "textField_mmdbi2n0";
- private static final String O_SUB_DAY_TEXT = "textField_mmd8g655";
- private static final String O_SUB_OPINION = "radioField_mpwhk294";
- /**
- * 一条待发起的项目工时行(前端 collectApprovalData 的等价)
- */
- private static class BillRow {
- String dayText; // yyyyMMdd
- long dayTs; // epoch ms
- String projectCode;
- String projectName;
- double hours; // delta = submitted - approved - change
- String submitterUid;
- String monthText; // yyyyMM
- }
- /**
- * 一条待发起的其他工时行
- */
- private static class OtherRow {
- String dayText;
- long dayTs;
- String activity; // Annual Leave / Sick Leave / ...
- double hours;
- String submitterUid;
- String monthText;
- }
- /**
- * 审批覆盖索引:RUNNING 明细直接阻止重复补发;COMPLETED 同意明细在汇总尚未计入时归 Bug2。
- */
- static class ApprovalCoverage {
- Set<String> runningKeys = new HashSet<>();
- Map<String, Double> completedAgreeHours = new HashMap<>();
- boolean shouldSkip(String key, double accountedHours) {
- if (runningKeys.contains(key)) {
- return true;
- }
- return completedAgreeHours.getOrDefault(key, 0d) > accountedHours + 1e-6;
- }
- }
- /**
- * 某员工某月的项目工时组;Raymond 兜底组带项目标识做项目隔离
- */
- private static class ManagerGroup {
- String managerId;
- String projectCode;
- String projectName;
- List<BillRow> billableRows = new ArrayList<>();
- List<BillRow> nonBillableRows = new ArrayList<>();
- double billableTotal = 0;
- double nonBillableTotal = 0;
- }
- /**
- * 对指定月份、指定员工(可空)执行补发起
- *
- * @param monthText yyyyMM
- * @param submitterUserId 可空; 空则处理该月所有员工
- * @return 结果统计
- */
- public ReSubmitApprovalResult resubmit(String monthText, String submitterUserId) {
- return resubmit(monthText, submitterUserId, false);
- }
- /**
- * 对指定月份、指定员工执行补发起或只读预演。
- *
- * @param monthText yyyyMM
- * @param submitterUserId 可空;空则处理该月所有员工
- * @param dryRun true 时只统计候选,不创建或更新审批
- * @return 执行或预演统计
- */
- public ReSubmitApprovalResult resubmit(String monthText,
- String submitterUserId,
- boolean dryRun) {
- McException.assertAccessException(StringUtils.isBlank(monthText), "月份不能为空");
- McException.assertAccessException(StringUtils.isBlank(whConf.getApprovalProcessCode()),
- "工时审批 processCode 未配置(workhours.approvalProcessCode)");
- McException.assertAccessException(StringUtils.isBlank(whConf.getOtherApprovalProcessCode()),
- "其他工时审批 processCode 未配置(workhours.otherApprovalProcessCode)");
- McException.assertAccessException(StringUtils.isBlank(whConf.getApprovalOriginatorUserId()),
- "审批补发起真人账号未配置(workhours.approvalOriginatorUserId)");
- // 1) 查汇总表:按月份 + 可选员工取全,是否待审批由子表 submitted-approved-change 实际差额判定。
- Map<String, Object> search = new HashMap<>();
- search.put(S_MONTH_TEXT, monthText);
- if (StringUtils.isNotBlank(submitterUserId)) {
- search.put(S_SUBMITTER_ID, submitterUserId);
- }
- List<Map> allRecords = queryAll(whConf.getFormUuidWorkHoursSummary(), search);
- ApprovalCoverage approvalCoverage = loadApprovalCoverage(monthText);
- Map<String, String> projectManagerCache = new HashMap<>();
- // 2) 按 (submitterUid) 分组员工月度记录
- Map<String, List<Map>> perSubmitter = new LinkedHashMap<>();
- for (Map rec : allRecords) {
- Map fd = (Map) rec.get("formData");
- if (fd == null) {
- continue;
- }
- String uid = str(fd, S_SUBMITTER_ID);
- if (StringUtils.isBlank(uid)) {
- continue;
- }
- perSubmitter.computeIfAbsent(uid, k -> new ArrayList<>()).add(rec);
- }
- log.info("[补发起] 待处理员工数 month={} submitter={} count={}", monthText, submitterUserId, perSubmitter.size());
- int billableCreated = 0, billableUpdated = 0;
- int otherCreated = 0, otherUpdated = 0;
- int skipped = 0, failed = 0;
- int billableCandidates = 0, otherCandidates = 0;
- double billableCandidateHours = 0, otherCandidateHours = 0;
- // 3) 每个员工:构造 managerGroups + otherRows,处理 processManagerApproval / processOtherApproval
- for (Map.Entry<String, List<Map>> e : perSubmitter.entrySet()) {
- String uid = e.getKey();
- List<Map> records = e.getValue();
- try {
- CollectResult cr = collectApprovalData(
- uid, monthText, records, approvalCoverage, projectManagerCache);
- if (cr.managerGroups.isEmpty() && cr.otherRows.isEmpty()) {
- skipped++;
- continue;
- }
- // 3a) 项目工时:普通 PM 按人合单,Raymond 兜底按项目拆单
- for (ManagerGroup mg : cr.managerGroups.values()) {
- billableCandidates += mg.billableRows.size() + mg.nonBillableRows.size();
- billableCandidateHours += mg.billableTotal + mg.nonBillableTotal;
- if (dryRun) {
- continue;
- }
- try {
- List<String> actions = processManagerApproval(mg, monthText, uid);
- for (String action : actions) {
- if ("created".equals(action)) {
- billableCreated++;
- } else if ("updated".equals(action)) {
- billableUpdated++;
- }
- }
- } catch (Exception ex) {
- failed++;
- log.error("[补发起] 项目工时发起失败 uid={} pm={} project={} month={}",
- uid, mg.managerId, mg.projectCode, monthText, ex);
- }
- }
- // 3b) 其他工时:一人一张单,approver 用该员工部门主管(记录中带),否则兜底 Raymond
- if (!cr.otherRows.isEmpty()) {
- otherCandidates += cr.otherRows.size();
- for (OtherRow row : cr.otherRows) {
- otherCandidateHours += row.hours;
- }
- if (dryRun) {
- continue;
- }
- try {
- List<String> actions = processOtherApproval(
- cr.otherRows, cr.deptManagerId, monthText, uid);
- for (String action : actions) {
- if ("created".equals(action)) {
- otherCreated++;
- } else if ("updated".equals(action)) {
- otherUpdated++;
- }
- }
- } catch (Exception ex) {
- failed++;
- log.error("[补发起] 其他工时发起失败 uid={} deptMgr={} month={}", uid, cr.deptManagerId, monthText, ex);
- }
- }
- } catch (Exception ex) {
- failed++;
- log.error("[补发起] 员工整月处理失败 uid={} month={}", uid, monthText, ex);
- }
- }
- // 4) 三档状态按汇总总额统一校正;覆盖无缺单但状态为空/错误的历史记录。
- StatusSyncResult statusSync = reconcileSummaryApprovalStatuses(allRecords, dryRun);
- ReSubmitApprovalResult r = ReSubmitApprovalResult.builder()
- .monthText(monthText)
- .submitterCount(perSubmitter.size())
- .dryRun(dryRun)
- .billableCandidates(billableCandidates)
- .billableCandidateHours(billableCandidateHours)
- .otherCandidates(otherCandidates)
- .otherCandidateHours(otherCandidateHours)
- .billableCreated(billableCreated)
- .billableUpdated(billableUpdated)
- .otherCreated(otherCreated)
- .otherUpdated(otherUpdated)
- .skipped(skipped)
- .failed(failed)
- .summaryStatusCandidates(statusSync.candidates)
- .summaryStatusUpdated(statusSync.updated)
- .summaryStatusFailed(statusSync.failed)
- .build();
- log.info("[补发起] 完成 {}", r);
- return r;
- }
- /**
- * 查询最近若干自然日发生修改的汇总记录,并按实际业务月份归集员工。
- * <p>
- * prd 定时兜底只跟踪增量修改,不按最近若干业务月份全量回扫;因此历史月份记录只要近期有变化也会命中。
- *
- * @param lookbackDays 最近自然日数量,最小按 1 天处理
- * @return monthText -> submitterUserId 集合
- */
- @SuppressWarnings("unchecked")
- public Map<String, Set<String>> findRecentlyModifiedTargets(int lookbackDays) {
- LocalDate today = LocalDate.now();
- String[] window = modifiedDateWindow(lookbackDays, today);
- List<Map> records = queryAllModified(
- whConf.getFormUuidWorkHoursSummary(), window[0], window[1]);
- Map<String, Set<String>> targets = new TreeMap<>();
- for (Map record : records) {
- Object rawFormData = record.get("formData");
- if (!(rawFormData instanceof Map)) {
- continue;
- }
- Map<String, Object> formData = (Map<String, Object>) rawFormData;
- String monthText = str(formData, S_MONTH_TEXT);
- String submitterUserId = str(formData, S_SUBMITTER_ID);
- if (!monthText.matches("\\d{6}") || StringUtils.isBlank(submitterUserId)) {
- continue;
- }
- targets.computeIfAbsent(monthText, key -> new LinkedHashSet<>()).add(submitterUserId);
- }
- log.info("[补发起兜底] 增量扫描 from={} to={} records={} months={} targets={}",
- window[0], window[1], records.size(), targets.size(),
- targets.values().stream().mapToInt(Set::size).sum());
- return targets;
- }
- /**
- * 计算最近 N 个自然日的宜搭修改时间查询窗口,结束日加 1 以覆盖当天全天。
- */
- static String[] modifiedDateWindow(int lookbackDays, LocalDate today) {
- int safeDays = Math.max(1, lookbackDays);
- DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
- LocalDate fromDate = today.minusDays(safeDays - 1L);
- LocalDate toDate = today.plusDays(1L);
- return new String[]{fromDate.format(formatter), toDate.format(formatter)};
- }
- /**
- * 使用真人 System 账号发起 TimeCard 工时审批。
- *
- * @param request 发起审批请求
- * @return true 表示宜搭发起接口调用成功
- */
- public boolean startApproval(TimeCardApprovalStartRequest request) {
- McException.assertAccessException(request == null, "请求不能为空");
- McException.assertAccessException(
- StringUtils.isAnyBlank(
- request.getFormUuid(),
- request.getProcessCode(),
- request.getFormDataJson()),
- "审批表单、流程编码和表单数据不能为空");
- boolean billable = StringUtils.equals(whConf.getFormUuidApproval(), request.getFormUuid())
- && StringUtils.equals(whConf.getApprovalProcessCode(), request.getProcessCode());
- boolean other = StringUtils.equals(whConf.getFormUuidOtherApproval(), request.getFormUuid())
- && StringUtils.equals(whConf.getOtherApprovalProcessCode(), request.getProcessCode());
- McException.assertAccessException(!billable && !other, "不允许发起非TimeCard审批流程");
- McException.assertAccessException(
- StringUtils.isBlank(whConf.getApprovalOriginatorUserId()),
- "审批发起真人账号未配置(workhours.approvalOriginatorUserId)");
- Map<String, Object> formData = JSON.parseObject(
- request.getFormDataJson(), Map.class);
- List<Map<String, Object>> shards = billable
- ? splitApprovalStartFormData(formData)
- : splitOtherApprovalStartFormData(formData);
- for (Map<String, Object> shard : shards) {
- startProcessInstance(
- request.getFormUuid(),
- request.getProcessCode(),
- shard,
- request.getDeptId());
- }
- return true;
- }
- private List<Map<String, Object>> splitApprovalStartFormData(Map<String, Object> formData) {
- List<Map> billableRows = listOrEmpty(formData, A_BIL_TABLE);
- List<Map> nonBillableRows = listOrEmpty(formData, A_NON_TABLE);
- List<RowShard> rowShards = splitRowsByHardLimit(billableRows, nonBillableRows);
- List<Map<String, Object>> result = new ArrayList<>();
- for (RowShard rowShard : rowShards) {
- Map<String, Object> shard = new HashMap<>(formData);
- shard.remove(A_BIL_TABLE);
- shard.remove(A_NON_TABLE);
- putRowsIfPresent(shard, A_BIL_TABLE, rowShard.firstRows);
- putRowsIfPresent(shard, A_NON_TABLE, rowShard.secondRows);
- double billableHours = sumHours(rowShard.firstRows, A_BIL_HOURS);
- double nonBillableHours = sumHours(rowShard.secondRows, A_NON_HOURS);
- shard.put(A_PROJECT_HOURS, billableHours);
- shard.put(A_NON_PROJECT_HOURS, nonBillableHours);
- shard.put(A_TOTAL_HOURS, billableHours + nonBillableHours);
- result.add(shard);
- }
- return result;
- }
- private List<Map<String, Object>> splitOtherApprovalStartFormData(Map<String, Object> formData) {
- List<Map> rows = listOrEmpty(formData, O_SUB_TABLE);
- List<RowShard> rowShards = splitRowsByHardLimit(rows, Collections.emptyList());
- List<Map<String, Object>> result = new ArrayList<>();
- for (RowShard rowShard : rowShards) {
- Map<String, Object> shard = new HashMap<>(formData);
- shard.remove(O_SUB_TABLE);
- putRowsIfPresent(shard, O_SUB_TABLE, rowShard.firstRows);
- shard.put(O_TOTAL_HOURS, sumHours(rowShard.firstRows, O_SUB_HOURS));
- result.add(shard);
- }
- return result;
- }
- private void putRowsIfPresent(Map<String, Object> formData,
- String tableFieldId,
- List<Map> rows) {
- if (!rows.isEmpty()) {
- formData.put(tableFieldId, rows);
- }
- }
- private double sumHours(List<Map> rows, String hoursFieldId) {
- double total = 0;
- for (Map row : rows) {
- total += num(row, hoursFieldId);
- }
- return total;
- }
- /**
- * collectApprovalData + collectOtherApprovalData 的等价:
- * 遍历该员工该月每天记录,按项目工时 / 其他工时反构造 delta 行。
- * 普通 PM 按审批人分组,Raymond 兜底按项目拆组;项目编号为空时使用项目名称。
- */
- private static class CollectResult {
- Map<String, ManagerGroup> managerGroups = new LinkedHashMap<>();
- List<OtherRow> otherRows = new ArrayList<>();
- String deptManagerId;
- }
- private static class StatusSyncResult {
- int candidates;
- int updated;
- int failed;
- }
- /**
- * 按主表提交/审批总额把状态统一校正为暂存、审批中或已审批。
- */
- @SuppressWarnings("unchecked")
- private StatusSyncResult reconcileSummaryApprovalStatuses(List<Map> records,
- boolean dryRun) {
- StatusSyncResult result = new StatusSyncResult();
- String fieldId = whConf.getSummaryApprovalStatusField();
- if (StringUtils.isBlank(fieldId)) {
- return result;
- }
- for (Map record : records) {
- Object rawFormData = record.get("formData");
- if (!(rawFormData instanceof Map)) {
- continue;
- }
- Map<String, Object> formData = (Map<String, Object>) rawFormData;
- String targetStatus = expectedSummaryApprovalStatus(formData);
- String currentStatus = str(formData, fieldId);
- String formInstanceId = firstNonBlank(
- str(record, "formInstanceId"),
- str(record, "formInstId"));
- if (StringUtils.isBlank(formInstanceId) || targetStatus.equals(currentStatus)) {
- continue;
- }
- result.candidates++;
- if (dryRun) {
- continue;
- }
- try {
- Map<String, Object> updateData = new HashMap<>();
- updateData.put(fieldId, targetStatus);
- ydClient.operateData(YDParam.builder()
- .appType(whConf.getYidaAppType())
- .systemToken(whConf.getYidaSystemToken())
- .formInstanceId(formInstanceId)
- .updateFormDataJson(JSON.toJSONString(updateData))
- .ignoreEmpty(false)
- .useLatestVersion(true)
- .build(), YDConf.FORM_OPERATION.update);
- result.updated++;
- } catch (Exception ex) {
- result.failed++;
- log.error("[补发起] 汇总三档状态校正失败 formInstanceId={} target={}",
- formInstanceId, targetStatus, ex);
- }
- }
- return result;
- }
- /**
- * 根据汇总主表三类已提交/已审批总额计算状态。
- */
- static String expectedSummaryApprovalStatus(Map<String, Object> formData) {
- double submitted = num(formData, S_BIL_SUBMITTED_TOTAL)
- + num(formData, S_NON_SUBMITTED_TOTAL)
- + num(formData, S_OTHER_SUBMITTED_TOTAL);
- double approved = num(formData, S_BIL_APPROVED_TOTAL)
- + num(formData, S_NON_APPROVED_TOTAL)
- + num(formData, S_OTHER_APPROVED_TOTAL);
- if (submitted > approved + 1e-6) {
- return SUMMARY_STATUS_IN_APPROVAL;
- }
- if (approved > 1e-6) {
- return SUMMARY_STATUS_APPROVED;
- }
- if (submitted > 1e-6) {
- return SUMMARY_STATUS_IN_APPROVAL;
- }
- return SUMMARY_STATUS_DRAFT;
- }
- private CollectResult collectApprovalData(String uid,
- String monthText,
- List<Map> records,
- ApprovalCoverage approvalCoverage,
- Map<String, String> projectManagerCache) {
- CollectResult cr = new CollectResult();
- Set<String> seenBillableKeys = new HashSet<>();
- Set<String> seenNonBillableKeys = new HashSet<>();
- Set<String> seenOtherKeys = new HashSet<>();
- for (Map rec : records) {
- Map fd = (Map) rec.get("formData");
- if (fd == null) {
- continue;
- }
- String dayText = str(fd, S_DAY_TEXT);
- if (StringUtils.isBlank(dayText)) {
- continue;
- }
- long dayTs = numLong(fd, S_DAY_DATE);
- // 部门主管兜底
- if (StringUtils.isBlank(cr.deptManagerId)) {
- cr.deptManagerId = firstIdOrText(fd, S_DEPT_MANAGER);
- }
- // Billable 子表
- for (Map row : listOrEmpty(fd, S_BIL_TABLE)) {
- double submitted = num(row, S_BIL_SUBMITTED);
- double approved = num(row, S_BIL_APPROVED);
- double change = num(row, S_BIL_CHANGE);
- double delta = submitted - approved - change;
- if (delta <= 0) {
- continue;
- }
- String projectCode = str(row, S_BIL_PROJECT_CODE);
- String projectName = firstNonBlank(
- str(row, S_BIL_PROJECT_NAME_TEXT), str(row, S_BIL_PROJECT_NAME));
- String coverageKey = approvalKey("billable", uid, dayText,
- approvalDetailKey(projectCode, projectName));
- if (!seenBillableKeys.add(coverageKey)
- || approvalCoverage.shouldSkip(coverageKey, approved + change)) {
- continue;
- }
- String pmRaw = firstIdOrText(row, S_BIL_MANAGER);
- final String pm = projectManagerCache.computeIfAbsent(
- StringUtils.defaultString(pmRaw), this::resolveProjectManager);
- String groupKey = approvalGroupKey(pm, projectCode, projectName);
- ManagerGroup mg = cr.managerGroups.computeIfAbsent(groupKey, k -> {
- ManagerGroup m = new ManagerGroup();
- m.managerId = pm;
- m.projectCode = DEFAULT_MANAGER_ID.equals(pm) ? projectCode : "";
- m.projectName = DEFAULT_MANAGER_ID.equals(pm) ? projectName : "";
- return m;
- });
- BillRow br = new BillRow();
- br.dayText = dayText;
- br.dayTs = dayTs;
- br.projectCode = projectCode;
- br.projectName = projectName;
- br.hours = delta;
- br.submitterUid = uid;
- br.monthText = monthText;
- mg.billableRows.add(br);
- mg.billableTotal += delta;
- }
- // Non Billable 子表
- for (Map row : listOrEmpty(fd, S_NON_TABLE)) {
- double submitted = num(row, S_NON_SUBMITTED);
- double approved = num(row, S_NON_APPROVED);
- double change = num(row, S_NON_CHANGE);
- double delta = submitted - approved - change;
- if (delta <= 0) {
- continue;
- }
- String projectCode = str(row, S_NON_PROJECT_CODE);
- String projectName = firstNonBlank(
- str(row, S_NON_PROJECT_NAME_TEXT), str(row, S_NON_PROJECT_NAME));
- String coverageKey = approvalKey("nonBillable", uid, dayText,
- approvalDetailKey(projectCode, projectName));
- if (!seenNonBillableKeys.add(coverageKey)
- || approvalCoverage.shouldSkip(coverageKey, approved + change)) {
- continue;
- }
- String pmRaw = firstIdOrText(row, S_NON_MANAGER);
- final String pm = projectManagerCache.computeIfAbsent(
- StringUtils.defaultString(pmRaw), this::resolveProjectManager);
- String groupKey = approvalGroupKey(pm, projectCode, projectName);
- ManagerGroup mg = cr.managerGroups.computeIfAbsent(groupKey, k -> {
- ManagerGroup m = new ManagerGroup();
- m.managerId = pm;
- m.projectCode = DEFAULT_MANAGER_ID.equals(pm) ? projectCode : "";
- m.projectName = DEFAULT_MANAGER_ID.equals(pm) ? projectName : "";
- return m;
- });
- BillRow br = new BillRow();
- br.dayText = dayText;
- br.dayTs = dayTs;
- br.projectCode = projectCode;
- br.projectName = projectName;
- br.hours = delta;
- br.submitterUid = uid;
- br.monthText = monthText;
- mg.nonBillableRows.add(br);
- mg.nonBillableTotal += delta;
- }
- // 其他工时子表
- for (Map row : listOrEmpty(fd, S_OTH_TABLE)) {
- double submitted = num(row, S_OTH_SUBMITTED);
- double approved = num(row, S_OTH_APPROVED);
- double change = num(row, S_OTH_CHANGE);
- double delta = submitted - approved - change;
- if (delta <= 0) {
- continue;
- }
- String activity = str(row, S_OTH_ACTIVITY);
- String coverageKey = approvalKey("other", uid, dayText,
- approvalDetailKey(activity, ""));
- if (!seenOtherKeys.add(coverageKey)
- || approvalCoverage.shouldSkip(coverageKey, approved + change)) {
- continue;
- }
- OtherRow or = new OtherRow();
- or.dayText = dayText;
- or.dayTs = dayTs;
- or.activity = activity;
- or.hours = delta;
- or.submitterUid = uid;
- or.monthText = monthText;
- cr.otherRows.add(or);
- }
- }
- if (StringUtils.isBlank(cr.deptManagerId)) {
- cr.deptManagerId = DEFAULT_MANAGER_ID;
- }
- return cr;
- }
- /**
- * 生成项目工时审批分组键。
- *
- * @param managerId 审批人 userId
- * @param projectCode 项目编号
- * @param projectName 项目名称
- * @return 普通 PM 返回 managerId;Raymond 返回项目隔离键,编号为空时用名称
- */
- static String approvalGroupKey(String managerId, String projectCode, String projectName) {
- if (DEFAULT_MANAGER_ID.equals(managerId)) {
- String projectIdentity = StringUtils.isNotBlank(projectCode)
- ? projectCode
- : "name:" + StringUtils.defaultString(projectName);
- return managerId + "|project|" + projectIdentity;
- }
- return managerId;
- }
- /**
- * prd 项目经理离职时,改由人员档案中的在职主管审批;仅在整条替补链路不可用时兜底 Raymond。
- */
- String resolveProjectManager(String managerId) {
- if (StringUtils.isBlank(managerId) || DEFAULT_MANAGER_ID.equals(managerId)) {
- return DEFAULT_MANAGER_ID;
- }
- String accessToken;
- try {
- accessToken = ddClient.getAccessToken();
- } catch (Exception ex) {
- log.warn("[补发起] 获取钉钉 accessToken 失败,项目经理兜底 Raymond pm={} err={}",
- managerId, ex.getMessage());
- return DEFAULT_MANAGER_ID;
- }
- if (isUserInService(accessToken, managerId)) {
- return managerId;
- }
- String supervisorId;
- try {
- supervisorId = queryPersonnelManagerId(managerId);
- } catch (Exception ex) {
- log.warn("[补发起] 查询离职 PM 人员档案主管失败,兜底 Raymond pm={} err={}",
- managerId, ex.getMessage());
- return DEFAULT_MANAGER_ID;
- }
- if (StringUtils.isBlank(supervisorId)) {
- log.warn("[补发起] 离职 PM 未配置人员档案主管,兜底 Raymond pm={}", managerId);
- return DEFAULT_MANAGER_ID;
- }
- if (DEFAULT_MANAGER_ID.equals(supervisorId)
- || isUserInService(accessToken, supervisorId)) {
- log.info("[补发起] 离职 PM 改由人员档案主管审批 pm={} supervisor={}",
- managerId, supervisorId);
- return supervisorId;
- }
- log.warn("[补发起] 离职 PM 的人员档案主管也不在职,兜底 Raymond pm={} supervisor={}",
- managerId, supervisorId);
- return DEFAULT_MANAGER_ID;
- }
- private boolean isUserInService(String accessToken, String userId) {
- try {
- Map user = ddClient_contacts.getUserInfoById(accessToken, userId);
- return user != null && StringUtils.isNotBlank(str(user, "userid"));
- } catch (Exception ex) {
- log.info("[补发起] 钉钉用户不在职或查询失败 userId={} err={}", userId, ex.getMessage());
- return false;
- }
- }
- private String queryPersonnelManagerId(String userId) {
- Map<String, Object> search = new HashMap<>();
- search.put(PERSONNEL_EMPLOYEE, Collections.singletonList(userId));
- DDR_New response = ydClient.queryData(YDParam.builder()
- .appType(whConf.getYidaAppType())
- .systemToken(whConf.getYidaSystemToken())
- .formUuid(whConf.getFormUuidPersonnel())
- .searchFieldJson(JSON.toJSONString(search))
- .currentPage(1)
- .pageSize(1)
- .build(), YDConf.FORM_QUERY.retrieve_search_form);
- List<Map> records = response == null ? null : (List<Map>) response.getData();
- if (records == null || records.isEmpty()) {
- return "";
- }
- Object formData = records.get(0).get("formData");
- if (!(formData instanceof Map)) {
- return "";
- }
- return firstEmployeeId((Map) formData, PERSONNEL_MANAGER);
- }
- /**
- * 加载指定月份两类审批单的完整明细覆盖索引。
- * fixme 流程列表内联子表恰好 50 行时必须 queryDetails 拉全,否则会把第 51 行以后误判为漏单。
- */
- private ApprovalCoverage loadApprovalCoverage(String monthText) {
- ApprovalCoverage coverage = new ApprovalCoverage();
- Map<String, Object> search = new HashMap<>();
- search.put(A_MONTH_TEXT, monthText);
- for (Map item : queryProcessAll(whConf.getFormUuidApproval(), search)) {
- collectProcessCoverage(item, false, coverage);
- }
- for (Map item : queryProcessAll(whConf.getFormUuidOtherApproval(), search)) {
- collectProcessCoverage(item, true, coverage);
- }
- log.info("[补发起] 审批覆盖索引 month={} runningKeys={} completedAgreeKeys={}",
- monthText, coverage.runningKeys.size(), coverage.completedAgreeHours.size());
- return coverage;
- }
- /**
- * 把单张审批单明细加入覆盖索引。
- */
- private void collectProcessCoverage(Map item, boolean isOther, ApprovalCoverage coverage) {
- String status = str(item, "instanceStatus").toUpperCase(Locale.ROOT);
- if (!"RUNNING".equals(status) && !"COMPLETED".equals(status)) {
- return;
- }
- String approvedResult = str(item, "approvedResult").toLowerCase(Locale.ROOT);
- String formInstanceId = firstNonBlank(
- str(item, "formInstanceId"),
- str(item, "processInstanceId"));
- Map formData = processFormData(item, formInstanceId);
- if (formData.isEmpty()) {
- return;
- }
- if (isOther) {
- collectCoverageRows(
- coverage,
- status,
- approvedResult,
- resolveFullRows(
- formInstanceId,
- whConf.getFormUuidOtherApproval(),
- O_SUB_TABLE,
- (List<Map>) formData.get(O_SUB_TABLE)),
- "other",
- O_SUB_SUBMITTER_UID,
- O_SUB_DAY_TEXT,
- O_SUB_ACTIVITY,
- null,
- O_SUB_HOURS,
- O_SUB_OPINION);
- return;
- }
- collectCoverageRows(
- coverage,
- status,
- approvedResult,
- resolveFullRows(
- formInstanceId,
- whConf.getFormUuidApproval(),
- A_BIL_TABLE,
- (List<Map>) formData.get(A_BIL_TABLE)),
- "billable",
- A_BIL_SUBMITTER_UID,
- A_BIL_DAY_TEXT,
- A_BIL_PROJECT_CODE,
- A_BIL_PROJECT_NAME,
- A_BIL_HOURS,
- A_BIL_OPINION);
- collectCoverageRows(
- coverage,
- status,
- approvedResult,
- resolveFullRows(
- formInstanceId,
- whConf.getFormUuidApproval(),
- A_NON_TABLE,
- (List<Map>) formData.get(A_NON_TABLE)),
- "nonBillable",
- A_NON_SUBMITTER_UID,
- A_NON_DAY_TEXT,
- A_NON_PROJECT_CODE,
- A_NON_PROJECT_NAME,
- A_NON_HOURS,
- A_NON_OPINION);
- }
- /**
- * 归一化一类审批子表行并写入覆盖索引。
- */
- private void collectCoverageRows(ApprovalCoverage coverage,
- String status,
- String approvedResult,
- List<Map> rows,
- String category,
- String uidField,
- String dayField,
- String detailKeyField,
- String detailFallbackField,
- String hoursField,
- String opinionField) {
- for (Map row : rows) {
- String uid = str(row, uidField);
- String day = str(row, dayField);
- String detailKey = approvalDetailKey(
- str(row, detailKeyField),
- detailFallbackField == null ? "" : str(row, detailFallbackField));
- if (StringUtils.isAnyBlank(uid, day, detailKey)) {
- continue;
- }
- String key = approvalKey(category, uid, day, detailKey);
- if ("RUNNING".equals(status)) {
- coverage.runningKeys.add(key);
- continue;
- }
- if (isCompletedApprovedRow(
- status,
- approvedResult,
- str(row, opinionField))) {
- coverage.completedAgreeHours.merge(key, num(row, hoursField), Math::max);
- }
- }
- }
- static boolean isCompletedApprovedRow(String status,
- String approvedResult,
- String opinion) {
- return "COMPLETED".equals(status)
- && "agree".equals(approvedResult)
- && (StringUtils.isBlank(opinion) || APPROVAL_OPINION_AGREE.equals(opinion));
- }
- /**
- * 获取流程列表项中的表单数据;列表不带数据时回查表单详情。
- */
- private Map processFormData(Map item, String formInstanceId) {
- Object formData = item.get("formData");
- if (!(formData instanceof Map)) {
- formData = item.get("data");
- }
- if (formData instanceof Map && !((Map) formData).isEmpty()) {
- return (Map) formData;
- }
- if (StringUtils.isBlank(formInstanceId)) {
- return Collections.emptyMap();
- }
- Map detail = ydClient.queryData(YDParam.builder()
- .appType(whConf.getYidaAppType())
- .systemToken(whConf.getYidaSystemToken())
- .formInstanceId(formInstanceId)
- .build(), YDConf.FORM_QUERY.retrieve_id).getFormData();
- return detail != null ? detail : Collections.emptyMap();
- }
- /**
- * 子表内联小于 50 行直接使用,恰好 50 行时递归拉全。
- */
- private List<Map> resolveFullRows(String formInstanceId,
- String formUuid,
- String tableFieldId,
- List<Map> inlineRows) {
- if (inlineRows == null) {
- return Collections.emptyList();
- }
- if (inlineRows.size() < 50) {
- return inlineRows;
- }
- List<Map> fullRows = ydService.queryDetails(YDParam.builder()
- .appType(whConf.getYidaAppType())
- .systemToken(whConf.getYidaSystemToken())
- .formUuid(formUuid)
- .formInstanceId(formInstanceId)
- .tableFieldId(tableFieldId)
- .pageNumber(1)
- .build());
- return fullRows != null ? fullRows : Collections.emptyList();
- }
- /**
- * 审批明细唯一键。
- */
- static String approvalKey(String category, String uid, String day, String detailKey) {
- return category + "|" + uid + "|" + day + "|" + detailKey;
- }
- /**
- * 审批明细业务标识优先使用编号;历史数据编号为空时退回项目名称。
- *
- * @param primary 编号或活动类型
- * @param fallback 项目名称兜底
- * @return 带来源前缀的稳定业务标识
- */
- static String approvalDetailKey(String primary, String fallback) {
- if (StringUtils.isNotBlank(primary)) {
- return "id:" + primary;
- }
- return "name:" + StringUtils.defaultString(fallback);
- }
- /**
- * processManagerApproval 等价 — queryProcess(RUNNING) 有则 update / 无则 create。
- * COMPLETED 覆盖已在 collectApprovalData 前过滤,避免历史已完成单被重复发起。
- */
- private List<String> processManagerApproval(ManagerGroup mg,
- String monthText,
- String submitterUid) {
- List<String> actions = new ArrayList<>();
- for (ManagerGroup shard : splitManagerGroupByHardLimit(mg)) {
- actions.add(processManagerApprovalShard(shard, monthText, submitterUid));
- }
- return actions;
- }
- private String processManagerApprovalShard(ManagerGroup mg,
- String monthText,
- String submitterUid) {
- Map<String, Object> searchFieldJson = new HashMap<>();
- searchFieldJson.put(A_MONTH_TEXT, monthText);
- // fixme employeeField 查询用数组: 单值 [uid]
- searchFieldJson.put(A_MANAGER, Collections.singletonList(mg.managerId));
- List<Map> running = queryProcessRunning(whConf.getFormUuidApproval(), searchFieldJson);
- // 优先追加到最新分片,避免每名员工都先扫描已满 500 行的旧审批单。
- running.sort(Comparator.comparingLong(
- (Map item) -> numLong(item, "gmtModified")).reversed());
- for (Map item : running) {
- String pid = str(item, "processInstanceId");
- String finstId = firstNonBlank(str(item, "formInstanceId"), pid);
- Map existingFormData = processFormData(item, finstId);
- if (!managerApprovalHasCapacity(finstId, mg, submitterUid, monthText, existingFormData)) {
- continue;
- }
- if (updateApprovalInstance(finstId, mg, monthText, submitterUid, existingFormData)) {
- return "updated";
- }
- }
- // fixme: 单张审批子表达到 500 行后必须新建分片,继续覆盖会丢失第 501 行后的明细。
- createApprovalInstance(mg, monthText, submitterUid);
- return "created";
- }
- private boolean managerApprovalHasCapacity(String formInstanceId,
- ManagerGroup mg,
- String submitterUid,
- String monthText,
- Map existingFormData) {
- List<Map> existBil = resolveFullRows(
- formInstanceId,
- whConf.getFormUuidApproval(),
- A_BIL_TABLE,
- (List<Map>) (existingFormData == null ? null : existingFormData.get(A_BIL_TABLE)));
- List<Map> existNon = resolveFullRows(
- formInstanceId,
- whConf.getFormUuidApproval(),
- A_NON_TABLE,
- (List<Map>) (existingFormData == null ? null : existingFormData.get(A_NON_TABLE)));
- MergeResult billable = mergeSubRows(
- existBil, mg.billableRows, submitterUid, monthText,
- A_BIL_SUBMITTER_UID, A_BIL_DAY_TEXT, A_BIL_PROJECT_CODE,
- br -> buildBillableApprovalRow(br, submitterUid));
- MergeResult nonBillable = mergeSubRows(
- existNon, mg.nonBillableRows, submitterUid, monthText,
- A_NON_SUBMITTER_UID, A_NON_DAY_TEXT, A_NON_PROJECT_CODE,
- br -> buildNonBillableApprovalRow(br, submitterUid));
- int existingBillableCount = countUniqueRows(
- existBil, A_BIL_SUBMITTER_UID, A_BIL_DAY_TEXT, A_BIL_PROJECT_CODE);
- int existingNonBillableCount = countUniqueRows(
- existNon, A_NON_SUBMITTER_UID, A_NON_DAY_TEXT, A_NON_PROJECT_CODE);
- if (!approvalMatchesProject(existBil, existNon, mg.projectCode, mg.projectName)) {
- return false;
- }
- int existingCount = existingBillableCount + existingNonBillableCount;
- int mergedCount = billable.rows.size() + nonBillable.rows.size();
- return canAppendToApprovalShard(existingCount, mergedCount);
- }
- /**
- * 创建新的工时审批实例(startProcessInstance)
- */
- private void createApprovalInstance(ManagerGroup mg, String monthText, String submitterUid) {
- McException.assertAccessException(
- !hasSubtableCapacity(
- mg.billableRows.size() + mg.nonBillableRows.size()),
- "单张工时审批明细不能超过 500 行");
- Map<String, Object> formData = new HashMap<>();
- formData.put(A_MONTH_DATE, monthToTs(monthText));
- formData.put(A_MONTH_TEXT, monthText);
- formData.put(A_MANAGER, Collections.singletonList(mg.managerId));
- formData.put(A_PROJECT_HOURS, mg.billableTotal);
- formData.put(A_NON_PROJECT_HOURS, mg.nonBillableTotal);
- formData.put(A_TOTAL_HOURS, mg.billableTotal + mg.nonBillableTotal);
- formData.put(A_VIEWERS, Collections.singletonList(submitterUid));
- List<Map<String, Object>> billableSub = new ArrayList<>();
- for (BillRow br : mg.billableRows) {
- billableSub.add(buildBillableApprovalRow(br, submitterUid));
- }
- List<Map<String, Object>> nonBillableSub = new ArrayList<>();
- for (BillRow br : mg.nonBillableRows) {
- nonBillableSub.add(buildNonBillableApprovalRow(br, submitterUid));
- }
- if (!billableSub.isEmpty()) {
- formData.put(A_BIL_TABLE, billableSub);
- }
- if (!nonBillableSub.isEmpty()) {
- formData.put(A_NON_TABLE, nonBillableSub);
- }
- startProcessInstance(
- whConf.getFormUuidApproval(),
- whConf.getApprovalProcessCode(),
- formData,
- whConf.getApprovalOriginatorDeptId());
- log.info("[补发起] 新建工时审批单 submitterUid={} pm={} project={} month={} bilTotal={} nonBilTotal={} rows={}+{}",
- submitterUid, mg.managerId, mg.projectCode, monthText,
- mg.billableTotal, mg.nonBillableTotal, billableSub.size(), nonBillableSub.size());
- }
- /**
- * 累计更新已有 RUNNING 工时审批单(按唯一键 userId + dayText + projectCode 去重合并)
- */
- private boolean updateApprovalInstance(String formInstanceId, ManagerGroup mg,
- String monthText, String submitterUid,
- Map existingFormData) {
- List<Map> existBil = resolveFullRows(
- formInstanceId,
- whConf.getFormUuidApproval(),
- A_BIL_TABLE,
- (List<Map>) (existingFormData == null ? null : existingFormData.get(A_BIL_TABLE)));
- List<Map> existNon = resolveFullRows(
- formInstanceId,
- whConf.getFormUuidApproval(),
- A_NON_TABLE,
- (List<Map>) (existingFormData == null ? null : existingFormData.get(A_NON_TABLE)));
- MergeResult mrBil = mergeSubRows(existBil, mg.billableRows, submitterUid, monthText,
- A_BIL_SUBMITTER_UID, A_BIL_DAY_TEXT, A_BIL_PROJECT_CODE,
- (br) -> buildBillableApprovalRow(br, submitterUid));
- MergeResult mrNon = mergeSubRows(existNon, mg.nonBillableRows, submitterUid, monthText,
- A_NON_SUBMITTER_UID, A_NON_DAY_TEXT, A_NON_PROJECT_CODE,
- (br) -> buildNonBillableApprovalRow(br, submitterUid));
- if (!hasSubtableCapacity(mrBil.rows.size() + mrNon.rows.size())) {
- return false;
- }
- if (mrBil.addedHours == 0 && mrNon.addedHours == 0) {
- log.info("[补发起] 已存在等效明细,跳过更新 formInstanceId={}", formInstanceId);
- return true;
- }
- double existProject = num(existingFormData, A_PROJECT_HOURS);
- double existNonProject = num(existingFormData, A_NON_PROJECT_HOURS);
- double newProject = existProject + mrBil.addedHours;
- double newNonProject = existNonProject + mrNon.addedHours;
- Map<String, Object> updateData = new HashMap<>();
- updateData.put(A_PROJECT_HOURS, newProject);
- updateData.put(A_NON_PROJECT_HOURS, newNonProject);
- updateData.put(A_TOTAL_HOURS, newProject + newNonProject);
- if (!mrBil.rows.isEmpty()) {
- updateData.put(A_BIL_TABLE, mrBil.rows);
- }
- if (!mrNon.rows.isEmpty()) {
- updateData.put(A_NON_TABLE, mrNon.rows);
- }
- // Viewers 去重(原有 + 本次申请人)
- Set<String> viewerSet = new LinkedHashSet<>();
- Object existViewersRaw = existingFormData == null ? null : existingFormData.get(A_VIEWERS + "_id");
- if (existViewersRaw instanceof List) {
- for (Object v : (List<?>) existViewersRaw) {
- if (v != null) {
- viewerSet.add(String.valueOf(v));
- }
- }
- }
- // 从原有子表行提取填报人
- for (Map r : new ArrayList<Map>(existBil == null ? Collections.emptyList() : existBil)) {
- String u = str(r, A_BIL_SUBMITTER_UID);
- if (StringUtils.isNotBlank(u)) {
- viewerSet.add(u);
- }
- }
- for (Map r : new ArrayList<Map>(existNon == null ? Collections.emptyList() : existNon)) {
- String u = str(r, A_NON_SUBMITTER_UID);
- if (StringUtils.isNotBlank(u)) {
- viewerSet.add(u);
- }
- }
- if (StringUtils.isNotBlank(submitterUid)) {
- viewerSet.add(submitterUid);
- }
- updateData.put(A_VIEWERS, new ArrayList<>(viewerSet));
- ydClient.operateData(YDParam.builder()
- .appType(whConf.getYidaAppType())
- .systemToken(whConf.getYidaSystemToken())
- .formInstanceId(formInstanceId)
- .updateFormDataJson(JSON.toJSONString(updateData))
- .ignoreEmpty(false)
- .useLatestVersion(true)
- .build(), YDConf.FORM_OPERATION.update);
- log.info("[补发起] 累计更新工时审批单 formInstanceId={} pm={} project={} addedBil={} addedNon={}",
- formInstanceId, mg.managerId, mg.projectCode, mrBil.addedHours, mrNon.addedHours);
- return true;
- }
- /**
- * 判断已有 Raymond 审批单是否只包含目标项目。
- *
- * @param billableRows Billable 明细
- * @param nonBillableRows Non Billable 明细
- * @param projectCode Raymond 组的项目编号
- * @param projectName Raymond 组的项目名称;编号为空时用于项目隔离,两者均空表示普通 PM 组
- * @return 可否追加到该审批单
- */
- static boolean approvalMatchesProject(List<Map> billableRows,
- List<Map> nonBillableRows,
- String projectCode,
- String projectName) {
- if (StringUtils.isBlank(projectCode) && StringUtils.isBlank(projectName)) {
- return true;
- }
- int rowCount = 0;
- for (Map row : billableRows) {
- rowCount++;
- if (!matchesProject(row, A_BIL_PROJECT_CODE, A_BIL_PROJECT_NAME, projectCode, projectName)) {
- return false;
- }
- }
- for (Map row : nonBillableRows) {
- rowCount++;
- if (!matchesProject(row, A_NON_PROJECT_CODE, A_NON_PROJECT_NAME, projectCode, projectName)) {
- return false;
- }
- }
- return rowCount > 0;
- }
- private static boolean matchesProject(Map row,
- String codeField,
- String nameField,
- String projectCode,
- String projectName) {
- String rowProjectCode = str(row, codeField);
- if (StringUtils.isNotBlank(projectCode)) {
- return projectCode.equals(rowProjectCode);
- }
- return StringUtils.isBlank(rowProjectCode) && StringUtils.equals(projectName, str(row, nameField));
- }
- /**
- * 其他工时审批处理(与 processManagerApproval 同构,但 approver 为员工的部门主管)
- */
- private List<String> processOtherApproval(List<OtherRow> otherRows,
- String deptManagerId,
- String monthText,
- String submitterUid) {
- List<String> actions = new ArrayList<>();
- for (List<OtherRow> shard : splitListByHardLimit(otherRows)) {
- actions.add(processOtherApprovalShard(
- shard, deptManagerId, monthText, submitterUid));
- }
- return actions;
- }
- private String processOtherApprovalShard(List<OtherRow> otherRows,
- String deptManagerId,
- String monthText,
- String submitterUid) {
- Map<String, Object> searchFieldJson = new HashMap<>();
- searchFieldJson.put(O_MONTH_TEXT, monthText);
- searchFieldJson.put(O_DEPT_MANAGER, Collections.singletonList(deptManagerId));
- List<Map> running = queryProcessRunning(whConf.getFormUuidOtherApproval(), searchFieldJson);
- running.sort(Comparator.comparingLong(
- (Map item) -> numLong(item, "gmtModified")).reversed());
- for (Map item : running) {
- String pid = str(item, "processInstanceId");
- String finstId = firstNonBlank(str(item, "formInstanceId"), pid);
- Map existingFormData = processFormData(item, finstId);
- if (!otherApprovalHasCapacity(finstId, otherRows, submitterUid, existingFormData)) {
- continue;
- }
- if (updateOtherApprovalInstance(
- finstId, otherRows, monthText, submitterUid, existingFormData)) {
- return "updated";
- }
- }
- createOtherApprovalInstance(otherRows, deptManagerId, monthText, submitterUid);
- return "created";
- }
- private boolean otherApprovalHasCapacity(String formInstanceId,
- List<OtherRow> otherRows,
- String submitterUid,
- Map existingFormData) {
- List<Map> existing = resolveFullRows(
- formInstanceId,
- whConf.getFormUuidOtherApproval(),
- O_SUB_TABLE,
- (List<Map>) (existingFormData == null ? null : existingFormData.get(O_SUB_TABLE)));
- Set<String> keys = new HashSet<>();
- for (Map row : existing) {
- keys.add(str(row, O_SUB_SUBMITTER_UID) + "|"
- + str(row, O_SUB_DAY_TEXT) + "|"
- + str(row, O_SUB_ACTIVITY));
- }
- int existingCount = keys.size();
- for (OtherRow row : otherRows) {
- keys.add(submitterUid + "|" + row.dayText + "|" + row.activity);
- }
- return canAppendToApprovalShard(existingCount, keys.size());
- }
- private void createOtherApprovalInstance(List<OtherRow> otherRows, String deptManagerId,
- String monthText, String submitterUid) {
- McException.assertAccessException(
- !hasSubtableCapacity(otherRows.size()),
- "单个员工的其他工时审批明细超过宜搭子表 500 行限制");
- double total = 0;
- List<Map<String, Object>> sub = new ArrayList<>();
- for (OtherRow or : otherRows) {
- sub.add(buildOtherApprovalRow(or, submitterUid));
- total += or.hours;
- }
- Map<String, Object> formData = new HashMap<>();
- formData.put(O_MONTH_DATE, monthToTs(monthText));
- formData.put(O_MONTH_TEXT, monthText);
- formData.put(O_DEPT_MANAGER, Collections.singletonList(deptManagerId));
- formData.put(O_TOTAL_HOURS, total);
- formData.put(O_VIEWERS, Collections.singletonList(submitterUid));
- if (!sub.isEmpty()) {
- formData.put(O_SUB_TABLE, sub);
- }
- startProcessInstance(
- whConf.getFormUuidOtherApproval(),
- whConf.getOtherApprovalProcessCode(),
- formData,
- whConf.getApprovalOriginatorDeptId());
- log.info("[补发起] 新建其他工时审批单 submitterUid={} deptMgr={} month={} total={} rows={}",
- submitterUid, deptManagerId, monthText, total, sub.size());
- }
- private boolean updateOtherApprovalInstance(String formInstanceId,
- List<OtherRow> otherRows,
- String monthText,
- String submitterUid,
- Map existingFormData) {
- List<Map> existRows = resolveFullRows(
- formInstanceId,
- whConf.getFormUuidOtherApproval(),
- O_SUB_TABLE,
- (List<Map>) (existingFormData == null ? null : existingFormData.get(O_SUB_TABLE)));
- // 唯一键: submitterUid + dayText + activity
- Set<String> existingKeys = new HashSet<>();
- List<Map<String, Object>> merged = new ArrayList<>();
- if (existRows != null) {
- for (Map row : existRows) {
- String uid = str(row, O_SUB_SUBMITTER_UID);
- String day = str(row, O_SUB_DAY_TEXT);
- String act = str(row, O_SUB_ACTIVITY);
- existingKeys.add(uid + "|" + day + "|" + act);
- merged.add(rebuildRow(row, O_SUB_SUBMITTER));
- }
- }
- double addedHours = 0;
- for (OtherRow or : otherRows) {
- String key = submitterUid + "|" + or.dayText + "|" + or.activity;
- if (existingKeys.contains(key)) {
- continue;
- }
- merged.add(buildOtherApprovalRow(or, submitterUid));
- addedHours += or.hours;
- }
- if (!hasSubtableCapacity(merged.size())) {
- return false;
- }
- if (addedHours == 0) {
- log.info("[补发起] 其他工时无新增明细 formInstanceId={}", formInstanceId);
- return true;
- }
- double existTotal = num(existingFormData, O_TOTAL_HOURS);
- Map<String, Object> updateData = new HashMap<>();
- updateData.put(O_TOTAL_HOURS, existTotal + addedHours);
- updateData.put(O_SUB_TABLE, merged);
- Set<String> viewerSet = new LinkedHashSet<>();
- Object existOViewersRaw = existingFormData == null ? null : existingFormData.get(O_VIEWERS + "_id");
- if (existOViewersRaw instanceof List) {
- for (Object v : (List<?>) existOViewersRaw) {
- if (v != null) {
- viewerSet.add(String.valueOf(v));
- }
- }
- }
- if (existRows != null) {
- for (Map r : existRows) {
- String u = str(r, O_SUB_SUBMITTER_UID);
- if (StringUtils.isNotBlank(u)) {
- viewerSet.add(u);
- }
- }
- }
- if (StringUtils.isNotBlank(submitterUid)) {
- viewerSet.add(submitterUid);
- }
- updateData.put(O_VIEWERS, new ArrayList<>(viewerSet));
- ydClient.operateData(YDParam.builder()
- .appType(whConf.getYidaAppType())
- .systemToken(whConf.getYidaSystemToken())
- .formInstanceId(formInstanceId)
- .updateFormDataJson(JSON.toJSONString(updateData))
- .ignoreEmpty(false)
- .useLatestVersion(true)
- .build(), YDConf.FORM_OPERATION.update);
- log.info("[补发起] 累计更新其他工时审批单 formInstanceId={} addedHours={}", formInstanceId, addedHours);
- return true;
- }
- // ============================================================
- // 子表行构造 + 合并 helper
- // ============================================================
- private Map<String, Object> buildBillableApprovalRow(BillRow br, String submitterUid) {
- Map<String, Object> r = new HashMap<>();
- r.put(A_BIL_WORK_DATE, br.dayTs);
- r.put(A_BIL_SUBMITTER, Collections.singletonList(submitterUid));
- r.put(A_BIL_SUBMITTER_UID, submitterUid);
- r.put(A_BIL_PROJECT_CODE, br.projectCode);
- r.put(A_BIL_PROJECT_NAME, br.projectName);
- r.put(A_BIL_HOURS, br.hours);
- r.put(A_BIL_MONTH_TEXT, br.monthText);
- r.put(A_BIL_DAY_TEXT, br.dayText);
- r.put(A_BIL_OPINION, APPROVAL_OPINION_AGREE);
- return r;
- }
- private Map<String, Object> buildNonBillableApprovalRow(BillRow br, String submitterUid) {
- Map<String, Object> r = new HashMap<>();
- r.put(A_NON_WORK_DATE, br.dayTs);
- r.put(A_NON_SUBMITTER, Collections.singletonList(submitterUid));
- r.put(A_NON_SUBMITTER_UID, submitterUid);
- r.put(A_NON_PROJECT_CODE, br.projectCode);
- r.put(A_NON_PROJECT_NAME, br.projectName);
- r.put(A_NON_HOURS, br.hours);
- r.put(A_NON_MONTH_TEXT, br.monthText);
- r.put(A_NON_DAY_TEXT, br.dayText);
- r.put(A_NON_OPINION, APPROVAL_OPINION_AGREE);
- return r;
- }
- private Map<String, Object> buildOtherApprovalRow(OtherRow or, String submitterUid) {
- Map<String, Object> r = new HashMap<>();
- r.put(O_SUB_WORK_DATE, or.dayTs);
- r.put(O_SUB_SUBMITTER, Collections.singletonList(submitterUid));
- r.put(O_SUB_SUBMITTER_UID, submitterUid);
- r.put(O_SUB_ACTIVITY, or.activity);
- r.put(O_SUB_HOURS, or.hours);
- r.put(O_SUB_MONTH_TEXT, or.monthText);
- r.put(O_SUB_DAY_TEXT, or.dayText);
- r.put(O_SUB_OPINION, APPROVAL_OPINION_AGREE);
- return r;
- }
- private interface RowBuilder<T> {
- Map<String, Object> build(T row);
- }
- private static class MergeResult {
- List<Map<String, Object>> rows = new ArrayList<>();
- double addedHours = 0;
- }
- private static class RowShard {
- List<Map> firstRows = new ArrayList<>();
- List<Map> secondRows = new ArrayList<>();
- int size() {
- return firstRows.size() + secondRows.size();
- }
- }
- private List<RowShard> splitRowsByHardLimit(List<Map> firstRows, List<Map> secondRows) {
- List<RowShard> shards = new ArrayList<>();
- shards.add(new RowShard());
- for (Map row : firstRows) {
- appendRowToShard(shards, row, true);
- }
- for (Map row : secondRows) {
- appendRowToShard(shards, row, false);
- }
- return shards;
- }
- private void appendRowToShard(List<RowShard> shards, Map row, boolean firstTable) {
- RowShard current = shards.get(shards.size() - 1);
- if (current.size() >= MAX_APPROVAL_SUBTABLE_ROWS) {
- current = new RowShard();
- shards.add(current);
- }
- (firstTable ? current.firstRows : current.secondRows).add(row);
- }
- private List<ManagerGroup> splitManagerGroupByHardLimit(ManagerGroup source) {
- if (source.billableRows.size() + source.nonBillableRows.size()
- <= MAX_APPROVAL_SUBTABLE_ROWS) {
- return Collections.singletonList(source);
- }
- List<ManagerGroup> shards = new ArrayList<>();
- shards.add(newManagerGroup(source.managerId, source.projectCode, source.projectName));
- for (BillRow row : source.billableRows) {
- appendManagerRow(shards, row, true);
- }
- for (BillRow row : source.nonBillableRows) {
- appendManagerRow(shards, row, false);
- }
- return shards;
- }
- private ManagerGroup newManagerGroup(String managerId, String projectCode, String projectName) {
- ManagerGroup group = new ManagerGroup();
- group.managerId = managerId;
- group.projectCode = projectCode;
- group.projectName = projectName;
- return group;
- }
- private void appendManagerRow(List<ManagerGroup> shards, BillRow row, boolean billable) {
- ManagerGroup current = shards.get(shards.size() - 1);
- if (current.billableRows.size() + current.nonBillableRows.size()
- >= MAX_APPROVAL_SUBTABLE_ROWS) {
- current = newManagerGroup(current.managerId, current.projectCode, current.projectName);
- shards.add(current);
- }
- if (billable) {
- current.billableRows.add(row);
- current.billableTotal += row.hours;
- } else {
- current.nonBillableRows.add(row);
- current.nonBillableTotal += row.hours;
- }
- }
- private <T> List<List<T>> splitListByHardLimit(List<T> rows) {
- List<List<T>> shards = new ArrayList<>();
- for (int from = 0; from < rows.size(); from += MAX_APPROVAL_SUBTABLE_ROWS) {
- int to = Math.min(from + MAX_APPROVAL_SUBTABLE_ROWS, rows.size());
- shards.add(new ArrayList<>(rows.subList(from, to)));
- }
- return shards;
- }
- static boolean hasSubtableCapacity(int rowCount) {
- return rowCount <= MAX_APPROVAL_SUBTABLE_ROWS;
- }
- static boolean canAppendToApprovalShard(int existingCount, int mergedCount) {
- if (!hasSubtableCapacity(mergedCount)) {
- return false;
- }
- // 已有等效明细允许幂等命中;空分片允许承载一个员工的完整明细组。
- return mergedCount == existingCount
- || existingCount == 0
- || mergedCount <= TARGET_APPROVAL_SUBTABLE_ROWS;
- }
- private int countUniqueRows(List<Map> rows,
- String uidField,
- String dayField,
- String keyField) {
- Set<String> keys = new HashSet<>();
- for (Map row : rows) {
- keys.add(str(row, uidField) + "|"
- + str(row, dayField) + "|"
- + str(row, keyField));
- }
- return keys.size();
- }
- /**
- * mergeApprovalSubRows 等价:唯一键 uid + dayText + projectCode/activity,已存在跳过
- * 保留原行时用 rebuildRow 修正 EmployeeField 主键(_id 后缀 → 主键)
- */
- private MergeResult mergeSubRows(List<Map> existing, List<BillRow> newRows, String submitterUid, String monthText,
- String uidField, String dayField, String keyField, RowBuilder<BillRow> builder) {
- MergeResult r = new MergeResult();
- Set<String> keys = new HashSet<>();
- if (existing != null) {
- for (Map row : existing) {
- String uid = str(row, uidField);
- String day = str(row, dayField);
- String key = str(row, keyField);
- if (!keys.add(uid + "|" + day + "|" + key)) {
- continue;
- }
- r.rows.add(rebuildRow(row, uidField.equals(A_BIL_SUBMITTER_UID) ? A_BIL_SUBMITTER : A_NON_SUBMITTER));
- }
- }
- for (BillRow br : newRows) {
- String key = submitterUid + "|" + br.dayText + "|" + br.projectCode;
- if (!keys.add(key)) {
- continue;
- }
- r.rows.add(builder.build(br));
- r.addedHours += br.hours;
- }
- return r;
- }
- /**
- * 子表行"重建":成员字段用 _id 后缀真实 userId 替换显示名(否则宜搭静默清空)
- * fixme 前端 v2.32 已确认这个坑 — 后端全量覆盖写子表时同规则
- */
- private Map<String, Object> rebuildRow(Map row, String submitterField) {
- Map<String, Object> nr = new HashMap<>();
- for (Object k : row.keySet()) {
- String key = String.valueOf(k);
- if (key.endsWith("_value")) {
- continue;
- }
- if (key.startsWith("employeeField_") && !key.endsWith("_id")) {
- continue;
- }
- if (key.endsWith("_id") && (key.startsWith("employeeField_") || key.startsWith("associationFormField_"))) {
- nr.put(key.substring(0, key.length() - 3), row.get(k));
- continue;
- }
- nr.put(key, row.get(k));
- }
- return nr;
- }
- // ============================================================
- // 宜搭 API 封装
- // ============================================================
- private void startProcessInstance(String formUuid,
- String processCode,
- Map<String, Object> formData,
- String deptId) {
- // fixme 必须用真人 System 账号发起,yida_pub_account 不触发钉钉待办通知。
- ydClient.operateData(YDParam.builder()
- .appType(whConf.getYidaAppType())
- .systemToken(whConf.getYidaSystemToken())
- .userId(whConf.getApprovalOriginatorUserId())
- .deptId(StringUtils.defaultIfBlank(deptId, whConf.getApprovalOriginatorDeptId()))
- .formUuid(formUuid)
- .processCode(processCode)
- .formDataJson(JSON.toJSONString(formData))
- .build(), YDConf.FORM_OPERATION.start);
- }
- /**
- * 查该月 + PM/deptManager + RUNNING 的审批单;分页取全
- */
- private List<Map> queryProcessRunning(String formUuid, Map<String, Object> searchFieldJson) {
- List<Map> all = new ArrayList<>();
- int page = 1;
- while (page <= 20) {
- DDR_New r = ydClient.queryData(YDParam.builder()
- .appType(whConf.getYidaAppType())
- .systemToken(whConf.getYidaSystemToken())
- .formUuid(formUuid)
- .searchFieldJson(JSON.toJSONString(searchFieldJson))
- .instanceStatus("RUNNING")
- .pageNumber(page)
- .pageSize(100)
- .build(), YDConf.FORM_QUERY.retrieve_search_process);
- List<Map> data = (List<Map>) r.getData();
- if (data == null || data.isEmpty()) {
- break;
- }
- all.addAll(data);
- if (data.size() < 100) {
- break;
- }
- page++;
- }
- return all;
- }
- /**
- * 查指定条件下全部状态的审批单,供补发起前构建覆盖索引。
- */
- private List<Map> queryProcessAll(String formUuid, Map<String, Object> searchFieldJson) {
- List<Map> all = new ArrayList<>();
- int page = 1;
- while (page <= 100) {
- DDR_New r = ydClient.queryData(YDParam.builder()
- .appType(whConf.getYidaAppType())
- .systemToken(whConf.getYidaSystemToken())
- .formUuid(formUuid)
- .searchFieldJson(JSON.toJSONString(searchFieldJson))
- .pageNumber(page)
- .pageSize(100)
- .build(), YDConf.FORM_QUERY.retrieve_search_process);
- List<Map> data = (List<Map>) r.getData();
- if (data == null || data.isEmpty()) {
- break;
- }
- all.addAll(data);
- if (data.size() < 100) {
- break;
- }
- page++;
- }
- return all;
- }
- /**
- * 汇总表分页取全
- */
- List<Map> queryAll(String formUuid, Map<String, Object> searchFieldJson) {
- List<Map> all = new ArrayList<>();
- int page = 1;
- while (page <= 200) {
- DDR_New r = ydClient.queryData(YDParam.builder()
- .appType(whConf.getYidaAppType())
- .systemToken(whConf.getYidaSystemToken())
- .formUuid(formUuid)
- .searchFieldJson(JSON.toJSONString(searchFieldJson))
- .currentPage(page)
- .pageSize(100)
- .build(), YDConf.FORM_QUERY.retrieve_search_form);
- List<Map> data = (List<Map>) r.getData();
- if (data == null || data.isEmpty()) {
- break;
- }
- all.addAll(data);
- if (data.size() < 100) {
- break;
- }
- page++;
- }
- return all;
- }
- /**
- * 按宜搭 gmtModified 时间窗口分页查询表单实例。
- */
- @SuppressWarnings("unchecked")
- private List<Map> queryAllModified(String formUuid, String fromDate, String toDate) {
- List<Map> all = new ArrayList<>();
- int page = 1;
- while (page <= 200) {
- DDR_New result = 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);
- List<Map> data = result == null ? null : (List<Map>) result.getData();
- if (data == null || data.isEmpty()) {
- break;
- }
- all.addAll(data);
- if (data.size() < 100) {
- break;
- }
- page++;
- }
- return all;
- }
- // ============================================================
- // 通用工具
- // ============================================================
- @SuppressWarnings("unchecked")
- private List<Map> listOrEmpty(Map m, String k) {
- if (m == null) {
- return Collections.emptyList();
- }
- Object v = m.get(k);
- if (v instanceof List) {
- return (List<Map>) v;
- }
- return Collections.emptyList();
- }
- private static double num(Map m, String k) {
- Object v = m == null ? null : m.get(k);
- if (v == null || "".equals(v)) {
- return 0d;
- }
- try {
- return Double.parseDouble(String.valueOf(v));
- } catch (NumberFormatException e) {
- return 0d;
- }
- }
- private long numLong(Map m, String k) {
- return (long) num(m, k);
- }
- private static String str(Map m, String k) {
- Object v = m == null ? null : m.get(k);
- return v == null ? "" : String.valueOf(v);
- }
- /**
- * 从 EmployeeField 值(数组)取第一个 userId;取不到时兜底返回文本
- */
- private String firstIdOrText(Map m, String fieldPrefix) {
- Object idArr = m == null ? null : m.get(fieldPrefix + "_id");
- if (idArr instanceof List && !((List) idArr).isEmpty()) {
- Object first = ((List) idArr).get(0);
- if (first != null) {
- return String.valueOf(first);
- }
- }
- Object nameArr = m == null ? null : m.get(fieldPrefix);
- if (nameArr instanceof List && !((List) nameArr).isEmpty()) {
- Object first = ((List) nameArr).get(0);
- if (first != null) {
- return String.valueOf(first);
- }
- }
- return "";
- }
- private String firstEmployeeId(Map m, String fieldPrefix) {
- Object idArr = m == null ? null : m.get(fieldPrefix + "_id");
- if (!(idArr instanceof List) || ((List) idArr).isEmpty()) {
- return "";
- }
- Object first = ((List) idArr).get(0);
- return first == null ? "" : String.valueOf(first);
- }
- private String firstNonBlank(String... vals) {
- for (String v : vals) {
- if (StringUtils.isNotBlank(v)) {
- return v;
- }
- }
- return "";
- }
- /**
- * yyyyMM → epoch ms(该月 1 号 00:00)
- */
- private long monthToTs(String monthText) {
- int y = Integer.parseInt(monthText.substring(0, 4));
- int m = Integer.parseInt(monthText.substring(4, 6));
- Calendar c = Calendar.getInstance();
- c.set(y, m - 1, 1, 0, 0, 0);
- c.set(Calendar.MILLISECOND, 0);
- return c.getTimeInMillis();
- }
- }
|