diff --git a/ocr-service/.classpath b/ocr-service/.classpath new file mode 100644 index 0000000..f7e4a1d --- /dev/null +++ b/ocr-service/.classpath @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ocr-service/.gitignore b/ocr-service/.gitignore new file mode 100644 index 0000000..40cbd51 --- /dev/null +++ b/ocr-service/.gitignore @@ -0,0 +1,3 @@ +/target/ +/logs/ +/.settings/ \ No newline at end of file diff --git a/ocr-service/.project b/ocr-service/.project new file mode 100644 index 0000000..44d49b7 --- /dev/null +++ b/ocr-service/.project @@ -0,0 +1,23 @@ + + + ocr-service + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/ocr-service/pom.xml b/ocr-service/pom.xml new file mode 100644 index 0000000..5e79b0d --- /dev/null +++ b/ocr-service/pom.xml @@ -0,0 +1,187 @@ + + + 4.0.0 + + com.bw + opai-service-center + 0.0.1-SNAPSHOT + + com.bw + ocr-service + 0.0.1-SNAPSHOT + ocr-service + http://maven.apache.org + + UTF-8 + + + + org.springframework.boot + spring-boot-starter-web + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + org.springframework.boot + spring-boot-starter-actuator + + + org.projectlombok + lombok + + + com.alibaba + fastjson + 2.0.17 + + + org.apache.httpcomponents + httpclient + 4.5.3 + + + org.apache.httpcomponents + httpmime + 4.5.13 + + + commons-lang + commons-lang + 2.6 + + + + com.squareup.okhttp3 + okhttp + 4.9.3 + + + org.springframework.kafka + spring-kafka + + + + org.apache.poi + poi + 4.1.2 + + + org.apache.poi + poi-ooxml + 4.1.2 + + + + + org.apache.poi + ooxml-schemas + 1.4 + + + + + + + + maven-clean-plugin + 3.1.0 + + + + maven-resources-plugin + 3.0.2 + + + maven-compiler-plugin + 3.8.0 + + + maven-surefire-plugin + 2.22.1 + + + maven-jar-plugin + 3.0.2 + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + + maven-site-plugin + 3.7.1 + + + maven-project-info-reports-plugin + 3.0.0 + + + + + org.springframework.boot + spring-boot-maven-plugin + + com.bw.ocr.Application + ZIP + + + ${project.groupId} + ${project.artifactId} + + + + + + + repackage + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.1.1 + + + copy + package + + copy-dependencies + + + jar + jar + runtime + ${project.build.directory}/libs + + + + + + + + diff --git a/ocr-service/src/main/java/com/bw/ocr/Application.java b/ocr-service/src/main/java/com/bw/ocr/Application.java new file mode 100644 index 0000000..4f37ee4 --- /dev/null +++ b/ocr-service/src/main/java/com/bw/ocr/Application.java @@ -0,0 +1,19 @@ +package com.bw.ocr; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + + +/** + * 系统接口启动类 + * @author jian.mao + * @date 2025年12月30日 + * @description + */ +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/ocr-service/src/main/java/com/bw/ocr/cache/ConfigCache.java b/ocr-service/src/main/java/com/bw/ocr/cache/ConfigCache.java new file mode 100644 index 0000000..da80c64 --- /dev/null +++ b/ocr-service/src/main/java/com/bw/ocr/cache/ConfigCache.java @@ -0,0 +1,37 @@ +package com.bw.ocr.cache; + +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.concurrent.LinkedBlockingDeque; + +/** + * @author jian.mao + * @date 2022年11月11日 + * @description 静态变量类 + */ +@Slf4j +public class ConfigCache { + + /**启动条件**/ + public static boolean isStart = true; + /*****任务队列*****/ + public static LinkedBlockingDeque> taskQueue = new LinkedBlockingDeque>(); + /****结果队列****/ + public static LinkedBlockingDeque> resultQueue = new LinkedBlockingDeque>(); + + + /** + * 队列录入任务 + * @param queue + * @param task + */ + public static void putQueue(LinkedBlockingDeque> queue,Map task){ + //next app 写入队列准备调出 + try { + queue.put(task); + } catch (InterruptedException e) { + log.error("队列写入data失败---"); + } + } +} diff --git a/ocr-service/src/main/java/com/bw/ocr/controller/TaskReceiveController.java b/ocr-service/src/main/java/com/bw/ocr/controller/TaskReceiveController.java new file mode 100644 index 0000000..1d2dff0 --- /dev/null +++ b/ocr-service/src/main/java/com/bw/ocr/controller/TaskReceiveController.java @@ -0,0 +1,39 @@ +package com.bw.ocr.controller; + +import javax.annotation.Resource; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; + +import com.bw.ocr.service.TaskReceiveService; + +import lombok.extern.slf4j.Slf4j; + +/** + * 任务接收控制层 + * @author jian.mao + * @date 2025年1月14日 + * @description + */ +@Controller +@RequestMapping("/task") +@Slf4j +public class TaskReceiveController { + @Resource + private TaskReceiveService taskReceiveService; + @PostMapping("/put") + @ResponseBody + public String put(@RequestBody String param){ + String response = taskReceiveService.put(param); + return response; + } + @RequestMapping(value = "/hello", method = RequestMethod.GET) + @ResponseBody + public String hello(String param, String token) { + return "123"; + } +} diff --git a/ocr-service/src/main/java/com/bw/ocr/entity/AppResultDoc.java b/ocr-service/src/main/java/com/bw/ocr/entity/AppResultDoc.java new file mode 100644 index 0000000..b5236dd --- /dev/null +++ b/ocr-service/src/main/java/com/bw/ocr/entity/AppResultDoc.java @@ -0,0 +1,31 @@ +package com.bw.ocr.entity; + + +import java.io.Serializable; +import java.util.Map; + +import lombok.Data; + +/** + * ES 索引:opai_app_result + * 应用执行结果文档 + * + * @author jian.mao + */ +@Data +public class AppResultDoc implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 完成时间(毫秒时间戳) */ + private Long finishTime; + + /** 执行状态 */ + private Integer status; + + /** 执行结果(可索引) */ + private Map result; + + /** 最后修改时间(毫秒时间戳) */ + private Long lastEdit; +} diff --git a/ocr-service/src/main/java/com/bw/ocr/entity/Constants.java b/ocr-service/src/main/java/com/bw/ocr/entity/Constants.java new file mode 100644 index 0000000..c1f6366 --- /dev/null +++ b/ocr-service/src/main/java/com/bw/ocr/entity/Constants.java @@ -0,0 +1,50 @@ +package com.bw.ocr.entity; + + +/** + * 常量实体类 + * @author jian.mao + * @date 2022年11月15日 + * @description + */ +public class Constants { + + + /** + * 空字符串常量 + */ + public static final String EMPTY = ""; + + /************************应用参数*************************************/ + public static final String CODE = "code"; + public static final String MESSAGE = "message"; + /******************************api使用*******************************/ + public static final String CONTENT = "content"; + public static final String WROD_COUNT = "wordCount"; + public static final String TRACE = "trace"; + public static final String PARSE_FIAL = "解析失败"; + public static final String FILEURL = "fileUrl"; + public static final String CREATEURL = "createUrl"; + public static final String JOBURL = "jobUrl"; + public static final String QUERYURL = "queryUrl"; + public static final String FILECONTENTS = "FileContents"; + public static final String FILENAME = "FileName"; + public static final String OPENPASSWORD = "OpenPassword"; + public static final String OWNERPASSWORD = "OwnerPassword"; + public static final String LOCATIONPATH = "LocationPath"; + public static final String SUCCESS = "success"; + public static final String FAILED = "failed"; + public static final String WORDS_RESULT = "words_result"; + public static final String WORDS = "words"; + public static final String JOBID = "jobId"; + public static final String PROGRESS = "Progress"; + public static final String JOBDOCUMENTS = "JobDocuments"; + public static final String OutputDocuments = "OutputDocuments"; + public static final String FILES = "Files"; + public static final String IDRENAME = "#id"; + + /** + * 任务id + */ + public static final String TASKID = "taskId"; +} diff --git a/ocr-service/src/main/java/com/bw/ocr/handler/MainHandler.java b/ocr-service/src/main/java/com/bw/ocr/handler/MainHandler.java new file mode 100644 index 0000000..682b03f --- /dev/null +++ b/ocr-service/src/main/java/com/bw/ocr/handler/MainHandler.java @@ -0,0 +1,208 @@ +package com.bw.ocr.handler; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + + +import org.apache.commons.io.FileUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import com.alibaba.fastjson.JSONObject; +import com.bw.ocr.cache.ConfigCache; +import com.bw.ocr.service.OcrTaskService; +import com.bw.ocr.utils.DateUtil; +import com.bw.ocr.utils.FileUtil; + +import lombok.extern.slf4j.Slf4j; + + +/** + * @author jian.mao + * @date 2025年1月13日 + * @description + */ +@Component +@Order(value = 1) +@RefreshScope +@Slf4j +public class MainHandler implements ApplicationRunner { + + @Value("${task.task-queue-path}") + private String taskPath; + @Value("${task.result-task-queue-path}") + private String resultTaskPath; + @Autowired + private OcrTaskService ocrTaskService; + /***线程池参数***/ + @Value("${threadPool.corePoolSize}") + private int corePoolSize; + @Value("${threadPool.maximumPoolSize}") + private int maximumPoolSize; + @Value("${threadPool.keepAliveTime}") + private long keepAliveTime; + @Value("${threadPool.queueSize}") + private int queueSize; + + /** + *执行入口 + */ + @Override + public void run(ApplicationArguments args) throws Exception { + //线程池方式 + ThreadPoolExecutor executor = new ThreadPoolExecutor( + corePoolSize, + maximumPoolSize, + keepAliveTime, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(queueSize), + new ThreadPoolExecutor.CallerRunsPolicy() + ); + //消费创建任务队列数据 + Thread consumerThread = new Thread(() -> { + while (true) { + try { + // 从队列中获取任务 + Map task = ConfigCache.taskQueue.take(); + // 提交给线程池执行 + executor.execute(() -> createTask(task)); + } catch (InterruptedException e) { + // 恢复中断状态 + Thread.currentThread().interrupt(); + log.error("任务消费线程被中断"); + break; + } + } + }); + consumerThread.start(); + log.info("创建任务消费线程启动-----"); + + + //消费结果任务队列数据 + Thread resultConsumerThread = new Thread(() -> { + while (true) { + try { + // 从队列中获取任务 + Map task = ConfigCache.resultQueue.take(); + // 提交给线程池执行 + executor.execute(() -> getResult(task)); + } catch (InterruptedException e) { + // 恢复中断状态 + Thread.currentThread().interrupt(); + log.error("任务消费线程被中断"); + break; + } + DateUtil.sleep(3000); + } + }); + resultConsumerThread.start(); + log.info("结果任务消费线程启动-----"); + //启动加载缓存任务 + readTask(taskPath, ConfigCache.taskQueue); + readTask(resultTaskPath, ConfigCache.resultQueue); + //停止处理 + waitDown(); + } + + /** + * 创建任务执行方法 + * @param task + */ + private void createTask(Map task) { + ocrTaskService.create(task); + } + + private void getResult(Map task) { + ocrTaskService.parse(task); + } + + + /****************************************************************load******************************************************************************/ + /** + * 加载文件中的任务 + * @param path 文件地址 + * @param queue 队列 + */ + @SuppressWarnings("unchecked") + public static void readTask(String path, LinkedBlockingDeque> queue) { + File file = new File(path); + if (file.exists()) { + List tasks = null; + try { + tasks = FileUtils.readLines(file, "UTF-8"); + } catch (IOException e) { + e.printStackTrace(); + } + for (String taskStr : tasks) { + Map task = JSONObject.parseObject(taskStr); + try { + queue.put(task); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + file.delete(); + } + } + + /*******************************************************************stop************************************************************************/ + + /** + * 结束触发钩子 + */ + public void waitDown() { + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + // 停止线程 + ConfigCache.isStart = false; + log.info("stop-------"); + writeTsskToFile(); + } + }); + } + + + /** + * 任务持久化到硬盘 + */ + public void writeTsskToFile() { + while (true) { + if (ConfigCache.taskQueue.size() > 0) { + try { + Map task = ConfigCache.taskQueue.take(); + FileUtil.writeFile(taskPath, JSONObject.toJSONString(task)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } else { + log.info("taskQueue write is file end"); + break; + } + } + while (true) { + if (ConfigCache.resultQueue.size() > 0) { + try { + Map task = ConfigCache.resultQueue.take(); + FileUtil.writeFile(resultTaskPath, JSONObject.toJSONString(task)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } else { + log.info("taskQueue write is file end"); + break; + } + } + } +} diff --git a/ocr-service/src/main/java/com/bw/ocr/service/OcrTaskService.java b/ocr-service/src/main/java/com/bw/ocr/service/OcrTaskService.java new file mode 100644 index 0000000..239ea68 --- /dev/null +++ b/ocr-service/src/main/java/com/bw/ocr/service/OcrTaskService.java @@ -0,0 +1,24 @@ +package com.bw.ocr.service; + +import java.util.Map; + +/** + * ocr识别处理接口 + * @author jian.mao + * @date 2025年2月18日 + * @description + */ +public interface OcrTaskService { + + /** + * ocr远端任务 + * @param task + */ + public void create(Map task); + + /** + * 解析结果 + * @param task + */ + public void parse(Map task); +} diff --git a/ocr-service/src/main/java/com/bw/ocr/service/TaskReceiveService.java b/ocr-service/src/main/java/com/bw/ocr/service/TaskReceiveService.java new file mode 100644 index 0000000..9a06d53 --- /dev/null +++ b/ocr-service/src/main/java/com/bw/ocr/service/TaskReceiveService.java @@ -0,0 +1,17 @@ +package com.bw.ocr.service; + +/** + * 任务接收服务层 + * @author jian.mao + * @date 2025年1月14日 + * @description + */ +public interface TaskReceiveService { + + /** + * 任务新增 + * @param dataJson + * @return + */ + public String put(String dataJson); +} diff --git a/ocr-service/src/main/java/com/bw/ocr/service/impl/OcrTaskServiceImpl.java b/ocr-service/src/main/java/com/bw/ocr/service/impl/OcrTaskServiceImpl.java new file mode 100644 index 0000000..84769a1 --- /dev/null +++ b/ocr-service/src/main/java/com/bw/ocr/service/impl/OcrTaskServiceImpl.java @@ -0,0 +1,329 @@ +package com.bw.ocr.service.impl; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.apache.http.HttpResponse; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.apache.poi.xwpf.extractor.XWPFWordExtractor; +import org.apache.poi.xwpf.usermodel.XWPFDocument; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.stereotype.Service; + +import com.alibaba.fastjson.JSONObject; +import com.bw.ocr.cache.ConfigCache; +import com.bw.ocr.entity.AppResultDoc; +import com.bw.ocr.entity.Constants; +import com.bw.ocr.service.OcrTaskService; +import com.bw.ocr.utils.DownLoadUtil; +import com.bw.ocr.utils.FileUtil; +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.extern.slf4j.Slf4j; + +/** + * ocr执行实现类 + * @author jian.mao + * @date 2025年2月18日 + * @description + */ +@Service +@Slf4j +@RefreshScope + +public class OcrTaskServiceImpl implements OcrTaskService { + + + @Value("${file.path-prefix}") + private String downloadFilePathPrefix; + + @Value("${api.create-url}") + private String createUrl; + + @Value("${api.job-url}") + private String jobUrl; + @Value("${api.query-url}") + private String queryUrl; + + @Value("${elasticsearch.url}") + private String esUrl; + + @Value("${elasticsearch.username}") + private String esUser; + + @Value("${elasticsearch.password}") + private String esPassword; + + @Value("${elasticsearch.index-name}") + private String indexName; + + private final ObjectMapper objectMapper; + + @Autowired + public OcrTaskServiceImpl(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public void create(Map task) { + // TODO Auto-generated method stub + try { + //源文件链接 + String fileUrl =task.get(Constants.FILEURL).toString(); + //下载源文件 + String format = fileUrl.replaceAll(".*\\.", Constants.EMPTY); + String fileName = UUID.randomUUID().toString() + "." + format; + String downloadFilePath = downloadFilePathPrefix + fileName; + DownLoadUtil.downloadFile(fileUrl, downloadFilePath); + //加载文件以base64编码 + String fileContent = encodeFileToBase64(downloadFilePath); + //删除文件 + FileUtil.delFile(downloadFilePath); + + Map param = new HashMap(16); + param.put(Constants.FILECONTENTS, fileContent); + param.put(Constants.FILENAME, fileName); + param.put(Constants.OPENPASSWORD, Constants.EMPTY); + param.put(Constants.OWNERPASSWORD, Constants.EMPTY); + param.put(Constants.LOCATIONPATH, Constants.EMPTY); + + String jobId = DownLoadUtil.doPost(createUrl, JSONObject.toJSONString(param)); + log.info("任务创建id:{}",jobId); + task.put(Constants.JOBID, URLEncoder.encode(jobId.replace("\"", ""), "UTF-8")); + //任务创建成功,放到监控结果队列中 + ConfigCache.resultQueue.put(task); + } catch (Throwable e) { + log.error("创建文档解析任务异常。e:",e); + //失败直接发送结果 + AppResultDoc entity = new AppResultDoc(); + long now = System.currentTimeMillis(); + entity.setFinishTime(now); + entity.setLastEdit(now); + Map result = new HashMap(16); + result.put(Constants.CONTENT, "识别任务创建异常"); + result.put(Constants.WROD_COUNT, 0); + entity.setResult(result); + entity.setStatus(2); + Map map = objectMapper.convertValue(entity, Map.class); + updateFields(task.get(Constants.TASKID).toString(),map); + } + + + } + + + /** + * 读取文件 base64格式 + * @param filePath 文件地址 + * @return + * @throws IOException + */ + private String encodeFileToBase64(String filePath) throws IOException { + byte[] fileContent = Files.readAllBytes(Paths.get(filePath)); + return Base64.getEncoder().encodeToString(fileContent); + } + + + @Override + public void parse(Map task) { + + // TODO Auto-generated method stub + try { + String jobId = (String) task.get(Constants.JOBID); + jobUrl = jobUrl.replace(Constants.IDRENAME,jobId); + log.info("jobUrl:{}",jobUrl); + String resStr = DownLoadUtil.doGet(jobUrl); + JSONObject res = JSONObject.parseObject(resStr); + int progress = (int) res.get(Constants.PROGRESS); + if (progress == 0) { + //识别中 -- 放回队列 + ConfigCache.resultQueue.put(task); + }else if (progress == 100) { + //识别成功 -- 获取请求结果 + queryUrl = queryUrl.replace(Constants.IDRENAME,jobId); + log.info("queryUrl:{}",queryUrl); + String queryResStr = DownLoadUtil.doGet(queryUrl); + Map queryRes = JSONObject.parseObject(queryResStr); + List> jobDocuments = (List>) queryRes.get(Constants.JOBDOCUMENTS); + List> outputDocuments = (List>) jobDocuments.get(0).get(Constants.OutputDocuments); + List> files = (List>) outputDocuments.get(0).get(Constants.FILES); + String fileContents = (String) files.get(0).get(Constants.FILECONTENTS); + String parseContent = readWordFromBase64(fileContents); + //成功 发送结果 + AppResultDoc entity = new AppResultDoc(); + long now = System.currentTimeMillis(); + entity.setFinishTime(now); + entity.setLastEdit(now); + Map result = new HashMap(16); + result.put(Constants.CONTENT, parseContent); + result.put(Constants.WROD_COUNT, parseContent.length()); + entity.setResult(result); + entity.setStatus(1); + Map map = objectMapper.convertValue(entity, Map.class); + updateFields(task.get(Constants.TASKID).toString(),map); + }else { + //识别异常 + log.error("文档识别异常:{}",resStr); + //发送失败结果 + AppResultDoc entity = new AppResultDoc(); + long now = System.currentTimeMillis(); + entity.setFinishTime(now); + entity.setLastEdit(now); + Map result = new HashMap(16); + result.put(Constants.CONTENT, "识别失败"); + result.put(Constants.WROD_COUNT, 0); + entity.setResult(result); + entity.setStatus(2); + Map map = objectMapper.convertValue(entity, Map.class); + updateFields(task.get(Constants.TASKID).toString(),map); + } + + + } catch (Throwable e) { + // TODO: handle exception + log.error("创建文档解析任务异常。e:",e); + //发送失败结果 + AppResultDoc entity = new AppResultDoc(); + long now = System.currentTimeMillis(); + entity.setFinishTime(now); + entity.setLastEdit(now); + Map result = new HashMap(16); + result.put(Constants.CONTENT, "源文件解析异常"); + result.put(Constants.WROD_COUNT, 0); + entity.setResult(result); + entity.setStatus(2); + Map map = objectMapper.convertValue(entity, Map.class); + updateFields(task.get(Constants.TASKID).toString(),map); + } + } + + /** + * 将Base64编码的Word文档读取为文本 + * @param base64Word Base64字符串 + * @return Word文本内容 + * @throws Exception + */ + public String readWordFromBase64(String base64Word) throws Exception { + byte[] bytes = Base64.getDecoder().decode(base64Word); + try (InputStream is = new ByteArrayInputStream(bytes)) { + // 尝试读取为docx格式 + try { + XWPFDocument docx = new XWPFDocument(is); + XWPFWordExtractor extractor = new XWPFWordExtractor(docx); + return extractor.getText(); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + } + + + /** + * 节点数据局部修改 + * @param docId 文档ID + * @param fields 要修改的字段集合 + * @return boolean 是否成功 + */ + private boolean updateFields(String docId, Map fields){ + if(fields == null || fields.isEmpty()){ + log.warn("修改失败,fields为空"); + return false; + } + + CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(esUser, esPassword)); + + CloseableHttpClient httpClient = null; + try { + if(esUser != null && !esUser.trim().equals(Constants.EMPTY)){ + httpClient = HttpClients.custom() + .setDefaultCredentialsProvider(credentialsProvider) + .build(); + } else { + httpClient = HttpClients.custom().build(); + } + + // 拼接 ES _update URL + StringBuilder url = new StringBuilder(); + url.append(esUrl) + .append("/") + .append(indexName) + .append("/_update/") + .append(docId); + + HttpPost httpPost = new HttpPost(url.toString()); + + // 构造请求体,只更新指定字段 + Map docMap = new HashMap<>(); + docMap.put("doc", fields); + + StringEntity entity = new StringEntity(JSONObject.toJSONString(docMap), ContentType.APPLICATION_JSON); + httpPost.setEntity(entity); + + HttpResponse response = httpClient.execute(httpPost); + int statusCode = response.getStatusLine().getStatusCode(); + String responseBody = EntityUtils.toString(response.getEntity()); + + if(statusCode == 200){ + log.info("数据成功局部更新到索引:{},文档ID:{},更新字段:{}", indexName, docId, fields.keySet()); + return true; + } else { + log.error("数据局部更新失败:{},文档ID:{},ES响应:{}", indexName, docId, responseBody); + return false; + } + + } catch (Exception e){ + log.error("数据局部更新异常:", e); + return false; + } finally { + try { + if(httpClient != null) { + httpClient.close(); + } + } catch (Exception ignored){} + } + } + + + public static void main(String[] args) throws Exception { +// OcrTaskServiceImpl ocrTaskServiceImpl = new OcrTaskServiceImpl(); +// String base64Word = "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"; +// String text = ocrTaskServiceImpl.readWordFromBase64(base64Word); +// System.out.println(text); +// String queryResStr = DownLoadUtil.doGet("https://frs.wefile.com:4431/FineReaderServer14/api/jobs/{92648AEE-3C07-4554-91F1-EA4F1F31C97B}"); +// System.out.println(queryResStr); +// OcrTaskServiceImpl ocrTaskServiceImpl = new OcrTaskServiceImpl(); +// String fileContent = ocrTaskServiceImpl.encodeFileToBase64("C:\\Users\\55007\\Downloads\\6bcafab08f1db336317904fab314ff66.png"); +// String fileName = "6bcafab08f1db336317904fab314ff66.png"; +// Map param = new HashMap(16); +// param.put(Constants.FILECONTENTS, fileContent); +// param.put(Constants.FILENAME, fileName); +// param.put(Constants.OPENPASSWORD, Constants.EMPTY); +// param.put(Constants.OWNERPASSWORD, Constants.EMPTY); +// param.put(Constants.LOCATIONPATH, Constants.EMPTY); +// String createUrl = "https://frs.wefile.com:4431/FineReaderServer14//api/workflows/Workflow-Macao/input/file"; +// String jobId = DownLoadUtil.doPost(createUrl,JSONObject.toJSONString(param)); +// System.out.println(jobId); + } +} diff --git a/ocr-service/src/main/java/com/bw/ocr/service/impl/TaskReceiveServiceImpl.java b/ocr-service/src/main/java/com/bw/ocr/service/impl/TaskReceiveServiceImpl.java new file mode 100644 index 0000000..867f6b8 --- /dev/null +++ b/ocr-service/src/main/java/com/bw/ocr/service/impl/TaskReceiveServiceImpl.java @@ -0,0 +1,55 @@ +package com.bw.ocr.service.impl; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.stereotype.Service; + +import com.alibaba.fastjson.JSONObject; +import com.bw.ocr.cache.ConfigCache; +import com.bw.ocr.entity.Constants; +import com.bw.ocr.service.TaskReceiveService; + +import lombok.extern.slf4j.Slf4j; + +/** + * 任务接收服务层实现类 + * @author jian.mao + * @date 2025年1月14日 + * @description + */ +@Service +@Slf4j +public class TaskReceiveServiceImpl implements TaskReceiveService { + + @Override + public String put(String dataJson) { + Map response = new HashMap<>(16); + int code = 200; + String message = "success"; + Map task = null; + try { + task = JSONObject.parseObject(dataJson); + } catch (Exception e) { + log.error("参数结构不合法,", e); + code = 100010; + message = "参数不合法"; + } + // 写入队列 + try { + if(task.containsKey(Constants.TRACE) && (boolean)task.get(Constants.TRACE)){ + ConfigCache.taskQueue.putFirst(task); + }else{ + ConfigCache.taskQueue.put(task); + } + } catch (InterruptedException e) { + log.error("任务写入队列异常,", e); + code = 100011; + message = "任务写入队列失败"; + } + response.put(Constants.CODE, code); + response.put(Constants.MESSAGE, message); + return JSONObject.toJSONString(response); + } + +} diff --git a/ocr-service/src/main/java/com/bw/ocr/utils/DataUtil.java b/ocr-service/src/main/java/com/bw/ocr/utils/DataUtil.java new file mode 100644 index 0000000..b887f25 --- /dev/null +++ b/ocr-service/src/main/java/com/bw/ocr/utils/DataUtil.java @@ -0,0 +1,48 @@ +package com.bw.ocr.utils; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.JSONPath; + +import java.util.Map; + +/** + * @author:jinming + * @className:DataUtil + * @version:1.0 + * @description: 获取dataValue的值 + * @Date:2023/11/1 9:54 + */ +public class DataUtil { + /** + * + * @param key 传入的key + * @param dataMap 数据map + * @return 根据传入的参数进行判断解析,返回正确的dataValue + */ + public static Object getValue(String key, Map dataMap) { + Object dataValue; + String isJson = "#json#"; + if (key.contains(isJson)) { + //进行第一次拆分,获取#json#前面的部分 + String[] keySplit = key.split(isJson); + String firstDataKey = keySplit[0]; + String[] firstDataKeySplit = firstDataKey.split(":"); + //取出前半部分对应的JSON数据并转换为JSONObject + String dataJson = (String) dataMap.get(firstDataKeySplit[0]); + JSONObject dataJsonObject = JSON.parseObject(dataJson); + //根据key的后半部分取出对应JSONObject中的值 + String firstDataKeyJson = (String) JSONPath.eval(dataJsonObject, firstDataKeySplit[1]); + String secDataKey = keySplit[1]; + JSONObject firstDataJsonObject = JSON.parseObject(firstDataKeyJson); + dataValue = JSONPath.eval(firstDataJsonObject, secDataKey); + return dataValue; + } + String[] keySplit = key.split(":"); + String jsonPath = keySplit[1]; + String dataJson = (String) dataMap.get(keySplit[0]); + JSONObject dataJsonObject = JSON.parseObject(dataJson); + dataValue = JSONPath.eval(dataJsonObject, jsonPath); + return dataValue; + } +} \ No newline at end of file diff --git a/ocr-service/src/main/java/com/bw/ocr/utils/DateUtil.java b/ocr-service/src/main/java/com/bw/ocr/utils/DateUtil.java new file mode 100644 index 0000000..f83af9f --- /dev/null +++ b/ocr-service/src/main/java/com/bw/ocr/utils/DateUtil.java @@ -0,0 +1,177 @@ +package com.bw.ocr.utils; + + +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Date; + +import lombok.extern.slf4j.Slf4j; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; + +/** + * 日期工具类 + * + * @author jian.mao + * @date 2022年11月15日 + * @description + */ +@Slf4j +public class DateUtil { + + /** + * @return + */ + public static String getTimeStrForNow() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHH"); + return sdf.format(new Date()); + } + + + public static String getTimeStrForDay(long time) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd"); + + return sdf.format(new Date(time * 1000)); + } + + public static String getTimeStrForDay() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd"); + + return sdf.format(new Date()); + } + + + public static String getDateTime() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String time = sdf.format(new Date()); + return time; + } + + public static String getDateTime(Long timestap) { + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String time = sdf.format(new Date(timestap)); + return time; + } + + public static String getDate(Long timestap) { + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + String time = sdf.format(new Date(timestap)); + return time; + } + + public static String getDateTimeForMonth() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM"); + String time = sdf.format(new Date()); + return time; + } + + /** + * 休眠 + * + * @param millis 毫秒 + */ + public static void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + /** + * 1. @Description:时间戳转时间 + * 2. @Author: ying.zhao + * 3. @Date: 2023/3/28 + */ + + public static String timestampToDate(String time) { + int thirteen = 13; + int ten = 10; + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); +// if (time.length() == thirteen) { + if (time.length() > ten) { + return sdf.format(new Date(Long.parseLong(time))); + } else { + return sdf.format(new Date(Integer.parseInt(time) * 1000L)); + } + } + + public static String parseCreated(String jsonTime){ + String formattedDateTime = getDateTime(); + try { + // 使用fastjson解析JSON数据 + JSONObject jsonObject = JSON.parseObject(jsonTime); + // 获取日期和时间的值 + JSONObject dateObject = jsonObject.getJSONObject("date"); + int day = dateObject.getIntValue("day"); + int month = dateObject.getIntValue("month"); + int year = dateObject.getIntValue("year"); + + JSONObject timeObject = jsonObject.getJSONObject("time"); + int hour = timeObject.getIntValue("hour"); + int minute = timeObject.getIntValue("minute"); + int second = timeObject.getIntValue("second"); + + // 创建LocalDateTime对象 + LocalDateTime dateTime = LocalDateTime.of(year, month, day, hour, minute, second); + + // 定义日期时间格式化器 + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + // 格式化日期时间 + formattedDateTime = dateTime.format(formatter); + } catch (Exception e) { + log.info("日期转换失败:{}",e); + } + return formattedDateTime; + } + + /** + * 字符串转换日期 + * @param format + * @param date + * @return + */ + public static Date strToDate(String format,String date){ + SimpleDateFormat sdf = new SimpleDateFormat(format); + if (date == null || date.equals("")){ + return new Date(); + }else{ + Date ru = null; + try { + ru = sdf.parse(date); + } catch (ParseException e) { + e.printStackTrace(); + } + return ru; + } + } + /** + * 日期格式话 + * @param format 日期格式 + * @param dater 要转换的日期,默认当前时间 + * @return + */ + public static String FormatDate(String format,Date date){ + String fromatDate = null; + SimpleDateFormat sdf = new SimpleDateFormat(format); + if (date == null){ + fromatDate = sdf.format(new Date()); + }else{ + fromatDate = sdf.format(date); + } + return fromatDate; + } + public static void main(String[] args) { + String time = timestampToDate("955814400000"); + System.out.println(time); + } +} diff --git a/ocr-service/src/main/java/com/bw/ocr/utils/DownLoadUtil.java b/ocr-service/src/main/java/com/bw/ocr/utils/DownLoadUtil.java new file mode 100644 index 0000000..99a6b60 --- /dev/null +++ b/ocr-service/src/main/java/com/bw/ocr/utils/DownLoadUtil.java @@ -0,0 +1,1004 @@ +package com.bw.ocr.utils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.StatusLine; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.AuthCache; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.HttpClient; +import org.apache.http.client.HttpRequestRetryHandler; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.config.SocketConfig; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.LayeredConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.auth.BasicScheme; +import org.apache.http.impl.client.BasicAuthCache; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.client.LaxRedirectStrategy; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.bw.ocr.entity.Constants; + +import okhttp3.Call; +import okhttp3.Headers; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + + + + + + + + + +/** + * 下载工具类 + * @author jian.mao + * @date 2023年9月19日 + * @description + */ +public class DownLoadUtil { + + private static String ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36"; + private final static Logger log = LoggerFactory.getLogger(DownLoadUtil.class); + /** 代理服务器(产品官网 www.16yun.cn) **/ + final static String PROXYHOST = "u270.40.tp.16yun.cn"; + final static Integer PROXYPORT = 6448; + /** 代理验证信息 **/ + final static String PROXYUSER = "16HFBVJC"; + final static String PROXYPASS = "897944"; + + private static PoolingHttpClientConnectionManager cm = null; + private static HttpRequestRetryHandler httpRequestRetryHandler = null; + private static HttpHost proxy = null; + + private static CredentialsProvider credsProvider = null; + private static RequestConfig reqConfig = null; + + static { + ConnectionSocketFactory plainsf = PlainConnectionSocketFactory + .getSocketFactory(); + LayeredConnectionSocketFactory sslsf = SSLConnectionSocketFactory + .getSocketFactory(); + + Registry registry = RegistryBuilder.create().register("http", plainsf) + .register("https", sslsf).build(); + + cm = new PoolingHttpClientConnectionManager(registry); + cm.setMaxTotal(20); + cm.setDefaultMaxPerRoute(5); + + proxy = new HttpHost(PROXYHOST, PROXYPORT, "https"); + + credsProvider = new BasicCredentialsProvider(); + credsProvider.setCredentials(AuthScope.ANY, + new UsernamePasswordCredentials(PROXYUSER, PROXYPASS)); + + reqConfig = RequestConfig.custom().setConnectionRequestTimeout(5000) + .setConnectTimeout(5000).setSocketTimeout(5000) + .setExpectContinueEnabled(false) + .setProxy(new HttpHost(PROXYHOST, PROXYPORT)).build(); + } + + /** + * 模拟客户端get请求 + * + * @param url + * 模拟请求得url + * @param headers + * 头部信息,没有可以不传 + * @return + */ + @SafeVarargs + public static String proxyDoGet(String url, Map... headers) { + // 设置超时时间 + int timeout = 30; + RequestConfig config = RequestConfig.custom() + .setConnectTimeout(timeout * 1000) + .setConnectionRequestTimeout(timeout * 1000) + .setSocketTimeout(timeout * 1000).build(); + SocketConfig socketConfig = SocketConfig.custom() + .setSoKeepAlive(false) + .setSoLinger(1) + .setSoReuseAddress(true) + .setSoTimeout(timeout * 1000) + .setTcpNoDelay(true).build(); + AuthCache authCache = new BasicAuthCache(); + authCache.put(proxy, new BasicScheme()); + HttpClientContext localContext = HttpClientContext.create(); + localContext.setAuthCache(authCache); + HttpClientBuilder httpBuilder = HttpClientBuilder.create(); + CloseableHttpClient httpClient = httpBuilder + .setDefaultSocketConfig(socketConfig) + .setDefaultRequestConfig(config) + .setDefaultCredentialsProvider(credsProvider).build(); + HttpGet httpGet = new HttpGet(url); + httpGet.setConfig(reqConfig); + if (headers != null && headers.length > 0) { + Map tempHeaders = headers[0]; + for (String key : tempHeaders.keySet()) { + httpGet.setHeader(key, tempHeaders.get(key).toString()); + } + } else { + httpGet.setHeader("Accept", + "application/json, text/javascript, */*; q=0.01"); + httpGet.setHeader("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8"); + } + CloseableHttpResponse response = null; + String html = ""; + int notFundCode = 404; + int successCode = 200; + try { + response = httpClient.execute(httpGet, localContext); + // 从响应模型中获取响应实体 + HttpEntity responseEntity = response.getEntity(); + StatusLine statusLine = response.getStatusLine(); + System.out.println("响应状态为:" + response.getStatusLine()); + if (statusLine.getStatusCode() == successCode) { + if (responseEntity != null) { + html = EntityUtils.toString(responseEntity, "utf-8"); + System.out.println("响应内容长度为:" + + responseEntity.getContentLength()); + // 下载结果为空不正常 + if (html.equals(Constants.EMPTY)) { + html = "Download failed error is:reslut is null"; + } + } + } else if (statusLine.getStatusCode() == notFundCode) { + html = "

