Pārlūkot izejas kodu

feat(workhours): 应填报工时全量+增量双模式定时任务

- 全量同步:每月1号凌晨2点,查询全部项目档案生成当月工时记录
- 增量同步:每月2~31号凌晨3点,按modifiedTimeGMT筛选最近2天修改的项目档案
- 新增WHConf配置类、WorkHoursCalcService核心服务、WorkHoursCalcTimer定时任务
- BeisenApplication ComponentScan加入workhours包
- application-dev.yml添加workhours配置块

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
malk 3 nedēļas atpakaļ
vecāks
revīzija
c8e0d33b55

+ 2 - 0
mjava-akdsbeisen/src/main/java/com/malk/BeisenApplication.java

@@ -19,9 +19,11 @@ import org.springframework.scheduling.annotation.EnableScheduling;
     "com.malk.service.beisen",
     "com.malk.service.aliwork",
     "com.malk.service.dingtalk",
+    "com.malk.service.workhours",
     "com.malk.server.beisen",
     "com.malk.server.aliwork",
     "com.malk.server.dingtalk",
+    "com.malk.server.workhours",
     "com.malk.server.common",
     "com.malk.delegate",
     "com.malk.timer",

+ 25 - 0
mjava-akdsbeisen/src/main/java/com/malk/server/workhours/WHConf.java

@@ -0,0 +1,25 @@
+package com.malk.server.workhours;
+
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Data
+@Component
+@ConfigurationProperties(prefix = "workhours")
+@Slf4j
+public class WHConf {
+
+    private String formUuidProject;
+
+    private String formUuidHoliday;
+
+    private String formUuidPersonnel;
+
+    private String formUuidRequiredHours;
+
+    private String yidaAppType;
+
+    private String yidaSystemToken;
+}

+ 477 - 0
mjava-akdsbeisen/src/main/java/com/malk/service/workhours/WorkHoursCalcService.java

