diff --git a/src/main/java/com/sczx/pay/controller/PaymentController.java b/src/main/java/com/sczx/pay/controller/PaymentController.java index f9bf424..cf37ced 100644 --- a/src/main/java/com/sczx/pay/controller/PaymentController.java +++ b/src/main/java/com/sczx/pay/controller/PaymentController.java @@ -3,6 +3,7 @@ package com.sczx.pay.controller; import com.sczx.pay.dto.PaymentRequest; import com.sczx.pay.dto.PaymentResponse; import com.sczx.pay.dto.RefundRequest; +import com.sczx.pay.mapper.CompanyWechatConfigMapper; import com.sczx.pay.service.WechatPayService; import com.sczx.pay.utils.WXPayUtil; import org.slf4j.Logger; @@ -27,6 +28,9 @@ public class PaymentController { @Autowired private WechatPayService wechatPayService; + @Autowired + private CompanyWechatConfigMapper companyWechatConfigMapper; + /** * 小程序统一下单接口 */ @@ -165,7 +169,59 @@ public class PaymentController { return buildResponse("FAIL", "处理异常"); } } + @PostMapping("/refundNotify") + public String refundNotify(@PathVariable HttpServletRequest request) { + try { + // 读取微信退款回调数据 + StringBuilder sb = new StringBuilder(); + BufferedReader reader = request.getReader(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + + String xmlData = sb.toString(); + logger.info("收到微信退款通知, 数据: {}", xmlData); + + // 解析XML数据 + Map notifyMap = WXPayUtil.xmlToMap(xmlData); + + Long companyId = companyWechatConfigMapper.getCompanyIdByMchId(notifyMap.get("mch_id")); + + // 验证签名 + if (!wechatPayService.verifyNotifySign(companyId, notifyMap)) { + logger.warn("微信退款通知签名验证失败,公司ID: {}", companyId); + return buildResponse("FAIL", "签名失败"); + } + + String returnCode = notifyMap.get("return_code"); + if (!"SUCCESS".equals(returnCode)) { + logger.warn("微信退款通知返回失败,公司ID: {}: {}", companyId, notifyMap.get("return_msg")); + return buildResponse("FAIL", "返回失败"); + } + + // 处理退款通知的业务逻辑 + String outRefundNo = notifyMap.get("out_refund_no"); + String refundId = notifyMap.get("refund_id"); + String refundStatus = notifyMap.get("refund_status"); + + // 更新数据库中的退款状态 + boolean success = wechatPayService.processRefundNotify(companyId, notifyMap); + if (success) { + logger.info("退款处理完成,公司ID: {}, 退款单号: {}, 微信退款单号: {}, 状态: {}", + companyId, outRefundNo, refundId, refundStatus); + return buildResponse("SUCCESS", "OK"); + } else { + logger.error("更新退款状态失败,公司ID: {}, 退款单号: {}", companyId, outRefundNo); + return buildResponse("FAIL", "更新退款状态失败"); + } + + } catch (Exception e) { + logger.error("处理微信退款通知异常", e); + return buildResponse("FAIL", "处理异常"); + } + } private String buildResponse(String returnCode, String returnMsg) { Map response = new HashMap<>(); response.put("return_code", returnCode); diff --git a/src/main/java/com/sczx/pay/entity/OrderMain.java b/src/main/java/com/sczx/pay/entity/OrderMain.java new file mode 100644 index 0000000..ff65765 --- /dev/null +++ b/src/main/java/com/sczx/pay/entity/OrderMain.java @@ -0,0 +1,28 @@ +package com.sczx.pay.entity; + +public class OrderMain { + private Long orderId; + private String orderNo; + private String orderStatus; + + public Long getOrderId() { + return orderId; + } + public void setOrderId(Long orderId) { + this.orderId = orderId; + } + + public String getOrderNo() { + return orderNo; + } + public void setOrderNo(String orderNo) { + this.orderNo = orderNo; + } + + public String getOrderStatus() { + return orderStatus; + } + public void setOrderStatus(String orderStatus) { + this.orderStatus = orderStatus; + } +} diff --git a/src/main/java/com/sczx/pay/entity/PayStatus.java b/src/main/java/com/sczx/pay/entity/PayStatus.java new file mode 100644 index 0000000..a070863 --- /dev/null +++ b/src/main/java/com/sczx/pay/entity/PayStatus.java @@ -0,0 +1,113 @@ +package com.sczx.pay.entity; + +/** + * 支付状态枚举 + */ +public enum PayStatus { + /** + * 未支付 + */ + NOTPAY(1, "NOTPAY", "未支付"), + + /** + * 支付成功 + */ + SUCCESS(2, "SUCCESS", "支付成功"), + + /** + * 转入退款 + */ + REFUND(3, "REFUND", "转入退款"), + + /** + * 已关闭 + */ + CLOSED(4, "CLOSED", "已关闭"), + + /** + * 已撤销 + */ + REVOKED(5, "REVOKED", "已撤销"), + + /** + * 支付失败 + */ + PAYERROR(6, "PAYERROR", "支付失败"); + + private final int id; + private final String code; + private final String description; + + PayStatus(int id, String code, String description) { + this.id = id; + this.code = code; + this.description = description; + } + + /** + * 根据ID获取支付状态 + */ + public static PayStatus fromId(int id) { + for (PayStatus status : PayStatus.values()) { + if (status.getId() == id) { + return status; + } + } + throw new IllegalArgumentException("未知的支付状态ID: " + id); + } + + /** + * 根据编码获取支付状态 + */ + public static PayStatus fromCode(String code) { + for (PayStatus status : PayStatus.values()) { + if (status.getCode().equals(code)) { + return status; + } + } + throw new IllegalArgumentException("未知的支付状态编码: " + code); + } + + /** + * 判断支付是否成功 + */ + public boolean isPaySuccess() { + return this == SUCCESS; + } + + /** + * 判断是否为退款状态 + */ + public boolean isRefund() { + return this == REFUND; + } + + /** + * 判断交易是否完成(成功或关闭) + */ + public boolean isTradeFinished() { + return this == SUCCESS || this == CLOSED || this == REVOKED; + } + + // Getter方法 + public int getId() { + return id; + } + + public String getCode() { + return code; + } + + public String getDescription() { + return description; + } + + @Override + public String toString() { + return "PayStatus{" + + "id=" + id + + ", code='" + code + '\'' + + ", description='" + description + '\'' + + '}'; + } +} diff --git a/src/main/java/com/sczx/pay/entity/PaymentNotifyRecord.java b/src/main/java/com/sczx/pay/entity/PaymentNotifyRecord.java new file mode 100644 index 0000000..472bde7 --- /dev/null +++ b/src/main/java/com/sczx/pay/entity/PaymentNotifyRecord.java @@ -0,0 +1,169 @@ +package com.sczx.pay.entity; + +import java.util.Date; + +/** + * 支付通知记录实体类 + */ +public class PaymentNotifyRecord { + private Long id; + private Long companyId; + private String outTradeNo; // 商户订单号 + private String outRefundNo; // 商户退款单号 + private String notifyData; // 通知原始数据 + private String notifyType; // 通知类型:PAY-支付通知,REFUND-退款通知 + private String tradeStatus; // 交易状态 + private String refundStatus; // 退款状态 + private String payChannel; // 支付渠道:WECHAT-微信支付,ALIPAY-支付宝 + private Integer processStatus; // 处理状态:0-未处理,1-已处理,2-处理失败 + private String processResult; // 处理结果 + private Date createTime; // 创建时间 + private Date updateTime; // 更新时间 + + // 通知类型枚举 + public enum NotifyType { + PAY("支付通知"), + REFUND("退款通知"); + + private final String description; + + NotifyType(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + } + + // 处理状态枚举 + public enum ProcessStatus { + UNPROCESSED(0, "未处理"), + PROCESSED(1, "已处理"), + FAILED(2, "处理失败"); + + private final int code; + private final String description; + + ProcessStatus(int code, String description) { + this.code = code; + this.description = description; + } + + public int getCode() { + return code; + } + + public String getDescription() { + return description; + } + } + + // 构造函数 + public PaymentNotifyRecord() {} + + // Getter和Setter方法 + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getCompanyId() { + return companyId; + } + + public void setCompanyId(Long companyId) { + this.companyId = companyId; + } + + public String getOutTradeNo() { + return outTradeNo; + } + + public void setOutTradeNo(String outTradeNo) { + this.outTradeNo = outTradeNo; + } + + public String getOutRefundNo() { + return outRefundNo; + } + + public void setOutRefundNo(String outRefundNo) { + this.outRefundNo = outRefundNo; + } + + public String getNotifyData() { + return notifyData; + } + + public void setNotifyData(String notifyData) { + this.notifyData = notifyData; + } + + public String getNotifyType() { + return notifyType; + } + + public void setNotifyType(String notifyType) { + this.notifyType = notifyType; + } + + public String getTradeStatus() { + return tradeStatus; + } + + public void setTradeStatus(String tradeStatus) { + this.tradeStatus = tradeStatus; + } + + public String getRefundStatus() { + return refundStatus; + } + + public void setRefundStatus(String refundStatus) { + this.refundStatus = refundStatus; + } + + public String getPayChannel() { + return payChannel; + } + + public void setPayChannel(String payChannel) { + this.payChannel = payChannel; + } + + public Integer getProcessStatus() { + return processStatus; + } + + public void setProcessStatus(Integer processStatus) { + this.processStatus = processStatus; + } + + public String getProcessResult() { + return processResult; + } + + public void setProcessResult(String processResult) { + this.processResult = processResult; + } + + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } + + public Date getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Date updateTime) { + this.updateTime = updateTime; + } +} diff --git a/src/main/java/com/sczx/pay/entity/RefundStatus.java b/src/main/java/com/sczx/pay/entity/RefundStatus.java new file mode 100644 index 0000000..cc2b465 --- /dev/null +++ b/src/main/java/com/sczx/pay/entity/RefundStatus.java @@ -0,0 +1,110 @@ +package com.sczx.pay.entity; + +/** + * 退款状态枚举 + */ +public enum RefundStatus { + /** + * 退款处理中 + */ + PROCESSING(1, "PROCESSING", "退款处理中"), + + /** + * 退款成功 + */ + SUCCESS(2, "SUCCESS", "退款成功"), + + /** + * 退款关闭 + */ + REFUNDCLOSE(3, "REFUNDCLOSE", "退款关闭"), + + /** + * 退款异常 + */ + CHANGE(4, "CHANGE", "退款异常"); + + private final int id; + private final String code; + private final String description; + + RefundStatus(int id, String code, String description) { + this.id = id; + this.code = code; + this.description = description; + } + + /** + * 根据ID获取退款状态 + */ + public static RefundStatus fromId(int id) { + for (RefundStatus status : RefundStatus.values()) { + if (status.getId() == id) { + return status; + } + } + throw new IllegalArgumentException("未知的退款状态ID: " + id); + } + + /** + * 根据编码获取退款状态 + */ + public static RefundStatus fromCode(String code) { + for (RefundStatus status : RefundStatus.values()) { + if (status.getCode().equals(code)) { + return status; + } + } + throw new IllegalArgumentException("未知的退款状态编码: " + code); + } + + /** + * 判断退款是否成功 + */ + public boolean isRefundSuccess() { + return this == SUCCESS; + } + + /** + * 判断退款是否完成(成功或关闭) + */ + public boolean isRefundFinished() { + return this == SUCCESS || this == REFUNDCLOSE; + } + + /** + * 判断退款是否异常 + */ + public boolean isRefundAbnormal() { + return this == CHANGE; + } + + /** + * 判断退款是否处理中 + */ + public boolean isProcessing() { + return this == PROCESSING; + } + + // Getter方法 + public int getId() { + return id; + } + + public String getCode() { + return code; + } + + public String getDescription() { + return description; + } + + @Override + public String toString() { + return "RefundStatus{" + + "id=" + id + + ", code='" + code + '\'' + + ", description='" + description + '\'' + + '}'; + } +} diff --git a/src/main/java/com/sczx/pay/mapper/CompanyWechatConfigMapper.java b/src/main/java/com/sczx/pay/mapper/CompanyWechatConfigMapper.java index 9dcb125..c9f44cf 100644 --- a/src/main/java/com/sczx/pay/mapper/CompanyWechatConfigMapper.java +++ b/src/main/java/com/sczx/pay/mapper/CompanyWechatConfigMapper.java @@ -15,4 +15,8 @@ public interface CompanyWechatConfigMapper { */ @Select("SELECT id, wechat_receiving_account AS mchId, wechat_key AS apikey FROM zc_company WHERE id = #{companyId}") CompanyWechatConfig getWechatConfigByCompanyId(@Param("companyId") Long companyId); + + + @Select("SELECT id FROM zc_company WHERE wechat_receiving_account = #{mchId}") + Long getCompanyIdByMchId(@Param("mchId") String mchId); } diff --git a/src/main/java/com/sczx/pay/mapper/OrderPayMapper.java b/src/main/java/com/sczx/pay/mapper/OrderPayMapper.java new file mode 100644 index 0000000..4dbafef --- /dev/null +++ b/src/main/java/com/sczx/pay/mapper/OrderPayMapper.java @@ -0,0 +1,51 @@ +package com.sczx.pay.mapper; + +import com.sczx.pay.entity.OrderMain; +import com.sczx.pay.entity.PayStatus; +import com.sczx.pay.entity.PaymentRecord; +import org.apache.ibatis.annotations.*; + +import java.util.Date; + +@Mapper +public interface OrderPayMapper { + + /** + * 根据商户订单号更新支付状态 + */ + @Update("UPDATE zc_order_main SET pay_type = #{payType}, pay_status = #{payStatus}, " + + "payment_no = #{transactionId}" + + "WHERE order_no = #{outTradeNo}") + int updatePaymentStatus(@Param("outTradeNo") String outTradeNo, + @Param("payType") String payType, + @Param("payStatus") String payStatus, + @Param("transactionId") String transactionId); + + @Update("update zc_order_main as om,zc_order_sub as os set os.transaction_id = #{transactionId},os.pay_status = #{payStatus}," + + "os.payment_method = #{payType}" + + " where om.order_id = os.order_id and om.order_no = #{outTradeNo}") + int updateSubOrderPaymentStatus(@Param("outTradeNo") String outTradeNo, + @Param("payType") String payType, + @Param("payStatus") String payStatus, + @Param("transactionId") String transactionId); + + @Update("UPDATE zc_order_main SET order_status = #{orderStatus}" + + "WHERE order_no = #{outTradeNo}") + int updateOrderStatus(@Param("outTradeNo") String outTradeNo, + @Param("orderStatus") String orderStatus); + + @Update("update zc_order_main as om,zc_order_sub as os set os.transaction_id = #{paymentId},os.pay_status = #{payStatus}" + + " where om.order_id = os.order_id and om.order_no = #{outTradeNo} and suborder_type = 'DEPOSIT'") + int updateRefundOrderStatus(@Param("outTradeNo") String outTradeNo, + @Param("orderStatus") String orderStatus, + @Param("transactionId") String transactionId); + + + @Select("select order_id,order_no,order_status from zc_order_main " + + "where order_id in (select order_id from zc_order_sub " + + "where payment_id = #{paymentId} and del_flag = '0')") + OrderMain getOrderStatusByOrderNo(@Param("paymentId") String paymentId); + + + +} diff --git a/src/main/java/com/sczx/pay/mapper/PaymentNotifyRecordMapper.java b/src/main/java/com/sczx/pay/mapper/PaymentNotifyRecordMapper.java new file mode 100644 index 0000000..a1be63d --- /dev/null +++ b/src/main/java/com/sczx/pay/mapper/PaymentNotifyRecordMapper.java @@ -0,0 +1,28 @@ +package com.sczx.pay.mapper; + +import com.sczx.pay.entity.PaymentNotifyRecord; +import org.apache.ibatis.annotations.*; + +@Mapper +public interface PaymentNotifyRecordMapper { + + /** + * 插入支付通知记录 + */ + @Insert("INSERT INTO payment_notify_record (company_id, out_trade_no, out_refund_no, notify_data, notify_type, " + + "trade_status, refund_status, pay_channel, process_status, process_result) " + + "VALUES (#{companyId}, #{outTradeNo}, #{outRefundNo}, #{notifyData}, #{notifyType}, " + + "#{tradeStatus}, #{refundStatus}, #{payChannel}, #{processStatus}, #{processResult})") + @Options(useGeneratedKeys = true, keyProperty = "id") + int insertPaymentNotifyRecord(PaymentNotifyRecord paymentNotifyRecord); + + /** + * 更新支付通知记录处理状态 + */ + @Update("UPDATE payment_notify_record SET process_status = #{processStatus}, process_result = #{processResult}, " + + "update_time = #{updateTime} WHERE id = #{id}") + int updateProcessStatus(@Param("id") Long id, + @Param("processStatus") Integer processStatus, + @Param("processResult") String processResult, + @Param("updateTime") java.util.Date updateTime); +} diff --git a/src/main/java/com/sczx/pay/service/WechatPayService.java b/src/main/java/com/sczx/pay/service/WechatPayService.java index 4b60e10..7d8fa6c 100644 --- a/src/main/java/com/sczx/pay/service/WechatPayService.java +++ b/src/main/java/com/sczx/pay/service/WechatPayService.java @@ -4,10 +4,9 @@ import com.sczx.pay.config.DynamicWXPayConfig; import com.sczx.pay.dto.PaymentRequest; import com.sczx.pay.dto.PaymentResponse; import com.sczx.pay.dto.RefundRequest; -import com.sczx.pay.entity.CompanyWechatConfig; -import com.sczx.pay.entity.PaymentRecord; -import com.sczx.pay.entity.RefundRecord; +import com.sczx.pay.entity.*; import com.sczx.pay.mapper.CompanyWechatConfigMapper; +import com.sczx.pay.mapper.OrderPayMapper; import com.sczx.pay.mapper.PaymentRecordMapper; import com.sczx.pay.mapper.RefundRecordMapper; import com.sczx.pay.sdk.WXPay; @@ -20,6 +19,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import javax.annotation.PostConstruct; import java.math.BigDecimal; import java.util.Date; import java.util.HashMap; @@ -33,6 +33,9 @@ public class WechatPayService { private static final Logger logger = LoggerFactory.getLogger(WechatPayService.class); + // 全局公网IP变量 + private static String SERVER_PUBLIC_IP = "127.0.0.1"; + @Autowired private CompanyWechatConfigMapper companyWechatConfigMapper; @@ -42,12 +45,39 @@ public class WechatPayService { @Autowired private RefundRecordMapper refundRecordMapper; + @Autowired + private OrderPayMapper orderPayMapper; + @Value("${wechat.pay.app-id}") private String appId; @Value("${wechat.pay.notify-url}") private String notifyUrl; + @Value("${wechat.pay.refund-notify-url}") + private String refundNotifyUrl; + + + /** + * 项目初始化时获取服务器公网IP + */ + @PostConstruct + public void initServerPublicIP() { + try { + SERVER_PUBLIC_IP = IPUtils.getServerPublicIP(); + logger.info("服务器公网IP初始化完成: {}", SERVER_PUBLIC_IP); + } catch (Exception e) { + logger.error("初始化服务器公网IP失败,使用默认IP: {}", SERVER_PUBLIC_IP, e); + } + } + + /** + * 获取服务器公网IP(静态方法) + */ + public static String getServerPublicIP() { + return SERVER_PUBLIC_IP; + } + /** * 小程序统一下单 */ @@ -80,13 +110,12 @@ public class WechatPayService { reqData.put("out_trade_no", request.getOutTradeNo()); reqData.put("total_fee", String.valueOf(request.getTotalFee())); // 自动获取服务器公网IP - reqData.put("spbill_create_ip", IPUtils.getServerPublicIP()); + reqData.put("spbill_create_ip", getServerPublicIP()); reqData.put("notify_url", notifyUrl); reqData.put("trade_type", "JSAPI"); reqData.put("openid", request.getOpenId()); reqData.put("attach", request.getCompanyId().toString()); - // 调用微信统一下单接口 Map result = wxPay.unifiedOrder(reqData); @@ -233,6 +262,8 @@ public class WechatPayService { reqData.put("out_refund_no", request.getOutRefundNo()); reqData.put("total_fee", String.valueOf(request.getTotalFee())); reqData.put("refund_fee", String.valueOf(request.getRefundFee())); + reqData.put("notify_url", refundNotifyUrl); + if (request.getRefundDesc() != null) { reqData.put("refund_desc", request.getRefundDesc()); @@ -365,7 +396,16 @@ public class WechatPayService { if (updated > 0) { logger.info("微信支付记录状态已更新,订单号: {}, 微信交易号: {}", outTradeNo, transactionId); - // TODO: 在这里调用其他业务服务更新实际订单状态 + //更新主订单状态 + OrderMain orderMain = orderPayMapper.getOrderStatusByOrderNo(outTradeNo); + String OrderStatus = orderMain.getOrderStatus(); + + if(OrderStatus.equals("WAIT_PAY")){ + orderPayMapper.updateOrderStatus(outTradeNo,"WAIT_PICK"); + }else if (OrderStatus.equals("RERENT_WAIT_PAY")){ + orderPayMapper.updateOrderStatus(outTradeNo,"RENT_ING"); + } + orderPayMapper.updateSubOrderPaymentStatus(outTradeNo,"WX_PAY",PayStatus.SUCCESS.getCode(),transactionId); return true; } else { logger.warn("未找到对应的微信支付记录,订单号: {}", outTradeNo); @@ -428,4 +468,58 @@ public class WechatPayService { return false; } } + + /** + * 处理退款通知并更新退款状态 + */ + @Transactional + public boolean processRefundNotify(Long companyId, Map notifyData) { + try { + String outRefundNo = notifyData.get("out_refund_no"); + String refundId = notifyData.get("refund_id"); + String refundStatus = notifyData.get("refund_status"); + String outTradeNo = notifyData.get("out_trade_no"); + + // 根据退款状态更新退款记录 + String statusDesc = ""; + switch (refundStatus) { + case "SUCCESS": + statusDesc = "退款成功"; + break; + case "REFUNDCLOSE": + statusDesc = "退款关闭"; + break; + case "PROCESSING": + statusDesc = "退款处理中"; + break; + case "CHANGE": + statusDesc = "退款异常"; + break; + default: + statusDesc = "未知状态"; + } + + int updated = refundRecordMapper.updateRefundStatus( + outRefundNo, + refundStatus, + statusDesc, + refundId, + "SUCCESS".equals(refundStatus) ? new Date() : null, // 退款成功时间 + new Date() // 更新时间 + ); + + if (updated > 0) { + logger.info("微信退款记录状态已更新,退款单号: {}, 微信退款单号: {}, 状态: {}", outRefundNo, refundId, refundStatus); + orderPayMapper.updateRefundOrderStatus(outTradeNo,"REFUND_SUCCESS",outRefundNo); + return true; + } else { + logger.warn("未找到对应的微信退款记录,退款单号: {}", outRefundNo); + return false; + } + } catch (Exception e) { + logger.error("处理微信退款通知异常,退款单号: {}", notifyData.get("out_refund_no"), e); + return false; + } + } + } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 17d51ce..c48b46a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,9 +1,10 @@ + server: port: 8019 spring: application: - name: sczx-singlepay # 微服务名称 + name: sczx_singlepay # 微服务名称 http: encoding: charset: UTF-8 @@ -18,7 +19,7 @@ spring: cloud: nacos: discovery: - server-addr: 115.190.8.52:8848 # Nacos 地址 + server-addr: 127.0.0.1:8848 # Nacos 地址 group: DEFAULT_GROUP metadata: version: 1.0.0 @@ -60,3 +61,4 @@ wechat: mch-id: your_mch_id key: your_api_key notify-url: http://115.190.8.52:8019/api/payment/notify + refund-notify-url: http://115.190.8.52:8019/api/payment/refundNotify