diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/util/ApiSignUtil.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/util/ApiSignUtil.java new file mode 100644 index 00000000..4661c6e7 --- /dev/null +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/util/ApiSignUtil.java @@ -0,0 +1,67 @@ +package com.zdxt.enforcementcode.common.util.api; + +import cn.hutool.core.codec.Base64; +import cn.hutool.crypto.SecureUtil; +import cn.hutool.crypto.asymmetric.KeyType; +import cn.hutool.crypto.asymmetric.RSA; + +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Collectors; + +public class ApiSignUtil { + + private static final String SIGN_ALGORITHM = "SHA256withRSA"; + + public static String generateRsaKeyPair() { + RSA rsa = SecureUtil.rsa(); + return "publicKey=" + rsa.getPublicKeyBase64() + "\nprivateKey=" + rsa.getPrivateKeyBase64(); + } + + public static String sign(String content, String privateKeyBase64) throws Exception { + byte[] keyBytes = Base64.decode(privateKeyBase64); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); + PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpec); + + Signature signature = Signature.getInstance(SIGN_ALGORITHM); + signature.initSign(privateKey); + signature.update(content.getBytes(StandardCharsets.UTF_8)); + return Base64.encode(signature.sign()); + } + + public static boolean verify(String content, String signBase64, String publicKeyBase64) throws Exception { + byte[] keyBytes = Base64.decode(publicKeyBase64); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); + PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(keySpec); + + Signature signature = Signature.getInstance(SIGN_ALGORITHM); + signature.initVerify(publicKey); + signature.update(content.getBytes(StandardCharsets.UTF_8)); + return signature.verify(Base64.decode(signBase64)); + } + + public static String buildSignContent(Map params) { + TreeMap sorted = new TreeMap<>(params); + return sorted.entrySet().stream() + .filter(e -> e.getValue() != null && !e.getValue().isEmpty()) + .map(e -> e.getKey() + "=" + e.getValue()) + .collect(Collectors.joining("&")); + } + + public static String rsaEncrypt(String data, String publicKeyBase64) { + RSA rsa = SecureUtil.rsa(null, publicKeyBase64); + return rsa.encryptBase64(data, StandardCharsets.UTF_8, KeyType.PublicKey); + } + + public static String rsaDecrypt(String data, String privateKeyBase64) { + RSA rsa = SecureUtil.rsa(privateKeyBase64, null); + return rsa.decryptStr(data, KeyType.PrivateKey, StandardCharsets.UTF_8); + } +} diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/config/ApiPlatformConfig.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/config/ApiPlatformConfig.java new file mode 100644 index 00000000..9b12475b --- /dev/null +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/config/ApiPlatformConfig.java @@ -0,0 +1,16 @@ +package com.zdxt.enforcementcode.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "api-platform") +public class ApiPlatformConfig { + + private boolean enabled = false; + + private long timestampExpireSeconds = 300; + +} diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/config/ApiPlatformWebConfig.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/config/ApiPlatformWebConfig.java new file mode 100644 index 00000000..2127f678 --- /dev/null +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/config/ApiPlatformWebConfig.java @@ -0,0 +1,22 @@ +package com.zdxt.enforcementcode.config; + + +import com.zdxt.enforcementcode.handler.ApiSignInterceptor; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@RequiredArgsConstructor +public class ApiPlatformWebConfig implements WebMvcConfigurer { + + private final ApiSignInterceptor apiSignInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(apiSignInterceptor) + .addPathPatterns("/external/api/**") + .order(10); + } +} diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/externalInterface/LongGangInterfaceController.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/externalInterface/LongGangInterfaceController.java new file mode 100644 index 00000000..2d076cba --- /dev/null +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/externalInterface/LongGangInterfaceController.java @@ -0,0 +1,82 @@ +package com.zdxt.enforcementcode.controller.externalInterface; + +import cn.dev33.satoken.annotation.SaIgnore; +import cn.hutool.core.util.StrUtil; +import com.zdxt.enforcementcode.domain.EfcodeCpPlan; +import com.zdxt.enforcementcode.domain.SysApiPlatformConfigEntity; +import com.zdxt.enforcementcode.domain.bo.EfcodeCpPlanBo; +import com.zdxt.enforcementcode.domain.bo.externalInterface.LongGangCreateTaskBo; +import com.zdxt.enforcementcode.domain.vo.EfcodeCpPlanVo; +import com.zdxt.enforcementcode.domain.vo.externalInterface.LongGangCreateTaskVo; +import com.zdxt.enforcementcode.service.IEfcodeCpPlanService; +import com.zdxt.enforcementcode.service.IEfcodeCpTaskService; +import com.zdxt.enforcementcode.service.ISysApiPlatformConfigService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.domain.R; +import org.dromara.common.core.utils.MapstructUtils; +import org.dromara.common.idempotent.annotation.RepeatSubmit; +import org.dromara.common.log.annotation.Log; +import org.dromara.common.log.enums.BusinessType; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@SaIgnore +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/external/api") +public class LongGangInterfaceController { + + private static final String HEADER_APP_KEY = "X-App-Key"; + + private final IEfcodeCpPlanService planService; + private final IEfcodeCpTaskService taskService; + private final ISysApiPlatformConfigService sysApiPlatformConfigService; + + @SaIgnore + @RepeatSubmit() + @Log(title = "外部接口-新增任务", businessType = BusinessType.INSERT) + @PostMapping("/createTask") + public R createTask( + @RequestHeader(value = HEADER_APP_KEY, required = false) String appKey, + @Validated @RequestBody LongGangCreateTaskBo bo) { + if (StrUtil.isBlank(appKey)) { + return R.fail("缺少请求头: " + HEADER_APP_KEY); + } + + SysApiPlatformConfigEntity consumer = sysApiPlatformConfigService.findByAppKey(appKey).orElse(null); + if (consumer == null) { + return R.fail("无效的 appKey"); + } + + String channelNo = consumer.getChannelNo(); + if (StrUtil.isBlank(channelNo)) { + return R.fail("渠道未配置 channelNo"); + } + + EfcodeCpPlanBo planBo = MapstructUtils.convert(bo, EfcodeCpPlanBo.class); + planBo.setChannelNo(channelNo); + planBo.setTaskId(""); + planBo.setReserveStatus(0L); + planBo.setSystemReserved(0L); + planBo.setManualReserved(0L); + planBo.setIsDelete(0L); + + planService.insertByBo(planBo); + + EfcodeCpPlanVo planVo = planService.queryById(planBo.getId()); + EfcodeCpPlan plan = MapstructUtils.convert(planVo, EfcodeCpPlan.class); + String taskId = taskService.forceGenerateTask(plan); + + LongGangCreateTaskVo vo = new LongGangCreateTaskVo(); + vo.setPlanId(planBo.getId()); + vo.setTaskId(taskId); + vo.setOutSerialNo(bo.getOutSerialNo()); + + log.info("[外部接口] 新增任务, appKey={}, channelNo={}, planId={}, taskId={}", + appKey, channelNo, planBo.getId(), taskId); + return R.ok(vo); + } +} diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/EfcodeCpPlan.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/EfcodeCpPlan.java index 0b854f60..d9026005 100644 --- a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/EfcodeCpPlan.java +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/EfcodeCpPlan.java @@ -184,5 +184,13 @@ public class EfcodeCpPlan extends BaseEntity { */ private String myRemark; + /** + * 渠道编号 0|市局,1|龙岗,2|龙华 + */ + private String channelNo; + /** + * 外部流水号 + */ + private String outSerialNo; } diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/SysApiPlatformConfigEntity.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/SysApiPlatformConfigEntity.java new file mode 100644 index 00000000..c9b72fc1 --- /dev/null +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/SysApiPlatformConfigEntity.java @@ -0,0 +1,72 @@ +package com.zdxt.enforcementcode.domain; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import org.dromara.common.mybatis.core.domain.BaseEntity; + +import java.io.Serial; + +@Data +@TableName("sys_api_platform_config") +public class SysApiPlatformConfigEntity extends BaseEntity { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @TableId(value = "id") + private Long id; + + /** + * 渠道名称 + */ + private String channelName; + + /** + * 渠道编号(0=市局,1=龙岗,2=龙华) + */ + private String channelNo; + + /** + * 应用Key + */ + private String appKey; + + /** + * 应用密钥(HMAC模式) + */ + private String appSecret; + + /** + * 平台公钥(RSA) + */ + private String platformPublicKey; + + /** + * 平台私钥(RSA) + */ + private String platformPrivateKey; + + /** + * 调用方公钥(RSA) + */ + private String callerPublicKey; + + /** + * 是否启用签名验证 + */ + private Boolean signEnabled; + + /** + * 是否启用加解密 + */ + private Boolean encryptEnabled; + + /** + * 状态(0正常 1停用) + */ + private String status; +} diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/bo/EfcodeCpPlanBo.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/bo/EfcodeCpPlanBo.java index c9e59229..d76e8168 100644 --- a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/bo/EfcodeCpPlanBo.java +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/bo/EfcodeCpPlanBo.java @@ -205,5 +205,13 @@ public class EfcodeCpPlanBo extends BaseEntity { */ private String myRemark; + /** + * 渠道编号 0|市局,1|龙岗,2|龙华 + */ + private String channelNo; + /** + * 外部流水号 + */ + private String outSerialNo; } diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/bo/externalInterface/LongGangCreateTaskBo.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/bo/externalInterface/LongGangCreateTaskBo.java new file mode 100644 index 00000000..c9d21e9d --- /dev/null +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/bo/externalInterface/LongGangCreateTaskBo.java @@ -0,0 +1,68 @@ +package com.zdxt.enforcementcode.domain.bo.externalInterface; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.util.Date; + +@Data +public class LongGangCreateTaskBo { + + @NotNull(message = "计划类别不能为空") + private Long planType; + + @NotNull(message = "计划执行日期不能为空") + @JsonFormat(pattern = "yyyy-MM-dd") + private Date planDate; + + @NotNull(message = "执行时段不能为空") + private Long planTime; + + @NotBlank(message = "执法事项不能为空") + private String enforceContent; + + @NotBlank(message = "企业名称不能为空") + private String enterpriseName; + + @NotBlank(message = "企业信用代码不能为空") + private String enterpriseNumber; + + private String enterpriseId; + + private String contactPerson; + + private String contactPhone; + + @NotBlank(message = "执法人员姓名不能为空") + private String enforceUserNames; + + private String enforceUserIds; + + private String enforceUserCodes; + + private String enforceUserTels; + + private Long isPublic = 0L; + + private Long isUnion = 0L; + + @NotBlank(message = "执法单位ID不能为空") + private String bureauDeptId; + + @NotBlank(message = "执法单位名称不能为空") + private String bureauDeptName; + + private String organId; + + private String organName; + + private String deptId; + + private String deptName; + + private String remark; + + private String outSerialNo; +} diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/vo/EfcodeCpPlanVo.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/vo/EfcodeCpPlanVo.java index ea47e041..b49ff0b9 100644 --- a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/vo/EfcodeCpPlanVo.java +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/vo/EfcodeCpPlanVo.java @@ -253,5 +253,16 @@ public class EfcodeCpPlanVo implements Serializable { @com.baomidou.mybatisplus.annotation.TableField(exist = false) private java.util.List reserves; + /** + * 渠道编号 0|市局,1|龙岗,2|龙华 + */ + @ExcelProperty(value = "渠道编号") + private String channelNo; + + /** + * 外部流水号 + */ + @ExcelProperty(value = "外部流水号") + private String outSerialNo; } diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/vo/externalInterface/LongGangCreateTaskVo.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/vo/externalInterface/LongGangCreateTaskVo.java new file mode 100644 index 00000000..a22fffcd --- /dev/null +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/vo/externalInterface/LongGangCreateTaskVo.java @@ -0,0 +1,19 @@ +package com.zdxt.enforcementcode.domain.vo.externalInterface; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +@Data +public class LongGangCreateTaskVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private String planId; + + private String taskId; + + private String outSerialNo; +} diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/handler/ApiCryptoFilter.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/handler/ApiCryptoFilter.java new file mode 100644 index 00000000..be454333 --- /dev/null +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/handler/ApiCryptoFilter.java @@ -0,0 +1,85 @@ +package com.zdxt.enforcementcode.handler; + +import cn.hutool.core.util.StrUtil; +import com.zdxt.enforcementcode.common.util.api.ApiSignUtil; +import com.zdxt.enforcementcode.config.ApiPlatformConfig; +import com.zdxt.enforcementcode.domain.SysApiPlatformConfigEntity; +import com.zdxt.enforcementcode.service.ISysApiPlatformConfigService; +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.constant.HttpStatus; +import org.dromara.common.core.domain.R; +import org.dromara.common.json.utils.JsonUtils; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Slf4j +@Component +@Order(FilterRegistrationBean.HIGHEST_PRECEDENCE + 10) +@RequiredArgsConstructor +public class ApiCryptoFilter implements Filter { + + private final ApiPlatformConfig apiPlatformConfig; + private final ISysApiPlatformConfigService sysApiPlatformConfigService; + + private static final String HEADER_ENCRYPTED = "X-Encrypted"; + private static final String HEADER_APP_KEY = "X-App-Key"; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + if (!apiPlatformConfig.isEnabled()) { + chain.doFilter(request, response); + return; + } + + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + + String appKey = httpRequest.getHeader(HEADER_APP_KEY); + String encryptedFlag = httpRequest.getHeader(HEADER_ENCRYPTED); + + if (StrUtil.isBlank(appKey) || !"true".equals(encryptedFlag)) { + chain.doFilter(request, response); + return; + } + + SysApiPlatformConfigEntity consumer = sysApiPlatformConfigService.findByAppKey(appKey).orElse(null); + + if (consumer == null || !consumer.getEncryptEnabled()) { + chain.doFilter(request, response); + return; + } + + ApiDecryptRequestWrapper decryptWrapper = new ApiDecryptRequestWrapper( + httpRequest, consumer.getPlatformPrivateKey()); + + ApiEncryptResponseWrapper encryptWrapper = new ApiEncryptResponseWrapper(httpResponse); + + try { + chain.doFilter(decryptWrapper, encryptWrapper); + } catch (Exception e) { + log.error("API加解密处理异常", e); + httpResponse.reset(); + httpResponse.setStatus(HttpStatus.SUCCESS); + httpResponse.setContentType("application/json;charset=UTF-8"); + httpResponse.getWriter().write(JsonUtils.toJsonString(R.fail("数据处理异常"))); + return; + } + + String responseBody = encryptWrapper.getContent(); + String encryptedResponse = ApiSignUtil.rsaEncrypt(responseBody, consumer.getCallerPublicKey()); + + httpResponse.reset(); + httpResponse.setContentType("application/json;charset=UTF-8"); + httpResponse.setHeader(HEADER_ENCRYPTED, "true"); + httpResponse.getWriter().write(encryptedResponse); + } +} diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/handler/ApiDecryptRequestWrapper.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/handler/ApiDecryptRequestWrapper.java new file mode 100644 index 00000000..78fc8126 --- /dev/null +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/handler/ApiDecryptRequestWrapper.java @@ -0,0 +1,67 @@ +package com.zdxt.enforcementcode.handler; + +import cn.hutool.core.io.IoUtil; +import com.zdxt.enforcementcode.common.util.api.ApiSignUtil; +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import org.springframework.http.MediaType; + +import java.io.*; +import java.nio.charset.StandardCharsets; + +public class ApiDecryptRequestWrapper extends HttpServletRequestWrapper { + + private final byte[] body; + + public ApiDecryptRequestWrapper(HttpServletRequest request, String platformPrivateKey) throws IOException { + super(request); + byte[] rawBytes = IoUtil.readBytes(request.getInputStream(), false); + String rawBody = new String(rawBytes, StandardCharsets.UTF_8); + + if (rawBody.startsWith("{") || rawBody.startsWith("[")) { + this.body = rawBytes; + } else { + String decrypted = ApiSignUtil.rsaDecrypt(rawBody, platformPrivateKey); + this.body = decrypted.getBytes(StandardCharsets.UTF_8); + } + } + + @Override + public BufferedReader getReader() { + return new BufferedReader(new InputStreamReader(getInputStream())); + } + + @Override + public int getContentLength() { + return body.length; + } + + @Override + public long getContentLengthLong() { + return body.length; + } + + @Override + public String getContentType() { + return MediaType.APPLICATION_JSON_VALUE; + } + + @Override + public ServletInputStream getInputStream() { + final ByteArrayInputStream bais = new ByteArrayInputStream(body); + return new ServletInputStream() { + @Override + public int read() { return bais.read(); } + @Override + public int available() { return body.length; } + @Override + public boolean isFinished() { return false; } + @Override + public boolean isReady() { return true; } + @Override + public void setReadListener(ReadListener readListener) { } + }; + } +} diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/handler/ApiEncryptResponseWrapper.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/handler/ApiEncryptResponseWrapper.java new file mode 100644 index 00000000..bd790c4a --- /dev/null +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/handler/ApiEncryptResponseWrapper.java @@ -0,0 +1,51 @@ +package com.zdxt.enforcementcode.handler; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; + +import java.io.*; + +public class ApiEncryptResponseWrapper extends HttpServletResponseWrapper { + + private final ByteArrayOutputStream byteArrayOutputStream; + private final ServletOutputStream servletOutputStream; + private final PrintWriter printWriter; + + public ApiEncryptResponseWrapper(HttpServletResponse response) throws IOException { + super(response); + this.byteArrayOutputStream = new ByteArrayOutputStream(); + this.servletOutputStream = new ServletOutputStream() { + @Override + public boolean isReady() { return true; } + @Override + public void setWriteListener(WriteListener writeListener) { } + @Override + public void write(int b) throws IOException { byteArrayOutputStream.write(b); } + @Override + public void write(byte[] b, int off, int len) throws IOException { byteArrayOutputStream.write(b, off, len); } + }; + this.printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream)); + } + + @Override + public PrintWriter getWriter() { return printWriter; } + + @Override + public ServletOutputStream getOutputStream() { return servletOutputStream; } + + @Override + public void flushBuffer() throws IOException { + servletOutputStream.flush(); + printWriter.flush(); + } + + @Override + public void reset() { byteArrayOutputStream.reset(); } + + public String getContent() throws IOException { + flushBuffer(); + return byteArrayOutputStream.toString(); + } +} diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/handler/ApiSignInterceptor.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/handler/ApiSignInterceptor.java new file mode 100644 index 00000000..3b83982e --- /dev/null +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/handler/ApiSignInterceptor.java @@ -0,0 +1,116 @@ +package com.zdxt.enforcementcode.handler; + +import cn.hutool.core.util.StrUtil; +import com.zdxt.enforcementcode.common.util.api.ApiSignUtil; +import com.zdxt.enforcementcode.config.ApiPlatformConfig; +import com.zdxt.enforcementcode.domain.SysApiPlatformConfigEntity; +import com.zdxt.enforcementcode.service.ISysApiPlatformConfigService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.constant.HttpStatus; +import org.dromara.common.core.domain.R; +import org.dromara.common.json.utils.JsonUtils; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ApiSignInterceptor implements HandlerInterceptor { + + private final ApiPlatformConfig apiPlatformConfig; + private final ISysApiPlatformConfigService sysApiPlatformConfigService; + + private static final String HEADER_APP_KEY = "X-App-Key"; + private static final String HEADER_TIMESTAMP = "X-Timestamp"; + private static final String HEADER_NONCE = "X-Nonce"; + private static final String HEADER_SIGN = "X-Sign"; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + if (!apiPlatformConfig.isEnabled()) { + return true; + } + + String appKey = request.getHeader(HEADER_APP_KEY); + String timestamp = request.getHeader(HEADER_TIMESTAMP); + String nonce = request.getHeader(HEADER_NONCE); + String sign = request.getHeader(HEADER_SIGN); + + if (StrUtil.hasBlank(appKey, timestamp, nonce, sign)) { + writeError(response, "缺少必要的签名参数: X-App-Key, X-Timestamp, X-Nonce, X-Sign"); + return false; + } + + SysApiPlatformConfigEntity consumer = sysApiPlatformConfigService.findByAppKey(appKey).orElse(null); + + if (consumer == null) { + writeError(response, "无效的 appKey"); + return false; + } + + if (!consumer.getSignEnabled()) { + return true; + } + + long ts; + try { + ts = Long.parseLong(timestamp); + } catch (NumberFormatException e) { + writeError(response, "timestamp 格式错误"); + return false; + } + + long now = System.currentTimeMillis() / 1000; + if (Math.abs(now - ts) > apiPlatformConfig.getTimestampExpireSeconds()) { + writeError(response, "请求已过期,timestamp 超时"); + return false; + } + + Map signParams = new HashMap<>(); + signParams.put("appKey", appKey); + signParams.put("timestamp", timestamp); + signParams.put("nonce", nonce); + + String queryString = request.getQueryString(); + if (StrUtil.isNotBlank(queryString)) { + for (String param : queryString.split("&")) { + String[] kv = param.split("=", 2); + if (kv.length == 2) { + signParams.put(kv[0], kv[1]); + } + } + } + + String signContent = ApiSignUtil.buildSignContent(signParams); + log.debug("签名原文: {}", signContent); + + boolean verified = ApiSignUtil.verify(signContent, sign, consumer.getAppSecret()); + if (!verified) { + try { + verified = ApiSignUtil.verify(signContent, sign, consumer.getCallerPublicKey()); + } catch (Exception ignored) { + } + } + + if (!verified) { + writeError(response, "签名验证失败"); + return false; + } + + log.info("API签名验证通过, appKey={}, path={}", appKey, request.getRequestURI()); + return true; + } + + private void writeError(HttpServletResponse response, String message) throws IOException { + response.setStatus(HttpStatus.FORBIDDEN); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write(JsonUtils.toJsonString(R.fail(message))); + } +} diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/mapper/SysApiPlatformConfigMapper.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/mapper/SysApiPlatformConfigMapper.java new file mode 100644 index 00000000..c0c50bfc --- /dev/null +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/mapper/SysApiPlatformConfigMapper.java @@ -0,0 +1,8 @@ +package com.zdxt.enforcementcode.mapper; + +import com.zdxt.enforcementcode.domain.SysApiPlatformConfigEntity; +import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; + +public interface SysApiPlatformConfigMapper extends BaseMapperPlus { + +} diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/ISysApiPlatformConfigService.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/ISysApiPlatformConfigService.java new file mode 100644 index 00000000..913c2519 --- /dev/null +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/ISysApiPlatformConfigService.java @@ -0,0 +1,12 @@ +package com.zdxt.enforcementcode.service; + +import com.zdxt.enforcementcode.domain.SysApiPlatformConfigEntity; + +import java.util.Optional; + +public interface ISysApiPlatformConfigService { + + void refreshCache(); + + Optional findByAppKey(String appKey); +} diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpPlanServiceImpl.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpPlanServiceImpl.java index 6730057c..cf8c853b 100644 --- a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpPlanServiceImpl.java +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpPlanServiceImpl.java @@ -329,6 +329,8 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService { lqw.like(StringUtils.isNotBlank(bo.getDeptName()), EfcodeCpPlan::getDeptName, bo.getDeptName()); lqw.eq(bo.getIsDelete() != null, EfcodeCpPlan::getIsDelete, bo.getIsDelete()); lqw.eq(StringUtils.isNotBlank(bo.getMyRemark()), EfcodeCpPlan::getMyRemark, bo.getMyRemark()); + lqw.eq(StringUtils.isNotBlank(bo.getChannelNo()), EfcodeCpPlan::getChannelNo, bo.getChannelNo()); + lqw.eq(StringUtils.isNotBlank(bo.getOutSerialNo()), EfcodeCpPlan::getOutSerialNo, bo.getOutSerialNo()); return lqw; } @@ -790,6 +792,7 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService { lqw.eq(EfcodeCpPlan::getIsPublic, 1L); lqw.eq(EfcodeCpPlan::getIsUnion, 1L); lqw.eq(EfcodeCpPlan::getIsDelete, 0L); + lqw.eq(EfcodeCpPlan::getChannelNo, "0"); lqw.isNotNull(EfcodeCpPlan::getCreateBy); lqw.orderByAsc(EfcodeCpPlan::getPlanDate); List pendingPlans = baseMapper.selectList(lqw); diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpReserveServiceImpl.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpReserveServiceImpl.java index 82c461c0..52a70afd 100644 --- a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpReserveServiceImpl.java +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpReserveServiceImpl.java @@ -200,6 +200,9 @@ public class EfcodeCpReserveServiceImpl implements IEfcodeCpReserveService { if (targetPlan == null) { return R.fail("目标计划不存在"); } + if (!"0".equals(targetPlan.getChannelNo())) {//只有市司法局的计划支持预约 + return R.fail("该渠道的计划不支持预约"); + } // 1.1 校验目标计划状态:只有待预约(1)、已取消(4)、预约中可加入(5)的计划才允许预约 Long reserveStatus = targetPlan.getReserveStatus(); if (reserveStatus == null || !Arrays.asList(1L, 4L, 5L).contains(reserveStatus)) { diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/SysApiPlatformConfigServiceImpl.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/SysApiPlatformConfigServiceImpl.java new file mode 100644 index 00000000..0e825cb3 --- /dev/null +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/SysApiPlatformConfigServiceImpl.java @@ -0,0 +1,48 @@ +package com.zdxt.enforcementcode.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.zdxt.enforcementcode.domain.SysApiPlatformConfigEntity; +import com.zdxt.enforcementcode.mapper.SysApiPlatformConfigMapper; +import com.zdxt.enforcementcode.service.ISysApiPlatformConfigService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import jakarta.annotation.PostConstruct; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SysApiPlatformConfigServiceImpl implements ISysApiPlatformConfigService{ + + private final SysApiPlatformConfigMapper sysApiPlatformConfigMapper; + + private final Map consumerCache = new ConcurrentHashMap<>(); + + @PostConstruct + public void init() { + refreshCache(); + } + + public void refreshCache() { + List list = sysApiPlatformConfigMapper.selectList( + new LambdaQueryWrapper() + .eq(SysApiPlatformConfigEntity::getStatus, "0") + ); + Map newMap = new ConcurrentHashMap<>(); + for (SysApiPlatformConfigEntity entity : list) { + newMap.put(entity.getAppKey(), entity); + } + consumerCache.clear(); + consumerCache.putAll(newMap); + log.info("API接入配置缓存刷新, 共加载{}个渠道", newMap.size()); + } + + public Optional findByAppKey(String appKey) { + return Optional.ofNullable(consumerCache.get(appKey)); + } +} diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/resources/mapper/enforcementcode/EfcodeCpPlanMapper.xml b/zdxt-modules/zdxt-enforcement-code/src/main/resources/mapper/enforcementcode/EfcodeCpPlanMapper.xml index c28c590e..f45de5f2 100644 --- a/zdxt-modules/zdxt-enforcement-code/src/main/resources/mapper/enforcementcode/EfcodeCpPlanMapper.xml +++ b/zdxt-modules/zdxt-enforcement-code/src/main/resources/mapper/enforcementcode/EfcodeCpPlanMapper.xml @@ -20,6 +20,7 @@ reserve_status in (1,4,5) AND is_delete = 0 + AND channel_no = '0' AND create_by != #{currentUserId} @@ -67,12 +68,13 @@ reserve_status, system_reserved, manual_reserved, bureau_dept_id, bureau_dept_name, organ_id, organ_name, dept_id, dept_name, - is_delete, remark, my_remark, + is_delete, remark, my_remark, channel_no, out_serial_no, create_by, create_dept, create_time, update_by, update_time FROM efcode_cp_plan cp cp.is_delete = 0 and cp.reserve_status!=2 + AND cp.channel_no = '0' AND cp.create_by != #{currentUserId} @@ -113,6 +115,12 @@ AND cp.enforce_content LIKE CONCAT('%', #{bo.enforceContent}, '%') + + AND cp.channel_no = #{bo.channelNo} + + + AND cp.out_serial_no = #{bo.outSerialNo} + ORDER BY cp.plan_date DESC, cp.plan_time DESC diff --git a/zdxt-web-server/zdxt-admin-web-server/src/main/resources/application-dev.yml b/zdxt-web-server/zdxt-admin-web-server/src/main/resources/application-dev.yml index a3280f98..ea160d58 100644 --- a/zdxt-web-server/zdxt-admin-web-server/src/main/resources/application-dev.yml +++ b/zdxt-web-server/zdxt-admin-web-server/src/main/resources/application-dev.yml @@ -109,7 +109,7 @@ spring: spring.data: redis: # 地址 - host: 139.186.148.21 + host: 146.56.223.227 # host: 172.16.20.72 # 端口,默认为6379 port: 6379 diff --git a/zdxt-web-server/zdxt-admin-web-server/src/main/resources/application.yml b/zdxt-web-server/zdxt-admin-web-server/src/main/resources/application.yml index 72bd5e56..f472b317 100644 --- a/zdxt-web-server/zdxt-admin-web-server/src/main/resources/application.yml +++ b/zdxt-web-server/zdxt-admin-web-server/src/main/resources/application.yml @@ -86,7 +86,7 @@ spring: # 国际化资源文件路径 basename: i18n/messages profiles: - active: prod + active: dev # 文件上传 servlet: multipart: @@ -335,3 +335,20 @@ http: connect-timeout: 10000 # 连接超时:5秒 read-timeout: 10000 # 读取超时:30秒 write-timeout: 10000 # 写入超时:30秒 + +# 外部接口平台配置 +api-platform: + enabled: true + timestamp-expire-seconds: 300 + consumers: + partner-a: + app-key: ak_partner_a_xxxx + app-secret: sk_partner_a_xxxx + platform-public-key: MIGfMA0GCSq... + platform-private-key: MIICdQIBADANBg... + caller-public-key: MIGfMA0GCSq... + sign-enabled: true + encrypt-enabled: true + allowed-paths: + - /api/v1/data/** + - /api/v1/query/**