Commit 7ef99d77fc257123c78bc8454f3ba5ba65aba7ae
1 parent
e6fca59a
improve cashier api
Showing
43 changed files
with
1462 additions
and
191 deletions
cashier-boss/src/main/java/com/diligrp/cashier/boss/Constants.java
cashier-boss/src/main/java/com/diligrp/cashier/boss/controller/CashierDeskController.java
| ... | ... | @@ -8,12 +8,11 @@ import com.diligrp.cashier.boss.util.CashierOrderConverter; |
| 8 | 8 | import com.diligrp.cashier.pipeline.domain.OnlinePaymentStatus; |
| 9 | 9 | import com.diligrp.cashier.shared.domain.Message; |
| 10 | 10 | import com.diligrp.cashier.shared.util.AssertUtils; |
| 11 | -import com.diligrp.cashier.trade.domain.CashierOrder; | |
| 12 | -import com.diligrp.cashier.trade.domain.CashierPayment; | |
| 13 | -import com.diligrp.cashier.trade.domain.Merchant; | |
| 11 | +import com.diligrp.cashier.trade.domain.*; | |
| 14 | 12 | import jakarta.annotation.Resource; |
| 15 | 13 | import org.springframework.web.bind.annotation.RequestBody; |
| 16 | 14 | import org.springframework.web.bind.annotation.RequestMapping; |
| 15 | +import org.springframework.web.bind.annotation.RequestParam; | |
| 17 | 16 | import org.springframework.web.bind.annotation.RestController; |
| 18 | 17 | |
| 19 | 18 | @RestController |
| ... | ... | @@ -43,6 +42,11 @@ public class CashierDeskController { |
| 43 | 42 | return Message.success(paymentUrl); |
| 44 | 43 | } |
| 45 | 44 | |
| 45 | + @RequestMapping("/orderInfo") | |
| 46 | + public Message<?> orderInfo(@RequestParam("token") String token) { | |
| 47 | + return Message.success(cashierDeskService.getCashierOrderByToken(token)); | |
| 48 | + } | |
| 49 | + | |
| 46 | 50 | @RequestMapping("/orderPayment") |
| 47 | 51 | public Message<?> orderPayment(@RequestBody CashierPayment request) { |
| 48 | 52 | // 基本参数校验 |
| ... | ... | @@ -52,4 +56,36 @@ public class CashierDeskController { |
| 52 | 56 | OnlinePaymentStatus paymentStatus = cashierDeskService.doPayment(request); |
| 53 | 57 | return Message.success(paymentStatus); |
| 54 | 58 | } |
| 59 | + | |
| 60 | + @RequestMapping("/orderClose") | |
| 61 | + public Message<?> orderPayment(@RequestParam("paymentId") String paymentId) { | |
| 62 | + cashierDeskService.closePrepayOrder(paymentId); | |
| 63 | + return Message.success(); | |
| 64 | + } | |
| 65 | + | |
| 66 | + @RequestMapping(value = "/paymentState") | |
| 67 | + public Message<?> paymentState(@RequestParam("paymentId") String paymentId, | |
| 68 | + @RequestParam(name = "mode", required = false) String mode) { | |
| 69 | + | |
| 70 | + OnlinePaymentResult response = cashierDeskService.queryPaymentState(paymentId, mode); | |
| 71 | + return Message.success(response); | |
| 72 | + } | |
| 73 | + | |
| 74 | + @RequestMapping(value = "/orderRefund.do") | |
| 75 | + public Message<?> requestRefund(@RequestBody OnlineRefundDTO request) { | |
| 76 | + AssertUtils.notEmpty(request.getTradeId(), "tradeId missed"); | |
| 77 | + AssertUtils.notNull(request.getAmount(), "amount missed"); | |
| 78 | + AssertUtils.isTrue(request.getAmount() > 0, "Invalid amount"); | |
| 79 | + | |
| 80 | + OnlineRefundResult response = cashierDeskService.sendRefundRequest(request); | |
| 81 | + return Message.success(response); | |
| 82 | + } | |
| 83 | + | |
| 84 | + @RequestMapping(value = "/refundState") | |
| 85 | + public Message<?> refundState(@RequestParam("refundId") String refundId, | |
| 86 | + @RequestParam(name = "mode", required = false) String mode) { | |
| 87 | + | |
| 88 | + OnlineRefundResult response = cashierDeskService.queryRefundState(refundId, mode); | |
| 89 | + return Message.success(response); | |
| 90 | + } | |
| 55 | 91 | } | ... | ... |
cashier-boss/src/main/java/com/diligrp/cashier/boss/controller/WechatPaymentController.java
0 → 100644
| 1 | +package com.diligrp.cashier.boss.controller; | |
| 2 | + | |
| 3 | +import com.diligrp.cashier.boss.exception.BossServiceException; | |
| 4 | +import com.diligrp.cashier.boss.util.HttpUtils; | |
| 5 | +import com.diligrp.cashier.pipeline.core.WechatPartnerPipeline; | |
| 6 | +import com.diligrp.cashier.pipeline.core.WechatPipeline; | |
| 7 | +import com.diligrp.cashier.pipeline.domain.OnlinePaymentResponse; | |
| 8 | +import com.diligrp.cashier.pipeline.domain.OnlineRefundResponse; | |
| 9 | +import com.diligrp.cashier.pipeline.domain.wechat.*; | |
| 10 | +import com.diligrp.cashier.pipeline.service.IPaymentPipelineManager; | |
| 11 | +import com.diligrp.cashier.pipeline.service.IWechatPaymentService; | |
| 12 | +import com.diligrp.cashier.pipeline.type.OutPaymentType; | |
| 13 | +import com.diligrp.cashier.pipeline.type.PaymentState; | |
| 14 | +import com.diligrp.cashier.pipeline.util.WechatConstants; | |
| 15 | +import com.diligrp.cashier.pipeline.util.WechatSignatureUtils; | |
| 16 | +import com.diligrp.cashier.pipeline.util.WechatStateUtils; | |
| 17 | +import com.diligrp.cashier.shared.ErrorCode; | |
| 18 | +import com.diligrp.cashier.shared.domain.Message; | |
| 19 | +import com.diligrp.cashier.shared.util.DateUtils; | |
| 20 | +import com.diligrp.cashier.shared.util.JsonUtils; | |
| 21 | +import com.diligrp.cashier.trade.model.OnlinePayment; | |
| 22 | +import com.diligrp.cashier.trade.service.ICashierPaymentService; | |
| 23 | +import com.diligrp.cashier.trade.service.ITradeAssistantService; | |
| 24 | +import jakarta.annotation.Resource; | |
| 25 | +import jakarta.servlet.http.HttpServletRequest; | |
| 26 | +import org.slf4j.Logger; | |
| 27 | +import org.slf4j.LoggerFactory; | |
| 28 | +import org.springframework.http.HttpStatus; | |
| 29 | +import org.springframework.http.ResponseEntity; | |
| 30 | +import org.springframework.web.bind.annotation.PathVariable; | |
| 31 | +import org.springframework.web.bind.annotation.RequestMapping; | |
| 32 | +import org.springframework.web.bind.annotation.RequestParam; | |
| 33 | +import org.springframework.web.bind.annotation.RestController; | |
| 34 | + | |
| 35 | +import java.time.LocalDateTime; | |
| 36 | + | |
| 37 | +@RestController | |
| 38 | +@RequestMapping(value = "/wechat") | |
| 39 | +public class WechatPaymentController { | |
| 40 | + | |
| 41 | + private static final Logger LOG = LoggerFactory.getLogger(WechatPaymentController.class); | |
| 42 | + | |
| 43 | + @Resource | |
| 44 | + private IWechatPaymentService wechatPaymentService; | |
| 45 | + | |
| 46 | + @Resource | |
| 47 | + private ITradeAssistantService tradeAssistantService; | |
| 48 | + | |
| 49 | + @Resource | |
| 50 | + private ICashierPaymentService cashierPaymentService; | |
| 51 | + | |
| 52 | + @Resource | |
| 53 | + private IPaymentPipelineManager paymentPipelineManager; | |
| 54 | + | |
| 55 | + @RequestMapping(value = "/payment/openId.do") | |
| 56 | + public Message<?> openId(@RequestParam("pipelineId") Long pipelineId, @RequestParam("code") String code) { | |
| 57 | + String openId = wechatPaymentService.openIdByCode(pipelineId, code); | |
| 58 | + return Message.success(openId); | |
| 59 | + } | |
| 60 | + | |
| 61 | + @RequestMapping(value = "/payment/deliver.do") | |
| 62 | + public Message<?> deliverGoods(@RequestParam("paymentId") String paymentId, | |
| 63 | + @RequestParam("logisticsType") Integer logisticsType) { | |
| 64 | + OnlinePayment payment = tradeAssistantService.findByPaymentId(paymentId); | |
| 65 | + if (!PaymentState.SUCCESS.equalTo(payment.getState())) { | |
| 66 | + throw new BossServiceException(ErrorCode.INVALID_OBJECT_STATE, "微信订单未完成支付,不能进行发货操作"); | |
| 67 | + } | |
| 68 | + | |
| 69 | + UploadShippingRequest request = UploadShippingRequest.of(payment.getOutTradeNo(), logisticsType, | |
| 70 | + payment.getGoods(), payment.getPayerId()); | |
| 71 | + wechatPaymentService.deliverGoods(payment.getPipelineId(), request); | |
| 72 | + return Message.success(); | |
| 73 | + } | |
| 74 | + | |
| 75 | + /** | |
| 76 | + * 微信支付结果通知 | |
| 77 | + */ | |
| 78 | + @RequestMapping(value = "/payment/{paymentId}/notify.do") | |
| 79 | + public ResponseEntity<NotifyResult> paymentNotify(HttpServletRequest request, @PathVariable("paymentId") String paymentId) { | |
| 80 | + LOG.info("Receiving wechat payment result notify for {}", paymentId); | |
| 81 | + String payload = HttpUtils.httpBody(request); | |
| 82 | + | |
| 83 | + try { | |
| 84 | + OnlinePayment payment = tradeAssistantService.findByPaymentId(paymentId); | |
| 85 | + WechatPipeline pipeline = paymentPipelineManager.findPipelineById(payment.getPipelineId(), WechatPipeline.class); | |
| 86 | + if (dataVerify(request, pipeline, payload)) { | |
| 87 | + WechatNotifyResponse response = JsonUtils.fromJsonString(payload, WechatNotifyResponse.class); | |
| 88 | + OnlinePaymentResponse paymentResponse = paymentResponse(pipeline, response); | |
| 89 | + if (WechatConstants.NOTIFY_EVENT_TYPE.equals(response.getEvent_type())) { | |
| 90 | + cashierPaymentService.notifyPaymentResponse(paymentResponse); | |
| 91 | + } | |
| 92 | + return ResponseEntity.ok(NotifyResult.success()); | |
| 93 | + } else { | |
| 94 | + LOG.error("Wechat payment result notify data verify failed"); | |
| 95 | + return ResponseEntity.badRequest().body(NotifyResult.failure("Data verify failed")); | |
| 96 | + } | |
| 97 | + } catch (Exception ex) { | |
| 98 | + LOG.error("Process wechat payment result notify exception", ex); | |
| 99 | + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) | |
| 100 | + .body(NotifyResult.failure("Process wechat payment result notify exception")); | |
| 101 | + } | |
| 102 | + } | |
| 103 | + | |
| 104 | + /*@RequestMapping(value = "/refund/{refundId}/notify.do") | |
| 105 | + public ResponseEntity<NotifyResult> refundNotify(HttpServletRequest request, @PathVariable("refundId") String refundId) { | |
| 106 | + LOG.info("Receiving wechat refund result notify for {}", refundId); | |
| 107 | + String payload = HttpUtils.httpBody(request); | |
| 108 | + | |
| 109 | + try { | |
| 110 | + OnlinePayment refund = onlinePaymentService.findByRefundId(refundId); | |
| 111 | + WechatPipeline pipeline = paymentPipelineManager.findPipelineById(refund.getPipelineId(), WechatPipeline.class); | |
| 112 | + | |
| 113 | + if (dataVerify(request, pipeline, payload)) { | |
| 114 | + WechatNotifyResponse notifyResponse = JsonUtils.fromJsonString(payload, WechatNotifyResponse.class); | |
| 115 | + if (WechatConstants.REFUND_EVENT_TYPE.equals(notifyResponse.getEvent_type())) { | |
| 116 | + OnlineRefundResponse refundResponse = refundResponse(pipeline, notifyResponse); | |
| 117 | + onlinePaymentService.notifyRefundResult(refundResponse); | |
| 118 | + } | |
| 119 | + return ResponseEntity.ok(NotifyResult.success()); | |
| 120 | + } else { | |
| 121 | + LOG.error("Wechat refund result notify data verify failed"); | |
| 122 | + return ResponseEntity.badRequest().body(NotifyResult.failure("Data verify failed")); | |
| 123 | + } | |
| 124 | + } catch (Exception ex) { | |
| 125 | + LOG.error("Process wechat refund result notify exception", ex); | |
| 126 | + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) | |
| 127 | + .body(NotifyResult.failure("Process wechat refund result notify exception")); | |
| 128 | + } | |
| 129 | + }*/ | |
| 130 | + | |
| 131 | + private OnlinePaymentResponse paymentResponse(WechatPipeline pipeline, WechatNotifyResponse notifyResponse) throws Exception { | |
| 132 | + WechatNotifyResponse.Resource resource = notifyResponse.getResource(); | |
| 133 | + String payload = WechatSignatureUtils.decrypt(resource.getCiphertext(), resource.getNonce(), | |
| 134 | + resource.getAssociated_data(), pipeline.getClient().getWechatConfig().getApiV3Key()); | |
| 135 | + if (pipeline instanceof WechatPartnerPipeline) { | |
| 136 | + PartnerPaymentResponse response = JsonUtils.fromJsonString(payload, PartnerPaymentResponse.class); | |
| 137 | + LocalDateTime when = DateUtils.parseDateTime(response.getSuccess_time(), WechatConstants.RFC3339_FORMAT); | |
| 138 | + String payer = response.getPayer() == null ? null : response.getPayer().getSp_openid(); | |
| 139 | + PaymentState paymentState = WechatStateUtils.getPaymentState(response.getTrade_state()); | |
| 140 | + return new OnlinePaymentResponse(response.getOut_trade_no(), response.getTransaction_id(), OutPaymentType.WXPAY, | |
| 141 | + payer, when, paymentState, response.getTrade_state_desc()); | |
| 142 | + } else { | |
| 143 | + DirectPaymentResponse response = JsonUtils.fromJsonString(payload, DirectPaymentResponse.class); | |
| 144 | + LocalDateTime when = DateUtils.parseDateTime(response.getSuccess_time(), WechatConstants.RFC3339_FORMAT); | |
| 145 | + String payer = response.getPayer() == null ? null : response.getPayer().getOpenid(); | |
| 146 | + PaymentState paymentState = WechatStateUtils.getPaymentState(response.getTrade_state()); | |
| 147 | + return new OnlinePaymentResponse(response.getOut_trade_no(), response.getTransaction_id(), OutPaymentType.WXPAY, | |
| 148 | + payer, when, paymentState, response.getTrade_state_desc()); | |
| 149 | + } | |
| 150 | + } | |
| 151 | + | |
| 152 | + private OnlineRefundResponse refundResponse(WechatPipeline pipeline, WechatNotifyResponse notifyResponse) throws Exception { | |
| 153 | + WechatNotifyResponse.Resource resource = notifyResponse.getResource(); | |
| 154 | + String payload = WechatSignatureUtils.decrypt(resource.getCiphertext(), resource.getNonce(), | |
| 155 | + resource.getAssociated_data(), pipeline.getClient().getWechatConfig().getApiV3Key()); | |
| 156 | + RefundNotifyResponse response = JsonUtils.fromJsonString(payload, RefundNotifyResponse.class); | |
| 157 | + LocalDateTime when = DateUtils.parseDateTime(response.getSuccess_time(), WechatConstants.RFC3339_FORMAT); | |
| 158 | + PaymentState refundState = WechatStateUtils.getRefundState(response.getRefund_status()); | |
| 159 | + return OnlineRefundResponse.of(response.getOut_refund_no(), response.getRefund_id(), when, | |
| 160 | + refundState.getCode(), response.getRefund_status()); | |
| 161 | + } | |
| 162 | + | |
| 163 | + private boolean dataVerify(HttpServletRequest request, WechatPipeline pipeline, String payload) { | |
| 164 | + String serialNo = request.getHeader(WechatConstants.HEADER_SERIAL_NO); | |
| 165 | + String timestamp = request.getHeader(WechatConstants.HEADER_TIMESTAMP); | |
| 166 | + String nonce = request.getHeader(WechatConstants.HEADER_NONCE); | |
| 167 | + String sign = request.getHeader(WechatConstants.HEADER_SIGNATURE); | |
| 168 | + | |
| 169 | + try { | |
| 170 | + return pipeline.getClient().dataVerify(serialNo, timestamp, nonce, sign, payload); | |
| 171 | + } catch (Exception ex) { | |
| 172 | + LOG.error("Wechat result notify data verify failed", ex); | |
| 173 | + return false; | |
| 174 | + } | |
| 175 | + } | |
| 176 | + | |
| 177 | + public static class NotifyResult { | |
| 178 | + private String code; | |
| 179 | + private String message; | |
| 180 | + | |
| 181 | + public static NotifyResult success() { | |
| 182 | + NotifyResult result = new NotifyResult(); | |
| 183 | + result.code = "SUCCESS"; | |
| 184 | + result.message = "SUCCESS"; | |
| 185 | + return result; | |
| 186 | + } | |
| 187 | + | |
| 188 | + public static NotifyResult failure(String message) { | |
| 189 | + NotifyResult result = new NotifyResult(); | |
| 190 | + result.code = "FAILED"; | |
| 191 | + result.message = message; | |
| 192 | + return result; | |
| 193 | + } | |
| 194 | + | |
| 195 | + public String getCode() { | |
| 196 | + return code; | |
| 197 | + } | |
| 198 | + | |
| 199 | + public String getMessage() { | |
| 200 | + return message; | |
| 201 | + } | |
| 202 | + } | |
| 203 | + | |
| 204 | +} | ... | ... |
cashier-boss/src/main/java/com/diligrp/cashier/boss/domain/CashierOrderInfo.java
0 → 100644
| 1 | +package com.diligrp.cashier.boss.domain; | |
| 2 | + | |
| 3 | +import java.util.List; | |
| 4 | + | |
| 5 | +public class CashierOrderInfo { | |
| 6 | + // 业务系统用户标识 | |
| 7 | + private final String userId; | |
| 8 | + // 支付通道 | |
| 9 | + private final List<PaymentPipeline> pipelines; | |
| 10 | + | |
| 11 | + public CashierOrderInfo(String userId, List<PaymentPipeline> pipelines) { | |
| 12 | + this.userId = userId; | |
| 13 | + this.pipelines = pipelines; | |
| 14 | + } | |
| 15 | + | |
| 16 | + public String getUserId() { | |
| 17 | + return userId; | |
| 18 | + } | |
| 19 | + | |
| 20 | + public List<PaymentPipeline> getPipelines() { | |
| 21 | + return pipelines; | |
| 22 | + } | |
| 23 | + | |
| 24 | + public static class PaymentPipeline { | |
| 25 | + // 支付通道 | |
| 26 | + private final Long pipelineId; | |
| 27 | + // 支付渠道 | |
| 28 | + private final Integer channelId; | |
| 29 | + | |
| 30 | + public PaymentPipeline(Long pipelineId, Integer channelId) { | |
| 31 | + this.pipelineId = pipelineId; | |
| 32 | + this.channelId = channelId; | |
| 33 | + } | |
| 34 | + | |
| 35 | + public Long getPipelineId() { | |
| 36 | + return pipelineId; | |
| 37 | + } | |
| 38 | + | |
| 39 | + public Integer getChannelId() { | |
| 40 | + return channelId; | |
| 41 | + } | |
| 42 | + } | |
| 43 | +} | ... | ... |
cashier-boss/src/main/java/com/diligrp/cashier/boss/domain/CashierOrderToken.java
| ... | ... | @@ -60,7 +60,7 @@ public class CashierOrderToken { |
| 60 | 60 | return Base62Cipher.decodeLong(payload); |
| 61 | 61 | } |
| 62 | 62 | |
| 63 | - public static CashierOrderToken decode(String payload) { | |
| 63 | + public static CashierOrderToken decodeCashierOrder(String payload) { | |
| 64 | 64 | if (payload != null) { |
| 65 | 65 | return JsonUtils.fromJsonString(payload, CashierOrderToken.class); |
| 66 | 66 | } | ... | ... |
cashier-boss/src/main/java/com/diligrp/cashier/boss/service/ICashierDeskService.java
| 1 | 1 | package com.diligrp.cashier.boss.service; |
| 2 | 2 | |
| 3 | +import com.diligrp.cashier.boss.domain.CashierOrderInfo; | |
| 3 | 4 | import com.diligrp.cashier.boss.domain.CashierPaymentUrl; |
| 4 | 5 | import com.diligrp.cashier.pipeline.domain.OnlinePaymentStatus; |
| 5 | -import com.diligrp.cashier.trade.domain.CashierOrder; | |
| 6 | -import com.diligrp.cashier.trade.domain.CashierPayment; | |
| 7 | -import com.diligrp.cashier.trade.domain.Merchant; | |
| 6 | +import com.diligrp.cashier.trade.domain.*; | |
| 8 | 7 | |
| 9 | 8 | public interface ICashierDeskService { |
| 10 | 9 | /** |
| ... | ... | @@ -17,10 +16,36 @@ public interface ICashierDeskService { |
| 17 | 16 | CashierPaymentUrl doSubmit(Merchant merchant, CashierOrder order); |
| 18 | 17 | |
| 19 | 18 | /** |
| 19 | + * 根据收银台TOKEN信息获取订单信息 | |
| 20 | + */ | |
| 21 | + CashierOrderInfo getCashierOrderByToken(String token); | |
| 22 | + | |
| 23 | + /** | |
| 20 | 24 | * 提交收银台支付 |
| 21 | 25 | * |
| 22 | 26 | * @param payment - 支付信息 |
| 23 | 27 | * @return 支付状态 |
| 24 | 28 | */ |
| 25 | 29 | OnlinePaymentStatus doPayment(CashierPayment payment); |
| 30 | + | |
| 31 | + /** | |
| 32 | + * 关闭预支付订单 | |
| 33 | + */ | |
| 34 | + void closePrepayOrder(String paymentId); | |
| 35 | + | |
| 36 | + /** | |
| 37 | + * 查询支付状态 | |
| 38 | + */ | |
| 39 | + OnlinePaymentResult queryPaymentState(String paymentId, String mode); | |
| 40 | + | |
| 41 | + /** | |
| 42 | + * 交易退款 | |
| 43 | + */ | |
| 44 | + OnlineRefundResult sendRefundRequest(OnlineRefundDTO request); | |
| 45 | + | |
| 46 | + /** | |
| 47 | + * 查询退款状态 | |
| 48 | + */ | |
| 49 | + OnlineRefundResult queryRefundState(String refundId, String mode); | |
| 50 | + | |
| 26 | 51 | } | ... | ... |
cashier-boss/src/main/java/com/diligrp/cashier/boss/service/impl/CashierDeskServiceImpl.java
| ... | ... | @@ -2,19 +2,26 @@ package com.diligrp.cashier.boss.service.impl; |
| 2 | 2 | |
| 3 | 3 | import com.diligrp.cashier.boss.CashierDeskProperties; |
| 4 | 4 | import com.diligrp.cashier.boss.Constants; |
| 5 | +import com.diligrp.cashier.boss.domain.CashierOrderInfo; | |
| 5 | 6 | import com.diligrp.cashier.boss.domain.CashierOrderToken; |
| 6 | 7 | import com.diligrp.cashier.boss.domain.CashierPaymentUrl; |
| 8 | +import com.diligrp.cashier.boss.exception.BossServiceException; | |
| 7 | 9 | import com.diligrp.cashier.boss.service.ICashierDeskService; |
| 10 | +import com.diligrp.cashier.pipeline.core.PaymentPipeline; | |
| 8 | 11 | import com.diligrp.cashier.pipeline.domain.OnlinePaymentStatus; |
| 9 | -import com.diligrp.cashier.trade.domain.CashierOrder; | |
| 10 | -import com.diligrp.cashier.trade.domain.CashierPayment; | |
| 11 | -import com.diligrp.cashier.trade.domain.Merchant; | |
| 12 | -import com.diligrp.cashier.trade.domain.MerchantParams; | |
| 12 | +import com.diligrp.cashier.pipeline.service.IPaymentPipelineManager; | |
| 13 | +import com.diligrp.cashier.shared.ErrorCode; | |
| 14 | +import com.diligrp.cashier.shared.util.ObjectUtils; | |
| 15 | +import com.diligrp.cashier.trade.domain.*; | |
| 16 | +import com.diligrp.cashier.trade.model.TradeOrder; | |
| 13 | 17 | import com.diligrp.cashier.trade.service.ICashierPaymentService; |
| 18 | +import com.diligrp.cashier.trade.service.ITradeAssistantService; | |
| 19 | +import com.diligrp.cashier.trade.type.TradeState; | |
| 14 | 20 | import jakarta.annotation.Resource; |
| 15 | 21 | import org.springframework.data.redis.core.StringRedisTemplate; |
| 16 | 22 | import org.springframework.stereotype.Service; |
| 17 | 23 | |
| 24 | +import java.util.List; | |
| 18 | 25 | import java.util.concurrent.TimeUnit; |
| 19 | 26 | |
| 20 | 27 | @Service("cashierDeskService") |
| ... | ... | @@ -24,6 +31,12 @@ public class CashierDeskServiceImpl implements ICashierDeskService { |
| 24 | 31 | private ICashierPaymentService cashierPaymentService; |
| 25 | 32 | |
| 26 | 33 | @Resource |
| 34 | + private ITradeAssistantService tradeAssistantService; | |
| 35 | + | |
| 36 | + @Resource | |
| 37 | + private IPaymentPipelineManager paymentPipelineManager; | |
| 38 | + | |
| 39 | + @Resource | |
| 27 | 40 | private CashierDeskProperties cashierDeskProperties; |
| 28 | 41 | |
| 29 | 42 | @Resource |
| ... | ... | @@ -54,6 +67,29 @@ public class CashierDeskServiceImpl implements ICashierDeskService { |
| 54 | 67 | return new CashierPaymentUrl(tradeId, paymentUrl); |
| 55 | 68 | } |
| 56 | 69 | |
| 70 | + @Override | |
| 71 | + public CashierOrderInfo getCashierOrderByToken(String token) { | |
| 72 | + CashierOrderToken.decode(token, cashierDeskProperties.getSecretKey()); | |
| 73 | + String tokenKey = String.format(Constants.TOKEN_REDIS_KEY, token); | |
| 74 | + String payload = stringRedisTemplate.opsForValue().get(tokenKey); | |
| 75 | + if (ObjectUtils.isEmpty(payload)) { | |
| 76 | + throw new BossServiceException(ErrorCode.ILLEGAL_ARGUMENT_ERROR, "收银台订单超时过期,不能支付"); | |
| 77 | + } | |
| 78 | + | |
| 79 | + CashierOrderToken orderToken = CashierOrderToken.decodeCashierOrder(payload); | |
| 80 | + TradeOrder trade = tradeAssistantService.findByTradeId(orderToken.getTradeId()); | |
| 81 | + if (TradeState.isFinished(trade.getState())) { | |
| 82 | + throw new BossServiceException(ErrorCode.ILLEGAL_ARGUMENT_ERROR, "收银台订单已完成,不能进行支付"); | |
| 83 | + } | |
| 84 | + List<PaymentPipeline> pipelines = paymentPipelineManager.listPipelines(orderToken.getMchId(), PaymentPipeline.class); | |
| 85 | + if (pipelines.isEmpty()) { | |
| 86 | + throw new BossServiceException(ErrorCode.ILLEGAL_ARGUMENT_ERROR, "商户无可用的支付通道"); | |
| 87 | + } | |
| 88 | + List<CashierOrderInfo.PaymentPipeline> pipelineList = pipelines.stream().map(pipeline -> | |
| 89 | + new CashierOrderInfo.PaymentPipeline(pipeline.pipelineId(), pipeline.supportedChannel().getCode())).toList(); | |
| 90 | + return new CashierOrderInfo(orderToken.getUserId(), pipelineList); | |
| 91 | + } | |
| 92 | + | |
| 57 | 93 | /** |
| 58 | 94 | * 提交收银台支付 |
| 59 | 95 | * |
| ... | ... | @@ -62,10 +98,31 @@ public class CashierDeskServiceImpl implements ICashierDeskService { |
| 62 | 98 | */ |
| 63 | 99 | @Override |
| 64 | 100 | public OnlinePaymentStatus doPayment(CashierPayment payment) { |
| 65 | - return cashierPaymentService.doPayment(payment); | |
| 66 | - // TODO 是否需要删除token | |
| 67 | -// String token = CashierOrderToken.encode(Long.valueOf(payment.getTradeId()), cashierDeskProperties.getSecretKey()); | |
| 68 | -// String tokenKey = String.format(Constants.TOKEN_REDIS_KEY, token); | |
| 69 | -// redisTemplate.delete(tokenKey); | |
| 101 | + OnlinePaymentStatus paymentStatus = cashierPaymentService.doPayment(payment); | |
| 102 | + // 只要提交了收银台支付,无论是否支付成功,都使token失效,收银台页面将无法重新打开 | |
| 103 | + String token = CashierOrderToken.encode(Long.valueOf(payment.getTradeId()), cashierDeskProperties.getSecretKey()); | |
| 104 | + String tokenKey = String.format(Constants.TOKEN_REDIS_KEY, token); | |
| 105 | + stringRedisTemplate.delete(tokenKey); | |
| 106 | + return paymentStatus; | |
| 107 | + } | |
| 108 | + | |
| 109 | + @Override | |
| 110 | + public void closePrepayOrder(String paymentId) { | |
| 111 | + cashierPaymentService.closePrepayOrder(paymentId); | |
| 112 | + } | |
| 113 | + | |
| 114 | + @Override | |
| 115 | + public OnlinePaymentResult queryPaymentState(String paymentId, String mode) { | |
| 116 | + return cashierPaymentService.queryPaymentState(paymentId, mode); | |
| 117 | + } | |
| 118 | + | |
| 119 | + @Override | |
| 120 | + public OnlineRefundResult sendRefundRequest(OnlineRefundDTO request) { | |
| 121 | + return cashierPaymentService.sendRefundRequest(request); | |
| 122 | + } | |
| 123 | + | |
| 124 | + @Override | |
| 125 | + public OnlineRefundResult queryRefundState(String refundId, String mode) { | |
| 126 | + return cashierPaymentService.queryRefundState(refundId, mode); | |
| 70 | 127 | } |
| 71 | 128 | } | ... | ... |
cashier-boss/src/main/java/com/diligrp/cashier/boss/util/HttpUtils.java
0 → 100644
| 1 | +package com.diligrp.cashier.boss.util; | |
| 2 | + | |
| 3 | +import com.diligrp.cashier.boss.Constants; | |
| 4 | +import jakarta.servlet.http.HttpServletRequest; | |
| 5 | +import jakarta.servlet.http.HttpServletResponse; | |
| 6 | +import org.slf4j.Logger; | |
| 7 | +import org.slf4j.LoggerFactory; | |
| 8 | + | |
| 9 | +import java.io.BufferedReader; | |
| 10 | +import java.io.IOException; | |
| 11 | +import java.nio.charset.StandardCharsets; | |
| 12 | + | |
| 13 | +/** | |
| 14 | + * HTTP工具类 | |
| 15 | + */ | |
| 16 | +public final class HttpUtils { | |
| 17 | + | |
| 18 | + private static final Logger LOG = LoggerFactory.getLogger(HttpUtils.class); | |
| 19 | + | |
| 20 | + public static String httpBody(HttpServletRequest request) { | |
| 21 | + StringBuilder payload = new StringBuilder(); | |
| 22 | + try { | |
| 23 | + String line; | |
| 24 | + BufferedReader reader = request.getReader(); | |
| 25 | + while ((line = reader.readLine()) != null) { | |
| 26 | + payload.append(line); | |
| 27 | + } | |
| 28 | + } catch (IOException iex) { | |
| 29 | + LOG.error("Failed to extract http body", iex); | |
| 30 | + } | |
| 31 | + | |
| 32 | + return payload.toString(); | |
| 33 | + } | |
| 34 | + | |
| 35 | + public static void sendResponse(HttpServletResponse response, String payload) { | |
| 36 | + try { | |
| 37 | + response.setContentType(Constants.CONTENT_TYPE); | |
| 38 | + byte[] responseBytes = payload.getBytes(StandardCharsets.UTF_8); | |
| 39 | + response.setContentLength(responseBytes.length); | |
| 40 | + response.getOutputStream().write(responseBytes); | |
| 41 | + response.flushBuffer(); | |
| 42 | + } catch (IOException iex) { | |
| 43 | + LOG.error("Failed to write data packet back"); | |
| 44 | + } | |
| 45 | + } | |
| 46 | +} | ... | ... |
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/client/WechatDirectHttpClient.java
| ... | ... | @@ -4,6 +4,7 @@ import com.diligrp.cashier.pipeline.domain.*; |
| 4 | 4 | import com.diligrp.cashier.pipeline.domain.wechat.ErrorMessage; |
| 5 | 5 | import com.diligrp.cashier.pipeline.domain.wechat.WechatConfig; |
| 6 | 6 | import com.diligrp.cashier.pipeline.exception.PaymentPipelineException; |
| 7 | +import com.diligrp.cashier.pipeline.type.OutPaymentType; | |
| 7 | 8 | import com.diligrp.cashier.pipeline.type.PaymentState; |
| 8 | 9 | import com.diligrp.cashier.pipeline.util.WechatConstants; |
| 9 | 10 | import com.diligrp.cashier.pipeline.util.WechatSignatureUtils; |
| ... | ... | @@ -130,8 +131,8 @@ public class WechatDirectHttpClient extends WechatHttpClient { |
| 130 | 131 | Map<String, Object> payer = (Map<String, Object>) response.get("payer"); |
| 131 | 132 | String openId = payer == null ? null : (String) payer.get("openid"); |
| 132 | 133 | PaymentState state = WechatStateUtils.getPaymentState((String) response.get("trade_state")); |
| 133 | - return OnlinePaymentResponse.of((String) response.get("out_trade_no"), (String) response.get("transaction_id"), | |
| 134 | - openId, when, state, (String) response.get("trade_state_desc")); | |
| 134 | + return new OnlinePaymentResponse((String) response.get("out_trade_no"), (String) response.get("transaction_id"), | |
| 135 | + OutPaymentType.WXPAY, openId, when, state, (String) response.get("trade_state_desc")); | |
| 135 | 136 | } else { |
| 136 | 137 | LOG.info("Wechat query transaction status failed: {}", result.statusCode); |
| 137 | 138 | ErrorMessage message = JsonUtils.fromJsonString(result.responseText, ErrorMessage.class); | ... | ... |
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/client/WechatPartnerHttpClient.java
| ... | ... | @@ -5,6 +5,7 @@ import com.diligrp.cashier.pipeline.domain.*; |
| 5 | 5 | import com.diligrp.cashier.pipeline.domain.wechat.ErrorMessage; |
| 6 | 6 | import com.diligrp.cashier.pipeline.domain.wechat.WechatConfig; |
| 7 | 7 | import com.diligrp.cashier.pipeline.exception.PaymentPipelineException; |
| 8 | +import com.diligrp.cashier.pipeline.type.OutPaymentType; | |
| 8 | 9 | import com.diligrp.cashier.pipeline.type.PaymentState; |
| 9 | 10 | import com.diligrp.cashier.pipeline.util.WechatConstants; |
| 10 | 11 | import com.diligrp.cashier.pipeline.util.WechatSignatureUtils; |
| ... | ... | @@ -134,8 +135,8 @@ public class WechatPartnerHttpClient extends WechatHttpClient { |
| 134 | 135 | Map<String, Object> payer = (Map<String, Object>) response.get("payer"); |
| 135 | 136 | String openId = payer == null ? null : (String) payer.get("sp_openid"); // 获取服务商APPID下的openId,而非子商户APPID下的openId |
| 136 | 137 | PaymentState state = WechatStateUtils.getPaymentState((String) response.get("trade_state")); |
| 137 | - return OnlinePaymentResponse.of((String) response.get("out_trade_no"), (String) response.get("transaction_id"), | |
| 138 | - openId, when, state, (String) response.get("trade_state_desc")); | |
| 138 | + return new OnlinePaymentResponse((String) response.get("out_trade_no"), (String) response.get("transaction_id"), | |
| 139 | + OutPaymentType.WXPAY, openId, when, state, (String) response.get("trade_state_desc")); | |
| 139 | 140 | } else { |
| 140 | 141 | LOG.info("Wechat query transaction status failed: {}", result.statusCode); |
| 141 | 142 | ErrorMessage message = JsonUtils.fromJsonString(result.responseText, ErrorMessage.class); | ... | ... |
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/core/DiliCardPipeline.java
| ... | ... | @@ -15,7 +15,7 @@ import org.slf4j.LoggerFactory; |
| 15 | 15 | /** |
| 16 | 16 | * 地利园区卡支付通道模型 |
| 17 | 17 | */ |
| 18 | -public class DiliCardPipeline extends OnlinePipeline<DiliCardPipeline.CardParams> { | |
| 18 | +public class DiliCardPipeline extends PaymentPipeline<DiliCardPipeline.CardParams> { | |
| 19 | 19 | |
| 20 | 20 | private static final Logger LOG = LoggerFactory.getLogger(DiliCardPipeline.class); |
| 21 | 21 | ... | ... |
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/domain/OnlinePaymentResponse.java
| ... | ... | @@ -18,21 +18,6 @@ public class OnlinePaymentResponse extends OnlinePaymentStatus { |
| 18 | 18 | // 交易备注 |
| 19 | 19 | private final String message; |
| 20 | 20 | |
| 21 | - public static OnlinePaymentResponse of(String paymentId, String outTradeNo, LocalDateTime when, | |
| 22 | - PaymentState state, String message) { | |
| 23 | - return new OnlinePaymentResponse(paymentId, outTradeNo, null, null, when, state, message); | |
| 24 | - } | |
| 25 | - | |
| 26 | - public static OnlinePaymentResponse of(String paymentId, String outTradeNo, String payerId, | |
| 27 | - LocalDateTime when, PaymentState state, String message) { | |
| 28 | - return new OnlinePaymentResponse(paymentId, outTradeNo, null, payerId, when, state, message); | |
| 29 | - } | |
| 30 | - | |
| 31 | - public static OnlinePaymentResponse of(String paymentId, String outTradeNo, OutPaymentType outPayType, | |
| 32 | - LocalDateTime when, PaymentState state, String message) { | |
| 33 | - return new OnlinePaymentResponse(paymentId, outTradeNo, outPayType, null, when, state, message); | |
| 34 | - } | |
| 35 | - | |
| 36 | 21 | public OnlinePaymentResponse(String paymentId, String outTradeNo, OutPaymentType outPayType, |
| 37 | 22 | String payerId, LocalDateTime when, PaymentState state, String message) { |
| 38 | 23 | super(paymentId, outTradeNo, state); | ... | ... |
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/domain/card/CardPaymentResponse.java
| ... | ... | @@ -8,7 +8,8 @@ import java.time.LocalDateTime; |
| 8 | 8 | |
| 9 | 9 | public class CardPaymentResponse extends OnlinePaymentResponse { |
| 10 | 10 | |
| 11 | - public CardPaymentResponse(String paymentId, String outTradeNo, OutPaymentType outPayType, String payerId, LocalDateTime when, PaymentState state, String message) { | |
| 11 | + public CardPaymentResponse(String paymentId, String outTradeNo, OutPaymentType outPayType, String payerId, | |
| 12 | + LocalDateTime when, PaymentState state, String message) { | |
| 12 | 13 | super(paymentId, outTradeNo, outPayType, payerId, when, state, message); |
| 13 | 14 | } |
| 14 | 15 | } | ... | ... |
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/service/IWechatPaymentService.java
0 → 100644
| 1 | +package com.diligrp.cashier.pipeline.service; | |
| 2 | + | |
| 3 | +import com.diligrp.cashier.pipeline.domain.wechat.UploadShippingRequest; | |
| 4 | + | |
| 5 | +public interface IWechatPaymentService { | |
| 6 | + /** | |
| 7 | + * 根据登陆凭证code获取openId | |
| 8 | + */ | |
| 9 | + String openIdByCode(Long pipelineId, String code); | |
| 10 | + | |
| 11 | + /** | |
| 12 | + * 通知微信发货 | |
| 13 | + * 服务商模式下,解决买家微信付款成功,卖家微信商户号无法收到钱,需要去自己的微信后台操作一下【发货】 | |
| 14 | + */ | |
| 15 | + void deliverGoods(Long pipelineId, UploadShippingRequest request); | |
| 16 | +} | ... | ... |
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/service/impl/WechatPaymentServiceImpl.java
0 → 100644
| 1 | +package com.diligrp.cashier.pipeline.service.impl; | |
| 2 | + | |
| 3 | +import com.diligrp.cashier.pipeline.client.WechatHttpClient; | |
| 4 | +import com.diligrp.cashier.pipeline.core.WechatPartnerPipeline; | |
| 5 | +import com.diligrp.cashier.pipeline.core.WechatPipeline; | |
| 6 | +import com.diligrp.cashier.pipeline.domain.wechat.UploadShippingRequest; | |
| 7 | +import com.diligrp.cashier.pipeline.domain.wechat.WechatAccessToken; | |
| 8 | +import com.diligrp.cashier.pipeline.service.IPaymentPipelineManager; | |
| 9 | +import com.diligrp.cashier.pipeline.service.IWechatPaymentService; | |
| 10 | +import jakarta.annotation.Resource; | |
| 11 | +import org.springframework.stereotype.Service; | |
| 12 | + | |
| 13 | +@Service("wechatPaymentService") | |
| 14 | +public class WechatPaymentServiceImpl implements IWechatPaymentService { | |
| 15 | + | |
| 16 | + @Resource | |
| 17 | + private IPaymentPipelineManager paymentPipelineManager; | |
| 18 | + | |
| 19 | + /** | |
| 20 | + * 根据登陆凭证code获取openId | |
| 21 | + */ | |
| 22 | + @Override | |
| 23 | + public String openIdByCode(Long pipelineId, String code) { | |
| 24 | + WechatPipeline pipeline = paymentPipelineManager.findPipelineById(pipelineId, WechatPipeline.class); | |
| 25 | + return pipeline.getClient().loginAuthorization(code); | |
| 26 | + } | |
| 27 | + | |
| 28 | + /** | |
| 29 | + * 通知微信发货 | |
| 30 | + * 服务商模式下,解决买家微信付款成功,卖家微信商户号无法收到钱,需要去自己的微信后台操作一下【发货】 | |
| 31 | + */ | |
| 32 | + @Override | |
| 33 | + public void deliverGoods(Long pipelineId, UploadShippingRequest request) { | |
| 34 | + WechatPartnerPipeline pipeline = paymentPipelineManager.findPipelineById(pipelineId, WechatPartnerPipeline.class); | |
| 35 | + // 获取微信接口登录凭证,并调用微信发货信息录入接口 | |
| 36 | + WechatHttpClient client = pipeline.getClient(); | |
| 37 | + WechatAccessToken accessToken = client.getAccessToken(); | |
| 38 | + client.sendUploadShippingRequest(request, accessToken.getToken()); | |
| 39 | + } | |
| 40 | +} | ... | ... |
cashier-pipeline/src/main/java/com/diligrp/cashier/pipeline/type/PaymentState.java
| ... | ... | @@ -46,14 +46,6 @@ public enum PaymentState implements IEnumType { |
| 46 | 46 | return Arrays.asList(PaymentState.values()); |
| 47 | 47 | } |
| 48 | 48 | |
| 49 | - public static boolean isPending(int code) { | |
| 50 | - return PaymentState.PENDING.equalTo(code); | |
| 51 | - } | |
| 52 | - | |
| 53 | - public static boolean isProcessing(int code) { | |
| 54 | - return PaymentState.PROCESSING.equalTo(code); | |
| 55 | - } | |
| 56 | - | |
| 57 | 49 | public static boolean isFinished(int code) { |
| 58 | 50 | return PaymentState.SUCCESS.equalTo(code) || PaymentState.FAILED.equalTo(code); |
| 59 | 51 | } | ... | ... |
cashier-shared/src/main/java/com/diligrp/cashier/shared/spi/IPaymentEventListener.java
cashier-shared/src/main/java/com/diligrp/cashier/shared/spi/PaymentEvent.java
| ... | ... | @@ -3,8 +3,10 @@ package com.diligrp.cashier.shared.spi; |
| 3 | 3 | import java.time.LocalDateTime; |
| 4 | 4 | |
| 5 | 5 | public class PaymentEvent { |
| 6 | - // 支付ID | |
| 6 | + // 交易号 | |
| 7 | 7 | private final String tradeId; |
| 8 | + // 支付ID | |
| 9 | + private final String paymentId; | |
| 8 | 10 | // 支付状态 |
| 9 | 11 | private final Integer state; |
| 10 | 12 | // 业务系统订单号 |
| ... | ... | @@ -18,8 +20,10 @@ public class PaymentEvent { |
| 18 | 20 | // 交易描述 |
| 19 | 21 | private final String message; |
| 20 | 22 | |
| 21 | - public PaymentEvent(String tradeId, int state, String outTradeNo, Integer outPayType, String payerId, LocalDateTime when, String message) { | |
| 23 | + public PaymentEvent(String tradeId, String paymentId, int state, String outTradeNo, Integer outPayType, | |
| 24 | + String payerId, LocalDateTime when, String message) { | |
| 22 | 25 | this.tradeId = tradeId; |
| 26 | + this.paymentId = paymentId; | |
| 23 | 27 | this.state = state; |
| 24 | 28 | this.outTradeNo = outTradeNo; |
| 25 | 29 | this.outPayType = outPayType; |
| ... | ... | @@ -32,6 +36,10 @@ public class PaymentEvent { |
| 32 | 36 | return tradeId; |
| 33 | 37 | } |
| 34 | 38 | |
| 39 | + public String getPaymentId() { | |
| 40 | + return paymentId; | |
| 41 | + } | |
| 42 | + | |
| 35 | 43 | public Integer getState() { |
| 36 | 44 | return state; |
| 37 | 45 | } | ... | ... |
cashier-shared/src/main/java/com/diligrp/cashier/shared/spi/RefundEvent.java
0 → 100644
| 1 | +package com.diligrp.cashier.shared.spi; | |
| 2 | + | |
| 3 | +import java.time.LocalDateTime; | |
| 4 | + | |
| 5 | +public class RefundEvent { | |
| 6 | + // 退款单号 | |
| 7 | + private final String refundId; | |
| 8 | + // 原支付ID | |
| 9 | + private final String tradeId; | |
| 10 | + // 支付状态 | |
| 11 | + private final Integer state; | |
| 12 | + // 发生时间 | |
| 13 | + private final LocalDateTime when; | |
| 14 | + // 交易描述 | |
| 15 | + private final String message; | |
| 16 | + | |
| 17 | + public RefundEvent(String refundId, String tradeId, int state, LocalDateTime when, String message) { | |
| 18 | + this.refundId = refundId; | |
| 19 | + this.tradeId = tradeId; | |
| 20 | + this.state = state; | |
| 21 | + this.when = when; | |
| 22 | + this.message = message; | |
| 23 | + } | |
| 24 | + | |
| 25 | + public String getRefundId() { | |
| 26 | + return refundId; | |
| 27 | + } | |
| 28 | + | |
| 29 | + public String getTradeId() { | |
| 30 | + return tradeId; | |
| 31 | + } | |
| 32 | + | |
| 33 | + public Integer getState() { | |
| 34 | + return state; | |
| 35 | + } | |
| 36 | + | |
| 37 | + public LocalDateTime getWhen() { | |
| 38 | + return when; | |
| 39 | + } | |
| 40 | + | |
| 41 | + public String getMessage() { | |
| 42 | + return message; | |
| 43 | + } | |
| 44 | +} | ... | ... |
cashier-trade/src/main/java/com/diligrp/cashier/trade/Constants.java
| ... | ... | @@ -2,6 +2,15 @@ package com.diligrp.cashier.trade; |
| 2 | 2 | |
| 3 | 3 | public final class Constants { |
| 4 | 4 | |
| 5 | + // 支付通道延时队列 | |
| 6 | + public static final String PAYMENT_DELAY_QUEUE = "cashier.payment.delayQueue"; | |
| 7 | + | |
| 8 | + // 支付通道延时交换机 | |
| 9 | + public static final String PAYMENT_DELAY_EXCHANGE = "cashier.payment.delayExchange"; | |
| 10 | + | |
| 11 | + // 支付通道延时路由KEY | |
| 12 | + public static final String PAYMENT_DELAY_KEY = "cashier.payment.delayKey"; | |
| 13 | + | |
| 5 | 14 | // 默认订单超时时间-秒, 十分钟 |
| 6 | 15 | public static final int DEFAULT_ORDER_TIMEOUT_SECONDS = 10 * 60 * 1000; |
| 7 | 16 | |
| ... | ... | @@ -14,10 +23,4 @@ public final class Constants { |
| 14 | 23 | // 支付订单分布式锁超时时长-秒 |
| 15 | 24 | public static final int TRADE_LOCK_TIMEOUT_SECONDS = 15 * 1000; |
| 16 | 25 | |
| 17 | - // 微信支付openId参数 | |
| 18 | - private static final String PARAM_OPEN_ID = "openId"; | |
| 19 | - | |
| 20 | - // 微信服务商模式下mchId子商户号参数 | |
| 21 | - public static final String PARAM_MCH_ID = "mchId"; | |
| 22 | - | |
| 23 | 26 | } | ... | ... |
cashier-trade/src/main/java/com/diligrp/cashier/trade/TradeConfiguration.java
| ... | ... | @@ -2,11 +2,45 @@ package com.diligrp.cashier.trade; |
| 2 | 2 | |
| 3 | 3 | import com.diligrp.cashier.shared.mybatis.MybatisMapperSupport; |
| 4 | 4 | import org.mybatis.spring.annotation.MapperScan; |
| 5 | +import org.springframework.amqp.core.Binding; | |
| 6 | +import org.springframework.amqp.core.BindingBuilder; | |
| 7 | +import org.springframework.amqp.core.CustomExchange; | |
| 8 | +import org.springframework.amqp.core.Queue; | |
| 9 | +import org.springframework.context.annotation.Bean; | |
| 5 | 10 | import org.springframework.context.annotation.ComponentScan; |
| 6 | 11 | import org.springframework.context.annotation.Configuration; |
| 7 | 12 | |
| 13 | +import java.util.HashMap; | |
| 14 | +import java.util.Map; | |
| 15 | + | |
| 8 | 16 | @Configuration |
| 9 | 17 | @ComponentScan("com.diligrp.cashier.trade") |
| 10 | 18 | @MapperScan(basePackages = {"com.diligrp.cashier.trade.dao"}, markerInterface = MybatisMapperSupport.class) |
| 11 | 19 | public class TradeConfiguration { |
| 20 | + /** | |
| 21 | + * 支付通道MQ延时队列 | |
| 22 | + * 队列为持久化、非独占式且不自动删除的队列, 利用RabbitMQ延时插件实现延时功能https://github.com/rabbitmq/rabbitmq-delayed-message-exchange | |
| 23 | + */ | |
| 24 | + @Bean | |
| 25 | + public Queue paymentDelayQueue() { | |
| 26 | + return new Queue(Constants.PAYMENT_DELAY_QUEUE, true, false, false); | |
| 27 | + } | |
| 28 | + | |
| 29 | + /** | |
| 30 | + * 支付通道MQ延时交换机 | |
| 31 | + */ | |
| 32 | + @Bean | |
| 33 | + public CustomExchange paymentDelayExchange() { | |
| 34 | + Map<String, Object> arguments = new HashMap<>(); | |
| 35 | + arguments.put("x-delayed-type", "direct"); | |
| 36 | + return new CustomExchange(Constants.PAYMENT_DELAY_EXCHANGE, "x-delayed-message", true, false, arguments); | |
| 37 | + } | |
| 38 | + | |
| 39 | + /** | |
| 40 | + * 支付通道MQ延时队列和交换机的绑定 | |
| 41 | + */ | |
| 42 | + @Bean | |
| 43 | + public Binding paymentDelayBinding() { | |
| 44 | + return BindingBuilder.bind(paymentDelayQueue()).to(paymentDelayExchange()).with(Constants.PAYMENT_DELAY_KEY).noargs(); | |
| 45 | + } | |
| 12 | 46 | } | ... | ... |
cashier-trade/src/main/java/com/diligrp/cashier/trade/dao/IOnlinePaymentDao.java
| ... | ... | @@ -21,5 +21,6 @@ public interface IOnlinePaymentDao extends MybatisMapperSupport { |
| 21 | 21 | |
| 22 | 22 | int compareAndSetState(PaymentStateDTO paymentDTO); |
| 23 | 23 | |
| 24 | - List<OnlinePayment> findByTradeId(@Param("tradeId") String tradeId, @Param("state") Integer state); | |
| 24 | + List<OnlinePayment> listOnlinePayments(@Param("tradeId") String tradeId, @Param("type") Integer type, | |
| 25 | + @Param("state") Integer state); | |
| 25 | 26 | } | ... | ... |
cashier-trade/src/main/java/com/diligrp/cashier/trade/domain/OnlinePaymentResult.java
0 → 100644
| 1 | +package com.diligrp.cashier.trade.domain; | |
| 2 | + | |
| 3 | +import com.diligrp.cashier.pipeline.type.OutPaymentType; | |
| 4 | +import com.diligrp.cashier.pipeline.type.PaymentState; | |
| 5 | +import com.diligrp.cashier.shared.spi.PaymentEvent; | |
| 6 | + | |
| 7 | +import java.time.LocalDateTime; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * 在线支付结果 - 用于业务系统支付结果通知 | |
| 11 | + */ | |
| 12 | +public class OnlinePaymentResult extends PaymentEvent { | |
| 13 | + public static OnlinePaymentResult of(String tradeId, String paymentId, PaymentState state, String outTradeNo, | |
| 14 | + OutPaymentType outPayType, String payerId, LocalDateTime when, String message) { | |
| 15 | + Integer outPayTypeCode = outPayType != null ? outPayType.getCode() : null; | |
| 16 | + return new OnlinePaymentResult(tradeId, paymentId, state.getCode(), outTradeNo, outPayTypeCode, payerId, when, message); | |
| 17 | + } | |
| 18 | + | |
| 19 | + public OnlinePaymentResult(String tradeId, String paymentId, int state, String outTradeNo, Integer outPayType, | |
| 20 | + String payerId, LocalDateTime when, String message) { | |
| 21 | + super(tradeId, paymentId, state, outTradeNo, outPayType, payerId, when, message); | |
| 22 | + } | |
| 23 | +} | ... | ... |
cashier-trade/src/main/java/com/diligrp/cashier/trade/domain/OnlineRefundDTO.java
0 → 100644
| 1 | +package com.diligrp.cashier.trade.domain; | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * 退款申请 | |
| 5 | + */ | |
| 6 | +public class OnlineRefundDTO { | |
| 7 | + // 原支付号 | |
| 8 | + private String tradeId; | |
| 9 | + // 退款金额 | |
| 10 | + private Long amount; | |
| 11 | + // 回调地址 | |
| 12 | + private String notifyUri; | |
| 13 | + // 退款原因 | |
| 14 | + private String description; | |
| 15 | + | |
| 16 | + public String getTradeId() { | |
| 17 | + return tradeId; | |
| 18 | + } | |
| 19 | + | |
| 20 | + public void setTradeId(String tradeId) { | |
| 21 | + this.tradeId = tradeId; | |
| 22 | + } | |
| 23 | + | |
| 24 | + public Long getAmount() { | |
| 25 | + return amount; | |
| 26 | + } | |
| 27 | + | |
| 28 | + public void setAmount(Long amount) { | |
| 29 | + this.amount = amount; | |
| 30 | + } | |
| 31 | + | |
| 32 | + public String getNotifyUri() { | |
| 33 | + return notifyUri; | |
| 34 | + } | |
| 35 | + | |
| 36 | + public void setNotifyUri(String notifyUri) { | |
| 37 | + this.notifyUri = notifyUri; | |
| 38 | + } | |
| 39 | + | |
| 40 | + public String getDescription() { | |
| 41 | + return description; | |
| 42 | + } | |
| 43 | + | |
| 44 | + public void setDescription(String description) { | |
| 45 | + this.description = description; | |
| 46 | + } | |
| 47 | +} | ... | ... |
cashier-trade/src/main/java/com/diligrp/cashier/trade/domain/OnlineRefundResult.java
0 → 100644
| 1 | +package com.diligrp.cashier.trade.domain; | |
| 2 | + | |
| 3 | +import com.diligrp.cashier.shared.spi.RefundEvent; | |
| 4 | + | |
| 5 | +import java.time.LocalDateTime; | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * 退款结果 | |
| 9 | + */ | |
| 10 | +public class OnlineRefundResult extends RefundEvent { | |
| 11 | + public OnlineRefundResult(String refundId, String tradeId, int state, LocalDateTime when, String message) { | |
| 12 | + super(refundId, tradeId, state, when, message); | |
| 13 | + } | |
| 14 | +} | ... | ... |
cashier-trade/src/main/java/com/diligrp/cashier/trade/domain/PaymentStateDTO.java
| ... | ... | @@ -116,7 +116,7 @@ public class PaymentStateDTO { |
| 116 | 116 | } |
| 117 | 117 | |
| 118 | 118 | public Builder outPayType(OutPaymentType outPayType) { |
| 119 | - PaymentStateDTO.this.outPayType = outPayType.getCode(); | |
| 119 | + PaymentStateDTO.this.outPayType = outPayType != null ? outPayType.getCode() : null; | |
| 120 | 120 | return this; |
| 121 | 121 | } |
| 122 | 122 | |
| ... | ... | @@ -135,6 +135,11 @@ public class PaymentStateDTO { |
| 135 | 135 | return this; |
| 136 | 136 | } |
| 137 | 137 | |
| 138 | + public Builder state(Integer state) { | |
| 139 | + PaymentStateDTO.this.state = state; | |
| 140 | + return this; | |
| 141 | + } | |
| 142 | + | |
| 138 | 143 | public Builder description(String description) { |
| 139 | 144 | PaymentStateDTO.this.description = description; |
| 140 | 145 | return this; | ... | ... |
cashier-trade/src/main/java/com/diligrp/cashier/trade/domain/TaskMessage.java
0 → 100644
| 1 | +package com.diligrp.cashier.trade.domain; | |
| 2 | + | |
| 3 | +import com.diligrp.cashier.shared.util.JsonUtils; | |
| 4 | + | |
| 5 | +/** | |
| 6 | + * 异步消息模型 | |
| 7 | + */ | |
| 8 | +public class TaskMessage { | |
| 9 | + // 收银台订单扫描,在固定周期后兜底处理预支付订单; | |
| 10 | + // 查询交易订单的支付状态,根据支付状态完成交易订单,或关闭交易订单 | |
| 11 | + public static final int TYPE_CASHIER_ORDER_SCAN = 10; | |
| 12 | + // 周期性查询退款状态 | |
| 13 | + public static final int TYPE_CASHIER_REFUND_SCAN = 20; | |
| 14 | + | |
| 15 | + // 消息类型 | |
| 16 | + private int type; | |
| 17 | + // 消息体 | |
| 18 | + private String payload; | |
| 19 | + // 消息参数 | |
| 20 | + private String params; | |
| 21 | + | |
| 22 | + public TaskMessage() { | |
| 23 | + } | |
| 24 | + | |
| 25 | + public TaskMessage(int type, String payload, String params) { | |
| 26 | + this.type = type; | |
| 27 | + this.payload = payload; | |
| 28 | + this.params = params; | |
| 29 | + } | |
| 30 | + | |
| 31 | + | |
| 32 | + public int getType() { | |
| 33 | + return type; | |
| 34 | + } | |
| 35 | + | |
| 36 | + public void setType(int type) { | |
| 37 | + this.type = type; | |
| 38 | + } | |
| 39 | + | |
| 40 | + public String getPayload() { | |
| 41 | + return payload; | |
| 42 | + } | |
| 43 | + | |
| 44 | + public void setPayload(String payload) { | |
| 45 | + this.payload = payload; | |
| 46 | + } | |
| 47 | + | |
| 48 | + public String getParams() { | |
| 49 | + return params; | |
| 50 | + } | |
| 51 | + | |
| 52 | + public void setParams(String params) { | |
| 53 | + this.params = params; | |
| 54 | + } | |
| 55 | + | |
| 56 | + public static TaskMessage of(int type, String payload) { | |
| 57 | + return of(type, payload, null); | |
| 58 | + } | |
| 59 | + | |
| 60 | + public static TaskMessage of(int type, String payload, String params) { | |
| 61 | + return new TaskMessage(type, payload, params); | |
| 62 | + } | |
| 63 | + | |
| 64 | + public static TaskMessage fromJson(String json) { | |
| 65 | + return JsonUtils.fromJsonString(json, TaskMessage.class); | |
| 66 | + } | |
| 67 | + | |
| 68 | + @Override | |
| 69 | + public String toString() { | |
| 70 | + return JsonUtils.toJsonString(this); | |
| 71 | + } | |
| 72 | +} | ... | ... |
cashier-trade/src/main/java/com/diligrp/cashier/trade/domain/TradePaymentResult.java deleted
100644 → 0
| 1 | -package com.diligrp.cashier.trade.domain; | |
| 2 | - | |
| 3 | -import com.diligrp.cashier.shared.spi.PaymentEvent; | |
| 4 | - | |
| 5 | -import java.time.LocalDateTime; | |
| 6 | - | |
| 7 | -/** | |
| 8 | - * 在线支付结果 - 用于业务系统支付结果通知 | |
| 9 | - */ | |
| 10 | -public class TradePaymentResult extends PaymentEvent { | |
| 11 | - public TradePaymentResult(String tradeId, int state, String outTradeNo, Integer outPayType, String payerId, | |
| 12 | - LocalDateTime when, String message) { | |
| 13 | - super(tradeId, state, outTradeNo, outPayType, payerId, when, message); | |
| 14 | - } | |
| 15 | -} |
cashier-trade/src/main/java/com/diligrp/cashier/trade/domain/TradeStateDTO.java
| ... | ... | @@ -19,6 +19,22 @@ public class TradeStateDTO { |
| 19 | 19 | // 修改时间 |
| 20 | 20 | private LocalDateTime modifiedTime; |
| 21 | 21 | |
| 22 | + public static TradeStateDTO of(String tradeId, TradeState state, Integer version, LocalDateTime modifiedTime) { | |
| 23 | + return new TradeStateDTO(tradeId, null, state.getCode(), version, modifiedTime); | |
| 24 | + } | |
| 25 | + | |
| 26 | + public static TradeStateDTO of(String tradeId, Long amount, TradeState state, Integer version, LocalDateTime modifiedTime) { | |
| 27 | + return new TradeStateDTO(tradeId, amount, state.getCode(), version, modifiedTime); | |
| 28 | + } | |
| 29 | + | |
| 30 | + public TradeStateDTO(String tradeId, Long amount, Integer state, Integer version, LocalDateTime modifiedTime) { | |
| 31 | + this.tradeId = tradeId; | |
| 32 | + this.amount = amount; | |
| 33 | + this.state = state; | |
| 34 | + this.version = version; | |
| 35 | + this.modifiedTime = modifiedTime; | |
| 36 | + } | |
| 37 | + | |
| 22 | 38 | public String getTradeId() { |
| 23 | 39 | return tradeId; |
| 24 | 40 | } |
| ... | ... | @@ -58,18 +74,4 @@ public class TradeStateDTO { |
| 58 | 74 | public void setModifiedTime(LocalDateTime modifiedTime) { |
| 59 | 75 | this.modifiedTime = modifiedTime; |
| 60 | 76 | } |
| 61 | - | |
| 62 | - public static TradeStateDTO of(String tradeId, TradeState state, Integer version, LocalDateTime modifiedTime) { | |
| 63 | - return of(tradeId, null, state, version, modifiedTime); | |
| 64 | - } | |
| 65 | - | |
| 66 | - public static TradeStateDTO of(String tradeId, Long amount, TradeState state, Integer version, LocalDateTime modifiedTime) { | |
| 67 | - TradeStateDTO tradeStateDTO = new TradeStateDTO(); | |
| 68 | - tradeStateDTO.tradeId = tradeId; | |
| 69 | - tradeStateDTO.amount = amount; | |
| 70 | - tradeStateDTO.state = state.getCode(); | |
| 71 | - tradeStateDTO.version = version; | |
| 72 | - tradeStateDTO.modifiedTime = modifiedTime; | |
| 73 | - return tradeStateDTO; | |
| 74 | - } | |
| 75 | 77 | } |
| 76 | 78 | \ No newline at end of file | ... | ... |
cashier-trade/src/main/java/com/diligrp/cashier/trade/manager/PaymentResultManager.java
| ... | ... | @@ -5,7 +5,8 @@ import com.diligrp.cashier.shared.service.ThreadPoolService; |
| 5 | 5 | import com.diligrp.cashier.shared.spi.IPaymentEventListener; |
| 6 | 6 | import com.diligrp.cashier.shared.util.JsonUtils; |
| 7 | 7 | import com.diligrp.cashier.shared.util.ObjectUtils; |
| 8 | -import com.diligrp.cashier.trade.domain.TradePaymentResult; | |
| 8 | +import com.diligrp.cashier.trade.domain.OnlineRefundResult; | |
| 9 | +import com.diligrp.cashier.trade.domain.OnlinePaymentResult; | |
| 9 | 10 | import jakarta.annotation.Resource; |
| 10 | 11 | import org.slf4j.Logger; |
| 11 | 12 | import org.slf4j.LoggerFactory; |
| ... | ... | @@ -25,7 +26,7 @@ public class PaymentResultManager { |
| 25 | 26 | /** |
| 26 | 27 | * 通知业务系统在线支付通道处理结果 |
| 27 | 28 | */ |
| 28 | - public void notifyPaymentResult(String uri, TradePaymentResult payload) { | |
| 29 | + public void notifyPaymentResult(String uri, OnlinePaymentResult payload) { | |
| 29 | 30 | ThreadPoolService.getIoThreadPoll().submit(() -> { |
| 30 | 31 | List<IPaymentEventListener> lifeCycles = eventListeners.stream().toList(); |
| 31 | 32 | for (IPaymentEventListener listener : lifeCycles) { |
| ... | ... | @@ -55,6 +56,39 @@ public class PaymentResultManager { |
| 55 | 56 | }); |
| 56 | 57 | } |
| 57 | 58 | |
| 59 | + /** | |
| 60 | + * 通知业务系统退款处理结果 | |
| 61 | + */ | |
| 62 | + public void notifyRefundResult(String uri, OnlineRefundResult payload) { | |
| 63 | + ThreadPoolService.getIoThreadPoll().submit(() -> { | |
| 64 | + List<IPaymentEventListener> lifeCycles = eventListeners.stream().toList(); | |
| 65 | + for (IPaymentEventListener listener : lifeCycles) { | |
| 66 | + try { | |
| 67 | + listener.onEvent(payload); | |
| 68 | + } catch (Exception ex) { | |
| 69 | + LOG.error("Failed to notify trade refund result", ex); | |
| 70 | + } | |
| 71 | + } | |
| 72 | + }); | |
| 73 | + | |
| 74 | + if (ObjectUtils.isEmpty(uri)) { | |
| 75 | + return; | |
| 76 | + } | |
| 77 | + | |
| 78 | + ThreadPoolService.getIoThreadPoll().submit(() -> { | |
| 79 | + try { | |
| 80 | + String body = JsonUtils.toJsonString(payload); | |
| 81 | + LOG.info("Notifying online trade refund result: {}", body); | |
| 82 | + ServiceEndpointSupport.HttpResult httpResult = new NotifyHttpClient(uri).send(body); | |
| 83 | + if (httpResult.statusCode != 200) { | |
| 84 | + LOG.error("Failed to notify trade refund result"); | |
| 85 | + } | |
| 86 | + } catch (Exception ex) { | |
| 87 | + LOG.error("Failed to notify trade refund result", ex); | |
| 88 | + } | |
| 89 | + }); | |
| 90 | + } | |
| 91 | + | |
| 58 | 92 | private static class NotifyHttpClient extends ServiceEndpointSupport { |
| 59 | 93 | private final String baseUrl; |
| 60 | 94 | ... | ... |
cashier-trade/src/main/java/com/diligrp/cashier/trade/manager/TaskMessageConsumer.java
0 → 100644
| 1 | +package com.diligrp.cashier.trade.manager; | |
| 2 | + | |
| 3 | +import com.diligrp.cashier.shared.util.NumberUtils; | |
| 4 | +import com.diligrp.cashier.trade.Constants; | |
| 5 | +import com.diligrp.cashier.trade.domain.TaskMessage; | |
| 6 | +import com.diligrp.cashier.trade.service.ICashierAssistantService; | |
| 7 | +import jakarta.annotation.Resource; | |
| 8 | +import org.slf4j.Logger; | |
| 9 | +import org.slf4j.LoggerFactory; | |
| 10 | +import org.springframework.amqp.core.Message; | |
| 11 | +import org.springframework.amqp.core.MessageProperties; | |
| 12 | +import org.springframework.amqp.rabbit.annotation.RabbitHandler; | |
| 13 | +import org.springframework.amqp.rabbit.annotation.RabbitListener; | |
| 14 | +import org.springframework.stereotype.Service; | |
| 15 | + | |
| 16 | +import java.nio.charset.StandardCharsets; | |
| 17 | + | |
| 18 | +@Service("taskMessageConsumer") | |
| 19 | +public class TaskMessageConsumer { | |
| 20 | + | |
| 21 | + private static final Logger LOG = LoggerFactory.getLogger(TaskMessageConsumer.class); | |
| 22 | + | |
| 23 | + @Resource | |
| 24 | + private ICashierAssistantService cashierAssistantService; | |
| 25 | + | |
| 26 | + /** | |
| 27 | + * 监听支付通道异步处理任务消息 | |
| 28 | + */ | |
| 29 | + @RabbitHandler | |
| 30 | + @RabbitListener(queues = {Constants.PAYMENT_DELAY_QUEUE}) | |
| 31 | + public void onDelayMessage(Message message) { | |
| 32 | + byte[] packet = message.getBody(); | |
| 33 | + MessageProperties properties = message.getMessageProperties(); | |
| 34 | + String charSet = properties != null && properties.getContentEncoding() != null | |
| 35 | + ? properties.getContentEncoding() : StandardCharsets.UTF_8.name(); | |
| 36 | + try { | |
| 37 | + String body = new String(packet, charSet); | |
| 38 | + LOG.info("Receiving online pipeline async task request: {}", body); | |
| 39 | + TaskMessage task = TaskMessage.fromJson(body); | |
| 40 | + int times = NumberUtils.str2Int(task.getParams(), Integer.MAX_VALUE); | |
| 41 | + if (task.getType() == TaskMessage.TYPE_CASHIER_ORDER_SCAN) { | |
| 42 | + cashierAssistantService.scanCashierTradeOrder(task.getPayload(), times); | |
| 43 | + } else if (task.getType() == TaskMessage.TYPE_CASHIER_REFUND_SCAN) { | |
| 44 | + cashierAssistantService.scanCashierRefundOrder(task.getPayload(), times); | |
| 45 | + } else { | |
| 46 | + LOG.error("Never happened"); | |
| 47 | + } | |
| 48 | + } catch (Exception ex) { | |
| 49 | + LOG.error("Consume online pipeline async message exception", ex); | |
| 50 | + } | |
| 51 | + } | |
| 52 | +} | ... | ... |
cashier-trade/src/main/java/com/diligrp/cashier/trade/manager/TaskMessageSender.java
0 → 100644
| 1 | +package com.diligrp.cashier.trade.manager; | |
| 2 | + | |
| 3 | +import com.diligrp.cashier.shared.service.ThreadPoolService; | |
| 4 | +import com.diligrp.cashier.shared.util.JsonUtils; | |
| 5 | +import com.diligrp.cashier.trade.Constants; | |
| 6 | +import com.diligrp.cashier.trade.domain.TaskMessage; | |
| 7 | +import jakarta.annotation.Resource; | |
| 8 | +import org.slf4j.Logger; | |
| 9 | +import org.slf4j.LoggerFactory; | |
| 10 | +import org.springframework.amqp.core.Message; | |
| 11 | +import org.springframework.amqp.core.MessageProperties; | |
| 12 | +import org.springframework.amqp.rabbit.core.RabbitTemplate; | |
| 13 | +import org.springframework.stereotype.Service; | |
| 14 | + | |
| 15 | +import java.nio.charset.StandardCharsets; | |
| 16 | + | |
| 17 | +@Service("taskMessageSender") | |
| 18 | +public class TaskMessageSender { | |
| 19 | + | |
| 20 | + private static final Logger LOG = LoggerFactory.getLogger(TaskMessageSender.class); | |
| 21 | + | |
| 22 | + private static final long ONE_MINUTE = 60 * 1000; | |
| 23 | + | |
| 24 | + private static final long TEN_MINUTES = 10 * ONE_MINUTE; | |
| 25 | + | |
| 26 | + private static final long ONE_SECOND = 1000; | |
| 27 | + | |
| 28 | + @Resource | |
| 29 | + private RabbitTemplate rabbitTemplate; | |
| 30 | + | |
| 31 | + /** | |
| 32 | + * 发送延时处理消息 | |
| 33 | + */ | |
| 34 | + public void sendDelayTaskMessage(TaskMessage task, long delayInMillis) { | |
| 35 | + if (delayInMillis < 0) { | |
| 36 | + LOG.info("No need send scan order message[type={}]", task.getType()); | |
| 37 | + return; | |
| 38 | + } | |
| 39 | + | |
| 40 | + ThreadPoolService.getIoThreadPoll().submit(() -> { | |
| 41 | + try { | |
| 42 | + MessageProperties properties = new MessageProperties(); | |
| 43 | + properties.setContentEncoding(StandardCharsets.UTF_8.name()); | |
| 44 | + properties.setContentType(MessageProperties.CONTENT_TYPE_BYTES); | |
| 45 | + // properties.setExpiration(String.valueOf(expiredTime)); | |
| 46 | + // RabbitMQ延时插件必须设置x-delay的header才能生效 | |
| 47 | + properties.setHeader("x-delay", String.valueOf(delayInMillis)); | |
| 48 | + String payload = JsonUtils.toJsonString(task); | |
| 49 | + Message message = new Message(payload.getBytes(StandardCharsets.UTF_8), properties); | |
| 50 | + LOG.info("Sending online payment order scan request for {}", task.getPayload()); | |
| 51 | + rabbitTemplate.send(Constants.PAYMENT_DELAY_EXCHANGE, Constants.PAYMENT_DELAY_KEY, message); | |
| 52 | + } catch (Exception ex) { | |
| 53 | + LOG.error("Failed to send online payment order scan request for {}", task.getPayload(), ex); | |
| 54 | + } | |
| 55 | + }); | |
| 56 | + } | |
| 57 | +} | ... | ... |
cashier-trade/src/main/java/com/diligrp/cashier/trade/model/OnlinePayment.java
| ... | ... | @@ -40,6 +40,8 @@ public class OnlinePayment extends BaseDO { |
| 40 | 40 | private Integer outPayType; |
| 41 | 41 | // 申请状态 |
| 42 | 42 | private Integer state; |
| 43 | + // 业务回调地址 | |
| 44 | + private String notifyUrl; | |
| 43 | 45 | // 备注 |
| 44 | 46 | private String description; |
| 45 | 47 | |
| ... | ... | @@ -163,6 +165,14 @@ public class OnlinePayment extends BaseDO { |
| 163 | 165 | this.state = state; |
| 164 | 166 | } |
| 165 | 167 | |
| 168 | + public String getNotifyUrl() { | |
| 169 | + return notifyUrl; | |
| 170 | + } | |
| 171 | + | |
| 172 | + public void setNotifyUrl(String notifyUrl) { | |
| 173 | + this.notifyUrl = notifyUrl; | |
| 174 | + } | |
| 175 | + | |
| 166 | 176 | public String getDescription() { |
| 167 | 177 | return description; |
| 168 | 178 | } |
| ... | ... | @@ -201,11 +211,21 @@ public class OnlinePayment extends BaseDO { |
| 201 | 211 | return this; |
| 202 | 212 | } |
| 203 | 213 | |
| 214 | + public Builder channelId(Integer channelType) { | |
| 215 | + OnlinePayment.this.channelId = channelType; | |
| 216 | + return this; | |
| 217 | + } | |
| 218 | + | |
| 204 | 219 | public Builder payType(PaymentType payType) { |
| 205 | 220 | OnlinePayment.this.payType = payType.getCode(); |
| 206 | 221 | return this; |
| 207 | 222 | } |
| 208 | 223 | |
| 224 | + public Builder payType(Integer payType) { | |
| 225 | + OnlinePayment.this.payType = payType; | |
| 226 | + return this; | |
| 227 | + } | |
| 228 | + | |
| 209 | 229 | public Builder pipelineId(Long pipelineId) { |
| 210 | 230 | OnlinePayment.this.pipelineId = pipelineId; |
| 211 | 231 | return this; |
| ... | ... | @@ -246,11 +266,26 @@ public class OnlinePayment extends BaseDO { |
| 246 | 266 | return this; |
| 247 | 267 | } |
| 248 | 268 | |
| 269 | + public Builder outPayType(Integer outPayType) { | |
| 270 | + OnlinePayment.this.outPayType = outPayType; | |
| 271 | + return this; | |
| 272 | + } | |
| 273 | + | |
| 249 | 274 | public Builder state(PaymentState state) { |
| 250 | 275 | OnlinePayment.this.state = state.getCode(); |
| 251 | 276 | return this; |
| 252 | 277 | } |
| 253 | 278 | |
| 279 | + public Builder state(Integer state) { | |
| 280 | + OnlinePayment.this.state = state; | |
| 281 | + return this; | |
| 282 | + } | |
| 283 | + | |
| 284 | + public Builder notifyUrl(String notifyUrl) { | |
| 285 | + OnlinePayment.this.notifyUrl = notifyUrl; | |
| 286 | + return this; | |
| 287 | + } | |
| 288 | + | |
| 254 | 289 | public Builder description(String description) { |
| 255 | 290 | OnlinePayment.this.description = description; |
| 256 | 291 | return this; | ... | ... |
cashier-trade/src/main/java/com/diligrp/cashier/trade/service/ICashierAssistantService.java
0 → 100644
cashier-trade/src/main/java/com/diligrp/cashier/trade/service/ICashierPaymentService.java
| 1 | 1 | package com.diligrp.cashier.trade.service; |
| 2 | 2 | |
| 3 | +import com.diligrp.cashier.pipeline.domain.OnlinePaymentResponse; | |
| 3 | 4 | import com.diligrp.cashier.pipeline.domain.OnlinePaymentStatus; |
| 4 | -import com.diligrp.cashier.trade.domain.CashierOrder; | |
| 5 | -import com.diligrp.cashier.trade.domain.CashierPayment; | |
| 6 | -import com.diligrp.cashier.trade.domain.Merchant; | |
| 5 | +import com.diligrp.cashier.pipeline.domain.OnlineRefundResponse; | |
| 6 | +import com.diligrp.cashier.trade.domain.*; | |
| 7 | 7 | |
| 8 | 8 | public interface ICashierPaymentService { |
| 9 | 9 | /** |
| 10 | 10 | * 提交收银台订单 |
| 11 | 11 | * |
| 12 | - * @param merchant - 接入商户 | |
| 12 | + * @param merchant - 接入商户 | |
| 13 | 13 | * @param cashierOrder - 订单申请 |
| 14 | 14 | * @return 支付ID |
| 15 | 15 | */ |
| ... | ... | @@ -22,4 +22,38 @@ public interface ICashierPaymentService { |
| 22 | 22 | * @return 支付状态 |
| 23 | 23 | */ |
| 24 | 24 | OnlinePaymentStatus doPayment(CashierPayment cashierPayment); |
| 25 | + | |
| 26 | + /** | |
| 27 | + * 支付结果通知 | |
| 28 | + * | |
| 29 | + * @param response - 支付结果 | |
| 30 | + */ | |
| 31 | + void notifyPaymentResponse(OnlinePaymentResponse response); | |
| 32 | + | |
| 33 | + /** | |
| 34 | + * 关闭预支付订单 | |
| 35 | + */ | |
| 36 | + void closePrepayOrder(String paymentId); | |
| 37 | + | |
| 38 | + /** | |
| 39 | + * 查询交易订单状态 | |
| 40 | + */ | |
| 41 | + OnlinePaymentResult queryPaymentState(String paymentId, String mode); | |
| 42 | + | |
| 43 | + /** | |
| 44 | + * 支付退款申请 | |
| 45 | + */ | |
| 46 | + OnlineRefundResult sendRefundRequest(OnlineRefundDTO request); | |
| 47 | + | |
| 48 | + /** | |
| 49 | + * 退款结果通知 | |
| 50 | + * | |
| 51 | + * @param response - 退款结果 | |
| 52 | + */ | |
| 53 | + void notifyRefundResult(OnlineRefundResponse response); | |
| 54 | + | |
| 55 | + /** | |
| 56 | + * 查询退款状态 | |
| 57 | + */ | |
| 58 | + OnlineRefundResult queryRefundState(String refundId, String mode); | |
| 25 | 59 | } | ... | ... |
cashier-trade/src/main/java/com/diligrp/cashier/trade/service/IPaymentAssistantService.java deleted
100644 → 0
cashier-trade/src/main/java/com/diligrp/cashier/trade/service/ITradeAssistantService.java
| ... | ... | @@ -5,6 +5,9 @@ import com.diligrp.cashier.trade.domain.TradeStateDTO; |
| 5 | 5 | import com.diligrp.cashier.trade.model.OnlinePayment; |
| 6 | 6 | import com.diligrp.cashier.trade.model.TradeOrder; |
| 7 | 7 | |
| 8 | +import java.util.List; | |
| 9 | +import java.util.Optional; | |
| 10 | + | |
| 8 | 11 | public interface ITradeAssistantService { |
| 9 | 12 | |
| 10 | 13 | /** |
| ... | ... | @@ -32,4 +35,8 @@ public interface ITradeAssistantService { |
| 32 | 35 | */ |
| 33 | 36 | void proceedOnlinePayment(PaymentStateDTO paymentDTO); |
| 34 | 37 | |
| 38 | + /** | |
| 39 | + * 查询退款订单 | |
| 40 | + */ | |
| 41 | + OnlinePayment findByRefundId(String refundId); | |
| 35 | 42 | } | ... | ... |
cashier-trade/src/main/java/com/diligrp/cashier/trade/service/impl/CashierAssistantServiceImpl.java
0 → 100644
| 1 | +package com.diligrp.cashier.trade.service.impl; | |
| 2 | + | |
| 3 | +import com.diligrp.cashier.pipeline.Constants; | |
| 4 | +import com.diligrp.cashier.pipeline.core.OnlinePipeline; | |
| 5 | +import com.diligrp.cashier.pipeline.core.PaymentPipeline; | |
| 6 | +import com.diligrp.cashier.pipeline.domain.OnlinePaymentResponse; | |
| 7 | +import com.diligrp.cashier.pipeline.domain.OnlinePrepayOrder; | |
| 8 | +import com.diligrp.cashier.pipeline.domain.OnlineRefundOrder; | |
| 9 | +import com.diligrp.cashier.pipeline.domain.OnlineRefundResponse; | |
| 10 | +import com.diligrp.cashier.pipeline.service.IPaymentPipelineManager; | |
| 11 | +import com.diligrp.cashier.pipeline.type.PaymentState; | |
| 12 | +import com.diligrp.cashier.trade.dao.IOnlinePaymentDao; | |
| 13 | +import com.diligrp.cashier.trade.domain.TradeStateDTO; | |
| 14 | +import com.diligrp.cashier.trade.model.OnlinePayment; | |
| 15 | +import com.diligrp.cashier.trade.model.TradeOrder; | |
| 16 | +import com.diligrp.cashier.trade.service.ICashierAssistantService; | |
| 17 | +import com.diligrp.cashier.trade.service.ICashierPaymentService; | |
| 18 | +import com.diligrp.cashier.trade.type.TradeState; | |
| 19 | +import com.diligrp.cashier.trade.type.TradeType; | |
| 20 | +import jakarta.annotation.Resource; | |
| 21 | +import org.redisson.api.RLock; | |
| 22 | +import org.redisson.api.RedissonClient; | |
| 23 | +import org.slf4j.Logger; | |
| 24 | +import org.slf4j.LoggerFactory; | |
| 25 | +import org.springframework.stereotype.Service; | |
| 26 | + | |
| 27 | +import java.time.LocalDateTime; | |
| 28 | +import java.util.List; | |
| 29 | + | |
| 30 | +@Service("cashierAssistantService") | |
| 31 | +public class CashierAssistantServiceImpl implements ICashierAssistantService { | |
| 32 | + | |
| 33 | + private static final Logger LOG = LoggerFactory.getLogger(CashierAssistantServiceImpl.class); | |
| 34 | + | |
| 35 | + @Resource | |
| 36 | + private IOnlinePaymentDao onlinePaymentDao; | |
| 37 | + | |
| 38 | + @Resource | |
| 39 | + private ICashierPaymentService cashierPaymentService; | |
| 40 | + | |
| 41 | + @Resource | |
| 42 | + private IPaymentPipelineManager paymentPipelineManager; | |
| 43 | + | |
| 44 | + @Resource | |
| 45 | + private TradeAssistantServiceImpl tradeAssistantService; | |
| 46 | + | |
| 47 | + @Resource | |
| 48 | + private RedissonClient redissonClient; | |
| 49 | + | |
| 50 | + @Override | |
| 51 | + public void scanCashierTradeOrder(String tradeId, int times) { | |
| 52 | + LOG.debug("scanCashierTradeOrder{}: processing cashier order {}", times, tradeId); | |
| 53 | + String lockKey = String.format(com.diligrp.cashier.trade.Constants.TRADE_LOCK_REDIS_KEY, tradeId); | |
| 54 | + RLock lock = redissonClient.getLock(lockKey); | |
| 55 | + try { | |
| 56 | + lock.lock(); | |
| 57 | + LocalDateTime now = LocalDateTime.now(); | |
| 58 | + // 理论上只会存在一条支付中的支付订单 | |
| 59 | + List<OnlinePayment> onlinePayments = onlinePaymentDao.listOnlinePayments(tradeId, | |
| 60 | + TradeType.TRADE.getCode(), PaymentState.PROCESSING.getCode()); | |
| 61 | + // 关闭所有未支付完成的支付订单 | |
| 62 | + for (OnlinePayment payment : onlinePayments) { | |
| 63 | + PaymentPipeline<?> pipeline = paymentPipelineManager.findPipelineById(payment.getPipelineId(), PaymentPipeline.class); | |
| 64 | + // 在线支付通道才能关闭订单, 园区卡支付不支持 | |
| 65 | + if (pipeline instanceof OnlinePipeline<?> onlinePipeline) { | |
| 66 | + OnlinePrepayOrder order = new OnlinePrepayOrder(payment.getPaymentId(), payment.getOutTradeNo()); | |
| 67 | + // 微信服务商模式,还需子商户 | |
| 68 | + order.attach(Constants.PARAM_MCH_ID, payment.getOutMchId()); | |
| 69 | + OnlinePaymentResponse response = onlinePipeline.queryPrepayResponse(order); | |
| 70 | + if (!PaymentState.isFinished(response.getState().getCode())) { | |
| 71 | + try { | |
| 72 | + onlinePipeline.closePrepayOrder(order); | |
| 73 | + LOG.debug("scanCashierTradeOrder: close online prepay order {}", payment.getPaymentId()); | |
| 74 | + response = new OnlinePaymentResponse(response.getPaymentId(), response.getOutTradeNo(), | |
| 75 | + response.getOutPayType(), response.getPayerId(), response.getWhen(), | |
| 76 | + PaymentState.FAILED, "自动关闭超时的支付订单"); | |
| 77 | + } catch (Exception ex) { | |
| 78 | + LOG.error("scanOnlinePrepayOrder: close online prepare order exception", ex); | |
| 79 | + } | |
| 80 | + } | |
| 81 | + cashierPaymentService.notifyPaymentResponse(response); | |
| 82 | + } | |
| 83 | + } | |
| 84 | + // 交易订单仍然未完成则关闭交易订单,交易订单不能继续支付 | |
| 85 | + TradeOrder trade = tradeAssistantService.findByTradeId(tradeId); | |
| 86 | + if (!TradeState.isFinished(trade.getState())) { | |
| 87 | + TradeStateDTO tradeStateDTO = TradeStateDTO.of(trade.getTradeId(), TradeState.FAILED, trade.getVersion(), now); | |
| 88 | + tradeAssistantService.proceedTradeOrder(tradeStateDTO); | |
| 89 | + } | |
| 90 | + } finally { | |
| 91 | + if (lock.isHeldByCurrentThread()) { | |
| 92 | + lock.unlock(); | |
| 93 | + } | |
| 94 | + } | |
| 95 | + } | |
| 96 | + | |
| 97 | + @Override | |
| 98 | + public void scanCashierRefundOrder(String refundId, int times) { | |
| 99 | + LOG.debug("scanCashierRefundOrder{}: processing online refund order {}", times, refundId); | |
| 100 | + OnlinePayment refund = tradeAssistantService.findByRefundId(refundId); | |
| 101 | + if (PaymentState.isFinished(refund.getState())) { | |
| 102 | + LOG.debug("scanCashierRefundOrder{}: online refund order {} already accomplished", times, refundId); | |
| 103 | + return; | |
| 104 | + } | |
| 105 | + | |
| 106 | + OnlinePipeline<?> pipeline = paymentPipelineManager.findPipelineById(refund.getPipelineId(), OnlinePipeline.class); | |
| 107 | + OnlineRefundOrder order = new OnlineRefundOrder(refundId, refund.getOutTradeNo()); | |
| 108 | + // 微信服务商模式,还需子商户 | |
| 109 | + order.attach(Constants.PARAM_MCH_ID, refund.getOutMchId()); | |
| 110 | + OnlineRefundResponse response = pipeline.queryRefundResponse(order); | |
| 111 | + cashierPaymentService.notifyRefundResult(response); | |
| 112 | + } | |
| 113 | +} | ... | ... |
cashier-trade/src/main/java/com/diligrp/cashier/trade/service/impl/CashierPaymentServiceImpl.java
| 1 | 1 | package com.diligrp.cashier.trade.service.impl; |
| 2 | 2 | |
| 3 | +import com.diligrp.cashier.assistant.service.KeyGenerator; | |
| 3 | 4 | import com.diligrp.cashier.assistant.service.impl.SnowflakeKeyManager; |
| 4 | 5 | import com.diligrp.cashier.pipeline.core.DiliCardPipeline; |
| 5 | 6 | import com.diligrp.cashier.pipeline.core.OnlinePipeline; |
| 6 | 7 | import com.diligrp.cashier.pipeline.core.PaymentPipeline; |
| 7 | -import com.diligrp.cashier.pipeline.domain.MiniProPrepayRequest; | |
| 8 | -import com.diligrp.cashier.pipeline.domain.MiniProPrepayResponse; | |
| 9 | -import com.diligrp.cashier.pipeline.domain.OnlinePaymentStatus; | |
| 8 | +import com.diligrp.cashier.pipeline.core.WechatPipeline; | |
| 9 | +import com.diligrp.cashier.pipeline.domain.*; | |
| 10 | 10 | import com.diligrp.cashier.pipeline.domain.card.CardPaymentRequest; |
| 11 | 11 | import com.diligrp.cashier.pipeline.domain.card.CardPaymentResponse; |
| 12 | 12 | import com.diligrp.cashier.pipeline.service.IPaymentPipelineManager; |
| ... | ... | @@ -20,6 +20,7 @@ import com.diligrp.cashier.trade.dao.IOnlinePaymentDao; |
| 20 | 20 | import com.diligrp.cashier.trade.dao.ITradeOrderDao; |
| 21 | 21 | import com.diligrp.cashier.trade.domain.*; |
| 22 | 22 | import com.diligrp.cashier.trade.exception.TradePaymentException; |
| 23 | +import com.diligrp.cashier.trade.manager.TaskMessageSender; | |
| 23 | 24 | import com.diligrp.cashier.trade.manager.PaymentResultManager; |
| 24 | 25 | import com.diligrp.cashier.trade.model.OnlinePayment; |
| 25 | 26 | import com.diligrp.cashier.trade.model.TradeOrder; |
| ... | ... | @@ -33,6 +34,8 @@ import com.diligrp.cashier.trade.util.MiniProPaymentConverter; |
| 33 | 34 | import jakarta.annotation.Resource; |
| 34 | 35 | import org.redisson.api.RLock; |
| 35 | 36 | import org.redisson.api.RedissonClient; |
| 37 | +import org.slf4j.Logger; | |
| 38 | +import org.slf4j.LoggerFactory; | |
| 36 | 39 | import org.springframework.stereotype.Service; |
| 37 | 40 | import org.springframework.transaction.annotation.Transactional; |
| 38 | 41 | |
| ... | ... | @@ -43,6 +46,8 @@ import java.util.concurrent.TimeUnit; |
| 43 | 46 | @Service("cashierPaymentService") |
| 44 | 47 | public class CashierPaymentServiceImpl implements ICashierPaymentService { |
| 45 | 48 | |
| 49 | + private static final Logger LOG = LoggerFactory.getLogger(CashierPaymentServiceImpl.class); | |
| 50 | + | |
| 46 | 51 | @Resource |
| 47 | 52 | private ITradeOrderDao tradeOrderDao; |
| 48 | 53 | |
| ... | ... | @@ -56,6 +61,9 @@ public class CashierPaymentServiceImpl implements ICashierPaymentService { |
| 56 | 61 | private IPaymentPipelineManager paymentPipelineManager; |
| 57 | 62 | |
| 58 | 63 | @Resource |
| 64 | + private TaskMessageSender taskMessageSender; | |
| 65 | + | |
| 66 | + @Resource | |
| 59 | 67 | private PaymentResultManager paymentResultManager; |
| 60 | 68 | |
| 61 | 69 | @Resource |
| ... | ... | @@ -86,7 +94,9 @@ public class CashierPaymentServiceImpl implements ICashierPaymentService { |
| 86 | 94 | tradeOrderDao.insertTradeOrder(tradeOrder); |
| 87 | 95 | |
| 88 | 96 | // TODO: userId是否需要存储 |
| 89 | - // TODO: 如果不打开收银台支付,定时关闭订单 | |
| 97 | + // 兜底处理交易订单,根据支付结果选择关闭或完成交易订单 | |
| 98 | + TaskMessage message = new TaskMessage(TaskMessage.TYPE_CASHIER_ORDER_SCAN, tradeId, "1"); | |
| 99 | + taskMessageSender.sendDelayTaskMessage(message, timeout); | |
| 90 | 100 | return tradeId; |
| 91 | 101 | } |
| 92 | 102 | |
| ... | ... | @@ -99,77 +109,76 @@ public class CashierPaymentServiceImpl implements ICashierPaymentService { |
| 99 | 109 | @Override |
| 100 | 110 | @Transactional(rollbackFor = Exception.class) |
| 101 | 111 | public OnlinePaymentStatus doPayment(CashierPayment cashierPayment) { |
| 102 | - // TODO: 防重复提交 | |
| 112 | + // TODO: 接口防重复提交 | |
| 103 | 113 | String lockKey = String.format(Constants.TRADE_LOCK_REDIS_KEY, cashierPayment.getTradeId()); |
| 104 | 114 | RLock lock = redissonClient.getLock(lockKey); |
| 105 | 115 | try { |
| 106 | 116 | boolean locked = lock.tryLock(Constants.TRADE_LOCK_TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 107 | 117 | if (locked) { |
| 108 | - TradeOrder tradeOrder = tradeAssistantService.findByTradeId(cashierPayment.getTradeId()); | |
| 109 | - CashierType cashierType = CashierType.getByCode(tradeOrder.getType()); | |
| 110 | - if (TradeState.isFinished(tradeOrder.getState())) { | |
| 111 | - throw new TradePaymentException(ErrorCode.OPERATION_NOT_ALLOWED, "该交易订单已经完成,不能进行支付"); | |
| 118 | + TradeOrder trade = tradeAssistantService.findByTradeId(cashierPayment.getTradeId()); | |
| 119 | + CashierType cashierType = CashierType.getByCode(trade.getType()); | |
| 120 | + if (TradeState.isFinished(trade.getState())) { | |
| 121 | + throw new TradePaymentException(ErrorCode.OPERATION_NOT_ALLOWED, "不能进行支付, 交易订单已经完成"); | |
| 122 | + } | |
| 123 | + // 目前只支持小程序收银台,后期将支持其他收银台类型 | |
| 124 | + if (cashierType != CashierType.MINIPRO) { | |
| 125 | + throw new TradePaymentException(ErrorCode.OPERATION_NOT_ALLOWED, "系统当前不支持的此收银台类型"); | |
| 112 | 126 | } |
| 113 | 127 | // 关闭支付中的支付订单, 避免一个交易订单存在多笔支付订单, 造成重复支付 |
| 114 | - if (!tradeAssistantService.resetTradeOrder(tradeOrder)) { | |
| 115 | - throw new TradePaymentException(ErrorCode.OPERATION_NOT_ALLOWED, "存在支付中的支付申请"); | |
| 128 | + if (!tradeAssistantService.resetTradeOrder(trade)) { | |
| 129 | + throw new TradePaymentException(ErrorCode.OPERATION_NOT_ALLOWED, "不能进行支付, 交易订单正在支付中"); | |
| 116 | 130 | } |
| 117 | 131 | |
| 118 | 132 | LocalDateTime now = LocalDateTime.now(); |
| 119 | 133 | // 获取支付通道 |
| 120 | - PaymentPipeline<?> pipeline = paymentPipelineManager.findPipelineById(cashierPayment.getPipelineId(), PaymentPipeline.class); | |
| 134 | + PaymentPipeline<?> paymentPipeline = paymentPipelineManager.findPipelineById( | |
| 135 | + cashierPayment.getPipelineId(), PaymentPipeline.class); | |
| 121 | 136 | String paymentId = snowflakeKeyManager.getKeyGenerator(SnowflakeKey.PAYMENT_ID).nextId(); |
| 122 | - if (pipeline instanceof DiliCardPipeline cardPipeline) { | |
| 137 | + if (paymentPipeline instanceof OnlinePipeline<?> pipeline) { // 在线支付通道 | |
| 138 | + // 在线支付通道: 不同的收银台类型使用不同的支付方式(目前只支持小程序收银台) | |
| 139 | + // 小程序收银台将使用在线支付通道的小程序支付 | |
| 140 | + MiniProPrepayRequest request = new MiniProPaymentConverter(trade, paymentId, now).convert(cashierPayment); | |
| 141 | + MiniProPrepayResponse response = pipeline.sendMiniProPrepayRequest(request); | |
| 142 | + // 微信服务商模式下outMchId为签约子商户 | |
| 143 | + String outMchId = request.getString(com.diligrp.cashier.pipeline.Constants.PARAM_MCH_ID); | |
| 144 | + OnlinePayment payment = OnlinePayment.builder().outMchId(outMchId).tradeId(trade.getTradeId()) | |
| 145 | + .type(TradeType.TRADE).paymentId(paymentId).channelId(pipeline.supportedChannel()) | |
| 146 | + .payType(PaymentType.MINI_PRO).pipelineId(pipeline.pipelineId()).goods(trade.getGoods()) | |
| 147 | + .amount(trade.getAmount()).payerId(request.getOpenId()).outTradeNo(response.getOutTradeNo()) | |
| 148 | + .state(response.getState()).version(0).createdTime(now).modifiedTime(now).build(); | |
| 149 | + onlinePaymentDao.insertOnlinePayment(payment); | |
| 150 | + return response; | |
| 151 | + } | |
| 152 | + if (paymentPipeline instanceof DiliCardPipeline pipeline) { // 园区卡支付通道 | |
| 123 | 153 | // 园区卡支付通道: 所有的收银台类型使用的是同一种园区卡支付流程 |
| 124 | - // 检查园区卡支付参数 | |
| 125 | - CardPaymentRequest request = new CardPaymentConverter(tradeOrder, paymentId, now).convert(cashierPayment); | |
| 154 | + CardPaymentRequest request = new CardPaymentConverter(trade, paymentId, now).convert(cashierPayment); | |
| 126 | 155 | // 修改支付状态为支付中,防止重复支付 |
| 127 | - CardPaymentResponse response = cardPipeline.sendPaymentRequest(request); | |
| 156 | + CardPaymentResponse response = pipeline.sendPaymentRequest(request); | |
| 128 | 157 | if (PaymentState.isFinished(response.getState().getCode())) { |
| 129 | 158 | // 园区卡支付通道outMchId为市场ID |
| 130 | - String outMchId = cardPipeline.params().getOutMchId(); | |
| 131 | - OnlinePayment payment = OnlinePayment.builder().outMchId(outMchId).tradeId(tradeOrder.getTradeId()) | |
| 159 | + String outMchId = pipeline.params().getOutMchId(); | |
| 160 | + OnlinePayment payment = OnlinePayment.builder().outMchId(outMchId).tradeId(trade.getTradeId()) | |
| 132 | 161 | .type(TradeType.TRADE).paymentId(paymentId).channelId(pipeline.supportedChannel()) |
| 133 | - .payType(PaymentType.DIRECT).pipelineId(pipeline.pipelineId()).goods(tradeOrder.getGoods()) | |
| 134 | - .amount(tradeOrder.getAmount()).payerId(response.getPayerId()) | |
| 162 | + .payType(PaymentType.DIRECT).pipelineId(pipeline.pipelineId()).goods(trade.getGoods()) | |
| 163 | + .amount(trade.getAmount()).payerId(response.getPayerId()) | |
| 135 | 164 | .finishTime(response.getWhen()).outTradeNo(response.getOutTradeNo()) |
| 136 | - .outPayType(OutPaymentType.DILICARD).state(response.getState()) | |
| 165 | + .outPayType(OutPaymentType.DILICARD).state(response.getState()).notifyUrl(trade.getNotifyUrl()) | |
| 137 | 166 | .description(response.getMessage()).version(0).createdTime(now).modifiedTime(now).build(); |
| 138 | 167 | onlinePaymentDao.insertOnlinePayment(payment); |
| 139 | - } | |
| 140 | - if (response.getState() == PaymentState.SUCCESS) { | |
| 141 | - TradeStateDTO tradeStateDTO = TradeStateDTO.of(tradeOrder.getTradeId(), TradeState.SUCCESS, | |
| 142 | - tradeOrder.getVersion(), now); | |
| 143 | - tradeAssistantService.proceedTradeOrder(tradeStateDTO); | |
| 144 | - // 通知业务系统支付结果 | |
| 145 | - TradePaymentResult paymentResult = new TradePaymentResult(tradeOrder.getTradeId(), | |
| 146 | - response.getState().getCode(), tradeOrder.getOutTradeNo(), OutPaymentType.DILICARD.getCode(), | |
| 147 | - response.getPayerId(), response.getWhen(), response.getMessage()); | |
| 148 | - paymentResultManager.notifyPaymentResult(tradeOrder.getNotifyUrl(), paymentResult); | |
| 149 | - } | |
| 150 | - return response; | |
| 151 | - } else if (pipeline instanceof OnlinePipeline<?> onlinePipeline) { | |
| 152 | - // 在线支付通道: 不同的收银台类型使用的支付方式不同 | |
| 153 | - if (cashierType == CashierType.MINIPRO) { | |
| 154 | - // 小程序支付收银台使用小程序支付 | |
| 155 | - MiniProPrepayRequest request = new MiniProPaymentConverter(tradeOrder, paymentId, now).convert(cashierPayment); | |
| 156 | - MiniProPrepayResponse response = onlinePipeline.sendMiniProPrepayRequest(request); | |
| 157 | - // 微信服务商模式下outMchId为签约子商户 | |
| 158 | - String outMchId = request.getString(com.diligrp.cashier.pipeline.Constants.PARAM_MCH_ID); | |
| 159 | - OnlinePayment payment = OnlinePayment.builder().outMchId(outMchId).tradeId(tradeOrder.getTradeId()) | |
| 160 | - .type(TradeType.TRADE).paymentId(paymentId).channelId(pipeline.supportedChannel()) | |
| 161 | - .payType(PaymentType.MINI_PRO).pipelineId(pipeline.pipelineId()).goods(tradeOrder.getGoods()) | |
| 162 | - .amount(tradeOrder.getAmount()).payerId(request.getOpenId()).outTradeNo(response.getOutTradeNo()) | |
| 163 | - .state(response.getState()).version(0).createdTime(now).modifiedTime(now).build(); | |
| 164 | - onlinePaymentDao.insertOnlinePayment(payment); | |
| 165 | 168 | |
| 166 | - TradeStateDTO tradeStateDTO = TradeStateDTO.of(tradeOrder.getTradeId(), TradeState.PROCESSING, | |
| 167 | - tradeOrder.getVersion(), now); | |
| 168 | - tradeAssistantService.proceedTradeOrder(tradeStateDTO); | |
| 169 | - return response; | |
| 170 | - } else { | |
| 171 | - throw new TradePaymentException(ErrorCode.OPERATION_NOT_ALLOWED, "不支持的收银台类型"); | |
| 169 | + // 只有成功才修改交易订单状态,此时交易订单仍然可以继续支付 | |
| 170 | + if (response.getState() == PaymentState.SUCCESS) { | |
| 171 | + TradeStateDTO tradeStateDTO = TradeStateDTO.of(trade.getTradeId(), TradeState.SUCCESS, | |
| 172 | + trade.getVersion(), now); | |
| 173 | + tradeAssistantService.proceedTradeOrder(tradeStateDTO); | |
| 174 | + // 支付成功才通知业务系统支付结果,失败可以再次进行支付 | |
| 175 | + OnlinePaymentResult paymentResult = new OnlinePaymentResult(trade.getTradeId(), paymentId, | |
| 176 | + response.getState().getCode(), trade.getOutTradeNo(), OutPaymentType.DILICARD.getCode(), | |
| 177 | + response.getPayerId(), response.getWhen(), response.getMessage()); | |
| 178 | + paymentResultManager.notifyPaymentResult(trade.getNotifyUrl(), paymentResult); | |
| 179 | + } | |
| 172 | 180 | } |
| 181 | + return response; | |
| 173 | 182 | } else { |
| 174 | 183 | // 目前只有两类支付通道: CardPipeline和OnlinePipeline, 程序逻辑不应该到达此代码块 |
| 175 | 184 | throw new TradePaymentException(ErrorCode.OPERATION_NOT_ALLOWED, "不支持的支付通道类型"); |
| ... | ... | @@ -187,4 +196,240 @@ public class CashierPaymentServiceImpl implements ICashierPaymentService { |
| 187 | 196 | } |
| 188 | 197 | } |
| 189 | 198 | } |
| 199 | + | |
| 200 | + /** | |
| 201 | + * 通知支付结果 | |
| 202 | + */ | |
| 203 | + @Override | |
| 204 | + @Transactional(rollbackFor = Exception.class) | |
| 205 | + public void notifyPaymentResponse(OnlinePaymentResponse response) { | |
| 206 | + OnlinePayment payment = tradeAssistantService.findByPaymentId(response.getPaymentId()); | |
| 207 | + if (PaymentState.isFinished(payment.getState())) { | |
| 208 | + LOG.warn("Duplicate process payment result notification for {}:{}", payment.getPaymentId(), response.getState()); | |
| 209 | + return; | |
| 210 | + } | |
| 211 | + LOG.info("Processing payment result notification: [{},{}]", payment.getPaymentId(), response.getState()); | |
| 212 | + | |
| 213 | + if (PaymentState.isFinished(response.getState().getCode())) { | |
| 214 | + LocalDateTime now = LocalDateTime.now(); | |
| 215 | + String lockKey = String.format(Constants.TRADE_LOCK_REDIS_KEY, payment.getTradeId()); | |
| 216 | + RLock lock = redissonClient.getLock(lockKey); | |
| 217 | + try { | |
| 218 | + lock.lock(); | |
| 219 | + TradeOrder trade = tradeAssistantService.findByTradeId(payment.getTradeId()); | |
| 220 | + PaymentStateDTO paymentDTO = PaymentStateDTO.builder().paymentId(payment.getPaymentId()) | |
| 221 | + .outTradeNo(response.getOutTradeNo()).outPayType(response.getOutPayType()).payerId(response.getPayerId()) | |
| 222 | + .finishTime(response.getWhen()).state(response.getState()).description(response.getMessage()) | |
| 223 | + .version(payment.getVersion()).modifiedTime(now).build(); | |
| 224 | + tradeAssistantService.proceedOnlinePayment(paymentDTO); | |
| 225 | + | |
| 226 | + // 交易订单未完成,支付订单成功支付时处理交易订单,并通知业务系统支付结果;如果支付订单支付失败,收银台交易订单可以继续支付 | |
| 227 | + if (TradeState.PENDING.equalTo(trade.getState()) && PaymentState.SUCCESS == response.getState()) { | |
| 228 | + TradeStateDTO tradeStateDTO = TradeStateDTO.of(trade.getTradeId(), TradeState.SUCCESS, trade.getVersion(), now); | |
| 229 | + tradeAssistantService.proceedTradeOrder(tradeStateDTO); | |
| 230 | + | |
| 231 | + OnlinePaymentResult paymentResult = OnlinePaymentResult.of(trade.getTradeId(), payment.getPaymentId(), | |
| 232 | + response.getState(), trade.getOutTradeNo(), response.getOutPayType(), response.getPayerId(), | |
| 233 | + response.getWhen() != null ? response.getWhen() : now, response.getMessage()); | |
| 234 | + paymentResultManager.notifyPaymentResult(trade.getNotifyUrl(), paymentResult); | |
| 235 | + } | |
| 236 | + } finally { | |
| 237 | + if (lock.isHeldByCurrentThread()) { | |
| 238 | + lock.unlock(); | |
| 239 | + } | |
| 240 | + } | |
| 241 | + } | |
| 242 | + } | |
| 243 | + | |
| 244 | + @Override | |
| 245 | + @Transactional(rollbackFor = Exception.class) | |
| 246 | + public void closePrepayOrder(String paymentId) { | |
| 247 | + OnlinePayment payment = tradeAssistantService.findByPaymentId(paymentId); | |
| 248 | + if (PaymentState.isFinished(payment.getState())) { | |
| 249 | + throw new TradePaymentException(ErrorCode.OPERATION_NOT_ALLOWED, "不能关闭预支付订单: 无效的订单状态"); | |
| 250 | + } | |
| 251 | + | |
| 252 | + String lockKey = String.format(Constants.TRADE_LOCK_REDIS_KEY, payment.getTradeId()); | |
| 253 | + RLock lock = redissonClient.getLock(lockKey); | |
| 254 | + try { | |
| 255 | + lock.lock(); | |
| 256 | + | |
| 257 | + LocalDateTime now = LocalDateTime.now(); | |
| 258 | + TradeOrder trade = tradeAssistantService.findByTradeId(payment.getTradeId()); | |
| 259 | + PaymentPipeline<?> pipeline = paymentPipelineManager.findPipelineById(payment.getPipelineId(), PaymentPipeline.class); | |
| 260 | + // 只有在线支付通道才可以关闭支付订单,园区卡支付不支持 | |
| 261 | + if (pipeline instanceof OnlinePipeline<?> onlinePipeline) { | |
| 262 | + OnlinePrepayOrder order = new OnlinePrepayOrder(paymentId, payment.getOutTradeNo()); | |
| 263 | + // 用于微信服务商模式下,微信子商户信息 | |
| 264 | + order.attach(com.diligrp.cashier.pipeline.Constants.PARAM_MCH_ID, payment.getOutMchId()); | |
| 265 | + onlinePipeline.closePrepayOrder(order); | |
| 266 | + PaymentStateDTO paymentDTO = PaymentStateDTO.builder().paymentId(payment.getPaymentId()) | |
| 267 | + .outTradeNo(null).outPayType(null).payerId(null).finishTime(now).state(PaymentState.FAILED) | |
| 268 | + .description("人工关闭支付订单").version(payment.getVersion()).modifiedTime(now).build(); | |
| 269 | + tradeAssistantService.proceedOnlinePayment(paymentDTO); | |
| 270 | + | |
| 271 | + TradeStateDTO tradeStateDTO = TradeStateDTO.of(trade.getTradeId(), TradeState.CLOSED, trade.getVersion(), now); | |
| 272 | + tradeAssistantService.proceedTradeOrder(tradeStateDTO); | |
| 273 | + } | |
| 274 | + } finally { | |
| 275 | + if (lock.isHeldByCurrentThread()) { | |
| 276 | + lock.unlock(); | |
| 277 | + } | |
| 278 | + } | |
| 279 | + } | |
| 280 | + | |
| 281 | + @Override | |
| 282 | + public OnlinePaymentResult queryPaymentState(String paymentId, String mode) { | |
| 283 | + OnlinePayment payment = tradeAssistantService.findByPaymentId(paymentId); | |
| 284 | + TradeOrder trade = tradeAssistantService.findByTradeId(payment.getTradeId()); | |
| 285 | + | |
| 286 | + LOG.debug("Query online prepay order[{}-{}] state...", paymentId, payment.getState()); | |
| 287 | + // online模式下,如果本地支付申请没有明确的支付结果,将调用支付通道查询预支付订单状态 | |
| 288 | + if (!PaymentState.isFinished(payment.getState()) && "online".equalsIgnoreCase(mode)) { | |
| 289 | + OnlinePrepayOrder order = new OnlinePrepayOrder(paymentId, payment.getOutTradeNo()); | |
| 290 | + PaymentPipeline<?> paymentPipeline = paymentPipelineManager.findPipelineById(payment.getPipelineId(), PaymentPipeline.class); | |
| 291 | + if (paymentPipeline instanceof OnlinePipeline<?> pipeline) { | |
| 292 | + // 用于微信服务商模式下,微信子商户信息 | |
| 293 | + order.attach(com.diligrp.cashier.pipeline.Constants.PARAM_MCH_ID, payment.getOutMchId()); | |
| 294 | + OnlinePaymentResponse response = pipeline.queryPrepayResponse(order); | |
| 295 | + return OnlinePaymentResult.of(trade.getTradeId(), paymentId, response.getState(), trade.getOutTradeNo(), | |
| 296 | + response.getOutPayType(), response.getPayerId(), response.getWhen(), response.getMessage()); | |
| 297 | + } else if (paymentPipeline instanceof DiliCardPipeline pipeline) { | |
| 298 | + // 园区卡支付outMchId为市场ID | |
| 299 | + order.attach(com.diligrp.cashier.pipeline.Constants.PARAM_MCH_ID, payment.getOutMchId()); | |
| 300 | + CardPaymentResponse response = pipeline.queryPaymentResponse(order); | |
| 301 | + return OnlinePaymentResult.of(trade.getTradeId(), paymentId, response.getState(), trade.getOutTradeNo(), | |
| 302 | + response.getOutPayType(), response.getPayerId(), response.getWhen(), response.getMessage()); | |
| 303 | + } | |
| 304 | + } | |
| 305 | + return new OnlinePaymentResult(trade.getTradeId(), paymentId, payment.getState(), trade.getOutTradeNo(), | |
| 306 | + payment.getOutPayType(), payment.getPayerId(), payment.getFinishTime(), payment.getDescription()); | |
| 307 | + /*LOG.debug("Query online trade order[{}] state...", tradeId); | |
| 308 | + TradeOrder trade = tradeAssistantService.findByTradeId(tradeId); | |
| 309 | + List<OnlinePayment> onlinePayments = onlinePaymentDao.listOnlinePayments(tradeId, TradeType.TRADE.getCode()); | |
| 310 | + if (TradeState.SUCCESS.equalTo(trade.getState()) || TradeState.REFUND.equalTo(trade.getState())) { | |
| 311 | + OnlinePayment payment = onlinePayments.stream().filter(p -> PaymentState.SUCCESS.equalTo(p.getState())) | |
| 312 | + .findFirst().orElseThrow(() -> new TradePaymentException(ErrorCode.OBJECT_NOT_FOUND, "未找到支付订单")); | |
| 313 | + return new OnlinePaymentResult(tradeId, payment.getState(), trade.getOutTradeNo(), payment.getOutPayType(), | |
| 314 | + payment.getPayerId(), payment.getFinishTime(), payment.getDescription()); | |
| 315 | + } else if (TradeState.FAILED.equalTo(trade.getState()) || TradeState.CLOSED.equalTo(trade.getState())) { | |
| 316 | + return OnlinePaymentResult.of(tradeId, PaymentState.FAILED, trade.getOutTradeNo(), | |
| 317 | + null, null, LocalDateTime.now(), "等待支付"); | |
| 318 | + } else { // 交易订单等待支付 | |
| 319 | + // 查找交易订单中正在支付中的支付记录 | |
| 320 | + OnlinePayment payment = onlinePayments.stream().filter(p -> PaymentState.PROCESSING.equalTo(p.getState())) | |
| 321 | + .findFirst().orElse(null); | |
| 322 | + if (payment == null) { // 交易订单没有支付记录 | |
| 323 | + return OnlinePaymentResult.of(tradeId, PaymentState.PENDING, trade.getOutTradeNo(), | |
| 324 | + null, null, LocalDateTime.now(), "等待支付"); | |
| 325 | + } | |
| 326 | + if ("online".equalsIgnoreCase(mode)) { | |
| 327 | + OnlinePrepayOrder order = new OnlinePrepayOrder(payment.getPaymentId(), payment.getOutTradeNo()); | |
| 328 | + OnlinePipeline<?> pipeline = paymentPipelineManager.findPipelineById(payment.getPipelineId(), OnlinePipeline.class); | |
| 329 | + // 用于微信服务商模式下,微信子商户信息 | |
| 330 | + order.attach(com.diligrp.cashier.pipeline.Constants.PARAM_MCH_ID, payment.getOutMchId()); | |
| 331 | + OnlinePaymentResponse response = pipeline.queryPrepayResponse(order); | |
| 332 | + return OnlinePaymentResult.of(tradeId, response.getState(), trade.getOutTradeNo(), | |
| 333 | + response.getOutPayType(), response.getPayerId(), response.getWhen(), response.getMessage()); | |
| 334 | + } else { | |
| 335 | + return new OnlinePaymentResult(tradeId, payment.getState(), trade.getOutTradeNo(), payment.getOutPayType(), | |
| 336 | + payment.getPayerId(), payment.getFinishTime(), payment.getDescription()); | |
| 337 | + } | |
| 338 | + | |
| 339 | + }*/ | |
| 340 | + } | |
| 341 | + | |
| 342 | + @Override | |
| 343 | + @Transactional(rollbackFor = Exception.class) | |
| 344 | + public OnlineRefundResult sendRefundRequest(OnlineRefundDTO request) { | |
| 345 | + TradeOrder trade = tradeAssistantService.findByTradeId(request.getTradeId()); | |
| 346 | + if (!TradeState.forRefund(trade.getState())) { | |
| 347 | + throw new TradePaymentException(ErrorCode.INVALID_OBJECT_STATE, "不能进行交易退款: 无效的交易状态"); | |
| 348 | + } | |
| 349 | + if (trade.getAmount() < request.getAmount()) { | |
| 350 | + throw new TradePaymentException(ErrorCode.OPERATION_NOT_ALLOWED, "申请退费金额超过原支付金额"); | |
| 351 | + } | |
| 352 | + // 理论上只会存在一条支付完成的支付订单 | |
| 353 | + OnlinePayment payment = onlinePaymentDao.listOnlinePayments(trade.getTradeId(), | |
| 354 | + TradeType.TRADE.getCode(), PaymentState.SUCCESS.getCode()).stream().findFirst() | |
| 355 | + .orElseThrow(() -> new TradePaymentException(ErrorCode.OPERATION_NOT_ALLOWED, "不能进行交易退款: 无支付信息")); | |
| 356 | + | |
| 357 | + LocalDateTime now = LocalDateTime.now().withNano(0); | |
| 358 | + KeyGenerator refundIdKey = snowflakeKeyManager.getKeyGenerator(SnowflakeKey.PAYMENT_ID); | |
| 359 | + String refundId = refundIdKey.nextId(); | |
| 360 | + OnlinePipeline<?> pipeline = paymentPipelineManager.findPipelineById(payment.getPipelineId(), OnlinePipeline.class); | |
| 361 | + OnlineRefundRequest refundRequest = OnlineRefundRequest.of(refundId, payment.getPaymentId(), payment.getOutTradeNo(), | |
| 362 | + trade.getMaxAmount(), request.getAmount(), request.getDescription(), now); | |
| 363 | + refundRequest.attach(com.diligrp.cashier.pipeline.Constants.PARAM_MCH_ID, payment.getOutMchId()); // 用于微信服务商模式下,微信子商户信息 | |
| 364 | + OnlineRefundResponse response = pipeline.sendRefundRequest(refundRequest); | |
| 365 | + | |
| 366 | + OnlinePayment refund = OnlinePayment.builder().outMchId(payment.getOutMchId()).tradeId(payment.getTradeId()) | |
| 367 | + .type(TradeType.REFUND).paymentId(refundId).channelId(payment.getChannelId()) | |
| 368 | + .payType(payment.getPayType()).pipelineId(payment.getPipelineId()).goods(payment.getGoods() + "-退款") | |
| 369 | + .amount(request.getAmount()).objectId(payment.getPaymentId()).payerId(payment.getPayerId()) | |
| 370 | + .finishTime(response.getWhen()).outTradeNo(response.getOutTradeNo()).outPayType(payment.getOutPayType()) | |
| 371 | + .state(response.getState()).description(request.getDescription()).version(0).createdTime(now).modifiedTime(now).build(); | |
| 372 | + onlinePaymentDao.insertOnlinePayment(refund); | |
| 373 | + | |
| 374 | + if (PaymentState.SUCCESS.equalTo(response.getState())) { | |
| 375 | + Long newAmount = trade.getAmount() - refund.getAmount(); | |
| 376 | + TradeStateDTO tradeState = TradeStateDTO.of(trade.getTradeId(), newAmount, TradeState.REFUND, trade.getVersion(), now); | |
| 377 | + tradeAssistantService.proceedTradeOrder(tradeState); | |
| 378 | + } else if (!PaymentState.isFinished(response.getState())) { | |
| 379 | + // 固定周期后,查询退款状态,根据状态完成退款订单 | |
| 380 | + TaskMessage message = new TaskMessage(TaskMessage.TYPE_CASHIER_REFUND_SCAN, refundId, "1"); | |
| 381 | + taskMessageSender.sendDelayTaskMessage(message, trade.getTimeout()); | |
| 382 | + } | |
| 383 | + | |
| 384 | + return new OnlineRefundResult(refundId, payment.getPaymentId(), response.getState(), response.getWhen(), response.getMessage()); | |
| 385 | + } | |
| 386 | + | |
| 387 | + @Override | |
| 388 | + @Transactional(rollbackFor = Exception.class) | |
| 389 | + public void notifyRefundResult(OnlineRefundResponse response) { | |
| 390 | + OnlinePayment refund = tradeAssistantService.findByRefundId(response.getRefundId()); | |
| 391 | + if (PaymentState.isFinished(refund.getState())) { | |
| 392 | + LOG.warn("Duplicate process online refund order notification for {}:{}", response.getRefundId(), response.getState()); | |
| 393 | + return; | |
| 394 | + } | |
| 395 | + if (!PaymentState.isFinished(response.getState())) { | |
| 396 | + LOG.warn("Ignore online refund order notification for {}:{}", response.getRefundId(), response.getState()); | |
| 397 | + return; | |
| 398 | + } | |
| 399 | + LOG.info("Processing online refund order notification for {}:{}", response.getRefundId(), response.getState()); | |
| 400 | + | |
| 401 | + LocalDateTime now = LocalDateTime.now(); | |
| 402 | + TradeOrder trade = tradeAssistantService.findByTradeId(refund.getTradeId()); | |
| 403 | + PaymentStateDTO refundDTO = PaymentStateDTO.builder().paymentId(refund.getPaymentId()) | |
| 404 | + .outTradeNo(response.getOutTradeNo()).finishTime(response.getWhen()).state(response.getState()) | |
| 405 | + .description(response.getMessage()).version(refund.getVersion()).modifiedTime(now).build(); | |
| 406 | + tradeAssistantService.proceedOnlinePayment(refundDTO); | |
| 407 | + | |
| 408 | + if (PaymentState.SUCCESS.equalTo(response.getState())) { | |
| 409 | + Long newAmount = trade.getAmount() - refund.getAmount(); | |
| 410 | + TradeStateDTO tradeState = TradeStateDTO.of(trade.getTradeId(), newAmount, TradeState.REFUND, trade.getVersion(), now); | |
| 411 | + tradeAssistantService.proceedTradeOrder(tradeState); | |
| 412 | + } | |
| 413 | + | |
| 414 | + OnlineRefundResult refundResult = new OnlineRefundResult(response.getRefundId(), refund.getObjectId(), | |
| 415 | + response.getState(), response.getWhen(), response.getMessage()); | |
| 416 | + paymentResultManager.notifyRefundResult(refund.getNotifyUrl(), refundResult); | |
| 417 | + } | |
| 418 | + | |
| 419 | + @Override | |
| 420 | + public OnlineRefundResult queryRefundState(String refundId, String mode) { | |
| 421 | + OnlinePayment refund = tradeAssistantService.findByRefundId(refundId); | |
| 422 | + | |
| 423 | + LOG.debug("Query online refund order[{}-{}] state...", refundId, refund.getState()); | |
| 424 | + // 微信支付通知较为及时和安全,非特殊情况可使用offline模式;一些本地状态与微信状态不一致的"异常订单"可使用online模式同步状态 | |
| 425 | + if (!PaymentState.isFinished(refund.getState()) && "online".equalsIgnoreCase(mode)) { | |
| 426 | + WechatPipeline pipeline = paymentPipelineManager.findPipelineById(refund.getPipelineId(), WechatPipeline.class); | |
| 427 | + OnlineRefundOrder order = new OnlineRefundOrder(refundId, refund.getOutTradeNo()); | |
| 428 | + // 用于微信服务商模式下,微信子商户信息 | |
| 429 | + order.attach(com.diligrp.cashier.pipeline.Constants.PARAM_MCH_ID, refund.getOutMchId()); | |
| 430 | + OnlineRefundResponse response = pipeline.queryRefundResponse(order); | |
| 431 | + return new OnlineRefundResult(refundId, refund.getObjectId(), response.getState(), response.getWhen(), response.getMessage()); | |
| 432 | + } | |
| 433 | + return new OnlineRefundResult(refundId, refund.getObjectId(), refund.getState(), refund.getFinishTime(), refund.getDescription()); | |
| 434 | + } | |
| 190 | 435 | } | ... | ... |
cashier-trade/src/main/java/com/diligrp/cashier/trade/service/impl/TradeAssistantServiceImpl.java
| ... | ... | @@ -15,7 +15,7 @@ import com.diligrp.cashier.trade.exception.TradePaymentException; |
| 15 | 15 | import com.diligrp.cashier.trade.model.OnlinePayment; |
| 16 | 16 | import com.diligrp.cashier.trade.model.TradeOrder; |
| 17 | 17 | import com.diligrp.cashier.trade.service.ITradeAssistantService; |
| 18 | -import com.diligrp.cashier.trade.type.TradeState; | |
| 18 | +import com.diligrp.cashier.trade.type.TradeType; | |
| 19 | 19 | import jakarta.annotation.Resource; |
| 20 | 20 | import org.slf4j.Logger; |
| 21 | 21 | import org.slf4j.LoggerFactory; |
| ... | ... | @@ -44,7 +44,7 @@ public class TradeAssistantServiceImpl implements ITradeAssistantService { |
| 44 | 44 | @Override |
| 45 | 45 | public TradeOrder findByTradeId(String tradeId) { |
| 46 | 46 | Optional<TradeOrder> tradeOpt = tradeOrderDao.findByTradeId(tradeId); |
| 47 | - return tradeOpt.orElseThrow(() -> new TradePaymentException(ErrorCode.OBJECT_NOT_FOUND, "支付订单不存在")); | |
| 47 | + return tradeOpt.orElseThrow(() -> new TradePaymentException(ErrorCode.OBJECT_NOT_FOUND, "交易订单不存在")); | |
| 48 | 48 | } |
| 49 | 49 | |
| 50 | 50 | @Override |
| ... | ... | @@ -58,46 +58,41 @@ public class TradeAssistantServiceImpl implements ITradeAssistantService { |
| 58 | 58 | * 关闭交易订单下状态为支付中的支付订单, 园区卡支付不存在支付中的订单 |
| 59 | 59 | * |
| 60 | 60 | * 独立数据库事务, 避免远程支付通道关单后, 支付订单状态仍然为支付中 |
| 61 | - * 任何一笔关闭订单失败都应该返回FALSE,且一笔失败不影响其他能正常的关单操作 | |
| 62 | 61 | */ |
| 63 | 62 | @Override |
| 64 | 63 | @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) |
| 65 | - public boolean resetTradeOrder(TradeOrder tradeOrder) { | |
| 66 | - // 只有处理中的交易订单存在状态为支付中的支付订单 | |
| 67 | - if (!TradeState.isProcessing(tradeOrder.getState())) { | |
| 68 | - return true; | |
| 69 | - } | |
| 70 | - | |
| 71 | - boolean result = true; | |
| 72 | - // 理论上只会存在一条支付中的支付订单 | |
| 73 | - List<OnlinePayment> onlinePayments = onlinePaymentDao.findByTradeId(tradeOrder.getTradeId(), PaymentState.PROCESSING.getCode()); | |
| 74 | - for (OnlinePayment onlinePayment : onlinePayments) { | |
| 75 | - try { | |
| 76 | - PaymentPipeline<?> pipeline = paymentPipelineManager.findPipelineById(onlinePayment.getPipelineId(), PaymentPipeline.class); | |
| 64 | + public boolean resetTradeOrder(TradeOrder trade) { | |
| 65 | + // 查询支付订单所有的支付记录, 关闭支付中的支付记录, 忽略支付失败的支付记录, 存在支付成功的支付记录时不允许继续支付 | |
| 66 | + List<OnlinePayment> onlinePayments = onlinePaymentDao.listOnlinePayments(trade.getTradeId(), TradeType.TRADE.getCode(), null); | |
| 67 | + for (OnlinePayment payment : onlinePayments) { | |
| 68 | + if (PaymentState.PENDING.equalTo(payment.getState()) || PaymentState.PROCESSING.equalTo(payment.getState())) { | |
| 69 | + PaymentPipeline<?> pipeline = paymentPipelineManager.findPipelineById(payment.getPipelineId(), PaymentPipeline.class); | |
| 77 | 70 | if (pipeline instanceof OnlinePipeline<?> onlinePipeline) { |
| 78 | 71 | LocalDateTime now = LocalDateTime.now(); |
| 79 | - PaymentStateDTO paymentStateDTO = PaymentStateDTO.builder().paymentId(onlinePayment.getPaymentId()) | |
| 72 | + PaymentStateDTO paymentStateDTO = PaymentStateDTO.builder().paymentId(payment.getPaymentId()) | |
| 80 | 73 | .finishTime(now).state(PaymentState.FAILED).description("主动关闭支付中的订单") |
| 81 | - .version(onlinePayment.getVersion()).modifiedTime(now).build(); | |
| 74 | + .version(payment.getVersion()).modifiedTime(now).build(); | |
| 82 | 75 | proceedOnlinePayment(paymentStateDTO); |
| 83 | - OnlinePrepayOrder prepayOrder = new OnlinePrepayOrder(onlinePayment.getPaymentId(), onlinePayment.getOutTradeNo()); | |
| 76 | + OnlinePrepayOrder prepayOrder = new OnlinePrepayOrder(payment.getPaymentId(), payment.getOutTradeNo()); | |
| 84 | 77 | // 微信服务商模式下, outMchId为签约子商户; 园区卡支付时, outMchId为市场ID |
| 85 | - prepayOrder.attach(Constants.PARAM_MCH_ID, onlinePayment.getOutMchId()); | |
| 78 | + prepayOrder.attach(Constants.PARAM_MCH_ID, payment.getOutMchId()); | |
| 86 | 79 | onlinePipeline.closePrepayOrder(prepayOrder); |
| 87 | 80 | } |
| 88 | - // 园区卡支付不存在支付中的订单 | |
| 89 | - } catch (Exception ex) { | |
| 90 | - result = false; | |
| 91 | - LOG.error("关闭支付订单失败: {}", onlinePayment.getPaymentId(), ex); | |
| 81 | + // 园区卡支付通道不会存在支付中的记录 | |
| 82 | + } else if (PaymentState.SUCCESS.equalTo(payment.getState())) { | |
| 83 | + return false; | |
| 84 | + } else if (PaymentState.FAILED.equalTo(payment.getState())) { | |
| 85 | + continue; | |
| 92 | 86 | } |
| 93 | 87 | } |
| 94 | - return result; | |
| 88 | + return true; | |
| 95 | 89 | } |
| 96 | 90 | |
| 97 | 91 | @Override |
| 98 | 92 | public OnlinePayment findByPaymentId(String paymentId) { |
| 99 | - Optional<OnlinePayment> paymentOpt = onlinePaymentDao.findByPaymentId(paymentId); | |
| 100 | - return paymentOpt.orElseThrow(() -> new TradePaymentException(ErrorCode.OBJECT_NOT_FOUND, "支付订单不存在")); | |
| 93 | + return onlinePaymentDao.findByPaymentId(paymentId) | |
| 94 | + .filter(p -> TradeType.TRADE.equalTo(p.getType())) | |
| 95 | + .orElseThrow(() -> new TradePaymentException(ErrorCode.OBJECT_NOT_FOUND, "支付订单不存在")); | |
| 101 | 96 | } |
| 102 | 97 | |
| 103 | 98 | @Override |
| ... | ... | @@ -106,4 +101,11 @@ public class TradeAssistantServiceImpl implements ITradeAssistantService { |
| 106 | 101 | throw new TradePaymentException(ErrorCode.SYSTEM_BUSY_ERROR, ErrorCode.MESSAGE_SYSTEM_BUSY); |
| 107 | 102 | } |
| 108 | 103 | } |
| 104 | + | |
| 105 | + @Override | |
| 106 | + public OnlinePayment findByRefundId(String refundId) { | |
| 107 | + return onlinePaymentDao.findByPaymentId(refundId) | |
| 108 | + .filter(p -> TradeType.REFUND.equalTo(p.getType())) | |
| 109 | + .orElseThrow(() -> new TradePaymentException(ErrorCode.OBJECT_NOT_FOUND, "退款订单不存在")); | |
| 110 | + } | |
| 109 | 111 | } | ... | ... |
cashier-trade/src/main/java/com/diligrp/cashier/trade/type/TradeState.java
| 1 | 1 | package com.diligrp.cashier.trade.type; |
| 2 | 2 | |
| 3 | -import com.diligrp.cashier.pipeline.type.PaymentState; | |
| 4 | 3 | import com.diligrp.cashier.shared.type.IEnumType; |
| 5 | 4 | |
| 6 | 5 | import java.util.Arrays; |
| ... | ... | @@ -15,8 +14,6 @@ public enum TradeState implements IEnumType { |
| 15 | 14 | |
| 16 | 15 | PENDING("待处理", 1), |
| 17 | 16 | |
| 18 | - PROCESSING("处理中", 2), | |
| 19 | - | |
| 20 | 17 | SUCCESS("交易成功", 4), |
| 21 | 18 | |
| 22 | 19 | REFUND("交易退款", 5), |
| ... | ... | @@ -50,14 +47,6 @@ public enum TradeState implements IEnumType { |
| 50 | 47 | return Arrays.asList(TradeState.values()); |
| 51 | 48 | } |
| 52 | 49 | |
| 53 | - public static boolean isPending(int code) { | |
| 54 | - return TradeState.PENDING.equalTo(code); | |
| 55 | - } | |
| 56 | - | |
| 57 | - public static boolean isProcessing(int code) { | |
| 58 | - return TradeState.PROCESSING.equalTo(code); | |
| 59 | - } | |
| 60 | - | |
| 61 | 50 | public static boolean isFinished(int code) { |
| 62 | 51 | return TradeState.SUCCESS.equalTo(code) || TradeState.FAILED.equalTo(code) || |
| 63 | 52 | TradeState.REFUND.equalTo(code) || TradeState.CLOSED.equalTo(code); | ... | ... |
cashier-trade/src/main/resources/com/diligrp/cashier/dao/mapper/IOnlinePaymentDao.xml
| ... | ... | @@ -20,6 +20,7 @@ |
| 20 | 20 | <result column="out_trade_no" property="outTradeNo"/> |
| 21 | 21 | <result column="out_pay_type" property="outPayType"/> |
| 22 | 22 | <result column="state" property="state"/> |
| 23 | + <result column="notify_url" property="notifyUrl"/> | |
| 23 | 24 | <result column="description" property="description"/> |
| 24 | 25 | <result column="version" property="version"/> |
| 25 | 26 | <result column="created_time" property="createdTime"/> |
| ... | ... | @@ -29,11 +30,11 @@ |
| 29 | 30 | <insert id="insertOnlinePayment" parameterType="com.diligrp.cashier.trade.model.OnlinePayment"> |
| 30 | 31 | INSERT INTO upay_online_payment(out_mch_id, trade_id, type, payment_id, channel_id, pay_type, pipeline_id, |
| 31 | 32 | goods, amount, object_id, payer_id, finish_time, out_trade_no, out_pay_type, |
| 32 | - state, description, version, created_time, modified_time) | |
| 33 | + state, notify_url, description, version, created_time, modified_time) | |
| 33 | 34 | VALUES |
| 34 | 35 | (#{outMchId}, #{tradeId}, #{type}, #{paymentId}, #{channelId}, #{payType}, #{pipelineId}, |
| 35 | 36 | #{goods}, #{amount}, #{objectId}, #{payerId}, #{finishTime}, #{outTradeNo}, #{outPayType} |
| 36 | - #{state}, #{description}, #{version}, #{createdTime}, #{modifiedTime}) | |
| 37 | + #{state}, #{notifyUrl}, #{description}, #{version}, #{createdTime}, #{modifiedTime}) | |
| 37 | 38 | </insert> |
| 38 | 39 | |
| 39 | 40 | <select id="findByPaymentId" parameterType="string" resultMap="OnlinePaymentMap"> |
| ... | ... | @@ -67,8 +68,11 @@ |
| 67 | 68 | payment_id = #{paymentId} AND version = #{version} |
| 68 | 69 | </update> |
| 69 | 70 | |
| 70 | - <select id="findByTradeId" resultMap="OnlinePaymentMap"> | |
| 71 | + <select id="listOnlinePayments" resultMap="OnlinePaymentMap"> | |
| 71 | 72 | SELECT * FROM upay_online_payment WHERE trade_id = #{tradeId} |
| 73 | + <if test="type != null"> | |
| 74 | + AND type = #{type} | |
| 75 | + </if> | |
| 72 | 76 | <if test="state != null"> |
| 73 | 77 | AND state = #{state} |
| 74 | 78 | </if> | ... | ... |
scripts/dili-cashier.sql
| ... | ... | @@ -26,14 +26,14 @@ CREATE TABLE `upay_trade_order` ( |
| 26 | 26 | `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', |
| 27 | 27 | `mch_id` BIGINT NOT NULL COMMENT '商户ID', |
| 28 | 28 | `trade_id` VARCHAR(40) NOT NULL COMMENT '交易ID', |
| 29 | - `type` TINYINT UNSIGNED NOT NULL COMMENT '业务类型', -- 购买会员 | |
| 29 | + `type` TINYINT UNSIGNED NOT NULL COMMENT '订单类型', | |
| 30 | 30 | `out_trade_no` VARCHAR(40) COMMENT '外部流水号', -- 商户流水号 |
| 31 | 31 | `amount` BIGINT NOT NULL COMMENT '金额-分', |
| 32 | 32 | `max_amount` BIGINT NOT NULL COMMENT '初始金额-分', |
| 33 | 33 | `goods` VARCHAR(128) NOT NULL COMMENT '商品描述', |
| 34 | 34 | `timeout` INTEGER UNSIGNED NOT NULL COMMENT '超时间隔时间-秒', |
| 35 | 35 | `state` TINYINT UNSIGNED NOT NULL COMMENT '交易状态', |
| 36 | - `notify_url` VARCHAR(128) COMMENT '业务回调链接', | |
| 36 | + `notify_url` VARCHAR(128) COMMENT '业务回调地址', | |
| 37 | 37 | `description` VARCHAR(128) COMMENT '交易备注', |
| 38 | 38 | `attach` VARCHAR(255) COMMENT '附加数据', |
| 39 | 39 | `source` TINYINT UNSIGNED COMMENT '订单来源', |
| ... | ... | @@ -56,7 +56,7 @@ CREATE TABLE `upay_online_payment` ( |
| 56 | 56 | `trade_id` VARCHAR(40) NOT NULL COMMENT '交易ID', |
| 57 | 57 | `type` TINYINT UNSIGNED NOT NULL COMMENT '交易类型', |
| 58 | 58 | `payment_id` VARCHAR(40) NOT NULL COMMENT '支付ID', |
| 59 | - `channel_id` TINYINT UNSIGNED NOT NULL COMMENT '支付通道', | |
| 59 | + `channel_id` TINYINT UNSIGNED NOT NULL COMMENT '支付渠道', | |
| 60 | 60 | `pay_type` TINYINT UNSIGNED NOT NULL COMMENT '支付方式', -- NATIVE MINIPRO |
| 61 | 61 | `pipeline_id` BIGINT NOT NULL COMMENT '通道ID', |
| 62 | 62 | `goods` VARCHAR(128) NOT NULL COMMENT '商品描述', |
| ... | ... | @@ -67,6 +67,7 @@ CREATE TABLE `upay_online_payment` ( |
| 67 | 67 | `out_trade_no` VARCHAR(40) COMMENT '通道流水号', |
| 68 | 68 | `out_pay_type` TINYINT UNSIGNED NOT NULL COMMENT '实际支付方式', -- 银行聚合支付时使用 |
| 69 | 69 | `state` TINYINT UNSIGNED NOT NULL COMMENT '申请状态', |
| 70 | + `notify_url` VARCHAR(128) COMMENT '业务回调地址', | |
| 70 | 71 | `description` VARCHAR(256) COMMENT '交易备注', |
| 71 | 72 | `version` INTEGER UNSIGNED NOT NULL COMMENT '数据版本号', |
| 72 | 73 | `created_time` DATETIME COMMENT '创建时间', | ... | ... |