| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305 |
- 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.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.util.*;
- /**
- * 补发起工时审批(bug 1 兜底)
- * <p>
- * 场景:汇总表 submitted > 0 但对应 (userId, day) 未出现在任何该月工时审批 / 其他工时审批的子表明细中,
- * 即前端 submitApprovalProcess 链路失败(startInstance 挂了/静默丢),导致部分员工工时从未进入审批。
- * <p>
- * 逻辑完全参考前端 TimeCard 的 submitApprovalProcess:
- * 1. collectApprovalData — 遍历汇总表每天子表,delta = submitted - approved - change, delta>0 才 push,按 PM 分组
- * 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;
- /**
- * 前端 v2.35 常量:发起时明细「审批意见」默认「同意」
- */
- private static final String APPROVAL_OPINION_AGREE = "同意";
- /**
- * 宜搭单个子表组件可稳定查询和覆盖更新的最大行数。
- */
- private static final int MAX_APPROVAL_SUBTABLE_ROWS = 500;
- /**
- * 批量历史补偿的目标分片大小;以员工整组为边界,允许单个员工整组略超目标值。
- */
- private static final int TARGET_APPROVAL_SUBTABLE_ROWS = 100;
- /**
- * 兜底部门主管 userId(Raymond,前端 v2.42 保持一致)
- */
- private static final String DEFAULT_MANAGER_ID = "634735129";
- // ===== 汇总表字段(与 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;
- }
- }
- /**
- * 某员工某月按 PM 聚合的项目工时组
- */
- private static class ManagerGroup {
- String managerId;
- 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) {
- 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);
- // 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;
- // 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);
- if (cr.managerGroups.isEmpty() && cr.otherRows.isEmpty()) {
- skipped++;
- continue;
- }
- // 3a) 项目工时:每个 PM 一张单
- for (ManagerGroup mg : cr.managerGroups.values()) {
- try {
- String action = processManagerApproval(mg, monthText, uid);
- if ("created".equals(action)) {
- billableCreated++;
- } else if ("updated".equals(action)) {
- billableUpdated++;
- }
- } catch (Exception ex) {
- failed++;
- log.error("[补发起] 项目工时发起失败 uid={} pm={} month={}", uid, mg.managerId, monthText, ex);
- }
- }
- // 3b) 其他工时:一人一张单,approver 用该员工部门主管(记录中带),否则兜底 Raymond
- if (!cr.otherRows.isEmpty()) {
- try {
- String action = processOtherApproval(cr.otherRows, cr.deptManagerId, monthText, uid);
- 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);
- }
- }
- ReSubmitApprovalResult r = ReSubmitApprovalResult.builder()
- .monthText(monthText)
- .submitterCount(perSubmitter.size())
- .billableCreated(billableCreated)
- .billableUpdated(billableUpdated)
- .otherCreated(otherCreated)
- .otherUpdated(otherUpdated)
- .skipped(skipped)
- .failed(failed)
- .build();
- log.info("[补发起] 完成 {}", r);
- return r;
- }
- /**
- * 使用真人 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);
- startProcessInstance(
- request.getFormUuid(),
- request.getProcessCode(),
- formData,
- request.getDeptId());
- return true;
- }
- /**
- * collectApprovalData + collectOtherApprovalData 的等价:
- * 遍历该员工该月每天记录,按项目工时 / 其他工时反构造 delta 行,按 PM 分组
- */
- private static class CollectResult {
- Map<String, ManagerGroup> managerGroups = new LinkedHashMap<>();
- List<OtherRow> otherRows = new ArrayList<>();
- String deptManagerId;
- }
- private CollectResult collectApprovalData(String uid, String monthText, List<Map> records,
- ApprovalCoverage approvalCoverage) {
- 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 coverageKey = approvalKey("billable", uid, dayText, projectCode);
- if (!seenBillableKeys.add(coverageKey)
- || approvalCoverage.shouldSkip(coverageKey, approved + change)) {
- continue;
- }
- String pmRaw = firstIdOrText(row, S_BIL_MANAGER);
- final String pm = StringUtils.isBlank(pmRaw) ? DEFAULT_MANAGER_ID : pmRaw;
- ManagerGroup mg = cr.managerGroups.computeIfAbsent(pm, k -> {
- ManagerGroup m = new ManagerGroup();
- m.managerId = pm;
- return m;
- });
- BillRow br = new BillRow();
- br.dayText = dayText;
- br.dayTs = dayTs;
- br.projectCode = projectCode;
- br.projectName = firstNonBlank(str(row, S_BIL_PROJECT_NAME_TEXT), str(row, S_BIL_PROJECT_NAME));
- 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 coverageKey = approvalKey("nonBillable", uid, dayText, projectCode);
- if (!seenNonBillableKeys.add(coverageKey)
- || approvalCoverage.shouldSkip(coverageKey, approved + change)) {
- continue;
- }
- String pmRaw = firstIdOrText(row, S_NON_MANAGER);
- final String pm = StringUtils.isBlank(pmRaw) ? DEFAULT_MANAGER_ID : pmRaw;
- ManagerGroup mg = cr.managerGroups.computeIfAbsent(pm, k -> {
- ManagerGroup m = new ManagerGroup();
- m.managerId = pm;
- return m;
- });
- BillRow br = new BillRow();
- br.dayText = dayText;
- br.dayTs = dayTs;
- br.projectCode = projectCode;
- br.projectName = firstNonBlank(str(row, S_NON_PROJECT_NAME_TEXT), str(row, S_NON_PROJECT_NAME));
- 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, 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;
- }
- /**
- * 加载指定月份两类审批单的完整明细覆盖索引。
- * 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,
- 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_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_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 hoursField,
- String opinionField) {
- for (Map row : rows) {
- String uid = str(row, uidField);
- String day = str(row, dayField);
- String detailKey = str(row, detailKeyField);
- 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;
- }
- /**
- * processManagerApproval 等价 — queryProcess(RUNNING) 有则 update / 无则 create。
- * COMPLETED 覆盖已在 collectApprovalData 前过滤,避免历史已完成单被重复发起。
- */
- private String processManagerApproval(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;
- }
- 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);
- return canAppendToApprovalShard(existingBillableCount, billable.rows.size())
- && canAppendToApprovalShard(existingNonBillableCount, nonBillable.rows.size());
- }
- /**
- * 创建新的工时审批实例(startProcessInstance)
- */
- private void createApprovalInstance(ManagerGroup mg, String monthText, String submitterUid) {
- McException.assertAccessException(
- !hasSubtableCapacity(mg.billableRows.size())
- || !hasSubtableCapacity(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={} month={} bilTotal={} nonBilTotal={} rows={}+{}",
- submitterUid, mg.managerId, monthText,
- mg.billableTotal, mg.nonBillableTotal, billableSub.size(), nonBillableSub.size());
- }
- /**
- * 累计更新已有 RUNNING 工时审批单(按唯一键 userId + dayText + projectCode 去重合并)
- */
- private void 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 (mrBil.addedHours == 0 && mrNon.addedHours == 0) {
- log.info("[补发起] 已存在等效明细,跳过更新 formInstanceId={}", formInstanceId);
- return;
- }
- 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={} addedBil={} addedNon={}",
- formInstanceId, mg.managerId, mrBil.addedHours, mrNon.addedHours);
- }
- /**
- * 其他工时审批处理(与 processManagerApproval 同构,但 approver 为员工的部门主管)
- */
- private String processOtherApproval(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;
- }
- 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 void 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 (addedHours == 0) {
- log.info("[补发起] 其他工时无新增明细 formInstanceId={}", formInstanceId);
- return;
- }
- 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);
- }
- // ============================================================
- // 子表行构造 + 合并 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;
- }
- 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;
- }
- // ============================================================
- // 通用工具
- // ============================================================
- @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 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 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 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();
- }
- }
|