@@ -0,0 +1,477 @@
+package com.malk.service.workhours;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.malk.server.aliwork.YDConf;
+import com.malk.server.aliwork.YDParam;
+import com.malk.server.dingtalk.DDR_New;
+import com.malk.server.workhours.WHConf;
+import com.malk.service.aliwork.YDClient;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.DayOfWeek;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+@Slf4j
+@Service
+public class WorkHoursCalcService {
+
+    @Autowired
+    private YDClient ydClient;
+
+    @Autowired
+    private WHConf whConf;
+
+    private static final int DAILY_HOURS = 8;
+
+    /**
+     * 主入口:计算指定月份每个工作日的应填报工时并写入宜搭(按天维度,每条记录8h)
+     *
+     * @param targetMonth 目标月份,null 则默认当前月
+     */
+    public void calculateAndSyncMonthlyHours(LocalDate targetMonth) {
+        if (targetMonth == null) {
+            targetMonth = LocalDate.now();
+        }
+        int year = targetMonth.getYear();
+        int month = targetMonth.getMonthValue();
+        log.info("开始计算{}年{}月应填报工时(按天维度)", year, month);
+
+        // 1. 查询项目档案,提取所有员工及经理映射
+        Map<String, String> employeeManagerMap = queryAllEmployeesFromProjects();
+        log.info("从项目档案获取到{}名员工", employeeManagerMap.size());
+        if (employeeManagerMap.isEmpty()) {
+            log.warn("项目档案中未查询到任何员工,跳过");
+            return;
+        }
+
+        // 2. 查询节假日规则
+        Map<LocalDate, String> holidayRules = queryHolidayRules(String.valueOf(year));
+        log.info("{}年节假日规则共{}条", year, holidayRules.size());
+
+        // 3. 计算当月所有工作日列表
+        List<LocalDate> workingDays = getWorkingDays(year, month, holidayRules);
+        log.info("{}年{}月工作日{}天", year, month, workingDays.size());
+
+        // 4. 一次性查询人员档案全量,内存中按 userId 匹配
+        Map<String, Map<String, Object>> personnelMap = queryAllPersonnelDetails();
+        log.info("人员档案共{}条", personnelMap.size());
+
+        // 5. 按员工 × 工作日逐条 upsert
+        int successCount = 0;
+        int failCount = 0;
+        for (Map.Entry<String, String> entry : employeeManagerMap.entrySet()) {
+            String employeeId = entry.getKey();
+            String managerId = entry.getValue();
+            // 从预加载的人员档案集合中 find 匹配,避免重复查询
+            Map<String, Object> personnelInfo = personnelMap.getOrDefault(employeeId, Collections.emptyMap());
+
+            for (LocalDate workDay : workingDays) {
+                try {
+                    upsertDailyHours(employeeId, managerId, workDay, personnelInfo);
+                    successCount++;
+                } catch (Exception e) {
+                    failCount++;
+                    log.error("员工{} {} 写入失败", employeeId, workDay, e);
+                }
+            }
+        }
+        log.info("应填报工时写入完成: 成功{}条, 失败{}条({}名员工 × {}个工作日)",
+                successCount, failCount, employeeManagerMap.size(), workingDays.size());
+    }
+
+    /**
+     * 增量同步:查询最近 N 天内修改过的项目档案,仅同步这些项目中的员工
+     *
+     * @param daysBack 回溯天数(默认2天)
+     */
+    public void incrementalSync(int daysBack) {
+        LocalDate today = LocalDate.now();
+        int year = today.getYear();
+        int month = today.getMonthValue();
+        LocalDate fromDate = today.minusDays(daysBack);
+        // modifiedToTimeGMT 默认0点,需要加1天确保包含当天
+        LocalDate toDate = today.plusDays(1);
+
+        log.info("开始增量同步: 查询{}~{}修改的项目档案, 同步{}年{}月数据",
+                fromDate, toDate, year, month);
+
+        // 1. 查询最近修改的项目档案,提取变动员工
+        Map<String, String> employeeManagerMap = queryEmployeesFromRecentProjects(fromDate, toDate);
+        log.info("增量: 从最近修改的项目档案获取到{}名员工", employeeManagerMap.size());
+        if (employeeManagerMap.isEmpty()) {
+            log.info("无项目档案变动,增量同步跳过");
+            return;
+        }
+
+        // 2~5 同全量逻辑
+        Map<LocalDate, String> holidayRules = queryHolidayRules(String.valueOf(year));
+        List<LocalDate> workingDays = getWorkingDays(year, month, holidayRules);
+        log.info("{}年{}月工作日{}天", year, month, workingDays.size());
+
+        Map<String, Map<String, Object>> personnelMap = queryAllPersonnelDetails();
+
+        int successCount = 0;
+        int failCount = 0;
+        for (Map.Entry<String, String> entry : employeeManagerMap.entrySet()) {
+            String employeeId = entry.getKey();
+            String managerId = entry.getValue();
+            Map<String, Object> personnelInfo = personnelMap.getOrDefault(employeeId, Collections.emptyMap());
+
+            for (LocalDate workDay : workingDays) {
+                try {
+                    upsertDailyHours(employeeId, managerId, workDay, personnelInfo);
+                    successCount++;
+                } catch (Exception e) {
+                    failCount++;
+                    log.error("增量-员工{} {} 写入失败", employeeId, workDay, e);
+                }
+            }
+        }
+        log.info("增量同步完成: 成功{}条, 失败{}条({}名员工 × {}个工作日)",
+                successCount, failCount, employeeManagerMap.size(), workingDays.size());
+    }
+
+    /**
+     * 获取指定月份的所有工作日列表
+     */
+    private List<LocalDate> getWorkingDays(int year, int month, Map<LocalDate, String> holidayRules) {
+        List<LocalDate> workingDays = new ArrayList<>();
+        int daysInMonth = LocalDate.of(year, month, 1).lengthOfMonth();
+
+        for (int day = 1; day <= daysInMonth; day++) {
+            LocalDate current = LocalDate.of(year, month, day);
+            DayOfWeek dow = current.getDayOfWeek();
+            boolean isWeekend = (dow == DayOfWeek.SATURDAY || dow == DayOfWeek.SUNDAY);
+            String holidayType = holidayRules.get(current);
+
+            if ("调休".equals(holidayType)) {
+                continue;
+            } else if ("加班".equals(holidayType)) {
+                workingDays.add(current);
+            } else if (!isWeekend) {
+                workingDays.add(current);
+            }
+        }
+        return workingDays;
+    }
+
+    /**
+     * 查询项目档案,提取所有唯一员工及对应经理
+     *
+     * @return Map<employeeUserId, managerUserId>
+     */
+    @SuppressWarnings("unchecked")
+    private Map<String, String> queryAllEmployeesFromProjects() {
+        Map<String, String> employeeManagerMap = new LinkedHashMap<>();
+        String appType = whConf.getYidaAppType();
+        String systemToken = whConf.getYidaSystemToken();
+
+        int currentPage = 1;
+        int pageSize = YDConf.PAGE_SIZE_LIMIT;
+        long totalCount;
+
+        do {
+            DDR_New result = ydClient.queryData(YDParam.builder()
+                    .appType(appType)
+                    .systemToken(systemToken)
+                    .formUuid(whConf.getFormUuidProject())
+                    .currentPage(currentPage)
+                    .pageSize(pageSize)
+                    .build(), YDConf.FORM_QUERY.retrieve_list_all);
+
+            totalCount = result.getTotalCount();
+            List<Map> dataList = (List<Map>) result.getData();
+            if (dataList == null || dataList.isEmpty()) break;
+
+            for (Map item : dataList) {
+                Map<String, Object> formData = (Map<String, Object>) item.get("formData");
+                if (formData == null) continue;
+
+                Object subTableObj = formData.get("tableField_mkowyn6d");
+                if (subTableObj instanceof List) {
+                    List<Map<String, Object>> subTable = (List<Map<String, Object>>) subTableObj;
+                    for (Map<String, Object> row : subTable) {
+                        String empId = extractEmployeeId(row, "employeeField_mmbfe0ij");
+                        String mgrId = extractEmployeeId(row, "employeeField_mkoxpswf");
+                        if (empId != null && !empId.isEmpty()) {
+                            employeeManagerMap.putIfAbsent(empId, mgrId != null ? mgrId : "");
+                        }
+                    }
+                }
+            }
+            currentPage++;
+        } while ((long) (currentPage - 1) * pageSize < totalCount);
+
+        return employeeManagerMap;
+    }
+
+    /**
+     * 查询指定时间范围内修改过的项目档案,提取员工及对应经理
+     *
+     * @param fromDate 修改开始日期(含)
+     * @param toDate   修改结束日期(不含,API 默认0点,需+1天)
+     * @return Map<employeeUserId, managerUserId>
+     */
+    @SuppressWarnings("unchecked")
+    private Map<String, String> queryEmployeesFromRecentProjects(LocalDate fromDate, LocalDate toDate) {
+        Map<String, String> employeeManagerMap = new LinkedHashMap<>();
+        String appType = whConf.getYidaAppType();
+        String systemToken = whConf.getYidaSystemToken();
+        DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+        int currentPage = 1;
+        int pageSize = YDConf.PAGE_SIZE_LIMIT;
+        long totalCount;
+
+        do {
+            DDR_New result = ydClient.queryData(YDParam.builder()
+                    .appType(appType)
+                    .systemToken(systemToken)
+                    .formUuid(whConf.getFormUuidProject())
+                    .currentPage(currentPage)
+                    .pageSize(pageSize)
+                    .modifiedFromTimeGMT(fromDate.format(fmt))
+                    .modifiedToTimeGMT(toDate.format(fmt))
+                    .build(), YDConf.FORM_QUERY.retrieve_list_all);
+
+            totalCount = result.getTotalCount();
+            List<Map> dataList = (List<Map>) result.getData();
+            if (dataList == null || dataList.isEmpty()) break;
+
+            log.info("增量: 第{}页查到{}条修改的项目档案", currentPage, dataList.size());
+
+            for (Map item : dataList) {
+                Map<String, Object> formData = (Map<String, Object>) item.get("formData");
+                if (formData == null) continue;
+
+                Object subTableObj = formData.get("tableField_mkowyn6d");
+                if (subTableObj instanceof List) {
+                    List<Map<String, Object>> subTable = (List<Map<String, Object>>) subTableObj;
+                    for (Map<String, Object> row : subTable) {
+                        String empId = extractEmployeeId(row, "employeeField_mmbfe0ij");
+                        String mgrId = extractEmployeeId(row, "employeeField_mkoxpswf");
+                        if (empId != null && !empId.isEmpty()) {
+                            employeeManagerMap.putIfAbsent(empId, mgrId != null ? mgrId : "");
+                        }
+                    }
+                }
+            }
+            currentPage++;
+        } while ((long) (currentPage - 1) * pageSize < totalCount);
+
+        return employeeManagerMap;
+    }
+
+    /**
+     * 查询节假日规则(按年份)
+     *
+     * @return Map<日期, 类型("调休"/"加班")>
+     */
+    @SuppressWarnings("unchecked")
+    private Map<LocalDate, String> queryHolidayRules(String year) {
+        Map<LocalDate, String> rules = new HashMap<>();
+        String appType = whConf.getYidaAppType();
+        String systemToken = whConf.getYidaSystemToken();
+
+        Map<String, String> searchField = new HashMap<>();
+        searchField.put("textField_mn76yxgb", year);
+
+        List<Map> dataList = (List<Map>) ydClient.queryData(YDParam.builder()
+                .appType(appType)
+                .systemToken(systemToken)
+                .formUuid(whConf.getFormUuidHoliday())
+                .searchFieldJson(JSON.toJSONString(searchField))
+                .pageSize(YDConf.PAGE_SIZE_LIMIT)
+                .build(), YDConf.FORM_QUERY.retrieve_search_form).getData();
+
+        if (dataList == null) return rules;
+
+        for (Map item : dataList) {
+            Map<String, Object> formData = (Map<String, Object>) item.get("formData");
+            if (formData == null) continue;
+
+            Object dateObj = formData.get("datefield_lKuSAJ2y");
+            Object typeObj = formData.get("radiofield_sEPqCTex");
+
+            if (dateObj != null && typeObj != null) {
+                LocalDate date = parseToLocalDate(dateObj);
+                if (date != null) {
+                    rules.put(date, typeObj.toString());
+                }
+            }
+        }
+        return rules;
+    }
+
+    /**
+     * 一次性查询人员档案全量,构建 userId → 员工信息 映射
+     * ppExt: 预加载到内存,后续通过 Map.get() 匹配,避免逐个员工重复查询 API
+     */
+    @SuppressWarnings("unchecked")
+    private Map<String, Map<String, Object>> queryAllPersonnelDetails() {
+        Map<String, Map<String, Object>> personnelMap = new HashMap<>();
+        String appType = whConf.getYidaAppType();
+        String systemToken = whConf.getYidaSystemToken();
+
+        int currentPage = 1;
+        int pageSize = YDConf.PAGE_SIZE_LIMIT;
+        long totalCount;
+
+        do {
+            DDR_New result = ydClient.queryData(YDParam.builder()
+                    .appType(appType)
+                    .systemToken(systemToken)
+                    .formUuid(whConf.getFormUuidPersonnel())
+                    .currentPage(currentPage)
+                    .pageSize(pageSize)
+                    .build(), YDConf.FORM_QUERY.retrieve_search_form);
+
+            totalCount = result.getTotalCount();
+            List<Map> dataList = (List<Map>) result.getData();
+            if (dataList == null || dataList.isEmpty()) break;
+
+            for (Map item : dataList) {
+                Map<String, Object> formData = (Map<String, Object>) item.get("formData");
+                if (formData == null) continue;
+
+                String empId = extractEmployeeId(formData, "employeeField_mkow4ydp");
+                if (empId != null && !empId.isEmpty()) {
+                    Map<String, Object> info = new HashMap<>();
+                    info.put("radioField_mkow4ydo", formData.get("radioField_mkow4ydo"));
+                    info.put("textField_mh8xhqc1", formData.get("textField_mh8xhqc1"));
+                    info.put("departmentSelectField_mkow4ydr", formData.get("departmentSelectField_mkow4ydr_id"));
+                    info.put("textField_mmekrcji", formData.get("textField_mmekrcji"));
+                    info.put("employeeField_mh8xhqc3", formData.get("employeeField_mh8xhqc3_id"));
+                    personnelMap.put(empId, info);
+                }
+            }
+            currentPage++;
+        } while ((long) (currentPage - 1) * pageSize < totalCount);
+
+        return personnelMap;
+    }
+
+    /**
+     * 按员工+日期 upsert 写入单日应填报工时(8h)
+     */
+    private void upsertDailyHours(String employeeId, String managerId, LocalDate workDay,
+                                  Map<String, Object> personnelInfo) {
+        String appType = whConf.getYidaAppType();
+        String systemToken = whConf.getYidaSystemToken();
+        long dayTimestamp = workDay.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
+
+        // 构建写入数据
+        JSONObject formData = new JSONObject();
+        formData.put("employeeField_mmd8onl4", Arrays.asList(employeeId));
+        formData.put("dateField_mmd8onl5", dayTimestamp);
+        formData.put("numberField_mmd8onl6", DAILY_HOURS);
+
+        // 经理:优先项目档案子表经理,兜底人员档案主管
+        if (managerId != null && !managerId.isEmpty()) {
+            formData.put("employeeField_mh8xhqc3", Arrays.asList(managerId));
+        } else if (personnelInfo.get("employeeField_mh8xhqc3") instanceof List) {
+            formData.put("employeeField_mh8xhqc3", personnelInfo.get("employeeField_mh8xhqc3"));
+        }
+
+        // 人员档案补充字段
+        if (!personnelInfo.isEmpty()) {
+            putIfNotNull(formData, "textField_mh8xhqc1", personnelInfo.get("textField_mh8xhqc1"));
+            putIfNotNull(formData, "radioField_mkow4ydo", personnelInfo.get("radioField_mkow4ydo"));
+            // 部门选择器字段:需要数组格式 ["deptId"],与成员字段格式一致
+            Object deptValue = personnelInfo.get("departmentSelectField_mkow4ydr");
+            if (deptValue != null) {
+                if (deptValue instanceof List) {
+                    formData.put("departmentSelectField_mkow4ydr", deptValue);
+                } else {
+                    String deptStr = String.valueOf(deptValue).trim();
+                    if (!deptStr.isEmpty()) {
+                        formData.put("departmentSelectField_mkow4ydr", Arrays.asList(deptStr));
+                    }
+                }
+            }
+            putIfNotNull(formData, "textField_mmekrcji", personnelInfo.get("textField_mmekrcji"));
+        }
+
+        // fixme: 日期组件在 searchCondition 中必须使用数组格式 [start, end]
+        JSONObject searchCondition = new JSONObject();
+        searchCondition.put("employeeField_mmd8onl4", employeeId);
+        searchCondition.put("dateField_mmd8onl5", Arrays.asList(String.valueOf(dayTimestamp), String.valueOf(dayTimestamp)));
+
+        YDParam param = YDParam.builder()
+                .appType(appType)
+                .systemToken(systemToken)
+                .formUuid(whConf.getFormUuidRequiredHours())
+                .searchCondition(JSON.toJSONString(searchCondition))
+                .formDataJson(formData.toJSONString())
+                .noExecuteExpression(false)
+                .build();
+
+        Object result = ydClient.operateData(param, YDConf.FORM_OPERATION.upsert);
+        log.debug("员工{} {} 写入8h,结果: {}", employeeId, workDay, JSON.toJSONString(result));
+    }
+
+    // ==================== 工具方法 ====================
+
+    private void putIfNotNull(JSONObject target, String key, Object value) {
+        if (value != null && !"".equals(String.valueOf(value).trim())) {
+            target.put(key, value);
+        }
+    }
+
+    /**
+     * 从 formData 中提取员工字段的 userId
+     * ppExt: 员工字段返回格式可能为 userId字符串、JSON数组字符串 或 _id 后缀字段
+     */
+    @SuppressWarnings("unchecked")
+    private String extractEmployeeId(Map<String, Object> data, String fieldId) {
+        Object idValue = data.get(fieldId + "_id");
+        if (idValue == null) {
+            idValue = data.get(fieldId);
+        }
+        if (idValue == null) return null;
+
+        if (idValue instanceof List) {
+            List list = (List) idValue;
+            return list.isEmpty() ? null : String.valueOf(list.get(0));
+        }
+        String str = String.valueOf(idValue);
+        if (str.startsWith("[")) {
+            try {
+                List<String> list = JSON.parseArray(str, String.class);
+                return list.isEmpty() ? null : list.get(0);
+            } catch (Exception e) {
+                return null;
+            }
+        }
+        return str;
+    }
+
+    /**
+     * 将日期对象转为 LocalDate(支持时间戳和日期字符串)
+     */
+    private LocalDate parseToLocalDate(Object dateObj) {
+        if (dateObj == null) return null;
+        try {
+            String dateStr = String.valueOf(dateObj).trim();
+            if (dateStr.isEmpty()) return null;
+
+            if (dateStr.matches("\\d+")) {
+                long timestamp = Long.parseLong(dateStr);
+                return Instant.ofEpochMilli(timestamp).atZone(ZoneId.systemDefault()).toLocalDate();
+            }
+            if (dateStr.length() >= 10) {
+                return LocalDate.parse(dateStr.substring(0, 10));
+            }
+        } catch (Exception e) {
+            log.warn("解析日期失败: {}", dateObj, e);
+        }
+        return null;
+    }
+}

