二级系统联合寻找数据

This commit is contained in:
2026-02-04 15:22:34 +08:00
parent d42b8ffef2
commit 5b3938e13f
19 changed files with 2167 additions and 352 deletions

View File

@@ -0,0 +1,44 @@
package com.klp.da.service;
import com.klp.common.core.page.TableDataInfo;
import com.klp.da.domain.bo.OeeQueryBo;
import com.klp.pocket.acid.domain.vo.AcidOeeDailySummaryVo;
import com.klp.pocket.acid.domain.vo.AcidOeeLoss7Vo;
import com.klp.pocket.acid.domain.vo.Klptcm1ProStoppageVo;
import java.util.List;
/**
* OEE 聚合 Service 接口(酸轧线)
*
* @author klp
* @date 2026-01-31
*/
public interface IDaAcidOeeService {
/**
* 获取 OEE 日汇总(当月走缓存,历史查询走实时)
*
* @param bo 查询条件
* @return 日汇总列表
*/
List<AcidOeeDailySummaryVo> getDailySummary(OeeQueryBo bo);
/**
* 获取 7 大损失汇总(当月走缓存,历史查询走实时)
*
* @param bo 查询条件
* @return 7 大损失列表
*/
List<AcidOeeLoss7Vo> getLoss7Summary(OeeQueryBo bo);
/**
* 获取停机事件明细(实时分页查询)
*
* @param bo 查询条件
* @return 分页后的停机事件列表
*/
TableDataInfo<Klptcm1ProStoppageVo> getStoppageEvents(OeeQueryBo bo);
}

View File

@@ -0,0 +1,132 @@
package com.klp.da.service;
import com.klp.common.config.DeepseekConfig;
import com.klp.common.utils.StringUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* OEE Word 导出:表格内容大模型分析(可选)
*
* 默认关闭,通过配置开启:
* oee:
* word:
* ai:
* enabled: true
*
* 大模型连接配置复用 sales.script.aiDeepSeek
* sales:
* script:
* ai:
* api-key/base-url/model-name/max-retries/temperature
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class OeeWordAiAnalysisService {
@Value("${oee.word.ai.enabled:false}")
private boolean enabled;
@Qualifier("salesScriptRestTemplate")
private final RestTemplate restTemplate;
private final DeepseekConfig deepseekConfig;
public String analyzeTable(String tableTitle, String tableMarkdown, String extraContext) {
if (!enabled) {
return null;
}
if (StringUtils.isBlank(tableMarkdown)) {
return null;
}
if (deepseekConfig == null || StringUtils.isBlank(deepseekConfig.getApiKey())
|| StringUtils.isBlank(deepseekConfig.getBaseUrl())
|| StringUtils.isBlank(deepseekConfig.getModelName())) {
log.warn("OEE Word AI 已开启但 sales.script.ai 配置不完整,跳过分析");
return null;
}
String prompt = buildPrompt(tableTitle, tableMarkdown, extraContext);
return callAi(prompt);
}
private String buildPrompt(String tableTitle, String tableMarkdown, String extraContext) {
StringBuilder sb = new StringBuilder();
sb.append("你是一名制造业OEE报表分析专家。请基于下面表格数据给出简洁的分析结论。\n");
sb.append("输出要求:\n");
sb.append("1) 用中文输出;\n");
sb.append("2) 只输出 3~6 条要点(每条不超过 30 字),不要写长段落;\n");
sb.append("3) 可以指出异常/波动/占比最高项/可能原因与建议方向;\n");
sb.append("4) 不要复述表格标题不要输出Markdown表格。\n\n");
sb.append("【表格】").append(tableTitle).append("\n");
if (StringUtils.isNotBlank(extraContext)) {
sb.append("【上下文】").append(extraContext).append("\n");
}
sb.append("【数据】\n");
sb.append(tableMarkdown);
return sb.toString();
}
@SuppressWarnings("unchecked")
private String callAi(String prompt) {
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("model", deepseekConfig.getModelName());
Map<String, String> systemMessage = new HashMap<>();
systemMessage.put("role", "system");
systemMessage.put("content", "你是一个严谨的数据分析助手");
Map<String, String> userMessage = new HashMap<>();
userMessage.put("role", "user");
userMessage.put("content", prompt);
requestBody.put("messages", Arrays.asList(systemMessage, userMessage));
requestBody.put("temperature", deepseekConfig.getTemperature() == null ? 0.2 : deepseekConfig.getTemperature());
requestBody.put("max_tokens", 800);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(deepseekConfig.getApiKey());
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
int retries = deepseekConfig.getMaxRetries() == null ? 1 : Math.max(1, deepseekConfig.getMaxRetries());
for (int i = 0; i < retries; i++) {
try {
ResponseEntity<Map> response = restTemplate.postForEntity(
deepseekConfig.getBaseUrl() + "/chat/completions", entity, Map.class);
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
Map<String, Object> body = response.getBody();
List<Map<String, Object>> choices = (List<Map<String, Object>>) body.get("choices");
if (choices != null && !choices.isEmpty()) {
Map<String, Object> choice = choices.get(0);
Map<String, Object> message = (Map<String, Object>) choice.get("message");
String content = message == null ? null : (String) message.get("content");
if (StringUtils.isNotBlank(content)) {
return content.trim();
}
}
}
} catch (Exception e) {
log.warn("OEE Word AI 调用失败,重试 {}/{}{}", i + 1, retries, e.getMessage());
}
}
return null;
}
}

