执行重构加入镀锌线1的后端调用接口

This commit is contained in:
2026-01-29 19:15:43 +08:00
parent 08a5f9bb13
commit e6c6dbc3e7
134 changed files with 3676 additions and 3104 deletions

View File

@@ -2,16 +2,7 @@ package com.klp.da.controller;
import com.klp.common.annotation.Log;
import com.klp.common.core.controller.BaseController;
import com.klp.common.core.domain.PageQuery;
import com.klp.common.core.domain.R;
import com.klp.common.core.page.TableDataInfo;
import com.klp.common.enums.BusinessType;
import com.klp.da.domain.bo.OeeQueryBo;
import com.klp.da.domain.vo.OeeLineSummaryVo;
import com.klp.da.domain.vo.OeeEventVo;
import com.klp.da.service.IOeeReportService;
import com.klp.da.service.OeeSummaryJobService;
import com.klp.da.service.OeeTheoryCycleJobService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
@@ -25,8 +16,6 @@ import java.util.Map;
/**
* OEE 报表聚合 Controller方式 A后端统一聚合多服务
*
* 当前仅提供接口“架子”,具体聚合逻辑在 {@link IOeeReportService} 中实现。
*
* 路由前缀与 docs/oee-report-design.md 设计文档保持一致:
* - /api/ems/oee/line/summary
* - /api/ems/oee/line/loss7
@@ -39,92 +28,6 @@ import java.util.Map;
@RequestMapping("/oee/line")
public class OeeReportController extends BaseController {
private final IOeeReportService oeeReportService;
private final OeeTheoryCycleJobService oeeTheoryCycleJobService;
private final OeeSummaryJobService oeeSummaryJobService;
/**
* KPI + 趋势汇总
*/
@Log(title = "OEE 报表-汇总", businessType = BusinessType.OTHER)
@GetMapping("/summary")
public R<List<OeeLineSummaryVo>> summary(OeeQueryBo queryBo) {
return R.ok(oeeReportService.summary(queryBo));
}
/**
* KPI + 趋势汇总(异步任务):立即返回 jobId通过 WebSocket 推送进度与结果。
*
* 前端订阅:
* ws://{host}/websocket?type={wsType}
*/
@Log(title = "OEE 报表-汇总(异步)", businessType = BusinessType.OTHER)
@GetMapping("/summary/job")
public R<Map<String, Object>> summaryJob(OeeQueryBo queryBo) {
System.out.println("[OEE][summary][job][controller] request received, thread=" + Thread.currentThread().getName()
+ ", startDate=" + (queryBo == null ? null : queryBo.getStartDate())
+ ", endDate=" + (queryBo == null ? null : queryBo.getEndDate())
+ ", lineIds=" + (queryBo == null ? null : queryBo.getLineIds()));
Map<String, Object> resp = oeeSummaryJobService.createAndStart(queryBo);
System.out.println("[OEE][summary][job][controller] job created, resp=" + resp);
return R.ok(resp);
}
/**
* 7 大损失汇总
*/
@Log(title = "OEE 报表-7大损失", businessType = BusinessType.OTHER)
@GetMapping("/loss7")
public R<Map<String, Object>> lossSummary(OeeQueryBo queryBo) {
Map<String, Object> result = oeeReportService.lossSummary(queryBo);
return R.ok(result);
}
/**
* 停机/损失事件明细
*/
@Log(title = "OEE 报表-事件明细", businessType = BusinessType.OTHER)
@GetMapping("/events")
public TableDataInfo<OeeEventVo> events(OeeQueryBo queryBo, PageQuery pageQuery) {
return oeeReportService.events(queryBo, pageQuery);
}
/**
* 导出 Word 报表(整体版式由后端模板控制)
*/
@Log(title = "OEE 报表-导出Word", businessType = BusinessType.EXPORT)
@GetMapping("/exportWord")
public void exportWord(OeeQueryBo queryBo, HttpServletResponse response) {
oeeReportService.exportWord(queryBo, response);
}
/**
* 理论节拍回归(散点+拟合线)
*
* 说明:该接口用于前端绘制“散点+回归折线”的图形,节拍数据来源于 WMS 回归结果。
*/
@Log(title = "OEE 报表-理论节拍回归", businessType = BusinessType.OTHER)
@GetMapping("/theoryCycle/regression")
public R<Map<String, Object>> theoryCycleRegression(OeeQueryBo queryBo) {
return R.ok(oeeReportService.theoryCycleRegression(queryBo));
}
/**
* 理论节拍回归(异步任务):立即返回 jobId通过 WebSocket 推送进度与结果。
*
* 前端订阅:
* ws://{host}/websocket?type={wsType}
*/
@Log(title = "OEE 报表-理论节拍回归(异步)", businessType = BusinessType.OTHER)
@GetMapping("/theoryCycle/regression/job")
public R<Map<String, Object>> theoryCycleRegressionJob(OeeQueryBo queryBo) {
System.out.println("[OEE][theoryCycle][job][controller] request received, thread=" + Thread.currentThread().getName()
+ ", startTime=" + (queryBo == null ? null : queryBo.getStartTime())
+ ", endTime=" + (queryBo == null ? null : queryBo.getEndTime())
+ ", lineIds=" + (queryBo == null ? null : queryBo.getLineIds()));
Map<String, Object> resp = oeeTheoryCycleJobService.createAndStart(queryBo);
System.out.println("[OEE][theoryCycle][job][controller] job created, resp=" + resp);
return R.ok(resp);
}
}

