加密
加密分为对称性和非对称性
对称性的意思就是加密和解密的密钥是同一个
加密分组模式:将明文分组加密
非对称加密是用公钥进行加密(可公开),使用私钥进行解密
使用公钥进行加密后只能使用私钥进行解密,反过来,私钥加密后也只能用公钥进行解密
RSA加密算法:最著名的非对称加密
两者的对比
对称加密:
优点:运算速度块
缺点:密钥需要信息交换的双方共享,一旦被窃取,消息会被破解
非对称加密
优点:私钥严格保密,公钥任意分发,黑客获取公钥无法破解密文
缺点:运算速度非常慢
开发中:我们常常使用非对称传递密钥,使用对成进行加密
身份认证
使用公钥加密,私钥解密是为了安全性
使用私钥加密,公钥解密是为了做身份认证
摘要算法和数据的完整性
Z->H
任意长度的经过加密后得到固定长度的密文(摘要)
1:不可逆:只有算法,没有密钥,不能解密
2.难题友好性:想要破解,只能暴力枚举
3.发散性:只要对原文进行一点点改动,摘要就会发生剧烈变化
4.抗碰撞性:原文不同,计算后的摘要也要不同
常见摘要算法:MD5、SHA1、SHA2
解决黑客获取后对原文进行修改,并生成新的摘要:
解决办法:现将原文加密成摘要,然后发送方使用私钥将摘要加密(加密后的结果,我们称之为签名),接收方取下数字签名,用公钥进行解密,得到摘要,接受方使用一样的加密算法对原文进行加密,对比两者是否一致,如果一致,证明没有被篡改
数字证书
案例:黑客将自己的公钥给你,告诉你这是对方的公钥,然后黑客使用自己的私钥给你发送,你用黑客给你的公钥可以进行解密,但是你以为自己一直是在和对方进行沟通
问题:公钥的信任,黑客可以伪造公钥,怎么判断公钥是真实的?
数字证书:
因为你拿到对方的文件后对对数字签名进行解析,里面有CA给对方的认证,黑客无法篡改,也无法伪造
https协议 数字证书
创建项目时学习到的新东西
使用mypatis-plu时,我们会将xml文件放在mapper下
为了打包的时候将xml一起打包
需要配置pom
微信支付APIv3搭建过程
server:
port: 8090 #端口
spring:
application:
name: payment-demo #项目名称
#设置时间格式
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/payment_demo?serverTimezone=GMT%2B8&useSSL=true&characterEncoding=utf-8
username: root
password: 123456
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #日志打印
mapper-locations: classpath:com/yhd.paymentdemo/mapper/xml/*.xml #配置文件
当我们准备好所有资料后
创建一个文件写入需要的资料
# 微信支付相关参数
# 商户号
wxpay.mch-id=1558950191
# 商户API证书序列号 需要保证该序列号和本地下载的证书序列号保持一致
wxpay.mch-serial-no=34345964330B66427E0D3D28826C4993C77E631F
# 商户私钥文件
wxpay.private-key-path=apiclient_key.pem
# APIv3密钥
wxpay.api-v3-key=UDuLFDcmy5Eb6o0nTNZdu6ek4DDh4K8B
# APPID
wxpay.appid=wx74862e0dfcf69954
# 微信服务器地址
wxpay.domain=https://api.mch.weixin.qq.com
# 接收结果通知地址
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
wxpay.notify-domain=https://77ea-221-239-177-21.ngrok.io
# APIv2密钥
wxpay.partnerKey: T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
创建一个配置类用来接受需要的参数
package com.yhd.paymentdemo.config;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Configuration
@PropertySource("classpath:wxpay.properties") //读取配置文件
@ConfigurationProperties(prefix="wxpay") //读取wxpay节点
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
@Slf4j
public class WxPayConfig {
// 商户号
private String mchId;
// 商户API证书序列号
private String mchSerialNo;
// 商户私钥文件
private String privateKeyPath;
// APIv3密钥
private String apiV3Key;
// APPID
private String appid;
// 微信服务器地址
private String domain;
// 接收结果通知地址
private String notifyDomain;
// APIv2密钥
private String partnerKey;
}
测试配置文件是否被读取
package com.yhd.paymentdemo.controller;
import com.yhd.paymentdemo.config.WxPayConfig;
import com.yhd.paymentdemo.vo.R;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
*
**/
@Api(tags = "测试控制器")
@RestController
@RequestMapping("/api/test")
public class TestController {
@Resource
private WxPayConfig wxPayConfig;
@GetMapping("")
public R getWxPayConfig(){
String mchId = wxPayConfig.getMchId();
return R.ok().data("mchId",mchId);
}
}
小技巧-将自定义配置文件变为应用文件
配置完后,发现变成小叶子,而且文件中的字也变了颜色
但是发现还是不能点击跳转
所以我们需要添加依赖
<!-- 生成自定义配置的元数据信息-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
更新后发现点击会跳转到java类的属性上,方便我们的开发
开始开发微信支付
加载商户私钥
我们首先将私钥文件复制到项目中
去微信支付官网查看api,需要引入微信支付的SDK
<!-- 微信支付SDK-->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.3.0</version>
</dependency>
微信配置类中创建该方法
**
* 获取商户私钥文件
* @param fileName
* @return
*/
public PrivateKey getPrivateKey(String fileName){
try {
return PemUtil.loadPrivateKey(new FileInputStream(fileName));
} catch (FileNotFoundException e) {
throw new RuntimeException("私钥文件不存在",e);
}
}
测试
package com.yhd.paymentdemo.config;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.security.PrivateKey;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest //果果测试中用到了Bean需要加该注解
class WxPayConfigTest {
@Resource
private WxPayConfig wxPayConfig;
@Test
void getPrivateKey() {
String privateKeyPath = wxPayConfig.getPrivateKeyPath();
PrivateKey privateKey = wxPayConfig.getPrivateKey(privateKeyPath);
System.out.println(privateKey);
}
}
获取验签器和HttpClient
package com.yhd.paymentdemo.config;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.ScheduledUpdateCertificatesVerifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
@Configuration
@PropertySource("classpath:wxpay.properties") //读取配置文件
@ConfigurationProperties(prefix="wxpay") //读取wxpay节点
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
@Slf4j
public class WxPayConfig {
// 商户号
private String mchId;
// 商户API证书序列号
private String mchSerialNo;
// 商户私钥文件
private String privateKeyPath;
// APIv3密钥
private String apiV3Key;
// APPID
private String appid;
// 微信服务器地址
private String domain;
// 接收结果通知地址
private String notifyDomain;
// APIv2密钥
private String partnerKey;
/**
* 获取商户私钥文件
* @param fileName
* @return
*/
private PrivateKey getPrivateKey(String fileName){
try {
return PemUtil.loadPrivateKey(new FileInputStream(fileName));
} catch (FileNotFoundException e) {
throw new RuntimeException("私钥文件不存在",e);
}
}
/**
* 获取签名验证器
* @return
*/
@Bean
public ScheduledUpdateCertificatesVerifier getVerifier(){
log.info("获取签名验证器");
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//私钥签名对象
PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
//身份认证对象
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
// 使用定时更新的签名验证器,不需要传入证书
ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
wechatPay2Credentials,
apiV3Key.getBytes(StandardCharsets.UTF_8));
return verifier;
}
/**
* 获取http请求对象
* @return
*/
@Bean
public CloseableHttpClient getWxPayClient(){
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//使用定时更新的签名认证器,不需要传入证书
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, privateKey)
.withValidator(new WechatPay2Validator(getVerifier()));
// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
return httpClient;
}
}
因为使用json进行交流
所以引进gson
<!-- json处理器-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
简单的下单业务(未存数据库)
package com.yhd.paymentdemo.service.impl;
import com.google.gson.Gson;
import com.yhd.paymentdemo.config.WxPayConfig;
import com.yhd.paymentdemo.entity.OrderInfo;
import com.yhd.paymentdemo.enums.OrderStatus;
import com.yhd.paymentdemo.enums.wxpay.WxApiType;
import com.yhd.paymentdemo.enums.wxpay.WxNotifyType;
import com.yhd.paymentdemo.service.WxPayService;
import com.yhd.paymentdemo.util.OrderNoUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
*
**/
@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService{
@Resource
private WxPayConfig wxPayConfig;
@Resource
private CloseableHttpClient wxPayClient;
/**
* 创建订单,调用native接口
* @param productId
* @return code_url 和 订单号
*/
@Override
public Map<String, Object> nativePay(Long productId) throws Exception{
log.info("生成订单");
//生成订单
OrderInfo orderInfo = new OrderInfo();
orderInfo.setTitle("test");
orderInfo.setOrderNo(OrderNoUtils.getOrderNo());//订单号
orderInfo.setProductId(productId);
orderInfo.setTotalFee(1);//单位是分
orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());
//TODO 存入数据库
//调用统一下单api
log.info("调用统一api");
HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));
// 请求body参数
Gson gson = new Gson();
Map paramsMap = new HashMap<>();
paramsMap.put("appid",wxPayConfig.getAppid());
paramsMap.put("mchid",wxPayConfig.getMchId());
paramsMap.put("description",orderInfo.getTitle());
paramsMap.put("out_trade_no",orderInfo.getOrderNo());
paramsMap.put("notify_url",wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));
Map amountMap=new HashMap<>();
amountMap.put("total",orderInfo.getTotalFee());
amountMap.put("currency","CNY");
paramsMap.put("amount",amountMap);
//转换成json字符串
String jsonMap = gson.toJson(paramsMap);
log.info("请求参数:"+paramsMap);
StringEntity entity = new StringEntity(jsonMap,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response = wxPayClient.execute(httpPost);
HashMap<String, Object> map = new HashMap<>();
try {
String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
int statusCode = response.getStatusLine().getStatusCode();//响应状态码
if (statusCode == 200) { //处理成功
log.info("成功,返回结果 = " + bodyAsString);
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功");
} else {
log.info("失败,响应码 = " + statusCode+ ", 返回结果 = " + EntityUtils.toString(response.getEntity()));
throw new IOException("request failed");
}
//响应结果
HashMap<String,String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
//二维码
String code_url = resultMap.get("code_url");
map.put("codeUrl",code_url);
map.put("orderNo",orderInfo.getOrderNo());
return map;
} finally {
response.close();
}
}
}
下单的控制层
package com.yhd.paymentdemo.controller;
import com.google.gson.Gson;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.yhd.paymentdemo.service.WxPayService;
import com.yhd.paymentdemo.vo.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/api/wx-pay")
@Api(tags = "网站微信支付APIv3")
@Slf4j
public class WxPayController {
@Resource
private WxPayService wxPayService;
@Resource
private Verifier verifier;
/**
* Native下单
* @param productId
* @return
* @throws Exception
*/
@ApiOperation("调用统一下单API,生成支付二维码")
@PostMapping("/native/{productId}")
public R nativePay(@PathVariable Long productId) throws Exception {
log.info("发起支付请求 v3");
//返回支付二维码连接和订单号
Map<String, Object> map = wxPayService.nativePay(productId);
return R.ok().setData(map);
}
}
微信如和生成签名串
生成订单模块
之前我们是写死了数据,现在我们需要获取数据并且存单数据库中
package com.yhd.paymentdemo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yhd.paymentdemo.entity.OrderInfo;
import com.yhd.paymentdemo.entity.Product;
import com.yhd.paymentdemo.enums.OrderStatus;
import com.yhd.paymentdemo.mapper.OrderInfoMapper;
import com.yhd.paymentdemo.mapper.ProductMapper;
import com.yhd.paymentdemo.service.OrderInfoService;
import com.yhd.paymentdemo.util.OrderNoUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService {
@Resource
private ProductMapper productMapper;
/**
* 创建订单
* @param productId
* @return
*/
@Override
public OrderInfo createOrderByProductId(Long productId) {
//查找已经存在但是未支付的订单;
OrderInfo orderInfo= this.getNoPayOrderByProductId(productId);
if(orderInfo!=null){
return orderInfo;
}
//获取商品信息
Product product=productMapper.selectById(productId);
//生成订单
orderInfo=new OrderInfo();//出现错误,没创建对象直接用null赋值
orderInfo.setTitle(product.getTitle());
orderInfo.setOrderNo(OrderNoUtils.getOrderNo());//订单号
orderInfo.setProductId(productId);
orderInfo.setTotalFee(product.getPrice());//单位是分
orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType());
//存入到数据库
baseMapper.insert(orderInfo);
return orderInfo;
}
/**
* 存储订单二维码
* @param orderNo
* @param codeUrl
*/
@Override
public void saveCodeUrl(String orderNo, String codeUrl) {
QueryWrapper<OrderInfo> queryWapper = new QueryWrapper<>();
queryWapper.eq("order_no",orderNo);
OrderInfo orderInfo = new OrderInfo();
orderInfo.setCodeUrl(codeUrl);
baseMapper.update(orderInfo,queryWapper);
}
/**
* 获取已经存在但是未支付的订单
* @param productId
* @return
*/
private OrderInfo getNoPayOrderByProductId(Long productId) {
QueryWrapper<OrderInfo> orderInfoQueryWrapper = new QueryWrapper<>();
orderInfoQueryWrapper.eq("product_id",productId)
.eq("order_status",OrderStatus.NOTPAY.getType());
OrderInfo orderInfo = baseMapper.selectOne(orderInfoQueryWrapper);
return orderInfo;
}
}
查询订单模块
/**
* 查询订单列表,并倒序查询
* @return
*/
@Override
public List<OrderInfo> listOrderByCreateTimeDesc() {
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.orderByDesc("create_time");
return baseMapper.selectList(queryWrapper);
}
/**
* 订单控制器
**/
@RestController
@Slf4j
@Api(tags = "商品订单管理")
@RequestMapping("/api/order-info")
public class OrderInfoController {
@Resource
private OrderInfoService orderInfoService;
@GetMapping("/list")
public R list(){
List<OrderInfo> list=orderInfoService.listOrderByCreateTimeDesc();
return R.ok().data("list",list);
}
}
内网穿透
使用工具进行内网穿透,达到外网访问本地网络
使用工具进行内网穿透后
需要修改:本次使用工具natapp 启动指令:natapp -authtoken=上面查看到的 authtoken
支付通知(微信平台发送)
@PostMapping("/native/notify")
public String nativeNotify(HttpServletRequest request,HttpServletResponse response){
Gson gson = new Gson();
Map<String,String> map=new HashMap<>();
//处理通知参数
String body = HttpUtils.readData(request);
Map<String,Object> bodMap = gson.fromJson(body, HashMap.class);
log.info("支付通知的id==>{}",bodMap.get("id"));
log.info("支付通知的完整数据==>{}",body);
//TODO :签名的验签
//TODO :处理订单
//成功应答
response.setStatus(200);
map.put("code","SUCCESS");
map.put("message","成功");
return gson.toJson(map);
}
在之前发送给微信支付借口了包含了通知返回的地址
支付通知-验签(验签微信请求的内容)
将响应验签的类进行修改
package com.yhd.paymentdemo.util;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;
/**
* @author xy-peng
*/
public class WechatPay2ValidatorForRequest {
protected static final Logger log = LoggerFactory.getLogger(WechatPay2ValidatorForRequest.class);
/**
* 应答超时时间,单位为分钟
*/
protected static final long RESPONSE_EXPIRED_MINUTES = 5;
protected final Verifier verifier;
protected final String requestId;
protected final String body;
public WechatPay2ValidatorForRequest(Verifier verifier, String requestId, String body) {
this.verifier = verifier;
this.requestId = requestId;
this.body = body;
}
protected static IllegalArgumentException parameterError(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("parameter error: " + message);
}
protected static IllegalArgumentException verifyFail(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("signature verify fail: " + message);
}
public final boolean validate(HttpServletRequest request) throws IOException {
try {
//处理请求参数
validateParameters(request);
//构造验签名串
String message = buildMessage(request);
//平台证书签名串
String serial = request.getHeader(WECHAT_PAY_SERIAL);
//平台证书序列号
String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
//验签
if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",
serial, message, signature, requestId);
}
} catch (IllegalArgumentException e) {
log.warn(e.getMessage());
return false;
}
return true;
}
protected final void validateParameters(HttpServletRequest request) {
// NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
String header = null;
for (String headerName : headers) {
header = request.getHeader(headerName);
if (header == null) {
throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
}
}
//判断请求是否过期
String timestampStr = header;
try {
Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
// 拒绝过期请求
if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
}
} catch (DateTimeException | NumberFormatException e) {
throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
}
}
protected final String buildMessage(HttpServletRequest request) throws IOException {
String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
String nonce = request.getHeader(WECHAT_PAY_NONCE);
return timestamp + "\n"
+ nonce + "\n"
+ body + "\n";
}
protected final String getResponseBody(CloseableHttpResponse response) throws IOException {
HttpEntity entity = response.getEntity();
return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";
}
}
支付通知-报文解密
@Override
public void processOrder(Map<String, Object> bodMap) throws GeneralSecurityException {
log.info("处理订单");
String plainText=this.decryptFromResource(bodMap);
}
/**
* 对称解密
* @param bodMap
* @return
*/
private String decryptFromResource(Map<String, Object> bodMap) throws GeneralSecurityException {
log.info("密文解密");
//通知数据
Map<String,String> resourceMap=(Map<String,String>)bodMap.get("resource");
//获取数据密文
String ciphertext = resourceMap.get("ciphertext");
//获取随机串
String nonce = resourceMap.get("nonce");
//获取附加数据
String associated_data = resourceMap.get("associated_data");
log.info("密文 ===> {}",ciphertext);
AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
String plainText=aesUtil.decryptToString(associated_data.getBytes(StandardCharsets.UTF_8),nonce.getBytes(StandardCharsets.UTF_8),
ciphertext);
log.info("明文 ===> {}",plainText);
return plainText;
}
支付通知-处理订单状态&记录支付日志
@Override
public void processOrder(Map<String, Object> bodMap) throws GeneralSecurityException {
log.info("处理订单");
//解密报文
String plainText=this.decryptFromResource(bodMap);
//将明文转为map
Gson gson = new Gson();
HashMap plaintTextMap = gson.fromJson(plainText, HashMap.class);
String orderNo = (String)plaintTextMap.get("out_trade_no");
//处理重复的通知
//接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。
String orderStatus=orderInfoService.getOrderStatus(orderNo);
if(!OrderStatus.NOTPAY.getType().equals(orderStatus)){
return;
}
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.SUCCESS);
//记录支付日志
paymentInfoService.createPaymentInfo(plainText);
}
/**
* 根据订单号更新订单状态
* @param orderNo
* @param orderStatus
*/
@Override
public void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus) {
log.info("更新订单状态===>{}",orderStatus.getType());
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_no",orderNo);
OrderInfo orderInfo = new OrderInfo();
orderInfo.setOrderStatus(orderStatus.getType());
baseMapper.update(orderInfo,queryWrapper);
}
package com.yhd.paymentdemo.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.gson.Gson;
import com.yhd.paymentdemo.entity.PaymentInfo;
import com.yhd.paymentdemo.enums.PayType;
import com.yhd.paymentdemo.mapper.PaymentInfoMapper;
import com.yhd.paymentdemo.service.PaymentInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
@Slf4j
public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, PaymentInfo> implements PaymentInfoService {
@Override
public void createPaymentInfo(String plainText) {
log.info("记录支付日志");
Gson gson = new Gson();
HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
//商户订单号
String orderNo = (String) plainTextMap.get("out_trade_no");
//微信订单号
String transactionId = (String) plainTextMap.get("transaction_id");
//支付渠道,这里是native
String tradeType = (String) plainTextMap.get("trade_type");
//交易状态
String tradeState = (String) plainTextMap.get("trade_state");
//订单金额
Map<String,Object> amount=(HashMap) plainTextMap.get("amount");
//用户支付金额
Integer payerTotal=((Double)amount.get("payer_total")).intValue();
PaymentInfo paymentInfo = new PaymentInfo();
paymentInfo.setOrderNo(orderNo);
paymentInfo.setPaymentType(PayType.WXPAY.getType());
paymentInfo.setTransactionId(transactionId);
paymentInfo.setTradeType(tradeType);
paymentInfo.setTradeState(tradeType);
paymentInfo.setPayerTotal(payerTotal);
paymentInfo.setContent(plainText);
baseMapper.insert(paymentInfo);
}
}
支付通知-解决重复通知
支付通知-数据锁
@Override
public void processOrder(Map<String, Object> bodMap) throws GeneralSecurityException {
log.info("处理订单");
//解密报文
String plainText=this.decryptFromResource(bodMap);
//将明文转为map
Gson gson = new Gson();
HashMap plaintTextMap = gson.fromJson(plainText, HashMap.class);
String orderNo = (String)plaintTextMap.get("out_trade_no");
/*
在对业务数据进行状态检查和处理之前,
要采用数据锁进行并发控制,
以避免函数重入造成的数据混乱
*/
//尝试获取锁,成功则立即返回true,获取失败则立即返回false,不必一直等待锁的释放
if(lock.tryLock()){
try {
//处理重复的通知
//接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。
String orderStatus=orderInfoService.getOrderStatus(orderNo);
if(!OrderStatus.NOTPAY.getType().equals(orderStatus)){
return;
}
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.SUCCESS);
//记录支付日志
paymentInfoService.createPaymentInfo(plainText);
} finally {
//主动释放锁
lock.unlock();
}
}
取消订单
/**
*用户取消订单
* @param orderNo
*/
@Override
public void cancelOrder(String orderNo) throws Exception {
//调用微信支付的关单接口
this.closeOrder(orderNo);
//更新商户端的订单状态
orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.CANCEL);
}
/**
* 调用关闭订单接口
* @param orderNo
*/
private void closeOrder(String orderNo) throws Exception {
log.info("关单接口的调用,订单号===>{}",orderNo);
//创建远程请求的对象
String url=String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(),orderNo);
url=wxPayConfig.getDomain().concat(url);
HttpPost httpPost = new HttpPost(url);
//组装json请i躯体
Gson gson = new Gson();
HashMap<String, String> paramsMap = new HashMap<>();
paramsMap.put("mchid",wxPayConfig.getMchId());
String jsonParams=gson.toJson(paramsMap);
log.info("请求参数===>{}",jsonParams);
//将请求参数设置到请求对象中
StringEntity entity = new StringEntity(jsonParams,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response = wxPayClient.execute(httpPost);
try {
int statusCode = response.getStatusLine().getStatusCode();//响应状态码
if (statusCode == 200) { //处理成功
log.info("成功200" );
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功204");
} else {
log.info("失败,响应码 = " + statusCode);
throw new IOException("request failed");
}
} finally {
response.close();
}
}
核实订单状态
该功能是在当我们未能接受到微信平台的支付成功回调通知时,我们主动查询该订单的支付状态,并对状态进行修改
启动定时任务
主启动类添加注解
@EnableScheduling
package com.yhd.paymentdemo.task;
import com.yhd.paymentdemo.entity.OrderInfo;
import com.yhd.paymentdemo.service.OrderInfoService;
import com.yhd.paymentdemo.service.WxPayService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
/**
*
**/
@Component
@Slf4j
public class WxPayTask {
@Resource
private OrderInfoService orderInfoService;
@Resource
private WxPayService wxPayService;
/**
* 秒 分 时 日 月 周
**:每秒都执行
* 1-3:从第1秒开始执行,到第3秒结束执行
* 0/3:从第0秒开始,每隔三秒执行1次
* 1,2,3:指定的第一秒,第2秒第3秒执行
* ?:不指定
* 日和周互斥不能同时指定,指定其中一个,则另一个设置为?
*
*/
@Scheduled(cron="0/3 * * * * ?")
public void task1(){
log.info("task1 被执行...");
}
/**
* 从第0秒开始每隔30秒执行一次,查询创建超过5分钟,并且未支付的订单
*/
@Scheduled(cron="0/3 * * * * ?")
public void orderConfirm() throws Exception {
log.info("orderConfirm 被执行...");
List<OrderInfo>orderInfoList= orderInfoService.getNoPayOrdeByDuration(5);
for (OrderInfo orderInfo : orderInfoList) {
String orderNo=orderInfo.getOrderNo();
log.warn("超时订单===》{}",orderNo);
//核实订单状态,调用微信支付查单接口
wxPayService.checkOrderStatus(orderNo);
}
}
}
/**
* 查询创建超过minutes分钟并且未支付的订单
* @param minutes
* @return
*/
@Override
public List<OrderInfo> getNoPayOrdeByDuration(int minutes) {
Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));
log.info("创建时间===>{}",instant);
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_status",OrderStatus.NOTPAY.getType());
queryWrapper.le("create_time",instant);
List<OrderInfo> orderInfoList = baseMapper.selectList(queryWrapper);
return orderInfoList;
}
/**
* 根据订单号查询微信支付查单接口,核实订单状态
* 如果订单已支付,则更新商户端订单状态
* 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态
* @param orderNo
*/
@Override
public void checkOrderStatus(String orderNo) throws Exception {
log.warn("根据订单号核实订单状态===》{}",orderNo);
//调用微信支付查单接口
String result = this.queryOrder(orderNo);
Gson gson = new Gson();
Map resultMap = gson.fromJson(result, HashMap.class);
//获取微信支付端的订单状态
Object tradeState = resultMap.get("trade_state");
if(WxTradeState.SUCCESS.getType().equals(tradeState)){
log.warn("核实订单已支付===>{}",orderNo);
//如果确认订单已经支付则更新本地订单状态
orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.SUCCESS);
//记录支付日志
paymentInfoService.createPaymentInfo(result);
}
if(WxTradeState.NOTPAY.getType().equals(tradeState)){
log.warn("核实订单未支付===>{}",orderNo);
//如果订单未支付,则调用关单接口
this.closeOrder(orderNo);
orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.CLOSED);
}
}
退款流程-申请退款
@ApiOperation("申请退款")
@PostMapping("/refunds/{orderNo}/{reason}")
public R refunds(@PathVariable String orderNo,@PathVariable String reason) throws Exception {
log.info("申请退款");
wxPayService.refund(orderNo,reason);
return R.ok();
}
@Transactional(rollbackFor = Exception.class)
@Override
public void refund(String orderNo, String reason) throws Exception {
log.info("创建退款单记录");
RefundInfo refundInfo=refundInfoService.createRefundByOrderNo(orderNo,reason);
log.info("调用退款API");
String url=wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());
HttpPost httpPost = new HttpPost(url);
Gson gson = new Gson();
Map paramsMap = new HashMap();
paramsMap.put("out_trade_no",orderNo);//订单编号
paramsMap.put("out_refund_no",refundInfo.getRefundNo());//退款编号
paramsMap.put("reason",reason);//退款原因
paramsMap.put("notify_url",wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));
Map amountMap = new HashMap<>();
amountMap.put("refund",refundInfo.getRefund());//退款金额
amountMap.put("total",refundInfo.getTotalFee());//原订单金额
amountMap.put("currency","CNY");//退款币种
paramsMap.put("amount",amountMap);
//参数转为json
String jsonParams = gson.toJson(paramsMap);
log.info("请求参数===>{}",jsonParams);
//将请求参数设置到请求对象中
StringEntity entity = new StringEntity(jsonParams,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response = wxPayClient.execute(httpPost);
try {
String bodyAsString=EntityUtils.toString(response.getEntity());
int statusCode = response.getStatusLine().getStatusCode();//响应状态码
if (statusCode == 200) { //处理成功
log.info("成功200" );
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功204");
} else {
log.info("退款异常,响应码 = " + statusCode+",退款返回结果 = "+bodyAsString);
throw new IOException("request failed");
}
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.REFUND_PROCESSING);
//更新退款单
refundInfoService.updateRefund(bodyAsString);
} finally {
response.close();
}
}
@Override
public RefundInfo createRefundByOrderNo(String orderNo, String reason) {
//根据订单号获取订单信息
OrderInfo orderInfo= orderInfoService.getOrderByOrderNo(orderNo);
//根据订单号生成退款订单
RefundInfo refundInfo = new RefundInfo();
refundInfo.setOrderNo(orderNo);
refundInfo.setRefundNo(OrderNoUtils.getRefundNo());//退款单编号(随机生成,可以使用uuid代替)
refundInfo.setTotalFee(orderInfo.getTotalFee());//原订单金额(分)
refundInfo.setRefund(orderInfo.getTotalFee());//退款金额(分)
refundInfo.setReason(reason);//退款原因
//保存退款订单
baseMapper.insert(refundInfo);
return refundInfo;
}
/**
* 根据订单号更新订单状态
* @param orderNo
* @param orderStatus
*/
@Override
public void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus) {
log.info("更新订单状态===>{}",orderStatus.getType());
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_no",orderNo);
OrderInfo orderInfo = new OrderInfo();
orderInfo.setOrderStatus(orderStatus.getType());
baseMapper.update(orderInfo,queryWrapper);
}
@Override
public void updateRefund(String bodyAsString) {
//将json转为map
Gson gson = new Gson();
Map<String,String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
//根据退款单编号修改退款单
QueryWrapper<RefundInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("refund_no",resultMap.get("out_refund_no"));
//设置修改的字段
RefundInfo refundInfo = new RefundInfo();
refundInfo.setRefundId(resultMap.get("refund_id"));//微信支付退款单号
//查询退款和申请退款中的返回参数
if(resultMap.get("status")!=null){
refundInfo.setRefundStatus(resultMap.get("status"));//退款状态
refundInfo.setContentReturn(bodyAsString);
}
//退款回调中的回调参数
if(resultMap.get("refund_status")!=null){
refundInfo.setRefundStatus(resultMap.get("refund_status"));//退款状态
}
refundInfo.setContentNotify(bodyAsString);//将全部响应结果存入数据库的content字段
//更新退款单
baseMapper.update(refundInfo,queryWrapper);
}
退款-查询退款
@ApiOperation("查询退款,测试用")
@GetMapping("/query-refund/{refundNo}")
public R queryRefund(@PathVariable String refundNo) throws Exception {
log.info("查询退款");
String result=wxPayService.querRefund(refundNo);
return R.ok().setMessage("查询成功").data("result",result);
}
Override
public String querRefund(String refundNo) throws Exception{
String url=String.format(WxApiType.DOMESTIC_REFUNDS_QUERY.getType(),refundNo);
url=wxPayConfig.getDomain().concat(url);
HttpGet httpGet = new HttpGet(url);
httpGet.setHeader("Accept","application/json");
//完成签名并执行请求
CloseableHttpResponse response = wxPayClient.execute(httpGet);
try {
String bodyAsString=EntityUtils.toString(response.getEntity());
int statusCode = response.getStatusLine().getStatusCode();//响应状态码
if (statusCode == 200) { //处理成功
log.info("成功200" );
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功204");
} else {
log.info("退款异常,响应码 = " + statusCode+",退款返回结果 = "+bodyAsString);
throw new IOException("request failed");
}
return bodyAsString;
} finally {
response.close();
}
}
退款-退款通知
@ApiOperation("退款通知")
@PostMapping("/refunds/notify")
public String refundsNotify(HttpServletRequest request,HttpServletResponse response){
log.info("退款通知执行");
Gson gson = new Gson();
Map<String,String> map=new HashMap<>();//应答对象
try {//快捷键CTRL+ALT+T
//处理通知参数
String body = HttpUtils.readData(request);
Map<String,Object> bodyMap = gson.fromJson(body, HashMap.class);
String requestId=(String)bodyMap.get("id");
log.info("退款通知的id==>{}",bodyMap.get("id"));
log.info("退款通知的完整数据==>{}",body);
//TODO :签名的验签
WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest = new WechatPay2ValidatorForRequest(verifier, requestId,body);
if(!wechatPay2ValidatorForRequest.validate(request)){
log.error("通知验签失败");
response.setStatus(500);
map.put("code","ERROR");
map.put("message","通知验签失败");
return gson.toJson(map);
}
log.info("通知验签成功");
//处理订单
wxPayService.processRefund(bodyMap);
//成功应答
response.setStatus(200);
map.put("code","SUCCESS");
map.put("message","成功");
return gson.toJson(map);
} catch (Exception e) {
e.printStackTrace();
response.setStatus(500);
map.put("code","ERROR");
map.put("message","失败");
return gson.toJson(map);
}
}
@Override
public void processRefund(Map<String, Object> bodyMap) throws GeneralSecurityException {
log.info("退款单");
//解密报文
String plainText=this.decryptFromResource(bodyMap);
//将明文转为map
Gson gson = new Gson();
HashMap plaintTextMap = gson.fromJson(plainText, HashMap.class);
String orderNo = (String)plaintTextMap.get("out_trade_no");
/*
在对业务数据进行状态检查和处理之前,
要采用数据锁进行并发控制,
以避免函数重入造成的数据混乱
*/
//尝试获取锁,成功则立即返回true,获取失败则立即返回false,不必一直等待锁的释放
if(lock.tryLock()){
try {
//处理重复的通知
//接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。
String orderStatus=orderInfoService.getOrderStatus(orderNo);
if(!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)){
return;
}
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.REFUND_SUCCESS);
//记录支付日志
refundInfoService.updateRefund(plainText);
} finally {
//主动释放锁
lock.unlock();
}
}
}
账单-获取账单
@ApiOperation("获取账单url:测试用")
@GetMapping("/quearybill/{billDate}/{type}")
public R quearyBill(@PathVariable String billDate,
@PathVariable String type) throws Exception {
log.info("获取账单url");
String downloadUrl=wxPayService.queryBill(billDate,type);
return R.ok().setMessage("获取账单url成功").data("downloadUrl",downloadUrl);
}
/**
* 查询账单
* @param billDate
* @param type
* @return
* @throws Exception
*/
@Override
public String queryBill(String billDate, String type)throws Exception{
log.info("申请账单接口调用 {}",billDate);
String url="";
if("tradebill".equals(type)){
url=WxApiType.TRADE_BILLS.getType();
}else if("fundflowbill".equals(type)){
url=WxApiType.FUND_FLOW_BILLS.getType();
}else{
throw new RuntimeException("不支持该账单类型");
}
url=wxPayConfig.getDomain().concat(url).concat("?bill_date=").concat(billDate);
//创建远程Get请求对象
HttpGet httpGet = new HttpGet(url);
httpGet.addHeader("Accept","application/json");
CloseableHttpResponse response = wxPayClient.execute(httpGet);
HashMap<String, Object> map = new HashMap<>();
try {
String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
int statusCode = response.getStatusLine().getStatusCode();//响应状态码
if (statusCode == 200) { //处理成功
log.info("成功,返回结果 = " + bodyAsString);
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功");
} else {
log.info("失败,响应码 = " + statusCode+ ", 返回结果 = " + EntityUtils.toString(response.getEntity()));
throw new RuntimeException("申请账单异常,响应码="+statusCode+",申请账单返回数据"+bodyAsString);
}
Gson gson = new Gson();
Map<String,String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
return resultMap.get("download_url");
} finally {
response.close();
}
}
账单-下载账单
@ApiOperation("下载账单")
@GetMapping("/downloadbill/{billDate}/{type}")
public R downloadBill(@PathVariable String billDate,
@PathVariable String type) throws Exception {
log.info("下载账单");
String result=wxPayService.downloadBill(billDate,type);
return R.ok().data("result",result);
}
/**
* 下载账单
* @param billDate
* @param type
* @return
*/
@Override
public String downloadBill(String billDate, String type) throws Exception {
log.info("下载账单");
String url = this.queryBill(billDate, type);
HttpGet httpGet = new HttpGet(url);
httpGet.setHeader("Accept","application/json");
//使用wxPayClient发送请求得到响应
CloseableHttpResponse response = wxPayNoSignClient.execute(httpGet);
try {
String bodyAsString = EntityUtils.toString(response.getEntity());
int statusCode = response.getStatusLine().getStatusCode();//响应状态码
if (statusCode == 200) { //处理成功
log.info("成功200" );
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功204");
} else {
log.info("失败,响应码 = " + statusCode);
throw new RuntimeException("下载账单异常,响应码:"+statusCode+",响应内容:"+bodyAsString);
}
return bodyAsString;
} finally {
response.close();
}
}
整套流程下来对微信支付有了一个初步的了解,还需要看更多的api
标签:info,log,微信,支付,new,import,com,String From: https://www.cnblogs.com/yhdxx/p/16955754.html