| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502 |
- 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.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.stereotype.Service;
- import java.util.*;
- import java.util.concurrent.*;
- import java.util.concurrent.atomic.AtomicInteger;
- /**
- * 审批结果回写工时汇总表
- * <p>
- * 审批结束或撤销后调用,入参为流程实例ID + 审批结果(0=拒绝/撤销,1=同意)。
- * 工时审批含两个明细子表(项目/非项目工时),其他工时审批含一个明细子表。
- * 明细按「填报人 + 工时日期(天) + 项目编号/Activity」1:1 对应工时汇总表(人+天一条)里的子表行:
- * - 拒绝 → 对应子表行「已提交工时」置 0
- * - 同意 → 对应子表行「已审批工时」= 该明细工时
- * 主表合计按子表行重算(幂等,撤销/重发不翻倍)。
- * <p>
- * 设计文档:后端/阿科德斯/审批结果回写工时汇总表.md
- */
- @Slf4j
- @Service
- public class ApprovalWriteBackService {
- @Autowired
- private YDClient ydClient;
- @Autowired
- private YDService ydService;
- @Autowired
- private WHConf whConf;
- private static final int THREAD_POOL_SIZE = 10;
- private static final int MAX_RETRY = 2;
- /**
- * 明细审批意见「同意」取值(单选,已确认)
- */
- 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=同意
- * @return 回写结果统计
- */
- public ApprovalWriteBackResult writeBack(String formInstanceId, int result) {
- McException.assertAccessException(StringUtils.isBlank(formInstanceId), "实例ID不能为空");
- // 1. 按实例ID查审批单表单详情(表单接口 retrieve_id),含主表 + 内联子表(子表内联上限 50)
- Map approvalFormData = ydClient.queryData(YDParam.builder()
- .appType(whConf.getYidaAppType())
- .systemToken(whConf.getYidaSystemToken())
- .formInstanceId(formInstanceId)
- .build(), YDConf.FORM_QUERY.retrieve_id).getFormData();
- log.info("[审批回写] 审批单详情 formInstanceId={} keys={}", formInstanceId,
- approvalFormData == null ? null : approvalFormData.keySet());
- McException.assertAccessException(approvalFormData == null || approvalFormData.isEmpty(),
- "未查询到审批单数据,实例ID=" + formInstanceId);
- // 2. 类型判定:工时审批含「项目工时明细」子表 tableField_mmae8t99,其他工时审批没有
- boolean isOther = !approvalFormData.containsKey(AP_BIL_TABLE);
- boolean approveWhole = (result == 1);
- // 3. 取审批单明细(内联子表 <50 直接用,==50 才 queryDetails 递归取全,避免无效请求)
- List<DetailRow> rows = new ArrayList<>();
- if (isOther) {
- rows.addAll(loadDetailRows(formInstanceId, approvalFormData, Cat.OTHER, approveWhole));
- } else {
- rows.addAll(loadDetailRows(formInstanceId, approvalFormData, Cat.BILLABLE, approveWhole));
- rows.addAll(loadDetailRows(formInstanceId, approvalFormData, Cat.NON_BILLABLE, approveWhole));
- }
- // 4. 按汇总表记录键(填报人 + 工时日期文本)分组
- Map<String, List<DetailRow>> groups = new LinkedHashMap<>();
- for (DetailRow r : rows) {
- if (StringUtils.isBlank(r.userId) || StringUtils.isBlank(r.dayText)) {
- 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(() -> {
- for (int retry = 0; retry <= MAX_RETRY; retry++) {
- try {
- int[] r = applyToRecord(userId, dayText, groupRows, limiter);
- if (r == null) {
- // 未命中汇总表记录
- break;
- }
- hitRecords.incrementAndGet();
- updatedRows.addAndGet(r[0]);
- missRows.addAndGet(r[1]);
- return;
- } catch (Exception ex) {
- if (retry < MAX_RETRY) {
- sleep(1000L * (retry + 1));
- } else {
- 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();
- }
- 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(failRecords.get())
- .build();
- log.info("[审批回写] 完成 {}", stats);
- return stats;
- }
- /**
- * 处理单条汇总表日记录
- *
- * @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();
- String summaryInstId = null;
- for (Map item : 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)))) {
- summaryInstId = String.valueOf(item.get("formInstanceId"));
- break;
- }
- }
- 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, 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;
- }
- if (dr.approve) {
- target.put(approvedField, dr.hours);
- } else {
- target.put(submittedField, 0);
- }
- 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);
- // 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};
- }
- /**
- * 加载审批单某类别明细(内联子表优先,必要时递归取全)
- */
- private List<DetailRow> loadDetailRows(String formInstanceId, Map approvalFormData, Cat cat, boolean approveWhole) {
- String tableId = apTable(cat);
- List<Map> raw = resolveDetailRows(formInstanceId, 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));
- // 有效意见:整单拒绝/撤销 → 全拒绝;整单同意 → 看单条明细意见
- dr.approve = approveWhole && OPINION_APPROVE.equals(opinion);
- list.add(dr);
- }
- return list;
- }
- /**
- * 子表取数控制:主表详情已内联返回子表时,行数 <50 视为完整直接用;
- * ==50(可能被宜搭截断)或内联缺失,才调 {@link YDService#queryDetails} 递归取全,避免无效请求。
- * 复用 ydService 封装,不重复造分页轮子。
- */
- private List<Map> resolveDetailRows(String formInstanceId, String tableId, List<Map> inlineRows) {
- if (inlineRows != null && inlineRows.size() < 50) {
- return inlineRows;
- }
- return ydService.queryDetails(YDParam.builder()
- .appType(whConf.getYidaAppType())
- .systemToken(whConf.getYidaSystemToken())
- .formInstanceId(formInstanceId)
- .tableFieldId(tableId)
- .pageNumber(1)
- .build());
- }
- /**
- * 重建子表行:保留全部字段,成员/关联字段用 _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();
- }
- }
- }
|