当前位置: 首页 > 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.xdnf.cn/news/1114561.html

相关文章:

  • HarmonyOS-ArkUI Web控件基础铺垫1-HTTP协议-数据包内容
  • Docker三剑客
  • AWS Lambda Container 方式部署 Flask 应用并通过 API Gateway 提供访问
  • 手写std::optional:告别空指针的痛苦
  • 系规备考论文:论IT服务知识管理
  • 010_学习资源与社区支持
  • C语言基础教程(002):变量介绍
  • Spring Boot 配置注解处理器 - spring-boot-configuration-processor
  • 初识计算机网络
  • Node.js 聊天内容加密解密实战教程(含缓存密钥优化)
  • python 列表(List) vs. 元组(Tuple):什么时候该用不可变的元组?它们在性能和用途上有什么区别?
  • C++使用Thread实现子线程延时重发
  • 语言模型常用的激活函数(Sigmoid ,GeLU ,SwiGLU,GLU,SiLU,Swish)
  • 【论文阅读】基于注意力机制的冥想脑电分类识别研究(2025)
  • LeetCode第 458 场周赛题解
  • 字符串问题(哈希表解决)
  • 【论文阅读】Think Only When You Need with Large Hybrid-Reasoning Models
  • 【源力觉醒 创作者计划】文心开源大模型ERNIE-4.5私有化部署保姆级教程与多功能界面窗口部署
  • 编译器优化——LLVM IR,零基础入门
  • 我做了一个windows端口占用查看跟释放工具
  • Spring AI 项目实战(十六):Spring + AI + 通义万相图像生成工具全栈项目实战(附完整源码)
  • linux-shell脚本
  • SpringCloud云间剑歌 第四章:藏经阁与信鸽传书
  • 打造你的专属智能生活:鸿蒙系统自定义场景开发全流程详解
  • package.json 与 package-lock.json
  • Redis缓存设计与性能优化指南
  • Web攻防-PHP反序列化原生内置类Exception类SoapClient类SimpleXMLElement
  • 分类问题-机器学习
  • 011_视觉能力与图像处理
  • 力扣面试150题--单词搜索