View File

@@ -0,0 +1 @@
占位文件,无需使用。

View File

@@ -1,39 +0,0 @@
package com.klp.da.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDateTime;
/**
* OEE 事件(停机/损失)明细 VO
*/
@Data
public class OeeEventVo {
private String lineId;
private String lineName;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime eventStartTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime eventEndTime;
/**
* 时长(分钟)
*/
private Integer durationMin;
private String rawReasonCode;
private String rawReasonName;
private String lossCategoryCode;
private String lossCategoryName;
private String remark;
}

View File

@@ -1,71 +0,0 @@
package com.klp.da.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
/**
* 产线 OEE 汇总 + 日趋势 VO
*
* 对应设计文档 7.1 返回结构中的一条 line 记录。
*/
@Data
public class OeeLineSummaryVo {
/**
* 产线 ID
*/
private String lineId;
/**
* 产线名称
*/
private String lineName;
/**
* 区间汇总
*/
private Summary total;
/**
* 日粒度数据(用于趋势图)
*/
private List<Daily> daily;
@Data
public static class Summary {
private Integer loadingTimeMin;
private Integer downtimeMin;
private Integer runTimeMin;
private BigDecimal totalOutput;
private BigDecimal goodOutput;
private BigDecimal defectOutput;
private BigDecimal availability;
private BigDecimal performance;
private BigDecimal quality;
private BigDecimal oee;
}
@Data
public static class Daily {
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate statDate;
private Integer loadingTimeMin;
private Integer downtimeMin;
private Integer runTimeMin;
private BigDecimal totalOutput;
private BigDecimal goodOutput;
private BigDecimal defectOutput;
private BigDecimal availability;
private BigDecimal performance;
private BigDecimal quality;
private BigDecimal oee;
}
}

View File

@@ -1,38 +0,0 @@
package com.klp.da.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* 7 大损失分类汇总 VO
*/
@Data
public class OeeLossCategorySummaryVo {
/**
* 损失类别编码1~7 或枚举)
*/
private String lossCategoryCode;
/**
* 损失类别名称
*/
private String lossCategoryName;
/**
* 损失时间(分钟)
*/
private Integer lossTimeMin;
/**
* 损失占比0~1 或 0~100随整体口径配置
*/
private BigDecimal lossTimeRate;
/**
* 事件次数(可选)
*/
private Integer count;
}

View File

@@ -1,40 +0,0 @@
package com.klp.da.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* 损失原因 TopN VO
*/
@Data
public class OeeLossReasonVo {
private String lineId;
/**
* 原因编码
*/
private String reasonCode;
/**
* 原因名称
*/
private String reasonName;
/**
* 所属损失类别编码
*/
private String lossCategoryCode;
/**
* 损失时间(分钟)
*/
private Integer lossTimeMin;
/**
* 时间占比
*/
private BigDecimal lossTimeRate;
}

View File

@@ -1,52 +0,0 @@
package com.klp.da.service;
import com.klp.common.core.domain.PageQuery;
import com.klp.common.core.page.TableDataInfo;
import com.klp.da.domain.bo.OeeQueryBo;
import com.klp.da.domain.vo.OeeEventVo;
import com.klp.da.domain.vo.OeeLineSummaryVo;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;
/**
* OEE 报表聚合 Service 接口(方式 A后端统一聚合多服务
*
* 实现类负责:
* - 调用酸轧线、镀锌一线等外部服务
* - 做数据汇总、口径统一和格式转换
*/
public interface IOeeReportService {
/**
* KPI + 趋势汇总
*/
List<OeeLineSummaryVo> summary(OeeQueryBo queryBo);
/**
* 7 大损失汇总
*
* 返回 Map 以便后续扩展:
* - byLine: List<LineLossSummary>
* - topReasons: List<OeeLossReasonVo>
* 等
*/
Map<String, Object> lossSummary(OeeQueryBo queryBo);
/**
* 事件明细
*/
TableDataInfo<OeeEventVo> events(OeeQueryBo queryBo, PageQuery pageQuery);
/**
* 导出 Word 报表
*/
void exportWord(OeeQueryBo queryBo, HttpServletResponse response);
/**
* 理论节拍线性回归(散点+拟合线),用于前端绘图。
*/
Map<String, Object> theoryCycleRegression(OeeQueryBo queryBo);
}

