|
|
@@ -5,6 +5,7 @@ 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;
|
|
|
@@ -30,7 +31,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|
|
* - 同意 → 对应子表行「已审批工时」= 该明细工时
|
|
|
* 主表合计按子表行重算(幂等,撤销/重发不翻倍)。
|
|
|
* <p>
|
|
|
- * 设计文档:后端/阿科德斯/审批结果回写工时汇总表.md
|
|
|
+ * 设计文档:后端/阿科德斯/审批结果回写工时汇总表/02-程序实现与接口.md
|
|
|
*/
|
|
|
@Slf4j
|
|
|
@Service
|
|
|
@@ -119,6 +120,17 @@ public class ApprovalWriteBackService {
|
|
|
boolean approve; // 有效意见:true=同意, false=拒绝
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 异步触发审批回写,供流程结束自动化调用。
|
|
|
+ *
|
|
|
+ * @param formInstanceId 审批表单实例 ID
|
|
|
+ * @param result 审批结果:0=拒绝/撤销,1=同意
|
|
|
+ */
|
|
|
+ @Async
|
|
|
+ public void writeBackAsync(String formInstanceId, int result) {
|
|
|
+ writeBack(formInstanceId, result);
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 审批结果回写工时汇总表
|
|
|
*
|
|
|
@@ -126,32 +138,33 @@ public class ApprovalWriteBackService {
|
|
|
* @param result 审批结果:0=拒绝/撤销,1=同意
|
|
|
* @return 回写结果统计
|
|
|
*/
|
|
|
- @Async
|
|
|
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();
|
|
|
+ // 1. 流程详情接口优先,流程表单可直接返回完整子表;旧回调传表单实例ID时回退表单详情接口。
|
|
|
+ Map approvalFormData = queryApprovalFormData(formInstanceId);
|
|
|
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);
|
|
|
+ // 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, approvalFormData, Cat.OTHER, approveWhole));
|
|
|
+ rows.addAll(loadDetailRows(
|
|
|
+ formInstanceId, approvalFormUuid, approvalFormData, Cat.OTHER, approveWhole));
|
|
|
} else {
|
|
|
- rows.addAll(loadDetailRows(formInstanceId, approvalFormData, Cat.BILLABLE, approveWhole));
|
|
|
- rows.addAll(loadDetailRows(formInstanceId, approvalFormData, Cat.NON_BILLABLE, approveWhole));
|
|
|
+ rows.addAll(loadDetailRows(
|
|
|
+ formInstanceId, approvalFormUuid, approvalFormData, Cat.BILLABLE, approveWhole));
|
|
|
+ rows.addAll(loadDetailRows(
|
|
|
+ formInstanceId, approvalFormUuid, approvalFormData, Cat.NON_BILLABLE, approveWhole));
|
|
|
}
|
|
|
|
|
|
// 4. 按汇总表记录键(填报人 + 工时日期文本)分组
|
|
|
@@ -182,12 +195,16 @@ public class ApprovalWriteBackService {
|
|
|
try {
|
|
|
int[] r = applyToRecord(userId, dayText, groupRows, limiter);
|
|
|
if (r == null) {
|
|
|
- // 未命中汇总表记录
|
|
|
- break;
|
|
|
+ // fixme 审批明细存在但汇总表日记录缺失属于失败,不能把审批单标记为「全部成功」。
|
|
|
+ failRecords.incrementAndGet();
|
|
|
+ return;
|
|
|
}
|
|
|
hitRecords.incrementAndGet();
|
|
|
updatedRows.addAndGet(r[0]);
|
|
|
missRows.addAndGet(r[1]);
|
|
|
+ if (r[1] > 0) {
|
|
|
+ failRecords.incrementAndGet();
|
|
|
+ }
|
|
|
return;
|
|
|
} catch (Exception ex) {
|
|
|
if (retry < MAX_RETRY) {
|
|
|
@@ -241,6 +258,113 @@ public class ApprovalWriteBackService {
|
|
|
return stats;
|
|
|
}
|
|
|
|
|
|
+ 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 任一为空则跳过该类别回写,允许业务方分阶段在宜搭后台建字段
|
|
|
@@ -290,16 +414,25 @@ public class ApprovalWriteBackService {
|
|
|
SUMMARY_SUBMITTER_ID + ", " + SUMMARY_DAY_TEXT, userId, dayText)))
|
|
|
.build(), YDConf.FORM_QUERY.retrieve_search_form).getData();
|
|
|
|
|
|
- String summaryInstId = null;
|
|
|
- for (Map item : found) {
|
|
|
+ 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)))) {
|
|
|
- summaryInstId = String.valueOf(item.get("formInstanceId"));
|
|
|
- break;
|
|
|
+ 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;
|
|
|
@@ -337,7 +470,11 @@ public class ApprovalWriteBackService {
|
|
|
|
|
|
// 子表取数控制:内联 <50 直接用,==50 才递归取全,避免全量覆盖时漏行被删
|
|
|
List<Map> rebuilt = rebuildRows(
|
|
|
- resolveDetailRows(summaryInstId, subTable, (List<Map>) formData.get(subTable)));
|
|
|
+ resolveDetailRows(
|
|
|
+ summaryInstId,
|
|
|
+ whConf.getFormUuidWorkHoursSummary(),
|
|
|
+ subTable,
|
|
|
+ (List<Map>) formData.get(subTable)));
|
|
|
|
|
|
for (DetailRow dr : e.getValue()) {
|
|
|
Map target = null;
|
|
|
@@ -388,6 +525,15 @@ public class ApprovalWriteBackService {
|
|
|
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();
|
|
|
@@ -403,12 +549,40 @@ public class ApprovalWriteBackService {
|
|
|
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 "暂存";
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 加载审批单某类别明细(内联子表优先,必要时递归取全)
|
|
|
*/
|
|
|
- private List<DetailRow> loadDetailRows(String formInstanceId, Map approvalFormData, Cat cat, boolean approveWhole) {
|
|
|
+ private List<DetailRow> loadDetailRows(String formInstanceId,
|
|
|
+ String formUuid,
|
|
|
+ Map approvalFormData,
|
|
|
+ Cat cat,
|
|
|
+ boolean approveWhole) {
|
|
|
String tableId = apTable(cat);
|
|
|
- List<Map> raw = resolveDetailRows(formInstanceId, tableId, (List<Map>) approvalFormData.get(tableId));
|
|
|
+ List<Map> raw = resolveDetailRows(
|
|
|
+ formInstanceId,
|
|
|
+ formUuid,
|
|
|
+ tableId,
|
|
|
+ (List<Map>) approvalFormData.get(tableId));
|
|
|
|
|
|
List<DetailRow> list = new ArrayList<>();
|
|
|
for (Map row : raw) {
|
|
|
@@ -419,13 +593,17 @@ public class ApprovalWriteBackService {
|
|
|
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);
|
|
|
+ // 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} 递归取全,避免无效请求。
|
|
|
@@ -435,7 +613,10 @@ public class ApprovalWriteBackService {
|
|
|
* 直接返回空 list,不再调 queryDetails——基座 _queryDetails 对宜搭返回 data=null 的空子表查询会触发
|
|
|
* ArrayList.addAll(null) NPE。queryDetails 返回 null 同样兜底空 list。
|
|
|
*/
|
|
|
- private List<Map> resolveDetailRows(String formInstanceId, String tableId, List<Map> inlineRows) {
|
|
|
+ private List<Map> resolveDetailRows(String formInstanceId,
|
|
|
+ String formUuid,
|
|
|
+ String tableId,
|
|
|
+ List<Map> inlineRows) {
|
|
|
if (inlineRows == null) {
|
|
|
return Collections.emptyList();
|
|
|
}
|
|
|
@@ -445,11 +626,16 @@ public class ApprovalWriteBackService {
|
|
|
List<Map> full = ydService.queryDetails(YDParam.builder()
|
|
|
.appType(whConf.getYidaAppType())
|
|
|
.systemToken(whConf.getYidaSystemToken())
|
|
|
+ .formUuid(formUuid)
|
|
|
.formInstanceId(formInstanceId)
|
|
|
.tableFieldId(tableId)
|
|
|
.pageNumber(1)
|
|
|
.build());
|
|
|
- return full != null ? full : Collections.emptyList();
|
|
|
+ McException.assertAccessException(
|
|
|
+ full == null || full.size() < inlineRows.size(),
|
|
|
+ "审批单子表完整数据获取失败 instanceId=" + formInstanceId
|
|
|
+ + ", tableFieldId=" + tableId);
|
|
|
+ return full;
|
|
|
}
|
|
|
|
|
|
/**
|