Prechádzať zdrojové kódy

商户的支付证书存储到指定路径中

zhangchen 1 mesiac pred
rodič
commit
d1e74a0d0a

+ 25 - 2
alien-store/src/main/java/shop/alien/store/controller/StorePaymentConfigController.java

@@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.StorePaymentConfig;
+import shop.alien.store.service.StorePaymentConfigCertService;
 import shop.alien.store.service.StorePaymentConfigService;
 import shop.alien.util.common.JwtUtil;
 
@@ -35,6 +36,7 @@ import java.util.List;
 public class StorePaymentConfigController {
 
     private final StorePaymentConfigService storePaymentConfigService;
+    private final StorePaymentConfigCertService storePaymentConfigCertService;
 
     @ApiOperationSupport(order = 1)
     @ApiOperation("新增支付配置(JSON  body)")
@@ -302,6 +304,27 @@ public class StorePaymentConfigController {
     }
 
     @ApiOperationSupport(order = 6)
+    @ApiOperation(value = "按店铺ID刷新证书到本地目录", notes = "先删除该店铺证书目录下原有文件,再根据 store_payment_config 重新写入并更新各证书路径字段")
+    @ApiImplicitParam(name = "storeId", value = "店铺ID", dataType = "int", paramType = "query", required = true)
+    @PostMapping("/refreshCertByStoreId")
+    public R<List<String>> refreshCertByStoreId(@RequestParam Integer storeId) {
+        log.info("StorePaymentConfigController.refreshCertByStoreId storeId={}", storeId);
+        if (storeId == null) {
+            return R.fail("店铺ID不能为空");
+        }
+        try {
+            List<String> paths = storePaymentConfigCertService.refreshCertByStoreId(storeId);
+            if (paths.isEmpty()) {
+                return R.fail("该店铺无支付配置或无可写入的证书内容");
+            }
+            return R.data(paths);
+        } catch (Exception e) {
+            log.error("刷新店铺证书失败 storeId={}", storeId, e);
+            return R.fail("刷新失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 7)
     @ApiOperation("根据店铺ID查询支付配置")
     @ApiImplicitParams({
             @ApiImplicitParam(name = "storeId", value = "店铺ID", dataType = "Integer", paramType = "query", required = true)
@@ -324,7 +347,7 @@ public class StorePaymentConfigController {
         }
     }
 
-    @ApiOperationSupport(order = 7)
+    @ApiOperationSupport(order = 8)
     @ApiOperation("分页查询支付配置列表")
     @ApiImplicitParams({
             @ApiImplicitParam(name = "page", value = "页码", dataType = "Integer", paramType = "query", defaultValue = "1"),
@@ -349,7 +372,7 @@ public class StorePaymentConfigController {
         }
     }
 
-    @ApiOperationSupport(order = 8)
+    @ApiOperationSupport(order = 9)
     @ApiOperation(value = "新增/更新微信支付配置信息", notes = "按 storeId 查找配置,存在则更新微信字段;不存在则新建一条仅含 storeId 与微信信息的配置。可传表单+两个证书文件(内容存入表)。")
     @ApiImplicitParams({
             @ApiImplicitParam(name = "storeId", value = "店铺ID", dataType = "int", paramType = "form", required = true),

+ 21 - 0
alien-store/src/main/java/shop/alien/store/service/StorePaymentConfigCertService.java

@@ -0,0 +1,21 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.store.StorePaymentConfig;
+
+import java.util.List;
+
+/**
+ * 店铺支付证书落盘服务:按店铺ID将 store_payment_config 中的证书写入本地目录(路径按 storeId + storeTel 区分),并回写路径到配置表。
+ *
+ * @author system
+ */
+public interface StorePaymentConfigCertService {
+
+    /**
+     * 按店铺ID刷新证书到本地目录:先删除该店铺证书目录下原有文件,再根据当前配置重新写入并更新路径。
+     *
+     * @param storeId 店铺ID
+     * @return 本次写入的证书完整路径列表;若配置不存在或无证书内容则返回空列表
+     */
+    List<String> refreshCertByStoreId(Integer storeId);
+}

+ 169 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StorePaymentConfigCertServiceImpl.java

@@ -0,0 +1,169 @@
+package shop.alien.store.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import shop.alien.entity.store.StoreInfo;
+import shop.alien.entity.store.StorePaymentConfig;
+import shop.alien.store.service.StoreInfoService;
+import shop.alien.store.service.StorePaymentConfigCertService;
+import shop.alien.store.service.StorePaymentConfigService;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.stream.Stream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/**
+ * 店铺支付证书落盘服务实现:删除店铺证书目录下原有文件后重新写入,并将完整证书路径回写配置表。
+ *
+ * @author system
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class StorePaymentConfigCertServiceImpl implements StorePaymentConfigCertService {
+
+    private static final Pattern NON_ALNUM = Pattern.compile("[^0-9a-zA-Z]");
+
+    @Value("${payment.cert.base-path:${user.dir}/payment-certs}")
+    private String certBasePath;
+
+    private final StorePaymentConfigService storePaymentConfigService;
+    private final StoreInfoService storeInfoService;
+
+    @Override
+    public List<String> refreshCertByStoreId(Integer storeId) {
+        if (storeId == null) {
+            return new ArrayList<>();
+        }
+        StorePaymentConfig config = storePaymentConfigService.getByStoreId(storeId);
+        if (config == null) {
+            log.warn("店铺支付配置不存在 storeId={}", storeId);
+            return new ArrayList<>();
+        }
+        try {
+            return writeCertFilesForConfigWithClean(config);
+        } catch (Exception e) {
+            log.error("店铺证书落盘失败 storeId={}", storeId, e);
+            throw new RuntimeException("证书落盘失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 先清空该店铺证书目录(删除目录下所有文件及目录本身再重建),再写入证书并回写路径。
+     */
+    private List<String> writeCertFilesForConfigWithClean(StorePaymentConfig config) throws Exception {
+        Integer storeId = config.getStoreId();
+        if (storeId == null) {
+            return new ArrayList<>();
+        }
+        String storeTel = null;
+        try {
+            StoreInfo store = storeInfoService.getById(storeId);
+            if (store != null && StringUtils.hasText(store.getStoreTel())) {
+                storeTel = store.getStoreTel();
+            }
+        } catch (Exception e) {
+            log.debug("获取店铺手机号失败 storeId={}", storeId, e);
+        }
+        String dirSegment = sanitizeDirSegment(storeTel);
+        String storeDirName = storeId + "_" + dirSegment;
+        Path baseDir = Paths.get(certBasePath).resolve(storeDirName);
+
+        // 删除店铺证书目录下原有内容:若目录存在则删除后重建
+        if (Files.exists(baseDir)) {
+            deleteDirectoryRecursively(baseDir);
+        }
+        Files.createDirectories(baseDir);
+
+        List<String> writtenPaths = new ArrayList<>();
+
+        // 支付宝:应用公钥、支付宝公钥、根证书
+        writeIfPresent(baseDir, config.getAppPublicCert(),
+                defaultName(config.getAppPublicCertName(), "app_public_cert.crt"),
+                (path, name) -> {
+                    config.setAppPublicCertPath(path);
+                    config.setAppPublicCertName(name);
+                }, writtenPaths);
+        writeIfPresent(baseDir, config.getAlipayPublicCert(),
+                defaultName(config.getAlipayPublicCertName(), "alipay_public_cert.crt"),
+                (path, name) -> {
+                    config.setAlipayPublicCertPath(path);
+                    config.setAlipayPublicCertName(name);
+                }, writtenPaths);
+        writeIfPresent(baseDir, config.getAlipayRootCert(),
+                defaultName(config.getAlipayRootCertName(), "alipay_root_cert.crt"),
+                (path, name) -> {
+                    config.setAlipayRootCertPath(path);
+                    config.setAlipayRootCertName(name);
+                }, writtenPaths);
+
+        // 微信:商户私钥、微信公钥
+        writeIfPresent(baseDir, config.getWechatPrivateKeyFile(),
+                defaultName(config.getWechatPrivateKeyName(), "wechat_private_key.pem"),
+                (path, name) -> {
+                    config.setWechatPrivateKeyPath(path);
+                    config.setWechatPrivateKeyName(name);
+                }, writtenPaths);
+        writeIfPresent(baseDir, config.getWechatPayPublicKeyFile(),
+                defaultName(config.getWechatPayPublicKeyFileName(), "wechat_public_key.pem"),
+                (path, name) -> {
+                    config.setWechatPayPublicKeyFilePath(path);
+                    config.setWechatPayPublicKeyFileName(name);
+                }, writtenPaths);
+
+        storePaymentConfigService.updateById(config);
+        log.info("店铺支付证书已刷新落盘 storeId={}, dir={}, 写入文件数={}", storeId, baseDir.toAbsolutePath(), writtenPaths.size());
+        return writtenPaths;
+    }
+
+    private static void deleteDirectoryRecursively(Path path) throws Exception {
+        if (!Files.exists(path)) {
+            return;
+        }
+        if (Files.isDirectory(path)) {
+            try (Stream<Path> stream = Files.list(path)) {
+                for (Path entry : stream.collect(Collectors.toList())) {
+                    deleteDirectoryRecursively(entry);
+                }
+            }
+        }
+        Files.delete(path);
+    }
+
+    private static String sanitizeDirSegment(String storeTel) {
+        if (!StringUtils.hasText(storeTel)) {
+            return "no_tel";
+        }
+        return NON_ALNUM.matcher(storeTel.trim()).replaceAll("_");
+    }
+
+    private static String defaultName(String name, String defaultName) {
+        return StringUtils.hasText(name) ? name.trim() : defaultName;
+    }
+
+    private interface PathUpdater {
+        void set(String certFilePath, String fileName);
+    }
+
+    private void writeIfPresent(Path baseDir, String content, String fileName, PathUpdater updater, List<String> writtenPaths) throws Exception {
+        if (!StringUtils.hasText(content)) {
+            return;
+        }
+        Path file = baseDir.resolve(fileName);
+        Files.write(file, content.getBytes(StandardCharsets.UTF_8));
+        String certFilePath = file.toAbsolutePath().toString();
+        writtenPaths.add(certFilePath);
+        if (updater != null) {
+            updater.set(certFilePath, fileName);
+        }
+    }
+}