程序员

SSM框架学习日记(8)——支付模块

2018-07-29  本文已影响0人  糯米团子_大芒果

支付模块

支付宝demo

我们需要集成支付宝,会需要一些支付宝的文档和沙箱环境,一步一步看吧
我们先去蚂蚁金服开放平台下载一个当面付的demo

demo
在自己的环境下看能不能跑起来,导入到idea之后,打开demo里的zfbinfo.properties,如图所示

open_api_domain = https://openapi.alipaydev.com/gateway.do
这个是支付宝沙箱的网关

pid = 2088102176227840
这个是商户UID

appid = 2016091800542227
这个就是appid

沙箱文档中我们可以详细的看到步骤,关于RSA2密钥,我们可以下载提供给我们的工具,下载window版或者mac版,把生成的公钥和私钥放到配置文件相应的位置

private_key = MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYw....
public_key = MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB....

然后回到我们的沙箱应用,点击查看应用公钥 在弹出的弹窗里把公钥复制到应用公钥里 然后点击保存之后发现页面刷新了一下,点击查看支付宝公钥,就得到了支付宝的公钥 然后复制支付宝公钥粘贴到配置文件里的

SHA256withRsa对应支付宝公钥
alipay_public_key = MIIBIjA.....

下面的配置就默认就好了,run一下Main,可以看见如下信息

就代表这个demo跑起来了,有兴趣的话可以把返回的https://qr.alipay.com/bax05682yyt0hbpqdsex0084用二维码生成器生成一下,再用支付宝提供的安卓版沙箱支付宝扫一扫看看结果

集成到项目

从demo里把支付宝需要的jar包复制到项目WEB-INF的lib里,我们只需要复制图中选中的这四个



因为下面的那些是公用的,我们在pom.xml里配置就好了

<!-- alipay -->
    <dependency>
      <groupId>commons-codec</groupId>
      <artifactId>commons-codec</artifactId>
      <version>1.10</version>
    </dependency>
    <dependency>
      <groupId>commons-configuration</groupId>
      <artifactId>commons-configuration</artifactId>
      <version>1.10</version>
    </dependency>
    <dependency>
      <groupId>commons-lang</groupId>
      <artifactId>commons-lang</artifactId>
      <version>2.6</version>
    </dependency>
    <dependency>
      <groupId>commons-logging</groupId>
      <artifactId>commons-logging</artifactId>
      <version>1.1.1</version>
    </dependency>
    <dependency>
      <groupId>com.google.zxing</groupId>
      <artifactId>core</artifactId>
      <version>2.1</version>
    </dependency>
    <dependency>
      <groupId>com.google.code.gson</groupId>
      <artifactId>gson</artifactId>
      <version>2.3.1</version>
    </dependency>
    <dependency>
      <groupId>org.hamcrest</groupId>
      <artifactId>hamcrest-core</artifactId>
      <version>1.3</version>
    </dependency>

还有一点就是需要在pom里增加如下的配置,因为在部署到服务器的时候,需要将lib下那四个文件一并打包上去,如果没有配置到时候部署到服务器的时候就有一堆报错

<build>
    <finalName>mmall</finalName>
    <plugins>
      <!-- geelynote maven的核心插件之-complier插件默认只支持编译Java 1.4,因此需要加上支持高版本jre的配置,在pom.xml里面加上 增加编译插件 -->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.7</source>
          <target>1.7</target>
          <encoding>UTF-8</encoding>
          <compilerArguments>
            <extdirs>${project.basedir}/src/main/webapp/WEB-INF/lib</extdirs>
          </compilerArguments>
        </configuration>
      </plugin>
    </plugins>
  </build>

支付

然后照旧新建controller,service,支付和订单紧密联系,所以放在同一个controller下,要通过request拿到上下文拿到upload的路径放二维码

@Controller
@RequestMapping("/order/")
public class OrderController {
    private static  final Logger logger = LoggerFactory.getLogger(OrderController.class);

    @Autowired
    private IOrderService iOrderService;

    @RequestMapping("pay.do")
    @ResponseBody
    public ServerResponse pay(HttpSession session, Long orderNo, HttpServletRequest request){
        User user = (User)session.getAttribute(Const.CURRENT_USER);
        if(user ==null){
            return ServerResponse.createByErrorCodeMessage(ResponseCode.NEED_LOGIN.getCode(),ResponseCode.NEED_LOGIN.getDesc());
        }
        String path = request.getSession().getServletContext().getRealPath("upload");
        return iOrderService.pay(orderNo,user.getId(),path);
    }
}

在service里写pay方法,首先验证一下该用户有没有这个订单,查得到的话再把订单号插到map里,然后去demo里copy生成支付二维码的方法过来

@Service("iOrderService")
public class OrderServiceImpl implements IOrderService {

