模式一和模式二提供了两种不同的能力,适用于不同的场景,看商户具体的需求。
两种模式,在支付的流程中,有一定的共同的流程:
1,生成订单。
2,用户支付。
差别在于:
模式一,先扫码,再生成订单。
模式二,先生成订单,再扫码。
而 生成订单,代表着 本次支付给商户的金额是否是已经确定了。
在模式一中,用户扫描的二维码,此时可以还没有确定实际要支付的金额。
在模式二中,用户扫描的二维码,金额已经是确定的。
可以这么理解,模式一中的二维码,是商品的二维码。
模式二中的二维码,是 订单的二维码,也因为这个是订单的二维码,所以必须要有时效性。
那么这两个场景的玩法,可以有一个明显的差别,
模式一,更适合无人职守的自动售卖机。所有的商品都有一个固定的二维码,价格相对稳定,当用户使用微信支付扫描了二维码,微信再请求自动售卖机的服务提供商的 后台接口,注意,这个请求中,是包含了商品ID以及用户信息的,这样,商户系统就可以根据 商品ID,以及用户的身份,再来确定用户实际要支付的金额。
模式二,更适合有人职守的,支付金额非常不确定的场合。比如,你去饭馆吃饭,虽然每个菜的金额是固定的,但一桌子饭菜的金额不固定,甚至是你还可能使用饭馆事先发放的代金券。这个时候,就需要收银员,预先创建一个订单,确定好金额,然后你再来扫描这个二维码来支付
该模块采用到的技术栈包括 spring boot 、spring jpa 、gradle、spock、springCloud contracts......包括产品,订单,折扣以及支付
首先创建产品表
CREATE TABLE `app_product` ( `id` VARCHAR(32) PRIMARY KEY, `name` VARCHAR(48) NOT NULL, `price` DOUBLE NOT NULL, `type` VARCHAR(24) NOT NULL )
建立对应Entity
@Entity @Table(name = "app_product") @Getter @Setter public class AppProduct { @Id private String id; private String name; private double price; @Enumerated(EnumType.STRING) private AppProductType type; }
建立对应的返回值DTO
@Getter @Setter public class AppProductDTO { private String id; private double price; private String name; private AppProductType type; private List<AppProductDiscountDTO> appProductDiscountDTOs; }
枚举产品类型 public enum AppProductType { VIP, }
@Repository public interface AppProductRepository extends JpaRepository<AppProduct, String> { List<AppProduct> findAllByType(AppProductType type); }
后面才发现的Collector和stream写法,函数式编程代码更简化,因为有checkstyle的缘故,详情请看JAVA8文档,所以有些超长的代码换到了下一行 public class AppProductService { private final AppProductRepository appProductRepository; private final OrderMapper orderMapper; private final AppProductDiscountRepository appProductDiscountRepository; @Autowired public AppProductService(AppProductRepository appProductRepository, OrderMapper orderMapper, AppProductDiscountRepository appProductDiscountRepository) { this.appProductRepository = appProductRepository; this.orderMapper = orderMapper; this.appProductDiscountRepository = appProductDiscountRepository; } public List<AppProductDTO> getVipProducts() { List<AppProduct> vipProducts = appProductRepository.findAllByType(AppProductType.VIP); if (vipProducts == null || vipProducts.size() == 0) { throw new UnprocessableEntityException("The vip products is empty "); } Map<String, List<AppProductDiscount>> productDiscounts = getAppProductDiscounts(vipProducts); return toAppProductDTOs(vipProducts, productDiscounts); } private List<AppProductDTO> toAppProductDTOs(List<AppProduct> vipProducts, Map<String, List<AppProductDiscount>> productDiscounts) { return orderMapper.mapList(vipProducts, AppProductDTO.class) .stream() .peek(item -> { List<AppProductDiscountDTO> discounts = orderMapper.mapList(productDiscounts.get(item.getId()), AppProductDiscountDTO.class); item.setAppProductDiscountDTOs(discounts); }).collect(Collectors.toList()); } private Map<String, List<AppProductDiscount>> getAppProductDiscounts( List<AppProduct> appProducts) { List<String> productIds = appProducts .stream() .map(AppProduct::getId) .collect(Collectors.toList()); Date now = new Date(); return appProductDiscountRepository .findAllByAppProductIdInAndIsDisabledIsFalseAndStartTimeBeforeAndEndTimeAfter( productIds, now, now) .stream() .collect(Collectors.groupingBy(AppProductDiscount::getAppProductId)); }
public class AppProductController { private final AppProductService appProductService; @Autowired public AppProductController(AppProductService appProductService) { this.appProductService = appProductService; } @GetMapping(value = "/vip") public List<AppProductDTO> getVipProducts() { return appProductService.getVipProducts(); } }
下面是折扣代码
@Entity @Table(name = "app_product_discount") @Getter @Setter public class AppProductDiscount { @Id private String id; private String appProductId; private Integer productNumber; private Double discount; private Date startTime; private Date endTime; private boolean isDisabled; private Date createdTime; }
public interface AppProductDiscountRepository extends JpaRepository<AppProductDiscount, String> { List<AppProductDiscount> findAllByAppProductIdInAndIsDisabledIsFalseAndStartTimeBeforeAndEndTimeAfter( List<String> appProductId, Date startTime, Date endTime); AppProductDiscount findByAppProductIdAndProductNumberAndIsDisabledIsFalse( String appProductId, int productNumber); }
订单代码
public enum OrderStatus { WAITING_PAYMENT,PAID, }
public class Order { @Id private String id; private Long userId; private double paymentPrice; private double totalPrice; @Enumerated(EnumType.STRING) private OrderStatus status; @Enumerated(EnumType.STRING) private PaymentWayType paymentWay; private Date createdTime; private Date updatedTime; }
@Service @Transactional public class OrderService {
private final OrderRepository orderRepository; private final AppProductRepository appProductRepository; private final OrderMapper orderMapper; private final AuthenticationFacade authenticationFacade; private final AppProductDiscountRepository appProductDiscountRepository; private final OrderAppProductRepository orderAppProductRepository; @Autowired public OrderService(OrderRepository orderRepository, AppProductRepository appProductRepository, OrderMapper orderMapper, AuthenticationFacade authenticationFacade, AppProductDiscountRepository appProductDiscountRepository, OrderAppProductRepository orderAppProductRepository) { this.orderRepository = orderRepository; this.appProductRepository = appProductRepository; this.orderMapper = orderMapper; this.authenticationFacade = authenticationFacade; this.appProductDiscountRepository = appProductDiscountRepository; this.orderAppProductRepository = orderAppProductRepository; } @Transactional public OrderDTO createOrder(OrderCommand command) { String appProductId = command.getAppProductId(); int appProductNumber = command.getAppProductNumber(); AppProduct appProduct = appProductRepository.findOne(appProductId); if (appProduct == null) { throw new UnprocessableEntityException("appProduct is not exists"); } AppProductDiscount discount = appProductDiscountRepository .findByAppProductIdAndProductNumberAndIsDisabledIsFalse( appProductId, appProductNumber); double paymentPrice = getPaymentPrice(appProduct, discount, appProductNumber); if (command.getPaymentPrice() != paymentPrice) { throw new UnprocessableEntityException("Invalid paymentPrice"); } Order order = saveOrder(paymentPrice, appProduct, appProductNumber); saveOrderAppProduct(order, appProduct, discount, appProductNumber); return orderMapper.map(order, OrderDTO.class); } private Order saveOrder(double paymentPrice, AppProduct appProduct, int appProductNumber) { Order order = new Order(); order.setId(IdUtil.createUuid()); order.setUserId(authenticationFacade.getUserId()); order.setPaymentPrice(paymentPrice); order.setTotalPrice(appProduct.getPrice() * appProductNumber); order.setStatus(OrderStatus.WAITING_PAYMENT); order.setCreatedTime(new Date()); order.setUpdatedTime(new Date()); return orderRepository.save(order); } private void saveOrderAppProduct(Order order, AppProduct appProduct, AppProductDiscount discount, int appProductNumber) { OrderAppProduct orderAppProduct = new OrderAppProduct(); orderAppProduct.setId(IdUtil.createUuid()); orderAppProduct.setAppProductId(appProduct.getId()); orderAppProduct.setAppProductName(appProduct.getName()); orderAppProduct.setAppProductPrice(appProduct.getPrice()); orderAppProduct.setOrderId(order.getId()); orderAppProduct.setAppProductNumber(appProductNumber); if (discount != null) { orderAppProduct.setAppProductDiscountId(discount.getId()); orderAppProduct.setAppProductDiscountDistcount(discount.getDiscount()); } orderAppProductRepository.save(orderAppProduct); } private double getPaymentPrice(AppProduct appProduct, AppProductDiscount discount, int productNumber) { double paymentPrice = appProduct.getPrice() * productNumber; if (discount != null) { paymentPrice = discount.getProductNumber() * discount.getDiscount() * appProduct.getPrice(); } BigDecimal b = new BigDecimal(paymentPrice); return b.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue(); } }
中间还有个订单产品产生的一个中间模块
对于微信支付,刚开始比较傻,还是用的老一套,自己封装xml,后来看了源码才发现好多东西微信已经封装好了,我们只需要传入参数,并按开发文档流程进行就好了,省了不少东西,也有效的减少了自己的测试。中间陷入了微信服务商号和普通商户号的误区
首先配置好一些基本参数:appId,mchId,mchKey,notifyUrl以及tradeType
@Configuration @ConditionalOnClass(WxPayService.class) public class WxPayConfiguration { @Value("${wechat.pay.appId}") private String appId; @Value("${wechat.pay.mchId}") private String mchId; @Value("${wechat.pay.mchKey}") private String mchKey; @Value("${wechat.pay.notifyUrl}") private String notifyUrl; @Value("${wechat.pay.tradeType}") private String tradeType; @Bean @ConditionalOnMissingBean public WxPayConfig payConfig() { WxPayConfig payConfig = new WxPayConfig(); payConfig.setAppId(appId); payConfig.setMchId(mchId); payConfig.setMchKey(mchKey); payConfig.setNotifyUrl(notifyUrl); payConfig.setTradeType(tradeType); return payConfig; } @Bean public WxPayService wxPayService(WxPayConfig payConfig) { WxPayService wxPayService = new WxPayServiceImpl(); wxPayService.setConfig(payConfig); return wxPayService; } }
接着就是生产二维码以及扫码过后的回调通知,比起自己拼装的xml一系类工具复杂程度简直不可比拟
public class WechatPayService implements Loggable { private final OrderRepository orderRepository; private final QrcodeUtil qrcodeUtil; @Value("${wechat.qwcode.path}") public String rootPath; @Value("${wechat.pay.spbillCreateIp}") private String spbillCreateIp; @Resource(name = "wxPayService") private WxPayService wxService; @Autowired public WechatPayService(OrderRepository orderRepository, QrcodeUtil qrcodeUtil, WxPayService wxService) { this.orderRepository = orderRepository; this.qrcodeUtil = qrcodeUtil; this.wxService = wxService; } //回调通知 public WxPayNotifyResponse wechatNotify(String xmlData) throws WxPayException { WxPayNotifyResponse response = new WxPayNotifyResponse(); WxPayOrderNotifyResult wxPayOrderNotifyResult = wxService .parseOrderNotifyResult(xmlData); WxPayOrderQueryResult confirmResult = wxService.queryOrder( null, wxPayOrderNotifyResult.getOutTradeNo()); if ("SUCCESS".equals(confirmResult.getResultCode())) { Order order = orderRepository.findOne(confirmResult.getOutTradeNo()); order.setStatus(OrderStatus.PAID); order.setPaymentWay(PaymentWayType.WX); orderRepository.save(order); response.setReturnCode("SUCCESS"); } else { response.setReturnCode("FAIL"); response.setReturnMsg("OutTradeNo is Empty"); } return response; } //生成二维码 public WechatPaymentDTO createPayment(WechatPayCommand wechatPayCommand) throws Exception { Order order = orderRepository.findOne(wechatPayCommand.getOrderId()); if (order == null) { throw new UnprocessableEntityException("order is not exists"); } WxPayUnifiedOrderRequest request = getWxPayUnifiedOrderRequest(order); WxPayUnifiedOrderResult wxPayUnifiedOrderResult; try { wxPayUnifiedOrderResult = wxService.unifiedOrder(request); } catch (WxPayException e) { error("wechat pay failed", e); throw e; } byte[] qrCodeUrl = wxService.createScanPayQrcodeMode2( wxPayUnifiedOrderResult.getCodeURL(), null, null); WechatPaymentDTO wechatPaymentDTO = new WechatPaymentDTO(); wechatPaymentDTO.setId(request.getOutTradeNo()); wechatPaymentDTO.setQrCodeUrl(qrCodeUrl); return wechatPaymentDTO; } //微信请求参数配置 private WxPayUnifiedOrderRequest getWxPayUnifiedOrderRequest(Order order) { WxPayUnifiedOrderRequest request = new WxPayUnifiedOrderRequest(); request.setOutTradeNo(IdUtil.createUuid()); request.setBody("VIP会员充值"); request.setProductId(order.getId()); request.setTotalFee((int) (order.getPaymentPrice() * 100)); request.setSpbillCreateIp(spbillCreateIp); return request; }