当前位置: 首页 > news >正文

jeepay开源项目开发中金支付如何像其他支付渠道对接那样简单集成,集成服务商模式,极简集成工具。

配置实例:

摘要:

由于中金提供的初始化支付环境的是以文件的形式,在初始化的时候需要读取文件处理。我们将文件里的参数灵活的运用jeepay的参数话方式配置的形式处理,这样渠道配置与其他支付渠道的开发配置模式一致。

引入包:

在项目根目录件一个libs,将提供的jar引入到项目中,在jeepay-payment的模块pom.xml引入

       <dependency><groupId>com.cpcn</groupId><artifactId>logback</artifactId><version>4.1.1.0</version><scope>system</scope><systemPath>${project.basedir}/../libs/logback-4.1.1.0.jar</systemPath></dependency><dependency><groupId>com.cpcn</groupId><artifactId>commons-codec</artifactId><version>1.11</version><scope>system</scope><systemPath>${project.basedir}/../libs/commons-codec-1.11.jar</systemPath></dependency><dependency><groupId>com.cpcn</groupId><artifactId>cpcn-payment-api</artifactId><version>2.6.2.1</version><scope>system</scope><systemPath>${project.basedir}/../libs/cpcn-payment-api-2.6.2.1.jar</systemPath></dependency>

配置文件:

以这样的方式创建两个文件夹,将支付配置文件导入到不同文件夹,将配置文件放到这俩个文件之下。

管理配置文件的处理类:

