目录
配置文件
MyBatisPlusConfig
package weixinpay.pay.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@MapperScan("weixinpay.pay.mapper")
@EnableTransactionManagement//启用事务管理
public class MyBatisPlusConfig {
}
WxPayConfig
package weixinpay.pay.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.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;
import static jdk.nashorn.internal.runtime.regexp.joni.Config.log;
@Slf4j
@Configuration
@PropertySource("classpath:wxpay.properties") //读取配置文件
@ConfigurationProperties(prefix = "wxpay") //读取wxpay节点
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
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;
/**
* 获取商户私钥文件
*
* @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请求对象
*
* @param verifier
* @return
*/
@Bean(name = "wxPayClient")
public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier) {
log.info("获取httpClient");
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, privateKey)
.withValidator(new WechatPay2Validator(verifier));
// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
return httpClient;
}
}
Swagger2config
由于我用的PostMan进行调试的可能Swagger2后面写的不全
package weixinpay.pay.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
public class Swagger2config {
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(new ApiInfoBuilder().title("微信支付案例").build());
}
}
商品订单管理
OrderInfoController
package weixinpay.pay.controller;
import org.springframework.web.bind.annotation.*;
import weixinpay.pay.entity.OrderInfo;
import weixinpay.pay.enums.OrderStatus;
import weixinpay.pay.service.OrderInfoService;
import weixinpay.pay.vo.R;
import javax.annotation.Resource;
import java.util.List;
@CrossOrigin
@RestController
@RequestMapping
public class OrderInfoController {
@Resource
private OrderInfoService orderInfoService;
@GetMapping("/list")
public R list() {
List<OrderInfo> list = orderInfoService.listOrderByCreateTimeDesc();
return R.ok().data("list", list);
}
/**
* 查询本地订单状态
*/
@GetMapping("query-order-status/{orderNo}")
public R queryOrderStatus(@PathVariable String orderNo) {
String orderStatus = orderInfoService.getOrderStatus(orderNo);
if (OrderStatus.SUCCESS.getType().equals(orderStatus)) {
return R.ok().setMessage("支付成功");
}
return R.ok().setCode(101).setMessage("支付中...");
}
}
OrderInfoService
package weixinpay.pay.service;
import com.baomidou.mybatisplus.extension.service.IService;
import weixinpay.pay.entity.OrderInfo;
import weixinpay.pay.enums.OrderStatus;
import java.util.List;
public interface OrderInfoService extends IService<OrderInfo> {
OrderInfo createOrderByProductId(Long productId);
void saveCodeUrl(String orderNo ,String codeUrl);
List<OrderInfo> listOrderByCreateTimeDesc();
void updateStatueByOrderNo(String orderNo, OrderStatus orderStatus);
String getOrderStatus(String orderNo);
List<OrderInfo> getNoPayOrderByDuration(int minutes);
void updateStatusByOrderNo(String orderNo, OrderStatus refundProcessing);
OrderInfo getOrderByOrderNo(String orderNo);
}
OrderInfoServiceImpl
package weixinpay.pay.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import weixinpay.pay.entity.OrderInfo;
import weixinpay.pay.entity.Product;
import weixinpay.pay.enums.OrderStatus;
import weixinpay.pay.mapper.OrderInfoMapper;
import weixinpay.pay.mapper.ProductMapper;
import weixinpay.pay.service.OrderInfoService;
import weixinpay.pay.util.OrderNoUtils;
import javax.annotation.Resource;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
@Slf4j
@Service
public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService {
@Resource
private ProductMapper productMapper;
// @Resource
// private OrderInfoMapper orderInfoMapper;
@Override
public OrderInfo createOrderByProductId(Long productId) {
//查找已存在但未支付的订单
OrderInfo orderInfo = this.getNoPayOrderByProductId(productId);
if (orderInfo !=null ){
return orderInfo;
}
//获取商品信息
Product product = productMapper.selectById(productId);
//生成订单
orderInfo = new OrderInfo();
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> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_no",orderNo);
OrderInfo orderInfo = new OrderInfo();
orderInfo.setCodeUrl(codeUrl);
baseMapper.update(orderInfo,queryWrapper);
}
@Override
public List<OrderInfo> listOrderByCreateTimeDesc() {
QueryWrapper<OrderInfo> queryWrapper =new QueryWrapper<OrderInfo>().orderByDesc("create_time");
return baseMapper.selectList(queryWrapper);
}
/**
* 根据订单号更新订单状态
* @param orderNo
* @param orderStatus
*/
@Override
public void updateStatueByOrderNo(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 String getOrderStatus(String orderNo) {
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_no",orderNo);
OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
if (orderInfo == null){
return null;
}
return orderInfo.getOrderStatus();
}
/**
* 查询创建超过minutes分钟并且未支付的订单
* @param minutes
* @return
*/
@Override
public List<OrderInfo> getNoPayOrderByDuration(int minutes) {
Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));
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;
}
@Override
public void updateStatusByOrderNo(String orderNo, OrderStatus refundProcessing) {
}
/**
* 根据订单号获取订单
* @param orderNo
* @return
*/
@Override
public OrderInfo getOrderByOrderNo(String orderNo) {
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_no", orderNo);
OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
return orderInfo;
}
/**
* 根据商品id查询未支付订单
* 防止重复创建订单对象
* @param productId
* @return
*/
private OrderInfo getNoPayOrderByProductId(Long productId) {
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
//productId是否等于product_id
queryWrapper.eq("product_id",productId);
queryWrapper.eq("order_status",OrderStatus.NOTPAY.getType());
// queryWrapper.eq("user_id",userId);
OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
return orderInfo;
}
}
商品管理
controller层
package weixinpay.pay.controller;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import weixinpay.pay.entity.Product;
import weixinpay.pay.service.ProductService;
import weixinpay.pay.vo.R;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
@CrossOrigin
@Api(tags = "商品管理")
@RestController
@RequestMapping("/api/product")
public class ProductController {
@Resource
private ProductService productService;
@GetMapping("/test")
public R test(){
return R.ok().data("message","hello").data("now",new Date());
}
@GetMapping("/list")
public R list(){
List<Product> list =productService.list();
return R.ok().data("productList",list);
}
}
Service层
由于我们使用的MybatisPuls 它给我们预定了一下增删改查等方法so
package weixinpay.pay.service;
import com.baomidou.mybatisplus.extension.service.IService;
import weixinpay.pay.entity.Product;
public interface ProductService extends IService<Product> {
}
Impl层
package weixinpay.pay.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import weixinpay.pay.entity.Product;
import weixinpay.pay.mapper.ProductMapper;
import weixinpay.pay.service.ProductService;
@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
}
网站微信支付API
controller层
package weixinpay.pay.controller;
import com.google.gson.Gson;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import weixinpay.pay.service.WxPayService;
import weixinpay.pay.util.HttpUtils;
import weixinpay.pay.util.WechatPay2ValidatorForRequest;
import weixinpay.pay.vo.R;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Map;
@CrossOrigin//跨域
@RestController
@RequestMapping("/api/wx-pay")
@Api(tags = "网站微信支付API")
@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.nativePayV2(productId);
return R.ok().setData(map);
}
@PostMapping("/native/notify")
public String nativeNotify(HttpServletResponse response, HttpServletRequest request) throws IOException, GeneralSecurityException {
Gson gson = new Gson();
//应答对象
HashMap<String, String> map = new HashMap<>();
//处理通知参数
String body = HttpUtils.readData(request);
Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
String requestId = (String) bodyMap.get("id");
log.info("支付通知的id ===> {}", requestId);
//log.info("支付通知的完整数据 ===> {}", body);
//签名验证
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);
}
//处理订单
wxPayService.processOrder(bodyMap);
//成功应答
response.setStatus(200);
map.put("code", "SUCCESS");
map.put("massage", "成功");
return gson.toJson(map);
}
/**
* 用户取消订单
*
* @param orderNo
* @return
* @throws IOException
*/
@GetMapping("/cancel/{orderNo}")
public R cancelOrderNo(@PathVariable String orderNo) throws IOException {
log.info("订单取消");
wxPayService.cancelOrderNO(orderNo);
return R.ok().setMessage("订单取消");
}
/**
* 查询订单
*
* @param orderNo
* @return
* @throws Exception
*/
@GetMapping
public R queryOrder(@PathVariable String orderNo) throws Exception {
log.info("查询订单");
String result=wxPayService.queryOrder(orderNo);
return R.ok().setMessage("查询成功").data("result", result);
}
public R refund(@PathVariable String orderNo,@PathVariable String reason) throws IOException {
log.info("申请退款");
wxPayService.refund(orderNo,reason);
return R.ok();
}
}
Service层
package weixinpay.pay.service;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Map;
public interface WxPayService {
Map<String, Object> nativePayV2(Long productId) throws Exception;
void processOrder(Map<String, Object> bodyMap) throws GeneralSecurityException;
void cancelOrderNO(String orderNo) throws IOException;
String queryOrder(String orderNo) throws Exception;
void checkOrderStatus(String orderNo) throws Exception;
void refund(String orderNo, String reason) throws IOException;
void checkRefundStatus(String refundNo) throws IOException;
}
Impl层
package weixinpay.pay.service.impl;
import com.google.gson.Gson;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
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 org.springframework.util.StringUtils;
import weixinpay.pay.config.WxPayConfig;
import weixinpay.pay.entity.OrderInfo;
import weixinpay.pay.entity.RefundInfo;
import weixinpay.pay.enums.OrderStatus;
import weixinpay.pay.enums.wxpay.WxApiType;
import weixinpay.pay.enums.wxpay.WxNotifyType;
import weixinpay.pay.enums.wxpay.WxRefundStatus;
import weixinpay.pay.enums.wxpay.WxTradeState;
import weixinpay.pay.service.OrderInfoService;
import weixinpay.pay.service.PaymentInfoService;
import weixinpay.pay.service.RefundInfoService;
import weixinpay.pay.service.WxPayService;
import weixinpay.pay.vo.R;
import javax.annotation.Resource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {
private final ReentrantLock lock = new ReentrantLock();
@Resource
private WxPayConfig wxPayConfig;
@Resource
private CloseableHttpClient wxPayClient;
@Resource
private OrderInfoService orderInfoService;
@Resource
private PaymentInfoService paymentInfoService;
@Resource
private RefundInfoService refundsInfoService;
/**
* 创建订单,调用Native支付接口
*
* @param productId
* @return code_url和订单号
* @throws Exception
*/
@Override
public Map<String, Object> nativePayV2(Long productId) throws Exception {
log.info("调用统一下单API");
OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId);
String codeUrl = orderInfo.getCodeUrl();
if (orderInfo != null && !StringUtils.isEmpty(codeUrl)) {
log.info("订单已存在,二维码以保存");
//返回二维码
Map<String, Object> map = new HashMap<>();
map.put("codeUrl", codeUrl);
map.put("orderNo", orderInfo.getOrderNo());
return map;
}
//调用统一下单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 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("成功, 返回结果 = " + bodyAsString);
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功");
} else {
log.info("Native下单失败,响应码 = " + statusCode + ",返回结果 = " + bodyAsString);
throw new IOException("request failed");
}
//响应结果
Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
//二维码
codeUrl = resultMap.get("code_url");
//保存二维码
String orderNo = orderInfo.getOrderNo();
orderInfoService.saveCodeUrl(orderNo, codeUrl);
//返回二维码
Map<String, Object> map = new HashMap<>();
map.put("codeUrl", codeUrl);
map.put("orderNo", orderInfo.getOrderNo());
return map;
} finally {
response.close();
}
}
@Override
public void processOrder(Map<String, Object> bodyMap) throws GeneralSecurityException {
log.info("处理订单");
String plainText = decryFromResource(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.NOTPAY.getType().equals(orderStatus)) {
return;
}
//模拟通知并发
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新订单状态
orderInfoService.updateStatueByOrderNo(orderNo, OrderStatus.SUCCESS);
//记录支付日志
paymentInfoService.createPaymentInfo(plainText);
} finally {
/*主动释放锁*/
lock.unlock();
}
}
}
@Override
public void cancelOrderNO(String orderNo) throws IOException {
//关闭订单号
this.closeOrder(orderNo);
//更新订单号
orderInfoService.updateStatueByOrderNo(orderNo, OrderStatus.CANCEL);
}
/**
* 查询退款接口调用
*
* @param * @return
*/
@Override
public String queryOrder(String orderNo) throws Exception {
log.info("查询接口调用===>", orderNo);
String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo);
url = wxPayConfig.getDomain().concat(url).concat("?mchid=").concat(wxPayConfig.getMchId());
HttpGet httpGet = new HttpGet();
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("成功, 查询退款返回结果 = " + bodyAsString);
} else if (statusCode == 204) {
log.info("成功");
} else {
throw new RuntimeException("查询退款异常, 响应码 = " + statusCode + ", 查询退款返回结果 = " + bodyAsString);
}
return bodyAsString;
} finally {
response.close();
}
}
@Override
public void checkOrderStatus(String orderNo) throws Exception {
log.warn("根据订单号核实订单状态===>{}", orderNo);
//调用微信支付查单接口
String result = this.queryOrder(orderNo);
Gson gson = new Gson();
HashMap<String, String> resultMap = gson.fromJson(result, HashMap.class);
//获取微信支付端的订单状态
String tradeState = resultMap.get("trade_state");
if (WxTradeState.SUCCESS.getType().equals(tradeState)) {
log.warn("核实订单已经支付===>", orderNo);
//如果确定订单已经支付则更新本地订单状态
orderInfoService.updateStatueByOrderNo(orderNo, OrderStatus.SUCCESS);
//记录支付日志
paymentInfoService.createPaymentInfo(result);
}
if (WxTradeState.NOTPAY.getType().equals(tradeState)){
log.warn("核实订单未支付 ===> {}", orderNo);
this.cancelOrderNO(orderNo);
orderInfoService.updateStatueByOrderNo(orderNo,OrderStatus.CLOSED);
}
}
@Override
public void refund(String orderNo, String reason) throws IOException {
log.info("创造退款单记录");
RefundInfo refundsInfo = refundsInfoService.createRefundByOrderNo(orderNo,reason);
log.info("调用退款API");
//调用统一下单API
String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());
HttpPost httpPost =new HttpPost(url);
//请求body参数
Gson gson = new Gson();
Map paramsMap = new HashMap<>();
paramsMap.put("out_trade_no", orderNo);//订单编号
paramsMap.put("out_refund_no", refundsInfo.getRefundNo());//退款单编号
paramsMap.put("reason",reason);//退款原因
paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));//退款通知地址
Map amountMap = new HashMap();
amountMap.put("refund", refundsInfo.getRefund());//退款金额
amountMap.put("total", refundsInfo.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("成功, 退款返回结果 = " + bodyAsString);
} else if (statusCode == 204) {
log.info("成功");
} else {
throw new RuntimeException("退款异常, 响应码 = " + statusCode+ ", 退款返回结果 = " + bodyAsString);
}
//更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_PROCESSING);
//更新退款单
refundsInfoService.updateRefund(bodyAsString);
} catch (IOException e) {
e.printStackTrace();
} finally {
response.close();
}
}
@Override
public void checkRefundStatus(String refundNo) throws IOException {
log.warn("根据退款单号核实退款单状态 ===> {}", refundNo);
//调用查询退款单接口
String result = this.queryRefund(refundNo);
//组装json请求体字符串
Gson gson = new Gson();
Map<String, String> resultMap = gson.fromJson(result, HashMap.class);
//获取微信支付端退款状态
String status = resultMap.get("status");
String orderNo = resultMap.get("out_trade_no");
if (WxRefundStatus.SUCCESS.getType().equals(status)) {
log.warn("核实订单已退款成功 ===> {}", refundNo);
//如果确认退款成功,则更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
//更新退款单
refundsInfoService.updateRefund(result);
}
if (WxRefundStatus.ABNORMAL.getType().equals(status)) {
log.warn("核实订单退款异常 ===> {}", refundNo);
//如果确认退款成功,则更新订单状态
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL);
//更新退款单
refundsInfoService.updateRefund(result);
}
}
private String queryRefund(String refundNo) throws IOException {
log.info("查询退款接口调用 ===> {}", refundNo);
String url = String.format(WxApiType.DOMESTIC_REFUNDS_QUERY.getType(), refundNo);
url = wxPayConfig.getDomain().concat(url);
//创建远程Get 请求对象
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("成功, 查询退款返回结果 = " + bodyAsString);
} else if (statusCode == 204) {
log.info("成功");
} else {
throw new RuntimeException("查询退款异常, 响应码 = " + statusCode+ ", 查询退款返回结果 = " + bodyAsString);
}
return bodyAsString;
} finally {
response.close();
}
}
private void closeOrder(String orderNo) throws IOException {
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请求体
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("Native下单失败,响应码 = " + statusCode);
throw new IOException("request failed");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
response.close();
}
}
/**
* 对称解密
*
* @param bodyMap
* @return
*/
private String decryFromResource(Map<String, Object> bodyMap) throws GeneralSecurityException {
log.info("密文解密");
//通知数据
Map<String, String> resourceMap = (Map) bodyMap.get("resource");
//数据密文
String ciphertext = resourceMap.get("ciphertext");
//随机串
String nonce = resourceMap.get("nonce");
//附加数据
log.info("密文===>{}", ciphertext);
String associatedData = resourceMap.get("associated_data");
AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
nonce.getBytes(StandardCharsets.UTF_8),
ciphertext);
log.info("明文===>{}", plainText);
return plainText;
}
}
退款 功能编写
由于退款的service serviceImpl 写入商品订单管理了,这里就不单独写了
package weixinpay.pay.task;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import weixinpay.pay.entity.OrderInfo;
import weixinpay.pay.entity.RefundInfo;
import weixinpay.pay.service.OrderInfoService;
import weixinpay.pay.service.RefundInfoService;
import weixinpay.pay.service.WxPayService;
import javax.annotation.Resource;
import java.util.List;
@Slf4j
@Component
public class WxPayTask {
@Resource
private OrderInfoService orderInfoService;
@Resource
private WxPayService wxPayService;
@Resource
private RefundInfoService refundInfoService;
/**
* 秒 分 时 日 月 周
* 以秒为例
* *:每秒都执行
* 1-3:从第1秒开始执行,到第3秒结束执行
* 0/3:从第0秒开始,每隔3秒执行1次
* 1,2,3:在指定的第1、2、3秒执行
* ?:不指定
* 日和周不能同时制定,指定其中之一,则另一个设置为?
*/
//@Scheduled(cron = "0/3 * * * * ?")
public void task1(){
log.info("task1 被执行......");
}
/**
* 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单
*/
@Scheduled(cron = "0/30 * * * * ?")
public void orderConfirm() throws Exception {
log.info("orderConfirm 被执行......");
List<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDuration(5);
for (OrderInfo orderInfo : orderInfoList) {
String orderNo = orderInfo.getOrderNo();
log.warn("超时订单 ===> {}", orderNo);
//核实订单状态:调用微信支付查单接口
wxPayService.checkOrderStatus(orderNo);
}
}
/**
* 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未成功的退款单
*/
@Scheduled(cron = "0/30 * * * * ?")
public void refundConfirm() throws Exception {
log.info("refundConfirm 被执行......");
//找出申请退款超过5分钟并且未成功的退款单
List<RefundInfo> refundInfoList = refundInfoService.getNoRefundOrderByDuration(1);
for (RefundInfo refundInfo : refundInfoList) {
String refundNo = refundInfo.getRefundNo();
log.warn("超时未退款的退款单号 ===> {}", refundNo);
//核实订单状态:调用微信支付查询退款接口
wxPayService.checkRefundStatus(refundNo);
}
}
}
工具类
HttpClientUtils
package com.atguigu.paymentdemo.util;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.*;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* http请求客户端
*/
public class HttpClientUtils {
private String url;
private Map<String, String> param;
private int statusCode;
private String content;
private String xmlParam;
private boolean isHttps;
public boolean isHttps() {
return isHttps;
}
public void setHttps(boolean isHttps) {
this.isHttps = isHttps;
}
public String getXmlParam() {
return xmlParam;
}
public void setXmlParam(String xmlParam) {
this.xmlParam = xmlParam;
}
public HttpClientUtils(String url, Map<String, String> param) {
this.url = url;
this.param = param;
}
public HttpClientUtils(String url) {
this.url = url;
}
public void setParameter(Map<String, String> map) {
param = map;
}
public void addParameter(String key, String value) {
if (param == null)
param = new HashMap<String, String>();
param.put(key, value);
}
public void post() throws ClientProtocolException, IOException {
HttpPost http = new HttpPost(url);
setEntity(http);
execute(http);
}
public void put() throws ClientProtocolException, IOException {
HttpPut http = new HttpPut(url);
setEntity(http);
execute(http);
}
public void get() throws ClientProtocolException, IOException {
if (param != null) {
StringBuilder url = new StringBuilder(this.url);
boolean isFirst = true;
for (String key : param.keySet()) {
if (isFirst) {
url.append("?");
isFirst = false;
}else {
url.append("&");
}
url.append(key).append("=").append(param.get(key));
}
this.url = url.toString();
}
HttpGet http = new HttpGet(url);
execute(http);
}
/**
* set http post,put param
*/
private void setEntity(HttpEntityEnclosingRequestBase http) {
if (param != null) {
List<NameValuePair> nvps = new LinkedList<NameValuePair>();
for (String key : param.keySet())
nvps.add(new BasicNameValuePair(key, param.get(key))); // 参数
http.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8)); // 设置参数
}
if (xmlParam != null) {
http.setEntity(new StringEntity(xmlParam, Consts.UTF_8));
}
}
private void execute(HttpUriRequest http) throws ClientProtocolException,
IOException {
CloseableHttpClient httpClient = null;
try {
if (isHttps) {
SSLContext sslContext = new SSLContextBuilder()
.loadTrustMaterial(null, new TrustStrategy() {
// 信任所有
public boolean isTrusted(X509Certificate[] chain,
String authType)
throws CertificateException {
return true;
}
}).build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslContext);
httpClient = HttpClients.custom().setSSLSocketFactory(sslsf)
.build();
} else {
httpClient = HttpClients.createDefault();
}
CloseableHttpResponse response = httpClient.execute(http);
try {
if (response != null) {
if (response.getStatusLine() != null)
statusCode = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity();
// 响应内容
content = EntityUtils.toString(entity, Consts.UTF_8);
}
} finally {
response.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
httpClient.close();
}
}
public int getStatusCode() {
return statusCode;
}
public String getContent() throws ParseException, IOException {
return content;
}
}
HttpUtils
package com.atguigu.paymentdemo.util;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
public class HttpUtils {
/**
* 将通知参数转化为字符串
* @param request
* @return
*/
public static String readData(HttpServletRequest request) {
BufferedReader br = null;
try {
StringBuilder result = new StringBuilder();
br = request.getReader();
for (String line; (line = br.readLine()) != null; ) {
if (result.length() > 0) {
result.append("\n");
}
result.append(line);
}
return result.toString();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
OrderNoUtils
package com.atguigu.paymentdemo.util;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
/**
* 订单号工具类
*
* @author qy
* @since 1.0
*/
public class OrderNoUtils {
/**
* 获取订单编号
* @return
*/
public static String getOrderNo() {
return "ORDER_" + getNo();
}
/**
* 获取退款单编号
* @return
*/
public static String getRefundNo() {
return "REFUND_" + getNo();
}
/**
* 获取编号
* @return
*/
public static String getNo() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
String newDate = sdf.format(new Date());
String result = "";
Random random = new Random();
for (int i = 0; i < 3; i++) {
result += random.nextInt(10);
}
return newDate + result;
}
}
R
package com.atguigu.paymentdemo.vo;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.HashMap;
import java.util.Map;
@Data
@Accessors(chain = true)
public class R {
private Integer code; //响应码
private String message; //响应消息
private Map<String, Object> data = new HashMap<>();
public static R ok(){
R r = new R();
r.setCode(0);
r.setMessage("成功");
return r;
}
public static R error(){
R r = new R();
r.setCode(-1);
r.setMessage("失败");
return r;
}
public R data(String key, Object value){
this.data.put(key, value);
return this;
}
}
YMl配置文件
server:
port: 8090 #服务端口
Spring :
application:
name: Pay #应用的名字
jackson:
data-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&characterEncoding=utf-8
username: root
password: 123456
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath:weixinpay/pay/mapper/xml/*.xml
logging:
level:
root: info
wxpay.properties
# 微信支付相关参数
# 商户号
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://500c-219-143-130-12.ngrok.io
# APIv2密钥
wxpay.partnerKey: T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
总结
此项目在线支付的一系列流程,项目虽小,但内容十分丰富。麻雀虽小但五脏俱全