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

发布于:2025-07-14 ⋅ 阅读:(19) ⋅ 点赞:(0)

配置实例:

摘要:

由于中金提供的初始化支付环境的是以文件的形式,在初始化的时候需要读取文件处理。我们将文件里的参数灵活的运用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 directory
            File 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 exists
                                        File 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 content
                                        try (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>() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    Files.delete(file);
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public 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;

    @Override
    public 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的方式配置管理的方式动态管理服务商支付参数了。

总结:

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


网站公告

今日签到

点亮在社区的每一天
去签到