View File

@@ -1,19 +0,0 @@
package com.klp.da.service;
import com.klp.da.domain.bo.OeeQueryBo;
import java.util.Map;
/**
* OEE summary 异步任务WebSocket 推送)
*/
public interface OeeSummaryJobService {
/**
* 创建任务并异步执行,立即返回 job 信息。
*
* @return Map: { jobId, wsType }
*/
Map<String, Object> createAndStart(OeeQueryBo queryBo);
}

View File

@@ -1,19 +0,0 @@
package com.klp.da.service;
import com.klp.da.domain.bo.OeeQueryBo;
import java.util.Map;
/**
* 理论节拍回归异步任务WebSocket 推送)
*/
public interface OeeTheoryCycleJobService {
/**
* 创建任务并异步执行,立即返回 job 信息。
*
* @return Map: { jobId, wsType }
*/
Map<String, Object> createAndStart(OeeQueryBo queryBo);
}

View File

@@ -1,155 +0,0 @@
package com.klp.da.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.klp.da.domain.bo.OeeQueryBo;
import com.klp.da.domain.vo.OeeLineSummaryVo;
import com.klp.da.service.IOeeReportService;
import com.klp.da.service.OeeSummaryJobService;
import com.klp.framework.websocket.TypeWebSocketUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Slf4j
@Service
@RequiredArgsConstructor
public class OeeSummaryJobServiceImpl implements OeeSummaryJobService {
public static final String WS_TYPE_PREFIX = "oee_summary:";
private static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(2, r -> {
Thread t = new Thread(r);
t.setName("oee-summary-job");
t.setDaemon(true);
return t;
});
private final IOeeReportService oeeReportService;
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public Map<String, Object> createAndStart(OeeQueryBo queryBo) {
String jobId = UUID.randomUUID().toString().replace("-", "");
String wsType = WS_TYPE_PREFIX + jobId;
String auth = captureAuthorization();
log.info("[OEE][summary][job] created jobId={}, wsType={}, window={}~{}, lines={}, authPresent={}",
jobId,
wsType,
queryBo == null ? null : queryBo.getStartDate(),
queryBo == null ? null : queryBo.getEndDate(),
queryBo == null ? null : queryBo.getLineIds(),
auth != null && !auth.trim().isEmpty());
push(jobId, wsType, "running", 5, "任务已创建,准备生成汇总…", null, null);
OeeQueryBo qb = copyQueryBo(queryBo);
EXECUTOR.submit(() -> run(jobId, wsType, qb, auth));
Map<String, Object> resp = new HashMap<>();
resp.put("jobId", jobId);
resp.put("wsType", wsType);
return resp;
}
private void run(String jobId, String wsType, OeeQueryBo queryBo, String authorization) {
long t0 = System.currentTimeMillis();
try {
System.out.println("[OEE][summary][job] start jobId=" + jobId + ", wsType=" + wsType
+ ", thread=" + Thread.currentThread().getName()
+ ", authPresent=" + (authorization != null && !authorization.trim().isEmpty()));
log.info("[OEE][summary][job] start jobId={}, wsType={}, thread={}, authPresent={}",
jobId, wsType, Thread.currentThread().getName(), authorization != null && !authorization.trim().isEmpty());
push(jobId, wsType, "running", 20, "正在聚合 KPI 与趋势…", null, null);
OeeReportServiceImpl.setAuthOverride(authorization);
List<OeeLineSummaryVo> lines = oeeReportService.summary(queryBo);
push(jobId, wsType, "running", 90, "正在整理汇总结果…", null, null);
Map<String, Object> data = new HashMap<>();
data.put("lines", lines);
push(jobId, wsType, "success", 100, "汇总生成完成", data, null);
System.out.println("[OEE][summary][job] success jobId=" + jobId + ", costMs=" + (System.currentTimeMillis() - t0)
+ ", lineCount=" + (lines == null ? 0 : lines.size()));
log.info("[OEE][summary][job] success jobId={}, costMs={}, lineCount={}",
jobId, (System.currentTimeMillis() - t0), lines == null ? 0 : lines.size());
} catch (Exception e) {
log.warn("summary job failed, jobId={}", jobId, e);
push(jobId, wsType, "failed", 100, "汇总生成失败", null, e.getMessage());
System.out.println("[OEE][summary][job] failed jobId=" + jobId + ", costMs=" + (System.currentTimeMillis() - t0)
+ ", msg=" + e.getMessage());
log.warn("[OEE][summary][job] failed jobId={}, costMs={}, msg={}",
jobId, (System.currentTimeMillis() - t0), e.getMessage());
} finally {
OeeReportServiceImpl.clearAuthOverride();
System.out.println("[OEE][summary][job] end jobId=" + jobId + ", costMs=" + (System.currentTimeMillis() - t0));
log.info("[OEE][summary][job] end jobId={}, costMs={}", jobId, (System.currentTimeMillis() - t0));
}
}
private void push(String jobId, String wsType, String status, int progress, String text,
Map<String, Object> data, String errorMsg) {
try {
Map<String, Object> msg = new HashMap<>();
msg.put("jobId", jobId);
msg.put("status", status);
msg.put("progress", progress);
msg.put("text", text);
if (data != null) {
msg.put("data", data);
}
if (errorMsg != null && !errorMsg.trim().isEmpty()) {
msg.put("errorMsg", errorMsg);
}
TypeWebSocketUtil.sendToType(wsType, objectMapper.writeValueAsString(msg));
} catch (Exception ignore) {
}
}
private OeeQueryBo copyQueryBo(OeeQueryBo queryBo) {
OeeQueryBo qb = new OeeQueryBo();
if (queryBo == null) {
return qb;
}
qb.setStartDate(queryBo.getStartDate());
qb.setEndDate(queryBo.getEndDate());
qb.setLineIds(queryBo.getLineIds());
qb.setStartTime(queryBo.getStartTime());
qb.setEndTime(queryBo.getEndTime());
qb.setTopN(queryBo.getTopN());
qb.setLossCategoryCode(queryBo.getLossCategoryCode());
qb.setKeyword(queryBo.getKeyword());
return qb;
}
private String captureAuthorization() {
try {
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs == null || attrs.getRequest() == null) {
return null;
}
HttpServletRequest req = attrs.getRequest();
String auth = req.getHeader("Authorization");
if (auth != null && !auth.trim().isEmpty()) {
return auth.trim();
}
String token = req.getHeader("token");
if (token != null && !token.trim().isEmpty()) {
return "Bearer " + token.trim();
}
} catch (Exception ignore) {
}
return null;
}
}

