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

支付模块

支付宝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();
    }

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,454评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,553评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,921评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,648评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,770评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,950评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,090评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,817评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,275评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,592评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,724评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,409评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,052评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,815评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,043评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,503评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,627评论 2 350

推荐阅读更多精彩内容