页面404,正常结束请求即可

"; + } else { + throw new Exception("请求错误,code码为:" + statusLine.getStatusCode()); + } + } catch (Exception e) { + e.printStackTrace(); + html = "Download failed error is:reslut is null"; + }finally{ + try { + response.close(); + httpClient.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + return html; + + } + + + public static String httpsslProxyGet(String url, Map... headers) throws Exception { + //采用绕过验证的方式处理https请求 + SSLContext sslcontext = createIgnoreVerifySSL(); + + // 设置协议http和https对应的处理socket链接工厂的对象 + Registry socketFactoryRegistry = RegistryBuilder.create() + .register("http", PlainConnectionSocketFactory.INSTANCE) + .register("https", new SSLConnectionSocketFactory(sslcontext)) + .build(); + PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry); + connManager.setMaxTotal(50); + connManager.setDefaultMaxPerRoute(10); + HttpClients.custom().setConnectionManager(connManager); + // 设置超时时间 + int timeout = 30; + RequestConfig config = RequestConfig.custom() + .setConnectTimeout(timeout * 1000) + .setConnectionRequestTimeout(timeout * 1000) + .setSocketTimeout(timeout * 1000).build(); + SocketConfig socketConfig = SocketConfig.custom() + .setSoKeepAlive(false) + .setSoLinger(1) + .setSoReuseAddress(true) + .setSoTimeout(timeout * 1000) + .setTcpNoDelay(true).build(); + AuthCache authCache = new BasicAuthCache(); + authCache.put(proxy, new BasicScheme()); + HttpClientContext localContext = HttpClientContext.create(); + localContext.setAuthCache(authCache); + HttpClientBuilder httpBuilder = HttpClientBuilder.create(); + CloseableHttpClient httpClient = httpBuilder + .setConnectionManager(connManager) + .setDefaultSocketConfig(socketConfig) + .setDefaultRequestConfig(config) + .setDefaultCredentialsProvider(credsProvider).build(); + HttpGet httpGet = new HttpGet(url); + httpGet.setConfig(reqConfig); + if (headers != null && headers.length > 0) { + Map tempHeaders = headers[0]; + for (String key : tempHeaders.keySet()) { + httpGet.setHeader(key, tempHeaders.get(key).toString()); + } + } else { + httpGet.setHeader("Accept", + "application/json, text/javascript, */*; q=0.01"); + httpGet.setHeader("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8"); + } + CloseableHttpResponse response = null; + String html = ""; + int notFundCode = 404; + int successCode = 200; + try { + response = httpClient.execute(httpGet, localContext); + // 从响应模型中获取响应实体 + HttpEntity responseEntity = response.getEntity(); + StatusLine statusLine = response.getStatusLine(); + System.out.println("响应状态为:" + response.getStatusLine()); + if (statusLine.getStatusCode() == successCode) { + if (responseEntity != null) { + html = EntityUtils.toString(responseEntity, "utf-8"); + System.out.println("响应内容长度为:" + + responseEntity.getContentLength()); + // 下载结果为空不正常 + if (html.equals(Constants.EMPTY)) { + html = "Download failed error is:reslut is null"; + } + } + } else if (statusLine.getStatusCode() == notFundCode) { + html = "

页面404,正常结束请求即可

"; + } else { + throw new Exception("请求错误,code码为:" + statusLine.getStatusCode()); + } + } catch (Exception e) { + e.printStackTrace(); + html = "Download failed error is:reslut is null"; + }finally{ + try { + response.close(); + httpClient.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + return html; + + } + + + /** + * json参数方式POST提交 + * @param url + * @param params + * @return + */ + public static String doPost(String url, String params, Map... headers){ + String strResult = ""; + //设置超时时间 + int timeout = 60; + RequestConfig config = RequestConfig.custom(). + setConnectTimeout(timeout * 1000). + setConnectionRequestTimeout(timeout * 1000). + setSocketTimeout(timeout * 1000).build(); + SocketConfig socketConfig = SocketConfig.custom() + .setSoKeepAlive(false) + .setSoLinger(1) + .setSoReuseAddress(true) + .setSoTimeout(timeout * 1000) + .setTcpNoDelay(true).build(); +// AuthCache authCache = new BasicAuthCache(); +// authCache.put(proxy, new BasicScheme()); +// HttpClientContext localContext = HttpClientContext.create(); +// localContext.setAuthCache(authCache); + // 1. 获取默认的client实例 + HttpClientBuilder httpBuilder = HttpClientBuilder.create(); + httpBuilder.setUserAgent(ua); + HttpClient client = httpBuilder.setDefaultSocketConfig(socketConfig).setDefaultRequestConfig(config).build(); +// HttpClient client = httpBuilder.setDefaultSocketConfig(socketConfig).setDefaultRequestConfig(config).setConnectionManager(cm) +// .setDefaultCredentialsProvider(credsProvider).build(); + // 2. 创建httppost实例 + HttpPost httpPost = new HttpPost(url); +// httpPost.setConfig(reqConfig); + if (headers != null && headers.length > 0) { + Map tempHeaders = headers[0]; + for (String key : tempHeaders.keySet()) { + httpPost.setHeader(key, tempHeaders.get(key).toString()); + } + } else { + httpPost.addHeader("Content-Type", "application/json;charset=utf-8"); + } + HttpResponse resp = null; + try { + httpPost.setEntity(new StringEntity(params,"utf-8")); + resp = client.execute(httpPost); +// resp = client.execute(httpPost,localContext); + StatusLine statusLine = resp.getStatusLine(); + System.out.println("响应状态为:" + resp.getStatusLine()); + int notFundCode = 300; + int successCode = 200; + if(statusLine.getStatusCode() >= successCode && statusLine.getStatusCode() < notFundCode){ + // 7. 获取响应entity + HttpEntity respEntity = resp.getEntity(); + strResult = EntityUtils.toString(respEntity, "UTF-8"); + if(strResult.equals(Constants.EMPTY)){ + strResult = "Download failed error is:reslut is null"; + } + }else{ + throw new Exception("请求错误,code码为:"+statusLine.getStatusCode()); + } + } catch (Exception e) { + e.printStackTrace(); + strResult = "Download failed error is:"+ThrowMessageUtil.getErrmessage(e); + } + return strResult; + } + public static String httpPost(String url,String params) { + String html=""; + html = doPost(url,params); + int i = 1; + while(true){ + if(html.contains("Download failed error is:")){ + log.error("DownLoadUtil------------->download is failure,url is:"+url); + DateUtil.sleep(5000); + i++; + }else{ + break; + } + if(i > 5){ + break; + } + html = doPost(url,params); + } + return html; + } + /** + * 绕过验证 + * + * @return + * @throws NoSuchAlgorithmException + * @throws KeyManagementException + */ + public static SSLContext createIgnoreVerifySSL() throws NoSuchAlgorithmException, KeyManagementException { + SSLContext sc = SSLContext.getInstance("SSLv3"); + + // 实现一个X509TrustManager接口,用于绕过验证,不用修改里面的方法 + X509TrustManager trustManager = new X509TrustManager() { + @Override + public void checkClientTrusted( + java.security.cert.X509Certificate[] paramArrayOfX509Certificate, + String paramString) throws CertificateException { + } + + @Override + public void checkServerTrusted( + java.security.cert.X509Certificate[] paramArrayOfX509Certificate, + String paramString) throws CertificateException { + } + + @Override + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return null; + } + }; + + sc.init(null, new TrustManager[] { trustManager }, null); + return sc; + } + /** + * 模拟请求 + * + * @param url 资源地址 + * @param map 参数列表 + * @param encoding 编码 + * @return + * @throws NoSuchAlgorithmException + * @throws KeyManagementException + * @throws IOException + * @throws ClientProtocolException + */ + public static String httpsslGet(String url,Map ... headers) { + String html=""; + CloseableHttpClient client = null; + HttpEntity responseEntity = null; + CloseableHttpResponse response = null; + try { + log.debug("DownLoadUtil------------->设置下载相关信息, start...."); + //采用绕过验证的方式处理https请求 + SSLContext sslcontext = createIgnoreVerifySSL(); + + // 设置协议http和https对应的处理socket链接工厂的对象 + Registry socketFactoryRegistry = RegistryBuilder.create() + .register("http", PlainConnectionSocketFactory.INSTANCE) + .register("https", new SSLConnectionSocketFactory(sslcontext)) + .build(); + PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry); + connManager.setMaxTotal(50); + connManager.setDefaultMaxPerRoute(10); + HttpClients.custom().setConnectionManager(connManager); + //设置超时时间 + int timeout = 30; + RequestConfig config = RequestConfig.custom(). + setConnectTimeout(timeout * 1000). + setConnectionRequestTimeout(timeout * 1000). + setSocketTimeout(timeout * 1000).build(); + SocketConfig socketConfig = SocketConfig.custom() + .setSoKeepAlive(false) + .setSoLinger(1) + .setSoReuseAddress(true) + .setSoTimeout(10000) + .setTcpNoDelay(true).build(); + // 设置重定向策略 + LaxRedirectStrategy redirectStrategy = new LaxRedirectStrategy(); + //创建自定义的httpclient对象 + client = HttpClients.custom().setConnectionManager(connManager).setDefaultRequestConfig(config).setRedirectStrategy(redirectStrategy).setDefaultSocketConfig(socketConfig).setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36").build(); +// CloseableHttpClient client = HttpClients.createDefault(); + + HttpGet httpGet = new HttpGet(url); + if(headers != null && headers.length > 0){ + Map tempHeaders = headers[0]; + for (String key : tempHeaders.keySet()) { + httpGet.setHeader(key,tempHeaders.get(key).toString()); + } + }else{ + httpGet.setHeader("Accept", "application/json, text/javascript, */*; q=0.01"); + httpGet.setHeader("Accept-Language","zh-CN,zh;q=0.9,en;q=0.8"); + httpGet.setHeader("User-Agent","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36"); + } + log.debug("DownLoadUtil------------->设置下载相关信息, end...."); + try { + int notFundCode = 404; + int successCode = 200; + log.debug("DownLoadUtil------------->下载执行,start...."); + httpGet.setConfig(config); + response = client.execute(httpGet); + log.debug("DownLoadUtil------------->下载执行,end...."); + // 从响应模型中获取响应实体 + StatusLine statusLine = response.getStatusLine(); + log.debug("DownLoadUtil------------->响应状态为:" + response.getStatusLine()+",下载请求没问题url:"+url+",read is start ...."); + System.out.println("响应状态为:" + response.getStatusLine()); + responseEntity = response.getEntity(); + log.debug("DownLoadUtil------------->响应状态为:" + response.getStatusLine()+",下载请求没问题url:"+url+",read is end ...."); + if(statusLine.getStatusCode() == successCode){ + if (responseEntity != null) { + html=EntityUtils.toString(responseEntity,"utf-8"); + System.out.println("响应内容长度为:" + responseEntity.getContentLength()); + } + }else if(statusLine.getStatusCode() == notFundCode){ + html = "

页面404,正常结束请求即可

"; + }else{ + throw new Exception("请求错误,code码为:"+statusLine.getStatusCode()); + } + } catch (Exception e) { + e.printStackTrace(); + html = "Download failed error is:"+ThrowMessageUtil.getErrmessage(e); + + } + } catch (Exception e) { + e.printStackTrace(); + html = "Download failed error is:"+ThrowMessageUtil.getErrmessage(e); + }finally{ + try { + responseEntity.getContent().close(); + response.close(); + client.close(); + } catch (Exception e) { + e.printStackTrace(); + } + + } + + + return html; + } + + public static String httpSSLGet(String url,Map ... headers) { + String html=""; + html = httpsslGet(url,headers); + int i = 1; + while(true){ + if(html.contains("Download failed error is:")){ + log.error("DownLoadUtil------------->download is failure,url is:"+url); + DateUtil.sleep(30000); + i++; + }else{ + break; + } + if(i > 5){ + break; + } + html = httpsslGet(url,headers); + } + return html; + } + public static String doPostFrom(String url,Map param,Map ... headers){ + //设置超时时间 + int timeout = 15; + RequestConfig config = RequestConfig.custom(). + setConnectTimeout(timeout * 1000). + setConnectionRequestTimeout(timeout * 1000). + setSocketTimeout(timeout * 1000).build(); + SocketConfig socketConfig = SocketConfig.custom() + .setSoKeepAlive(false) + .setSoLinger(1) + .setSoReuseAddress(true) + .setSoTimeout(10000) + .setTcpNoDelay(true).build(); +// AuthCache authCache = new BasicAuthCache(); +// authCache.put(proxy, new BasicScheme()); +// HttpClientContext localContext = HttpClientContext.create(); +// localContext.setAuthCache(authCache); + HttpClientBuilder httpBuilder = HttpClientBuilder.create(); + httpBuilder.setUserAgent(ua); +// HttpClient httpClient = httpBuilder.setDefaultSocketConfig(socketConfig).setDefaultRequestConfig(config).setConnectionManager(cm) +// .setDefaultCredentialsProvider(credsProvider).build(); + HttpClient httpClient = httpBuilder.setDefaultSocketConfig(socketConfig).setDefaultRequestConfig(config).build(); + HttpPost httpPost = new HttpPost(url); +// httpPost.setConfig(reqConfig); + if(headers != null && headers.length > 0){ + Map tempHeaders = headers[0]; + for (String key : tempHeaders.keySet()) { + httpPost.setHeader(key,tempHeaders.get(key).toString()); + } + }else{ + httpPost.addHeader("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"); + httpPost.addHeader("accept-Language", "zh-CN,zh;q=0.9,en;q=0.8"); + httpPost.addHeader("content-type", "application/x-www-form-urlencoded"); + httpPost.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36"); +// httpPost.addHeader("Referer", "http://www.neeq.com.cn/rule/Business_rules.html"); + } + // 创建请求参数 + List list = new LinkedList<>(); + for (String key : param.keySet()) { + BasicNameValuePair param1 = new BasicNameValuePair(key,param.get(key).toString()); + list.add(param1); + } + // 使用URL实体转换工具 + String html=""; + try { + UrlEncodedFormEntity entityParam = new UrlEncodedFormEntity(list, "UTF-8"); + httpPost.setEntity(entityParam); + HttpResponse response = httpClient.execute(httpPost); +// HttpResponse response = httpClient.execute(httpPost,localContext); + // 从响应模型中获取响应实体 + HttpEntity responseEntity = response.getEntity(); + StatusLine statusLine = response.getStatusLine(); + System.out.println("响应状态为:" + response.getStatusLine()); + int notFundCode = 404; + int successCode = 200; + if(statusLine.getStatusCode() == successCode){ + if (responseEntity != null) { + html=EntityUtils.toString(responseEntity,"utf-8"); + } + }else{ + throw new Exception("请求错误,code码为:"+statusLine.getStatusCode()); + } + + } catch (Exception e) { + e.printStackTrace(); + html = "Download failed error is:"+ThrowMessageUtil.getErrmessage(e); + } + + return html; + + } + public static String httpPostForm(String url,Map params,Map ... headers) { + String html=""; + html = doPostFrom(url,params); + int i = 1; + while(true){ + if(html.contains("Download failed error is:")){ + log.error("DownLoadUtil------------->download is failure,url is:"+url); + DateUtil.sleep(5000); + i++; + }else{ + break; + } + if(i > 5){ + break; + } + html = doPostFrom(url,params,headers); + } + return html; + } + + public static String dosslPost(String url,String params,Map ... headers) { + String html=""; + CloseableHttpClient client = null; + HttpEntity responseEntity = null; + CloseableHttpResponse response = null; + try { + //采用绕过验证的方式处理https请求 + SSLContext sslcontext = createIgnoreVerifySSL(); + // 设置协议http和https对应的处理socket链接工厂的对象 + Registry socketFactoryRegistry = RegistryBuilder.create() + .register("http", PlainConnectionSocketFactory.INSTANCE) + .register("https", new SSLConnectionSocketFactory(sslcontext)) + .build(); + PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry); + HttpClients.custom().setConnectionManager(connManager); + //设置超时时间 + int timeout = 5; + RequestConfig config = RequestConfig.custom(). + setConnectTimeout(timeout * 1000). + setConnectionRequestTimeout(timeout * 1000). + setSocketTimeout(timeout * 1000).build(); + SocketConfig socketConfig = SocketConfig.custom() + .setSoKeepAlive(false) + .setSoLinger(1) + .setSoReuseAddress(true) + .setSoTimeout(10000) + .setTcpNoDelay(true).build(); + //创建自定义的httpclient对象 + client = HttpClients.custom().setConnectionManager(connManager).setDefaultRequestConfig(config).setDefaultSocketConfig(socketConfig).build(); +// CloseableHttpClient client = HttpClients.createDefault(); + // 2. 创建httppost实例 + HttpPost httpPost = new HttpPost(url); +// httpPost.setConfig(reqConfig); + httpPost.addHeader("Content-Type", "application/json;charset=utf-8"); + if(headers != null && headers.length > 0){ + Map tempHeaders = headers[0]; + for (String key : tempHeaders.keySet()) { + httpPost.setHeader(key,tempHeaders.get(key).toString()); + } + }else{ + httpPost.setHeader("Accept", "application/json, text/javascript, */*; q=0.01"); + httpPost.setHeader("Accept-Language","zh-CN,zh;q=0.9,en;q=0.8"); + httpPost.setHeader("User-Agent","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36"); + } + + try { + httpPost.setEntity(new StringEntity(params,"utf-8")); + response = client.execute(httpPost); + int notFundCode = 404; + int successCode = 200; + // 从响应模型中获取响应实体 + StatusLine statusLine = response.getStatusLine(); + System.out.println("响应状态为:" + response.getStatusLine()); + responseEntity = response.getEntity(); + if(statusLine.getStatusCode() == successCode){ + if (responseEntity != null) { + html=EntityUtils.toString(responseEntity,"utf-8"); + System.out.println("响应内容长度为:" + responseEntity.getContentLength()); + } + }else if(statusLine.getStatusCode() == notFundCode){ + html = "