    @Autowired
    private OrderMapper orderMapper;

    public ServerResponse pay(Long orderNo, Integer userId, String path){
        Map<String ,String> resultMap = Maps.newHashMap();
        Order order = orderMapper.selectByUserIdAndOrderNo(userId,orderNo);
        if(order == null){
            return ServerResponse.createByErrorMessage("用户没有该订单");
        }
        resultMap.put("orderNo",String.valueOf(order.getOrderNo()));
        ....
    }
}
找到demo里的生成支付二维码方法,把所有的属性都copy到pay方法下,再一个一个改
  1. 第一个订单号,改成我们商城的订单号

String outTradeNo = order.getOrderNo().toString();

  1. 第二个订单标题,我们自己拼装一个

String subject = new StringBuilder().append("kamisama 扫码支付,订单号:").append(outTradeNo).toString();

  1. 第三个订单总价钱,从订单里拿

String totalAmount = order.getPayment().toString();

  1. 不可打折金额和Id不改

String undiscountableAmount = "0";
String sellerId = "";

  1. 订单描述自己拼装,下面的直到商品明细列表之前的不改

// 订单描述,可以对交易或商品进行一个详细地描述,比如填写"购买商品2件共15.00元"
String body = new StringBuilder().append("订单").append(outTradeNo).append("购买商品共").append(totalAmount).append("元").toString();
// 商户操作员编号,添加此参数可以为商户操作员做销售统计
String operatorId = "test_operator_id";
// (必填) 商户门店编号,通过门店号和商家后台可以配置精准到门店的折扣信息,详询支付宝技术支持
String storeId = "test_store_id";
// 业务扩展参数,目前可添加由支付宝分配的系统商编号(通过setSysServiceProviderId方法),详情请咨询支付宝技术支持
ExtendParams extendParams = new ExtendParams();
extendParams.setSysServiceProviderId("2088100200300400500");
// 支付超时,定义为120分钟
String timeoutExpress = "120m";

  1. 商品详细列表,用for循环把商品详情用支付宝的GoodsDetail.newInstance(...)一个个添加到支付宝的集合中去

// 商品明细列表,需填写购买商品详细信息,
List<GoodsDetail> goodsDetailList = new ArrayList<GoodsDetail>();

// 商品明细列表,需填写购买商品详细信息,
List<GoodsDetail> goodsDetailList = new ArrayList<GoodsDetail>();

List<OrderItem> orderItemList = orderItemMapper.getByOrderNoUserId(orderNo,userId);
然后在orderItemMapper里新增一个新的查询,selectByOrderNoUserId,通过订单号和用户id查询出相应的orderItem,然后for循环去拼装每一个goods,从demo里或者到newInstance方法里可以看到需要的参数分别是商品id,商品名称,价格(单位为分),和数量

所以在for循环里拼装好goods添加到list里,价钱单位转换为分的时候用到了乘法所以要用我们之前写好的BigDecimalUtil里的mul()方法

for(OrderItem orderItem : orderItemList){
    // 创建一个商品信息,参数含义分别为商品id(使用国标)、名称、单价(单位为分)、数量,如果需要添加商品类别,详见GoodsDetail
     GoodsDetail goods = GoodsDetail.newInstance(orderItem.getProductId().toString(), orderItem.getProductName(),
     BigDecimalUtil.mul(orderItem.getCurrentUnitPrice().doubleValue(),new Double(100).doubleValue()).longValue(),
                    orderItem.getQuantity());
     goodsDetailList.add(goods);
}
  1. 创建扫码支付请求builder,设置请求参数

AlipayTradePrecreateRequestBuilder builder = new AlipayTradePrecreateRequestBuilder()
.setSubject(subject).setTotalAmount(totalAmount).setOutTradeNo(outTradeNo)
.setUndiscountableAmount(undiscountableAmount).setSellerId(sellerId).setBody(body)
.setOperatorId(operatorId).setStoreId(storeId).setExtendParams(extendParams)
.setTimeoutExpress(timeoutExpress)
.setNotifyUrl(PropertiesUtil.getProperty("alipay.callback.url"))
.setGoodsDetailList(goodsDetailList);

参数都是上面配置的那些,这里的setNotifyUrl()是设置支付宝回调地址,需要在沙箱里配置,然后把地址写在mmall.properties里,读取时用PropertiesUtil读取就可以了

mmall.properties
这里有个点要注意的,支付宝的回调地址可以是域名也可以是ip地址,所以如果没有服务器的话,就用花生壳等外网穿透软件,如果有服务器的话并且有域名的话,那就用域名好了,如果没有域名那就把服务器tomcat监听的端口改为80端口,因为支付宝的回调地址不允许ip+端口的形式,只有ip的话就这么处理
  1. 现在代码里tradeService,报错,那么在demo里,找到tradeService,可以看到是声明了一个静态变量然后再静态块里初始化,所以我们把tradeService这部分代码copy到自己的代码里去


    demo

    所以把tradeService这部分代码copy到自己的代码里去

