SSM框架学习日记(8)——支付模块
支付模块
支付宝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方法下,再一个一个改
- 第一个订单号,改成我们商城的订单号
String outTradeNo = order.getOrderNo().toString();
- 第二个订单标题,我们自己拼装一个
String subject = new StringBuilder().append("kamisama 扫码支付,订单号:").append(outTradeNo).toString();
- 第三个订单总价钱,从订单里拿
String totalAmount = order.getPayment().toString();
- 不可打折金额和Id不改
String undiscountableAmount = "0";
String sellerId = "";
- 订单描述自己拼装,下面的直到商品明细列表之前的不改
// 订单描述,可以对交易或商品进行一个详细地描述,比如填写"购买商品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";
- 商品详细列表,用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);
}
- 创建扫码支付请求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读取就可以了
这里有个点要注意的,支付宝的回调地址可以是域名也可以是ip地址,所以如果没有服务器的话,就用花生壳等外网穿透软件,如果有服务器的话并且有域名的话,那就用域名好了,如果没有域名那就把服务器tomcat监听的端口改为80端口,因为支付宝的回调地址不允许ip+端口的形式,只有ip的话就这么处理
-
现在代码里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();
}
...
}
- 然后发现代码里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();
}
这样与支付宝对接的三个接口就都写完了