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.service.dingtalk.DDClient;
import com.malk.service.dingtalk.DDClient_Contacts;
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.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
/**
* 补发起工时审批(bug 1 兜底)
*
* 场景:汇总表 submitted > 0 但对应 (userId, day) 未出现在任何该月工时审批 / 其他工时审批的子表明细中,
* 即前端 submitApprovalProcess 链路失败(startInstance 挂了/静默丢),导致部分员工工时从未进入审批。
*
* 逻辑完全参考前端 TimeCard 的 submitApprovalProcess:
* 1. collectApprovalData — 遍历汇总表每天子表,delta = submitted - approved - change, delta>0 才 push;
* 普通 PM 按审批人分组,Raymond 兜底按项目拆组
* 2. processManagerApproval — queryProcess(month + PM + RUNNING) 有同项目可用分片则 update / 无则 create
* COMPLETED 明细先进入覆盖索引;汇总尚未落值时归回写处理,不重复发起
* 3. collectOtherApprovalData / processOtherApproval — 其他工时按当前员工的部门主管作 approver,同样 query→update/create
*
* 发起人 userId 必须用真人钉钉账号(建议 System),不能用 yida_pub_account(不触发钉钉待办通知)
*
* 设计文档:后端/阿科德斯/审批链路补发起.md
*/
@Slf4j
@Service
public class ReSubmitApprovalService {
@Autowired
private YDClient ydClient;
@Autowired
private YDService ydService;
@Autowired
private WHConf whConf;
@Autowired
private DDClient ddClient;
@Autowired
private DDClient_Contacts ddClient_contacts;
/**
* 前端 v2.35 常量:发起时明细「审批意见」默认「同意」
*/
private static final String APPROVAL_OPINION_AGREE = "同意";
private static final String SUMMARY_STATUS_IN_APPROVAL = "审批中";
private static final String SUMMARY_STATUS_DRAFT = "暂存";
private static final String SUMMARY_STATUS_APPROVED = "已审批";
private static final String S_BIL_SUBMITTED_TOTAL = "numberField_mmacxewa";
private static final String S_BIL_APPROVED_TOTAL = "numberField_mmacxewg";
private static final String S_NON_SUBMITTED_TOTAL = "numberField_mmad6jbz";
private static final String S_NON_APPROVED_TOTAL = "numberField_mmad6jc1";
private static final String S_OTHER_SUBMITTED_TOTAL = "numberField_mmd1smeq";
private static final String S_OTHER_APPROVED_TOTAL = "numberField_mmd1smes";
/**
* 宜搭单个子表组件可稳定查询和覆盖更新的最大行数。
*/
private static final int MAX_APPROVAL_SUBTABLE_ROWS = 500;
/**
* 批量历史补偿的目标分片大小;以员工整组为边界,允许单个员工整组略超目标值。
*/
private static final int TARGET_APPROVAL_SUBTABLE_ROWS = 300;
/**
* 兜底部门主管 userId(Raymond,前端 v2.42 保持一致)
*/
private static final String DEFAULT_MANAGER_ID = "634735129";
private static final String PERSONNEL_EMPLOYEE = "employeeField_mkow4ydp";
private static final String PERSONNEL_MANAGER = "employeeField_mh8xhqc3";
// ===== 汇总表字段(与 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 runningKeys = new HashSet<>();
Map completedAgreeHours = new HashMap<>();
boolean shouldSkip(String key, double accountedHours) {
if (runningKeys.contains(key)) {
return true;
}
return completedAgreeHours.getOrDefault(key, 0d) > accountedHours + 1e-6;
}
}
/**
* 某员工某月的项目工时组;Raymond 兜底组带项目标识做项目隔离
*/
private static class ManagerGroup {
String managerId;
String projectCode;
String projectName;
List billableRows = new ArrayList<>();
List nonBillableRows = new ArrayList<>();
double billableTotal = 0;
double nonBillableTotal = 0;
}
/**
* 对指定月份、指定员工(可空)执行补发起
*
* @param monthText yyyyMM
* @param submitterUserId 可空; 空则处理该月所有员工
* @return 结果统计
*/
public ReSubmitApprovalResult resubmit(String monthText, String submitterUserId) {
return resubmit(monthText, submitterUserId, false);
}
/**
* 对指定月份、指定员工执行补发起或只读预演。
*
* @param monthText yyyyMM
* @param submitterUserId 可空;空则处理该月所有员工
* @param dryRun true 时只统计候选,不创建或更新审批
* @return 执行或预演统计
*/
public ReSubmitApprovalResult resubmit(String monthText,
String submitterUserId,
boolean dryRun) {
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 search = new HashMap<>();
search.put(S_MONTH_TEXT, monthText);
if (StringUtils.isNotBlank(submitterUserId)) {
search.put(S_SUBMITTER_ID, submitterUserId);
}
List