页面404,正常结束请求即可

"; + }else{ + throw new Exception("请求错误,code码为:"+statusLine.getStatusCode()); + } + } catch (Exception e) { + e.printStackTrace(); + html = "Download failed error is:"+ThrowMessageUtil.getErrmessage(e); + + } + } catch (Exception e) { + e.printStackTrace(); + html = "Download failed error is:"+ThrowMessageUtil.getErrmessage(e); + }finally{ + try { + responseEntity.getContent().close(); + response.close(); + client.close(); + } catch (UnsupportedOperationException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + + } + + + return html; + } + public static String dosslPostForm(String url,Map param,Map ... headers) { + String html=""; + try { + //采用绕过验证的方式处理https请求 + SSLContext sslcontext = createIgnoreVerifySSL(); + + // 设置协议http和https对应的处理socket链接工厂的对象 + Registry socketFactoryRegistry = RegistryBuilder.create() + .register("http", PlainConnectionSocketFactory.INSTANCE) + .register("https", new SSLConnectionSocketFactory(sslcontext)) + .build(); + PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry); + HttpClients.custom().setConnectionManager(connManager); + //设置超时时间 + int timeout = 5; + RequestConfig config = RequestConfig.custom(). + setConnectTimeout(timeout * 1000). + setConnectionRequestTimeout(timeout * 1000). + setSocketTimeout(timeout * 1000).build(); + SocketConfig socketConfig = SocketConfig.custom() + .setSoKeepAlive(false) + .setSoLinger(1) + .setSoReuseAddress(true) + .setSoTimeout(10000) + .setTcpNoDelay(true).build(); + //创建自定义的httpclient对象 + CloseableHttpClient client = HttpClients.custom().setConnectionManager(connManager).setDefaultRequestConfig(config).setDefaultSocketConfig(socketConfig).build(); +// CloseableHttpClient client = HttpClients.createDefault(); + // 2. 创建httppost实例 + HttpPost httpPost = new HttpPost(url); +// httpPost.setConfig(reqConfig); + if(headers != null && headers.length > 0){ + Map tempHeaders = headers[0]; + for (String key : tempHeaders.keySet()) { + httpPost.setHeader(key,tempHeaders.get(key).toString()); + } + }else{ + httpPost.setHeader("Accept", "application/json, text/javascript, */*; q=0.01"); + httpPost.setHeader("Accept-Language","zh-CN,zh;q=0.9,en;q=0.8"); + httpPost.addHeader("content-type", "application/x-www-form-urlencoded"); + httpPost.setHeader("User-Agent","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36"); + } + + // 创建请求参数 + List list = new LinkedList<>(); + for (String key : param.keySet()) { + BasicNameValuePair param1 = new BasicNameValuePair(key,param.get(key).toString()); + list.add(param1); + } + // 使用URL实体转换工具 + try { + UrlEncodedFormEntity entityParam = new UrlEncodedFormEntity(list, "UTF-8"); + httpPost.setEntity(entityParam); + HttpResponse response = client.execute(httpPost); +// HttpResponse response = httpClient.execute(httpPost,localContext); + // 从响应模型中获取响应实体 + int notFundCode = 404; + int successCode = 200; + HttpEntity responseEntity = response.getEntity(); + StatusLine statusLine = response.getStatusLine(); + System.out.println("响应状态为:" + response.getStatusLine()); + if(statusLine.getStatusCode() == successCode){ + if (responseEntity != null) { + html=EntityUtils.toString(responseEntity,"utf-8"); + } + }else{ + throw new Exception("请求错误,code码为:"+statusLine.getStatusCode()); + } + + } catch (Exception e) { + e.printStackTrace(); + html = "Download failed error is:"+ThrowMessageUtil.getErrmessage(e); + } + } catch (Exception e) { + e.printStackTrace(); + html = "Download failed error is:"+ThrowMessageUtil.getErrmessage(e); + } + + + return html; + } + public static String httpSSLPostForm(String url,Map params,Map ...headers) { + String html=""; + try { + html = dosslPostForm(url,params,headers); + } catch (Exception e) { + e.printStackTrace(); + // TODO: handle exception + html = "Download failed error is:Exception!"; + } + int i = 1; + while(true){ + if(html.contains("Download failed error is:")){ + log.error("DownLoadUtil------------->download is failure,url is:"+url); + DateUtil.sleep(30000); + i++; + }else{ + break; + } + if(i > 5){ + break; + } + try { + html = dosslPostForm(url,params,headers); + } catch (Exception e) { + e.printStackTrace(); + // TODO: handle exception + html = "Download failed error is:Exception!"; + } + } + return html; + } + public static String httpSSLPost(String url,String params,Map ...headers) { + String html=""; + try { + html = dosslPost(url,params,headers); + } catch (Throwable e) { + e.printStackTrace(); + // TODO: handle exception + html = "Download failed error is:Exception!"; + } + int i = 1; + while(true){ + if(html.contains("Download failed error is:")){ + log.error("DownLoadUtil------------->download is failure,url is:"+url); + DateUtil.sleep(30000); + i++; + }else{ + break; + } + if(i > 5){ + break; + } + try { + html = dosslPost(url,params,headers); + } catch (Throwable e) { + e.printStackTrace(); + // TODO: handle exception + html = "Download failed error is:Exception!"; + } + } + return html; + } + + /** + * 模拟客户端get请求 + * @param url 模拟请求得url + * @param headers 头部信息,没有可以不传 + * @return + */ + public static String doGet(String url,Map ... headers){ + //设置超时时间 + int timeout = 15; + RequestConfig config = RequestConfig.custom(). + setConnectTimeout(timeout * 1000). + setConnectionRequestTimeout(timeout * 1000). + setSocketTimeout(timeout * 1000).build(); + SocketConfig socketConfig = SocketConfig.custom() + .setSoKeepAlive(false) + .setSoLinger(1) + .setSoReuseAddress(true) + .setSoTimeout(10000) + .setTcpNoDelay(true).build(); + HttpClientBuilder httpBuilder = HttpClientBuilder.create(); + httpBuilder.setUserAgent(ua); + HttpClient httpClient = httpBuilder.setDefaultSocketConfig(socketConfig).setDefaultRequestConfig(config).build(); + HttpGet httpGet = new HttpGet(url); + if(headers != null && headers.length > 0){ + Map tempHeaders = headers[0]; + for (String key : tempHeaders.keySet()) { + httpGet.setHeader(key,tempHeaders.get(key).toString()); + } + }else{ + httpGet.setHeader("Accept", "application/json, text/javascript, */*; q=0.01"); + httpGet.setHeader("Accept-Language","zh-CN,zh;q=0.9,en;q=0.8"); + } + String html=""; + try { + HttpResponse response = httpClient.execute(httpGet); + // 从响应模型中获取响应实体 + HttpEntity responseEntity = response.getEntity(); + StatusLine statusLine = response.getStatusLine(); + System.out.println("响应状态为:" + response.getStatusLine()); + int notFundCode = 404; + int successCode = 200; + int successCodeMax = 300; + if(statusLine.getStatusCode() >= successCode && statusLine.getStatusCode() < successCodeMax){ + if (responseEntity != null) { + html=EntityUtils.toString(responseEntity,"utf-8"); + if(html.equals("")){ + html = "Download failed error is:reslut is null"; + } + } + }else if(statusLine.getStatusCode() == notFundCode){ + html = "

页面404,正常结束请求即可

"; + }else{ + throw new Exception("请求错误,code码为:"+statusLine.getStatusCode()); + } + } catch (Exception e) { + e.printStackTrace(); + html = "Download failed error is:"+ThrowMessageUtil.getErrmessage(e); + } + return html; + + } + + /** + * 文件下载 + * @param fileURL 文件链接 + * @param destinationFilePath 文件存储地址 + * @throws IOException + */ + public static void downloadFile(String fileURL, String destinationFilePath) throws IOException { + // 设置连接超时和读取超时 + RequestConfig config = RequestConfig.custom() + // 设置连接超时为10秒 + .setConnectTimeout(10000) + // 设置读取超时为30秒 + .setSocketTimeout(30000) + .build(); + + // 创建 HttpClient 实例 + try (CloseableHttpClient httpClient = HttpClients.custom() + .setDefaultRequestConfig(config) + .build()) { + + // 创建 HttpGet 请求 + HttpGet request = new HttpGet(URI.create(fileURL)); + + // 执行请求 + try (CloseableHttpResponse response = httpClient.execute(request)) { + // 获取响应的输入流 + InputStream inputStream = response.getEntity().getContent(); + try (FileOutputStream outputStream = new FileOutputStream(destinationFilePath)) { + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + } + log.info("文件下载成功---{}" , destinationFilePath); + } + } + } + + /** + * 文件上传 + * @param filePath + * @param gofastUrl + * @return + */ + public static String upLoadFile(String filePath,String gofastUrl) { + File file = new File(filePath); + String realFilename = filePath.substring(filePath.lastIndexOf(File.separator) + 1); + MultipartBody.Builder builder = new MultipartBody.Builder().setType(MultipartBody.FORM); + builder.addPart(Headers.of("Content-Disposition", "form-data; name=\"file\";filename=\"" + realFilename + "\""), + RequestBody.create(MediaType.parse("image/png"), file) + + ).addFormDataPart("output", "json").build(); + RequestBody body = builder.build(); + Request request = new Request.Builder().url(gofastUrl).post(body).header("Expect", "100-continue").build(); + OkHttpClient.Builder okBuilder = new OkHttpClient.Builder(); + OkHttpClient client = okBuilder.connectTimeout(600, TimeUnit.MILLISECONDS) + .readTimeout(600, TimeUnit.SECONDS).build(); + Call call = client.newCall(request); + String html = ""; + Response response = null; + try { + response = call.execute(); + html = response.body().string(); + } catch (IOException e) { + log.info("upload fail:{}", filePath); + e.printStackTrace(); + } finally { + response.close(); + } + return html; + } + public static void main(String[] args) throws Exception { + + } +} diff --git a/ocr-service/src/main/java/com/bw/ocr/utils/EncryptionUtil.java b/ocr-service/src/main/java/com/bw/ocr/utils/EncryptionUtil.java new file mode 100644 index 0000000..60dff1b --- /dev/null +++ b/ocr-service/src/main/java/com/bw/ocr/utils/EncryptionUtil.java @@ -0,0 +1,27 @@ +package com.bw.ocr.utils; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * @author jian.mao + * @date 2023年3月10日 + * @description + */ +public class EncryptionUtil { + public static String md5(String text) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(text.getBytes()); + byte[] bytes = md.digest(); + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02x", b & 0xff)); + } + return sb.toString(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + return null; + } + } +} diff --git a/ocr-service/src/main/java/com/bw/ocr/utils/FileUtil.java b/ocr-service/src/main/java/com/bw/ocr/utils/FileUtil.java new file mode 100644 index 0000000..904c819 --- /dev/null +++ b/ocr-service/src/main/java/com/bw/ocr/utils/FileUtil.java @@ -0,0 +1,41 @@ +package com.bw.ocr.utils; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +/** + * 文件工具类 + * @author jian.mao + * @date 2023年7月14日 + * @description + */ +public class FileUtil { + + /** + * 数据写入文件 + * @param Path 文件路径 + * @param result 数据 + * @throws IOException + */ + public static void writeFile(String path,String result){ + try { + FileWriter fw = new FileWriter(path,true); + fw.write(result+"\n"); + fw.flush(); + fw.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static void delFile(String path) { + try { + File file = new File(path); + file.delete(); + } catch (Exception e) { + // TODO: handle exception + e.printStackTrace(); + } + } +} diff --git a/ocr-service/src/main/java/com/bw/ocr/utils/GPTResultParseUtil.java b/ocr-service/src/main/java/com/bw/ocr/utils/GPTResultParseUtil.java new file mode 100644 index 0000000..6a6ab4f --- /dev/null +++ b/ocr-service/src/main/java/com/bw/ocr/utils/GPTResultParseUtil.java @@ -0,0 +1,53 @@ +package com.bw.ocr.utils; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.JSONException; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author:jinming + * @className:GPTResultParseUtil + * @version:1.0 + * @description: + * @Date:2024/6/28 10:11 + */ +public class GPTResultParseUtil { + public static Map parseGPTResult(Map output, String gptContent) { + Map jsonResult = new HashMap<>(); + try { + // 替换```json, ``` 和 \n + String jsonContent = gptContent.replace("```json", "").replace("```", "").replace("\n", ""); + JSONObject jsonGPT = JSON.parseObject(jsonContent); + + for (String key : output.keySet()) { + if (jsonGPT.containsKey(key)) { + jsonResult.put(key, jsonGPT.get(key)); + } + } + return jsonResult; + } catch (JSONException e) { + try { + // 直接解析失败,使用正则表达式匹配外层的 {} + Pattern pattern = Pattern.compile("\\{.*\\}", Pattern.DOTALL); + Matcher matcher = pattern.matcher(gptContent.replace("\n", "")); + if (matcher.find()) { + JSONObject jsonGPT = JSON.parseObject(matcher.group()); + for (String key : output.keySet()) { + if (jsonGPT.containsKey(key)) { + jsonResult.put(key, jsonGPT.get(key)); + } + } + return jsonResult; + } else { + return null; + } + } catch (Exception ex) { + ex.printStackTrace(); + return null; + } + } + } +} \ No newline at end of file diff --git a/ocr-service/src/main/java/com/bw/ocr/utils/OtherUtils.java b/ocr-service/src/main/java/com/bw/ocr/utils/OtherUtils.java new file mode 100644 index 0000000..f275fb2 --- /dev/null +++ b/ocr-service/src/main/java/com/bw/ocr/utils/OtherUtils.java @@ -0,0 +1,33 @@ +package com.bw.ocr.utils; + +import java.security.MessageDigest; + +/** + * 其他工具类 + * @author jian.mao + * @date 2023年9月19日 + * @description + */ +public class OtherUtils { + + + + public static String getMd5(String string) { + try { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + byte[] bs = md5.digest(string.getBytes("UTF-8")); + StringBuilder sb = new StringBuilder(40); + for (byte x : bs) { + if ((x & 0xff) >> 4 == 0) { + sb.append("0").append(Integer.toHexString(x & 0xff)); + } else { + sb.append(Integer.toHexString(x & 0xff)); + } + } + return sb.toString(); + } catch (Exception e) { + + return "nceaform" + System.currentTimeMillis(); + } + } +} diff --git a/ocr-service/src/main/java/com/bw/ocr/utils/QueueUtil.java b/ocr-service/src/main/java/com/bw/ocr/utils/QueueUtil.java new file mode 100644 index 0000000..126a2d9 --- /dev/null +++ b/ocr-service/src/main/java/com/bw/ocr/utils/QueueUtil.java @@ -0,0 +1,18 @@ +package com.bw.ocr.utils; + +import java.util.Map; +import java.util.concurrent.LinkedBlockingDeque; + +/** + * @author:jinming + * @className:QueueUtil + * @version:1.0 + * @description: + * @Date:2023/7/13 15:00 + */ +public class QueueUtil { + + public static LinkedBlockingDeque> taskQueue = new LinkedBlockingDeque>(); + + public static LinkedBlockingDeque sendQueue = new LinkedBlockingDeque(); +} \ No newline at end of file diff --git a/ocr-service/src/main/java/com/bw/ocr/utils/SpringBootKafka.java b/ocr-service/src/main/java/com/bw/ocr/utils/SpringBootKafka.java new file mode 100644 index 0000000..be43bf1 --- /dev/null +++ b/ocr-service/src/main/java/com/bw/ocr/utils/SpringBootKafka.java @@ -0,0 +1,45 @@ +package com.bw.ocr.utils; + +import com.alibaba.fastjson.JSONObject; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.support.SendResult; +import org.springframework.stereotype.Component; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.ListenableFutureCallback; + +/** + * @PROJECT_NAME: companybusinesscrawl + * @DESCRIPTION:SpringBootKafka 工具类 + * @DATE: 2023/4/6 11:09 + */ +@Slf4j +@Component +public class SpringBootKafka { + @Autowired + private KafkaTemplate kafkaTemplate; + /** + * 自定义topicKafkaTemplate + */ + /** + * public static final String TOPIC = "companyBussTest"; + **/ + public void send(String topic, String message) { + //发送消息 + ListenableFuture> future = kafkaTemplate.send(topic, message); + future.addCallback(new ListenableFutureCallback>() { + @Override + public void onFailure(Throwable throwable) { + //发送失败的处理 + log.info(topic + " - 生产者 发送消息失败:" + throwable.getMessage()); + } + + @Override + public void onSuccess(SendResult stringObjectSendResult) { + //成功的处理 + log.info(topic + " - 生产者 发送消息成功" ); + } + }); + } +} diff --git a/ocr-service/src/main/java/com/bw/ocr/utils/ThrowMessageUtil.java b/ocr-service/src/main/java/com/bw/ocr/utils/ThrowMessageUtil.java new file mode 100644 index 0000000..29ed943 --- /dev/null +++ b/ocr-service/src/main/java/com/bw/ocr/utils/ThrowMessageUtil.java @@ -0,0 +1,23 @@ +package com.bw.ocr.utils; + +import java.io.PrintWriter; +import java.io.StringWriter; + +/** + * @author jian.mao + * @date 2023年3月22日 + * @description + */ +public class ThrowMessageUtil { + + /** + * 获取异常信息 + * @param t + * @return + */ + public static String getErrmessage(Throwable t){ + StringWriter stringWriter=new StringWriter(); + t.printStackTrace(new PrintWriter(stringWriter,true)); + return stringWriter.getBuffer().toString(); + } +} diff --git a/ocr-service/src/main/resources/bootstrap.yml b/ocr-service/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..05acdc6 --- /dev/null +++ b/ocr-service/src/main/resources/bootstrap.yml @@ -0,0 +1,52 @@ +# ==================== 必须文件:bootstrap.yml ==================== +# 这个文件用于配置Nacos客户端,优先级最高 +spring: + application: + name: ocr-service # 服务名,对应Nacos中的Data ID + + cloud: + nacos: + # ======== 配置中心 ======== + config: + server-addr: 127.0.0.1:8848 # Nacos地址 + username: nacos # 用户名 + password: nacos # 密码 + group: public_dev # 分组 + namespace: opai # 命名空间(默认public) + file-extension: yaml # 配置文件格式 + timeout: 5000 # 超时时间(ms) + + # 核心配置:开启动态刷新 + refresh-enabled: true # 必须为true! + + # 主配置文件(从Nacos加载) + data-id: ${spring.application.name}.${spring.cloud.nacos.config.file-extension} + + # 共享配置文件(可选) + shared-configs[0]: + data-id: application.yaml # 公共配置 + group: public_dev # 公共分组 + namespace: opai + refresh: true # 公共配置也要刷新 + + # 扩展配置(可选) + # extension-configs[0]: + # data-id: datasource.yaml + # group: dev + # refresh: true + + # ======== 服务发现 ======== + discovery: + server-addr: ${spring.cloud.nacos.config.server-addr} + username: ${spring.cloud.nacos.config.username} + password: ${spring.cloud.nacos.config.password} + group: ${spring.cloud.nacos.config.group} + namespace: ${spring.cloud.nacos.config.namespace} + +logging: + level: + root: info + com.alibaba.nacos.client.config.impl: WARN + file: + path: ../logs + \ No newline at end of file diff --git a/ocr-service/src/main/resources/logback-spring.xml b/ocr-service/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..3cde3d0 --- /dev/null +++ b/ocr-service/src/main/resources/logback-spring.xml @@ -0,0 +1,36 @@ + + + + + + + + + true + + ${logging.level} + + + ${log-path}/ocr-service.log + + + ${log-path}/ocr-service.log.%d{yyyy-MM-dd} + 7 + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %line %-5level %logger{50} - %msg%n + UTF-8 + + + + + + + + diff --git a/opai-api/.classpath b/opai-api/.classpath new file mode 100644 index 0000000..f7e4a1d --- /dev/null +++ b/opai-api/.classpath @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opai-api/.gitignore b/opai-api/.gitignore new file mode 100644 index 0000000..40cbd51 --- /dev/null +++ b/opai-api/.gitignore @@ -0,0 +1,3 @@ +/target/ +/logs/ +/.settings/ \ No newline at end of file diff --git a/opai-api/.project b/opai-api/.project new file mode 100644 index 0000000..4d8679e --- /dev/null +++ b/opai-api/.project @@ -0,0 +1,23 @@ + + + opai-api + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/opai-api/pom.xml b/opai-api/pom.xml new file mode 100644 index 0000000..8b3a524 --- /dev/null +++ b/opai-api/pom.xml @@ -0,0 +1,173 @@ + + + 4.0.0 + + com.bw + opai-service-center + 0.0.1-SNAPSHOT + ../pom.xml + + com.bw + opai-api + 0.0.1-SNAPSHOT + opai-api + http://maven.apache.org + + UTF-8 + + + + org.springframework.boot + spring-boot-starter-web + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.springframework.boot + spring-boot-starter-jdbc + + + mysql + mysql-connector-java + runtime + + + + + com.baomidou + mybatis-plus-boot-starter + 3.4.3.4 + + + org.projectlombok + lombok + + + com.alibaba + fastjson + 2.0.17 + + + org.apache.httpcomponents + httpclient + 4.5.3 + + + org.apache.httpcomponents + httpmime + 4.5.13 + + + commons-lang + commons-lang + 2.6 + + + com.baomidou + mybatis-plus-boot-starter + 3.4.3.4 + + + + + + + maven-clean-plugin + 3.1.0 + + + maven-resources-plugin + 3.0.2 + + + maven-compiler-plugin + 3.8.0 + + + maven-surefire-plugin + 2.22.1 + + + maven-jar-plugin + 3.0.2 + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + maven-site-plugin + 3.7.1 + + + maven-project-info-reports-plugin + 3.0.0 + + + + org.springframework.boot + spring-boot-maven-plugin + + com.bw.opai.Application + ZIP + + + ${project.groupId} + ${project.artifactId} + + + + + + + repackage + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.1.1 + + + copy + package + + copy-dependencies + + + jar + jar + runtime + ${project.build.directory}/libs + + + + + + + + diff --git a/opai-api/src/main/java/com/bw/opai/Application.java b/opai-api/src/main/java/com/bw/opai/Application.java new file mode 100644 index 0000000..5d9770b --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/Application.java @@ -0,0 +1,19 @@ +package com.bw.opai; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + + +/** + * 系统接口启动类 + * @author jian.mao + * @date 2025年12月30日 + * @description + */ +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/opai-api/src/main/java/com/bw/opai/app/controller/AppController.java b/opai-api/src/main/java/com/bw/opai/app/controller/AppController.java new file mode 100644 index 0000000..40d3cbe --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/app/controller/AppController.java @@ -0,0 +1,83 @@ +package com.bw.opai.app.controller; + +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.bw.opai.app.service.AppService; +import com.bw.opai.common.Res; + +import lombok.extern.slf4j.Slf4j; + +/** + * 应用控制层 + * @author jian.mao + * @date 2025年12月31日 + * @description + */ +@RestController +@Slf4j +@RefreshScope +@RequestMapping("/apps") + +public class AppController { + + @Autowired + private AppService appService; + /** + * 获取应用列表 + * @param page 页码,默认第1页 + * @param limit 每页条数,默认10条 + * @return 应用列表 + */ + @GetMapping("/list") + public Res getApps( + @RequestParam(value = "page", defaultValue = "1", required = false) Integer page, + @RequestParam(value = "size", defaultValue = "10", required = false) Integer size) { + + return appService.getApps(page, size); + + } + + /** + * 根据应用 ID 查询详情 + * @param id 应用 ID + * @return 应用详情 + */ + @GetMapping("/detail/{id}") + public Res getAppDetail(@PathVariable("id") Integer id) { + return appService.getAppDetail(id); + } + + /** + * 启动应用 + * @param param JSON 请求参数 + * @return 启动结果 + */ + @PostMapping("/start") + public Res startApp(@RequestBody Map param) { + + return appService.startApp(param); + } + + + /** + * 查询当前登录用户的任务列表(ES) + */ + @GetMapping("/tasks") + public Res getMyTasks( + @RequestParam(value = "page", defaultValue = "1", required = false) Integer page, + @RequestParam(value = "size", defaultValue = "10", required = false) Integer size) { + + return appService.getMyTasks(page, size); + } + +} diff --git a/opai-api/src/main/java/com/bw/opai/app/dto/App.java b/opai-api/src/main/java/com/bw/opai/app/dto/App.java new file mode 100644 index 0000000..7fee3ca --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/app/dto/App.java @@ -0,0 +1,60 @@ +package com.bw.opai.app.dto; +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.time.LocalDateTime; + +/** + * 应用表 + * @author jian.mao + * @date 2026年1月4日 + * @description + */ +@Data +@TableName("app") +public class App { + + /** + * 应用id + */ + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + private Integer createUserId; + private String createUser; + private Integer updateUserId; + private String updateUser; + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; + + /** + * 删除标识 + */ + @TableLogic + private Integer del; + + /** + * 应用logo + */ + private String logo; + + /** + * 功能 + */ + private String feature; + /** + * 名称 + */ + private String name; + /** + * 说明 + */ + private String description; + /** + * 应用接口地址 + */ + private String api; +} diff --git a/opai-api/src/main/java/com/bw/opai/app/entity/AppResultDoc.java b/opai-api/src/main/java/com/bw/opai/app/entity/AppResultDoc.java new file mode 100644 index 0000000..3b4b029 --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/app/entity/AppResultDoc.java @@ -0,0 +1,49 @@ +package com.bw.opai.app.entity; + + +import java.io.Serializable; +import java.util.Map; + +import lombok.Data; + +/** + * ES 索引:opai_app_result + * 应用执行结果文档 + * + * @author jian.mao + */ +@Data +public class AppResultDoc implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 应用ID */ + private Integer appId; + + /** 应用名称 */ + private String appName; + + /** 用户ID */ + private String userId; + + /** 任务参数(可索引) */ + private Map task; + + /** 创建时间(毫秒时间戳) */ + private Long createTime; + + /** 完成时间(毫秒时间戳) */ + private Long finishTime; + + /** 执行状态 */ + private Integer status; + + /** 执行结果(可索引) */ + private Map result; + + /** 逻辑删除标识:0-未删除 1-已删除 */ + private Integer del; + + /** 最后修改时间(毫秒时间戳) */ + private Long lastEdit; +} diff --git a/opai-api/src/main/java/com/bw/opai/app/mapper/AppMapper.java b/opai-api/src/main/java/com/bw/opai/app/mapper/AppMapper.java new file mode 100644 index 0000000..f374c87 --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/app/mapper/AppMapper.java @@ -0,0 +1,18 @@ +package com.bw.opai.app.mapper; + +import org.apache.ibatis.annotations.Mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.bw.opai.app.dto.App; + +/** + * 应用mapper + * @author jian.mao + * @date 2026年1月4日 + * @description + */ +@Mapper +public interface AppMapper extends BaseMapper { + + +} diff --git a/opai-api/src/main/java/com/bw/opai/app/service/AppService.java b/opai-api/src/main/java/com/bw/opai/app/service/AppService.java new file mode 100644 index 0000000..fa65630 --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/app/service/AppService.java @@ -0,0 +1,44 @@ +package com.bw.opai.app.service; + +import java.util.Map; + +import com.bw.opai.common.Res; + +/** + * 应用服务层接口 + * @author jian.mao + * @date 2026年1月4日 + * @description + */ +public interface AppService { + + /** + * 应用列表 + * @param page 页码数 + * @param limit 条数 + * @return + */ + public Res getApps(Integer page,Integer size); + + /** + * 根据id查询应用 + * @param id + * @return + */ + public Res getAppDetail(Integer id); + + /** + * 应用启动 + * @param param + * @return + */ + public Res startApp(Map param); + + /** + * 查询当前账号任务 + * @param page + * @param size + * @return + */ + public Res getMyTasks(Integer page, Integer size); +} diff --git a/opai-api/src/main/java/com/bw/opai/app/service/impl/AppServiceImpl.java b/opai-api/src/main/java/com/bw/opai/app/service/impl/AppServiceImpl.java new file mode 100644 index 0000000..f2fc42b --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/app/service/impl/AppServiceImpl.java @@ -0,0 +1,358 @@ +package com.bw.opai.app.service.impl; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.http.HttpResponse; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.stereotype.Service; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.bw.opai.app.dto.App; +import com.bw.opai.app.entity.AppResultDoc; +import com.bw.opai.app.mapper.AppMapper; +import com.bw.opai.app.service.AppService; +import com.bw.opai.common.Res; +import com.bw.opai.utils.Constants; +import com.bw.opai.utils.DownLoadUtil; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +@RefreshScope +@RequiredArgsConstructor +public class AppServiceImpl implements AppService { + + private final AppMapper appMapper; + + + @Value("${elasticsearch.url}") + private String esUrl; + + @Value("${elasticsearch.username}") + private String esUser; + + @Value("${elasticsearch.password}") + private String esPassword; + + @Value("${elasticsearch.index-name}") + private String indexName; + + @Override + public Res getApps(Integer page, Integer size) { + // 参数校验 + if (page == null || page < 1) { + page = 1; + } + if (size == null || size < 1 || size > 100) { + // 限制最大查询条数,防止过度查询 + size = 10; + } + try { + Page pageParam = new Page<>(page, size); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("del", 0); + Page result = appMapper.selectPage(pageParam, queryWrapper); + // 调用公共分页封装方法 + return Res.page(result); + + } catch (Exception e) { + log.error("查询应用列表失败", e); + return Res.fail("查询应用列表失败"); + } + + } + @Override + public Res getAppDetail(Integer id) { + try { + if (id == null || id <= 0) { + return Res.fail("应用ID不合法"); + } + // 根据 ID 查询 + App app = appMapper.selectById(id); + // del = 0 表示未删除 + if (app == null || app.getDel() != 0) { + return Res.fail("未找到对应应用"); + } + return Res.ok(app); + } catch (Exception e) { + log.error("查询应用详情失败, id={}", id, e); + return Res.fail("查询应用详情失败"); + } + } + @Override + public Res startApp(Map param) { + // TODO Auto-generated method stub + if (param == null || param.isEmpty()) { + return Res.fail("请求参数不能为空"); + } + try { + // 这里写你的启动逻辑 + // 比如:获取应用ID -> 更新状态 -> 返回结果 + Integer appId = param.get("id") == null ? null : Integer.valueOf(param.get("id").toString()); + if (appId == null) { + return Res.fail("应用ID不能为空"); + } + //任务id校验 + String taskId = param.get("taskId") == null||param.get("taskId").toString().equals("")? null : param.get("taskId").toString(); + if (taskId == null) { + return Res.fail("任务ID不能为空"); + } + // 伪示例:根据 ID 查询应用并启动 + App app = appMapper.selectById(appId); + if (app == null || app.getDel() != 0) { + return Res.fail("未找到对应应用"); + } + //应用调用 + String appUrl = app.getApi(); + String downloadRes = DownLoadUtil.doPost(appUrl, JSONObject.toJSONString(param)); + if (downloadRes.contains(Constants.DOWNLOAD_ERROR_SUFFIX)) { + //下载失败直接返回 + log.error("启动应用请求异常, param={},download error:{}", param, downloadRes); + return Res.fail("启动应用请求异常"); + } + //任务记录写入es + long now = System.currentTimeMillis(); + + AppResultDoc esEntity = new AppResultDoc(); + esEntity.setAppId(app.getId()); + esEntity.setAppName(app.getName()); + //用户token -- 先给默认值 + esEntity.setUserId("1"); + + esEntity.setTask(param); + esEntity.setResult(null); + + esEntity.setCreateTime(now); + esEntity.setFinishTime(null); + // 运行中 0 是运行中,1是完成,2是失败 + esEntity.setStatus(0); + esEntity.setDel(0); + esEntity.setLastEdit(now); + + // ---------- 写入 ES ---------- + boolean isSuccess = save(esEntity, taskId); + if(!isSuccess) { + //记录写入失败,直接返回错误信息 + log.error("启动应用记录写入异常, param={}", param); + return Res.fail("启动应用记录写入异常"); + } + // ---------- 返回 ---------- + return Res.ok(esEntity); + } catch (Exception e) { + log.error("启动应用未知异常, param={}", param, e); + return Res.fail("启动应用未知异常"); + } + } + + /** + * 节点数据持久化 + * @param esEntity + * @return + */ + private boolean save(AppResultDoc esEntity,String docId){ + // 创建凭据提供者 + CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(esUser, esPassword)); + // 创建一个 HttpClient,设置凭据提供者 + CloseableHttpClient httpClient = null; + try { + // 创建一个 HTTP POST 请求用于写入数据到索引 + if(esUser != null && !esUser.trim().equals(Constants.EMPTY)){ + // 创建凭据提供者 + httpClient = HttpClients.custom() + .setDefaultCredentialsProvider(credentialsProvider) + .build(); + }else{ + httpClient = HttpClients.custom().build(); + } + + StringBuffer host = new StringBuffer(); + host.append(esUrl); + host.append("/"); + host.append(indexName); + host.append("/_doc/"); + // Elasticsearch主机、索引名称和文档ID + host.append(docId); + HttpPost httpPost = new HttpPost(host.toString()); + // 设置请求体,包含要写入的文档数据 + StringEntity entity = new StringEntity(JSONObject.toJSONString(esEntity), ContentType.APPLICATION_JSON); + httpPost.setEntity(entity); + // 发送请求并获取响应 + HttpResponse response = httpClient.execute(httpPost); + // 处理响应 + int statusCode = response.getStatusLine().getStatusCode(); + String responseBody = EntityUtils.toString(response.getEntity()); + int code = 201; + int updateCode = 200; + if (statusCode == code) { + log.info("数据成功写入到索引:{},文档ID:{},appid:{},userId:{}",indexName,docId,esEntity.getAppId(),esEntity.getUserId()); + return true; + }else if(statusCode == updateCode){ + log.info("数据成功更新到索引:{},文档ID:{},appid:{},userId:{}",indexName,docId,esEntity.getAppId(),esEntity.getUserId()); + return true; + } else { + log.error("数据写入失败:{},文档ID:{},appid:{},es响应内容:{}",indexName,docId,esEntity.getAppId(),responseBody); + return false; + } + } catch (Exception e) { + log.error("数据写入异常:",e); + return false; + } finally { + try { + if(httpClient != null) { + httpClient.close(); + } + } catch (Exception ignored){} + } + } + + @Override + public Res getMyTasks(Integer page, Integer size) { + + if (page == null || page < 1) { + page = 1; + } + if (size == null || size < 1 || size > 100) { + size = 10; + } + + // 从登录上下文获取 userId(你项目里已有) 先写死 + String userId = "1"; + if (userId == null || userId.trim().equals("")) { + return Res.fail("未获取到用户信息"); + } + + CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials( + AuthScope.ANY, + new UsernamePasswordCredentials(esUser, esPassword) + ); + + CloseableHttpClient httpClient = null; + try { + if (esUser != null && !esUser.trim().equals("")) { + httpClient = HttpClients.custom() + .setDefaultCredentialsProvider(credentialsProvider) + .build(); + } else { + httpClient = HttpClients.custom().build(); + } + + // ================== 构建查询 DSL ================== + int from = (page - 1) * size; + + Map query = new HashMap(); + query.put("from", from); + query.put("size", size); + + // must 条件 + List> mustList = new ArrayList>(); + + Map termUser = new HashMap(); + termUser.put("userId", userId); + Map termUserWrap = new HashMap(); + termUserWrap.put("term", termUser); + mustList.add(termUserWrap); + + Map termDel = new HashMap(); + termDel.put("del", 0); + Map termDelWrap = new HashMap(); + termDelWrap.put("term", termDel); + mustList.add(termDelWrap); + + Map bool = new HashMap(); + bool.put("must", mustList); + + Map queryBody = new HashMap(); + queryBody.put("bool", bool); + + query.put("query", queryBody); + + // sort + List> sortList = new ArrayList>(); + Map order = new HashMap(); + order.put("order", "desc"); + Map sortField = new HashMap(); + sortField.put("createTime", order); + sortList.add(sortField); + query.put("sort", sortList); + + // ================== 发起 HTTP 请求 ================== + StringBuffer host = new StringBuffer(); + host.append(esUrl) + .append("/") + .append(indexName) + .append("/_search"); + + HttpPost httpPost = new HttpPost(host.toString()); + httpPost.setHeader("Content-Type", "application/json"); + + StringEntity entity = new StringEntity( + JSONObject.toJSONString(query), + ContentType.APPLICATION_JSON + ); + httpPost.setEntity(entity); + + HttpResponse response = httpClient.execute(httpPost); + int statusCode = response.getStatusLine().getStatusCode(); + String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8"); + + if (statusCode != 200) { + log.error("ES 查询失败 status={}, body={}", statusCode, responseBody); + return Res.fail("ES 查询失败"); + } + + // ================== 解析返回 ================== + JSONObject json = JSONObject.parseObject(responseBody); + JSONObject hits = json.getJSONObject("hits"); + + Long total = hits.getJSONObject("total").getLong("value"); + + List list = new ArrayList(); + JSONArray hitList = hits.getJSONArray("hits"); + for (int i = 0; i < hitList.size(); i++) { + JSONObject source = hitList.getJSONObject(i).getJSONObject("_source"); + AppResultDoc item = source.toJavaObject(AppResultDoc.class); + list.add(item); + } + + Map result = new HashMap(); + result.put("page", page); + result.put("size", size); + result.put("total", total); + result.put("list", list); + + return Res.ok(result); + + } catch (Exception e) { + log.error("查询用户任务失败 userId={}", userId, e); + return Res.fail("查询任务失败"); + } finally { + if (httpClient != null) { + try { + httpClient.close(); + } catch (Exception ignored) {} + } + } + } + +} diff --git a/opai-api/src/main/java/com/bw/opai/common/Res.java b/opai-api/src/main/java/com/bw/opai/common/Res.java new file mode 100644 index 0000000..675a20b --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/common/Res.java @@ -0,0 +1,81 @@ +package com.bw.opai.common; + +import java.util.HashMap; +import java.util.Map; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; + +/** + * 通用返回对象 + * + * @author jian.mao + * @date 2025年9月17日 + * @description + * @param + */ + +public class Res { + private int resCode; + private String resMsg; + private T data; + + public Res() { + } + + public Res(int resCode, String resMsg, T data) { + this.resCode = resCode; + this.resMsg = resMsg; + this.data = data; + } + + /** + * 分页结果封装 + * @param page MyBatis-Plus 分页对象 + * @param 实体类型 + * @return Res 包含 total/page/size/records + */ + public static Res> page(Page page) { + Map pageData = new HashMap<>(); + pageData.put("total", page.getTotal()); + pageData.put("page", page.getCurrent()); + pageData.put("size", page.getSize()); + pageData.put("records", page.getRecords()); + return Res.ok(pageData); + } + + public static Res ok(T data) { + return new Res(ResponseCode.SUCCESS.code(), ResponseCode.SUCCESS.message(), data); + } + + public static Res fail(String msg) { + return new Res(ResponseCode.FAIL.code(), msg, null); + } + public static Res checkError(T error) { + return new Res(ResponseCode.FAIL.code(), ResponseCode.CHECKERROR.message(), error); + } + + // getter & setter + public int getResCode() { + return resCode; + } + + public void setResCode(int resCode) { + this.resCode = resCode; + } + + public String getResMsg() { + return resMsg; + } + + public void setResMsg(String resMsg) { + this.resMsg = resMsg; + } + + public T getData() { + return data; + } + + public void setData(T data) { + this.data = data; + } +} diff --git a/opai-api/src/main/java/com/bw/opai/common/ResponseCode.java b/opai-api/src/main/java/com/bw/opai/common/ResponseCode.java new file mode 100644 index 0000000..e87eaf7 --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/common/ResponseCode.java @@ -0,0 +1,30 @@ +package com.bw.opai.common; + +/** + * 状态码管理 + * @author jian.mao + * @date 2025年9月17日 + * @description + */ +public enum ResponseCode { + + SUCCESS(0, "success"), + FAIL(-1, "fail"), + CHECKERROR(400, "paramsError"); + + private final int code; + private final String message; + + ResponseCode(int code, String message) { + this.code = code; + this.message = message; + } + + public int code() { + return code; + } + + public String message() { + return message; + } +} diff --git a/opai-api/src/main/java/com/bw/opai/file/controller/FileController.java b/opai-api/src/main/java/com/bw/opai/file/controller/FileController.java new file mode 100644 index 0000000..defefde --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/file/controller/FileController.java @@ -0,0 +1,34 @@ +package com.bw.opai.file.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import com.bw.opai.common.Res; +import com.bw.opai.file.service.FileService; + +import lombok.extern.slf4j.Slf4j; + +/** + * 文件操作控制层 + * @author jian.mao + * @date 2026年1月6日 + * @description + */ +@RestController +@Slf4j +@RefreshScope +@RequestMapping("/file") +public class FileController { + + @Autowired + private FileService fileService; + @PostMapping("/upload") + public Res uploadFile(@RequestParam("file") MultipartFile file) { + return fileService.uploadFile(file); + } +} diff --git a/opai-api/src/main/java/com/bw/opai/file/service/FileService.java b/opai-api/src/main/java/com/bw/opai/file/service/FileService.java new file mode 100644 index 0000000..a9d24c5 --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/file/service/FileService.java @@ -0,0 +1,16 @@ +package com.bw.opai.file.service; + +import org.springframework.web.multipart.MultipartFile; + +import com.bw.opai.common.Res; + +/** + * 文件操作业务接口 + * @author jian.mao + * @date 2026年1月6日 + * @description + */ +public interface FileService { + + public Res uploadFile(MultipartFile file); +} diff --git a/opai-api/src/main/java/com/bw/opai/file/service/impl/FileServiceImpl.java b/opai-api/src/main/java/com/bw/opai/file/service/impl/FileServiceImpl.java new file mode 100644 index 0000000..757a4b2 --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/file/service/impl/FileServiceImpl.java @@ -0,0 +1,107 @@ +package com.bw.opai.file.service.impl; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.http.HttpEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.mime.HttpMultipartMode; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import com.alibaba.fastjson.JSONObject; +import com.bw.opai.common.Res; +import com.bw.opai.file.service.FileService; + +import lombok.extern.slf4j.Slf4j; + +/** + * 文件操作服务实现类 + * @author jian.mao + * @date 2026年1月6日 + * @description + */ +@Service +@Slf4j +@RefreshScope +public class FileServiceImpl implements FileService { + + @Value("${gofast.upload-url}") + private String gofastUploadUrl; + + @Value("${gofast.access-prefix}") + private String gofastAccessPrefix; + + + @Override + public Res uploadFile(MultipartFile file) { + + if (file == null || file.isEmpty()) { + return Res.fail("上传文件不能为空"); + } + + CloseableHttpClient httpClient = HttpClients.createDefault(); + + try { + HttpPost httpPost = new HttpPost(gofastUploadUrl); + + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE); + + // 文件流 + builder.addBinaryBody( + "file", + file.getInputStream(), + ContentType.APPLICATION_OCTET_STREAM, + file.getOriginalFilename() + ); + // 👇 关键:要求 GoFast 返回 JSON + builder.addTextBody("output", "json"); + HttpEntity entity = builder.build(); + httpPost.setEntity(entity); + + CloseableHttpResponse response = httpClient.execute(httpPost); + + int statusCode = response.getStatusLine().getStatusCode(); + String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8"); + + log.info("GoFast upload response: {}", responseBody); + + if (statusCode != 200) { + return Res.fail("文件服务器上传失败"); + } + + // 解析返回 JSON + JSONObject json = JSONObject.parseObject(responseBody); + + // ⚠️ 根据你 GoFast 实际返回字段调整 + String path = json.getString("path"); + String url = gofastAccessPrefix + path; + + Map result = new HashMap(); + result.put("fileName", file.getOriginalFilename()); + result.put("fileSize", file.getSize()); + result.put("url", url); + + return Res.ok(result); + + } catch (Exception e) { + log.error("上传到 GoFast 失败", e); + return Res.fail("文件上传失败"); + } finally { + try { + httpClient.close(); + } catch (Exception ignored) {} + } + } + + +} diff --git a/opai-api/src/main/java/com/bw/opai/utils/Constants.java b/opai-api/src/main/java/com/bw/opai/utils/Constants.java new file mode 100644 index 0000000..6e2afe4 --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/utils/Constants.java @@ -0,0 +1,19 @@ +package com.bw.opai.utils; + +/** + * 常量类 + * @author jian.mao + * @date 2025年11月6日 + * @description + */ +public class Constants { + + + /***空常量****/ + public static final String EMPTY = ""; + + /** + * 请求失败结果前缀 + */ + public static final String DOWNLOAD_ERROR_SUFFIX = "Download failed error is:"; +} diff --git a/opai-api/src/main/java/com/bw/opai/utils/DateUtil.java b/opai-api/src/main/java/com/bw/opai/utils/DateUtil.java new file mode 100644 index 0000000..a9b573d --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/utils/DateUtil.java @@ -0,0 +1,177 @@ +package com.bw.opai.utils; + + +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Date; + +import lombok.extern.slf4j.Slf4j; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; + +/** + * 日期工具类 + * + * @author jian.mao + * @date 2022年11月15日 + * @description + */ +@Slf4j +public class DateUtil { + + /** + * @return + */ + public static String getTimeStrForNow() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHH"); + return sdf.format(new Date()); + } + + + public static String getTimeStrForDay(long time) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd"); + + return sdf.format(new Date(time * 1000)); + } + + public static String getTimeStrForDay() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd"); + + return sdf.format(new Date()); + } + + + public static String getDateTime() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String time = sdf.format(new Date()); + return time; + } + + public static String getDateTime(Long timestap) { + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String time = sdf.format(new Date(timestap)); + return time; + } + + public static String getDate(Long timestap) { + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + String time = sdf.format(new Date(timestap)); + return time; + } + + public static String getDateTimeForMonth() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM"); + String time = sdf.format(new Date()); + return time; + } + + /** + * 休眠 + * + * @param millis 毫秒 + */ + public static void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + /** + * 1. @Description:时间戳转时间 + * 2. @Author: ying.zhao + * 3. @Date: 2023/3/28 + */ + + public static String timestampToDate(String time) { + int thirteen = 13; + int ten = 10; + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); +// if (time.length() == thirteen) { + if (time.length() > ten) { + return sdf.format(new Date(Long.parseLong(time))); + } else { + return sdf.format(new Date(Integer.parseInt(time) * 1000L)); + } + } + + public static String parseCreated(String jsonTime){ + String formattedDateTime = getDateTime(); + try { + // 使用fastjson解析JSON数据 + JSONObject jsonObject = JSON.parseObject(jsonTime); + // 获取日期和时间的值 + JSONObject dateObject = jsonObject.getJSONObject("date"); + int day = dateObject.getIntValue("day"); + int month = dateObject.getIntValue("month"); + int year = dateObject.getIntValue("year"); + + JSONObject timeObject = jsonObject.getJSONObject("time"); + int hour = timeObject.getIntValue("hour"); + int minute = timeObject.getIntValue("minute"); + int second = timeObject.getIntValue("second"); + + // 创建LocalDateTime对象 + LocalDateTime dateTime = LocalDateTime.of(year, month, day, hour, minute, second); + + // 定义日期时间格式化器 + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + // 格式化日期时间 + formattedDateTime = dateTime.format(formatter); + } catch (Exception e) { + log.info("日期转换失败:{}",e); + } + return formattedDateTime; + } + + /** + * 字符串转换日期 + * @param format + * @param date + * @return + */ + public static Date strToDate(String format,String date){ + SimpleDateFormat sdf = new SimpleDateFormat(format); + if (date == null || date.equals("")){ + return new Date(); + }else{ + Date ru = null; + try { + ru = sdf.parse(date); + } catch (ParseException e) { + e.printStackTrace(); + } + return ru; + } + } + /** + * 日期格式话 + * @param format 日期格式 + * @param dater 要转换的日期,默认当前时间 + * @return + */ + public static String FormatDate(String format,Date date){ + String fromatDate = null; + SimpleDateFormat sdf = new SimpleDateFormat(format); + if (date == null){ + fromatDate = sdf.format(new Date()); + }else{ + fromatDate = sdf.format(date); + } + return fromatDate; + } + public static void main(String[] args) { + String time = timestampToDate("955814400000"); + System.out.println(time); + } +} diff --git a/opai-api/src/main/java/com/bw/opai/utils/DownLoadUtil.java b/opai-api/src/main/java/com/bw/opai/utils/DownLoadUtil.java new file mode 100644 index 0000000..1bba823 --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/utils/DownLoadUtil.java @@ -0,0 +1,1005 @@ +package com.bw.opai.utils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.StatusLine; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.AuthCache; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.HttpClient; +import org.apache.http.client.HttpRequestRetryHandler; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.config.SocketConfig; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.LayeredConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.auth.BasicScheme; +import org.apache.http.impl.client.BasicAuthCache; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.client.LaxRedirectStrategy; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + + + + + + + + + + +/** + * 下载工具类 + * @author jian.mao + * @date 2023年9月19日 + * @description + */ +public class DownLoadUtil { + + private static String ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36"; + private final static Logger log = LoggerFactory.getLogger(DownLoadUtil.class); + /** 代理服务器(产品官网 www.16yun.cn) **/ + final static String PROXYHOST = "u270.40.tp.16yun.cn"; + final static Integer PROXYPORT = 6448; + /** 代理验证信息 **/ + final static String PROXYUSER = "16HFBVJC"; + final static String PROXYPASS = "897944"; + + private static PoolingHttpClientConnectionManager cm = null; + private static HttpRequestRetryHandler httpRequestRetryHandler = null; + private static HttpHost proxy = null; + + private static CredentialsProvider credsProvider = null; + private static RequestConfig reqConfig = null; + + static { + ConnectionSocketFactory plainsf = PlainConnectionSocketFactory + .getSocketFactory(); + LayeredConnectionSocketFactory sslsf = SSLConnectionSocketFactory + .getSocketFactory(); + + Registry registry = RegistryBuilder.create().register("http", plainsf) + .register("https", sslsf).build(); + + cm = new PoolingHttpClientConnectionManager(registry); + cm.setMaxTotal(20); + cm.setDefaultMaxPerRoute(5); + + proxy = new HttpHost(PROXYHOST, PROXYPORT, "https"); + + credsProvider = new BasicCredentialsProvider(); + credsProvider.setCredentials(AuthScope.ANY, + new UsernamePasswordCredentials(PROXYUSER, PROXYPASS)); + + reqConfig = RequestConfig.custom().setConnectionRequestTimeout(5000) + .setConnectTimeout(5000).setSocketTimeout(5000) + .setExpectContinueEnabled(false) + .setProxy(new HttpHost(PROXYHOST, PROXYPORT)).build(); + } + + /** + * 模拟客户端get请求 + * + * @param url + * 模拟请求得url + * @param headers + * 头部信息,没有可以不传 + * @return + */ + @SafeVarargs + public static String proxyDoGet(String url, Map... headers) { + // 设置超时时间 + int timeout = 30; + RequestConfig config = RequestConfig.custom() + .setConnectTimeout(timeout * 1000) + .setConnectionRequestTimeout(timeout * 1000) + .setSocketTimeout(timeout * 1000).build(); + SocketConfig socketConfig = SocketConfig.custom() + .setSoKeepAlive(false) + .setSoLinger(1) + .setSoReuseAddress(true) + .setSoTimeout(timeout * 1000) + .setTcpNoDelay(true).build(); + AuthCache authCache = new BasicAuthCache(); + authCache.put(proxy, new BasicScheme()); + HttpClientContext localContext = HttpClientContext.create(); + localContext.setAuthCache(authCache); + HttpClientBuilder httpBuilder = HttpClientBuilder.create(); + CloseableHttpClient httpClient = httpBuilder + .setDefaultSocketConfig(socketConfig) + .setDefaultRequestConfig(config) + .setDefaultCredentialsProvider(credsProvider).build(); + HttpGet httpGet = new HttpGet(url); + httpGet.setConfig(reqConfig); + if (headers != null && headers.length > 0) { + Map tempHeaders = headers[0]; + for (String key : tempHeaders.keySet()) { + httpGet.setHeader(key, tempHeaders.get(key).toString()); + } + } else { + httpGet.setHeader("Accept", + "application/json, text/javascript, */*; q=0.01"); + httpGet.setHeader("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8"); + } + CloseableHttpResponse response = null; + String html = ""; + int notFundCode = 404; + int successCode = 200; + try { + response = httpClient.execute(httpGet, localContext); + // 从响应模型中获取响应实体 + HttpEntity responseEntity = response.getEntity(); + StatusLine statusLine = response.getStatusLine(); + System.out.println("响应状态为:" + response.getStatusLine()); + if (statusLine.getStatusCode() == successCode) { + if (responseEntity != null) { + html = EntityUtils.toString(responseEntity, "utf-8"); + System.out.println("响应内容长度为:" + + responseEntity.getContentLength()); + // 下载结果为空不正常 + if (html.equals(Constants.EMPTY)) { + html = "Download failed error is:reslut is null"; + } + } + } else if (statusLine.getStatusCode() == notFundCode) { + html = "

