Commit 7ef99d77fc257123c78bc8454f3ba5ba65aba7ae

Authored by huanggang
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
... ... @@ -13,4 +13,7 @@ public final class Constants {
13 13 public static final int TOKEN_SIGN_LENGTH = 8;
14 14 // TOKEN过期时长,单位秒
15 15 public static final long TOKEN_TIMEOUT_SECONDS = 60;
  16 +
  17 + public final static String CONTENT_TYPE = "application/json;charset=UTF-8";
  18 +
16 19 }
... ...
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
1 1 package com.diligrp.cashier.shared.spi;
2 2  
3   -@FunctionalInterface
4 3 public interface IPaymentEventListener {
5 4  
6 5 void onEvent(PaymentEvent event);
  6 +
  7 + void onEvent(RefundEvent event);
7 8 }
... ...
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
  1 +package com.diligrp.cashier.trade.service;
  2 +
  3 +public interface ICashierAssistantService {
  4 + /**
  5 + * 扫描收银台交易订单
  6 + */
  7 + void scanCashierTradeOrder(String tradeId, int times);
  8 +
  9 + /**
  10 + * 兜底处理退款订单
  11 + */
  12 + void scanCashierRefundOrder(String refundId, int times);
  13 +}
... ...
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
1   -package com.diligrp.cashier.trade.service;
2   -
3   -public interface IPaymentAssistantService {
4   -}
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 '创建时间',
... ...