src/main/java/cc/mrbird/febs/common/configure/WebMvcConfigure.java
@@ -39,6 +39,7 @@ registration.excludePathPatterns("/api/tokenview/**"); registration.excludePathPatterns("/api/order/lwPayNotify"); registration.excludePathPatterns("/api/order/lwPayReturn"); registration.excludePathPatterns("/api/order/bsPayNotify"); // 添加Swagger UI相关路径 registration.excludePathPatterns("/api/swagger-ui.html"); src/main/java/cc/mrbird/febs/mall/controller/AdminSystemController.java
@@ -95,6 +95,28 @@ return new FebsResponse().success().message("保存成功"); } @PostMapping(value = "/bsPayConfig") @ControllerEndpoint(operation = "保存BSPAY配置") public FebsResponse bsPayConfig(BsPayConfigDto dto) { if (StrUtil.isBlank(dto.getMemberId())) { return new FebsResponse().fail().message("商户号不能为空"); } if (StrUtil.isBlank(dto.getSecretKey())) { return new FebsResponse().fail().message("签名密钥不能为空"); } if (StrUtil.isBlank(dto.getNotifyUrl())) { return new FebsResponse().fail().message("回调地址不能为空"); } if (StrUtil.isBlank(dto.getApiBaseUrl())) { return new FebsResponse().fail().message("API基础地址不能为空"); } commonService.addDataDic("BSPAY_CONFIG", "MEMBER_ID", dto.getMemberId(), "BSPAY商户号", false); commonService.addDataDic("BSPAY_CONFIG", "SECRET_KEY", dto.getSecretKey(), "BSPAY签名密钥", false); commonService.addDataDic("BSPAY_CONFIG", "NOTIFY_URL", dto.getNotifyUrl(), "BSPAY异步回调地址", false); commonService.addDataDic("BSPAY_CONFIG", "API_BASE_URL", dto.getApiBaseUrl(), "BSPAY API基础地址", false); return new FebsResponse().success().message("保存成功"); } @PostMapping(value = "/tokenviewConfig") @ControllerEndpoint(operation = "保存Tokenview配置") public FebsResponse tokenviewConfig(TokenviewConfigDto dto) { src/main/java/cc/mrbird/febs/mall/controller/ViewSystemController.java
@@ -147,6 +147,22 @@ return FebsUtil.view("modules/system/lwPayConfig"); } @GetMapping("bsPayConfig") @RequiresPermissions("bsPayConfig:update") public String bsPayConfig(Model model) { BsPayConfigDto dto = new BsPayConfigDto(); DataDictionaryCustom memberId = dataDictionaryCustomMapper.selectDicDataByTypeAndCode("BSPAY_CONFIG", "MEMBER_ID"); DataDictionaryCustom secretKey = dataDictionaryCustomMapper.selectDicDataByTypeAndCode("BSPAY_CONFIG", "SECRET_KEY"); DataDictionaryCustom notifyUrl = dataDictionaryCustomMapper.selectDicDataByTypeAndCode("BSPAY_CONFIG", "NOTIFY_URL"); DataDictionaryCustom apiBaseUrl = dataDictionaryCustomMapper.selectDicDataByTypeAndCode("BSPAY_CONFIG", "API_BASE_URL"); if (memberId != null) dto.setMemberId(memberId.getValue()); if (secretKey != null) dto.setSecretKey(secretKey.getValue()); if (notifyUrl != null) dto.setNotifyUrl(notifyUrl.getValue()); if (apiBaseUrl != null) dto.setApiBaseUrl(apiBaseUrl.getValue()); model.addAttribute("bsPayConfig", dto); return FebsUtil.view("modules/system/bsPayConfig"); } @GetMapping("tokenviewConfig") @RequiresPermissions("tokenviewConfig:update") public String tokenviewConfig(Model model) { src/main/java/cc/mrbird/febs/mall/controller/dependentStation/ApiMallOrderController.java
@@ -14,8 +14,10 @@ import cc.mrbird.febs.mall.vo.ApiOrderPayVo; import cc.mrbird.febs.mall.vo.OrderDetailVo; import cc.mrbird.febs.mall.vo.OrderListVo; import cc.mrbird.febs.pay.service.BsPayService; import cc.mrbird.febs.pay.service.IXcxPayService; import cc.mrbird.febs.pay.service.LwPayService; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; @@ -46,6 +48,7 @@ private final IApiMallOrderInfoService mallOrderInfoService; private final IXcxPayService iXcxPayService; private final LwPayService lwPayService; private final BsPayService bsPayService; private final IMallCountryDeliveryService countryDeliveryService; @ApiOperation(value = "创建订单--验证是否允许创建", notes = "创建订单--验证是否允许创建") @@ -353,6 +356,121 @@ return "redirect:/pages/payResult?orderNo=" + orderId + "&code=" + returncode; } // ==================== BSPAY (巴西PIX) 支付 ==================== /** * 创建订单并通过 BSPAY (巴西PIX) 支付 * <p> * 流程:创建订单 → 调 BSPAY 下单接口 → 返回支付 URL (mweb_url) * <p> * 交易类型:trade_type=201 (巴西PIX),货币:BRL */ @ApiOperation(value = "创建订单-BSPAY巴西PIX支付", notes = "创建订单并返回BSPAY PIX支付URL") @PostMapping(value = "/createOrderByBsPay") @Limit(key = "createOrderByBsPay", period = 1, count = 1, name = "BSPAY下单", prefix = "limit", limitType = LimitType.IP) public FebsResponse createOrderByBsPay(@RequestBody @Validated BsPayCreateOrderDto bsPayDto) { // 1. 创建订单 AddOrderDto addOrderDto = bsPayDto.getOrder(); Long orderId = mallOrderInfoService.createOrder(addOrderDto); // 2. 获取订单详情 MallOrderInfo order = mallOrderInfoService.getById(orderId); if (order != null && OrderStatusEnum.WAIT_PAY.getValue() == order.getStatus() ) { // 3. 调用 BSPAY 下单接口 try { String payUrl = bsPayService.createPayment(order); Map<String, Object> result = new HashMap<>(); result.put("orderNo", order.getOrderNo()); result.put("amount", order.getAmount()); result.put("payUrl", payUrl); return new FebsResponse().success().data(result); } catch (Exception e) { log.error("BSPAY 下单失败: orderId={}", orderId, e); return new FebsResponse().fail().message("Payment channel exception: " + e.getMessage()); } } return new FebsResponse().fail().message("Payment channel exception"); } @ApiOperation(value = "BSPAY巴西PIX支付", notes = "BSPAY巴西PIX支付") @ApiResponses({ @ApiResponse(code = 200, message = "success", response = ApiOrderPayVo.class) }) @PostMapping(value = "/payOrderByBsPay", produces = "application/json") public FebsResponse payOrderByBsPay(@RequestBody @Validated ApiOrderPayDto payDto) { Long orderId = payDto.getOrderId(); Integer payType = payDto.getPayType(); // 2. 获取订单详情 MallOrderInfo order = mallOrderInfoService.getById(orderId); if (order != null && OrderConstants.PAY_TYPE_BS == payType && OrderStatusEnum.WAIT_PAY.getValue() == order.getStatus() ) { // 3. 调用 BSPAY 下单接口 try { String payUrl = bsPayService.createPayment(order); Map<String, Object> result = new HashMap<>(); result.put("orderNo", order.getOrderNo()); result.put("amount", order.getAmount()); result.put("payUrl", payUrl); return new FebsResponse().success().data(result).message("success"); } catch (Exception e) { log.error("BSPAY 下单失败: orderId={}", orderId, e); return new FebsResponse().fail().message("Payment channel exception: " + e.getMessage()); } } return new FebsResponse().fail().message("Payment channel exception"); } /** * BSPAY 支付结果通知(服务端回调) * <p> * BSPAY 在支付完成后会 POST JSON 通知到此地址。 * 需在 BSPAY 下单时传 notify_url 为此地址。 * <p> * 注意:收到后必须响应 "SUCCESS",BSPAY 才会停止重试(最多5次)。 */ @ApiOperation(value = "BSPAY 支付回调", notes = "接收BSPAY异步通知") @PostMapping(value = "/bsPayNotify") public String bsPayNotify(HttpServletRequest request) { // 读取 JSON body try { java.io.BufferedReader reader = request.getReader(); StringBuilder sb = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { sb.append(line); } String body = sb.toString(); log.info("BSPAY 回调原始数据: {}", body); if (StrUtil.isBlank(body)) { log.error("BSPAY 回调数据为空"); return "FAIL"; } // 解析 JSON 为 Map Map<String, String> params = new HashMap<>(); com.alibaba.fastjson.JSONObject json = com.alibaba.fastjson.JSONObject.parseObject(body); for (String key : json.keySet()) { params.put(key, json.getString(key)); } log.info("BSPAY 回调参数: {}", params); boolean success = bsPayService.handleCallback(params); return success ? "SUCCESS" : "FAIL"; } catch (Exception e) { log.error("BSPAY 回调处理异常", e); return "FAIL"; } } @ApiOperation(value = "根据国家编码查询运费", notes = "根据国家编码查询对应运费") @ApiResponses({ @ApiResponse(code = 200, message = "success") src/main/java/cc/mrbird/febs/mall/controller/dependentStation/constant/OrderConstants.java
@@ -29,13 +29,14 @@ public static final String ORDER_STATE = "order_state"; /** * 支付方式 1-微信 2-支付宝 3-USDT 4-LWPAY支付 5-XT支付 * 支付方式 1-微信 2-支付宝 3-USDT 4-LWPAY支付 5-XT支付 6-BSPAY支付(巴西PIX) */ public static final Integer PAY_TYPE_WECHAT = 1; public static final Integer PAY_TYPE_ALIPAY = 2; public static final Integer PAY_TYPE_USDT = 3; public static final Integer PAY_TYPE_SYSTEM = 4; public static final Integer PAY_TYPE_XT = 5; public static final Integer PAY_TYPE_BS = 6; public static final String ORDER_PAY_TYPE = "order_pay_type"; public static final String TRC20_SYSTEM_ADDRESS = "pay.trc20.address"; src/main/java/cc/mrbird/febs/mall/dto/ApiOrderPayDto.java
@@ -17,6 +17,6 @@ private Long orderId; @NotNull(message = "支付方式不能为空") @ApiModelProperty(value = "支付方式 1-微信 2-支付宝 3-USDT 4-LwPAY 5-XTPAY", example = "you_ke_*****") @ApiModelProperty(value = "支付方式 1-微信 2-支付宝 3-USDT 4-LwPAY 5-XTPAY 6-BSPAY", example = "you_ke_*****") private Integer payType; } src/main/java/cc/mrbird/febs/mall/dto/BsPayConfigDto.java
New file @@ -0,0 +1,21 @@ package cc.mrbird.febs.mall.dto; import lombok.Data; /** * BSPAY (巴西PIX) 支付配置 DTO * 对应 data_dictionary_custom 表 type='BSPAY_CONFIG' 的多条记录 * * @author auto-generated */ @Data public class BsPayConfigDto { /** BSPAY 商户号 */ private String memberId; /** BSPAY 签名密钥 */ private String secretKey; /** 异步回调通知地址 */ private String notifyUrl; /** BSPAY API 基础地址 */ private String apiBaseUrl; } src/main/java/cc/mrbird/febs/mall/dto/BsPayCreateOrderDto.java
New file @@ -0,0 +1,23 @@ package cc.mrbird.febs.mall.dto; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import javax.validation.Valid; import javax.validation.constraints.NotNull; /** * BSPAY (巴西PIX) 创建订单请求 DTO * * @author auto-generated */ @Data @ApiModel(value = "BsPayCreateOrderDto", description = "BSPAY巴西PIX支付下单参数") public class BsPayCreateOrderDto { @Valid @NotNull(message = "订单信息不能为空") @ApiModelProperty(value = "订单信息", required = true) private AddOrderDto order; } src/main/java/cc/mrbird/febs/pay/service/BsPayService.java
New file @@ -0,0 +1,382 @@ package cc.mrbird.febs.pay.service; import cc.mrbird.febs.common.enumerates.OrderStatusEnum; import cc.mrbird.febs.common.utils.RedisUtils; import cc.mrbird.febs.mall.controller.dependentStation.constant.OrderConstants; import cc.mrbird.febs.mall.entity.DataDictionaryCustom; import cc.mrbird.febs.mall.entity.MallOrderInfo; import cc.mrbird.febs.mall.mapper.DataDictionaryCustomMapper; import cc.mrbird.febs.mall.mapper.MallOrderInfoMapper; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.SecureUtil; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import lombok.extern.slf4j.Slf4j; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Date; import java.util.Map; import java.util.TreeMap; /** * BSPAY (巴西PIX) 支付服务 * <p> * 处理 BSPAY 下单(PIX支付)和回调通知 * <p> * BSPAY API 说明: * - 传输方式:HTTPS,POST,JSON * - 签名算法:参数按 ASCII 字典序排序 → key=value 拼接 → 末尾追加 &key={secret} → MD5 → 大写 * - 下单接口:POST /api/pay/applyOrder (Content-Type: application/json) * - 支付回调:JSON POST,收到后需响应 "SUCCESS" * - 交易类型:trade_type=201 (巴西PIX) * - 货币类型:fee_type=BRL * - 金额单位:分 (1元=100分) * * @author auto-generated */ @Slf4j @Service public class BsPayService { @Resource private MallOrderInfoMapper mallOrderInfoMapper; @Resource private DataDictionaryCustomMapper dataDictionaryCustomMapper; @Resource private RedisUtils redisUtils; /** BSPAY 配置字典 type */ private static final String DICT_TYPE = "BSPAY_CONFIG"; /** BSPAY 下单接口路径 */ private static final String APPLY_ORDER_PATH = "/api/pay/applyOrder"; /** OkHttp JSON MediaType */ private static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json;charset=UTF-8"); /** 交易类型:巴西PIX */ private static final String TRADE_TYPE_PIX = "201"; /** 货币类型:巴西雷亚尔 */ private static final String FEE_TYPE_BRL = "BRL"; /** 签名类型 */ private static final String SIGN_TYPE_MD5 = "MD5"; // ==================== 下单接口 ==================== /** * 创建 BSPAY PIX 支付订单 → 获取支付跳转 URL * * @param order 订单实体 * @return BSPAY 支付页面 URL (mweb_url) */ public String createPayment(MallOrderInfo order) { String memberId = getConfigValue("MEMBER_ID"); String secretKey = getConfigValue("SECRET_KEY"); String notifyUrl = getConfigValue("NOTIFY_URL"); String apiBaseUrl = getConfigValue("API_BASE_URL"); if (StrUtil.isBlank(memberId) || StrUtil.isBlank(secretKey)) { throw new RuntimeException("BSPAY 商户号/秘钥未配置"); } if (StrUtil.isBlank(apiBaseUrl)) { throw new RuntimeException("BSPAY API基础地址未配置"); } if (StrUtil.isBlank(notifyUrl)) { throw new RuntimeException("BSPAY 回调地址未配置"); } // 金额转 BRL 分 (cents) String totalFee = getBRLAmountInCents(order.getAmount()); // 构建参数(TreeMap 自动按 ASCII 字典序排序) TreeMap<String, String> params = new TreeMap<>(); params.put("member_id", memberId); params.put("out_trade_no", order.getOrderNo()); params.put("body", "商品订单-" + order.getOrderNo()); params.put("total_fee", totalFee); params.put("fee_type", FEE_TYPE_BRL); params.put("notify_url", notifyUrl); params.put("trade_type", TRADE_TYPE_PIX); params.put("sign_type", SIGN_TYPE_MD5); // 生成签名 String sign = generateSign(params, secretKey); params.put("sign", sign); log.info("BSPAY 下单请求: memberId={}, orderNo={}, amount={}, totalFee={}", memberId, order.getOrderNo(), order.getAmount(), totalFee); // 发送 POST JSON 请求 String requestUrl = apiBaseUrl + APPLY_ORDER_PATH; String response = postJson(requestUrl, params); log.info("BSPAY 下单响应: {}", response); if (StrUtil.isBlank(response)) { throw new RuntimeException("BSPAY 响应为空"); } // 解析响应 JSONObject json = JSONObject.parseObject(response); // 检查通信状态 String returnCode = json.getString("return_code"); if (!"SUCCESS".equals(returnCode)) { String returnMsg = json.getString("return_msg"); String errCode = json.getString("err_code"); String errCodeDes = json.getString("err_code_des"); throw new RuntimeException("BSPAY 下单失败: " + returnMsg + (errCode != null ? " [" + errCode + ":" + errCodeDes + "]" : "")); } // 验证签名 String respSign = json.getString("sign"); if (StrUtil.isNotBlank(respSign)) { TreeMap<String, String> verifyParams = new TreeMap<>(); for (String key : json.keySet()) { if (!"sign".equals(key)) { String val = json.getString(key); if (StrUtil.isNotBlank(val)) { verifyParams.put(key, val); } } } if (!verifySign(verifyParams, secretKey, respSign)) { log.warn("BSPAY 响应签名验证失败,但仍继续处理"); } } // 检查业务结果 String resultCode = json.getString("result_code"); if (!"SUCCESS".equals(resultCode)) { throw new RuntimeException("BSPAY 下单业务失败: " + json.getString("err_code_des")); } String mwebUrl = json.getString("mweb_url"); if (StrUtil.isBlank(mwebUrl)) { throw new RuntimeException("BSPAY 下单未返回支付链接"); } // 记录平台支付订单号 String prepayId = json.getString("prepay_id"); if (StrUtil.isNotBlank(prepayId)) { mallOrderInfoMapper.update( null, Wrappers.lambdaUpdate(MallOrderInfo.class) .set(MallOrderInfo::getPayOrderNo, prepayId) .eq(MallOrderInfo::getId, order.getId()) ); } return mwebUrl; } // ==================== 回调处理 ==================== /** * 处理 BSPAY 支付结果通知 * <p> * 回调参数为 JSON 格式: * return_code, return_msg, member_id, sign, result_code, trade_type, * total_fee, transaction_id, out_trade_no, time_end * * @param params 回调参数 Map * @return true-处理成功, false-处理失败 */ public boolean handleCallback(Map<String, String> params) { String secretKey = getConfigValue("SECRET_KEY"); if (StrUtil.isBlank(secretKey)) { log.error("BSPAY 秘钥未配置,无法验签"); return false; } // 1. 验证签名(sign 不参与签名) String receivedSign = params.get("sign"); if (StrUtil.isBlank(receivedSign)) { log.error("BSPAY 回调缺少 sign 参数"); return false; } TreeMap<String, String> signParams = new TreeMap<>(); for (Map.Entry<String, String> entry : params.entrySet()) { if (!"sign".equals(entry.getKey()) && StrUtil.isNotBlank(entry.getValue())) { signParams.put(entry.getKey(), entry.getValue()); } } if (!verifySign(signParams, secretKey, receivedSign)) { log.error("BSPAY 回调签名验证失败: params={}", params); return false; } String returnCode = params.get("return_code"); String resultCode = params.get("result_code"); String outTradeNo = params.get("out_trade_no"); String totalFee = params.get("total_fee"); String transactionId = params.get("transaction_id"); String tradeType = params.get("trade_type"); log.info("BSPAY 回调: out_trade_no={}, return_code={}, result_code={}, total_fee={}, transaction_id={}", outTradeNo, returnCode, resultCode, totalFee, transactionId); // 2. 判断通信状态 if (!"SUCCESS".equals(returnCode)) { log.warn("BSPAY 回调通信失败: out_trade_no={}, return_code={}", outTradeNo, returnCode); return true; } // 3. 判断支付结果 if (!"SUCCESS".equals(resultCode)) { log.warn("BSPAY 支付未成功: out_trade_no={}, result_code={}", outTradeNo, resultCode); return true; } // 4. 查询订单 MallOrderInfo order = mallOrderInfoMapper.selectOne( Wrappers.lambdaQuery(MallOrderInfo.class) .eq(MallOrderInfo::getOrderNo, outTradeNo) ); if (order == null) { log.error("BSPAY 回调订单不存在: out_trade_no={}", outTradeNo); return false; } // 5. 幂等检查(避免重复回调) if (OrderStatusEnum.WAIT_PAY.getValue() != order.getStatus()) { log.info("BSPAY 订单已处理: out_trade_no={}, status={}", outTradeNo, order.getStatus()); return true; } // 6. 更新订单状态为待发货 mallOrderInfoMapper.update( null, Wrappers.lambdaUpdate(MallOrderInfo.class) .set(MallOrderInfo::getStatus, OrderStatusEnum.WAIT_SHIPPING.getValue()) .set(MallOrderInfo::getTradeHash, transactionId) .set(MallOrderInfo::getPayTime, new Date()) .set(MallOrderInfo::getPayMethod, "BSPAY支付(PIX)") .set(MallOrderInfo::getPayResult, "1") .eq(MallOrderInfo::getId, order.getId()) ); // 清理 Redis 中的金额匹配 key String amountKey = OrderConstants.TRC20_ORDER_KEY + order.getAmount(); redisUtils.del(amountKey); log.info("BSPAY 支付成功: out_trade_no={}, transaction_id={}, total_fee={}", outTradeNo, transactionId, totalFee); return true; } // ==================== 金额转换 ==================== /** * 将订单金额转换为 BRL 分(cents) * 1. 先按汇率转换为 BRL * 2. 再乘以 100 转换为分 * * @param amount 订单金额 * @return BRL 金额(分,不含小数) */ private String getBRLAmountInCents(BigDecimal amount) { DataDictionaryCustom dataDictionaryCustom = dataDictionaryCustomMapper.selectDicDataByTypeAndCode( "MONEY_CHANGE", "BRL" ); BigDecimal rate = new BigDecimal("5.18"); if (dataDictionaryCustom != null) { rate = new BigDecimal(dataDictionaryCustom.getValue()); } // 转 BRL → 转分 → 取整数 return amount.multiply(rate) .multiply(new BigDecimal("100")) .setScale(0, RoundingMode.DOWN) .toString(); } // ==================== 签名工具 ==================== /** * 生成 MD5 签名(大写) * <p> * 1. 参数按 ASCII 字典序排序 (TreeMap) * 2. key=value&key=value 拼接 * 3. 末尾追加 &key={secretKey} * 4. MD5 → 大写 */ private String generateSign(TreeMap<String, String> params, String secretKey) { String signStr = buildSignString(params) + "&key=" + secretKey; log.info("BSPAY 待签名字符串: {}", signStr); return SecureUtil.md5(signStr).toUpperCase(); } /** * 验证签名 */ private boolean verifySign(TreeMap<String, String> params, String secretKey, String expectedSign) { String calculatedSign = generateSign(params, secretKey); boolean valid = calculatedSign.equalsIgnoreCase(expectedSign); if (!valid) { log.warn("BSPAY 签名不匹配: expected={}, calculated={}", expectedSign, calculatedSign); } return valid; } /** * 将参数拼接为 key=value&key=value 格式(不含 &key=) */ private String buildSignString(TreeMap<String, String> params) { StringBuilder sb = new StringBuilder(); for (Map.Entry<String, String> entry : params.entrySet()) { if (StrUtil.isNotBlank(entry.getValue())) { if (sb.length() > 0) { sb.append("&"); } sb.append(entry.getKey()).append("=").append(entry.getValue()); } } return sb.toString(); } // ==================== 配置读取 ==================== /** * 从数据字典获取 BSPAY 配置 */ public String getConfigValue(String code) { DataDictionaryCustom dict = dataDictionaryCustomMapper.selectDicDataByTypeAndCode(DICT_TYPE, code); return dict != null ? dict.getValue() : null; } // ==================== HTTP 请求工具 ==================== /** * POST JSON 请求 */ private String postJson(String url, TreeMap<String, String> params) { try { String jsonBody = JSONObject.toJSONString(params); log.info("BSPAY POST JSON: url={}, body={}", url, jsonBody); OkHttpClient client = new OkHttpClient(); RequestBody body = RequestBody.create(JSON_MEDIA_TYPE, jsonBody); Request request = new Request.Builder() .url(url) .post(body) .build(); try (Response response = client.newCall(request).execute()) { if (response.body() != null) { return response.body().string(); } return null; } } catch (Exception e) { log.error("BSPAY POST JSON 异常: url={}", url, e); return null; } } } src/main/resources/templates/febs/views/modules/system/bsPayConfig.html
New file @@ -0,0 +1,88 @@ <div class="layui-fluid layui-anim febs-anim" id="bspay-config" lay-title="BSPAY巴西PIX支付设置"> <div class="layui-row layui-col-space8 febs-container"> <form class="layui-form" action="" lay-filter="bspay-config-form"> <div class="layui-card"> <div class="layui-card-header">BSPAY 巴西PIX支付参数配置</div> <div class="layui-card-body"> <div class="layui-form-item"> <label class="layui-form-label required">商户号:</label> <div class="layui-input-block"> <input type="text" name="memberId" data-th-id="${bsPayConfig.memberId}" lay-verify="required" autocomplete="off" class="layui-input" placeholder="BSPAY 商户号(member_id)"> </div> </div> <div class="layui-form-item"> <label class="layui-form-label required">签名密钥:</label> <div class="layui-input-block"> <input type="text" name="secretKey" data-th-id="${bsPayConfig.secretKey}" lay-verify="required" autocomplete="off" class="layui-input" placeholder="BSPAY 签名密钥/key"> </div> </div> <div class="layui-form-item"> <label class="layui-form-label required">回调地址:</label> <div class="layui-input-block"> <input type="text" name="notifyUrl" data-th-id="${bsPayConfig.notifyUrl}" lay-verify="required|url" autocomplete="off" class="layui-input" placeholder="异步回调通知地址(notify_url)"> </div> <div class="layui-word-aux" style="margin-left: 150px;">BSPAY 支付成功后异步通知的地址,必须以 http(s):// 开头</div> </div> <div class="layui-form-item"> <label class="layui-form-label required">API基础地址:</label> <div class="layui-input-block"> <input type="text" name="apiBaseUrl" data-th-id="${bsPayConfig.apiBaseUrl}" lay-verify="required|url" autocomplete="off" class="layui-input" placeholder="BSPAY API基础地址"> </div> <div class="layui-word-aux" style="margin-left: 150px;">BSPAY API服务器地址,如 https://api.bspay.com,下单路径为 {apiBaseUrl}/api/pay/applyOrder</div> </div> </div> <div class="layui-card-footer"> <button class="layui-btn layui-btn-normal" lay-submit="" lay-filter="bspay-config-form-submit" id="submit">保存</button> </div> </div> </form> </div> </div> <style> .layui-form-label { width: 120px; } .layui-form-item .layui-input-block { margin-left: 150px; } .layui-table-form .layui-form-item { margin-bottom: 20px !important; } </style> <script data-th-inline="javascript" type="text/javascript"> layui.use(['dropdown', 'jquery', 'validate', 'febs', 'form'], function () { var $ = layui.jquery, febs = layui.febs, form = layui.form, bsPayConfig = [[${bsPayConfig}]], validate = layui.validate, $view = $('#bspay-config'); form.verify(validate); if (bsPayConfig) { form.val("bspay-config-form", { "memberId": bsPayConfig.memberId, "secretKey": bsPayConfig.secretKey, "notifyUrl": bsPayConfig.notifyUrl, "apiBaseUrl": bsPayConfig.apiBaseUrl }); } form.render(); form.on('submit(bspay-config-form-submit)', function (data) { febs.post(ctx + 'admin/system/bsPayConfig', data.field, function (res) { febs.alert.success('保存成功'); }); return false; }); }); </script>