页面404,正常结束请求即可

"; + } else { + throw new Exception("请求错误,code码为:" + statusLine.getStatusCode()); + } + } catch (Exception e) { + e.printStackTrace(); + html = "Download failed error is:reslut is null"; + }finally{ + try { + response.close(); + httpClient.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + return html; + + } + + + public static String httpsslProxyGet(String url, Map... headers) throws Exception { + //采用绕过验证的方式处理https请求 + SSLContext sslcontext = createIgnoreVerifySSL(); + + // 设置协议http和https对应的处理socket链接工厂的对象 + Registry socketFactoryRegistry = RegistryBuilder.create() + .register("http", PlainConnectionSocketFactory.INSTANCE) + .register("https", new SSLConnectionSocketFactory(sslcontext)) + .build(); + PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry); + connManager.setMaxTotal(50); + connManager.setDefaultMaxPerRoute(10); + HttpClients.custom().setConnectionManager(connManager); + // 设置超时时间 + int timeout = 30; + RequestConfig config = RequestConfig.custom() + .setConnectTimeout(timeout * 1000) + .setConnectionRequestTimeout(timeout * 1000) + .setSocketTimeout(timeout * 1000).build(); + SocketConfig socketConfig = SocketConfig.custom() + .setSoKeepAlive(false) + .setSoLinger(1) + .setSoReuseAddress(true) + .setSoTimeout(timeout * 1000) + .setTcpNoDelay(true).build(); + AuthCache authCache = new BasicAuthCache(); + authCache.put(proxy, new BasicScheme()); + HttpClientContext localContext = HttpClientContext.create(); + localContext.setAuthCache(authCache); + HttpClientBuilder httpBuilder = HttpClientBuilder.create(); + CloseableHttpClient httpClient = httpBuilder + .setConnectionManager(connManager) + .setDefaultSocketConfig(socketConfig) + .setDefaultRequestConfig(config) + .setDefaultCredentialsProvider(credsProvider).build(); + HttpGet httpGet = new HttpGet(url); + httpGet.setConfig(reqConfig); + if (headers != null && headers.length > 0) { + Map tempHeaders = headers[0]; + for (String key : tempHeaders.keySet()) { + httpGet.setHeader(key, tempHeaders.get(key).toString()); + } + } else { + httpGet.setHeader("Accept", + "application/json, text/javascript, */*; q=0.01"); + httpGet.setHeader("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8"); + } + CloseableHttpResponse response = null; + String html = ""; + int notFundCode = 404; + int successCode = 200; + try { + response = httpClient.execute(httpGet, localContext); + // 从响应模型中获取响应实体 + HttpEntity responseEntity = response.getEntity(); + StatusLine statusLine = response.getStatusLine(); + System.out.println("响应状态为:" + response.getStatusLine()); + if (statusLine.getStatusCode() == successCode) { + if (responseEntity != null) { + html = EntityUtils.toString(responseEntity, "utf-8"); + System.out.println("响应内容长度为:" + + responseEntity.getContentLength()); + // 下载结果为空不正常 + if (html.equals(Constants.EMPTY)) { + html = "Download failed error is:reslut is null"; + } + } + } else if (statusLine.getStatusCode() == notFundCode) { + html = "

页面404,正常结束请求即可

"; + } else { + throw new Exception("请求错误,code码为:" + statusLine.getStatusCode()); + } + } catch (Exception e) { + e.printStackTrace(); + html = "Download failed error is:reslut is null"; + }finally{ + try { + response.close(); + httpClient.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + return html; + + } + + + /** + * json参数方式POST提交 + * @param url + * @param params + * @return + */ + public static String doPost(String url, String params, Map... headers){ + String strResult = ""; + //设置超时时间 + int timeout = 600; + RequestConfig config = RequestConfig.custom(). + setConnectTimeout(timeout * 1000). + setConnectionRequestTimeout(timeout * 1000). + setSocketTimeout(timeout * 1000).build(); + SocketConfig socketConfig = SocketConfig.custom() + .setSoKeepAlive(false) + .setSoLinger(1) + .setSoReuseAddress(true) + .setSoTimeout(timeout * 1000) + .setTcpNoDelay(true).build(); +// AuthCache authCache = new BasicAuthCache(); +// authCache.put(proxy, new BasicScheme()); +// HttpClientContext localContext = HttpClientContext.create(); +// localContext.setAuthCache(authCache); + // 1. 获取默认的client实例 + HttpClientBuilder httpBuilder = HttpClientBuilder.create(); + httpBuilder.setUserAgent(ua); + HttpClient client = httpBuilder.setDefaultSocketConfig(socketConfig).setDefaultRequestConfig(config).build(); +// HttpClient client = httpBuilder.setDefaultSocketConfig(socketConfig).setDefaultRequestConfig(config).setConnectionManager(cm) +// .setDefaultCredentialsProvider(credsProvider).build(); + // 2. 创建httppost实例 + HttpPost httpPost = new HttpPost(url); +// httpPost.setConfig(reqConfig); + if (headers != null && headers.length > 0) { + Map tempHeaders = headers[0]; + for (String key : tempHeaders.keySet()) { + httpPost.setHeader(key, tempHeaders.get(key).toString()); + } + } else { + httpPost.addHeader("Content-Type", "application/json;charset=utf-8"); + } + HttpResponse resp = null; + try { + httpPost.setEntity(new StringEntity(params,"utf-8")); + resp = client.execute(httpPost); +// resp = client.execute(httpPost,localContext); + StatusLine statusLine = resp.getStatusLine(); +// System.out.println("响应状态为:" + resp.getStatusLine()); + int successCode = 200; + if(statusLine.getStatusCode() == successCode){ + // 7. 获取响应entity + HttpEntity respEntity = resp.getEntity(); + strResult = EntityUtils.toString(respEntity, "UTF-8"); + if(strResult.equals(Constants.EMPTY)){ + strResult = "Download failed error is:reslut is null"; + } + }else{ + throw new Exception("请求错误,code码为:"+statusLine.getStatusCode()); + } + } catch (Exception e) { + e.printStackTrace(); + strResult = "Download failed error is:"+ThrowMessageUtil.getErrmessage(e); + } + return strResult; + } + public static String httpPost(String url,String params) { + String html=""; + html = doPost(url,params); + int i = 1; + while(true){ + if(html.contains("Download failed error is:")){ + log.error("DownLoadUtil------------->download is failure,url is:"+url); + DateUtil.sleep(5000); + i++; + }else{ + break; + } + if(i > 5){ + break; + } + html = doPost(url,params); + } + return html; + } + /** + * 绕过验证 + * + * @return + * @throws NoSuchAlgorithmException + * @throws KeyManagementException + */ + public static SSLContext createIgnoreVerifySSL() throws NoSuchAlgorithmException, KeyManagementException { + SSLContext sc = SSLContext.getInstance("SSLv3"); + + // 实现一个X509TrustManager接口,用于绕过验证,不用修改里面的方法 + X509TrustManager trustManager = new X509TrustManager() { + @Override + public void checkClientTrusted( + java.security.cert.X509Certificate[] paramArrayOfX509Certificate, + String paramString) throws CertificateException { + } + + @Override + public void checkServerTrusted( + java.security.cert.X509Certificate[] paramArrayOfX509Certificate, + String paramString) throws CertificateException { + } + + @Override + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return null; + } + }; + + sc.init(null, new TrustManager[] { trustManager }, null); + return sc; + } + /** + * 模拟请求 + * + * @param url 资源地址 + * @param map 参数列表 + * @param encoding 编码 + * @return + * @throws NoSuchAlgorithmException + * @throws KeyManagementException + * @throws IOException + * @throws ClientProtocolException + */ + public static String httpsslGet(String url,Map ... headers) { + String html=""; + CloseableHttpClient client = null; + HttpEntity responseEntity = null; + CloseableHttpResponse response = null; + try { + log.debug("DownLoadUtil------------->设置下载相关信息, start...."); + //采用绕过验证的方式处理https请求 + SSLContext sslcontext = createIgnoreVerifySSL(); + + // 设置协议http和https对应的处理socket链接工厂的对象 + Registry socketFactoryRegistry = RegistryBuilder.create() + .register("http", PlainConnectionSocketFactory.INSTANCE) + .register("https", new SSLConnectionSocketFactory(sslcontext)) + .build(); + PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry); + connManager.setMaxTotal(50); + connManager.setDefaultMaxPerRoute(10); + HttpClients.custom().setConnectionManager(connManager); + //设置超时时间 + int timeout = 30; + RequestConfig config = RequestConfig.custom(). + setConnectTimeout(timeout * 1000). + setConnectionRequestTimeout(timeout * 1000). + setSocketTimeout(timeout * 1000).build(); + SocketConfig socketConfig = SocketConfig.custom() + .setSoKeepAlive(false) + .setSoLinger(1) + .setSoReuseAddress(true) + .setSoTimeout(10000) + .setTcpNoDelay(true).build(); + // 设置重定向策略 + LaxRedirectStrategy redirectStrategy = new LaxRedirectStrategy(); + //创建自定义的httpclient对象 + client = HttpClients.custom().setConnectionManager(connManager).setDefaultRequestConfig(config).setRedirectStrategy(redirectStrategy).setDefaultSocketConfig(socketConfig).setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36").build(); +// CloseableHttpClient client = HttpClients.createDefault(); + + HttpGet httpGet = new HttpGet(url); + if(headers != null && headers.length > 0){ + Map tempHeaders = headers[0]; + for (String key : tempHeaders.keySet()) { + httpGet.setHeader(key,tempHeaders.get(key).toString()); + } + }else{ + httpGet.setHeader("Accept", "application/json, text/javascript, */*; q=0.01"); + httpGet.setHeader("Accept-Language","zh-CN,zh;q=0.9,en;q=0.8"); + httpGet.setHeader("User-Agent","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36"); + } + log.debug("DownLoadUtil------------->设置下载相关信息, end...."); + try { + int notFundCode = 404; + int successCode = 200; + log.debug("DownLoadUtil------------->下载执行,start...."); + httpGet.setConfig(config); + response = client.execute(httpGet); + log.debug("DownLoadUtil------------->下载执行,end...."); + // 从响应模型中获取响应实体 + StatusLine statusLine = response.getStatusLine(); + log.debug("DownLoadUtil------------->响应状态为:" + response.getStatusLine()+",下载请求没问题url:"+url+",read is start ...."); + System.out.println("响应状态为:" + response.getStatusLine()); + responseEntity = response.getEntity(); + log.debug("DownLoadUtil------------->响应状态为:" + response.getStatusLine()+",下载请求没问题url:"+url+",read is end ...."); + if(statusLine.getStatusCode() == successCode){ + if (responseEntity != null) { + html=EntityUtils.toString(responseEntity,"utf-8"); + System.out.println("响应内容长度为:" + responseEntity.getContentLength()); + } + }else if(statusLine.getStatusCode() == notFundCode){ + html = "

页面404,正常结束请求即可

"; + }else{ + throw new Exception("请求错误,code码为:"+statusLine.getStatusCode()); + } + } catch (Exception e) { + e.printStackTrace(); + html = "Download failed error is:"+ThrowMessageUtil.getErrmessage(e); + + } + } catch (Exception e) { + e.printStackTrace(); + html = "Download failed error is:"+ThrowMessageUtil.getErrmessage(e); + }finally{ + try { + responseEntity.getContent().close(); + response.close(); + client.close(); + } catch (Exception e) { + e.printStackTrace(); + } + + } + + + return html; + } + + public static String httpSSLGet(String url,Map ... headers) { + String html=""; + html = httpsslGet(url,headers); + int i = 1; + while(true){ + if(html.contains("Download failed error is:")){ + log.error("DownLoadUtil------------->download is failure,url is:"+url); + DateUtil.sleep(30000); + i++; + }else{ + break; + } + if(i > 5){ + break; + } + html = httpsslGet(url,headers); + } + return html; + } + public static String doPostFrom(String url,Map param,Map ... headers){ + //设置超时时间 + int timeout = 15; + RequestConfig config = RequestConfig.custom(). + setConnectTimeout(timeout * 1000). + setConnectionRequestTimeout(timeout * 1000). + setSocketTimeout(timeout * 1000).build(); + SocketConfig socketConfig = SocketConfig.custom() + .setSoKeepAlive(false) + .setSoLinger(1) + .setSoReuseAddress(true) + .setSoTimeout(10000) + .setTcpNoDelay(true).build(); +// AuthCache authCache = new BasicAuthCache(); +// authCache.put(proxy, new BasicScheme()); +// HttpClientContext localContext = HttpClientContext.create(); +// localContext.setAuthCache(authCache); + HttpClientBuilder httpBuilder = HttpClientBuilder.create(); + httpBuilder.setUserAgent(ua); +// HttpClient httpClient = httpBuilder.setDefaultSocketConfig(socketConfig).setDefaultRequestConfig(config).setConnectionManager(cm) +// .setDefaultCredentialsProvider(credsProvider).build(); + HttpClient httpClient = httpBuilder.setDefaultSocketConfig(socketConfig).setDefaultRequestConfig(config).build(); + HttpPost httpPost = new HttpPost(url); +// httpPost.setConfig(reqConfig); + if(headers != null && headers.length > 0){ + Map tempHeaders = headers[0]; + for (String key : tempHeaders.keySet()) { + httpPost.setHeader(key,tempHeaders.get(key).toString()); + } + }else{ + httpPost.addHeader("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"); + httpPost.addHeader("accept-Language", "zh-CN,zh;q=0.9,en;q=0.8"); + httpPost.addHeader("content-type", "application/x-www-form-urlencoded"); + httpPost.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36"); +// httpPost.addHeader("Referer", "http://www.neeq.com.cn/rule/Business_rules.html"); + } + // 创建请求参数 + List list = new LinkedList<>(); + for (String key : param.keySet()) { + BasicNameValuePair param1 = new BasicNameValuePair(key,param.get(key).toString()); + list.add(param1); + } + // 使用URL实体转换工具 + String html=""; + try { + UrlEncodedFormEntity entityParam = new UrlEncodedFormEntity(list, "UTF-8"); + httpPost.setEntity(entityParam); + HttpResponse response = httpClient.execute(httpPost); +// HttpResponse response = httpClient.execute(httpPost,localContext); + // 从响应模型中获取响应实体 + HttpEntity responseEntity = response.getEntity(); + StatusLine statusLine = response.getStatusLine(); + System.out.println("响应状态为:" + response.getStatusLine()); + int notFundCode = 404; + int successCode = 200; + if(statusLine.getStatusCode() == successCode){ + if (responseEntity != null) { + html=EntityUtils.toString(responseEntity,"utf-8"); + } + }else{ + throw new Exception("请求错误,code码为:"+statusLine.getStatusCode()); + } + + } catch (Exception e) { + e.printStackTrace(); + html = "Download failed error is:"+ThrowMessageUtil.getErrmessage(e); + } + + return html; + + } + public static String httpPostForm(String url,Map params,Map ... headers) { + String html=""; + html = doPostFrom(url,params); + int i = 1; + while(true){ + if(html.contains("Download failed error is:")){ + log.error("DownLoadUtil------------->download is failure,url is:"+url); + DateUtil.sleep(5000); + i++; + }else{ + break; + } + if(i > 5){ + break; + } + html = doPostFrom(url,params,headers); + } + return html; + } + + public static String dosslPost(String url,String params,Map ... headers) { + String html=""; + CloseableHttpClient client = null; + HttpEntity responseEntity = null; + CloseableHttpResponse response = null; + try { + //采用绕过验证的方式处理https请求 + SSLContext sslcontext = createIgnoreVerifySSL(); + // 设置协议http和https对应的处理socket链接工厂的对象 + Registry socketFactoryRegistry = RegistryBuilder.create() + .register("http", PlainConnectionSocketFactory.INSTANCE) + .register("https", new SSLConnectionSocketFactory(sslcontext)) + .build(); + PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry); + HttpClients.custom().setConnectionManager(connManager); + //设置超时时间 + int timeout = 5; + RequestConfig config = RequestConfig.custom(). + setConnectTimeout(timeout * 1000). + setConnectionRequestTimeout(timeout * 1000). + setSocketTimeout(timeout * 1000).build(); + SocketConfig socketConfig = SocketConfig.custom() + .setSoKeepAlive(false) + .setSoLinger(1) + .setSoReuseAddress(true) + .setSoTimeout(10000) + .setTcpNoDelay(true).build(); + //创建自定义的httpclient对象 + client = HttpClients.custom().setConnectionManager(connManager).setDefaultRequestConfig(config).setDefaultSocketConfig(socketConfig).build(); +// CloseableHttpClient client = HttpClients.createDefault(); + // 2. 创建httppost实例 + HttpPost httpPost = new HttpPost(url); +// httpPost.setConfig(reqConfig); + httpPost.addHeader("Content-Type", "application/json;charset=utf-8"); + if(headers != null && headers.length > 0){ + Map tempHeaders = headers[0]; + for (String key : tempHeaders.keySet()) { + httpPost.setHeader(key,tempHeaders.get(key).toString()); + } + }else{ + httpPost.setHeader("Accept", "application/json, text/javascript, */*; q=0.01"); + httpPost.setHeader("Accept-Language","zh-CN,zh;q=0.9,en;q=0.8"); + httpPost.setHeader("User-Agent","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36"); + } + + try { + httpPost.setEntity(new StringEntity(params,"utf-8")); + response = client.execute(httpPost); + int notFundCode = 404; + int successCode = 200; + // 从响应模型中获取响应实体 + StatusLine statusLine = response.getStatusLine(); + System.out.println("响应状态为:" + response.getStatusLine()); + responseEntity = response.getEntity(); + if(statusLine.getStatusCode() == successCode){ + if (responseEntity != null) { + html=EntityUtils.toString(responseEntity,"utf-8"); + System.out.println("响应内容长度为:" + responseEntity.getContentLength()); + } + }else if(statusLine.getStatusCode() == notFundCode){ + html = "