@Service("iOrderService")
public class OrderServiceImpl implements IOrderService {
    ...
    private static  AlipayTradeService tradeService;
    static {
        /** 一定要在创建AlipayTradeService之前调用Configs.init()设置默认参数
         *  Configs会读取classpath下的zfbinfo.properties文件配置信息,如果找不到该文件则确认该文件是否在classpath目录
         */
        Configs.init("zfbinfo.properties");
        /** 使用Configs提供的默认参数
         *  AlipayTradeService可以使用单例或者为静态成员对象,不需要反复new
         */
        tradeService = new AlipayTradeServiceImpl.ClientBuilder().build();
    }
    ...
}
  1. 然后发现代码里dumpResponse方法报错,是一个打印应答的方法,也直接从demo拿过来用了
// 简单打印应答
    private void dumpResponse(AlipayResponse response) {
        if (response != null) {
            log.info(String.format("code:%s, msg:%s", response.getCode(), response.getMsg()));
            if (StringUtils.isNotEmpty(response.getSubCode())) {
                log.info(String.format("subCode:%s, subMsg:%s", response.getSubCode(),
                    response.getSubMsg()));
            }
            log.info("body:" + response.getBody());
        }
    }

如果下单成功,那就要生成二维码。
先创建一个File,指向传过来的path,判断不存在后,然后给予写权限然后新建,

logger.info("支付宝预下单成功: )");

AlipayTradePrecreateResponse response = result.getResponse();
dumpResponse(response);

File folder = new File(path);
if(!folder.exists()){
    folder.setWritable(true);
    folder.mkdirs();
}

然后生成二维码并上传到服务器ZxingUtils.getQRCodeImge()是支付宝封装好的方法,生成二维码,上传之后把url返回回去

// 需要修改为运行机器上的路径
String qrPath = String.format(path+"/qr-%s.png",response.getOutTradeNo());
String qrFileName = String.format("qr-%s.png",response.getOutTradeNo());
ZxingUtils.getQRCodeImge(response.getQrCode(), 256, qrPath);

File targetFile = new File(path,qrFileName);
try {
    FTPUtil.uploadFile(Lists.newArrayList(targetFile));
} catch (IOException e) {
    logger.error("上传二维码异常",e);
}
logger.info("qrPath:" + qrPath);

String qrUrl = PropertiesUtil.getProperty("ftp.server.http.prefix")+targetFile.getName();
resultMap.put("qrUrl",qrUrl);
return ServerResponse.createBySuccess(resultMap);

把case里的break都换成我们自己的消息return就好啦

case FAILED:
    logger.error("支付宝预下单失败!!!");
    return ServerResponse.createByErrorMessage("支付宝预下单失败!!!");

case UNKNOWN:
    logger.error("系统异常,预下单状态未知!!!");
    return ServerResponse.createByErrorMessage("系统异常,预下单状态未知!!!");

default:
    logger.error("不支持的交易状态,交易返回异常!!!");
    return ServerResponse.createByErrorMessage("不支持的交易状态,交易返回异常!!!");

到这里pay方法就写完了,在controller里调用就好了

    @RequestMapping("pay.do")
    @ResponseBody
    public ServerResponse pay(HttpSession session, Long orderNo, HttpServletRequest request){
        User user = (User)session.getAttribute(Const.CURRENT_USER);
        if(user ==null){
            return ServerResponse.createByErrorCodeMessage(ResponseCode.NEED_LOGIN.getCode(),ResponseCode.NEED_LOGIN.getDesc());
        }
        String path = request.getSession().getServletContext().getRealPath("upload");
        return iOrderService.pay(orderNo,user.getId(),path);
    }

回调方法

controller,根据alipay的要求的返回来返回,所以返回一个Object,参数只有request,应为支付宝回调把数据放在request里,取出来放map里就好了,用一个迭代器遍历一下,取出key和value,然后把value数组拼接到一个字符串,用逗号分割就变成了,value1,value2,value3 这种形式,然后把key和拼接好的value字符串放到另一个map里

    @RequestMapping("alipay_callback.do")
    @ResponseBody
    public Object alipayCallback(HttpServletRequest request){
        Map<String,String> params = Maps.newHashMap();

        Map requestParams = request.getParameterMap();
        for(Iterator iter = requestParams.keySet().iterator(); iter.hasNext();){
            String name = (String)iter.next();
            String[] values = (String[]) requestParams.get(name);
            String valueStr = "";
            for(int i = 0 ; i <values.length;i++){
                valueStr = (i == values.length -1)?valueStr + values[i]:valueStr + values[i]+",";
            }
            params.put(name,valueStr);
        }
        logger.info("支付宝回调,sign:{},trade_status:{},参数:{}",params.get("sign"),params.get("trade_status"),params.toString());
    }

