| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784 |
- package com.malk.service.workhours;
- import com.alibaba.fastjson.JSON;
- import com.google.common.util.concurrent.RateLimiter;
- 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.ApprovalWriteBackResult;
- 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.scheduling.annotation.Async;
- import org.springframework.stereotype.Service;
- import java.util.*;
- import java.util.concurrent.*;
- import java.util.concurrent.atomic.AtomicInteger;
- /**
- * 审批结果回写工时汇总表
- * <p>
- * 审批结束或撤销后调用,入参为流程实例ID + 审批结果(0=拒绝/撤销,1=同意)。
- * 工时审批含两个明细子表(项目/非项目工时),其他工时审批含一个明细子表。
- * 明细按「填报人 + 工时日期(天) + 项目编号/Activity」1:1 对应工时汇总表(人+天一条)里的子表行:
- * - 拒绝 → 对应子表行「已提交工时」置 0
- * - 同意 → 对应子表行「已审批工时」= 该明细工时,且「已提交工时」不得低于已审批工时
- * 主表合计按子表行重算(幂等,撤销/重发不翻倍)。
- * <p>
- * 设计文档:后端/阿科德斯/审批结果回写工时汇总表/02-程序实现与接口.md
- */
- @Slf4j
- @Service
- public class ApprovalWriteBackService {
- @Autowired
- private YDClient ydClient;
- @Autowired
- private YDService ydService;
- @Autowired
- private WHConf whConf;
- @Autowired
- private RepairApprovalWriteBackService repairApprovalWriteBackService;
- private static final int THREAD_POOL_SIZE = 10;
- /**
- * 明细审批意见「同意」取值(单选,已确认)
- */
- private static final String OPINION_APPROVE = "同意";
- // ===== 工时汇总表:记录定位键 + 主表合计字段 =====
- private static final String SUMMARY_SUBMITTER_ID = "textField_mmbffvd9"; // 填报人ID(文本)
- private static final String SUMMARY_DAY_TEXT = "textField_mmbffvdb"; // 工时日期(文本) yyyyMMdd
- private static final String M_DAY_SUBMITTED = "numberField_mmd1smem"; // 已提交工时(日)
- private static final String M_DAY_APPROVED = "numberField_mmeakgi4"; // 已审批工时(日)
- private static final String M_BIL_SUBMITTED = "numberField_mmacxewa"; // Billable已提交
- private static final String M_BIL_APPROVED = "numberField_mmacxewg"; // Billable已审批
- private static final String M_NON_SUBMITTED = "numberField_mmad6jbz"; // Non Billable已提交
- private static final String M_NON_APPROVED = "numberField_mmad6jc1"; // Non Billable已审批
- private static final String M_OTH_SUBMITTED = "numberField_mmd1smeq"; // 已提交(其他)
- private static final String M_OTH_APPROVED = "numberField_mmd1smes"; // 已审批(其他)
- // ===== 工时汇总表:三个子表 =====
- private static final String SUB_BIL_TABLE = "tableField_mmczo634";
- private static final String SUB_BIL_KEY = "textField_mmacxew3"; // 项目编号
- private static final String SUB_BIL_SUBMITTED = "numberField_mmczo636"; // 已提交工时
- private static final String SUB_BIL_APPROVED = "numberField_mmczo637"; // 已审批工时
- private static final String SUB_NON_TABLE = "tableField_mmczo63h";
- private static final String SUB_NON_KEY = "textField_mmczo639"; // 项目编号
- private static final String SUB_NON_SUBMITTED = "numberField_mmczo63c";
- private static final String SUB_NON_APPROVED = "numberField_mmczo63d";
- private static final String SUB_OTH_TABLE = "tableField_mmeakgid";
- private static final String SUB_OTH_KEY = "selectField_mmeakgie"; // Activity
- private static final String SUB_OTH_SUBMITTED = "numberField_mmeakgij";
- private static final String SUB_OTH_APPROVED = "numberField_mmeakgii";
- // ===== 审批单明细子表字段(按类别)=====
- // 工时审批·项目工时明细(Billable)
- private static final String AP_BIL_TABLE = "tableField_mmae8t99";
- private static final String AP_BIL_USERID = "textField_mmd1ozk4";
- private static final String AP_BIL_KEY = "textField_mmacxew3"; // 项目编号
- private static final String AP_BIL_DAYTEXT = "textField_mmd8g654";
- private static final String AP_BIL_HOURS = "numberField_mmacxew9";
- private static final String AP_BIL_OPINION = "radioField_mpwhk294";
- // 工时审批·非项目工时明细(Non Billable)
- private static final String AP_NON_TABLE = "tableField_mmd1wu9h";
- private static final String AP_NON_USERID = "textField_mmd1wu9f";
- private static final String AP_NON_KEY = "textField_mmd1wu9d"; // 项目编号
- private static final String AP_NON_DAYTEXT = "textField_mmd8g655";
- private static final String AP_NON_HOURS = "numberField_mmd1wu9e";
- private static final String AP_NON_OPINION = "radioField_mpwhk295";
- // 其他工时审批·明细(与非项目同子表ID,但语义为 Activity)
- private static final String AP_OTH_TABLE = "tableField_mmd1wu9h";
- private static final String AP_OTH_USERID = "textField_mmd1wu9f";
- private static final String AP_OTH_KEY = "selectField_mmeakgie"; // Activity
- private static final String AP_OTH_DAYTEXT = "textField_mmd8g655";
- private static final String AP_OTH_HOURS = "numberField_mmd1wu9e";
- private static final String AP_OTH_OPINION = "radioField_mpwhk294";
- private enum Cat {BILLABLE, NON_BILLABLE, OTHER}
- /**
- * 单条审批明细(归一化)
- */
- private static class DetailRow {
- Cat cat;
- String userId;
- String dayText;
- String key; // 项目编号 / Activity
- double hours;
- boolean approve; // 有效意见:true=同意, false=拒绝
- }
- /**
- * 异步触发审批回写,供流程结束自动化调用。
- *
- * @param formInstanceId 审批表单实例 ID
- * @param result 审批结果:0=拒绝/撤销,1=同意
- */
- @Async
- public void writeBackAsync(String formInstanceId, int result) {
- writeBack(formInstanceId, result);
- }
- /**
- * 审批结果回写工时汇总表
- *
- * @param formInstanceId 流程实例ID
- * @param result 审批结果:0=拒绝/撤销,1=同意
- * @return 回写结果统计
- */
- public ApprovalWriteBackResult writeBack(String formInstanceId, int result) {
- McException.assertAccessException(StringUtils.isBlank(formInstanceId), "实例ID不能为空");
- // 1. 流程详情接口优先,流程表单可直接返回完整子表;旧回调传表单实例ID时回退表单详情接口。
- Map approvalFormData = queryApprovalFormData(formInstanceId);
- log.info("[审批回写] 审批单详情 formInstanceId={} keys={}", formInstanceId,
- approvalFormData == null ? null : approvalFormData.keySet());
- McException.assertAccessException(approvalFormData == null || approvalFormData.isEmpty(),
- "未查询到审批单数据,实例ID=" + formInstanceId);
- // prd 补填审批只走独立回写:来源日写补填汇总,提交日写原汇总变更工时。
- // 正常审批(工时类型为空/正常)继续执行下方原有逻辑。
- String fillType = whConf.getApprovalFillTypeField() == null
- ? "" : String.valueOf(approvalFormData.get(whConf.getApprovalFillTypeField()));
- if ("补填".equals(fillType) && repairApprovalWriteBackService != null) {
- return repairApprovalWriteBackService.writeBack(formInstanceId, result);
- }
- // 2. 类型判定:不能只看项目工时子表;仅填非项目工时的工时审批也可能没有 AP_BIL_TABLE。
- boolean isOther = isOtherApprovalForm(approvalFormData);
- String approvalFormUuid = isOther
- ? whConf.getFormUuidOtherApproval()
- : whConf.getFormUuidApproval();
- boolean approveWhole = (result == 1);
- // 3. 取审批单明细(内联子表 <50 直接用,==50 才 queryDetails 递归取全,避免无效请求)
- List<DetailRow> rows = new ArrayList<>();
- if (isOther) {
- rows.addAll(loadDetailRows(
- formInstanceId, approvalFormUuid, approvalFormData, Cat.OTHER, approveWhole));
- } else {
- rows.addAll(loadDetailRows(
- formInstanceId, approvalFormUuid, approvalFormData, Cat.BILLABLE, approveWhole));
- rows.addAll(loadDetailRows(
- formInstanceId, approvalFormUuid, approvalFormData, Cat.NON_BILLABLE, approveWhole));
- }
- // 4. 按汇总表记录键(填报人 + 工时日期文本)分组
- Map<String, List<DetailRow>> groups = new LinkedHashMap<>();
- int invalidRows = 0;
- for (DetailRow r : rows) {
- if (StringUtils.isBlank(r.userId) || StringUtils.isBlank(r.dayText)) {
- invalidRows++;
- continue;
- }
- groups.computeIfAbsent(r.userId + "" + r.dayText, k -> new ArrayList<>()).add(r);
- }
- // 5. 并发逐「日记录」处理
- AtomicInteger hitRecords = new AtomicInteger(0);
- AtomicInteger updatedRows = new AtomicInteger(0);
- AtomicInteger missRows = new AtomicInteger(0);
- AtomicInteger failRecords = new AtomicInteger(0);
- RateLimiter limiter = RateLimiter.create(20.0);
- ExecutorService executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
- try {
- List<Future<?>> futures = new ArrayList<>();
- for (Map.Entry<String, List<DetailRow>> e : groups.entrySet()) {
- List<DetailRow> groupRows = e.getValue();
- String userId = groupRows.get(0).userId;
- String dayText = groupRows.get(0).dayText;
- futures.add(executor.submit(() -> {
- try {
- int[] dayResult = applyToRecord(userId, dayText, groupRows, limiter);
- if (dayResult == null) {
- // fixme 审批明细存在但汇总表日记录缺失属于失败,不能把审批单标记为「全部成功」。
- failRecords.incrementAndGet();
- return;
- }
- hitRecords.incrementAndGet();
- updatedRows.addAndGet(dayResult[0]);
- missRows.addAndGet(dayResult[1]);
- if (dayResult[1] > 0) {
- failRecords.incrementAndGet();
- }
- } catch (Exception ex) {
- failRecords.incrementAndGet();
- log.error("[审批回写] 记录处理失败,交由定时器本轮结束后补试 userId={} day={}",
- userId, dayText, ex);
- }
- }));
- }
- for (Future<?> f : futures) {
- try {
- f.get();
- } catch (Exception ex) {
- log.error("[审批回写] 线程执行异常", ex);
- }
- }
- } finally {
- executor.shutdown();
- }
- // 6. 同步状态汇总(syncTotal=分组数;failCount=失败组数)
- // fixme 状态字面值对齐宜搭审批表 selectField_mq58cd5p 选项:"全部成功" / "部分失败" / "全部失败"
- int syncTotal = groups.size() + invalidRows;
- int failCount = failRecords.get() + invalidRows;
- // fixme 审批明细为空同样不能标记为成功,否则兜底定时会永久跳过该异常实例。
- if (syncTotal == 0) {
- syncTotal = 1;
- failCount = 1;
- }
- String syncStatus = determineSyncStatus(syncTotal, failCount);
- ApprovalWriteBackResult stats = ApprovalWriteBackResult.builder()
- .formType(isOther ? "其他工时审批" : "工时审批")
- .formInstanceId(formInstanceId)
- .result(result)
- .detailRows(rows.size())
- .groups(groups.size())
- .hitRecords(hitRecords.get())
- .updatedRows(updatedRows.get())
- .missRows(missRows.get())
- .failRecords(failCount)
- .syncStatus(syncStatus)
- .syncTotal(syncTotal)
- .failCount(failCount)
- .build();
- log.info("[审批回写] 完成 {}", stats);
- // 7. 延迟 2s 等宜搭审批结束的内部同步索引/锁释放,再回写审批单同步字段,避免触发同步锁失败
- sleep(2000);
- writebackSyncFields(isOther, formInstanceId, syncStatus, syncTotal, failCount);
- return stats;
- }
- static String determineSyncStatus(int syncTotal, int failCount) {
- if (syncTotal <= 0 || failCount >= syncTotal) {
- return "全部失败";
- }
- return failCount > 0 ? "部分失败" : "全部成功";
- }
- private Map queryApprovalFormData(String instanceId) {
- try {
- DDR_New processDetail = ydClient.queryData(YDParam.builder()
- .appType(whConf.getYidaAppType())
- .systemToken(whConf.getYidaSystemToken())
- .processInstanceId(instanceId)
- .build(), YDConf.FORM_QUERY.retrieve_search_process_id);
- Map processFormData = extractProcessFormData(processDetail);
- if (processFormData != null && !processFormData.isEmpty()) {
- return processFormData;
- }
- } catch (Exception ex) {
- log.warn("[审批回写] 流程详情查询失败,回退表单详情 instanceId={}", instanceId, ex);
- }
- try {
- DDR_New legacyProcessDetail = ydClient.queryData(YDParam.builder()
- .appType(whConf.getYidaAppType())
- .systemToken(whConf.getYidaSystemToken())
- .processInstanceId(instanceId)
- .build(), YDConf.FORM_QUERY.retrieve_process_legacy_id);
- Map legacyFormData = extractProcessFormData(legacyProcessDetail);
- if (legacyFormData != null && !legacyFormData.isEmpty()) {
- return legacyFormData;
- }
- } catch (Exception ex) {
- log.warn("[审批回写] 旧版流程详情查询失败,回退表单详情 instanceId={}", instanceId, ex);
- }
- DDR_New formDetail = ydClient.queryData(YDParam.builder()
- .appType(whConf.getYidaAppType())
- .systemToken(whConf.getYidaSystemToken())
- .formInstanceId(instanceId)
- .build(), YDConf.FORM_QUERY.retrieve_id);
- return formDetail == null ? null : formDetail.getFormData();
- }
- @SuppressWarnings("unchecked")
- static Map extractProcessFormData(DDR_New processDetail) {
- if (processDetail == null) {
- return null;
- }
- if (processDetail.getFormData() != null && !processDetail.getFormData().isEmpty()) {
- return processDetail.getFormData();
- }
- if (processDetail.getResult() instanceof Map) {
- Map result = (Map) processDetail.getResult();
- Object nested = result.get("formData");
- if (!(nested instanceof Map)) {
- nested = result.get("data");
- }
- if (nested instanceof Map) {
- return (Map) nested;
- }
- if (result.containsKey(AP_BIL_TABLE)
- || result.containsKey(AP_NON_TABLE)
- || result.containsKey(AP_OTH_TABLE)) {
- return result;
- }
- }
- if (!(processDetail.getData() instanceof Map)) {
- return null;
- }
- Map data = (Map) processDetail.getData();
- Object nested = data.get("formData");
- if (!(nested instanceof Map)) {
- nested = data.get("data");
- }
- if (nested instanceof Map) {
- return (Map) nested;
- }
- if (data.containsKey(AP_BIL_TABLE)
- || data.containsKey(AP_NON_TABLE)
- || data.containsKey(AP_OTH_TABLE)) {
- return data;
- }
- return null;
- }
- /**
- * 区分「工时审批」与「其他工时审批」。
- * 两者共用 tableField_mmd1wu9h,但明细键分别为项目编号和 Activity。
- */
- @SuppressWarnings("unchecked")
- static boolean isOtherApprovalForm(Map approvalFormData) {
- if (approvalFormData == null) {
- return false;
- }
- if (approvalFormData.containsKey(AP_BIL_TABLE)) {
- return false;
- }
- Object sharedRows = approvalFormData.get(AP_NON_TABLE);
- if (sharedRows instanceof List) {
- for (Object item : (List<?>) sharedRows) {
- if (!(item instanceof Map)) {
- continue;
- }
- Map row = (Map) item;
- if (row.containsKey(AP_NON_KEY)) {
- return false;
- }
- if (row.containsKey(AP_OTH_KEY)) {
- return true;
- }
- }
- }
- return true;
- }
- /**
- * 把同步状态/同步总数/失败数量回写到原审批单(fieldId 走 WHConf 配置)
- * fixme 三个 fieldId 任一为空则跳过该类别回写,允许业务方分阶段在宜搭后台建字段
- */
- private void writebackSyncFields(boolean isOther, String formInstanceId,
- String syncStatus, int syncTotal, int failCount) {
- String statusField = isOther ? whConf.getOtherApprovalSyncStatusField() : whConf.getApprovalSyncStatusField();
- String totalField = isOther ? whConf.getOtherApprovalSyncTotalField() : whConf.getApprovalSyncTotalField();
- String failField = isOther ? whConf.getOtherApprovalSyncFailField() : whConf.getApprovalSyncFailField();
- if (StringUtils.isAnyBlank(statusField, totalField, failField)) {
- log.info("[审批回写] 跳过审批单同步字段回写(配置未启用) isOther={} formInstanceId={}", isOther, formInstanceId);
- return;
- }
- try {
- Map<String, Object> sync = new HashMap<>();
- sync.put(statusField, syncStatus);
- sync.put(totalField, syncTotal);
- sync.put(failField, failCount);
- ydClient.operateData(YDParam.builder()
- .appType(whConf.getYidaAppType())
- .systemToken(whConf.getYidaSystemToken())
- .formInstanceId(formInstanceId)
- .updateFormDataJson(JSON.toJSONString(sync))
- .ignoreEmpty(false)
- .useLatestVersion(true)
- .build(), YDConf.FORM_OPERATION.update);
- log.info("[审批回写] 审批单同步字段已回写 isOther={} formInstanceId={} status={} total={} fail={}",
- isOther, formInstanceId, syncStatus, syncTotal, failCount);
- } catch (Exception ex) {
- log.error("[审批回写] 审批单同步字段回写失败 isOther={} formInstanceId={}", isOther, formInstanceId, ex);
- throw new IllegalStateException("审批单同步字段回写失败 formInstanceId=" + formInstanceId, ex);
- }
- }
- /**
- * 处理单条汇总表日记录
- *
- * @return {updatedRows, missRows};汇总表无此记录返回 null
- */
- private int[] applyToRecord(String userId, String dayText, List<DetailRow> groupRows, RateLimiter limiter) {
- // a. 定位汇总表记录(填报人ID + 工时日期文本,代码内精确过滤)
- limiter.acquire();
- List<Map> found = (List<Map>) ydClient.queryData(YDParam.builder()
- .appType(whConf.getYidaAppType())
- .systemToken(whConf.getYidaSystemToken())
- .formUuid(whConf.getFormUuidWorkHoursSummary())
- .searchFieldJson(JSON.toJSONString(UtilMap.map(
- SUMMARY_SUBMITTER_ID + ", " + SUMMARY_DAY_TEXT, userId, dayText)))
- .build(), YDConf.FORM_QUERY.retrieve_search_form).getData();
- List<String> matchingIds = new ArrayList<>();
- for (Map item : found == null ? Collections.<Map>emptyList() : found) {
- Map fd = (Map) item.get("formData");
- if (fd == null) continue;
- if (userId.equals(String.valueOf(fd.get(SUMMARY_SUBMITTER_ID)))
- && dayText.equals(String.valueOf(fd.get(SUMMARY_DAY_TEXT)))) {
- Object id = item.get("formInstanceId");
- if (id == null) {
- id = item.get("formInstId");
- }
- if (id != null && StringUtils.isNotBlank(String.valueOf(id))) {
- matchingIds.add(String.valueOf(id));
- }
- }
- }
- McException.assertAccessException(
- matchingIds.size() > 1,
- "工时汇总表唯一键重复,拒绝回写 userId=" + userId + ", day=" + dayText);
- String summaryInstId = matchingIds.isEmpty() ? null : matchingIds.get(0);
- if (StringUtils.isBlank(summaryInstId)) {
- log.warn("[审批回写] 未命中汇总表记录 userId={} day={}", userId, dayText);
- return null;
- }
- // b. 取汇总表记录完整数据(含三子表)
- limiter.acquire();
- Map formData = ydClient.queryData(YDParam.builder()
- .appType(whConf.getYidaAppType())
- .systemToken(whConf.getYidaSystemToken())
- .formInstanceId(summaryInstId)
- .build(), YDConf.FORM_QUERY.retrieve_id).getFormData();
- Map<String, Object> updateData = new HashMap<>();
- int updated = 0;
- int miss = 0;
- // c. 按类别分组本记录的明细,逐类别重建子表 + 置值
- Map<Cat, List<DetailRow>> byCat = new EnumMap<>(Cat.class);
- for (DetailRow r : groupRows) {
- byCat.computeIfAbsent(r.cat, k -> new ArrayList<>()).add(r);
- }
- // 各类别已提交/已审批合计(受影响类别从重建行重算,未受影响沿用主表现值)
- double bilSubmitted = num(formData, M_BIL_SUBMITTED), bilApproved = num(formData, M_BIL_APPROVED);
- double nonSubmitted = num(formData, M_NON_SUBMITTED), nonApproved = num(formData, M_NON_APPROVED);
- double othSubmitted = num(formData, M_OTH_SUBMITTED), othApproved = num(formData, M_OTH_APPROVED);
- for (Map.Entry<Cat, List<DetailRow>> e : byCat.entrySet()) {
- Cat cat = e.getKey();
- String subTable = subTable(cat);
- String keyField = subKey(cat);
- String submittedField = subSubmitted(cat);
- String approvedField = subApproved(cat);
- // 子表取数控制:内联 <50 直接用,==50 才递归取全,避免全量覆盖时漏行被删
- List<Map> rebuilt = rebuildRows(
- resolveDetailRows(
- summaryInstId,
- whConf.getFormUuidWorkHoursSummary(),
- subTable,
- (List<Map>) formData.get(subTable)));
- for (DetailRow dr : e.getValue()) {
- Map target = null;
- for (Map row : rebuilt) {
- if (dr.key != null && dr.key.equals(String.valueOf(row.get(keyField)))) {
- target = row;
- break;
- }
- }
- if (target == null) {
- miss++;
- log.warn("[审批回写] 子表行未匹配 cat={} key={} userId={} day={}", cat, dr.key, userId, dayText);
- continue;
- }
- double submittedBefore = num(target, submittedField);
- if (dr.approve) {
- target.put(approvedField, dr.hours);
- }
- // fixme 拒绝仍清零;同意时兜底恢复已提交,避免补发审批只写 approved 后形成 submitted=0。
- target.put(submittedField, resolveSubmittedAfterWriteBack(
- dr.approve, submittedBefore, dr.hours));
- updated++;
- }
- updateData.put(subTable, rebuilt);
- double sumSubmitted = sumRows(rebuilt, submittedField);
- double sumApproved = sumRows(rebuilt, approvedField);
- switch (cat) {
- case BILLABLE:
- bilSubmitted = sumSubmitted;
- bilApproved = sumApproved;
- break;
- case NON_BILLABLE:
- nonSubmitted = sumSubmitted;
- nonApproved = sumApproved;
- break;
- case OTHER:
- othSubmitted = sumSubmitted;
- othApproved = sumApproved;
- break;
- }
- }
- // d. 主表合计重算
- updateData.put(M_BIL_SUBMITTED, bilSubmitted);
- updateData.put(M_BIL_APPROVED, bilApproved);
- updateData.put(M_NON_SUBMITTED, nonSubmitted);
- updateData.put(M_NON_APPROVED, nonApproved);
- updateData.put(M_OTH_SUBMITTED, othSubmitted);
- updateData.put(M_OTH_APPROVED, othApproved);
- updateData.put(M_DAY_SUBMITTED, bilSubmitted + nonSubmitted + othSubmitted);
- updateData.put(M_DAY_APPROVED, bilApproved + nonApproved + othApproved);
- // prd 审批回写完成后同步记录级审批状态,避免数字已回写但汇总状态仍停留在“审批中”
- String approvalStatusField = whConf.getSummaryApprovalStatusField();
- if (StringUtils.isNotBlank(approvalStatusField)) {
- updateData.put(
- approvalStatusField,
- resolveSummaryApprovalStatus(
- bilSubmitted + nonSubmitted + othSubmitted,
- bilApproved + nonApproved + othApproved));
- }
- // e. 全量覆盖更新(子表整组替换 + 主表合计)
- limiter.acquire();
- ydClient.operateData(YDParam.builder()
- .appType(whConf.getYidaAppType())
- .systemToken(whConf.getYidaSystemToken())
- .formInstanceId(summaryInstId)
- .updateFormDataJson(JSON.toJSONString(updateData))
- .ignoreEmpty(false)
- .useLatestVersion(true)
- .build(), YDConf.FORM_OPERATION.update);
- return new int[]{updated, miss};
- }
- /**
- * 计算工时汇总记录的审批状态。
- *
- * @param submitted 已提交合计
- * @param approved 已审批合计
- * @return 已审批 / 审批中 / 暂存
- */
- static String resolveSummaryApprovalStatus(double submitted, double approved) {
- if (submitted > approved + 1e-6) {
- return "审批中";
- }
- if (approved > 0) {
- return "已审批";
- }
- if (submitted > 0) {
- return "审批中";
- }
- return "暂存";
- }
- /**
- * 计算审批回写后的已提交工时。
- *
- * @param approve 是否审批同意
- * @param currentSubmitted 当前已提交工时
- * @param approvedHours 本次审批明细工时
- * @return 拒绝时为 0;同意时不低于本次审批工时且不降低当前已提交值
- */
- static double resolveSubmittedAfterWriteBack(
- boolean approve,
- double currentSubmitted,
- double approvedHours) {
- if (!approve) {
- return 0d;
- }
- return Math.max(currentSubmitted, approvedHours);
- }
- /**
- * 加载审批单某类别明细(内联子表优先,必要时递归取全)
- */
- private List<DetailRow> loadDetailRows(String formInstanceId,
- String formUuid,
- Map approvalFormData,
- Cat cat,
- boolean approveWhole) {
- String tableId = apTable(cat);
- List<Map> raw = resolveDetailRows(
- formInstanceId,
- formUuid,
- tableId,
- (List<Map>) approvalFormData.get(tableId));
- List<DetailRow> list = new ArrayList<>();
- for (Map row : raw) {
- DetailRow dr = new DetailRow();
- dr.cat = cat;
- dr.userId = str(row, apUserId(cat));
- dr.dayText = str(row, apDayText(cat));
- dr.key = str(row, apKey(cat));
- dr.hours = num(row, apHours(cat));
- String opinion = str(row, apOpinion(cat));
- // fixme 历史审批单可能没有逐行意见:整单同意时空意见按同意处理,仅显式非同意值按拒绝处理。
- dr.approve = isDetailApproved(approveWhole, opinion);
- list.add(dr);
- }
- return list;
- }
- static boolean isDetailApproved(boolean approveWhole, String opinion) {
- return approveWhole && (StringUtils.isBlank(opinion) || OPINION_APPROVE.equals(opinion));
- }
- /**
- * 子表取数控制:主表详情已内联返回子表时,行数 <50 视为完整直接用;
- * ==50(可能被宜搭截断)才调 {@link YDService#queryDetails} 递归取全,避免无效请求。
- * 复用 ydService 封装,不重复造分页轮子。
- * <p>
- * fixme inlineRows==null 表示审批单不含该子表(如其他工时审批单无 BILLABLE/NON_BILLABLE,或工时审批单某一子表未填):
- * 直接返回空 list,不再调 queryDetails——基座 _queryDetails 对宜搭返回 data=null 的空子表查询会触发
- * ArrayList.addAll(null) NPE。queryDetails 返回 null 同样兜底空 list。
- */
- private List<Map> resolveDetailRows(String formInstanceId,
- String formUuid,
- String tableId,
- List<Map> inlineRows) {
- if (inlineRows == null) {
- return Collections.emptyList();
- }
- if (inlineRows.size() < 50) {
- return inlineRows;
- }
- List<Map> full = ydService.queryDetails(YDParam.builder()
- .appType(whConf.getYidaAppType())
- .systemToken(whConf.getYidaSystemToken())
- .formUuid(formUuid)
- .formInstanceId(formInstanceId)
- .tableFieldId(tableId)
- .pageNumber(1)
- .build());
- McException.assertAccessException(
- full == null || full.size() < inlineRows.size(),
- "审批单子表完整数据获取失败 instanceId=" + formInstanceId
- + ", tableFieldId=" + tableId);
- return full;
- }
- /**
- * 重建子表行:保留全部字段,成员/关联字段用 _id 取真实ID(全量覆盖写回前置)
- */
- private List<Map> rebuildRows(List<Map> rows) {
- List<Map> out = new ArrayList<>();
- if (rows == null) {
- return out;
- }
- for (Map row : rows) {
- Map newRow = new HashMap();
- for (Object keyObj : row.keySet()) {
- String key = String.valueOf(keyObj);
- if (key.endsWith("_value") || (key.startsWith("employeeField_") && !key.endsWith("_id"))) {
- continue;
- }
- String k = key;
- if (key.endsWith("_id")) {
- if (key.startsWith("employeeField_") || key.startsWith("associationFormField_")) {
- k = key.substring(0, key.length() - 3);
- } else {
- continue;
- }
- }
- newRow.put(k, YDConf.getDataByCompId(row, k));
- }
- out.add(newRow);
- }
- return out;
- }
- // ===== 字段映射辅助 =====
- private String apTable(Cat c) {
- return c == Cat.BILLABLE ? AP_BIL_TABLE : (c == Cat.NON_BILLABLE ? AP_NON_TABLE : AP_OTH_TABLE);
- }
- private String apUserId(Cat c) {
- return c == Cat.BILLABLE ? AP_BIL_USERID : (c == Cat.NON_BILLABLE ? AP_NON_USERID : AP_OTH_USERID);
- }
- private String apDayText(Cat c) {
- return c == Cat.BILLABLE ? AP_BIL_DAYTEXT : (c == Cat.NON_BILLABLE ? AP_NON_DAYTEXT : AP_OTH_DAYTEXT);
- }
- private String apKey(Cat c) {
- return c == Cat.BILLABLE ? AP_BIL_KEY : (c == Cat.NON_BILLABLE ? AP_NON_KEY : AP_OTH_KEY);
- }
- private String apHours(Cat c) {
- return c == Cat.BILLABLE ? AP_BIL_HOURS : (c == Cat.NON_BILLABLE ? AP_NON_HOURS : AP_OTH_HOURS);
- }
- private String apOpinion(Cat c) {
- return c == Cat.BILLABLE ? AP_BIL_OPINION : (c == Cat.NON_BILLABLE ? AP_NON_OPINION : AP_OTH_OPINION);
- }
- private String subTable(Cat c) {
- return c == Cat.BILLABLE ? SUB_BIL_TABLE : (c == Cat.NON_BILLABLE ? SUB_NON_TABLE : SUB_OTH_TABLE);
- }
- private String subKey(Cat c) {
- return c == Cat.BILLABLE ? SUB_BIL_KEY : (c == Cat.NON_BILLABLE ? SUB_NON_KEY : SUB_OTH_KEY);
- }
- private String subSubmitted(Cat c) {
- return c == Cat.BILLABLE ? SUB_BIL_SUBMITTED : (c == Cat.NON_BILLABLE ? SUB_NON_SUBMITTED : SUB_OTH_SUBMITTED);
- }
- private String subApproved(Cat c) {
- return c == Cat.BILLABLE ? SUB_BIL_APPROVED : (c == Cat.NON_BILLABLE ? SUB_NON_APPROVED : SUB_OTH_APPROVED);
- }
- // ===== 通用工具 =====
- private double num(Map m, String k) {
- Object v = m == null ? null : m.get(k);
- if (v == null) {
- return 0d;
- }
- try {
- return Double.parseDouble(String.valueOf(v));
- } catch (Exception e) {
- return 0d;
- }
- }
- private double sumRows(List<Map> rows, String field) {
- double sum = 0d;
- for (Map r : rows) {
- sum += num(r, field);
- }
- return sum;
- }
- private String str(Map m, String k) {
- Object v = m == null ? null : m.get(k);
- return v == null ? "" : String.valueOf(v);
- }
- private void sleep(long ms) {
- try {
- Thread.sleep(ms);
- } catch (InterruptedException ie) {
- Thread.currentThread().interrupt();
- }
- }
- }
|