+ 41 - 0
mjava-akdsbeisen/src/main/java/com/malk/timer/WorkHoursCalcTimer.java

@@ -0,0 +1,41 @@
+package com.malk.timer;
+
+import com.malk.service.workhours.WorkHoursCalcService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.annotation.Scheduled;
+
+@Slf4j
+@Configuration
+@EnableScheduling
+public class WorkHoursCalcTimer {
+
+    @Autowired
+    private WorkHoursCalcService workHoursCalcService;
+
+    // 每月1号凌晨2点:全量同步当月应填报工时
+    @Scheduled(cron = "0 0 2 1 * ?")
+    public void calcMonthlyFullSync() {
+        log.info("开始执行应填报工时【全量】同步任务");
+        try {
+            workHoursCalcService.calculateAndSyncMonthlyHours(null);
+            log.info("应填报工时全量同步任务执行完成");
+        } catch (Exception e) {
+            log.error("应填报工时全量同步任务执行失败", e);
+        }
+    }
+
+    // 每月2号~31号,每天凌晨3点:增量同步(查询项目档案最近2天修改的)
+    @Scheduled(cron = "0 0 3 2-31 * ?")
+    public void calcDailyIncrementalSync() {
+        log.info("开始执行应填报工时【增量】同步任务");
+        try {
+            workHoursCalcService.incrementalSync(2);
+            log.info("应填报工时增量同步任务执行完成");
+        } catch (Exception e) {
+            log.error("应填报工时增量同步任务执行失败", e);
+        }
+    }
+}

+ 8 - 0
mjava-akdsbeisen/src/main/resources/application-dev.yml

@@ -65,3 +65,11 @@ beisen:
   formUuidLeave: "FORM-D15D90EA85234A77B94564110F5D24424ERL"
   yidaAppType: "APP_ZQ3I7XO2RSHDJ4QDEVNB"
   yidaSystemToken: "FOD66381NOS25MERLN2UK92FY96Y21UMHD7LM36S"
+
+workhours:
+  formUuidProject: "FORM-A4A04FCDE2024B23B386504954760F55HIB9"
+  formUuidHoliday: "FORM-BMG66GA16LC4CNCZLUUTKAX0G27U3YTMC27NM1"
+  formUuidPersonnel: "FORM-CCEEE5D461694CBAB5999A8C2926D0C1RXQP"
+  formUuidRequiredHours: "FORM-D9144092C2C24C93A1B02A8EEED0663FFD8G"
+  yidaAppType: "APP_ZQ3I7XO2RSHDJ4QDEVNB"
+  yidaSystemToken: "FOD66381NOS25MERLN2UK92FY96Y21UMHD7LM36S"