This commit is contained in:
gong
2026-01-06 18:36:11 +08:00
parent 8501e21cf4
commit 193f4d016a
7 changed files with 366 additions and 84 deletions

View File

@@ -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<Supplier<EntryRespDto>> tasks = new ArrayList<>();
tasks.add(() -> WechatEntryManager.entryMerchant(null, reqDto));
tasks.add(() -> AlipayEntryManager.entryMerchant(null, reqDto));
// 执行所有任务
List<AsyncTaskExecutor.TaskResult<EntryRespDto>> results = AsyncTaskExecutor.executeAll(tasks);
for (AsyncTaskExecutor.TaskResult<EntryRespDto> result : results) {
// 合并两个进件结果
}
return new EntryRespDto();
}
/**
* 上传图片至第三方
* 请先执行 {@link com.czg.EntryManager#verifyEntryParam(AggregateMerchantDto)} 验证进件参数
*
* @param reqDto 进件参数
*/
public static void uploadParamImage(AggregateMerchantDto reqDto) {
List<Supplier<Void>> 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);
}
}

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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")

View File

@@ -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 <T> 任务返回值类型
* @return 所有任务的执行结果(包含成功/失败信息)
*/
public static <T> List<TaskResult<T>> executeAll(List<Supplier<T>> tasks) {
return executeAll(tasks, DEFAULT_EXECUTOR);
}
/**
* 执行多个异步任务(自定义线程池),等待所有任务完成后统一返回结果
*
* @param tasks 任务列表
* @param executor 自定义线程池(如需要使用传统线程池可传入)
* @param <T> 任务返回值类型
* @return 所有任务的执行结果
*/
public static <T> List<TaskResult<T>> executeAll(List<Supplier<T>> tasks, ExecutorService executor) {
// 校验入参
if (tasks == null || tasks.isEmpty()) {
return List.of();
}
if (executor == null) {
throw new IllegalArgumentException("线程池不能为null");
}
// 1. 提交所有异步任务获取CompletableFuture列表
List<CompletableFuture<TaskResult<T>>> futureList = tasks.stream()
.map(task -> CompletableFuture.supplyAsync(
() -> executeSingleTask(task),
executor
))
.collect(Collectors.toList());
// 2. 等待所有任务完成(无超时)
CompletableFuture<Void> 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<T>
.<TaskResult<T>>map(future -> {
try {
// 这里强制指定泛型,避免类型推断模糊
return future.get();
} catch (Exception e) {
// 理论上不会走到这里因为singleTask已捕获异常allOf已等待完成
return new TaskResult<>(null, false, "结果收集异常:" + e.getMessage());
}
})
.collect(Collectors.toList());
}
/**
* 执行单个任务,捕获任务执行过程中的异常
*/
private static <T> TaskResult<T> executeSingleTask(Supplier<T> task) {
try {
T result = task.get();
return new TaskResult<>(result, true, null);
} catch (Exception e) {
// 捕获单个任务的所有异常,封装为失败结果
return new TaskResult<>(null, false, "任务执行失败:" + e.getMessage());
}
}
/**
* 处理全局等待过程中的异常(如超时、中断),标记未完成任务为失败
*/
private static <T> void handleGlobalException(List<CompletableFuture<TaskResult<T>>> 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 <T> 结果类型
* @param result Getter 方法 任务返回值成功时非null
* @param success 是否执行成功
* @param errorMsg 失败信息失败时非null
*/
public record TaskResult<T>(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();
}
}
}

View File

@@ -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";
}
}