首页 / 资讯中心 / 文章详情

微信小程序支付后台Java实现:签名验签、回调幂等与退款闭环

微信小程序支付后台Java实现:签名验签、回调幂等与退款闭环 ★ FEATURED ARTICLE
简介本资源是一份面向Java后端开发者与微信小程序项目实践者的支付功能落地指南聚焦小程序JSAPI支付在Java服务端的完整实现路径。内容涵盖OpenId获取、订单号生成、统一下单接口调用、XML解析与二次签名、前端支付唤起、环境变量安全配置、异常处理及notify回调验证等核心环节直击小程序支付开发中的典型坑点与最佳实践。资源为1个71KB的PDF文档结构清晰含可直接参考的LeanCloud云引擎部署示例代码重点展示了基于TreeMap排序签名、WXPayUtil工具调用、AVObject订单查询及支付参数动态组装等关键逻辑。已有2340人学习下载适合具备Java基础与HTTP通信经验的开发者快速掌握微信支付后台集成要点规避硬编码敏感信息、签名失效、回调验签失败等高频问题。1. 微信小程序支付后台 Java 实现不是调个 API 就完事而是要扛住并发、验得准签名、退得清账单你写完小程序前端的wx.requestPayment点下“立即支付”按钮页面卡住三秒后弹出“支付失败签名错误”——这不是前端锅是后台 Java 服务在签名生成环节漏了字段、时间戳没对齐、证书路径读错了或者更隐蔽微信回调通知被 Nginx 丢包、验签时用了 HTTP Body 原始流却没关 Spring Boot 的ContentCachingRequestWrapper缓存开关。微信小程序支付后台 Java 实现本质是一套「高可信链路」工程它必须在毫秒级完成商户订单落库、统一下单接口调用、签名严格遵循微信 V3 规范含证书序列号、SHA256withRSA 签名、AES-256-GCM 加密响应、异步回调验签幂等处理状态更新财务对账触发。它不追求炫技但容不得半点松懈——一次签名错用户付不了款一次回调漏钱进了账却没发货一次幂等崩用户被扣双份。适合正在交付电商类、知识付费类、SaaS 工具类小程序的 Java 后端工程师尤其当你已踩过“沙箱能通正式环境炸”“本地调试收不到回调”“退款成功但用户没到账”这类坑需要一份可直接抄作业、带参数解释、带避坑清单、不绕弯子的落地笔记。2. 从零搭起支付后台选型、依赖与最小可运行骨架微信小程序支付后台 Java 实现核心不在“能不能做”而在“怎么稳着做”。常见误区是直接上手写HttpURLConnection调微信 API —— 这不是不行而是把签名、证书加载、HTTP 头构造、响应解密、异常重试这些重复劳动全揽身上后期维护成本陡增。我们采用微信官方推荐 社区验证成熟度最高的组合Spring Boot 2.7.x兼容 JDK 8/11 微信支付 V3 官方 SDKweixin-java-payv4.5.0 MySQL 5.7支撑订单与回调幂等表。不引入 RocketMQ/Kafka 做回调解耦——那是日订单 10W 的场景中小项目用数据库唯一索引 本地锁足矣也不硬上分布式事务框架——支付状态变更必须强一致用Transactional 补单任务兜底更可控。2.1 引入核心依赖看清版本边界与替代方案Mavenpom.xml中关键依赖如下注意版本锁定避免weixin-java-pay与 Spring Boot 版本冲突!-- 微信支付 V3 官方 SDK重点必须用 v4.5.0v3.x 不支持 V3 接口 -- dependency groupIdcom.github.binarywang/groupId artifactIdweixin-java-pay/artifactId version4.5.0/version /dependency !-- Spring Boot Web基础 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId version2.7.18/version /dependency !-- MySQL 驱动建议 8.0.33兼容 SSL 和时区 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- MyBatis Plus简化 CRUD非必须但极大提效 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency提示weixin-java-payv4.5.0 是当前2024 年中最稳定支持 V3 接口的版本。若你用 Spring Boot 3.xJDK 17需升至weixin-java-payv4.6.0否则WxPayService初始化会因 Jakarta EE 包路径报错。不要迷信“最新版”v4.5.0 经历过百万级订单压测比刚发布的 v4.6.2 更值得信赖。2.2 初始化 WxPayService四要素缺一不可证书路径是最大雷区微信支付 V3 接口强制 HTTPS 双向认证Java 后台必须加载平台证书.pem和商户私钥.p12。初始化WxPayService是整个链路起点代码必须放在Configuration类中且不能懒加载否则首次支付请求会卡在证书加载Configuration public class WxPayConfig { Value(${wxpay.mch-id}) // 商户号10位纯数字如 1900000100 private String mchId; Value(${wxpay.appid}) // 小程序 AppIDwx开头16位如 wx8888888888888888 private String appid; Value(${wxpay.api-v3-key}) // APIv3 密钥32位字母数字微信商户平台设置 private String apiV3Key; Value(${wxpay.private-key-path}) // 商户私钥 .p12 文件绝对路径如 /opt/certs/apiclient_cert.p12 private String privateKeyPath; Value(${wxpay.cert-path}) // 微信平台证书 .pem 文件绝对路径如 /opt/certs/wechatpay.pem private String certPath; Bean Scope(ConfigurableBeanFactory.SCOPE_SINGLETON) public WxPayService wxPayService() { WxPayConfig config new WxPayConfig(); config.setAppId(appid); config.setMchId(mchId); config.setMchKey(apiV3Key); // 注意此处是 APIv3 Key不是 API Key config.setPrivateKeyPath(privateKeyPath); config.setCertPath(certPath); // 关键必须显式设置证书密码默认为 mch_id但务必确认 config.setMchId(mchId); // 此行必须有用于构造 p12 密码 config.setKeyPassword(mchId); // p12 密码 商户号微信硬编码规则 WxPayService service new WxPayServiceImpl(); service.setConfig(config); return service; } }参数说明与血泪经验privateKeyPath必须是.p12文件不是.pem或.key且该文件由微信商户平台下载不能用 OpenSSL 自己转——微信校验证书指纹自转会导致签名失败。certPath平台证书.pem每 24 小时需自动更新SDK 内置定时任务但首次必须手动放对位置。keyPassword99% 的“签名错误”源于此。微信规定.p12密码 商户号mchId字符串形式不是数字。例如商户号1900000100密码就是1900000100不是1900000100int也不是mch_1900000100。setMchKey(apiV3Key)传入的是你在微信商户平台【API安全】→【APIv3密钥】里设置的 32 位密钥不是老的 API Key那个只用于 V2 接口。填错这里所有 V3 接口返回401 Unauthorized。2.3 创建订单实体与数据库表字段设计直指支付风控支付后台不是 CRUD而是围绕“资金流”建模。订单表t_order必须包含以下字段缺一不可字段名类型说明是否索引idBIGINT PK自增主键—out_trade_noVARCHAR(64)商户系统内部订单号全局唯一强烈建议用雪花 ID 或 UUIDUNIQUEtransaction_idVARCHAR(32)微信支付订单号回调时回传用于查单INDEXappidVARCHAR(32)小程序 AppID用于多租户隔离INDEXmch_idVARCHAR(32)商户号同上INDEXtotal_feeINT订单总金额分必须是整数不能带小数点—statusTINYINT订单状态0-待支付1-已支付2-已退款3-已关闭INDEXcreate_timeDATETIME创建时间INDEXpay_timeDATETIME支付成功时间回调更新INDEXnotify_countINT回调失败重试次数防刷—CREATE TABLE t_order ( id bigint NOT NULL AUTO_INCREMENT, out_trade_no varchar(64) NOT NULL COMMENT 商户订单号, transaction_id varchar(32) DEFAULT NULL COMMENT 微信订单号, appid varchar(32) NOT NULL COMMENT 小程序AppID, mch_id varchar(32) NOT NULL COMMENT 商户号, total_fee int NOT NULL COMMENT 总金额分, status tinyint NOT NULL DEFAULT 0 COMMENT 状态0待支付,1已支付,2已退款,3已关闭, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, pay_time datetime NULL DEFAULT NULL, notify_count int NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_out_trade_no (out_trade_no), KEY idx_status_ctime (status,create_time), KEY idx_transaction_id (transaction_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT支付订单主表;提示out_trade_no必须全局唯一且不能含特殊字符或空格。微信校验正则为^[a-zA-Z0-9_-]{1,32}$但实际建议用纯数字或字母下划线组合如ORD20240520123456789避免前端传参时 URL 编码问题。total_fee是分100 表示 1 元——这是微信硬性要求传100.00或100字符串都会报错。3. 统一下单接口生成 prepay_id 并返回给小程序小程序前端调用wx.requestPayment必须传入timeStamp、nonceStr、package、signType、paySign五个参数。其中package的值是prepay_idwx123...而prepay_id来自微信统一下单接口https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi。这一步是支付链路第一道闸门必须保证参数合法、签名正确、网络可达、超时合理。3.1 构造下单请求对象别漏scene_info否则 iOS 无法唤起支付微信 V3 下单接口要求 JSON Bodyweixin-java-paySDK 封装了WxPayUnifiedOrderV3Request但必须手动补全scene_info字段否则在 iOS 微信内打开小程序时wx.requestPayment会静默失败无报错但支付框不弹Service public class PayService { Autowired private WxPayService wxPayService; Autowired private OrderMapper orderMapper; public MapString, String createJsapiOrder(String openId, String outTradeNo, Integer totalFee, String description) { // 1. 构造请求对象 WxPayUnifiedOrderV3Request request new WxPayUnifiedOrderV3Request(); request.setAppid(wxPayService.getConfig().getAppId()); // 小程序 AppID request.setMchid(wxPayService.getConfig().getMchId()); // 商户号 request.setDescription(description); // 商品描述最长127字符 request.setOutTradeNo(outTradeNo); // 商户订单号 request.setAttach(miniapp); // 附加数据可选用于回调区分来源 request.setNotifyUrl(https://yourdomain.com/api/wx/notify); // 回调地址必须备案域名且 HTTPS // 2. 必填金额对象 WxPayAmount amount new WxPayAmount(); amount.setTotal(totalFee); // 单位分 amount.setCurrency(CNY); request.setAmount(amount); // 3. 必填支付者信息openId 是关键 WxPayPayer payer new WxPayPayer(); payer.setOpenid(openId); // 用户在小程序的 openid从登录态获取 request.setPayer(payer); // 4. ⚠️ 重点scene_info - iOS 唤起支付的救命字段 WxPaySceneInfo sceneInfo new WxPaySceneInfo(); WxPaySceneInfoDevice device new WxPaySceneInfoDevice(); device.setIp(127.0.0.1); // 用户真实 IP生产环境务必换成 request.getRemoteAddr() sceneInfo.setDevice(device); request.setSceneInfo(sceneInfo); try { // 5. 调用微信接口返回 WxPayUnifiedOrderV3Result WxPayUnifiedOrderV3Result result wxPayService.unifiedOrderV3(request); // 6. 构造小程序所需参数 MapString, String payParams new HashMap(); payParams.put(appId, wxPayService.getConfig().getAppId()); payParams.put(timeStamp, String.valueOf(System.currentTimeMillis() / 1000)); payParams.put(nonceStr, RandomUtil.randomString(32)); // 使用 Hutool 的随机工具 payParams.put(package, prepay_id result.getPrepayId()); payParams.put(signType, RSA); // 微信 V3 固定为 RSA // 7. 生成 paySign注意签名原文是 appIdtimeStampnonceStrpackage 的拼接 String signMessage String.format(%s\n%s\n%s\n%s\n, wxPayService.getConfig().getAppId(), payParams.get(timeStamp), payParams.get(nonceStr), payParams.get(package)); payParams.put(paySign, wxPayService.createSign(signMessage, HMACSHA256)); return payParams; } catch (WxPayException e) { log.error(统一下单失败outTradeNo: {}, error: {}, outTradeNo, e.getMessage(), e); throw new RuntimeException(支付下单异常 e.getMessage()); } } }关键逻辑说明scene_info.device.ip必须传用户真实公网 IP非127.0.0.1微信风控会校验。Nginx 反向代理时需配置proxy_set_header X-Real-IP $remote_addr;并在 Java 中用request.getHeader(X-Real-IP)获取。paySign生成V3 接口签名与 V2 不同不是对整个 JSON 签名而是对appIdtimeStampnonceStrpackage四个字符串按\n拼接后用 HMAC-SHA256 算哈希。SDK 的createSign()方法已封装此逻辑传入拼接字符串和算法名即可。description不能为空且长度 ≤127 字符。若含中文确保 UTF-8 编码否则微信返回INVALID_PARAMETER。3.2 Controller 层接收前端请求返回支付参数Controller 层只做轻量转换不处理业务逻辑RestController RequestMapping(/api/wx) public class WxPayController { Autowired private PayService payService; PostMapping(/jsapi/create-order) public ResultMapString, String createJsapiOrder(RequestBody JsapiOrderRequest request) { // 1. 校验必要参数openId、金额、订单号 if (StrUtil.isBlank(request.getOpenId()) || request.getTotalFee() null || request.getTotalFee() 0) { return Result.fail(参数错误openId 或金额缺失); } // 2. 生成唯一订单号生产环境建议用雪花 ID String outTradeNo ORD System.currentTimeMillis() RandomUtil.randomNumbers(6); // 3. 调用 Service 下单 MapString, String payParams payService.createJsapiOrder( request.getOpenId(), outTradeNo, request.getTotalFee(), request.getDescription() ); // 4. 同时落库状态0 待支付 Order order new Order(); order.setOutTradeNo(outTradeNo); order.setAppid(wxPayService.getConfig().getAppId()); order.setMchId(wxPayService.getConfig().getMchId()); order.setTotalFee(request.getTotalFee()); order.setStatus(0); order.setDescription(request.getDescription()); orderMapper.insert(order); return Result.success(payParams); } }提示JsapiOrderRequest是自定义 DTO包含openId、totalFee、description字段。不要在 Controller 里做金额计算或库存扣减——那是 Service 层的事。Controller 只负责“参数搬运工”角色保持轻量。4. 支付结果异步通知验签、幂等、状态更新的生死线微信不会等你wx.requestPayment返回成功才回调而是支付成功后立即发起 HTTP POST 到你配置的notify_url。这个回调是支付链路的“唯一真相源”一切以它为准。但它极不稳定可能延迟、可能重复、可能被劫持。你的后台必须做到100% 验签通过、100% 幂等处理、100% 状态准确更新、100% 失败重试。4.1 回调接口设计用原始 InputStream禁用 Spring Boot 自动解析微信回调 Body 是 AES-256-GCM 加密的 JSONSpring Boot 默认的RequestBody会提前读取流并关闭导致 SDK 无法解密。必须用HttpServletRequest原始流PostMapping(value /notify, consumes MediaType.APPLICATION_JSON_VALUE) public ResponseEntityString handleNotify(HttpServletRequest request) { try { // 1. 获取原始输入流关键不能用 RequestBody InputStream inputStream request.getInputStream(); String notifyData IOUtils.toString(inputStream, StandardCharsets.UTF_8); // 2. SDK 自动验签 解密内部调用 verifyAndDecryptNotification WxPayNotifyResult result wxPayService.parseOrderNotifyResult(notifyData); // 3. 验证业务字段状态必须为 SUCCESS且金额匹配 if (!SUCCESS.equals(result.getEvent().getType())) { log.warn(回调事件类型非 SUCCESS{}, result.getEvent().getType()); return ResponseEntity.ok(FAIL); } if (!SUCCESS.equals(result.getResource().getDecryptedResource().getSummary())) { log.warn(解密后支付状态非 SUCCESS{}, result.getResource().getDecryptedResource().getSummary()); return ResponseEntity.ok(FAIL); } // 4. 提取关键数据 String outTradeNo result.getResource().getDecryptedResource().getOutTradeNo(); String transactionId result.getResource().getDecryptedResource().getTransactionId(); Integer totalFee result.getResource().getDecryptedResource().getAmount().getTotal(); // 5. 执行幂等更新见 4.2 boolean updateSuccess payService.handlePaySuccess(outTradeNo, transactionId, totalFee); if (!updateSuccess) { log.warn(幂等更新失败outTradeNo: {}, outTradeNo); return ResponseEntity.ok(FAIL); } // 6. 返回 SUCCESS 告诉微信“我收到了” return ResponseEntity.ok(SUCCESS); } catch (WxPayException e) { log.error(解析回调失败, e); return ResponseEntity.ok(FAIL); } catch (IOException e) { log.error(读取请求流失败, e); return ResponseEntity.ok(FAIL); } }注意consumes MediaType.APPLICATION_JSON_VALUE是微信要求的 Header必须显式声明。返回SUCCESS字符串无 JSON 包裹且 HTTP 状态码必须是 200。4.2 幂等更新数据库唯一索引 乐观锁双保险handlePaySuccess方法是核心必须保证同一out_trade_no的多次回调只成功更新一次订单状态Transactional(rollbackFor Exception.class) public boolean handlePaySuccess(String outTradeNo, String transactionId, Integer totalFee) { // 1. 先查订单FOR UPDATE 行锁防并发 Order order orderMapper.selectOne(new QueryWrapperOrder() .eq(out_trade_no, outTradeNo) .last(FOR UPDATE)); if (order null) { log.warn(订单不存在outTradeNo: {}, outTradeNo); return false; } // 2. 状态校验只允许从 0待支付更新为 1已支付 if (order.getStatus() ! 0) { log.info(订单状态非待支付跳过更新outTradeNo: {}, status: {}, outTradeNo, order.getStatus()); return true; // 幂等视为成功 } // 3. 金额校验防恶意篡改回调 if (!Objects.equals(order.getTotalFee(), totalFee)) { log.error(金额不匹配outTradeNo: {}, db: {}, callback: {}, outTradeNo, order.getTotalFee(), totalFee); return false; } // 4. 更新状态乐观锁where status 0 UpdateWrapperOrder updateWrapper new UpdateWrapper(); updateWrapper.eq(out_trade_no, outTradeNo) .eq(status, 0) // 确保只更新待支付状态 .set(status, 1) .set(transaction_id, transactionId) .set(pay_time, new Date()) .set(notify_count, order.getNotifyCount() 1); int updated orderMapper.update(null, updateWrapper); if (updated 0) { log.warn(乐观锁更新失败可能已被其他回调更新outTradeNo: {}, outTradeNo); return true; // 幂等视为成功 } // 5. 发送支付成功消息如 MQ、邮件、短信此处省略 // notifyService.sendPaySuccessMsg(order); return true; }为什么用双重保障SELECT ... FOR UPDATE在查到订单瞬间加行锁阻止其他线程同时查到同一行。UPDATE ... WHERE status 0即使锁失效极端情况乐观锁条件也能保证只有“待支付”状态才能被更新。notify_count字段记录回调次数便于后续排查如某订单回调 5 次都失败说明网络或代码有顽疾。4.3 避坑微信回调的 4 个致命陷阱现象 1本地调试收不到回调Nginx 日志显示 404原因微信回调域名必须是已备案的 HTTPS 域名且notify_url路径必须与 Spring Bootserver.servlet.context-path一致。例如你配置server.servlet.context-path/api那么notify_url必须是https://your.com/api/wx/notify少/api就 404。解决检查 Nginx 配置是否透传了context-path用curl -v https://your.com/api/wx/notify测试能否访问到接口。现象 2回调返回SUCCESS但订单状态没变原因parseOrderNotifyResult()内部会调用verifyAndDecryptNotification()它需要微信平台证书.pem来验签。如果certPath指向的证书过期微信平台证书 24 小时轮换或文件权限不足Linux 下 Java 进程无读取权限解密失败SDK 直接抛WxPayException你 catch 了但没打日志误以为成功。解决在catch (WxPayException e)块中必须打印e.getMessage()和e.getCause()90% 的解密失败会在cause里暴露 “Certificate expired”。现象 3用户支付成功但小程序前端一直显示“支付中”原因wx.requestPayment的success回调只表示“微信客户端唤起成功”不代表支付完成。真正完成以回调为准。前端必须轮询你自己的订单查询接口如/api/order/status?outTradeNoxxx直到返回status1才跳转成功页。解决前端实现轮询间隔 1s最多 10 次后端提供轻量查询接口只查out_trade_no和status走索引。现象 4回调偶尔失败微信重试 5 次后停止原因微信重试机制是失败后 15s、30s、3m、10m、30m 各重试一次。如果你的接口耗时 15s如 DB 锁等待、远程调用第一次就超时触发重试。解决回调接口内禁止任何耗时操作如发邮件、调第三方 API、复杂计算所有异步任务如发券、更新库存用Async或 MQ 延后执行设置server.tomcat.connection-timeout1000010 秒让 Tomcat 主动断开长连接逼迫微信重试。5. 查询订单与申请退款闭环支付流程的最后两环支付不是单向动作必须支持“查单”和“退款”。用户问“我付没付”、“我要退款”你的后台得秒答。这两接口看似简单实则暗藏玄机查单要防刷退款要验资失败要可追溯。5.1 主动查单用out_trade_no或transaction_id别裸奔微信提供两个查单接口/v3/pay/transactions/out-trade-no/{out_trade_no}和/v3/pay/transactions/id/{transaction_id}。推荐优先用out_trade_no商户订单号因为它是你系统可控的而transaction_id是微信生成的用户不一定记得。Service public class OrderQueryService { Autowired private WxPayService wxPayService; public WxPayOrderQueryV3Result queryOrderByOutTradeNo(String outTradeNo) { try { // SDK 封装自动拼接 URL 和签名 return wxPayService.queryOrderV3(outTradeNo, null); } catch (WxPayException e) { log.error(查单失败outTradeNo: {}, outTradeNo, e); throw new RuntimeException(查单异常 e.getMessage()); } } }关键参数说明第二个参数是subMchId子商户号普通小程序不用填null。返回的WxPayOrderQueryV3Result包含statusSUCCESS/REFUND/NOTPAY/CLOSED、amount.total分、payer.openid等。不要用此接口替代回调它只是辅助手段最终状态以回调为准。高频查单如每秒 10 次会被微信限流应加缓存如 RedisTTL 5 分钟。5.2 申请退款金额、账户、凭证一个都不能少退款接口/v3/pay/transactions/out-trade-no/{out_trade_no}/refunds要求退款金额 ≤ 原订单金额商户账户余额充足微信后台校验必须传reason退款原因最长30字符生产环境必须传notify_url退款结果回调与支付回调分离。public WxPayRefundResult applyRefund(String outTradeNo, Integer refundFee, String reason) { WxPayRefundRequest request new WxPayRefundRequest(); request.setOutTradeNo(outTradeNo); request.setOutRefundNo(REF System.currentTimeMillis() RandomUtil.randomNumbers(6)); // 退款单号 request.setReason(reason); WxPayAmount amount new WxPayAmount(); amount.setTotal(refundFee); // 原订单金额分 amount.setRefund(refundFee); // 退款金额分 request.setAmount(amount); request.setNotifyUrl(https://yourdomain.com/api/wx/refund-notify); // 退款回调地址 try { return wxPayService.refundV3(request); } catch (WxPayException e) { log.error(退款申请失败outTradeNo: {}, refundFee: {}, outTradeNo, refundFee, e); throw new RuntimeException(退款异常 e.getMessage()); } }提示outRefundNo必须全局唯一且不能与out_trade_no相同。微信校验规则同一out_trade_no下所有outRefundNo必须不同。建议用REF前缀 时间戳 随机数。5.3 退款结果回调比支付回调更需谨慎微信退款回调结构与支付类似但字段名不同。关键点result字段值为SUCCESS成功、ABNORMAL异常、PROCESSING处理中refund_id是微信退款单号需存库用于对账必须校验out_refund_no和out_trade_no的关联关系防止恶意伪造回调。PostMapping(/refund-notify) public ResponseEntityString handleRefundNotify(HttpServletRequest request) { try { InputStream inputStream request.getInputStream(); String notifyData IOUtils.toString(inputStream, StandardCharsets.UTF_8); WxPayRefundNotifyResult result wxPayService.parseRefundNotifyResult(notifyData); if (!SUCCESS.equals(result.getEvent().getType())) { return ResponseEntity.ok(FAIL); } String outTradeNo result.getResource().getDecryptedResource().getOutTradeNo(); String outRefundNo result.getResource().getDecryptedResource().getOutRefundNo(); String refundId result.getResource().getDecryptedResource().getRefundId(); String status result.getResource().getDecryptedResource().getStatus(); // SUCCESS/ABNORMAL/PROCESSING // 校验outRefundNo 是否属于 outTradeNo 的退款 boolean valid refundService.validateRefundRelation(outTradeNo, outRefundNo); if (!valid) { log.error(退款关系校验失败outTradeNo: {}, outRefundNo: {}, outTradeNo, outRefundNo); return ResponseEntity.ok(FAIL); } // 更新退款单状态此处省略具体 SQL refundService.updateRefundStatus(outRefundNo, status, refundId); return ResponseEntity.ok(SUCCESS); } catch (Exception e) { log.error(解析退款回调失败, e); return ResponseEntity.ok(FAIL); } }6. 生产环境必做的 5 件小事让支付后台从“能跑”变成“敢上线”写完代码本地 Postman 调通甚至沙箱测试成功不等于能上生产。微信支付是资金通道生产环境必须做足防御。这 5 件事是我在线上扛过百万订单后每次新项目必做的 checklist少一件半夜就可能被报警电话叫醒。6.1 对账文件自动下载与解析微信每天凌晨推送前一日交易汇总微信提供对账单下载接口/v3/bill/tradebill返回 CSV 格式。必须每天自动拉取与你数据库t_order对账差额 0 立即告警。脚本核心逻辑# 每日凌晨 2 点执行crontab 0 2 * * * /usr/bin/java -jar /opt/pay-bill-checker.jar --date$(date -d yesterday %Y%m%d)Java 端用wxPayService.downloadBill()下载字节数组再用 OpenCSV 解析// CSV 字段顺序固定微信订单号,商户订单号,商户号,appid,交易状态,付款银行,... ListString[] records new CsvToBeanBuilder(new StringReader(new String(csvBytes))) .withType(CsvEntry.class).build().parse(); for (String[] record : records) { String transactionId record[0]; String outTradeNo record[1]; String status record[4]; // 交易状态 Integer totalFee Integer.parseInt(record[10]); // 总金额分 // 查询本地订单 Order order orderMapper.selectOne(new QueryWrapperOrder().eq(out_trade_no, outTradeNo)); if (order null) { alarmService.send(对账异常微信有单本地无单outTradeNo outTradeNo); continue; } if (!SUCCESS.equals(status) order.getStatus() 1) { alarmService p a hrefhttps://download.csdn.net/download/weixin_38680811/12780664 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
阅读完成 · 觉得有帮助?
咨询建站