import cn.hutool.core.io.FileUtil;
import com.jeequan.jeepay.core.model.params.cpnc.CpncIsvParams;
import cpcn.institution.tools.util.Base64;
import cpcn.institution.tools.util.DigitalEnvelopeUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.core.io.Resource;
import payment.api.system.PaymentEnvironment;import java.io.*;
import java.net.JarURLConnection;
import java.net.URL;
import java.nio.channels.FileChannel;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.jar.JarFile;/*** 中金支付环境初始化工具类* 用于初始化中金支付的环境配置*/
@Slf4j
public class CpcnEnvUtil {public static String generateFilePath(Resource resource,String payConfigPath) throws IOException {File tmpDir = FileUtil.getTmpDir();URL resourceUrl = resource.getURL();if ("jar".equals(resourceUrl.getProtocol())) {log.info("Extracting resources from JAR to temporary directory...");// Create target directoryFile targetDir = new File(tmpDir, payConfigPath);if (!targetDir.exists() && !targetDir.mkdirs()) {throw new IOException("Failed to create target directory: " + targetDir.getAbsolutePath());}try {JarURLConnection jarConnection = (JarURLConnection) resourceUrl.openConnection();try (JarFile jarFile = jarConnection.getJarFile()) {String resourcePath = jarConnection.getEntryName();// Normalize the resource path to ensure it ends with '/'String basePath = resourcePath.endsWith("/") ? resourcePath : resourcePath + "/";jarFile.stream().filter(entry -> entry.getName().startsWith(basePath) && !entry.getName().equals(basePath)).forEach(entry -> {String relativePath = entry.getName().substring(basePath.length());File destFile = new File(targetDir, relativePath);try {if (entry.isDirectory()) {if (!destFile.exists() && !destFile.mkdirs()) {log.warn("Directory creation failed: {}", destFile.getAbsolutePath());}} else {// Ensure parent directory existsFile parent = destFile.getParentFile();if (parent != null && !parent.exists() && !parent.mkdirs()) {log.warn("Parent directory creation failed: {}", parent.getAbsolutePath());return;}log.info("复制的文件: {}", entry.getName());// Copy file contenttry (InputStream in = jarFile.getInputStream(entry);FileOutputStream out = new FileOutputStream(destFile)) {IOUtils.copy(in, out);out.getChannel().force(true);log.debug("Copied: {} -> {}", entry.getName(), destFile.getAbsolutePath());}}} catch (IOException e) {log.error("Failed to process JAR entry: {}", entry.getName(), e);}});}} catch (IOException e) {log.error("JAR processing failed", e);throw e;}} else {log.info("正在从文件系统中复制资源...");File file = resource.getFile();if (!file.isDirectory()) {log.error("资源路径不是一个目录: {}", file.getPath());throw new IOException("资源路径不是一个目录: " + file.getPath());}copyFile(file, tmpDir);}log.info("所有文件已成功复制到临时目录: {}", tmpDir.getPath());return tmpDir.getPath();}private static void copyFile(File file, File tmpDir) throws IOException {File[] files = file.listFiles();if (files != null) {for (File srcFile : files) {if (srcFile.isFile()) {File destFile = new File(tmpDir, srcFile.getName());try {FileUtil.copy(srcFile, destFile, false);FileChannel destChannel = FileChannel.open(destFile.toPath(),StandardOpenOption.WRITE);destChannel.force(true);destChannel.close();log.info("已复制文件: {} -> {}", srcFile.getName(), destFile.getPath());} catch (IOException e) {log.error("复制文件失败: {}", srcFile.getName(), e);throw e;}}}}}public static String initEnv(String baseUploadPath, String cnpcFileRootPath, CpncIsvParams cpncIsvParams) {String institutionID = cpncIsvParams.getInstitutionID();String keystore = cpncIsvParams.getKeystore();String keystorePassword = cpncIsvParams.getKeystorePassword();String certificate = cpncIsvParams.getCertificate();String trustJks = cpncIsvParams.getTrustJks();String trustJksPassword = cpncIsvParams.getTrustJksPassword();// 日志输入参数(密码脱敏)log.info("开始初始化中金支付环境,参数如下:");log.info("机构ID: {}", institutionID);log.info("基础上传路径: {}", baseUploadPath);log.info("目标根路径: {}", cnpcFileRootPath);log.info("密钥库文件: {}", keystore);log.info("密钥库密码: {}", maskPassword(keystorePassword));log.info("证书文件: {}", certificate);log.info("信任库文件: {}", trustJks);log.info("信任库密码: {}", maskPassword(trustJksPassword));try {// 清理并创建目标根目录(处理残留文件/目录冲突)Path targetDir = Paths.get(cnpcFileRootPath);cleanAndCreateDirectory(targetDir);// 复制密钥库文件log.info("开始复制密钥库文件...");copyFileWithSync(baseUploadPath, cnpcFileRootPath, "private/" + keystore, keystore);log.info("密钥库文件复制完成");// 复制证书文件log.info("开始复制证书文件...");copyFileWithSync(baseUploadPath, cnpcFileRootPath, "private/" + certificate, certificate);log.info("证书文件复制完成");// 复制信任库文件log.info("开始复制信任库文件...");copyFileWithSync(baseUploadPath, cnpcFileRootPath, "private/" + trustJks, trustJks);log.info("信任库文件复制完成");// 修改配置文件log.info("开始修改配置文件...");String iniFilePath = cnpcFileRootPath + "/common.ini";log.info("配置文件路径: {}", iniFilePath);modifyIniFile(iniFilePath, "my.keystore.filename", keystore.replace("cert/", ""));modifyIniFile(iniFilePath, "my.keystore.password", keystorePassword);modifyIniFile(iniFilePath, "payment.certificate.filename", certificate.replace("cert/", ""));modifyIniFile(iniFilePath, "trust.keystore.filename", trustJks.replace("cert/", ""));modifyIniFile(iniFilePath, "trust.keystore.password", trustJksPassword);log.info("配置文件修改完成");log.info("中金支付环境初始化成功,机构ID: {}", institutionID);} catch (IOException e) {log.error("中金支付环境初始化失败", e);throw new RuntimeException("中金支付环境初始化失败", e);}return institutionID;}/*** 清理并创建目录(若存在同名文件则删除,若为目录则保留),并强制刷盘确保元数据同步*/private static void cleanAndCreateDirectory(Path dirPath) throws IOException {log.info("处理目录: {}", dirPath.toAbsolutePath());if (Files.exists(dirPath)) {if (Files.isDirectory(dirPath)) {log.info("目录已存在: {}", dirPath);// 强制刷新父目录元数据(确保目录存在性被持久化)forceParentDirectorySync(dirPath);} else {// 存在同名文件,删除后创建目录log.warn("路径存在但为文件,删除后重建目录: {}", dirPath);Files.delete(dirPath);Files.createDirectories(dirPath);// 强制刷新父目录元数据forceParentDirectorySync(dirPath);}} else {// 目录不存在,直接创建Files.createDirectories(dirPath);log.info("目录创建成功: {}", dirPath);// 强制刷新父目录元数据forceParentDirectorySync(dirPath);}}/*** 强制刷新父目录的元数据到磁盘(确保目录操作被持久化)*/private static void forceParentDirectorySync(Path dirPath) throws IOException {Path parentDir = dirPath.getParent();if (parentDir == null) {// 若为根目录,无需刷新(根目录元数据由系统维护)log.debug("目录是根目录,无需刷新元数据: {}", dirPath);return;}// 通过打开父目录的文件通道,强制刷新元数据try (FileChannel channel = FileChannel.open(parentDir, StandardOpenOption.READ)) {// 强制刷新通道元数据(对目录而言,主要是确保子目录/文件的存在性被持久化)channel.force(true);log.debug("父目录元数据已强制刷盘: {}", parentDir);} catch (IOException e) {log.warn("刷新父目录元数据失败(不影响功能,但可能导致瞬时不一致): {}", parentDir, e);// 非致命错误,仅日志警告,不中断流程}}/*** 安全复制文件并确保数据同步到磁盘(处理目标路径冲突)*/private static void copyFileWithSync(String basePath, String targetPath, String sourceFile, String originalFileName) throws IOException {Path sourcePath = Paths.get(basePath, sourceFile);String targetFileName = originalFileName.replace("cert/", "");Path targetFilePath = Paths.get(targetPath, targetFileName);log.debug("准备复制文件: 源路径={}, 目标路径={}", sourcePath, targetFilePath);// 验证源文件存在if (!Files.exists(sourcePath) || !Files.isRegularFile(sourcePath)) {throw new FileNotFoundException("源文件不存在或不是文件: " + sourcePath);}// 处理目标父目录Path parentDir = targetFilePath.getParent();if (parentDir != null) {cleanAndCreateDirectory(parentDir);}// 处理目标文件(若为目录则删除)if (Files.exists(targetFilePath)) {if (Files.isDirectory(targetFilePath)) {log.warn("目标路径是目录,删除后重建文件: {}", targetFilePath);deleteDirectoryRecursively(targetFilePath);}// 若为文件则直接覆盖(后续输出流会覆盖)}// 复制文件并强制同步到磁盘try (FileInputStream fis = new FileInputStream(sourcePath.toFile());FileOutputStream fos = new FileOutputStream(targetFilePath.toFile()); // 覆盖模式FileChannel inChannel = fis.getChannel();FileChannel outChannel = fos.getChannel()) {inChannel.transferTo(0, inChannel.size(), outChannel);outChannel.force(true); // 强制刷新到磁盘log.debug("文件数据已同步到磁盘");}log.info("文件复制成功: {} -> {}", sourcePath, targetFilePath);}/*** 修改INI文件值并确保数据同步(处理文件路径冲突)*/private static void modifyIniFile(String iniFilePath, String key, String value) throws IOException {Path path = Paths.get(iniFilePath);log.debug("准备修改INI文件: {}, 键: {}, 值: {}", iniFilePath, key, maskIfPassword(key, value));// 处理目标路径(若为目录则删除)if (Files.exists(path)) {if (Files.isDirectory(path)) {log.warn("INI路径是目录,删除后重建文件: {}", path);deleteDirectoryRecursively(path);}}// 确保文件存在if (!Files.exists(path)) {log.debug("INI文件不存在,创建新文件: {}", path);Files.createFile(path);}// 修改INI值IniFileModifier.modifyIniValue(iniFilePath, key, value);// 强制同步到磁盘try (FileOutputStream fos = new FileOutputStream(path.toFile(), true);FileChannel channel = fos.getChannel()) {channel.force(true);}log.info("INI文件修改成功: {} = {} in {}", key, maskIfPassword(key, value), iniFilePath);}/*** 递归删除目录(用于清理误创建为目录的文件路径)*/private static void deleteDirectoryRecursively(Path dirPath) throws IOException {if (Files.isDirectory(dirPath)) {// 递归删除子文件和子目录Files.walkFileTree(dirPath, new SimpleFileVisitor<Path>() {@Overridepublic FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {Files.delete(file);return FileVisitResult.CONTINUE;}@Overridepublic FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {if (exc == null) {Files.delete(dir);return FileVisitResult.CONTINUE;} else {throw exc;}}});log.debug("目录已递归删除: {}", dirPath);}}/*** 响应消息验证与解密*/public static void verify(String[] respMsg) throws Exception {String plainText = "";try {log.info("开始对响应消息做对称解密...");if ("YES".equals(PaymentEnvironment.isDoubleCert)) {log.info("双证解密模式");respMsg[0] = DigitalEnvelopeUtil.doubleDecryptResponse(respMsg[0], respMsg[3], respMsg[5], respMsg[6]);} else {log.info("单证解密模式");respMsg[0] = DigitalEnvelopeUtil.decryptResponse(respMsg[0], respMsg[3], respMsg[5], respMsg[6]);}plainText = respMsg[0];respMsg[0] = Base64.encode(respMsg[0], "UTF-8");respMsg[0] = respMsg[0] + "," + respMsg[5] + "," + respMsg[4] + "," + respMsg[2];log.info("响应消息解密完成");log.info("响应原始报文:[" + plainText + "]");} catch (Exception e) {log.error("响应消息解密异常", e);throw new Exception("对称解密异常: " + e.getMessage(), e);}log.debug("[message]=[" + respMsg[0] + "]");log.debug("[signature]=[" + respMsg[1] + "]");log.debug("[plainText]=[" + plainText + "]");}/*** 密码脱敏处理*/private static String maskPassword(String password) {return (password == null || password.isEmpty()) ? "null/empty" : "******";}/*** 根据key判断是否为密码字段,若是则脱敏*/private static String maskIfPassword(String key, String value) {return (key.toLowerCase().contains("password")) ? maskPassword(value) : value;}// 假设IniFileModifier是已存在的INI文件处理工具类private static class IniFileModifier {public static void modifyIniValue(String filePath, String key, String value) throws IOException {// 实现INI文件修改逻辑(根据实际业务补充)// 例如:读取文件内容,替换key对应的value,再写回文件Path path = Paths.get(filePath);String content = new String(Files.readAllBytes(path), java.nio.charset.StandardCharsets.UTF_8);String newContent = content.replaceAll("(?i)^" + key + "=.*", key + "=" + value);if (newContent.equals(content) && !content.contains(key + "=")) {// 若key不存在则追加newContent += "\n" + key + "=" + value;}Files.write(path, newContent.getBytes(java.nio.charset.StandardCharsets.UTF_8));}}
}

使用工具:

使用上述提到的工具类,以下是我的示例代码:

/*** 初始化环境** @param cpncIsvParams 服务商参数*/public String initEnv(CpncIsvParams cpncIsvParams) throws Exception {String institutionID;boolean testEnvironment = isTestEnvironment();if (testEnvironment) {Resource resource = resolver.getResource("classpath:cpnc/dev");String payConfigPath = "cpnc/dev";String filePath = CpcnEnvUtil.generateFilePath(resource, payConfigPath);String configPath = filePath + File.separator + payConfigPath;log.info("中金测试配置所在目录:{}", configPath);institutionID = cpncIsvParams.getInstitutionID();PaymentEnvironment.initialize(configPath);} else {String payConfigPath = "cpnc/prod";Resource resource = resolver.getResource("classpath:cpnc/prod");String filePath = CpcnEnvUtil.generateFilePath(resource, payConfigPath);String configPath = filePath + File.separator + payConfigPath;log.info("中金生产配置所在目录:{}", configPath);institutionID = CpcnEnvUtil.initEnv(baseUploadPath, configPath, cpncIsvParams);PaymentEnvironment.initialize(configPath);}return institutionID;}

其中 CpncIsvParams 为服务商的配置类信息:

@Data
public class CpncIsvParams extends IsvParams {private String institutionID;private String keystore;private String keystorePassword;private String certificate;private String trustJks;private String trustJksPassword;@Overridepublic String deSenData() {CpncIsvParams params = this;params.setInstitutionID(this.institutionID);params.setKeystore(this.getKeystore());params.setKeystorePassword(this.keystorePassword);params.setCertificate(this.getCertificate());params.setTrustJks(this.getTrustJks());params.setTrustJksPassword(this.trustJksPassword);return ((JSONObject) JSON.toJSON(params)).toJSONString();}
}

通过上述的方式,我们就可以使用jeepay的方式配置管理的方式动态管理服务商支付参数了。

总结:

主要用到的技术手段这里用到的方式是将配置文件复制到临时文件夹,将必要的参数通过配置文件的参数替换的方式去试下

http://www.dtcms.com/a/276996.html

相关文章:

