| @ -0,0 +1,226 @@ | |||||
| package com.fkzy.warn.common.util; | |||||
| import com.alibaba.fastjson2.JSONArray; | |||||
| import com.alibaba.fastjson2.JSONObject; | |||||
| import com.fkzy.warn.common.constants.LawResearchUrlConstants; | |||||
| import com.fkzy.warn.model.InvocationRecord; | |||||
| import org.springframework.http.*; | |||||
| import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; | |||||
| import org.springframework.web.client.RestTemplate; | |||||
| import java.io.File; | |||||
| import java.io.FileNotFoundException; | |||||
| import java.io.PrintWriter; | |||||
| import java.util.HashMap; | |||||
| import java.util.Map; | |||||
| import java.util.UUID; | |||||
| /** | |||||
| * @author zhangjing | |||||
| * @date 2026/03/17 14:31 | |||||
| * @description | |||||
| */ | |||||
| public class FyOpenGatewayApiUtil { | |||||
| public static final String FY_OPEN_GATEWAY_URL = "https://api.cjbdi.com:8443"; | |||||
| public static final String ACCESS_TOKEN_URL = FY_OPEN_GATEWAY_URL + "/auth/api/token/getAccess"; | |||||
| /** | |||||
| * 请求开放平台网关获取accessToken | |||||
| * | |||||
| * @param url 调用地址 | |||||
| * @param appKey 应用key--从控制台应用系统中获取 | |||||
| * @param appSecret 应用密码--从控制台应用系统中获取 | |||||
| * @return 返回数据格式参考在线文档说明 | |||||
| */ | |||||
| public static String getAccessToken(String url, String appKey, String appSecret) { | |||||
| Map<String, String> paramMap = new HashMap<>(); | |||||
| paramMap.put("appKey", appKey); | |||||
| paramMap.put("appSecret", appSecret); | |||||
| try { | |||||
| return HttpUtil.post(url, paramMap); | |||||
| } catch (Exception e) { | |||||
| throw new RuntimeException(e); | |||||
| } | |||||
| } | |||||
| // /** | |||||
| // * 接口调用--url传参 | |||||
| // * | |||||
| // * @param method 请求方法类型 | |||||
| // * @param url 接口地址,包含url参数 | |||||
| // * @param headerMap 请求头 | |||||
| // * @return 返回数据格式参考在线文档说明 | |||||
| // * @see cn.hutool.http.Method | |||||
| // */ | |||||
| // public static String invokeUrlApi(Method method, String url, Map<String, String> headerMap) { | |||||
| // HttpRequest request = HttpUtil.createRequest(method, url); | |||||
| // request.headerMap(headerMap, true); | |||||
| // try (HttpResponse execute = request.execute()) { | |||||
| // int status = execute.getStatus(); | |||||
| // if (status == 200) { | |||||
| // return execute.body(); | |||||
| // } | |||||
| // } | |||||
| // return null; | |||||
| // } | |||||
| // /** | |||||
| // * 接口调用--form表单提交 | |||||
| // * | |||||
| // * @param url 接口地址,包含url参数 | |||||
| // * @param headerMap 请求头 | |||||
| // * @param formParams form表单参数 | |||||
| // * @return 返回数据格式参考在线文档说明 | |||||
| // */ | |||||
| // public static String invokeFormApi(String url, Map<String, String> headerMap, Map<String, Object> formParams) { | |||||
| // HttpRequest request = HttpUtil.createPost(url); | |||||
| // request.headerMap(headerMap, true); | |||||
| // request.form(formParams); | |||||
| // try (HttpResponse execute = request.execute()) { | |||||
| // int status = execute.getStatus(); | |||||
| // if (status == 200) { | |||||
| // return execute.body(); | |||||
| // } | |||||
| // } | |||||
| // return null; | |||||
| // } | |||||
| // /** | |||||
| // * 接口调用--body提交 | |||||
| // * | |||||
| // * @param url 接口地址 | |||||
| // * @param headerMap 请求头 | |||||
| // * @param body body参数 | |||||
| // * @return 返回数据格式参考在线文档说明 | |||||
| // */ | |||||
| // public static String invokeBodyApi(String url, Map<String, String> headerMap, String body) { | |||||
| // HttpRequest request = HttpUtil.createPost(url); | |||||
| // request.headerMap(headerMap, true); | |||||
| // request.body(body); | |||||
| // try (HttpResponse execute = request.execute()) { | |||||
| // int status = execute.getStatus(); | |||||
| // if (status == 200) { | |||||
| // return execute.body(); | |||||
| // } | |||||
| // } | |||||
| // return null; | |||||
| // } | |||||
| public static void main(String[] args) { | |||||
| // 控制台应用系统中的apiKey | |||||
| String appKey = ""; | |||||
| // 控制台应用系统中的apiSecret | |||||
| String appSecret = ""; | |||||
| // 控制台应用系统中的appId | |||||
| String appId = ""; | |||||
| // 请求获取accessToken | |||||
| String response = getAccessToken(ACCESS_TOKEN_URL, LawResearchUrlConstants.API_KEY, LawResearchUrlConstants.API_SECRET); | |||||
| System.out.println(response); | |||||
| // 构建请求头 | |||||
| Map<String, String> headerMap = new HashMap<>(); | |||||
| // 请求id,每次请求需重新生成,保证唯一。用于做重复请求验证 | |||||
| headerMap.put("FYDN-OP-RequestId", UUID.randomUUID().toString().replaceAll("-", "")); | |||||
| // 签名串,使用在线文档中的MD5Util示例加密生成 | |||||
| headerMap.put("FYDN-OP-Sign", ""); | |||||
| // 需要从getAccessToken返回结果中解析出accessKey,返回结构样例参考在线文档 | |||||
| headerMap.put("FYDN-OP-AccessToken", LawResearchUrlConstants.ACCESS_KEY); | |||||
| headerMap.put("FYDN-OP-AppID", LawResearchUrlConstants.APPID); | |||||
| // 调用的环境(prod:线上环境,默认;test:测试环境) | |||||
| headerMap.put("FYDN-OP-Env", "prod"); | |||||
| // 用户所在系统的唯一标识 | |||||
| headerMap.put("FYDN-OP-UserId", ""); | |||||
| // 以下url、参数等为示例,以实际调用的信息为准 | |||||
| /*--------------------------------json传参-----------------------------------*/ | |||||
| // String jsonApiUrl = "http://127.0.0.1:8080/jsonApi"; | |||||
| // headerMap.put(Header.CONTENT_TYPE.getValue(), ContentType.JSON.getValue()); | |||||
| // String body = "{\"name\":\"test\", \"age\":18}"; | |||||
| // String jsonApiResult = FyOpenGatewayApiUtil.invokeBodyApi(jsonApiUrl, headerMap, body); | |||||
| // System.out.println(jsonApiResult); | |||||
| // | |||||
| // /*--------------------------------form传参-----------------------------------*/ | |||||
| // String formApiUrl = "http://127.0.0.1:8080/formApi"; | |||||
| // // 普通form表单时,设置为application/x-www-form-urlencoded | |||||
| // headerMap.put(Header.CONTENT_TYPE.getValue(), ContentType.FORM_URLENCODED.getValue()); | |||||
| // // 文件上传form表单提交时,设置为multipart/form-data | |||||
| //// headerMap.put(Header.CONTENT_TYPE.getValue(), ContentType.MULTIPART.getValue()); | |||||
| // Map<String, Object> formParams = new HashMap<>(); | |||||
| // formParams.put("name", "test"); | |||||
| // formParams.put("age", 18); | |||||
| // String formApiResult = FyOpenGatewayApiUtil.invokeFormApi(formApiUrl, headerMap, formParams); | |||||
| // System.out.println(formApiResult); | |||||
| // | |||||
| // /*--------------------------------url传参-----------------------------------*/ | |||||
| // String url = "http://127.0.0.1:8080/urlApi?name=test&age=18"; | |||||
| // headerMap.put(Header.CONTENT_TYPE.getValue(), ContentType.FORM_URLENCODED.getValue()); | |||||
| // String result = FyOpenGatewayApiUtil.invokeUrlApi(Method.GET, url, headerMap); | |||||
| // System.out.println(result); | |||||
| } | |||||
| private static InvocationRecord getData(String apiUrl, JSONArray array, JSONObject jsonObject, String method, | |||||
| String key, String ticket, String apiName | |||||
| ) { | |||||
| InvocationRecord invocationRecord = new InvocationRecord(); | |||||
| invocationRecord.setApiKey(key); | |||||
| invocationRecord.setApiTicket(ticket); | |||||
| invocationRecord.setApiUrl(apiUrl); | |||||
| if (array!=null){ | |||||
| invocationRecord.setInputArr(array.toJSONString()); | |||||
| } | |||||
| if (jsonObject!=null){ | |||||
| invocationRecord.setInputObj(jsonObject.toJSONString()); | |||||
| } | |||||
| invocationRecord.setApiName(apiName); | |||||
| RestTemplate restTemplate = new RestTemplate(); | |||||
| HttpComponentsClientHttpRequestFactory requestFactory | |||||
| = new HttpComponentsClientHttpRequestFactory(); | |||||
| requestFactory.setConnectionRequestTimeout(60000); | |||||
| requestFactory.setConnectTimeout(60000); | |||||
| requestFactory.setReadTimeout(60000); | |||||
| restTemplate.setRequestFactory(requestFactory); | |||||
| HttpHeaders headers = new HttpHeaders(); | |||||
| headers.setContentType(MediaType.APPLICATION_JSON); | |||||
| headers.set("ticket", ticket); | |||||
| headers.set("Authorization", ticket); | |||||
| String request = (array != null) ? array.toJSONString() : jsonObject.toJSONString(); | |||||
| try { | |||||
| //需要对入参进行 aes 加密 | |||||
| String requestBody = AES.aesEncrypt(request, key); | |||||
| System.out.println("requestBody=====" + requestBody); | |||||
| //设置访问的 Entity | |||||
| HttpEntity entity = new HttpEntity<>(requestBody, headers); | |||||
| //执行 请求 | |||||
| ResponseEntity<String> result = | |||||
| restTemplate.exchange(apiUrl, method == null ? HttpMethod.POST : HttpMethod.GET, entity, | |||||
| String.class); | |||||
| // 指定文件路径 | |||||
| String filePath = "output.txt"; | |||||
| String resultData=null; | |||||
| try (PrintWriter writer = new PrintWriter(new File(filePath))) { | |||||
| //接收到返回结果需要进行 aes 解密 | |||||
| invocationRecord.setIsSuccess(0); | |||||
| resultData = AES.aesDecrypt(result.getBody(), key); | |||||
| //写入内容 | |||||
| writer.println(resultData); | |||||
| invocationRecord.setIsSuccess(1); | |||||
| invocationRecord.setApiResult(resultData); | |||||
| // 自动关闭,因为使用了 try-with-resources 语句 | |||||
| } catch (FileNotFoundException e) { | |||||
| System.err.println("文件未找到: " + e.getMessage()); | |||||
| } | |||||
| } catch (Exception e) { | |||||
| e.printStackTrace(); | |||||
| } finally { | |||||
| return invocationRecord; | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,180 @@ | |||||
| package com.fkzy.warn.common.util; | |||||
| import com.alibaba.fastjson2.JSONObject; | |||||
| import com.alibaba.fastjson2.JSONWriter; | |||||
| import lombok.extern.slf4j.Slf4j; | |||||
| import org.apache.commons.lang3.StringUtils; | |||||
| import java.nio.charset.StandardCharsets; | |||||
| import java.security.MessageDigest; | |||||
| import java.security.NoSuchAlgorithmException; | |||||
| import java.util.Iterator; | |||||
| import java.util.Objects; | |||||
| import java.util.TreeMap; | |||||
| /** | |||||
| * MD5工具类 | |||||
| */ | |||||
| @Slf4j | |||||
| public class MD5Util { | |||||
| /** | |||||
| * 生成签名串 | |||||
| * | |||||
| * @param params 升序的参数 | |||||
| * @param slat 加盐:应用系统的apiKey,从控制台应用系统中获取 | |||||
| * @return 16进制签名串 | |||||
| */ | |||||
| public static String md5Sign(String params, String slat) { | |||||
| return digestToHex(params + slat); | |||||
| } | |||||
| /** | |||||
| * 验签 | |||||
| * | |||||
| * @param params 升序的参数 | |||||
| * @param sign 签名串 | |||||
| * @param slat 加盐:应用系统的apiKey,从控制台应用系统中获取 | |||||
| * @return true 表示验签通过 | |||||
| */ | |||||
| public static boolean checkSign(String params, String sign, String slat) { | |||||
| try { | |||||
| String signData = digestToHex(params + slat); | |||||
| log.info("生成的签名:{}", signData); | |||||
| return StringUtils.equals(signData, sign); | |||||
| } catch (Exception e) { | |||||
| log.error("验签异常:", e); | |||||
| return false; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * json参数排序 <br/> | |||||
| * <p> | |||||
| * post请求为json传参时,调用该方法,按json中的key升序排列 | |||||
| * </p> | |||||
| * @param obj json参数 | |||||
| * @return key升序的json字符串 | |||||
| */ | |||||
| public static String sortJsonToStr(Object obj) { | |||||
| if (Objects.isNull(obj) || "".equals(obj)) { | |||||
| return ""; | |||||
| } | |||||
| if (obj instanceof Integer | |||||
| || obj instanceof Short | |||||
| || obj instanceof Long | |||||
| || obj instanceof Float | |||||
| || obj instanceof Double | |||||
| || obj instanceof Boolean | |||||
| || obj instanceof Character | |||||
| || obj instanceof Byte) { | |||||
| return String.valueOf(obj); | |||||
| } | |||||
| if (obj instanceof String) { | |||||
| return JSONObject.toJSONString(JSONObject.parseObject(String.valueOf(obj)), JSONWriter.Feature.MapSortField); | |||||
| } | |||||
| try { | |||||
| return JSONObject.toJSONString(obj, JSONWriter.Feature.MapSortField); | |||||
| } catch (Exception e) { | |||||
| return String.valueOf(obj); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * url参数排序 | |||||
| * <br/> | |||||
| * 该方法适用范围: | |||||
| * <ul> | |||||
| * <li>get请求:url参数传参</li> | |||||
| * <li>post请求:form-data传参</li> | |||||
| * </ul> | |||||
| * <p> | |||||
| * 比如: a=1&c=3&b=2 排序后 a=1&b=2&c=3 | |||||
| * </p> | |||||
| * | |||||
| * @param paraMap TreeMap类型,将参数都放入其中 | |||||
| * @return 按顺序拼接好的参数串 | |||||
| */ | |||||
| public static String getUrlParamsSign(TreeMap<String, Object> paraMap, String slat) { | |||||
| if (Objects.isNull(paraMap) || paraMap.isEmpty()) { | |||||
| return md5Sign("", slat); | |||||
| } | |||||
| Iterator<String> it = paraMap.keySet().iterator(); | |||||
| StringBuilder paramStr = new StringBuilder(); | |||||
| while (it.hasNext()) { | |||||
| String key = it.next(); | |||||
| Object o = paraMap.get(key); | |||||
| if (Objects.isNull(o)) { | |||||
| paramStr.append("&").append(key).append("="); | |||||
| } else { | |||||
| paramStr.append("&").append(key).append("=").append(o); | |||||
| } | |||||
| } | |||||
| String urlParams = paramStr.substring(1); | |||||
| return md5Sign(urlParams, slat); | |||||
| } | |||||
| private static String digestToHex(String s) { | |||||
| try { | |||||
| MessageDigest digest = MessageDigest.getInstance("MD5"); | |||||
| byte[] b = digest.digest(s.getBytes(StandardCharsets.UTF_8)); | |||||
| return toHexString(b); | |||||
| } catch (NoSuchAlgorithmException e) { | |||||
| log.info("MD5异常,", e); | |||||
| return null; | |||||
| } | |||||
| } | |||||
| private static final char[] HEX_CHARS = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; | |||||
| private static String toHexString(byte[] b) { | |||||
| return b == null ? null : toHexString(b, 0, b.length); | |||||
| } | |||||
| private static String toHexString(byte[] b, int offset, int length) { | |||||
| if (b == null) { | |||||
| return null; | |||||
| } else { | |||||
| int end = offset + length; | |||||
| StringBuilder sb = new StringBuilder(length * 2); | |||||
| for (int i = offset; i < end; ++i) { | |||||
| int c = b[i] >> 4 & 15; | |||||
| sb.append(HEX_CHARS[c]); | |||||
| c = b[i] & 15; | |||||
| sb.append(HEX_CHARS[c]); | |||||
| } | |||||
| return sb.toString(); | |||||
| } | |||||
| } | |||||
| public static void main(String[] args) { | |||||
| // 签名的盐值,控制台应用系统中的apiKey | |||||
| String apiKey = "123456"; | |||||
| // url传参方式签名 比如a=1&b=2&c=3 | |||||
| TreeMap<String, Object> paramMap = new TreeMap<>(); | |||||
| paramMap.put("a", 1); | |||||
| paramMap.put("b", 2); | |||||
| paramMap.put("c", 3); | |||||
| String urlParamsSign = getUrlParamsSign(paramMap, apiKey); | |||||
| System.out.println("url参数签名(getUrlParamsSign方法): " + urlParamsSign); | |||||
| String urlParamsSign2 = md5Sign("a=1&b=2&c=3", apiKey); | |||||
| System.out.println("url参数签名(md5Sign方法): " + urlParamsSign2); | |||||
| // json传参方式签名 | |||||
| String obj = "{\"a\":\"0\",\"c\":\"\",\"d\":0,\"b\":\"0\",\"c1\":\"\",\"a1\":\"0\"}"; | |||||
| String sortedJsonStr = sortJsonToStr(obj); | |||||
| System.out.println("json字符串排序结果:" + sortedJsonStr); | |||||
| String jsonStrSign = md5Sign(sortedJsonStr, apiKey); | |||||
| System.out.println("json字符串签名结果:" + jsonStrSign); | |||||
| JSONObject jsonObject = JSONObject.parseObject(obj); | |||||
| String sortedJsonStr2 = sortJsonToStr(jsonObject); | |||||
| System.out.println("json对象排序结果:" + sortedJsonStr2); | |||||
| String jsonSign = md5Sign(sortedJsonStr2, apiKey); | |||||
| System.out.println("json对象签名结果:" + jsonSign); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,20 @@ | |||||
| package com.fkzy.warn.model; | |||||
| import lombok.Data; | |||||
| /** | |||||
| * @author zhangjing | |||||
| * @date 2026/03/18 15:23 | |||||
| * @description | |||||
| */ | |||||
| @Data | |||||
| public class AImessage { | |||||
| /** | |||||
| * 会话角色(user代表用户,assistant代表系统) | |||||
| */ | |||||
| String role; | |||||
| /** | |||||
| * 会话内容 | |||||
| */ | |||||
| String content; | |||||
| } | |||||
| @ -0,0 +1,20 @@ | |||||
| package com.fkzy.warn.model; | |||||
| import lombok.Data; | |||||
| /** | |||||
| * @author zhangjing | |||||
| * @date 2026/03/17 17:20 | |||||
| * @description | |||||
| */ | |||||
| @Data | |||||
| public class AccessToken { | |||||
| private String accessKey; | |||||
| private String dateTime; | |||||
| private Long expires; | |||||
| private String publicKey; | |||||
| } | |||||
| @ -0,0 +1,17 @@ | |||||
| package com.fkzy.warn.model; | |||||
| import lombok.Data; | |||||
| /** | |||||
| * @author zhangjing | |||||
| * @date 2026/03/17 17:16 | |||||
| * @description | |||||
| */ | |||||
| @Data | |||||
| public class ApiResult<T> { | |||||
| private Integer code; | |||||
| private String msg; | |||||
| private T data; // 使用泛型 T | |||||
| } | |||||
| @ -0,0 +1,38 @@ | |||||
| package com.fkzy.warn.model; | |||||
| import lombok.Data; | |||||
| import java.util.List; | |||||
| /** | |||||
| * @author zhangjing | |||||
| * @date 2026/03/18 16:43 | |||||
| * @description | |||||
| */ | |||||
| @Data | |||||
| public class ChapterNode { | |||||
| /** | |||||
| * 节点类型 (例如: chapter, article, clause) | |||||
| */ | |||||
| private String type; | |||||
| /** | |||||
| * 名称 | |||||
| */ | |||||
| private String name; | |||||
| /** | |||||
| * 内容 | |||||
| */ | |||||
| private String content; | |||||
| /** | |||||
| * 编码/序号 (用于排序,例如 "1", "2", "A", "B") | |||||
| */ | |||||
| private String code; | |||||
| /** | |||||
| * 子节点列表 (例如: 该章节下的条款、款、项等) | |||||
| */ | |||||
| private List<ChapterNode> children; | |||||
| } | |||||
| @ -0,0 +1,76 @@ | |||||
| package com.fkzy.warn.model; | |||||
| import lombok.Data; | |||||
| import java.util.Date; | |||||
| import java.util.List; | |||||
| /** | |||||
| * @author zhangjing | |||||
| * @date 2026/03/18 15:11 | |||||
| * @description | |||||
| */ | |||||
| @Data | |||||
| public class LawGroup { | |||||
| private String title; | |||||
| private String organ; | |||||
| private String pubDate; | |||||
| private String exprityDate; | |||||
| private String timeliness; | |||||
| private String id; | |||||
| // 数据库或系统相关字段 | |||||
| private String createBy; | |||||
| private Date createTime; | |||||
| private String updateBy; | |||||
| private Date updateTime; | |||||
| private String remark; | |||||
| private Integer pageNum; | |||||
| private Integer pageSize; | |||||
| // 核心业务字段 | |||||
| private String lawId; | |||||
| private String base; | |||||
| private String preface; | |||||
| private String source; | |||||
| // 文件路径相关字段 | |||||
| private String minioPath; | |||||
| private String pdfPathName; | |||||
| private String isPdf; | |||||
| private String syncFlag; | |||||
| // 查询辅助字段 | |||||
| private String timelinessIn; | |||||
| private String cnt; | |||||
| private String name; | |||||
| private String titleEq; | |||||
| private String titleIn; | |||||
| private String pubdate_range_start; | |||||
| private String pubdate_range_end; | |||||
| private String content; | |||||
| // 系统标识字段 | |||||
| private String fyopentoken; | |||||
| private String fyopensysid; | |||||
| private String fyopensign; | |||||
| private String fyopserviceid; | |||||
| // 列表查询字段 | |||||
| private List<String> sourceList; | |||||
| private List<String> timelinessList; | |||||
| private List<String> organList; | |||||
| private List<String> pubDateList; | |||||
| private List<String> exprityDateList; | |||||
| // 排序字段 | |||||
| private String sortItem; | |||||
| private String sortMethod; | |||||
| // 其他状态字段 | |||||
| private String isValid; | |||||
| private String lawType; | |||||
| } | |||||
| @ -0,0 +1,45 @@ | |||||
| package com.fkzy.warn.model; | |||||
| import java.util.List; | |||||
| /** | |||||
| * @author zhangjing | |||||
| * @date 2026/03/18 16:25 | |||||
| * @description | |||||
| */ | |||||
| public class LawGroupInfo { | |||||
| /** | |||||
| * 时间 | |||||
| */ | |||||
| private String time; | |||||
| /** | |||||
| * 标题 | |||||
| */ | |||||
| private String title; | |||||
| /** | |||||
| * 发布机构 | |||||
| */ | |||||
| private String organ; | |||||
| /** | |||||
| * 失效日期 | |||||
| */ | |||||
| private String exprityDate; | |||||
| /** | |||||
| * 时效性 (例如: 现行有效, 已废止, 尚未生效) | |||||
| */ | |||||
| private String timeliness; | |||||
| /** | |||||
| * 文号 | |||||
| */ | |||||
| private String doc_number; | |||||
| /** | |||||
| * 子节点列表 (例如: 附件、子通知等) | |||||
| */ | |||||
| private List<ChapterNode> children; | |||||
| } | |||||
| @ -0,0 +1,17 @@ | |||||
| package com.fkzy.warn.model; | |||||
| import lombok.Data; | |||||
| import java.util.List; | |||||
| /** | |||||
| * @author zhangjing | |||||
| * @date 2026/03/18 16:03 | |||||
| * @description | |||||
| */ | |||||
| @Data | |||||
| public class LawPageData<T> { | |||||
| Integer total; | |||||
| List<T> result; | |||||
| } | |||||
| @ -0,0 +1,25 @@ | |||||
| package com.fkzy.warn.model.params; | |||||
| import com.fkzy.warn.model.AImessage; | |||||
| import lombok.Data; | |||||
| import java.util.List; | |||||
| /** | |||||
| * @author zhangjing | |||||
| * @date 2026/03/18 15:26 | |||||
| * @description | |||||
| */ | |||||
| @Data | |||||
| public class AILawParams { | |||||
| /** | |||||
| * 模型标识 中国法研LLM | |||||
| */ | |||||
| String model; | |||||
| /** | |||||
| * 流式开关(固定值true) | |||||
| */ | |||||
| Boolean stream; | |||||
| List<AImessage> messages; | |||||
| } | |||||
| @ -0,0 +1,68 @@ | |||||
| package com.fkzy.warn.model.params; | |||||
| import lombok.Data; | |||||
| import java.util.List; | |||||
| /** | |||||
| * @author zhangjing | |||||
| * @date 2026/03/17 16:20 | |||||
| * @description | |||||
| */ | |||||
| @Data | |||||
| public class LawGroupParam { | |||||
| /** | |||||
| * 每页条数 | |||||
| */ | |||||
| private Integer pageSize; | |||||
| /** | |||||
| * 当前页码 | |||||
| */ | |||||
| private Integer pageNum; | |||||
| /** | |||||
| * 法规名称(关键词) | |||||
| */ | |||||
| private String title; | |||||
| /** | |||||
| * 发布部门 | |||||
| */ | |||||
| private String organ; | |||||
| /** | |||||
| * 时效状态(如:现行有效) | |||||
| */ | |||||
| private String timeliness; | |||||
| /** | |||||
| * 发布时间 | |||||
| */ | |||||
| private String pubDate; | |||||
| /** | |||||
| * 发布部门列表(支持多选) | |||||
| */ | |||||
| private List<String> organList; | |||||
| /** | |||||
| * 时效状态列表(支持多选) | |||||
| */ | |||||
| private List<String> timelinessList; | |||||
| /** | |||||
| * 发布时间列表(支持多选) | |||||
| */ | |||||
| private List<String> pubDateList; | |||||
| /** | |||||
| * 效力级别列表(如:地方政府规章) | |||||
| */ | |||||
| private List<String> sourceList; | |||||
| /** | |||||
| * 实施日期列表(如:2015) | |||||
| */ | |||||
| private List<String> exprityDateList; | |||||
| } | |||||
| @ -0,0 +1,25 @@ | |||||
| package com.fkzy.warn.model.params; | |||||
| import lombok.Data; | |||||
| import java.util.List; | |||||
| /** | |||||
| * @author zhangjing | |||||
| * @date 2026/03/17 16:20 | |||||
| * @description | |||||
| */ | |||||
| @Data | |||||
| public class PageParam { | |||||
| /** | |||||
| * 每页条数 | |||||
| */ | |||||
| private Integer pageSize; | |||||
| /** | |||||
| * 当前页码 | |||||
| */ | |||||
| private Integer pageNum; | |||||
| } | |||||
| @ -0,0 +1,23 @@ | |||||
| package com.fkzy.warn.service; | |||||
| import com.baomidou.mybatisplus.extension.service.IService; | |||||
| import com.fkzy.warn.model.LawCase; | |||||
| import com.fkzy.warn.model.params.AILawParams; | |||||
| import com.fkzy.warn.model.params.LawGroupParam; | |||||
| import java.util.List; | |||||
| /** | |||||
| * @author zhangjing | |||||
| * @date 2023/10/16 18:17 | |||||
| * @description | |||||
| */ | |||||
| public interface FyApiService extends IService<LawCase> { | |||||
| void getAccessToken(); | |||||
| void getLawGroup(LawGroupParam lawGroupParam); | |||||
| void getLawGroupInfo(String id); | |||||
| void sendAILaw(AILawParams aiLawParams); | |||||
| } | |||||
| @ -0,0 +1,373 @@ | |||||
| package com.fkzy.warn.service.impl; | |||||
| import com.alibaba.fastjson2.JSON; | |||||
| import com.alibaba.fastjson2.JSONObject; | |||||
| import com.alibaba.fastjson2.TypeReference; | |||||
| import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; | |||||
| import com.fkzy.warn.common.constants.LawResearchUrlConstants; | |||||
| import com.fkzy.warn.common.util.MD5Util; | |||||
| import com.fkzy.warn.common.util.RedisUtil; | |||||
| import com.fkzy.warn.mapper.LawCaseMapper; | |||||
| import com.fkzy.warn.model.*; | |||||
| import com.fkzy.warn.model.params.AILawParams; | |||||
| import com.fkzy.warn.model.params.LawGroupParam; | |||||
| import com.fkzy.warn.model.params.PageParam; | |||||
| import com.fkzy.warn.service.FyApiService; | |||||
| import com.fkzy.warn.service.InvocationRecordService; | |||||
| import lombok.extern.slf4j.Slf4j; | |||||
| import okhttp3.*; | |||||
| import okio.BufferedSource; | |||||
| import org.apache.http.client.config.RequestConfig; | |||||
| import org.apache.http.client.methods.CloseableHttpResponse; | |||||
| import org.apache.http.client.methods.HttpPost; | |||||
| import org.apache.http.entity.StringEntity; | |||||
| 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.http.*; | |||||
| import org.springframework.http.MediaType; | |||||
| import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; | |||||
| import org.springframework.stereotype.Service; | |||||
| import org.springframework.web.client.RestTemplate; | |||||
| import javax.annotation.Resource; | |||||
| import java.io.IOException; | |||||
| import java.net.URLEncoder; | |||||
| import java.nio.charset.StandardCharsets; | |||||
| import java.util.UUID; | |||||
| import java.util.concurrent.TimeUnit; | |||||
| /** | |||||
| * @author zhangjing | |||||
| * @date 2023/10/16 18:17 | |||||
| * @description | |||||
| */ | |||||
| @Slf4j | |||||
| @Service | |||||
| public class FyApiServiceImpl extends ServiceImpl<LawCaseMapper, LawCase> implements FyApiService { | |||||
| @Value(value = "${api.accessTokenUrl}") | |||||
| private String accessTokenUrl; | |||||
| @Value(value = "${api.appKey}") | |||||
| private String appKey; | |||||
| @Value(value = "${api.appSecret}") | |||||
| private String appSecret; | |||||
| @Value(value = "${api.lawGroupUrl}") | |||||
| private String lawGroupUrl; | |||||
| @Value(value = "${api.lawGroupInfoUrl}") | |||||
| private String lawGroupInfoUrl; | |||||
| @Value(value = "${api.aiLawUrl}") | |||||
| private String aiLawUrl; | |||||
| @Value(value = "${api.apiId}") | |||||
| private String apiId; | |||||
| @Resource | |||||
| RedisUtil redisUtil; | |||||
| @Resource | |||||
| InvocationRecordService invocationRecordService; | |||||
| @Override | |||||
| public void getAccessToken() { | |||||
| try { | |||||
| String result = postAccessToken(accessTokenUrl, appKey, appSecret); | |||||
| ApiResult<AccessToken> apiResult = JSON.parseObject( | |||||
| result, | |||||
| new TypeReference<ApiResult<AccessToken>>() { | |||||
| } | |||||
| ); | |||||
| if (apiResult != null && apiResult.getCode().equals(1000)) { | |||||
| String accessKey = apiResult.getData().getAccessKey(); | |||||
| redisUtil.del("accessKey"); | |||||
| redisUtil.set("accessKey", accessKey, 60 * 48); | |||||
| } | |||||
| } catch (Exception e) { | |||||
| throw new RuntimeException(e); | |||||
| } | |||||
| } | |||||
| @Override | |||||
| public void getLawGroup(LawGroupParam lawGroupParam) { | |||||
| // getAccessToken(); | |||||
| // if (true){ | |||||
| // return; | |||||
| // } | |||||
| PageParam pageParam = new PageParam(); | |||||
| pageParam.setPageNum(1); | |||||
| pageParam.setPageSize(20); | |||||
| String json = JSON.toJSONString(lawGroupParam); | |||||
| HttpHeaders headers = getRequestHeader(json); | |||||
| String result = postAip(lawGroupUrl, headers, json, "法研平台-法律法规分组列表"); | |||||
| ApiResult<LawPageData<LawGroup>> resultData = JSON.parseObject( | |||||
| result, | |||||
| new TypeReference<ApiResult<LawPageData<LawGroup>>>() { | |||||
| } | |||||
| ); | |||||
| } | |||||
| @Override | |||||
| public void getLawGroupInfo(String id) { | |||||
| JSONObject object = new JSONObject(); | |||||
| object.put("id", id); | |||||
| String json = object.toJSONString(); | |||||
| HttpHeaders headers = getRequestHeader(json); | |||||
| String result = postAip(lawGroupInfoUrl, headers, json, "法研平台-法律法规详情"); | |||||
| ApiResult<LawGroupInfo> resultData = JSON.parseObject( | |||||
| result, | |||||
| new TypeReference<ApiResult<LawGroupInfo>>() { | |||||
| } | |||||
| ); | |||||
| } | |||||
| @Override | |||||
| public void sendAILaw(AILawParams aiLawParams) { | |||||
| // getAccessToken(); | |||||
| // if (true) { | |||||
| // return; | |||||
| // } | |||||
| String jsonParams = JSON.toJSONString(aiLawParams); | |||||
| HttpHeaders headers = getRequestHeader(jsonParams); | |||||
| postStream(aiLawUrl, headers, jsonParams, "法研平台-AI检索法规"); | |||||
| } | |||||
| /** | |||||
| * 发送流式请求到指定URL,并处理SSE响应 | |||||
| * | |||||
| * @param url 请求的完整URL | |||||
| * @param headers 已经构建好的请求头,应包含认证等信息 | |||||
| * @param jsonParams JSON格式的请求体 | |||||
| * @param logDesc 用于日志描述的标识 | |||||
| */ | |||||
| public void postStream(String url, HttpHeaders headers, String jsonParams, String logDesc) { | |||||
| // 1. 创建 OkHttpClient 实例 | |||||
| OkHttpClient client = new OkHttpClient.Builder() | |||||
| .connectTimeout(30, TimeUnit.SECONDS) | |||||
| // SSE是长连接,设置较长的读取超时 | |||||
| .readTimeout(5, TimeUnit.MINUTES) | |||||
| .build(); | |||||
| // 2. 构建 Request | |||||
| okhttp3.MediaType JSON_TYPE = okhttp3.MediaType.parse("application/json; charset=utf-8"); | |||||
| RequestBody body = RequestBody.create(JSON_TYPE, jsonParams); | |||||
| Request.Builder requestBuilder = new Request.Builder() | |||||
| .url(url) | |||||
| .post(body); | |||||
| // 将 Spring 的 HttpHeaders 复制到 OkHttp 的 Request.Builder 中 | |||||
| for (String headerName : headers.keySet()) { | |||||
| String headerValue = headers.getFirst(headerName); | |||||
| if (headerValue != null) { | |||||
| requestBuilder.addHeader(headerName, headerValue); | |||||
| } | |||||
| } | |||||
| Request request = requestBuilder.build(); | |||||
| // 3. 发起异步请求 | |||||
| Call call = client.newCall(request); | |||||
| call.enqueue(new Callback() { | |||||
| @Override | |||||
| public void onFailure(Call call, IOException e) { | |||||
| System.err.println(logDesc + " - 请求失败: " + e.getMessage()); | |||||
| e.printStackTrace(); | |||||
| } | |||||
| @Override | |||||
| public void onResponse(Call call, Response response) throws IOException { | |||||
| try { | |||||
| if (!response.isSuccessful()) { | |||||
| System.err.println(logDesc + " - 请求失败,状态码: " + response.code()); | |||||
| System.err.println(logDesc + " - 错误响应体: " + response.body().string()); | |||||
| return; | |||||
| } | |||||
| // 检查 Content-Type | |||||
| String contentType = response.header("Content-Type"); | |||||
| if (contentType != null && contentType.toLowerCase().contains("text/event-stream")) { | |||||
| System.out.println(logDesc + " - 检测到 content-type: " + contentType); | |||||
| } else { | |||||
| System.err.println(logDesc + " - 警告: 响应的 content-type 不是 'text/event-stream': " + contentType); | |||||
| } | |||||
| // 获取响应体源 | |||||
| ResponseBody responseBody = response.body(); | |||||
| if (responseBody == null) { | |||||
| System.err.println(logDesc + " - 响应体为空"); | |||||
| return; | |||||
| } | |||||
| BufferedSource source = responseBody.source(); | |||||
| System.out.println(logDesc + " - 请求成功,开始处理流式响应..."); | |||||
| // 4. 逐行读取并处理流 | |||||
| while (!source.exhausted()) { | |||||
| String line = source.readUtf8LineStrict(); | |||||
| if (line.startsWith("data:")) { | |||||
| // 提取 data 字段后的 JSON 内容 | |||||
| String jsonData = line.substring(5).trim(); // 去掉 "data:" 前缀和空格 | |||||
| try { | |||||
| // 假设服务器返回的 data 是 JSON,可以使用 Fastjson2 解析 | |||||
| Object parsedData = JSON.parse(jsonData); | |||||
| System.out.println(logDesc + " - 收到数据: " + parsedData); | |||||
| // 在这里,你可以将 parsedData 转换为你需要的对象类型, | |||||
| // 并进行后续的业务处理 | |||||
| } catch (Exception e) { | |||||
| // 如果 data 不是 JSON,直接打印 | |||||
| System.out.println(logDesc + " - 收到非JSON数据: " + jsonData); | |||||
| } | |||||
| } else if (line.startsWith("event:") || line.startsWith("id:") || line.startsWith("retry:")) { | |||||
| // 处理其他 SSE 事件字段 (如果需要) | |||||
| System.out.println(logDesc + " - SSE Event Line: " + line); | |||||
| } else if (line.isEmpty()) { | |||||
| // SSE 规范中,空行表示一个事件的结束 | |||||
| // System.out.println(logDesc + " - Event End (empty line)"); | |||||
| } else { | |||||
| // 有些服务器可能直接发送数据而不带 "data:" 前缀 | |||||
| System.out.println(logDesc + " - Received raw line: " + line); | |||||
| } | |||||
| } | |||||
| System.out.println(logDesc + " - 流式响应处理完毕。"); | |||||
| } finally { | |||||
| response.close(); // 确保资源被释放 | |||||
| } | |||||
| } | |||||
| }); | |||||
| System.out.println(logDesc + " - 异步请求已发送,正在监听流式响应..."); | |||||
| } | |||||
| private String postAip(String lawGroupUrl, HttpHeaders headers | |||||
| , String request, String apiName) { | |||||
| //接口调用日志 | |||||
| InvocationRecord invocationRecord = new InvocationRecord(); | |||||
| RestTemplate restTemplate = new RestTemplate(); | |||||
| HttpComponentsClientHttpRequestFactory requestFactory | |||||
| = new HttpComponentsClientHttpRequestFactory(); | |||||
| requestFactory.setConnectionRequestTimeout(60000); | |||||
| requestFactory.setConnectTimeout(60000); | |||||
| requestFactory.setReadTimeout(60000); | |||||
| restTemplate.setRequestFactory(requestFactory); | |||||
| try { | |||||
| HttpEntity<String> entity = new HttpEntity<>(request, headers); | |||||
| ResponseEntity<String> response = restTemplate.exchange( | |||||
| lawGroupUrl, | |||||
| HttpMethod.POST, | |||||
| entity, | |||||
| String.class | |||||
| ); | |||||
| String rawResponseBody = response.getBody(); | |||||
| String resultData; | |||||
| // 获取响应头中的 Content-Type | |||||
| String contentType = response.getHeaders().getContentType().toString(); | |||||
| if (contentType != null && contentType.toLowerCase().contains("charset=utf-8")) { | |||||
| // 指定了UTF-8,则使用UTF-8解码 | |||||
| resultData = new String(rawResponseBody.getBytes("ISO-8859-1"), "UTF-8"); | |||||
| } else { | |||||
| // 兜底 | |||||
| resultData = new String(rawResponseBody.getBytes("ISO-8859-1"), "UTF-8"); | |||||
| } | |||||
| //日志 | |||||
| invocationRecord.setApiUrl(lawGroupUrl); | |||||
| if (request != null) { | |||||
| invocationRecord.setInputObj(request); | |||||
| } | |||||
| invocationRecord.setApiName(apiName); | |||||
| invocationRecord.setIsSuccess(0); | |||||
| invocationRecord.setApiResult(resultData); | |||||
| JSONObject jsonObject = JSONObject.parseObject(resultData); | |||||
| if (jsonObject != null && jsonObject.getString("code").equals("1000")) { | |||||
| invocationRecord.setIsSuccess(1); | |||||
| return resultData; | |||||
| } | |||||
| } catch (Exception e) { | |||||
| // 记录失败情况 | |||||
| invocationRecord.setIsSuccess(0); | |||||
| invocationRecord.setApiResult("Error: " + e.getMessage()); | |||||
| e.printStackTrace(); // 或者使用更合适的日志框架 | |||||
| } finally { | |||||
| // 无论成功还是失败,都确保记录被保存 | |||||
| invocationRecordService.save(invocationRecord); | |||||
| } | |||||
| return null; | |||||
| } | |||||
| private HttpHeaders getRequestHeader(String params) { | |||||
| HttpHeaders headers = new HttpHeaders(); | |||||
| headers.setContentType(MediaType.APPLICATION_JSON); | |||||
| headers.set("FYDN-OP-RequestId", UUID.randomUUID().toString().replaceAll("-", "")); | |||||
| // 签名串,使用在线文档中的MD5Util示例加密生成 | |||||
| String sortString = MD5Util.sortJsonToStr(params); | |||||
| String sign = MD5Util.md5Sign(sortString, appKey); | |||||
| headers.set("FYDN-OP-Sign", sign); | |||||
| // 需要从getAccessToken返回结果中解析出accessKey,返回结构样例参考在线文档 | |||||
| String accessToken = getRedisAccessToken(); | |||||
| headers.set("FYDN-OP-AccessToken", accessToken); | |||||
| headers.set("FYDN-OP-AppID", apiId); | |||||
| // 调用的环境(prod:线上环境,默认;test:测试环境) | |||||
| headers.set("FYDN-OP-Env", "prod"); | |||||
| // 用户所在系统的唯一标识 | |||||
| headers.set("FYDN-OP-UserId", ""); | |||||
| return headers; | |||||
| } | |||||
| private String getRedisAccessToken() { | |||||
| String accessToken = redisUtil.getString("accessKey"); | |||||
| if (accessToken == null) { | |||||
| getAccessToken(); | |||||
| accessToken = redisUtil.getString("accessKey"); | |||||
| } | |||||
| if (accessToken != null && accessToken.startsWith("\"") && accessToken.endsWith("\"")) { | |||||
| // 使用 substring 去掉第一个和最后一个字符 | |||||
| return accessToken.substring(1, accessToken.length() - 1); | |||||
| } | |||||
| return accessToken; | |||||
| } | |||||
| public String postAccessToken(String apiUrl, String appKey, String appSecret) throws Exception { | |||||
| // 将 paramMap 转为 JSON 字符串 | |||||
| try (CloseableHttpClient httpclient = HttpClients.createDefault()) { | |||||
| HttpPost httpPost = new HttpPost(apiUrl); | |||||
| RequestConfig config = RequestConfig.custom().setSocketTimeout(10000).setConnectTimeout(10000).setConnectionRequestTimeout(10000).build(); | |||||
| httpPost.setConfig(config); | |||||
| // 构造 x-www-form-urlencoded 参数 | |||||
| String params = "appKey=" + URLEncoder.encode(appKey, StandardCharsets.UTF_8.toString()) + "&appSecret=" + URLEncoder.encode(appSecret, StandardCharsets.UTF_8.toString()); | |||||
| StringEntity entity = new StringEntity(params, StandardCharsets.UTF_8); | |||||
| httpPost.setEntity(entity); | |||||
| httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded"); | |||||
| try (CloseableHttpResponse response = httpclient.execute(httpPost)) { | |||||
| int statusCode = response.getStatusLine().getStatusCode(); | |||||
| if (statusCode == 200) { | |||||
| org.apache.http.HttpEntity resEntity = response.getEntity(); | |||||
| return EntityUtils.toString(resEntity, StandardCharsets.UTF_8); | |||||
| } else { | |||||
| System.err.println("HTTP Error: " + statusCode); | |||||
| return null; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||