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,4 +13,7 @@ public final class Constants {
13 public static final int TOKEN_SIGN_LENGTH = 8; 13 public static final int TOKEN_SIGN_LENGTH = 8;
14 // TOKEN过期时长,单位秒 14 // TOKEN过期时长,单位秒
15 public static final long TOKEN_TIMEOUT_SECONDS = 60; 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,12 +8,11 @@ import com.diligrp.cashier.boss.util.CashierOrderConverter;
8 import com.diligrp.cashier.pipeline.domain.OnlinePaymentStatus; 8 import com.diligrp.cashier.pipeline.domain.OnlinePaymentStatus;
9 import com.diligrp.cashier.shared.domain.Message; 9 import com.diligrp.cashier.shared.domain.Message;
10 import com.diligrp.cashier.shared.util.AssertUtils; 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 import jakarta.annotation.Resource; 12 import jakarta.annotation.Resource;
15 import org.springframework.web.bind.annotation.RequestBody; 13 import org.springframework.web.bind.annotation.RequestBody;
16 import org.springframework.web.bind.annotation.RequestMapping; 14 import org.springframework.web.bind.annotation.RequestMapping;
  15 +import org.springframework.web.bind.annotation.RequestParam;
17 import org.springframework.web.bind.annotation.RestController; 16 import org.springframework.web.bind.annotation.RestController;
18 17
19 @RestController 18 @RestController
@@ -43,6 +42,11 @@ public class CashierDeskController { @@ -43,6 +42,11 @@ public class CashierDeskController {
43 return Message.success(paymentUrl); 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 @RequestMapping("/orderPayment") 50 @RequestMapping("/orderPayment")
47 public Message<?> orderPayment(@RequestBody CashierPayment request) { 51 public Message<?> orderPayment(@RequestBody CashierPayment request) {
48 // 基本参数校验 52 // 基本参数校验
@@ -52,4 +56,36 @@ public class CashierDeskController { @@ -52,4 +56,36 @@ public class CashierDeskController {
52 OnlinePaymentStatus paymentStatus = cashierDeskService.doPayment(request); 56 OnlinePaymentStatus paymentStatus = cashierDeskService.doPayment(request);
53 return Message.success(paymentStatus); 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,7 +60,7 @@ public class CashierOrderToken {
60 return Base62Cipher.decodeLong(payload); 60 return Base62Cipher.decodeLong(payload);
61 } 61 }
62 62
63 - public static CashierOrderToken decode(String payload) { 63 + public static CashierOrderToken decodeCashierOrder(String payload) {
64 if (payload != null) { 64 if (payload != null) {
65 return JsonUtils.fromJsonString(payload, CashierOrderToken.class); 65 return JsonUtils.fromJsonString(payload, CashierOrderToken.class);
66 } 66 }
cashier-boss/src/main/java/com/diligrp/cashier/boss/service/ICashierDeskService.java
1 package com.diligrp.cashier.boss.service; 1 package com.diligrp.cashier.boss.service;
2 2
  3 +import com.diligrp.cashier.boss.domain.CashierOrderInfo;
3 import com.diligrp.cashier.boss.domain.CashierPaymentUrl; 4 import com.diligrp.cashier.boss.domain.CashierPaymentUrl;
4 import com.diligrp.cashier.pipeline.domain.OnlinePaymentStatus; 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 public interface ICashierDeskService { 8 public interface ICashierDeskService {
10 /** 9 /**
@@ -17,10 +16,36 @@ public interface ICashierDeskService { @@ -17,10 +16,36 @@ public interface ICashierDeskService {
17 CashierPaymentUrl doSubmit(Merchant merchant, CashierOrder order); 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 * @param payment - 支付信息 26 * @param payment - 支付信息
23 * @return 支付状态 27 * @return 支付状态
24 */ 28 */
25 OnlinePaymentStatus doPayment(CashierPayment payment); 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,19 +2,26 @@ package com.diligrp.cashier.boss.service.impl;
2 2
3 import com.diligrp.cashier.boss.CashierDeskProperties; 3 import com.diligrp.cashier.boss.CashierDeskProperties;
4 import com.diligrp.cashier.boss.Constants; 4 import com.diligrp.cashier.boss.Constants;
  5 +import com.diligrp.cashier.boss.domain.CashierOrderInfo;
5 import com.diligrp.cashier.boss.domain.CashierOrderToken; 6 import com.diligrp.cashier.boss.domain.CashierOrderToken;
6 import com.diligrp.cashier.boss.domain.CashierPaymentUrl; 7 import com.diligrp.cashier.boss.domain.CashierPaymentUrl;
  8 +import com.diligrp.cashier.boss.exception.BossServiceException;
7 import com.diligrp.cashier.boss.service.ICashierDeskService; 9 import com.diligrp.cashier.boss.service.ICashierDeskService;
  10 +import com.diligrp.cashier.pipeline.core.PaymentPipeline;
8 import com.diligrp.cashier.pipeline.domain.OnlinePaymentStatus; 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 import com.diligrp.cashier.trade.service.ICashierPaymentService; 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 import jakarta.annotation.Resource; 20 import jakarta.annotation.Resource;
15 import org.springframework.data.redis.core.StringRedisTemplate; 21 import org.springframework.data.redis.core.StringRedisTemplate;
16 import org.springframework.stereotype.Service; 22 import org.springframework.stereotype.Service;
17 23
  24 +import java.util.List;
18 import java.util.concurrent.TimeUnit; 25 import java.util.concurrent.TimeUnit;
19 26
20 @Service("cashierDeskService") 27 @Service("cashierDeskService")
@@ -24,6 +31,12 @@ public class CashierDeskServiceImpl implements ICashierDeskService { @@ -24,6 +31,12 @@ public class CashierDeskServiceImpl implements ICashierDeskService {
24 private ICashierPaymentService cashierPaymentService; 31 private ICashierPaymentService cashierPaymentService;
25 32
26 @Resource 33 @Resource
  34 + private ITradeAssistantService tradeAssistantService;
  35 +
  36 + @Resource
  37 + private IPaymentPipelineManager paymentPipelineManager;
  38 +
  39 + @Resource
27 private CashierDeskProperties cashierDeskProperties; 40 private CashierDeskProperties cashierDeskProperties;
28 41
29 @Resource 42 @Resource
@@ -54,6 +67,29 @@ public class CashierDeskServiceImpl implements ICashierDeskService { @@ -54,6 +67,29 @@ public class CashierDeskServiceImpl implements ICashierDeskService {
54 return new CashierPaymentUrl(tradeId, paymentUrl); 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,10 +98,31 @@ public class CashierDeskServiceImpl implements ICashierDeskService {
62 */ 98 */
63 @Override 99 @Override
64 public OnlinePaymentStatus doPayment(CashierPayment payment) { 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,6 +4,7 @@ import com.diligrp.cashier.pipeline.domain.*;
4 import com.diligrp.cashier.pipeline.domain.wechat.ErrorMessage; 4 import com.diligrp.cashier.pipeline.domain.wechat.ErrorMessage;
5 import com.diligrp.cashier.pipeline.domain.wechat.WechatConfig; 5 import com.diligrp.cashier.pipeline.domain.wechat.WechatConfig;
6 import com.diligrp.cashier.pipeline.exception.PaymentPipelineException; 6 import com.diligrp.cashier.pipeline.exception.PaymentPipelineException;
  7 +import com.diligrp.cashier.pipeline.type.OutPaymentType;
7 import com.diligrp.cashier.pipeline.type.PaymentState; 8 import com.diligrp.cashier.pipeline.type.PaymentState;
8 import com.diligrp.cashier.pipeline.util.WechatConstants; 9 import com.diligrp.cashier.pipeline.util.WechatConstants;
9 import com.diligrp.cashier.pipeline.util.WechatSignatureUtils; 10 import com.diligrp.cashier.pipeline.util.WechatSignatureUtils;
@@ -130,8 +131,8 @@ public class WechatDirectHttpClient extends WechatHttpClient { @@ -130,8 +131,8 @@ public class WechatDirectHttpClient extends WechatHttpClient {
130 Map<String, Object> payer = (Map<String, Object>) response.get("payer"); 131 Map<String, Object> payer = (Map<String, Object>) response.get("payer");
131 String openId = payer == null ? null : (String) payer.get("openid"); 132 String openId = payer == null ? null : (String) payer.get("openid");
132 PaymentState state = WechatStateUtils.getPaymentState((String) response.get("trade_state")); 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 } else { 136 } else {
136 LOG.info("Wechat query transaction status failed: {}", result.statusCode); 137 LOG.info("Wechat query transaction status failed: {}", result.statusCode);
137 ErrorMessage message = JsonUtils.fromJsonString(result.responseText, ErrorMessage.class); 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,6 +5,7 @@ import com.diligrp.cashier.pipeline.domain.*;
5 import com.diligrp.cashier.pipeline.domain.wechat.ErrorMessage; 5 import com.diligrp.cashier.pipeline.domain.wechat.ErrorMessage;
6 import com.diligrp.cashier.pipeline.domain.wechat.WechatConfig; 6 import com.diligrp.cashier.pipeline.domain.wechat.WechatConfig;
7 import com.diligrp.cashier.pipeline.exception.PaymentPipelineException; 7 import com.diligrp.cashier.pipeline.exception.PaymentPipelineException;
  8 +import com.diligrp.cashier.pipeline.type.OutPaymentType;
8 import com.diligrp.cashier.pipeline.type.PaymentState; 9 import com.diligrp.cashier.pipeline.type.PaymentState;
9 import com.diligrp.cashier.pipeline.util.WechatConstants; 10 import com.diligrp.cashier.pipeline.util.WechatConstants;
10 import com.diligrp.cashier.pipeline.util.WechatSignatureUtils; 11 import com.diligrp.cashier.pipeline.util.WechatSignatureUtils;
@@ -134,8 +135,8 @@ public class WechatPartnerHttpClient extends WechatHttpClient { @@ -134,8 +135,8 @@ public class WechatPartnerHttpClient extends WechatHttpClient {
134 Map<String, Object> payer = (Map<String, Object>) response.get("payer"); 135 Map<String, Object> payer = (Map<String, Object>) response.get("payer");
135 String openId = payer == null ? null : (String) payer.get("sp_openid"); // 获取服务商APPID下的openId,而非子商户APPID下的openId 136 String openId = payer == null ? null : (String) payer.get("sp_openid"); // 获取服务商APPID下的openId,而非子商户APPID下的openId
136 PaymentState state = WechatStateUtils.getPaymentState((String) response.get("trade_state")); 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 } else { 140 } else {
140 LOG.info("Wechat query transaction status failed: {}", result.statusCode); 141 LOG.info("Wechat query transaction status failed: {}", result.statusCode);
141 ErrorMessage message = JsonUtils.fromJsonString(result.responseText, ErrorMessage.class); 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,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 private static final Logger LOG = LoggerFactory.getLogger(DiliCardPipeline.class); 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,21 +18,6 @@ public class OnlinePaymentResponse extends OnlinePaymentStatus {
18 // 交易备注 18 // 交易备注
19 private final String message; 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 public OnlinePaymentResponse(String paymentId, String outTradeNo, OutPaymentType outPayType, 21 public OnlinePaymentResponse(String paymentId, String outTradeNo, OutPaymentType outPayType,
37 String payerId, LocalDateTime when, PaymentState state, String message) { 22 String payerId, LocalDateTime when, PaymentState state, String message) {
38 super(paymentId, outTradeNo, state); 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,7 +8,8 @@ import java.time.LocalDateTime;
8 8
9 public class CardPaymentResponse extends OnlinePaymentResponse { 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 super(paymentId, outTradeNo, outPayType, payerId, when, state, message); 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,14 +46,6 @@ public enum PaymentState implements IEnumType {
46 return Arrays.asList(PaymentState.values()); 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 public static boolean isFinished(int code) { 49 public static boolean isFinished(int code) {
58 return PaymentState.SUCCESS.equalTo(code) || PaymentState.FAILED.equalTo(code); 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 package com.diligrp.cashier.shared.spi; 1 package com.diligrp.cashier.shared.spi;
2 2
3 -@FunctionalInterface  
4 public interface IPaymentEventListener { 3 public interface IPaymentEventListener {
5 4
6 void onEvent(PaymentEvent event); 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,8 +3,10 @@ package com.diligrp.cashier.shared.spi;
3 import java.time.LocalDateTime; 3 import java.time.LocalDateTime;
4 4
5 public class PaymentEvent { 5 public class PaymentEvent {
6 - // 支付ID 6 + // 交易号
7 private final String tradeId; 7 private final String tradeId;
  8 + // 支付ID
  9 + private final String paymentId;
8 // 支付状态 10 // 支付状态
9 private final Integer state; 11 private final Integer state;
10 // 业务系统订单号 12 // 业务系统订单号
@@ -18,8 +20,10 @@ public class PaymentEvent { @@ -18,8 +20,10 @@ public class PaymentEvent {
18 // 交易描述 20 // 交易描述
19 private final String message; 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 this.tradeId = tradeId; 25 this.tradeId = tradeId;
  26 + this.paymentId = paymentId;
23 this.state = state; 27 this.state = state;
24 this.outTradeNo = outTradeNo; 28 this.outTradeNo = outTradeNo;
25 this.outPayType = outPayType; 29 this.outPayType = outPayType;
@@ -32,6 +36,10 @@ public class PaymentEvent { @@ -32,6 +36,10 @@ public class PaymentEvent {
32 return tradeId; 36 return tradeId;
33 } 37 }
34 38
  39 + public String getPaymentId() {
  40 + return paymentId;
  41 + }
  42 +
35 public Integer getState() { 43 public Integer getState() {
36 return state; 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,6 +2,15 @@ package com.diligrp.cashier.trade;
2 2
3 public final class Constants { 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 public static final int DEFAULT_ORDER_TIMEOUT_SECONDS = 10 * 60 * 1000; 15 public static final int DEFAULT_ORDER_TIMEOUT_SECONDS = 10 * 60 * 1000;
7 16
@@ -14,10 +23,4 @@ public final class Constants { @@ -14,10 +23,4 @@ public final class Constants {
14 // 支付订单分布式锁超时时长-秒 23 // 支付订单分布式锁超时时长-秒
15 public static final int TRADE_LOCK_TIMEOUT_SECONDS = 15 * 1000; 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,11 +2,45 @@ package com.diligrp.cashier.trade;
2 2
3 import com.diligrp.cashier.shared.mybatis.MybatisMapperSupport; 3 import com.diligrp.cashier.shared.mybatis.MybatisMapperSupport;
4 import org.mybatis.spring.annotation.MapperScan; 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 import org.springframework.context.annotation.ComponentScan; 10 import org.springframework.context.annotation.ComponentScan;
6 import org.springframework.context.annotation.Configuration; 11 import org.springframework.context.annotation.Configuration;
7 12
  13 +import java.util.HashMap;
  14 +import java.util.Map;
  15 +
8 @Configuration 16 @Configuration
9 @ComponentScan("com.diligrp.cashier.trade") 17 @ComponentScan("com.diligrp.cashier.trade")
10 @MapperScan(basePackages = {"com.diligrp.cashier.trade.dao"}, markerInterface = MybatisMapperSupport.class) 18 @MapperScan(basePackages = {"com.diligrp.cashier.trade.dao"}, markerInterface = MybatisMapperSupport.class)
11 public class TradeConfiguration { 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,5 +21,6 @@ public interface IOnlinePaymentDao extends MybatisMapperSupport {
21 21
22 int compareAndSetState(PaymentStateDTO paymentDTO); 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,7 +116,7 @@ public class PaymentStateDTO {
116 } 116 }
117 117
118 public Builder outPayType(OutPaymentType outPayType) { 118 public Builder outPayType(OutPaymentType outPayType) {
119 - PaymentStateDTO.this.outPayType = outPayType.getCode(); 119 + PaymentStateDTO.this.outPayType = outPayType != null ? outPayType.getCode() : null;
120 return this; 120 return this;
121 } 121 }
122 122
@@ -135,6 +135,11 @@ public class PaymentStateDTO { @@ -135,6 +135,11 @@ public class PaymentStateDTO {
135 return this; 135 return this;
136 } 136 }
137 137
  138 + public Builder state(Integer state) {
  139 + PaymentStateDTO.this.state = state;
  140 + return this;
  141 + }
  142 +
138 public Builder description(String description) { 143 public Builder description(String description) {
139 PaymentStateDTO.this.description = description; 144 PaymentStateDTO.this.description = description;
140 return this; 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,6 +19,22 @@ public class TradeStateDTO {
19 // 修改时间 19 // 修改时间
20 private LocalDateTime modifiedTime; 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 public String getTradeId() { 38 public String getTradeId() {
23 return tradeId; 39 return tradeId;
24 } 40 }
@@ -58,18 +74,4 @@ public class TradeStateDTO { @@ -58,18 +74,4 @@ public class TradeStateDTO {
58 public void setModifiedTime(LocalDateTime modifiedTime) { 74 public void setModifiedTime(LocalDateTime modifiedTime) {
59 this.modifiedTime = modifiedTime; 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 \ No newline at end of file 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,7 +5,8 @@ import com.diligrp.cashier.shared.service.ThreadPoolService;
5 import com.diligrp.cashier.shared.spi.IPaymentEventListener; 5 import com.diligrp.cashier.shared.spi.IPaymentEventListener;
6 import com.diligrp.cashier.shared.util.JsonUtils; 6 import com.diligrp.cashier.shared.util.JsonUtils;
7 import com.diligrp.cashier.shared.util.ObjectUtils; 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 import jakarta.annotation.Resource; 10 import jakarta.annotation.Resource;
10 import org.slf4j.Logger; 11 import org.slf4j.Logger;
11 import org.slf4j.LoggerFactory; 12 import org.slf4j.LoggerFactory;
@@ -25,7 +26,7 @@ public class PaymentResultManager { @@ -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 ThreadPoolService.getIoThreadPoll().submit(() -> { 30 ThreadPoolService.getIoThreadPoll().submit(() -> {
30 List<IPaymentEventListener> lifeCycles = eventListeners.stream().toList(); 31 List<IPaymentEventListener> lifeCycles = eventListeners.stream().toList();
31 for (IPaymentEventListener listener : lifeCycles) { 32 for (IPaymentEventListener listener : lifeCycles) {
@@ -55,6 +56,39 @@ public class PaymentResultManager { @@ -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 private static class NotifyHttpClient extends ServiceEndpointSupport { 92 private static class NotifyHttpClient extends ServiceEndpointSupport {
59 private final String baseUrl; 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,6 +40,8 @@ public class OnlinePayment extends BaseDO {
40 private Integer outPayType; 40 private Integer outPayType;
41 // 申请状态 41 // 申请状态
42 private Integer state; 42 private Integer state;
  43 + // 业务回调地址
  44 + private String notifyUrl;
43 // 备注 45 // 备注
44 private String description; 46 private String description;
45 47
@@ -163,6 +165,14 @@ public class OnlinePayment extends BaseDO { @@ -163,6 +165,14 @@ public class OnlinePayment extends BaseDO {
163 this.state = state; 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 public String getDescription() { 176 public String getDescription() {
167 return description; 177 return description;
168 } 178 }
@@ -201,11 +211,21 @@ public class OnlinePayment extends BaseDO { @@ -201,11 +211,21 @@ public class OnlinePayment extends BaseDO {
201 return this; 211 return this;
202 } 212 }
203 213
  214 + public Builder channelId(Integer channelType) {
  215 + OnlinePayment.this.channelId = channelType;
  216 + return this;
  217 + }
  218 +
204 public Builder payType(PaymentType payType) { 219 public Builder payType(PaymentType payType) {
205 OnlinePayment.this.payType = payType.getCode(); 220 OnlinePayment.this.payType = payType.getCode();
206 return this; 221 return this;
207 } 222 }
208 223
  224 + public Builder payType(Integer payType) {
  225 + OnlinePayment.this.payType = payType;
  226 + return this;
  227 + }
  228 +
209 public Builder pipelineId(Long pipelineId) { 229 public Builder pipelineId(Long pipelineId) {
210 OnlinePayment.this.pipelineId = pipelineId; 230 OnlinePayment.this.pipelineId = pipelineId;
211 return this; 231 return this;
@@ -246,11 +266,26 @@ public class OnlinePayment extends BaseDO { @@ -246,11 +266,26 @@ public class OnlinePayment extends BaseDO {
246 return this; 266 return this;
247 } 267 }
248 268
  269 + public Builder outPayType(Integer outPayType) {
  270 + OnlinePayment.this.outPayType = outPayType;
  271 + return this;
  272 + }
  273 +
249 public Builder state(PaymentState state) { 274 public Builder state(PaymentState state) {
250 OnlinePayment.this.state = state.getCode(); 275 OnlinePayment.this.state = state.getCode();
251 return this; 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 public Builder description(String description) { 289 public Builder description(String description) {
255 OnlinePayment.this.description = description; 290 OnlinePayment.this.description = description;
256 return this; 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 package com.diligrp.cashier.trade.service; 1 package com.diligrp.cashier.trade.service;
2 2
  3 +import com.diligrp.cashier.pipeline.domain.OnlinePaymentResponse;
3 import com.diligrp.cashier.pipeline.domain.OnlinePaymentStatus; 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 public interface ICashierPaymentService { 8 public interface ICashierPaymentService {
9 /** 9 /**
10 * 提交收银台订单 10 * 提交收银台订单
11 * 11 *
12 - * @param merchant - 接入商户 12 + * @param merchant - 接入商户
13 * @param cashierOrder - 订单申请 13 * @param cashierOrder - 订单申请
14 * @return 支付ID 14 * @return 支付ID
15 */ 15 */
@@ -22,4 +22,38 @@ public interface ICashierPaymentService { @@ -22,4 +22,38 @@ public interface ICashierPaymentService {
22 * @return 支付状态 22 * @return 支付状态
23 */ 23 */
24 OnlinePaymentStatus doPayment(CashierPayment cashierPayment); 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,6 +5,9 @@ import com.diligrp.cashier.trade.domain.TradeStateDTO;
5 import com.diligrp.cashier.trade.model.OnlinePayment; 5 import com.diligrp.cashier.trade.model.OnlinePayment;
6 import com.diligrp.cashier.trade.model.TradeOrder; 6 import com.diligrp.cashier.trade.model.TradeOrder;
7 7
  8 +import java.util.List;
  9 +import java.util.Optional;
  10 +
8 public interface ITradeAssistantService { 11 public interface ITradeAssistantService {
9 12
10 /** 13 /**
@@ -32,4 +35,8 @@ public interface ITradeAssistantService { @@ -32,4 +35,8 @@ public interface ITradeAssistantService {
32 */ 35 */
33 void proceedOnlinePayment(PaymentStateDTO paymentDTO); 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 package com.diligrp.cashier.trade.service.impl; 1 package com.diligrp.cashier.trade.service.impl;
2 2
  3 +import com.diligrp.cashier.assistant.service.KeyGenerator;
3 import com.diligrp.cashier.assistant.service.impl.SnowflakeKeyManager; 4 import com.diligrp.cashier.assistant.service.impl.SnowflakeKeyManager;
4 import com.diligrp.cashier.pipeline.core.DiliCardPipeline; 5 import com.diligrp.cashier.pipeline.core.DiliCardPipeline;
5 import com.diligrp.cashier.pipeline.core.OnlinePipeline; 6 import com.diligrp.cashier.pipeline.core.OnlinePipeline;
6 import com.diligrp.cashier.pipeline.core.PaymentPipeline; 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 import com.diligrp.cashier.pipeline.domain.card.CardPaymentRequest; 10 import com.diligrp.cashier.pipeline.domain.card.CardPaymentRequest;
11 import com.diligrp.cashier.pipeline.domain.card.CardPaymentResponse; 11 import com.diligrp.cashier.pipeline.domain.card.CardPaymentResponse;
12 import com.diligrp.cashier.pipeline.service.IPaymentPipelineManager; 12 import com.diligrp.cashier.pipeline.service.IPaymentPipelineManager;
@@ -20,6 +20,7 @@ import com.diligrp.cashier.trade.dao.IOnlinePaymentDao; @@ -20,6 +20,7 @@ import com.diligrp.cashier.trade.dao.IOnlinePaymentDao;
20 import com.diligrp.cashier.trade.dao.ITradeOrderDao; 20 import com.diligrp.cashier.trade.dao.ITradeOrderDao;
21 import com.diligrp.cashier.trade.domain.*; 21 import com.diligrp.cashier.trade.domain.*;
22 import com.diligrp.cashier.trade.exception.TradePaymentException; 22 import com.diligrp.cashier.trade.exception.TradePaymentException;
  23 +import com.diligrp.cashier.trade.manager.TaskMessageSender;
23 import com.diligrp.cashier.trade.manager.PaymentResultManager; 24 import com.diligrp.cashier.trade.manager.PaymentResultManager;
24 import com.diligrp.cashier.trade.model.OnlinePayment; 25 import com.diligrp.cashier.trade.model.OnlinePayment;
25 import com.diligrp.cashier.trade.model.TradeOrder; 26 import com.diligrp.cashier.trade.model.TradeOrder;
@@ -33,6 +34,8 @@ import com.diligrp.cashier.trade.util.MiniProPaymentConverter; @@ -33,6 +34,8 @@ import com.diligrp.cashier.trade.util.MiniProPaymentConverter;
33 import jakarta.annotation.Resource; 34 import jakarta.annotation.Resource;
34 import org.redisson.api.RLock; 35 import org.redisson.api.RLock;
35 import org.redisson.api.RedissonClient; 36 import org.redisson.api.RedissonClient;
  37 +import org.slf4j.Logger;
  38 +import org.slf4j.LoggerFactory;
36 import org.springframework.stereotype.Service; 39 import org.springframework.stereotype.Service;
37 import org.springframework.transaction.annotation.Transactional; 40 import org.springframework.transaction.annotation.Transactional;
38 41
@@ -43,6 +46,8 @@ import java.util.concurrent.TimeUnit; @@ -43,6 +46,8 @@ import java.util.concurrent.TimeUnit;
43 @Service("cashierPaymentService") 46 @Service("cashierPaymentService")
44 public class CashierPaymentServiceImpl implements ICashierPaymentService { 47 public class CashierPaymentServiceImpl implements ICashierPaymentService {
45 48
  49 + private static final Logger LOG = LoggerFactory.getLogger(CashierPaymentServiceImpl.class);
  50 +
46 @Resource 51 @Resource
47 private ITradeOrderDao tradeOrderDao; 52 private ITradeOrderDao tradeOrderDao;
48 53
@@ -56,6 +61,9 @@ public class CashierPaymentServiceImpl implements ICashierPaymentService { @@ -56,6 +61,9 @@ public class CashierPaymentServiceImpl implements ICashierPaymentService {
56 private IPaymentPipelineManager paymentPipelineManager; 61 private IPaymentPipelineManager paymentPipelineManager;
57 62
58 @Resource 63 @Resource
  64 + private TaskMessageSender taskMessageSender;
  65 +
  66 + @Resource
59 private PaymentResultManager paymentResultManager; 67 private PaymentResultManager paymentResultManager;
60 68
61 @Resource 69 @Resource
@@ -86,7 +94,9 @@ public class CashierPaymentServiceImpl implements ICashierPaymentService { @@ -86,7 +94,9 @@ public class CashierPaymentServiceImpl implements ICashierPaymentService {
86 tradeOrderDao.insertTradeOrder(tradeOrder); 94 tradeOrderDao.insertTradeOrder(tradeOrder);
87 95
88 // TODO: userId是否需要存储 96 // TODO: userId是否需要存储
89 - // TODO: 如果不打开收银台支付,定时关闭订单 97 + // 兜底处理交易订单,根据支付结果选择关闭或完成交易订单
  98 + TaskMessage message = new TaskMessage(TaskMessage.TYPE_CASHIER_ORDER_SCAN, tradeId, "1");
  99 + taskMessageSender.sendDelayTaskMessage(message, timeout);
90 return tradeId; 100 return tradeId;
91 } 101 }
92 102
@@ -99,77 +109,76 @@ public class CashierPaymentServiceImpl implements ICashierPaymentService { @@ -99,77 +109,76 @@ public class CashierPaymentServiceImpl implements ICashierPaymentService {
99 @Override 109 @Override
100 @Transactional(rollbackFor = Exception.class) 110 @Transactional(rollbackFor = Exception.class)
101 public OnlinePaymentStatus doPayment(CashierPayment cashierPayment) { 111 public OnlinePaymentStatus doPayment(CashierPayment cashierPayment) {
102 - // TODO: 防重复提交 112 + // TODO: 接口防重复提交
103 String lockKey = String.format(Constants.TRADE_LOCK_REDIS_KEY, cashierPayment.getTradeId()); 113 String lockKey = String.format(Constants.TRADE_LOCK_REDIS_KEY, cashierPayment.getTradeId());
104 RLock lock = redissonClient.getLock(lockKey); 114 RLock lock = redissonClient.getLock(lockKey);
105 try { 115 try {
106 boolean locked = lock.tryLock(Constants.TRADE_LOCK_TIMEOUT_SECONDS, TimeUnit.SECONDS); 116 boolean locked = lock.tryLock(Constants.TRADE_LOCK_TIMEOUT_SECONDS, TimeUnit.SECONDS);
107 if (locked) { 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 LocalDateTime now = LocalDateTime.now(); 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 String paymentId = snowflakeKeyManager.getKeyGenerator(SnowflakeKey.PAYMENT_ID).nextId(); 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 if (PaymentState.isFinished(response.getState().getCode())) { 157 if (PaymentState.isFinished(response.getState().getCode())) {
129 // 园区卡支付通道outMchId为市场ID 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 .type(TradeType.TRADE).paymentId(paymentId).channelId(pipeline.supportedChannel()) 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 .finishTime(response.getWhen()).outTradeNo(response.getOutTradeNo()) 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 .description(response.getMessage()).version(0).createdTime(now).modifiedTime(now).build(); 166 .description(response.getMessage()).version(0).createdTime(now).modifiedTime(now).build();
138 onlinePaymentDao.insertOnlinePayment(payment); 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 } else { 182 } else {
174 // 目前只有两类支付通道: CardPipeline和OnlinePipeline, 程序逻辑不应该到达此代码块 183 // 目前只有两类支付通道: CardPipeline和OnlinePipeline, 程序逻辑不应该到达此代码块
175 throw new TradePaymentException(ErrorCode.OPERATION_NOT_ALLOWED, "不支持的支付通道类型"); 184 throw new TradePaymentException(ErrorCode.OPERATION_NOT_ALLOWED, "不支持的支付通道类型");
@@ -187,4 +196,240 @@ public class CashierPaymentServiceImpl implements ICashierPaymentService { @@ -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,7 +15,7 @@ import com.diligrp.cashier.trade.exception.TradePaymentException;
15 import com.diligrp.cashier.trade.model.OnlinePayment; 15 import com.diligrp.cashier.trade.model.OnlinePayment;
16 import com.diligrp.cashier.trade.model.TradeOrder; 16 import com.diligrp.cashier.trade.model.TradeOrder;
17 import com.diligrp.cashier.trade.service.ITradeAssistantService; 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 import jakarta.annotation.Resource; 19 import jakarta.annotation.Resource;
20 import org.slf4j.Logger; 20 import org.slf4j.Logger;
21 import org.slf4j.LoggerFactory; 21 import org.slf4j.LoggerFactory;
@@ -44,7 +44,7 @@ public class TradeAssistantServiceImpl implements ITradeAssistantService { @@ -44,7 +44,7 @@ public class TradeAssistantServiceImpl implements ITradeAssistantService {
44 @Override 44 @Override
45 public TradeOrder findByTradeId(String tradeId) { 45 public TradeOrder findByTradeId(String tradeId) {
46 Optional<TradeOrder> tradeOpt = tradeOrderDao.findByTradeId(tradeId); 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 @Override 50 @Override
@@ -58,46 +58,41 @@ public class TradeAssistantServiceImpl implements ITradeAssistantService { @@ -58,46 +58,41 @@ public class TradeAssistantServiceImpl implements ITradeAssistantService {
58 * 关闭交易订单下状态为支付中的支付订单, 园区卡支付不存在支付中的订单 58 * 关闭交易订单下状态为支付中的支付订单, 园区卡支付不存在支付中的订单
59 * 59 *
60 * 独立数据库事务, 避免远程支付通道关单后, 支付订单状态仍然为支付中 60 * 独立数据库事务, 避免远程支付通道关单后, 支付订单状态仍然为支付中
61 - * 任何一笔关闭订单失败都应该返回FALSE,且一笔失败不影响其他能正常的关单操作  
62 */ 61 */
63 @Override 62 @Override
64 @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) 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 if (pipeline instanceof OnlinePipeline<?> onlinePipeline) { 70 if (pipeline instanceof OnlinePipeline<?> onlinePipeline) {
78 LocalDateTime now = LocalDateTime.now(); 71 LocalDateTime now = LocalDateTime.now();
79 - PaymentStateDTO paymentStateDTO = PaymentStateDTO.builder().paymentId(onlinePayment.getPaymentId()) 72 + PaymentStateDTO paymentStateDTO = PaymentStateDTO.builder().paymentId(payment.getPaymentId())
80 .finishTime(now).state(PaymentState.FAILED).description("主动关闭支付中的订单") 73 .finishTime(now).state(PaymentState.FAILED).description("主动关闭支付中的订单")
81 - .version(onlinePayment.getVersion()).modifiedTime(now).build(); 74 + .version(payment.getVersion()).modifiedTime(now).build();
82 proceedOnlinePayment(paymentStateDTO); 75 proceedOnlinePayment(paymentStateDTO);
83 - OnlinePrepayOrder prepayOrder = new OnlinePrepayOrder(onlinePayment.getPaymentId(), onlinePayment.getOutTradeNo()); 76 + OnlinePrepayOrder prepayOrder = new OnlinePrepayOrder(payment.getPaymentId(), payment.getOutTradeNo());
84 // 微信服务商模式下, outMchId为签约子商户; 园区卡支付时, outMchId为市场ID 77 // 微信服务商模式下, outMchId为签约子商户; 园区卡支付时, outMchId为市场ID
85 - prepayOrder.attach(Constants.PARAM_MCH_ID, onlinePayment.getOutMchId()); 78 + prepayOrder.attach(Constants.PARAM_MCH_ID, payment.getOutMchId());
86 onlinePipeline.closePrepayOrder(prepayOrder); 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 @Override 91 @Override
98 public OnlinePayment findByPaymentId(String paymentId) { 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 @Override 98 @Override
@@ -106,4 +101,11 @@ public class TradeAssistantServiceImpl implements ITradeAssistantService { @@ -106,4 +101,11 @@ public class TradeAssistantServiceImpl implements ITradeAssistantService {
106 throw new TradePaymentException(ErrorCode.SYSTEM_BUSY_ERROR, ErrorCode.MESSAGE_SYSTEM_BUSY); 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 package com.diligrp.cashier.trade.type; 1 package com.diligrp.cashier.trade.type;
2 2
3 -import com.diligrp.cashier.pipeline.type.PaymentState;  
4 import com.diligrp.cashier.shared.type.IEnumType; 3 import com.diligrp.cashier.shared.type.IEnumType;
5 4
6 import java.util.Arrays; 5 import java.util.Arrays;
@@ -15,8 +14,6 @@ public enum TradeState implements IEnumType { @@ -15,8 +14,6 @@ public enum TradeState implements IEnumType {
15 14
16 PENDING("待处理", 1), 15 PENDING("待处理", 1),
17 16
18 - PROCESSING("处理中", 2),  
19 -  
20 SUCCESS("交易成功", 4), 17 SUCCESS("交易成功", 4),
21 18
22 REFUND("交易退款", 5), 19 REFUND("交易退款", 5),
@@ -50,14 +47,6 @@ public enum TradeState implements IEnumType { @@ -50,14 +47,6 @@ public enum TradeState implements IEnumType {
50 return Arrays.asList(TradeState.values()); 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 public static boolean isFinished(int code) { 50 public static boolean isFinished(int code) {
62 return TradeState.SUCCESS.equalTo(code) || TradeState.FAILED.equalTo(code) || 51 return TradeState.SUCCESS.equalTo(code) || TradeState.FAILED.equalTo(code) ||
63 TradeState.REFUND.equalTo(code) || TradeState.CLOSED.equalTo(code); 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,6 +20,7 @@
20 <result column="out_trade_no" property="outTradeNo"/> 20 <result column="out_trade_no" property="outTradeNo"/>
21 <result column="out_pay_type" property="outPayType"/> 21 <result column="out_pay_type" property="outPayType"/>
22 <result column="state" property="state"/> 22 <result column="state" property="state"/>
  23 + <result column="notify_url" property="notifyUrl"/>
23 <result column="description" property="description"/> 24 <result column="description" property="description"/>
24 <result column="version" property="version"/> 25 <result column="version" property="version"/>
25 <result column="created_time" property="createdTime"/> 26 <result column="created_time" property="createdTime"/>
@@ -29,11 +30,11 @@ @@ -29,11 +30,11 @@
29 <insert id="insertOnlinePayment" parameterType="com.diligrp.cashier.trade.model.OnlinePayment"> 30 <insert id="insertOnlinePayment" parameterType="com.diligrp.cashier.trade.model.OnlinePayment">
30 INSERT INTO upay_online_payment(out_mch_id, trade_id, type, payment_id, channel_id, pay_type, pipeline_id, 31 INSERT INTO upay_online_payment(out_mch_id, trade_id, type, payment_id, channel_id, pay_type, pipeline_id,
31 goods, amount, object_id, payer_id, finish_time, out_trade_no, out_pay_type, 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 VALUES 34 VALUES
34 (#{outMchId}, #{tradeId}, #{type}, #{paymentId}, #{channelId}, #{payType}, #{pipelineId}, 35 (#{outMchId}, #{tradeId}, #{type}, #{paymentId}, #{channelId}, #{payType}, #{pipelineId},
35 #{goods}, #{amount}, #{objectId}, #{payerId}, #{finishTime}, #{outTradeNo}, #{outPayType} 36 #{goods}, #{amount}, #{objectId}, #{payerId}, #{finishTime}, #{outTradeNo}, #{outPayType}
36 - #{state}, #{description}, #{version}, #{createdTime}, #{modifiedTime}) 37 + #{state}, #{notifyUrl}, #{description}, #{version}, #{createdTime}, #{modifiedTime})
37 </insert> 38 </insert>
38 39
39 <select id="findByPaymentId" parameterType="string" resultMap="OnlinePaymentMap"> 40 <select id="findByPaymentId" parameterType="string" resultMap="OnlinePaymentMap">
@@ -67,8 +68,11 @@ @@ -67,8 +68,11 @@
67 payment_id = #{paymentId} AND version = #{version} 68 payment_id = #{paymentId} AND version = #{version}
68 </update> 69 </update>
69 70
70 - <select id="findByTradeId" resultMap="OnlinePaymentMap"> 71 + <select id="listOnlinePayments" resultMap="OnlinePaymentMap">
71 SELECT * FROM upay_online_payment WHERE trade_id = #{tradeId} 72 SELECT * FROM upay_online_payment WHERE trade_id = #{tradeId}
  73 + <if test="type != null">
  74 + AND type = #{type}
  75 + </if>
72 <if test="state != null"> 76 <if test="state != null">
73 AND state = #{state} 77 AND state = #{state}
74 </if> 78 </if>
scripts/dili-cashier.sql
@@ -26,14 +26,14 @@ CREATE TABLE `upay_trade_order` ( @@ -26,14 +26,14 @@ CREATE TABLE `upay_trade_order` (
26 `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', 26 `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
27 `mch_id` BIGINT NOT NULL COMMENT '商户ID', 27 `mch_id` BIGINT NOT NULL COMMENT '商户ID',
28 `trade_id` VARCHAR(40) NOT NULL COMMENT '交易ID', 28 `trade_id` VARCHAR(40) NOT NULL COMMENT '交易ID',
29 - `type` TINYINT UNSIGNED NOT NULL COMMENT '业务类型', -- 购买会员 29 + `type` TINYINT UNSIGNED NOT NULL COMMENT '订单类型',
30 `out_trade_no` VARCHAR(40) COMMENT '外部流水号', -- 商户流水号 30 `out_trade_no` VARCHAR(40) COMMENT '外部流水号', -- 商户流水号
31 `amount` BIGINT NOT NULL COMMENT '金额-分', 31 `amount` BIGINT NOT NULL COMMENT '金额-分',
32 `max_amount` BIGINT NOT NULL COMMENT '初始金额-分', 32 `max_amount` BIGINT NOT NULL COMMENT '初始金额-分',
33 `goods` VARCHAR(128) NOT NULL COMMENT '商品描述', 33 `goods` VARCHAR(128) NOT NULL COMMENT '商品描述',
34 `timeout` INTEGER UNSIGNED NOT NULL COMMENT '超时间隔时间-秒', 34 `timeout` INTEGER UNSIGNED NOT NULL COMMENT '超时间隔时间-秒',
35 `state` TINYINT UNSIGNED NOT NULL COMMENT '交易状态', 35 `state` TINYINT UNSIGNED NOT NULL COMMENT '交易状态',
36 - `notify_url` VARCHAR(128) COMMENT '业务回调链接', 36 + `notify_url` VARCHAR(128) COMMENT '业务回调地址',
37 `description` VARCHAR(128) COMMENT '交易备注', 37 `description` VARCHAR(128) COMMENT '交易备注',
38 `attach` VARCHAR(255) COMMENT '附加数据', 38 `attach` VARCHAR(255) COMMENT '附加数据',
39 `source` TINYINT UNSIGNED COMMENT '订单来源', 39 `source` TINYINT UNSIGNED COMMENT '订单来源',
@@ -56,7 +56,7 @@ CREATE TABLE `upay_online_payment` ( @@ -56,7 +56,7 @@ CREATE TABLE `upay_online_payment` (
56 `trade_id` VARCHAR(40) NOT NULL COMMENT '交易ID', 56 `trade_id` VARCHAR(40) NOT NULL COMMENT '交易ID',
57 `type` TINYINT UNSIGNED NOT NULL COMMENT '交易类型', 57 `type` TINYINT UNSIGNED NOT NULL COMMENT '交易类型',
58 `payment_id` VARCHAR(40) NOT NULL COMMENT '支付ID', 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 `pay_type` TINYINT UNSIGNED NOT NULL COMMENT '支付方式', -- NATIVE MINIPRO 60 `pay_type` TINYINT UNSIGNED NOT NULL COMMENT '支付方式', -- NATIVE MINIPRO
61 `pipeline_id` BIGINT NOT NULL COMMENT '通道ID', 61 `pipeline_id` BIGINT NOT NULL COMMENT '通道ID',
62 `goods` VARCHAR(128) NOT NULL COMMENT '商品描述', 62 `goods` VARCHAR(128) NOT NULL COMMENT '商品描述',
@@ -67,6 +67,7 @@ CREATE TABLE `upay_online_payment` ( @@ -67,6 +67,7 @@ CREATE TABLE `upay_online_payment` (
67 `out_trade_no` VARCHAR(40) COMMENT '通道流水号', 67 `out_trade_no` VARCHAR(40) COMMENT '通道流水号',
68 `out_pay_type` TINYINT UNSIGNED NOT NULL COMMENT '实际支付方式', -- 银行聚合支付时使用 68 `out_pay_type` TINYINT UNSIGNED NOT NULL COMMENT '实际支付方式', -- 银行聚合支付时使用
69 `state` TINYINT UNSIGNED NOT NULL COMMENT '申请状态', 69 `state` TINYINT UNSIGNED NOT NULL COMMENT '申请状态',
  70 + `notify_url` VARCHAR(128) COMMENT '业务回调地址',
70 `description` VARCHAR(256) COMMENT '交易备注', 71 `description` VARCHAR(256) COMMENT '交易备注',
71 `version` INTEGER UNSIGNED NOT NULL COMMENT '数据版本号', 72 `version` INTEGER UNSIGNED NOT NULL COMMENT '数据版本号',
72 `created_time` DATETIME COMMENT '创建时间', 73 `created_time` DATETIME COMMENT '创建时间',