  • AI驱动的软件工程(中):文档驱动的编码与执行
  • 深入解析 ArrayList
  • XGBoost三部曲:XGBoost原理
  • Docker一键安装中间件(RocketMq、Nginx、MySql、Minio、Jenkins、Redis)脚步
  • Transformer 小记(一):深入理解 Transformer 中的位置关系
  • 【PTA数据结构 | C语言版】字符串截取子串操作
  • ABP VNext + 多级缓存架构:本地 + Redis + CDN
  • ref 和 reactive
  • EWSGAN:自动搜索高性能的GAN生成器架构
  • LeetCode 1156.单字符重复子串的最大长度
  • 维基艺术图片: 数据标注 (2)
  • C语言基础教程(002):变量介绍
  • 一文读懂现代卷积神经网络—使用块的网络(VGG)
  • 基于Prompt结构的语校解析:3H日本语学校信息建模实录(4/500)
  • 08.如何正确关闭文件
  • 数智管理学(三十三)
  • 归并排序递归法和非递归法的简单简单介绍
  • Gin框架统一响应与中间件机制学习笔记
  • DH(Denavit–Hartenberg)矩阵
  • KL散度:信息差异的量化标尺 | 从概率分布对齐到模型优化的核心度量
  • 使用QtTest
  • 反激变换器设计全流程(一)——电路拓扑及工作流程
  • Chrome v109.0.5414.168 绿色便携版 Windows 7/2012R2 最终版 下载
  • 开发语言的优劣势对比及主要应用领域分析
  • GROW领导力模型
  • Unity物理系统由浅入深第四节:物理约束求解与稳定性
  • 【算法分析与设计】研究生第一次算法作业latex源码+pdf
  • docker容器高级管理-dockerfile创建镜像
  • 飞算 JavaAI 智能编程助手:颠覆编程旧模式,重构开发生态
  • Java小白-线程 vs 虚拟线程,Java并发的新旧对决