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;
/**
* 审批结果回写工时汇总表
*
* 审批结束或撤销后调用,入参为流程实例ID + 审批结果(0=拒绝/撤销,1=同意)。
* 工时审批含两个明细子表(项目/非项目工时),其他工时审批含一个明细子表。
* 明细按「填报人 + 工时日期(天) + 项目编号/Activity」1:1 对应工时汇总表(人+天一条)里的子表行:
* - 拒绝 → 对应子表行「已提交工时」置 0
* - 同意 → 对应子表行「已审批工时」= 该明细工时
* 主表合计按子表行重算(幂等,撤销/重发不翻倍)。
*
* 设计文档:后端/阿科德斯/审批结果回写工时汇总表.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 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> 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> futures = new ArrayList<>();
for (Map.Entry> e : groups.entrySet()) {
List 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 groupRows, RateLimiter limiter) {
// a. 定位汇总表记录(填报人ID + 工时日期文本,代码内精确过滤)
limiter.acquire();
List