View File

@@ -1,182 +0,0 @@
package com.klp.da.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.klp.da.domain.bo.OeeQueryBo;
import com.klp.da.service.IOeeReportService;
import com.klp.da.service.OeeTheoryCycleJobService;
import com.klp.framework.websocket.TypeWebSocketUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Slf4j
@Service
@RequiredArgsConstructor
public class OeeTheoryCycleJobServiceImpl implements OeeTheoryCycleJobService {
/**
* 推送 type 前缀:前端用 ws://host/websocket?type=oee_theory_cycle_regression:{jobId} 订阅。
*/
public static final String WS_TYPE_PREFIX = "oee_theory_cycle_regression:";
private static final long TTL_MS = 10 * 60_000L;
private static final ConcurrentHashMap<String, JobState> JOBS = new ConcurrentHashMap<>();
private static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(2, r -> {
Thread t = new Thread(r);
t.setName("oee-theory-cycle-job");
t.setDaemon(true);
return t;
});
private final IOeeReportService oeeReportService;
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public Map<String, Object> createAndStart(OeeQueryBo queryBo) {
String jobId = UUID.randomUUID().toString().replace("-", "");
String wsType = WS_TYPE_PREFIX + jobId;
JobState st = new JobState();
st.updatedAt = System.currentTimeMillis();
JOBS.put(jobId, st);
String auth = captureAuthorization();
log.info("[OEE][theoryCycle][job] created jobId={}, wsType={}, window={}~{}, lines={}, authPresent={}",
jobId,
wsType,
queryBo == null ? null : queryBo.getStartTime(),
queryBo == null ? null : queryBo.getEndTime(),
queryBo == null ? null : queryBo.getLineIds(),
auth != null && !auth.trim().isEmpty());
// 立即推送“已创建”
push(jobId, wsType, "running", 5, "任务已创建,准备计算…", null, null);
// 异步执行
OeeQueryBo qb = copyQueryBo(queryBo);
EXECUTOR.submit(() -> run(jobId, wsType, qb, auth));
Map<String, Object> resp = new HashMap<>();
resp.put("jobId", jobId);
resp.put("wsType", wsType);
return resp;
}
private void run(String jobId, String wsType, OeeQueryBo queryBo, String authorization) {
long t0 = System.currentTimeMillis();
try {
cleanupExpired();
System.out.println("[OEE][theoryCycle][job] start jobId=" + jobId + ", wsType=" + wsType
+ ", thread=" + Thread.currentThread().getName()
+ ", authPresent=" + (authorization != null && !authorization.trim().isEmpty()));
log.info("[OEE][theoryCycle][job] start jobId={}, wsType={}, thread={}, authPresent={}",
jobId, wsType, Thread.currentThread().getName(), authorization != null && !authorization.trim().isEmpty());
push(jobId, wsType, "running", 20, "开始计算理论节拍回归…", null, null);
// 这里复用现有 WMS 同步回归聚合逻辑(内部会走 WMS 接口)
push(jobId, wsType, "running", 50, "正在拉取样本并拟合回归…", null, null);
OeeReportServiceImpl.setAuthOverride(authorization);
Map<String, Object> data = oeeReportService.theoryCycleRegression(queryBo);
push(jobId, wsType, "running", 90, "正在整理回归结果…", null, null);
JobState st = JOBS.get(jobId);
if (st != null) {
st.updatedAt = System.currentTimeMillis();
}
push(jobId, wsType, "success", 100, "回归计算完成", data, null);
System.out.println("[OEE][theoryCycle][job] success jobId=" + jobId + ", costMs=" + (System.currentTimeMillis() - t0));
log.info("[OEE][theoryCycle][job] success jobId={}, costMs={}", jobId, (System.currentTimeMillis() - t0));
} catch (Exception e) {
log.warn("theoryCycle job failed, jobId={}", jobId, e);
JobState st = JOBS.get(jobId);
if (st != null) {
st.updatedAt = System.currentTimeMillis();
}
push(jobId, wsType, "failed", 100, "回归计算失败", null, e.getMessage());
System.out.println("[OEE][theoryCycle][job] failed jobId=" + jobId + ", costMs=" + (System.currentTimeMillis() - t0)
+ ", msg=" + e.getMessage());
log.warn("[OEE][theoryCycle][job] failed jobId={}, costMs={}, msg={}",
jobId, (System.currentTimeMillis() - t0), e.getMessage());
} finally {
OeeReportServiceImpl.clearAuthOverride();
System.out.println("[OEE][theoryCycle][job] end jobId=" + jobId + ", costMs=" + (System.currentTimeMillis() - t0));
log.info("[OEE][theoryCycle][job] end jobId={}, costMs={}", jobId, (System.currentTimeMillis() - t0));
}
}
private void push(String jobId, String wsType, String status, int progress, String text,
Map<String, Object> data, String errorMsg) {
try {
Map<String, Object> msg = new HashMap<>();
msg.put("jobId", jobId);
msg.put("status", status);
msg.put("progress", progress);
msg.put("text", text);
if (data != null) {
msg.put("data", data);
}
if (errorMsg != null && !errorMsg.trim().isEmpty()) {
msg.put("errorMsg", errorMsg);
}
TypeWebSocketUtil.sendToType(wsType, objectMapper.writeValueAsString(msg));
} catch (Exception ignore) {
}
}
private void cleanupExpired() {
long now = System.currentTimeMillis();
JOBS.entrySet().removeIf(e -> e.getValue() == null || (now - e.getValue().updatedAt) > TTL_MS);
}
private OeeQueryBo copyQueryBo(OeeQueryBo queryBo) {
OeeQueryBo qb = new OeeQueryBo();
if (queryBo == null) {
return qb;
}
qb.setStartDate(queryBo.getStartDate());
qb.setEndDate(queryBo.getEndDate());
qb.setLineIds(queryBo.getLineIds());
qb.setStartTime(queryBo.getStartTime());
qb.setEndTime(queryBo.getEndTime());
qb.setTopN(queryBo.getTopN());
qb.setLossCategoryCode(queryBo.getLossCategoryCode());
qb.setKeyword(queryBo.getKeyword());
return qb;
}
private String captureAuthorization() {
try {
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs == null || attrs.getRequest() == null) {
return null;
}
HttpServletRequest req = attrs.getRequest();
String auth = req.getHeader("Authorization");
if (auth != null && !auth.trim().isEmpty()) {
return auth.trim();
}
String token = req.getHeader("token");
if (token != null && !token.trim().isEmpty()) {
return "Bearer " + token.trim();
}
} catch (Exception ignore) {
}
return null;
}
private static class JobState {
long updatedAt;
}
}