ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/dto/AppUserInfoDto.java
@@ -24,6 +24,18 @@ @ApiModelProperty("当前积分") private Integer points; @ApiModelProperty("微信openid") private String wxOpenid; @ApiModelProperty("支付宝openid") private String aliOpenid; @ApiModelProperty("支付宝userId") private String aliUserId; @ApiModelProperty("支付宝unionId") private String aliUnionId; @ApiModelProperty("当前余额") private BigDecimal balance; @ApiModelProperty("当前优惠卷数量") ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcPaymentReq.java
@@ -30,11 +30,21 @@ private String body; /** * 原微信/支付宝 openId,骨架阶段透传给工行适配层。 * 工行 open_id。微信小程序传微信 openId;支付宝小程序传支付宝 16 位 userId。 */ private String openId; /** * 工行 union_id,预留给支付宝生活号等 access_type=8 场景。 */ private String unionId; /** * 小程序设备标识,用于工行 deviceInfo/decive_info。 */ private String deviceInfo; /** * 兼容原支付方式:1=微信,2=支付宝,3=支付宝H5。 */ private Integer paymentType; ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderRefundReq.java
New file @@ -0,0 +1,27 @@ package com.ruoyi.payment.api.vo; import lombok.Data; import java.math.BigDecimal; /** * 工行聚合支付小订单拆分退款请求。 */ @Data public class IcbcSubOrderRefundReq { private String chargingOrderCode; private String seqNo; private String busiType; private String oriMerId; private String oriTrxDate; private String subOrderId; private String retSubOrderId; private String outtrxSerialNo; private String subMerId; private String subMerPrtclNo; private BigDecimal refundAmount; private String refundSource; private String merAcct; private String merSpInfor; private String termId; } ruoyi-api/ruoyi-api-payment/src/main/java/com/ruoyi/payment/api/vo/IcbcSubOrderRefundResp.java
New file @@ -0,0 +1,20 @@ package com.ruoyi.payment.api.vo; import lombok.Data; /** * 工行聚合支付小订单拆分退款响应。 */ @Data public class IcbcSubOrderRefundResp { private String chargingOrderCode; private String subOrderId; private String retSubOrderId; private String outtrxSerialNo; private String orderId; private String subOrderSeq; private String subOrderTrxid; private String refundAmount; private String status; private String rawResponse; } ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/model/LoginUserApplet.java
@@ -54,4 +54,24 @@ * 地址 */ private String address; /** * 微信小程序 openId */ private String wxOpenid; /** * 支付宝小程序 openId */ private String aliOpenid; /** * 支付宝 userId。 */ private String aliUserId; /** * 支付宝 unionId,工行支付宝小程序支付 union_id 使用。 */ private String aliUnionId; } ruoyi-modules/ruoyi-system/src/main/resources/bootstrap.yml
@@ -12,7 +12,7 @@ profiles: # 环境配置 # active: dev active: prod active: dev --- spring: config: @@ -22,7 +22,7 @@ nacos: discovery: # 服务注册地址 server-addr: 192.168.110.169:8848 server-addr: 192.168.110.80:8848 service: ${spring.application.name} group: DEFAULT_GROUP namespace: b5290bc2-e3aa-4988-8a7d-9c07e4e073cb @@ -30,7 +30,7 @@ password: nacos config: # 配置中心地址 server-addr: 192.168.110.169:8848 server-addr: 192.168.110.80:8848 namespace: b5290bc2-e3aa-4988-8a7d-9c07e4e073cb group: DEFAULT_GROUP username: nacos ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/AliLoginController.java
@@ -9,6 +9,7 @@ import com.alipay.api.response.AlipaySystemOauthTokenResponse; import com.alipay.api.response.AlipayUserInfoShareResponse; import com.alipay.api.response.AlipayUserUserinfoShareResponse; import com.alibaba.fastjson.JSON; import com.baomidou.mybatisplus.core.toolkit.ObjectUtils; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.ruoyi.account.ali.Constant.AliConstant; @@ -28,6 +29,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -56,8 +58,16 @@ // 登录 AlipaySystemOauthTokenResponse response = new AliAppletTools(aliProperties).login(appletUserLogin.getCode()); if (response.isSuccess()) { // 获取用户信息 // AlipayUserInfoShareResponse userInfo = new AliAppletTools(aliProperties).getUserInfo(response.getAccessToken()); String aliUnionId = firstNotBlank(readStringProperty(response, "getUnionId"), readStringProperty(response, "getUnion_id"), getResponseBodyValue(response, "union_id"), getResponseBodyValue(response, "unionId")); String aliUserId = firstNotBlank(response.getUserId(), getResponseBodyValue(response, "user_id"), getResponseBodyValue(response, "userId")); log.info("支付宝授权换取用户标识成功,openId:{},userId:{},unionId:{}", response.getOpenId(), aliUserId, aliUnionId); AlipayUserInfoShareResponse userInfo = new AliAppletTools(aliProperties).getUserInfo(response.getAccessToken()); log.info("支付宝用户信息接口返回:{}", JSON.toJSONString(userInfo)); aliUserId = firstNotBlank(aliUserId, userInfo == null ? null : userInfo.getUserId(), getResponseBodyValue(userInfo, "user_id"), getResponseBodyValue(userInfo, "userId")); aliUnionId = firstNotBlank(aliUnionId, readStringProperty(userInfo, "getUnionId"), readStringProperty(userInfo, "getUnion_id"), getResponseBodyValue(userInfo, "union_id"), getResponseBodyValue(userInfo, "unionId")); if(StringUtils.isEmpty(appletUserLogin.getToken())){ return AjaxResult.success("已拒绝授权"); } @@ -65,7 +75,8 @@ String phoneNumber = new AliAppletTools(aliProperties).getPhoneNumber(appletUserLogin.getToken()); log.info("获取支付宝用户信息:{}", phoneNumber); // 用户信息封装 return AjaxResult.success(appUserService.aliLogin(response,phoneNumber,appletUserLogin.getInviteUserId())); return AjaxResult.success(appUserService.aliLogin(response,phoneNumber,appletUserLogin.getInviteUserId(), aliUserId, aliUnionId)); } else { String diagnosisUrl = DiagnosisUtils.getDiagnosisUrl(response); log.warn("诊断结果:{}",diagnosisUrl); @@ -73,4 +84,55 @@ } } private String getResponseBodyValue(Object response, String key) { String body = readStringProperty(response, "getBody"); if (StringUtils.isEmpty(body)) { return null; } try { com.alibaba.fastjson.JSONObject bodyJson = JSON.parseObject(body); String value = bodyJson.getString(key); if (StringUtils.isNotEmpty(value)) { return value; } for (String nestedKey : bodyJson.keySet()) { com.alibaba.fastjson.JSONObject nested = bodyJson.getJSONObject(nestedKey); if (nested != null) { value = nested.getString(key); if (StringUtils.isNotEmpty(value)) { return value; } } } return null; } catch (Exception e) { return null; } } private String readStringProperty(Object target, String methodName) { if (target == null) { return null; } try { Method method = target.getClass().getMethod(methodName); Object value = method.invoke(target); return value == null ? null : String.valueOf(value); } catch (Exception e) { return null; } } private String firstNotBlank(String... values) { if (values == null) { return null; } for (String value : values) { if (StringUtils.isNotEmpty(value)) { return value; } } return null; } } ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java
@@ -851,7 +851,8 @@ @ApiOperation(value = "个人中心信息", tags = {"小程序-个人中心"}) @GetMapping(value = "/user/info") public R<AppUserInfoDto> info() { Long userId = tokenService.getLoginUserApplet().getUserId(); LoginUserApplet loginUserApplet = tokenService.getLoginUserApplet(); Long userId = loginUserApplet.getUserId(); TAppUser byId = appUserService.getById(userId); AppUserInfoDto appUserInfoDto = new AppUserInfoDto(); TVip data = vipClient.getVipInfoByType(2).getData(); @@ -886,6 +887,10 @@ appUserInfoDto.setUserCars(list); appUserInfoDto.setFirstAdd(byId.getFirstAdd()!=null?byId.getFirstAdd():0); appUserInfoDto.setPoints(byId.getPoints()==null?0:byId.getPoints()); appUserInfoDto.setWxOpenid(byId.getWxOpenid()); appUserInfoDto.setAliOpenid(byId.getAliOpenid()); appUserInfoDto.setAliUserId(loginUserApplet.getAliUserId()); appUserInfoDto.setAliUnionId(loginUserApplet.getAliUnionId()); appUserInfoDto.setBalance(byId.getBalance()==null?BigDecimal.ZERO:byId.getBalance()); return R.ok(appUserInfoDto); } ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/TAppUserService.java
@@ -32,7 +32,7 @@ * @return */ // Map<String, Object> aliLogin(AlipaySystemOauthTokenResponse response, AlipayUserInfoShareResponse userInfo); Map<String, Object> aliLogin(AlipaySystemOauthTokenResponse response,String phone,Long inviteUserId); Map<String, Object> aliLogin(AlipaySystemOauthTokenResponse response,String phone,Long inviteUserId,String aliUserId,String aliUnionId); /** * 封装用户信息和token ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/service/impl/TAppUserServiceImpl.java
@@ -88,7 +88,7 @@ } @Override public Map<String, Object> aliLogin(AlipaySystemOauthTokenResponse response,String phone,Long inviteUserId) { public Map<String, Object> aliLogin(AlipaySystemOauthTokenResponse response,String phone,Long inviteUserId,String aliUserId,String aliUnionId) { // 通过手机号查询用户,是否已存在手动导入用户,包含微信用户 TAppUser appUser = this.getOne(Wrappers.lambdaQuery(TAppUser.class) .eq(TAppUser::getPhone, phone) @@ -129,7 +129,10 @@ if(Objects.nonNull(inviteUserId)){ inviteUserService.saveInviteUser(appUser.getId(), inviteUserId); } return this.getUserInfo(appUser); LoginUserApplet loginUserApplet = buildLoginUserApplet(appUser); loginUserApplet.setAliUserId(aliUserId); loginUserApplet.setAliUnionId(aliUnionId); return buildTokenInfo(loginUserApplet); } // @Override // public Map<String, Object> aliLogin(AlipaySystemOauthTokenResponse response, AlipayUserInfoShareResponse userInfo) { @@ -163,6 +166,10 @@ @Override public Map<String, Object> getUserInfo(TAppUser appUser) { return buildTokenInfo(buildLoginUserApplet(appUser)); } private LoginUserApplet buildLoginUserApplet(TAppUser appUser) { LoginUserApplet loginUserApplet = new LoginUserApplet(); if(ObjectUtils.isNotNull(appUser)){ loginUserApplet.setUserId(appUser.getId()); @@ -171,7 +178,13 @@ loginUserApplet.setPhone(appUser.getPhone()); loginUserApplet.setAvatar(appUser.getAvatar()); loginUserApplet.setAddress(appUser.getProvince()+appUser.getCity()); loginUserApplet.setWxOpenid(appUser.getWxOpenid()); loginUserApplet.setAliOpenid(appUser.getAliOpenid()); } return loginUserApplet; } private Map<String, Object> buildTokenInfo(LoginUserApplet loginUserApplet) { Map<String, Object> tokenInfos = new HashMap<>(); tokenInfos.put("token",tokenService.createTokenApplet(loginUserApplet)); tokenInfos.put("info",loginUserApplet); ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
@@ -6257,7 +6257,7 @@ record.setSeqNo(chargingOrder.getCode()); record.setBusiType("1"); record.setOriTrxDate(chargingOrder.getPayTime() == null ? LocalDate.now() : chargingOrder.getPayTime().toLocalDate()); record.setSubOrderId("ICBCS" + chargingOrder.getCode()); record.setSubOrderId(chargingOrder.getCode()); } if ("ICBC".equals(payChannel) && partner != null) { record.setSubMerId(partner.getIcbcSubMerId()); ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/controller/IcbcPayController.java
@@ -1,5 +1,7 @@ package com.ruoyi.payment.controller; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.ruoyi.common.core.domain.R; import com.ruoyi.order.api.feignClient.ChargingOrderClient; import com.ruoyi.payment.api.vo.IcbcPaymentReq; @@ -8,32 +10,178 @@ import com.ruoyi.payment.api.vo.IcbcRefundResp; import com.ruoyi.payment.api.vo.IcbcSubOrderQueryReq; import com.ruoyi.payment.api.vo.IcbcSubOrderQueryResp; import com.ruoyi.payment.api.vo.IcbcSubOrderRefundReq; import com.ruoyi.payment.api.vo.IcbcSubOrderRefundResp; import com.ruoyi.payment.api.vo.IcbcSubOrderSplitReq; import com.ruoyi.payment.api.vo.IcbcSubOrderSplitResp; import com.ruoyi.payment.icbc.config.IcbcPayProperties; import com.ruoyi.payment.icbc.service.IcbcPayService; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.math.BigDecimal; import java.io.PrintWriter; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Enumeration; import java.util.LinkedHashMap; import java.util.Map; /** * 工行统一支付入口。 */ @Slf4j @RestController @RequestMapping("/icbc") public class IcbcPayController { private static final String TEST_ORDER_PREFIX = "ICBCTEST"; private static final String TEST_SUB_MER_ID = "231011990055"; private static final String TEST_SUB_MER_PRTCL_NO = "2310119900550201"; @Resource private IcbcPayService icbcPayService; @Resource private IcbcPayProperties icbcPayProperties; @Resource private ChargingOrderClient chargingOrderClient; @PostMapping("/payment") public R<IcbcPaymentResp> payment(@RequestBody IcbcPaymentReq req) { return R.ok(icbcPayService.payment(req)); log.info("工行支付请求入参:{}", JSON.toJSONString(req)); IcbcPaymentResp resp = icbcPayService.payment(req); log.info("工行支付返回前端:{}", JSON.toJSONString(resp)); return R.ok(resp); } @PostMapping("/test/payment") public R<IcbcPaymentResp> testPayment(@RequestBody(required = false) IcbcPaymentReq req) { log.info("工行测试支付请求入参:{}", JSON.toJSONString(req)); IcbcPaymentReq paymentReq = req == null ? new IcbcPaymentReq() : req; paymentReq.setOutTradeNo(TEST_ORDER_PREFIX + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"))); if (paymentReq.getAmount() == null || paymentReq.getAmount().compareTo(BigDecimal.ZERO) <= 0) { paymentReq.setAmount(new BigDecimal("0.01")); } if (paymentReq.getSubject() == null || paymentReq.getSubject().trim().isEmpty()) { paymentReq.setSubject("工行测试支付"); } if (paymentReq.getBody() == null || paymentReq.getBody().trim().isEmpty()) { paymentReq.setBody("工行测试支付"); } if (paymentReq.getPaymentType() == null) { paymentReq.setPaymentType(1); } log.info("工行测试支付实际下单参数:{}", JSON.toJSONString(paymentReq)); IcbcPaymentResp resp = icbcPayService.payment(paymentReq); log.info("工行测试支付返回前端:{}", JSON.toJSONString(resp)); return R.ok(resp); } @PostMapping("/test/query") public R<IcbcPaymentResp> testQuery(@RequestBody Map<String, Object> req) { String outTradeNo = getString(req, "outTradeNo"); log.info("工行测试查询请求入参:{}", JSON.toJSONString(req)); IcbcPaymentResp resp = icbcPayService.query(outTradeNo); log.info("工行测试查询返回前端:{}", JSON.toJSONString(resp)); return R.ok(resp); } @PostMapping("/test/refund") public R<IcbcRefundResp> testRefund(@RequestBody(required = false) IcbcRefundReq req) { req = req == null ? new IcbcRefundReq() : req; log.info("工行测试退款请求入参:{}", JSON.toJSONString(req)); if (req.getRefundAmount() == null || req.getRefundAmount().compareTo(BigDecimal.ZERO) <= 0) { req.setRefundAmount(new BigDecimal("0.01")); } if (req.getTotalAmount() == null || req.getTotalAmount().compareTo(BigDecimal.ZERO) <= 0) { req.setTotalAmount(req.getRefundAmount()); } if (req.getOutRefundNo() == null || req.getOutRefundNo().trim().isEmpty()) { req.setOutRefundNo("ICBCREFUND" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"))); } if (req.getReason() == null || req.getReason().trim().isEmpty()) { req.setReason("工行测试退款"); } log.info("工行测试退款实际参数:{}", JSON.toJSONString(req)); IcbcRefundResp resp = icbcPayService.refund(req); log.info("工行测试退款返回前端:{}", JSON.toJSONString(resp)); return R.ok(resp); } @PostMapping("/test/sub-order/split") public R<IcbcSubOrderSplitResp> testSplitSubOrder(@RequestBody(required = false) IcbcSubOrderSplitReq req) { req = req == null ? new IcbcSubOrderSplitReq() : req; log.info("工行测试分账请求入参:{}", JSON.toJSONString(req)); if (req.getSubOrderId() == null || req.getSubOrderId().trim().isEmpty()) { req.setSubOrderId("ICBCSUB" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"))); } if (req.getSeqNo() == null || req.getSeqNo().trim().isEmpty()) { req.setSeqNo(req.getChargingOrderCode()); } if (req.getBusiType() == null || req.getBusiType().trim().isEmpty()) { req.setBusiType("1"); } if (req.getOriTrxDate() == null || req.getOriTrxDate().trim().isEmpty()) { req.setOriTrxDate(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); } if (req.getClassifyAmt() == null || req.getClassifyAmt().compareTo(BigDecimal.ZERO) <= 0) { req.setClassifyAmt(new BigDecimal("0.01")); } if (req.getSubMerId() == null || req.getSubMerId().trim().isEmpty()) { req.setSubMerId(TEST_SUB_MER_ID); } if (req.getSubMerPrtclNo() == null || req.getSubMerPrtclNo().trim().isEmpty()) { req.setSubMerPrtclNo(TEST_SUB_MER_PRTCL_NO); } if (req.getOriMerId() == null || req.getOriMerId().trim().isEmpty()) { req.setOriMerId(icbcPayProperties.getMerId()); } log.info("工行测试分账实际参数:{}", JSON.toJSONString(req)); IcbcSubOrderSplitResp resp = icbcPayService.splitSubOrder(req); log.info("工行测试分账返回前端:{}", JSON.toJSONString(resp)); return R.ok(resp); } @PostMapping("/test/sub-order/refund") public R<IcbcSubOrderRefundResp> testRefundSubOrder(@RequestBody(required = false) IcbcSubOrderRefundReq req) { req = req == null ? new IcbcSubOrderRefundReq() : req; log.info("工行测试分账退款请求入参:{}", JSON.toJSONString(req)); if (req.getSeqNo() == null || req.getSeqNo().trim().isEmpty()) { req.setSeqNo(req.getChargingOrderCode()); } if (req.getBusiType() == null || req.getBusiType().trim().isEmpty()) { req.setBusiType("1"); } if (req.getOriTrxDate() == null || req.getOriTrxDate().trim().isEmpty()) { req.setOriTrxDate(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); } if (req.getRefundAmount() == null || req.getRefundAmount().compareTo(BigDecimal.ZERO) <= 0) { req.setRefundAmount(new BigDecimal("0.01")); } if (req.getRetSubOrderId() == null || req.getRetSubOrderId().trim().isEmpty()) { req.setRetSubOrderId("ICBCSUBREFUND" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"))); } if (req.getOuttrxSerialNo() == null || req.getOuttrxSerialNo().trim().isEmpty()) { req.setOuttrxSerialNo("ICBCSUBREFUNDNO" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"))); } if (req.getSubMerId() == null || req.getSubMerId().trim().isEmpty()) { req.setSubMerId(TEST_SUB_MER_ID); } if (req.getSubMerPrtclNo() == null || req.getSubMerPrtclNo().trim().isEmpty()) { req.setSubMerPrtclNo(TEST_SUB_MER_PRTCL_NO); } if (req.getOriMerId() == null || req.getOriMerId().trim().isEmpty()) { req.setOriMerId(icbcPayProperties.getMerId()); } log.info("工行测试分账退款实际参数:{}", JSON.toJSONString(req)); IcbcSubOrderRefundResp resp = icbcPayService.refundSubOrder(req); log.info("工行测试分账退款返回前端:{}", JSON.toJSONString(resp)); return R.ok(resp); } @PostMapping("/query") @@ -56,25 +204,48 @@ return R.ok(icbcPayService.splitSubOrder(req)); } @PostMapping("/sub-order/refund") public R<IcbcSubOrderRefundResp> refundSubOrder(@RequestBody IcbcSubOrderRefundReq req) { return R.ok(icbcPayService.refundSubOrder(req)); } @PostMapping("/sub-order/query") public R<IcbcSubOrderQueryResp> querySubOrder(@RequestBody IcbcSubOrderQueryReq req) { return R.ok(icbcPayService.querySubOrder(req)); } @PostMapping("/callBack") @RequestMapping(value = "/callBack", method = {RequestMethod.GET, RequestMethod.POST}) public void callBack(HttpServletRequest request, HttpServletResponse response) throws Exception { Map<String, String> callback = icbcPayService.parseNotify(request.getParameterMap()); dispatchChargingOrderCallback(callback); PrintWriter writer = response.getWriter(); writer.println("success"); writer.flush(); writer.close(); Map<String, String[]> requestParams = new LinkedHashMap<>(request.getParameterMap()); String rawBody = requestParams.isEmpty() ? readRequestBody(request) : ""; mergeJsonBody(requestParams, rawBody); log.info("工行支付回调原始请求,method:{},uri:{},query:{},contentType:{},params:{},body:{},headers:{}", request.getMethod(), request.getRequestURI(), request.getQueryString(), request.getContentType(), JSON.toJSONString(requestParams), rawBody, summarizeHeaders(request)); try { Map<String, String> callback = icbcPayService.parseNotify(requestParams); log.info("工行支付回调解析结果:{}", JSON.toJSONString(callback)); dispatchChargingOrderCallback(callback); } catch (Exception e) { log.error("工行支付回调处理失败,已记录原始请求并返回success避免重复通知", e); } finally { writeCallbackResponse(response); } } private void dispatchChargingOrderCallback(Map<String, String> callback) { String outTradeNo = callback.get("out_trade_no"); String transactionId = callback.get("transaction_id"); String attach = callback.get("attach"); if (outTradeNo != null && outTradeNo.startsWith(TEST_ORDER_PREFIX)) { log.info("工行测试支付回调成功,订单号:{},流水号:{},支付类型:{}", outTradeNo, transactionId, attach); return; } Integer paymentType = Integer.valueOf(attach); if (Integer.valueOf(1).equals(paymentType)) { chargingOrderClient.chargingOrderWXCallback(outTradeNo, transactionId, attach); @@ -90,4 +261,67 @@ } throw new IllegalArgumentException("工行回调支付类型不支持:" + attach); } private String getString(Map<String, Object> req, String key) { if (req == null || req.get(key) == null) { throw new IllegalArgumentException("缺少参数:" + key); } return String.valueOf(req.get(key)); } private String readRequestBody(HttpServletRequest request) { StringBuilder body = new StringBuilder(); try (BufferedReader reader = request.getReader()) { String line; while ((line = reader.readLine()) != null) { body.append(line); } } catch (Exception e) { log.warn("读取工行支付回调body失败", e); } return body.toString(); } private void mergeJsonBody(Map<String, String[]> requestParams, String rawBody) { if (rawBody == null || rawBody.trim().isEmpty()) { return; } try { JSONObject jsonObject = JSON.parseObject(rawBody); if (jsonObject == null) { return; } for (Map.Entry<String, Object> entry : jsonObject.entrySet()) { if (entry.getValue() != null && !requestParams.containsKey(entry.getKey())) { requestParams.put(entry.getKey(), new String[]{String.valueOf(entry.getValue())}); } } } catch (Exception e) { log.warn("工行支付回调body不是JSON格式,body:{}", rawBody); } } private String summarizeHeaders(HttpServletRequest request) { Map<String, String> headers = new LinkedHashMap<>(); Enumeration<String> names = request.getHeaderNames(); while (names != null && names.hasMoreElements()) { String name = names.nextElement(); if ("user-agent".equalsIgnoreCase(name) || "host".equalsIgnoreCase(name) || "x-forwarded-for".equalsIgnoreCase(name) || "x-real-ip".equalsIgnoreCase(name)) { headers.put(name, request.getHeader(name)); } } return JSON.toJSONString(headers); } private void writeCallbackResponse(HttpServletResponse response) throws Exception { response.setCharacterEncoding("UTF-8"); response.setContentType("text/plain;charset=UTF-8"); PrintWriter writer = response.getWriter(); writer.println("success"); writer.flush(); writer.close(); } } ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/config/IcbcPayProperties.java
@@ -20,13 +20,19 @@ private String charset = "UTF-8"; private String format = "json"; private String gatewayUrl; private String orderQueryUrl; private String refundUrl; private String subOrderSplitUrl; private String subOrderRefundUrl; private String subOrderQueryUrl; private String notifyUrl; private String merId; private String merPrtclNo; private String serviceMerId; private String serviceMerPrtclNo; private String tpAppId; private String wxTpAppId; private String aliTpAppId; private String spbillCreateIp; private String profitSharing; private Boolean verifyNotifySign = true; ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/IcbcPayService.java
@@ -6,6 +6,8 @@ import com.ruoyi.payment.api.vo.IcbcRefundResp; import com.ruoyi.payment.api.vo.IcbcSubOrderQueryReq; import com.ruoyi.payment.api.vo.IcbcSubOrderQueryResp; import com.ruoyi.payment.api.vo.IcbcSubOrderRefundReq; import com.ruoyi.payment.api.vo.IcbcSubOrderRefundResp; import com.ruoyi.payment.api.vo.IcbcSubOrderSplitReq; import com.ruoyi.payment.api.vo.IcbcSubOrderSplitResp; @@ -25,6 +27,8 @@ IcbcSubOrderSplitResp splitSubOrder(IcbcSubOrderSplitReq req); IcbcSubOrderRefundResp refundSubOrder(IcbcSubOrderRefundReq req); IcbcSubOrderQueryResp querySubOrder(IcbcSubOrderQueryReq req); Map<String, String> parseNotify(Map<String, String[]> requestParams); ruoyi-service/ruoyi-payment/src/main/java/com/ruoyi/payment/icbc/service/impl/IcbcPayServiceImpl.java
@@ -2,17 +2,21 @@ import com.alibaba.fastjson.JSON; import com.icbc.api.DefaultIcbcClient; import com.icbc.api.IcbcRequest; import com.icbc.api.IcbcResponse; import com.icbc.api.request.CardbusinessAggregatepayB2cOnlineOrderqryRequestV2; import com.icbc.api.request.AggregatePaymentRequestV3; import com.icbc.api.request.CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2; import com.icbc.api.request.CardbusinessAggregatepayB2cOnlineMerrefundRequestV1; import com.icbc.api.request.CardbusinessAggregatepayB2cOnlineSersuborderRequestV1; import com.icbc.api.request.CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1; import com.icbc.api.request.CardbusinessOrderSuborderqueryRequestV1; import com.icbc.api.internal.util.internal.util.fastjson.JSONArray; import com.icbc.api.internal.util.internal.util.fastjson.JSONObject; import com.icbc.api.response.CardbusinessAggregatepayB2cOnlineOrderqryResponseV2; import com.icbc.api.response.CardbusinessAggregatepayB2cOnlineConsumepurchaseResponseV2; import com.icbc.api.response.CardbusinessAggregatepayB2cOnlineMerrefundResponseV1; import com.icbc.api.response.CardbusinessAggregatepayB2cOnlineSersuborderResponseV1; import com.icbc.api.response.CardbusinessAggregatepayB2cOnlineSersuborderrefundResponseV1; import com.icbc.api.response.CardbusinessOrderSuborderqueryResponseV1; import com.icbc.api.utils.IcbcSignature; import com.ruoyi.common.core.utils.StringUtils; @@ -22,6 +26,8 @@ import com.ruoyi.payment.api.vo.IcbcRefundResp; import com.ruoyi.payment.api.vo.IcbcSubOrderQueryReq; import com.ruoyi.payment.api.vo.IcbcSubOrderQueryResp; import com.ruoyi.payment.api.vo.IcbcSubOrderRefundReq; import com.ruoyi.payment.api.vo.IcbcSubOrderRefundResp; import com.ruoyi.payment.api.vo.IcbcSubOrderSplitReq; import com.ruoyi.payment.api.vo.IcbcSubOrderSplitResp; import com.ruoyi.payment.icbc.config.IcbcPayProperties; @@ -50,20 +56,26 @@ @Override public IcbcPaymentResp payment(IcbcPaymentReq req) { AggregatePaymentRequestV3 request = new AggregatePaymentRequestV3(); request.setServiceUrl(properties.getGatewayUrl()); request.setBizContent(buildAggregatePaymentBiz(req)); IcbcPaymentResp resp = new IcbcPaymentResp(); resp.setOrderId(req.getOutTradeNo()); if (req != null) { resp.setOrderId(req.getOutTradeNo()); } try { IcbcResponse icbcResponse = buildClient().execute(request); resp.setTradeNo(icbcResponse.getMsgId()); resp.setStatus(icbcResponse.isSuccess() ? "SUCCESS" : "FAIL"); resp.setPayData(JSON.toJSONString(icbcResponse)); resp.setRawResponse(JSON.toJSONString(icbcResponse)); validateCommonConfig(); validateConfig("icbc.pay.gatewayUrl", properties.getGatewayUrl()); validatePaymentReq(req); CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2 request = new CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2(); request.setServiceUrl(properties.getGatewayUrl()); request.setBizContent(buildConsumePurchaseBiz(req)); CardbusinessAggregatepayB2cOnlineConsumepurchaseResponseV2 response = executeIcbc("聚合支付下单", request); resp.setOrderId(firstNotBlank(response.getOut_trade_no(), req.getOutTradeNo())); resp.setTradeNo(firstNotBlank(response.getOrder_id(), response.getMsgId())); resp.setStatus(response.isSuccess() ? "SUCCESS" : "FAIL"); resp.setPayData(buildConsumePurchasePayData(response, req.getPaymentType())); resp.setRawResponse(JSON.toJSONString(response)); } catch (Exception e) { log.error("工行聚合支付调用失败,订单号:{}", req.getOutTradeNo(), e); log.error("工行聚合支付调用失败,订单号:{}", req == null ? null : req.getOutTradeNo(), e); resp.setStatus("FAIL"); resp.setRawResponse(e.getMessage()); } @@ -72,13 +84,15 @@ @Override public IcbcPaymentResp query(String outTradeNo) { validateCommonConfig(); validateConfig("icbc.pay.orderQueryUrl", properties.getOrderQueryUrl()); IcbcPaymentResp resp = new IcbcPaymentResp(); resp.setOrderId(outTradeNo); try { CardbusinessAggregatepayB2cOnlineOrderqryRequestV2 request = new CardbusinessAggregatepayB2cOnlineOrderqryRequestV2(); request.setServiceUrl(properties.getGatewayUrl()); request.setServiceUrl(properties.getOrderQueryUrl()); request.setBizContent(buildOrderQueryBiz(outTradeNo)); CardbusinessAggregatepayB2cOnlineOrderqryResponseV2 response = buildClient().execute(request); CardbusinessAggregatepayB2cOnlineOrderqryResponseV2 response = executeIcbc("聚合支付订单查询", request); resp.setOrderId(firstNotBlank(response.getOut_trade_no(), outTradeNo)); resp.setTradeNo(firstNotBlank(response.getOrder_id(), response.getThird_trade_no(), response.getMsgId())); resp.setStatus(firstNotBlank(response.getOrderStatus(), response.getPay_status())); @@ -104,6 +118,8 @@ @Override public IcbcRefundResp refund(IcbcRefundReq req) { validateCommonConfig(); validateConfig("icbc.pay.refundUrl", properties.getRefundUrl()); IcbcRefundResp resp = new IcbcRefundResp(); resp.setOutTradeNo(req.getOutTradeNo()); resp.setOrderId(req.getOrderId()); @@ -112,7 +128,7 @@ CardbusinessAggregatepayB2cOnlineMerrefundRequestV1 request = new CardbusinessAggregatepayB2cOnlineMerrefundRequestV1(); request.setServiceUrl(properties.getRefundUrl()); request.setBizContent(buildRefundBiz(req)); CardbusinessAggregatepayB2cOnlineMerrefundResponseV1 response = buildClient().execute(request); CardbusinessAggregatepayB2cOnlineMerrefundResponseV1 response = executeIcbc("线上POS退款", request); resp.setStatus(response.isSuccess() ? "SUCCESS" : "FAIL"); resp.setRawResponse(JSON.toJSONString(response)); resp.setOutTradeNo(firstNotBlank(response.getOutTradeNo(), req.getOutTradeNo())); @@ -131,6 +147,9 @@ @Override public IcbcSubOrderSplitResp splitSubOrder(IcbcSubOrderSplitReq req) { validateCommonConfig(); validateConfig("icbc.pay.merPrtclNo", properties.getMerPrtclNo()); validateConfig("icbc.pay.subOrderSplitUrl", properties.getSubOrderSplitUrl()); IcbcSubOrderSplitResp resp = new IcbcSubOrderSplitResp(); resp.setChargingOrderCode(req.getChargingOrderCode()); resp.setSubOrderId(req.getSubOrderId()); @@ -138,7 +157,7 @@ CardbusinessAggregatepayB2cOnlineSersuborderRequestV1 request = new CardbusinessAggregatepayB2cOnlineSersuborderRequestV1(); request.setServiceUrl(properties.getSubOrderSplitUrl()); request.setBizContent(buildSubOrderSplitBiz(req)); CardbusinessAggregatepayB2cOnlineSersuborderResponseV1 response = buildClient().execute(request); CardbusinessAggregatepayB2cOnlineSersuborderResponseV1 response = executeIcbc("B2C小订单拆分", request); resp.setStatus(response.isSuccess() ? "SUCCESS" : "FAIL"); resp.setRawResponse(JSON.toJSONString(response)); fillSplitOutput(resp, response.getOrderSubmitActOutput()); @@ -151,13 +170,43 @@ } @Override public IcbcSubOrderRefundResp refundSubOrder(IcbcSubOrderRefundReq req) { validateCommonConfig(); validateConfig("icbc.pay.subOrderRefundUrl", properties.getSubOrderRefundUrl()); IcbcSubOrderRefundResp resp = new IcbcSubOrderRefundResp(); resp.setChargingOrderCode(req.getChargingOrderCode()); resp.setSubOrderId(req.getSubOrderId()); resp.setRetSubOrderId(req.getRetSubOrderId()); resp.setOuttrxSerialNo(req.getOuttrxSerialNo()); try { CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1 request = new CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1(); request.setServiceUrl(properties.getSubOrderRefundUrl()); request.setBizContent(buildSubOrderRefundBiz(req)); CardbusinessAggregatepayB2cOnlineSersuborderrefundResponseV1 response = executeIcbc("B2C小订单拆分退款", request); resp.setStatus(response.isSuccess() ? "SUCCESS" : "FAIL"); resp.setRawResponse(JSON.toJSONString(response)); resp.setOrderId(response.getOrder_id()); resp.setSubOrderSeq(response.getSub_order_seq()); resp.setSubOrderTrxid(response.getSub_order_trxid()); resp.setRefundAmount(toFen(req.getRefundAmount())); } catch (Exception e) { log.error("工行小订单拆分退款失败,订单号:{},子订单号:{},退款流水:{}", req.getChargingOrderCode(), req.getSubOrderId(), req.getOuttrxSerialNo(), e); resp.setStatus("FAIL"); resp.setRawResponse(e.getMessage()); } return resp; } @Override public IcbcSubOrderQueryResp querySubOrder(IcbcSubOrderQueryReq req) { validateCommonConfig(); validateConfig("icbc.pay.subOrderQueryUrl", properties.getSubOrderQueryUrl()); IcbcSubOrderQueryResp resp = new IcbcSubOrderQueryResp(); try { CardbusinessOrderSuborderqueryRequestV1 request = new CardbusinessOrderSuborderqueryRequestV1(); request.setServiceUrl(properties.getSubOrderQueryUrl()); request.setBizContent(buildSubOrderQueryBiz(req)); CardbusinessOrderSuborderqueryResponseV1 response = buildClient().execute(request); CardbusinessOrderSuborderqueryResponseV1 response = executeIcbc("子订单查询", request); resp.setOrderStatus(response.getOrderStatus()); resp.setSubOutMerNo(response.getSubOutMerNo()); resp.setSubOutAgreeNo(response.getSubOutAgreeNo()); @@ -187,7 +236,8 @@ if (!verifyNotify(params)) { throw new IllegalArgumentException("工行回调验签失败"); } String status = firstNotBlank(params.get("orderStatus"), params.get("pay_status"), params.get("status")); mergeBizContent(params); String status = firstNotBlank(params.get("return_code"), params.get("orderStatus"), params.get("pay_status"), params.get("status")); if (StringUtils.isNotEmpty(status) && !isPaidStatus(status)) { throw new IllegalArgumentException("工行回调支付状态非成功:" + status); } @@ -210,45 +260,114 @@ private DefaultIcbcClient buildClient() { return new DefaultIcbcClient( properties.getAppId(), properties.getPrivateKey(), properties.getSignType(), properties.getCharset(), properties.getFormat(), properties.getPrivateKey(), properties.getIcbcPublicKey()); } private AggregatePaymentRequestV3.AggregatePaymentRequestV3Biz buildAggregatePaymentBiz(IcbcPaymentReq req) { AggregatePaymentRequestV3.AggregatePaymentRequestV3Biz biz = new AggregatePaymentRequestV3.AggregatePaymentRequestV3Biz(); biz.setApp_id(properties.getAppId()); biz.setCharset(properties.getCharset()); biz.setFormat(properties.getFormat()); biz.setSign_type(properties.getSignType()); biz.setInterfaceVersion("1.0.0"); biz.setMerId(properties.getMerId()); biz.setOutTradeNo(req.getOutTradeNo()); biz.setOrderAmount(toFen(req.getAmount())); biz.setTotal_amt(toFen(req.getAmount())); biz.setGoodsBody(req.getBody()); biz.setMerHint(req.getSubject()); private <T extends IcbcResponse> T executeIcbc(String scene, IcbcRequest<T> request) throws Exception { log.info("工行{}请求,url:{},bizContent:{}", scene, request.getServiceUrl(), JSON.toJSONString(request.getBizContent())); T response = buildClient().execute(request, System.currentTimeMillis() + ""); log.info("工行{}响应:{}", scene, JSON.toJSONString(response)); return response; } private void validateCommonConfig() { validateConfig("icbc.pay.appId", properties.getAppId()); validateConfig("icbc.pay.privateKey", properties.getPrivateKey()); validateConfig("icbc.pay.icbcPublicKey", properties.getIcbcPublicKey()); validateConfig("icbc.pay.merId", properties.getMerId()); } private void validateConfig(String name, String value) { if (StringUtils.isEmpty(value)) { throw new IllegalStateException("工行配置缺失:" + name); } } private void validatePaymentReq(IcbcPaymentReq req) { if (req == null) { throw new IllegalArgumentException("工行支付请求不能为空"); } validateConfig("outTradeNo", req.getOutTradeNo()); if (req.getAmount() == null || req.getAmount().compareTo(BigDecimal.ZERO) <= 0) { throw new IllegalArgumentException("工行支付金额必须大于0"); } if (Integer.valueOf(1).equals(req.getPaymentType()) || Integer.valueOf(2).equals(req.getPaymentType())) { validateConfig("icbc.pay." + (Integer.valueOf(1).equals(req.getPaymentType()) ? "wxTpAppId" : "aliTpAppId"), getTpAppId(req.getPaymentType())); } if (Integer.valueOf(1).equals(req.getPaymentType()) || Integer.valueOf(2).equals(req.getPaymentType())) { validateConfig("openId", req.getOpenId()); } } private CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2.CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2Biz buildConsumePurchaseBiz(IcbcPaymentReq req) { CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2.CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2Biz biz = new CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2.CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2Biz(); biz.setMer_id(properties.getMerId()); setMerPrtclNoIfPresent(biz::setMer_prtcl_no); biz.setOut_trade_no(req.getOutTradeNo()); biz.setIcbc_appid(properties.getAppId()); biz.setShop_appid(getTpAppId(req.getPaymentType())); if (Integer.valueOf(1).equals(req.getPaymentType()) || Integer.valueOf(2).equals(req.getPaymentType())) { biz.setOpen_id(req.getOpenId()); } setAlipayUnionIdIfNeeded(biz, req); biz.setPay_mode(toIcbcPayMode(req.getPaymentType())); biz.setAccess_type(toIcbcAccessType(req.getPaymentType())); biz.setOrig_date_time(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").format(new Date())); biz.setDecive_info(buildDeviceInfo(req)); biz.setBody(firstNotBlank(req.getBody(), req.getSubject(), "充电充值")); biz.setFee_type("001"); biz.setTotal_fee(toFen(req.getAmount())); biz.setSpbill_create_ip(properties.getSpbillCreateIp()); biz.setMer_url(StringUtils.isNotEmpty(properties.getNotifyUrl()) ? properties.getNotifyUrl() : req.getNotifyUrl()); biz.setReturn_url(req.getReturnUrl()); biz.setAttach(String.valueOf(req.getPaymentType())); biz.setTpOpenId(req.getOpenId()); biz.setNotifyUrl(StringUtils.isNotEmpty(properties.getNotifyUrl()) ? properties.getNotifyUrl() : req.getNotifyUrl()); biz.setReturnUrl(req.getReturnUrl()); biz.setOrderDate(new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())); biz.setChannelId(toIcbcChannel(req.getPaymentType())); biz.setSpbillCreateIp(properties.getSpbillCreateIp()); biz.setTpAppId(properties.getTpAppId()); // TODO 将 req.profitSharing/properties.profitSharing 按工行分账文档映射到子订单或分账请求。 biz.setNotify_type("HS"); biz.setResult_type("0"); biz.setOrder_apd_inf(req.getSubject()); return biz; } private void setAlipayUnionIdIfNeeded(CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2.CardbusinessAggregatepayB2cOnlineConsumepurchaseRequestV2Biz biz, IcbcPaymentReq req) { if (!Integer.valueOf(2).equals(req.getPaymentType()) && !Integer.valueOf(3).equals(req.getPaymentType())) { return; } if (StringUtils.isNotEmpty(req.getUnionId())) { biz.setUnion_id(truncate(req.getUnionId(), 16)); } } private String buildConsumePurchasePayData(CardbusinessAggregatepayB2cOnlineConsumepurchaseResponseV2 response, Integer paymentType) { Map<String, Object> payData = new HashMap<>(); payData.put("orderId", response.getOrder_id()); payData.put("outTradeNo", response.getOut_trade_no()); payData.put("totalAmt", response.getTotal_amt()); payData.put("payMode", response.getPay_mode()); payData.put("accessType", response.getAccess_type()); payData.put("tradeType", response.getTrade_type()); payData.put("wxDataPackage", response.getWx_data_package()); payData.put("zfbDataPackage", response.getZfb_data_package()); payData.put("unionDataPackage", response.getUnion_data_package()); if (Integer.valueOf(1).equals(paymentType) && StringUtils.isNotEmpty(response.getWx_data_package())) { payData.put("package", response.getWx_data_package()); } if ((Integer.valueOf(2).equals(paymentType) || Integer.valueOf(3).equals(paymentType)) && StringUtils.isNotEmpty(response.getZfb_data_package())) { payData.put("package", response.getZfb_data_package()); } return JSON.toJSONString(payData); } private CardbusinessAggregatepayB2cOnlineOrderqryRequestV2.CardbusinessAggregatepayB2cOnlineOrderqryRequestV2Biz buildOrderQueryBiz(String outTradeNo) { CardbusinessAggregatepayB2cOnlineOrderqryRequestV2.CardbusinessAggregatepayB2cOnlineOrderqryRequestV2Biz biz = new CardbusinessAggregatepayB2cOnlineOrderqryRequestV2.CardbusinessAggregatepayB2cOnlineOrderqryRequestV2Biz(); biz.setOut_trade_no(outTradeNo); biz.setDeal_flag("0"); biz.setIcbc_appid(properties.getAppId()); biz.setMer_id(properties.getMerId()); biz.setMer_prtcl_no(properties.getMerPrtclNo()); setMerPrtclNoIfPresent(biz::setMer_prtcl_no); return biz; } @@ -259,9 +378,9 @@ biz.setOut_trade_no(req.getOutTradeNo()); biz.setOuttrx_serial_no(req.getOutRefundNo()); biz.setRet_total_amt(toFen(req.getRefundAmount())); biz.setTrnsc_ccy("CNY"); biz.setTrnsc_ccy("001"); biz.setMer_id(properties.getMerId()); biz.setMer_prtcl_no(properties.getMerPrtclNo()); setMerPrtclNoIfPresent(biz::setMer_prtcl_no); biz.setIcbc_appid(properties.getAppId()); biz.setMer_acct(req.getMerAcct()); biz.setOrder_apd_inf(req.getReason()); @@ -287,11 +406,35 @@ item.put("mer_sp_infor", req.getMerSpInfor()); item.put("term_id", req.getTermId()); orderSubmitActInput.add(item); biz.setMerId(properties.getMerId()); biz.setMerPrtclNo(properties.getMerPrtclNo()); biz.setMerId(firstNotBlank(properties.getServiceMerId(), properties.getMerId())); biz.setMerPrtclNo(firstNotBlank(properties.getServiceMerPrtclNo(), properties.getMerPrtclNo())); biz.setIcbcAppid(properties.getAppId()); biz.setOrderNum("1"); biz.setOrderSubmitActInput(orderSubmitActInput); return biz; } private CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1.CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1Biz buildSubOrderRefundBiz(IcbcSubOrderRefundReq req) { CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1.CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1Biz biz = new CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1.CardbusinessAggregatepayB2cOnlineSersuborderrefundRequestV1Biz(); biz.setMerId(firstNotBlank(properties.getServiceMerId(), properties.getMerId())); biz.setMerPrtclNo(firstNotBlank(properties.getServiceMerPrtclNo(), properties.getMerPrtclNo())); biz.setIcbcAppid(properties.getAppId()); biz.setOperFlag("0"); biz.setSubOrderId(req.getSubOrderId()); biz.setSeqNo(req.getSeqNo()); biz.setBusiType(firstNotBlank(req.getBusiType(), "1")); biz.setOriTrxDate(req.getOriTrxDate()); biz.setOriMerId(firstNotBlank(req.getOriMerId(), properties.getMerId())); biz.setSubMerId(req.getSubMerId()); biz.setSubMerPrtclNo(req.getSubMerPrtclNo()); biz.setClassifyAmt(toFen(req.getRefundAmount())); biz.setRetSubOrderId(req.getRetSubOrderId()); biz.setOuttrxSerialNo(req.getOuttrxSerialNo()); biz.setRefundSource(req.getRefundSource()); biz.setMerAcct(req.getMerAcct()); biz.setMerSpInfor(req.getMerSpInfor()); biz.setTermId(req.getTermId()); return biz; } @@ -332,14 +475,46 @@ return amount == null ? "0" : amount.multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP).toPlainString(); } private String toIcbcChannel(Integer paymentType) { private String getTpAppId(Integer paymentType) { if (Integer.valueOf(1).equals(paymentType)) { return "WECHAT"; return firstNotBlank(properties.getWxTpAppId(), properties.getTpAppId()); } if (isAlipay(paymentType)) { return firstNotBlank(properties.getAliTpAppId(), properties.getTpAppId()); } return properties.getTpAppId(); } private boolean isAlipay(Integer paymentType) { return Integer.valueOf(2).equals(paymentType) || Integer.valueOf(3).equals(paymentType); } private void setMerPrtclNoIfPresent(java.util.function.Consumer<String> setter) { if (StringUtils.isNotEmpty(properties.getMerPrtclNo())) { setter.accept(properties.getMerPrtclNo()); } } private String buildDeviceInfo(IcbcPaymentReq req) { String value = firstNotBlank(req.getOpenId(), req.getUnionId(), "USER") + "_" + firstNotBlank(req.getDeviceInfo(), properties.getSpbillCreateIp(), "MINIPROGRAM"); return value.length() > 32 ? value.substring(0, 32) : value; } private String toIcbcPayMode(Integer paymentType) { if (Integer.valueOf(1).equals(paymentType)) { return "9"; } if (Integer.valueOf(2).equals(paymentType) || Integer.valueOf(3).equals(paymentType)) { return "ALIPAY"; return "10"; } return "ICBC"; return "9"; } private String toIcbcAccessType(Integer paymentType) { if (Integer.valueOf(3).equals(paymentType)) { return "7"; } return "9"; } private boolean verifyNotify(Map<String, String> params) { @@ -352,25 +527,69 @@ } try { String content = buildSignContent(params); return IcbcSignature.verify(content, properties.getSignType(), sign, properties.getCharset(), properties.getIcbcPublicKey()); String signType = firstNotBlank(params.get("sign_type"), properties.getSignType()); String publicKey = normalizePublicKey(properties.getIcbcPublicKey()); log.info("工行回调验签参数,signType:{},publicKeyLength:{},content:{}", signType, publicKey == null ? 0 : publicKey.length(), content); return IcbcSignature.verify(content, signType, sign, properties.getCharset(), publicKey); } catch (Exception e) { log.error("工行回调验签异常", e); return false; } } private void mergeBizContent(Map<String, String> params) { String bizContent = params.get("biz_content"); if (StringUtils.isEmpty(bizContent)) { return; } try { JSONObject biz = JSONObject.parseObject(bizContent); if (biz == null) { return; } for (Map.Entry<String, Object> entry : biz.entrySet()) { if (entry.getValue() != null && !params.containsKey(entry.getKey())) { params.put(entry.getKey(), String.valueOf(entry.getValue())); } } } catch (Exception e) { log.warn("工行回调biz_content解析失败,bizContent:{}", bizContent, e); } } private String normalizePublicKey(String publicKey) { if (publicKey == null) { return null; } String normalized = publicKey; String lower = normalized.toLowerCase(); int start = lower.indexOf("start public key"); int end = lower.indexOf("end public key"); if (start >= 0 && end > start) { normalized = normalized.substring(start + "start public key".length(), end); } return normalized .replace("-----BEGIN PUBLIC KEY-----", "") .replace("-----END PUBLIC KEY-----", "") .replace("#", "") .replaceAll("\\s+", ""); } private String buildSignContent(Map<String, String> params) { Map<String, String> sorted = new TreeMap<>(params); String api = sorted.remove("api"); sorted.remove("sign"); sorted.remove("sign_type"); StringBuilder content = new StringBuilder(); StringBuilder content = new StringBuilder(firstNotBlank(api, "")); content.append('?'); boolean hasParam = false; for (Map.Entry<String, String> entry : sorted.entrySet()) { if (StringUtils.isEmpty(entry.getValue())) { continue; } if (content.length() > 0) { if (hasParam) { content.append('&'); } hasParam = true; content.append(entry.getKey()).append('=').append(entry.getValue()); } return content.toString(); @@ -388,4 +607,11 @@ } return ""; } private String truncate(String value, int maxLength) { if (StringUtils.isEmpty(value) || value.length() <= maxLength) { return value; } return value.substring(0, maxLength); } }