页面404,正常结束请求即可

"; + }else{ + throw new Exception("请求错误,code码为:"+statusLine.getStatusCode()); + } + } catch (Exception e) { + e.printStackTrace(); + html = "Download failed error is:"+ThrowMessageUtil.getErrmessage(e); + + } + } catch (Exception e) { + e.printStackTrace(); + html = "Download failed error is:"+ThrowMessageUtil.getErrmessage(e); + }finally{ + try { + responseEntity.getContent().close(); + response.close(); + client.close(); + } catch (UnsupportedOperationException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + + } + + + return html; + } + public static String dosslPostForm(String url,Map param,Map ... headers) { + String html=""; + try { + //采用绕过验证的方式处理https请求 + SSLContext sslcontext = createIgnoreVerifySSL(); + + // 设置协议http和https对应的处理socket链接工厂的对象 + Registry socketFactoryRegistry = RegistryBuilder.create() + .register("http", PlainConnectionSocketFactory.INSTANCE) + .register("https", new SSLConnectionSocketFactory(sslcontext)) + .build(); + PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry); + HttpClients.custom().setConnectionManager(connManager); + //设置超时时间 + int timeout = 5; + RequestConfig config = RequestConfig.custom(). + setConnectTimeout(timeout * 1000). + setConnectionRequestTimeout(timeout * 1000). + setSocketTimeout(timeout * 1000).build(); + SocketConfig socketConfig = SocketConfig.custom() + .setSoKeepAlive(false) + .setSoLinger(1) + .setSoReuseAddress(true) + .setSoTimeout(10000) + .setTcpNoDelay(true).build(); + //创建自定义的httpclient对象 + CloseableHttpClient client = HttpClients.custom().setConnectionManager(connManager).setDefaultRequestConfig(config).setDefaultSocketConfig(socketConfig).build(); +// CloseableHttpClient client = HttpClients.createDefault(); + // 2. 创建httppost实例 + HttpPost httpPost = new HttpPost(url); +// httpPost.setConfig(reqConfig); + if(headers != null && headers.length > 0){ + Map tempHeaders = headers[0]; + for (String key : tempHeaders.keySet()) { + httpPost.setHeader(key,tempHeaders.get(key).toString()); + } + }else{ + httpPost.setHeader("Accept", "application/json, text/javascript, */*; q=0.01"); + httpPost.setHeader("Accept-Language","zh-CN,zh;q=0.9,en;q=0.8"); + httpPost.addHeader("content-type", "application/x-www-form-urlencoded"); + httpPost.setHeader("User-Agent","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36"); + } + + // 创建请求参数 + List list = new LinkedList<>(); + for (String key : param.keySet()) { + BasicNameValuePair param1 = new BasicNameValuePair(key,param.get(key).toString()); + list.add(param1); + } + // 使用URL实体转换工具 + try { + UrlEncodedFormEntity entityParam = new UrlEncodedFormEntity(list, "UTF-8"); + httpPost.setEntity(entityParam); + HttpResponse response = client.execute(httpPost); +// HttpResponse response = httpClient.execute(httpPost,localContext); + // 从响应模型中获取响应实体 + int notFundCode = 404; + int successCode = 200; + HttpEntity responseEntity = response.getEntity(); + StatusLine statusLine = response.getStatusLine(); + System.out.println("响应状态为:" + response.getStatusLine()); + if(statusLine.getStatusCode() == successCode){ + if (responseEntity != null) { + html=EntityUtils.toString(responseEntity,"utf-8"); + } + }else{ + throw new Exception("请求错误,code码为:"+statusLine.getStatusCode()); + } + + } catch (Exception e) { + e.printStackTrace(); + html = "Download failed error is:"+ThrowMessageUtil.getErrmessage(e); + } + } catch (Exception e) { + e.printStackTrace(); + html = "Download failed error is:"+ThrowMessageUtil.getErrmessage(e); + } + + + return html; + } + public static String httpSSLPostForm(String url,Map params,Map ...headers) { + String html=""; + try { + html = dosslPostForm(url,params,headers); + } catch (Exception e) { + e.printStackTrace(); + // TODO: handle exception + html = "Download failed error is:Exception!"; + } + int i = 1; + while(true){ + if(html.contains("Download failed error is:")){ + log.error("DownLoadUtil------------->download is failure,url is:"+url); + DateUtil.sleep(30000); + i++; + }else{ + break; + } + if(i > 5){ + break; + } + try { + html = dosslPostForm(url,params,headers); + } catch (Exception e) { + e.printStackTrace(); + // TODO: handle exception + html = "Download failed error is:Exception!"; + } + } + return html; + } + public static String httpSSLPost(String url,String params,Map ...headers) { + String html=""; + try { + html = dosslPost(url,params,headers); + } catch (Throwable e) { + e.printStackTrace(); + // TODO: handle exception + html = "Download failed error is:Exception!"; + } + int i = 1; + while(true){ + if(html.contains("Download failed error is:")){ + log.error("DownLoadUtil------------->download is failure,url is:"+url); + DateUtil.sleep(30000); + i++; + }else{ + break; + } + if(i > 5){ + break; + } + try { + html = dosslPost(url,params,headers); + } catch (Throwable e) { + e.printStackTrace(); + // TODO: handle exception + html = "Download failed error is:Exception!"; + } + } + return html; + } + + /** + * 模拟客户端get请求 + * @param url 模拟请求得url + * @param headers 头部信息,没有可以不传 + * @return + */ + public static String doGet(String url,Map ... headers){ + //设置超时时间 + int timeout = 15; + RequestConfig config = RequestConfig.custom(). + setConnectTimeout(timeout * 1000). + setConnectionRequestTimeout(timeout * 1000). + setSocketTimeout(timeout * 1000).build(); + SocketConfig socketConfig = SocketConfig.custom() + .setSoKeepAlive(false) + .setSoLinger(1) + .setSoReuseAddress(true) + .setSoTimeout(10000) + .setTcpNoDelay(true).build(); + HttpClientBuilder httpBuilder = HttpClientBuilder.create(); + httpBuilder.setUserAgent(ua); + HttpClient httpClient = httpBuilder.setDefaultSocketConfig(socketConfig).setDefaultRequestConfig(config).build(); + HttpGet httpGet = new HttpGet(url); + if(headers != null && headers.length > 0){ + Map tempHeaders = headers[0]; + for (String key : tempHeaders.keySet()) { + httpGet.setHeader(key,tempHeaders.get(key).toString()); + } + }else{ + httpGet.setHeader("Accept", "application/json, text/javascript, */*; q=0.01"); + httpGet.setHeader("Accept-Language","zh-CN,zh;q=0.9,en;q=0.8"); + } + String html=""; + try { + int notFundCode = 404; + int successCode = 200; + HttpResponse response = httpClient.execute(httpGet); + // 从响应模型中获取响应实体 + HttpEntity responseEntity = response.getEntity(); + StatusLine statusLine = response.getStatusLine(); + System.out.println("响应状态为:" + response.getStatusLine()); + if(statusLine.getStatusCode() == successCode){ + if (responseEntity != null) { + html=EntityUtils.toString(responseEntity,"utf-8"); + if(html.equals("")){ + html = "Download failed error is:reslut is null"; + } + } + }else if(statusLine.getStatusCode() == notFundCode){ + html = "

页面404,正常结束请求即可

"; + }else{ + throw new Exception("请求错误,code码为:"+statusLine.getStatusCode()); + } + } catch (Exception e) { + e.printStackTrace(); + html = "Download failed error is:"+ThrowMessageUtil.getErrmessage(e); + } + return html; + + } + + /** + * 文件下载 + * @param fileURL 文件连接 + * @param saveFilePath 文件存储地址 + * @throws IOException + */ + public static void downloadFile(String fileURL, String saveFilePath,Map ... headers) { + //设置超时时间 + int timeout = 60; + RequestConfig config = RequestConfig.custom(). + setConnectTimeout(timeout * 1000). + setConnectionRequestTimeout(timeout * 1000). + setSocketTimeout(timeout * 1000).build(); + SocketConfig socketConfig = SocketConfig.custom() + .setSoKeepAlive(false) + .setSoLinger(1) + .setSoReuseAddress(true) + .setSoTimeout(timeout * 1000) + .setTcpNoDelay(true).build(); + HttpClientBuilder httpBuilder = HttpClientBuilder.create(); + httpBuilder.setUserAgent(ua); + HttpClient httpClient = httpBuilder.setDefaultSocketConfig(socketConfig).setDefaultRequestConfig(config).build(); + HttpGet httpGet = new HttpGet(fileURL); + if(headers != null && headers.length > 0){ + Map tempHeaders = headers[0]; + for (String key : tempHeaders.keySet()) { + httpGet.setHeader(key,tempHeaders.get(key).toString()); + } + }else{ + httpGet.setHeader("Accept", "application/json, text/javascript, */*; q=0.01"); + httpGet.setHeader("Accept-Language","zh-CN,zh;q=0.9,en;q=0.8"); + } + try { + int successCode = 200; + HttpResponse response = httpClient.execute(httpGet); + // 从响应模型中获取响应实体 + StatusLine statusLine = response.getStatusLine(); + int statusCode = statusLine.getStatusCode(); + if(statusCode == successCode){ + // 获取响应体 + HttpEntity entity = response.getEntity(); + if (entity != null) { + // 创建输入流来读取文件内容 + try (InputStream inputStream = entity.getContent(); + FileOutputStream fileOutputStream = new FileOutputStream(new File(saveFilePath))) { + + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + fileOutputStream.write(buffer, 0, bytesRead); + } + } + log.info("文件下载成功,保存路径为: {}" , saveFilePath); + } + } else { + log.error("文件下载失败,HTTP 响应状态码为: {}" , statusCode); + } + EntityUtils.consume(response.getEntity()); + } catch (Exception e) { + log.error("下载文件异常:{}",e); + } + + } + + /** + * 删除文件 + * @param filePath + */ + public static void delFile(String filePath) { + try { + // 创建 File 对象 + File file = new File(filePath); + // 检查文件是否存在 + if (file.exists()) { + // 尝试删除文件 + if (file.delete()) { + log.info("文件删除成功: " + filePath); + } else { + log.error("无法删除文件: " + filePath); + } + } else { + log.warn("文件不存在: " + filePath); + } + } catch (Exception e) { + // TODO: handle exception + log.error("删除文件异常:{}",filePath); + } + } + +} diff --git a/opai-api/src/main/java/com/bw/opai/utils/ThrowMessageUtil.java b/opai-api/src/main/java/com/bw/opai/utils/ThrowMessageUtil.java new file mode 100644 index 0000000..9fb6314 --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/utils/ThrowMessageUtil.java @@ -0,0 +1,23 @@ +package com.bw.opai.utils; + +import java.io.PrintWriter; +import java.io.StringWriter; + +/** + * @author jian.mao + * @date 2023年3月22日 + * @description + */ +public class ThrowMessageUtil { + + /** + * 获取异常信息 + * @param t + * @return + */ + public static String getErrmessage(Throwable t){ + StringWriter stringWriter=new StringWriter(); + t.printStackTrace(new PrintWriter(stringWriter,true)); + return stringWriter.getBuffer().toString(); + } +} diff --git a/opai-api/src/main/resources/bootstrap.yml b/opai-api/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..9df65ef --- /dev/null +++ b/opai-api/src/main/resources/bootstrap.yml @@ -0,0 +1,52 @@ +# ==================== 必须文件:bootstrap.yml ==================== +# 这个文件用于配置Nacos客户端,优先级最高 +spring: + application: + name: opai-api # 服务名,对应Nacos中的Data ID + + cloud: + nacos: + # ======== 配置中心 ======== + config: + server-addr: 127.0.0.1:8848 # Nacos地址 + username: nacos # 用户名 + password: nacos # 密码 + group: public_dev # 分组 + namespace: opai # 命名空间(默认public) + file-extension: yaml # 配置文件格式 + timeout: 5000 # 超时时间(ms) + + # 核心配置:开启动态刷新 + refresh-enabled: true # 必须为true! + + # 主配置文件(从Nacos加载) + data-id: ${spring.application.name}.${spring.cloud.nacos.config.file-extension} + + # 共享配置文件(可选) + shared-configs[0]: + data-id: application.yaml # 公共配置 + group: public_dev # 公共分组 + namespace: opai + refresh: true # 公共配置也要刷新 + + # 扩展配置(可选) + # extension-configs[0]: + # data-id: datasource.yaml + # group: dev + # refresh: true + + # ======== 服务发现 ======== + discovery: + server-addr: ${spring.cloud.nacos.config.server-addr} + username: ${spring.cloud.nacos.config.username} + password: ${spring.cloud.nacos.config.password} + group: ${spring.cloud.nacos.config.group} + namespace: ${spring.cloud.nacos.config.namespace} + +logging: + level: + root: info + com.alibaba.nacos.client.config.impl: WARN + file: + path: ../logs + \ No newline at end of file diff --git a/opai-api/src/main/resources/logback-spring.xml b/opai-api/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..40c09bf --- /dev/null +++ b/opai-api/src/main/resources/logback-spring.xml @@ -0,0 +1,36 @@ + + + + + + + + + true + + ${logging.level} + + + ${log-path}/opai-api.log + + + ${log-path}/opai-api.log.%d{yyyy-MM-dd} + 7 + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %line %-5level %logger{50} - %msg%n + UTF-8 + + + + + + + + diff --git a/pom.xml b/pom.xml index a9a2907..f8bb766 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,41 @@ - - 4.0.0 - com.bw - opai-service-center - 0.0.1-SNAPSHOT - \ No newline at end of file + + + 4.0.0 + com.bw + opai-service-center + 0.0.1-SNAPSHOT + pom + + opai-api + ocr-service + + + + org.springframework.boot + spring-boot-starter-parent + 2.2.4.RELEASE + + + + + + + + org.springframework.cloud + spring-cloud-dependencies + Hoxton.SR9 + pom + import + + + + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + 2.2.6.RELEASE + pom + import + + + +