diff --git a/cash-sdk/aggregation-pay/src/main/java/com/czg/EntryManager.java b/cash-sdk/aggregation-pay/src/main/java/com/czg/EntryManager.java index b988f1abb..b8bb1dc50 100644 --- a/cash-sdk/aggregation-pay/src/main/java/com/czg/EntryManager.java +++ b/cash-sdk/aggregation-pay/src/main/java/com/czg/EntryManager.java @@ -3,12 +3,16 @@ package com.czg; import cn.hutool.core.util.StrUtil; import com.czg.dto.req.*; import com.czg.dto.resp.BankBranchDto; +import com.czg.dto.resp.EntryRespDto; import com.czg.exception.CzgException; import com.czg.third.alipay.AlipayEntryManager; import com.czg.third.wechat.WechatEntryManager; import com.czg.utils.AssertUtil; +import com.czg.utils.AsyncTaskExecutor; +import java.util.ArrayList; import java.util.List; +import java.util.function.Supplier; /** * 进件管理 @@ -34,101 +38,136 @@ public class EntryManager { * 请先执行: * 1. {@link com.czg.EntryManager#verifyEntryParam(AggregateMerchantDto)} 验证进件参数 * 2. {@link com.czg.EntryManager#uploadParamImage(AggregateMerchantDto)} 上传图片至第三方 + * * @param reqDto 进件参数 */ - public static void entryMerchant(AggregateMerchantDto reqDto) { + public static EntryRespDto entryMerchant(AggregateMerchantDto reqDto) { + List> tasks = new ArrayList<>(); + tasks.add(() -> WechatEntryManager.entryMerchant(null, reqDto)); + tasks.add(() -> AlipayEntryManager.entryMerchant(null, reqDto)); + // 执行所有任务 + List> results = AsyncTaskExecutor.executeAll(tasks); + for (AsyncTaskExecutor.TaskResult result : results) { + // 合并两个进件结果 + } + + return new EntryRespDto(); } /** * 上传图片至第三方 * 请先执行 {@link com.czg.EntryManager#verifyEntryParam(AggregateMerchantDto)} 验证进件参数 + * * @param reqDto 进件参数 */ public static void uploadParamImage(AggregateMerchantDto reqDto) { + List> tasks = new ArrayList<>(); + MerchantBaseInfoDto baseInfo = reqDto.getMerchantBaseInfo(); // 联系人身份证反面 - if (baseInfo.getContactIdCardBackPic() != null) { + tasks.add(() -> { uploadImageToThird(baseInfo.getContactIdCardBackPic()); - } + return null; + }); // 联系人身份证正面 - if (baseInfo.getContactIdCardFrontPic() != null) { + tasks.add(() -> { uploadImageToThird(baseInfo.getContactIdCardFrontPic()); - } + return null; + }); LegalPersonInfoDto legalPersonInfo = reqDto.getLegalPersonInfo(); // 法人身份证反面 - if (legalPersonInfo.getIdCardBackPic() != null) { + tasks.add(() -> { uploadImageToThird(legalPersonInfo.getIdCardBackPic()); - } + return null; + }); // 法人身份证正面 - if (legalPersonInfo.getIdCardFrontPic() != null) { + tasks.add(() -> { uploadImageToThird(legalPersonInfo.getIdCardFrontPic()); - } + return null; + }); // 法人手持身份证 - if (legalPersonInfo.getIdCardHandPic() != null) { + tasks.add(() -> { uploadImageToThird(legalPersonInfo.getIdCardHandPic()); - } + return null; + }); BusinessLicenceInfoDto businessLicenceInfo = reqDto.getBusinessLicenceInfo(); // 营业执照 - if (businessLicenceInfo.getLicensePic() != null) { + tasks.add(() -> { uploadImageToThird(businessLicenceInfo.getLicensePic()); - } + return null; + }); SettlementInfoDto settlementInfo = reqDto.getSettlementInfo(); // 银行卡背面 - if (settlementInfo.getBankCardBackPic() != null) { + tasks.add(() -> { uploadImageToThird(settlementInfo.getBankCardBackPic()); - } + return null; + }); // 银行卡正面 - if (settlementInfo.getBankCardFrontPic() != null) { + tasks.add(() -> { uploadImageToThird(settlementInfo.getBankCardFrontPic()); - } + return null; + }); // 开户许可证 - if (settlementInfo.getOpenAccountLicencePic() != null) { + tasks.add(() -> { uploadImageToThird(settlementInfo.getOpenAccountLicencePic()); - } + return null; + }); // 非法人手持授权函 - if (settlementInfo.getNoLegalHandSettleAuthPic() != null) { + tasks.add(() -> { uploadImageToThird(settlementInfo.getNoLegalHandSettleAuthPic()); - } + return null; + }); // 非法人授权函 - if (settlementInfo.getNoLegalSettleAuthPic() != null) { + tasks.add(() -> { uploadImageToThird(settlementInfo.getNoLegalSettleAuthPic()); - } + return null; + }); // 非法人身份证反面 - if (settlementInfo.getNoLegalIdCardBackPic() != null) { + tasks.add(() -> { uploadImageToThird(settlementInfo.getNoLegalIdCardBackPic()); - } + return null; + }); // 非法人身份证正面 - if (settlementInfo.getNoLegalIdCardFrontPic() != null) { + tasks.add(() -> { uploadImageToThird(settlementInfo.getNoLegalIdCardFrontPic()); - } + return null; + }); StoreInfoDto storeInfo = reqDto.getStoreInfo(); // 店内图片 - if (storeInfo.getInsidePic() != null) { + tasks.add(() -> { uploadImageToThird(storeInfo.getInsidePic()); - } + return null; + }); // 门店门头图片 - if (storeInfo.getDoorPic() != null) { + tasks.add(() -> { uploadImageToThird(storeInfo.getDoorPic()); - } + return null; + }); // 收银台图片 - if (storeInfo.getCashierDeskPic() != null) { + tasks.add(() -> { uploadImageToThird(storeInfo.getCashierDeskPic()); - } + return null; + }); + + // 执行所有任务 + AsyncTaskExecutor.executeAll(tasks); } private static void uploadImageToThird(ImageDto dto) { - if (StrUtil.isBlank(dto.getWechatId())) { - String image = WechatEntryManager.uploadImage(dto.getUrl()); - dto.setWechatId(image); - } - if (StrUtil.isBlank(dto.getAlipayId())) { - String image = AlipayEntryManager.uploadImage(dto.getUrl()); - dto.setAlipayId(image); + if (dto != null && StrUtil.isNotBlank(dto.getUrl())) { + if (StrUtil.isBlank(dto.getWechatId())) { + String image = WechatEntryManager.uploadImage(null, dto.getUrl()); + dto.setWechatId(image); + } + if (StrUtil.isBlank(dto.getAlipayId())) { + String image = AlipayEntryManager.uploadImage(null, dto.getUrl()); + dto.setAlipayId(image); + } } } @@ -267,6 +306,7 @@ public class EntryManager { AggregateMerchantDto merchantDto = new AggregateMerchantDto(); merchantDto.setMerchantCode("20220106000000000001"); - verifyEntryParam(merchantDto); +// verifyEntryParam(merchantDto); + uploadParamImage(merchantDto); } } diff --git a/cash-sdk/aggregation-pay/src/main/java/com/czg/dto/resp/EntryRespDto.java b/cash-sdk/aggregation-pay/src/main/java/com/czg/dto/resp/EntryRespDto.java new file mode 100644 index 000000000..1780c699e --- /dev/null +++ b/cash-sdk/aggregation-pay/src/main/java/com/czg/dto/resp/EntryRespDto.java @@ -0,0 +1,34 @@ +package com.czg.dto.resp; + +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 进件相应结果 + * @author yjjie + * @date 2026/1/6 18:15 + */ +@Data +@Accessors(chain = true) +public class EntryRespDto { + + /** + * 微信申请 Id + */ + private String wechatApplyId; + + /** + * 微信状态 + */ + private String wechatStatus; + + /** + * 支付宝订单 Id + */ + private String alipayOrderId; + + /** + * 支付宝状态 + */ + private String alipayStatus; +} diff --git a/cash-sdk/aggregation-pay/src/main/java/com/czg/third/alipay/AlipayClient.java b/cash-sdk/aggregation-pay/src/main/java/com/czg/third/alipay/AlipayClient.java index c8a71033c..7af9a1578 100644 --- a/cash-sdk/aggregation-pay/src/main/java/com/czg/third/alipay/AlipayClient.java +++ b/cash-sdk/aggregation-pay/src/main/java/com/czg/third/alipay/AlipayClient.java @@ -3,6 +3,8 @@ package com.czg.third.alipay; import com.alipay.api.AlipayApiException; import com.alipay.api.AlipayConfig; import com.alipay.api.DefaultAlipayClient; +import com.alipay.v3.ApiClient; +import com.alipay.v3.Configuration; import com.czg.third.alipay.dto.config.AlipayConfigDto; import lombok.extern.slf4j.Slf4j; @@ -22,6 +24,23 @@ public class AlipayClient { } } + public static ApiClient getApiClient(AlipayConfigDto configDto) { + try { + ApiClient defaultClient = Configuration.getDefaultApiClient(); + // 初始化alipay参数(全局设置一次) + com.alipay.v3.util.model.AlipayConfig alipayConfig = new com.alipay.v3.util.model.AlipayConfig(); + alipayConfig.setServerUrl(configDto.getDomain()); + alipayConfig.setAppId(configDto.getAppId()); + alipayConfig.setAlipayPublicKey(configDto.getAlipayPublicKey()); + alipayConfig.setPrivateKey(configDto.getPrivateKey()); + defaultClient.setAlipayConfig(alipayConfig); + return defaultClient; + } catch (Exception e) { + log.error("创建支付宝客户端失败", e); + return null; + } + } + public static AlipayConfig getAlipayConfig(AlipayConfigDto configDto) { if (configDto == null) { configDto = AlipayConfigDto.getDefaultConfig(); diff --git a/cash-sdk/aggregation-pay/src/main/java/com/czg/third/alipay/AlipayEntryManager.java b/cash-sdk/aggregation-pay/src/main/java/com/czg/third/alipay/AlipayEntryManager.java index 03b9ef58d..909b7cb7b 100644 --- a/cash-sdk/aggregation-pay/src/main/java/com/czg/third/alipay/AlipayEntryManager.java +++ b/cash-sdk/aggregation-pay/src/main/java/com/czg/third/alipay/AlipayEntryManager.java @@ -12,8 +12,11 @@ import com.alipay.v3.api.AlipayOpenAgentApi; import com.alipay.v3.api.AntMerchantExpandIndirectImageApi; import com.alipay.v3.model.*; import com.alipay.v3.util.model.AlipayConfig; +import com.czg.dto.req.AggregateMerchantDto; import com.czg.dto.resp.BankBranchDto; -import com.czg.utils.CommonUtil; +import com.czg.dto.resp.EntryRespDto; +import com.czg.third.alipay.dto.config.AlipayConfigDto; +import com.czg.utils.UploadFileUtil; import lombok.extern.slf4j.Slf4j; import java.io.File; @@ -33,9 +36,29 @@ import java.util.List; @Slf4j public class AlipayEntryManager { - public static String uploadImage(String url) { + /** + * 进件商户 + * + * @param configDto 配置信息 + * @param reqDto 请求信息 + */ + public static EntryRespDto entryMerchant(AlipayConfigDto configDto, AggregateMerchantDto reqDto) { + return new EntryRespDto(); + } + + /** + * 上传图片 + * + * @param configDto 配置信息 + * @param url 图片地址 + * @return 图片ID + */ + public static String uploadImage(AlipayConfigDto configDto, String url) { + if (configDto == null) { + configDto = AlipayConfigDto.getDefaultConfig(); + } try { - byte[] bytes = CommonUtil.downloadImage(url); + byte[] bytes = UploadFileUtil.downloadImage(url); Path tempFile = Files.createTempFile("image_", ".png"); @@ -45,10 +68,10 @@ public class AlipayEntryManager { File file = tempFile.toFile(); AntMerchantExpandIndirectImageApi imageApi = new AntMerchantExpandIndirectImageApi(); - + imageApi.setApiClient(AlipayClient.getApiClient(configDto)); AntMerchantExpandIndirectImageUploadModel model = new AntMerchantExpandIndirectImageUploadModel(); // 从 url 中获取图片 后缀 - model.setImageType(CommonUtil.extractImageExtension(url)); + model.setImageType(UploadFileUtil.extractImageExtension(url)); AntMerchantExpandIndirectImageUploadResponseModel upload = imageApi.upload(model, file); return upload.getImageId(); } catch (Exception e) { diff --git a/cash-sdk/aggregation-pay/src/main/java/com/czg/third/wechat/WechatEntryManager.java b/cash-sdk/aggregation-pay/src/main/java/com/czg/third/wechat/WechatEntryManager.java index 6fee949ea..64a5d5d90 100644 --- a/cash-sdk/aggregation-pay/src/main/java/com/czg/third/wechat/WechatEntryManager.java +++ b/cash-sdk/aggregation-pay/src/main/java/com/czg/third/wechat/WechatEntryManager.java @@ -1,20 +1,17 @@ package com.czg.third.wechat; -import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONWriter; -import com.czg.third.wechat.dto.req.entry.WechatEntryReqDto; -import com.czg.utils.CommonUtil; +import com.czg.dto.req.AggregateMerchantDto; +import com.czg.dto.resp.EntryRespDto; import com.czg.third.wechat.dto.config.WechatPayConfigDto; +import com.czg.third.wechat.dto.req.entry.WechatEntryReqDto; +import com.czg.utils.UploadFileUtil; import com.wechat.pay.java.service.file.FileUploadService; import com.wechat.pay.java.service.file.model.FileUploadResponse; import lombok.extern.slf4j.Slf4j; -import java.io.File; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; import java.util.Map; /** @@ -26,6 +23,16 @@ import java.util.Map; @Slf4j public class WechatEntryManager { + /** + * 进件商户 + * + * @param configDto 配置信息 + * @param reqDto 请求信息 + */ + public static EntryRespDto entryMerchant(WechatPayConfigDto configDto, AggregateMerchantDto reqDto) { + return new EntryRespDto(); + } + public static JSONObject queryBankList(WechatPayConfigDto configDto, Integer offset, Integer limit) { String resp = WechatReqUtils.getReq(configDto, "/v3/capital/capitallhh/banks/corporate-banking", Map.of("offset", offset, "limit", limit)); log.info("查询银行列表:{}", resp); @@ -67,8 +74,10 @@ public class WechatEntryManager { * @param url 图片URL * @return 图片ID */ - public static String uploadImage(String url) { - WechatPayConfigDto configDto = WechatPayConfigDto.getDefaultConfig(); + public static String uploadImage(WechatPayConfigDto configDto, String url) { + if (configDto == null) { + configDto = WechatPayConfigDto.getDefaultConfig(); + } // 校验入参 if (url == null || url.trim().isEmpty()) { log.error("上传图片失败:URL参数为空"); @@ -79,7 +88,7 @@ public class WechatEntryManager { try { // 获取图片字节数组 - byte[] bytes = CommonUtil.downloadImage(url); + byte[] bytes = UploadFileUtil.downloadImage(url); if (bytes.length == 0) { log.error("下载的图片内容为空,URL:{}", url); return ""; @@ -92,7 +101,7 @@ public class WechatEntryManager { JSONObject meta = new JSONObject(); meta.put("sha256", sha256Hex); // 从URL提取文件名,若提取失败则使用默认名 - String fileName = extractFileNameFromUrl(url); + String fileName = UploadFileUtil.extractFileNameFromUrl(url); meta.put("filename", fileName); // 4. 上传图片到微信接口 @@ -110,32 +119,6 @@ public class WechatEntryManager { return ""; } - /** - * 从URL中提取文件名 - * - * @param url 图片URL - * @return 提取的文件名,失败则返回默认名 - */ - private static String extractFileNameFromUrl(String url) { - try { - if (url.contains("/")) { - String fileName = url.substring(url.lastIndexOf("/") + 1); - // 如果文件名包含参数,截取?之前的部分 - if (fileName.contains("?")) { - fileName = fileName.substring(0, fileName.indexOf("?")); - } - // 如果提取的文件名有效,直接返回 - if (fileName.contains(".")) { - return fileName; - } - } - } catch (Exception e) { - log.warn("提取文件名失败,使用默认文件名", e); - } - // 默认文件名 - return "upload_" + System.currentTimeMillis() + ".png"; - } - public static void main(String[] args) throws IOException { WechatPayConfigDto dto = new WechatPayConfigDto() .setMerchantId("1643779408") diff --git a/cash-sdk/aggregation-pay/src/main/java/com/czg/utils/AsyncTaskExecutor.java b/cash-sdk/aggregation-pay/src/main/java/com/czg/utils/AsyncTaskExecutor.java new file mode 100644 index 000000000..619acdf11 --- /dev/null +++ b/cash-sdk/aggregation-pay/src/main/java/com/czg/utils/AsyncTaskExecutor.java @@ -0,0 +1,157 @@ +package com.czg.utils; + +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * Java 21 异步多任务执行工具类 + * 功能:异步执行多个任务,等待所有任务完成后统一返回结果(包含成功/失败信息) + * 特性:基于虚拟线程、支持泛型、完善的异常处理、可自定义线程池 + * + * @author yjjie + * @date 2026/1/6 18:21 + */ +public class AsyncTaskExecutor { + + // 默认线程池:Java 21 虚拟线程池(轻量级、高并发) + private static final ExecutorService DEFAULT_EXECUTOR = Executors.newVirtualThreadPerTaskExecutor(); + + /** + * 执行多个异步任务,等待所有任务完成后统一返回结果 + * + * @param tasks 任务列表(每个任务是一个 Supplier 函数式接口,封装具体业务逻辑) + * @param 任务返回值类型 + * @return 所有任务的执行结果(包含成功/失败信息) + */ + public static List> executeAll(List> tasks) { + return executeAll(tasks, DEFAULT_EXECUTOR); + } + + /** + * 执行多个异步任务(自定义线程池),等待所有任务完成后统一返回结果 + * + * @param tasks 任务列表 + * @param executor 自定义线程池(如需要使用传统线程池可传入) + * @param 任务返回值类型 + * @return 所有任务的执行结果 + */ + public static List> executeAll(List> tasks, ExecutorService executor) { + // 校验入参 + if (tasks == null || tasks.isEmpty()) { + return List.of(); + } + if (executor == null) { + throw new IllegalArgumentException("线程池不能为null"); + } + + // 1. 提交所有异步任务,获取CompletableFuture列表 + List>> futureList = tasks.stream() + .map(task -> CompletableFuture.supplyAsync( + () -> executeSingleTask(task), + executor + )) + .collect(Collectors.toList()); + + // 2. 等待所有任务完成(无超时) + CompletableFuture allFutures = CompletableFuture.allOf( + futureList.toArray(new CompletableFuture[0]) + ); + + try { + // 阻塞等待所有任务完成(可根据业务需求添加超时,如 allFutures.get(10, TimeUnit.SECONDS)) + allFutures.get(); + } catch (Exception e) { + // 全局等待异常(如超时、中断),标记所有未完成的任务为失败 + handleGlobalException(futureList, e); + } + + // 3. 收集所有任务结果 + return futureList.stream() + // 显式指定泛型类型,让编译器明确知道map的返回类型是TaskResult + .>map(future -> { + try { + // 这里强制指定泛型,避免类型推断模糊 + return future.get(); + } catch (Exception e) { + // 理论上不会走到这里,因为singleTask已捕获异常,allOf已等待完成 + return new TaskResult<>(null, false, "结果收集异常:" + e.getMessage()); + } + }) + .collect(Collectors.toList()); + } + + /** + * 执行单个任务,捕获任务执行过程中的异常 + */ + private static TaskResult executeSingleTask(Supplier task) { + try { + T result = task.get(); + return new TaskResult<>(result, true, null); + } catch (Exception e) { + // 捕获单个任务的所有异常,封装为失败结果 + return new TaskResult<>(null, false, "任务执行失败:" + e.getMessage()); + } + } + + /** + * 处理全局等待过程中的异常(如超时、中断),标记未完成任务为失败 + */ + private static void handleGlobalException(List>> futureList, Exception e) { + String errorMsg = "全局等待异常:" + e.getMessage(); + futureList.forEach(future -> { + if (!future.isDone()) { + // 取消未完成的任务,并标记为失败 + future.complete(new TaskResult<>(null, false, errorMsg)); + } + }); + // 恢复线程中断状态(如果是中断异常) + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + } + + /** + * 任务结果封装类 + * 包含:返回值、是否成功、失败信息 + * + * @param 结果类型 + * @param result Getter 方法 任务返回值(成功时非null) + * @param success 是否执行成功 + * @param errorMsg 失败信息(失败时非null) + */ + public record TaskResult(T result, boolean success, String errorMsg) { + + // 重写toString,方便打印结果 + @NotNull + @Override + public String toString() { + if (success) { + return "TaskResult{success=true, result=" + result + "}"; + } else { + return "TaskResult{success=false, errorMsg='" + errorMsg + "'}"; + } + } + } + + /** + * 关闭默认线程池(可选,如应用退出时调用) + */ + public static void shutdownDefaultExecutor() { + DEFAULT_EXECUTOR.shutdown(); + try { + if (!DEFAULT_EXECUTOR.awaitTermination(5, TimeUnit.SECONDS)) { + DEFAULT_EXECUTOR.shutdownNow(); + } + } catch (InterruptedException e) { + DEFAULT_EXECUTOR.shutdownNow(); + Thread.currentThread().interrupt(); + } + } +} diff --git a/cash-sdk/aggregation-pay/src/main/java/com/czg/utils/CommonUtil.java b/cash-sdk/aggregation-pay/src/main/java/com/czg/utils/UploadFileUtil.java similarity index 64% rename from cash-sdk/aggregation-pay/src/main/java/com/czg/utils/CommonUtil.java rename to cash-sdk/aggregation-pay/src/main/java/com/czg/utils/UploadFileUtil.java index b46e3cc5e..b8b61121d 100644 --- a/cash-sdk/aggregation-pay/src/main/java/com/czg/utils/CommonUtil.java +++ b/cash-sdk/aggregation-pay/src/main/java/com/czg/utils/UploadFileUtil.java @@ -13,7 +13,7 @@ import java.util.regex.Pattern; * @date 2026/1/4 13:58 */ @Slf4j -public class CommonUtil { +public class UploadFileUtil { // 匹配常见的图片扩展名 private static final Pattern PATTERN = Pattern.compile("\\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)(?:[\\?#]|$)", Pattern.CASE_INSENSITIVE); @@ -58,4 +58,30 @@ public class CommonUtil { return new byte[0]; } } + + /** + * 从URL中提取文件名 + * + * @param url 图片URL + * @return 提取的文件名,失败则返回默认名 + */ + public static String extractFileNameFromUrl(String url) { + try { + if (url.contains("/")) { + String fileName = url.substring(url.lastIndexOf("/") + 1); + // 如果文件名包含参数,截取?之前的部分 + if (fileName.contains("?")) { + fileName = fileName.substring(0, fileName.indexOf("?")); + } + // 如果提取的文件名有效,直接返回 + if (fileName.contains(".")) { + return fileName; + } + } + } catch (Exception e) { + log.warn("提取文件名失败,使用默认文件名", e); + } + // 默认文件名 + return "upload_" + System.currentTimeMillis() + ".png"; + } }