接下来呢非常重要,非常重要,要验证回调的正确性,是不是支付宝发的,并且呢还要避免重复通知
看看支付宝的文档怎么说

支付宝文档
那么按步骤做就行了,有一些部分sdk已经做了,去看看源码就知道了 依赖jar包里验证的部分源码 RSA2check
可以看到源码里并没有去掉sign_type,所以只能手动remove掉。check里第一个参数传我们自己组装的map,然后是支付宝公钥,然后是字符集,最后是sign_type(配置文件里有)
    @RequestMapping("alipay_callback.do")
    @ResponseBody
    public Object alipayCallback(HttpServletRequest request){
        ....
        //非常重要,验证回调的正确性,是不是支付宝发的.并且呢还要避免重复通知.
        params.remove("sign_type");
        try {
            boolean alipayRSACheckedV2 = AlipaySignature.rsaCheckV2(params, Configs.getAlipayPublicKey(),"utf-8",Configs.getSignType());
            if(!alipayRSACheckedV2){
                return ServerResponse.createByErrorMessage("非法请求,验证不通过");
            }
        } catch (AlipayApiException e) {
            logger.error("支付宝验证回调异常",e);
        }
        //todo 验证各种数据

        //
}

验证通过之后还得验证各种数据,就先放个todo之后再做吧



所有都验证完了之后,就要有一些订单状态库存之类的处理了,再service里新增方法,先判断订单号是否有效,然后判断订单状态,如果是交易成功就把订单状态置成已付款。然后组装payinfo

    public ServerResponse aliCallback(Map<String,String> params){
        Long orderNo = Long.parseLong(params.get("out_trade_no"));
        String tradeNo = params.get("trade_no");
        String tradeStatus = params.get("trade_status");
        Order order = orderMapper.selectByOrderNo(orderNo);
        if(order == null){
            return ServerResponse.createByErrorMessage("非商城的订单,回调忽略");
        }
        if(order.getStatus() >= Const.OrderStatusEnum.PAID.getCode()){
            return ServerResponse.createBySuccess("支付宝重复调用");
        }
        if(Const.AlipayCallback.TRADE_STATUS_TRADE_SUCCESS.equals(tradeStatus)){
            order.setPaymentTime(DateTimeUtil.strToDate(params.get("gmt_payment")));
            order.setStatus(Const.OrderStatusEnum.PAID.getCode());
            orderMapper.updateByPrimaryKeySelective(order);
        }

        PayInfo payInfo = new PayInfo();
        payInfo.setUserId(order.getUserId());
        payInfo.setOrderNo(order.getOrderNo());
        payInfo.setPayPlatform(Const.PayPlatformEnum.ALIPAY.getCode());
        payInfo.setPlatformNumber(tradeNo);
        payInfo.setPlatformStatus(tradeStatus);

        payInfoMapper.insert(payInfo);

        return ServerResponse.createBySuccess();
    }

controller调用一下

ServerResponse serverResponse = iOrderService.aliCallback(params);
if(serverResponse.isSuccess()){
    return Const.AlipayCallback.RESPONSE_SUCCESS;
}
return Const.AlipayCallback.RESPONSE_FAILED;

返回成功,这样回调就做完了

订单状态接口

用户扫完二维码付款之后,要查一下是不是付款成功了,在controller新增

    @RequestMapping("query_order_pay_status.do")
    @ResponseBody
    public ServerResponse<Boolean> queryOrderPayStatus(HttpSession session, Long orderNo){
        User user = (User)session.getAttribute(Const.CURRENT_USER);
        if(user ==null){
            return ServerResponse.createByErrorCodeMessage(ResponseCode.NEED_LOGIN.getCode(),ResponseCode.NEED_LOGIN.getDesc());
        }
        ServerResponse serverResponse = iOrderService.queryOrderPayStatus(user.getId(),orderNo);
        if(serverResponse.isSuccess()){
            return ServerResponse.createBySuccess(true);
        }
        return ServerResponse.createBySuccess(false);
    }

然后在service里新增一下代码,让controller调用就行了

    public ServerResponse queryOrderPayStatus(Integer userId,Long orderNo){
        Order order = orderMapper.selectByUserIdAndOrderNo(userId,orderNo);
        if(order == null){
            return ServerResponse.createByErrorMessage("用户没有该订单");
        }
        if(order.getStatus() >= Const.OrderStatusEnum.PAID.getCode()){
            return ServerResponse.createBySuccess();
        }
        return ServerResponse.createByError();
    }

这样与支付宝对接的三个接口就都写完了

上一篇下一篇

猜你喜欢

热点阅读