View File

@@ -0,0 +1,92 @@
package com.klp.da.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
import com.github.pagehelper.PageInfo;
import com.klp.common.core.page.TableDataInfo;
import com.klp.common.utils.DateUtils;
import com.klp.common.utils.StringUtils;
import com.klp.da.domain.bo.OeeQueryBo;
import com.klp.da.service.IDaAcidOeeService;
import com.klp.pocket.acid.domain.vo.AcidOeeDailySummaryVo;
import com.klp.pocket.acid.domain.vo.AcidOeeLoss7Vo;
import com.klp.pocket.acid.domain.vo.Klptcm1ProStoppageVo;
import com.klp.pocket.acid.service.IAcidOeeService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.List;
@Slf4j
@RequiredArgsConstructor
@Service
public class DaAcidOeeServiceImpl implements IDaAcidOeeService {
private static final String SUMMARY_KEY_PATTERN = "oee:report:month:summary:%s:SY";
private static final DateTimeFormatter YEAR_MONTH_FMT = DateTimeFormatter.ofPattern("yyyyMM");
private final IAcidOeeService acidOeeService;
private final StringRedisTemplate stringRedisTemplate;
@Override
public List<AcidOeeDailySummaryVo> getDailySummary(OeeQueryBo bo) {
// 检查是否查询当月,如果是,则尝试从 Redis 缓存获取
if (isCurrentMonthQuery(bo.getStartDate(), bo.getEndDate())) {
String yyyyMM = LocalDate.now().format(YEAR_MONTH_FMT);
String summaryKey = String.format(SUMMARY_KEY_PATTERN, yyyyMM);
try {
String summaryJson = stringRedisTemplate.opsForValue().get(summaryKey);
if (StringUtils.isNotBlank(summaryJson)) {
log.info("[DaAcidOeeService] Hit cache for acid summary, key={}", summaryKey);
return JSON.parseObject(summaryJson, new TypeReference<List<AcidOeeDailySummaryVo>>() {});
}
} catch (Exception e) {
log.error("[DaAcidOeeService] Failed to get acid summary from redis cache, key={}", summaryKey, e);
}
}
// 缓存未命中或查询历史数据,则实时调用 pocket service
log.info("[DaAcidOeeService] Cache miss for acid summary, calling pocket service...");
return acidOeeService.getDailySummary(
DateUtils.parseDateToStr("yyyy-MM-dd", DateUtils.toDate(bo.getStartDate())),
DateUtils.parseDateToStr("yyyy-MM-dd", DateUtils.toDate(bo.getEndDate()))
);
}
@Override
public List<AcidOeeLoss7Vo> getLoss7Summary(OeeQueryBo bo) {
// 7大损失目前没有预计算直接实时调用
return acidOeeService.getLoss7Summary(
DateUtils.parseDateToStr("yyyy-MM-dd", DateUtils.toDate(bo.getStartDate())),
DateUtils.parseDateToStr("yyyy-MM-dd", DateUtils.toDate(bo.getEndDate()))
);
}
@Override
public TableDataInfo<Klptcm1ProStoppageVo> getStoppageEvents(OeeQueryBo bo) {
// 实时分页查询
List<Klptcm1ProStoppageVo> list = acidOeeService.getStoppageEvents(
DateUtils.parseDateToStr("yyyy-MM-dd", DateUtils.toDate(bo.getStartDate())),
DateUtils.parseDateToStr("yyyy-MM-dd", DateUtils.toDate(bo.getEndDate()))
);
return TableDataInfo.build(list);
}
/**
* 判断查询范围是否为当月
*/
private boolean isCurrentMonthQuery(LocalDate startDate, LocalDate endDate) {
if (startDate == null || endDate == null) {
return false;
}
LocalDate now = LocalDate.now();
LocalDate firstDayOfMonth = now.withDayOfMonth(1);
return !startDate.isBefore(firstDayOfMonth) && !endDate.isAfter(now);
}
}