说明:

1)微信支付必须有营业执照才可以申请

2)微信支付官方api是全套的,我这是抽取其中的统一下单api,做了一个简单的封装

首先看看微信支付

商户系统和微信支付系统主要交互:

1、小程序内调用登录接口,获取到用户的openid,api参见公共api【小程序登录API

2、商户server调用支付统一下单,api参见公共api【统一下单API

3、商户server调用再次签名,api参见公共api【再次签名

4、商户server接收支付通知,api参见公共api【支付结果通知API

5、商户server查询支付结果,api参见公共api【查询订单API

注意上面有两次签名  

1.配置文件类

 public final class WxConfig {
public final static String appId="wxe86f60xxxxxxx"; // 小程序appid
public final static String mchId="15365xxxxx";// 商户ID
public final static String key="Ucsdfl782167bjslNCJD129863skkqoo"; // 跟微信支付约定的密钥
public final static String notifyPath="/admin/wxnotify"; // 回调地址
public final static String payUrl="https://api.mch.weixin.qq.com/pay/unifiedorder"; // 统一下单地址
public final static String tradeType="JSAPI"; // 支付方式 }

2.微信工具类,统一下单,签名,生成随机字符串。。

 import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList; import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.*; @Slf4j
public class WxUtil {
private static final String WXPAYSDK_VERSION = "WXPaySDK/3.0.9";
private static final String USER_AGENT = WXPAYSDK_VERSION +
" (" + System.getProperty("os.arch") + " " + System.getProperty("os.name") + " " + System.getProperty("os.version") +
") Java/" + System.getProperty("java.version") + " HttpClient/" + HttpClient.class.getPackage().getImplementationVersion(); private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final Random RANDOM = new SecureRandom();
// 统一下单接口
public static Map<String, String> unifiedOrder(Map<String, String> reqData) throws Exception {
// map格式转xml 方法在下面
String reqBody = mapToXml(reqData);
// 发起一次统一下单的请求 方法内容在下面
String responseBody = requestOnce(WxConfig.payUrl, reqBody);
// 将得到的结果由xml格式转为map格式 方法内容在下面
Map<String,String> response= processResponseXml(responseBody);
// 得到prepayId
String prepayId = response.get("prepay_id");
// 组装参数package_str 为什么这样? 因为二次签名微信规定这样的格式
String package_str = "prepay_id="+prepayId;
Map<String,String> payParameters = new HashMap<>();
long epochSecond = Instant.now().getEpochSecond();
payParameters.put("appId",WxConfig.appId);
payParameters.put("nonceStr", WxUtil.generateNonceStr());
payParameters.put("package", package_str);
payParameters.put("signType", SignType.MD5.name());
payParameters.put("timeStamp", String.valueOf(epochSecond));
// 二次签名
payParameters.put("paySign", WxUtil.generateSignature(payParameters, WxConfig.key, SignType.MD5));
// 返回签名后的map
return payParameters;
} /**
* 生成签名. 注意,若含有sign_type字段,必须和signType参数保持一致。
*
* @param data 待签名数据
* @param key API密钥
* @param signType 签名方式
* @return 签名
*/
public static String generateSignature(final Map<String, String> data, String key, SignType signType) throws Exception {
Set<String> keySet = data.keySet();
String[] keyArray = keySet.toArray(new String[keySet.size()]);
Arrays.sort(keyArray);
StringBuilder sb = new StringBuilder();
for (String k : keyArray) {
if (k.equals("sign")) {
continue;
}
if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名
sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
sb.append("key=").append(key);
if (SignType.MD5.equals(signType)) {
return MD5(sb.toString()).toUpperCase();
}
else if (SignType.HMACSHA256.equals(signType)) {
return HMACSHA256(sb.toString(), key);
}
else {
throw new Exception(String.format("Invalid sign_type: %s", signType));
}
} /**
* 生成 MD5
*
* @param data 待处理数据
* @return MD5结果
*/
private static String MD5(String data) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] array = md.digest(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
} public static String generateNonceStr() {
char[] nonceChars = new char[32];
for (int index = 0; index < nonceChars.length; ++index) {
nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
}
return new String(nonceChars);
} public static String mapToXml(Map<String, String> data) throws Exception {
Document document = newDocument();
Element root = document.createElement("xml");
document.appendChild(root);
for (String key: data.keySet()) {
String value = data.get(key);
if (value == null) {
value = "";
}
value = value.trim();
Element filed = document.createElement(key);
filed.appendChild(document.createTextNode(value));
root.appendChild(filed);
}
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
DOMSource source = new DOMSource(document);
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
transformer.transform(source, result);
String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
try {
writer.close();
}
catch (Exception ex) {
}
return output;
} // 判断签名是否有效
private static Map<String, String> processResponseXml(String xmlStr) throws Exception {
String RETURN_CODE = "return_code";
String return_code;
Map<String, String> respData = xmlToMap(xmlStr);
if (respData.containsKey(RETURN_CODE)) {
return_code = respData.get(RETURN_CODE);
} else {
throw new Exception(String.format("No `return_code` in XML: %s", xmlStr));
} if (return_code.equals("FAIL")) {
return respData;
}
else if (return_code.equals("SUCCESS")) {
if (isResponseSignatureValid(respData)) {
return respData;
}
else {
throw new Exception(String.format("Invalid sign value in XML: %s", xmlStr));
}
}
else {
throw new Exception(String.format("return_code value %s is invalid in XML: %s", return_code, xmlStr));
}
}
// 判断签名
private static boolean isResponseSignatureValid(Map<String, String> data) throws Exception {
String signKeyword = "sign";
if (!data.containsKey(signKeyword) ) {
return false;
}
String sign = data.get(signKeyword);
return generateSignature(data, WxConfig.key, SignType.MD5).equals(sign);
} // 发起一次请求
private static String requestOnce(String payUrl, String data) throws Exception {
BasicHttpClientConnectionManager connManager;
connManager = new BasicHttpClientConnectionManager(
RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", SSLConnectionSocketFactory.getSocketFactory())
.build(),
null,
null,
null
); HttpClient httpClient = HttpClientBuilder.create()
.setConnectionManager(connManager)
.build();
HttpPost httpPost = new HttpPost(payUrl); RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(8000).setConnectTimeout(6000).build();
httpPost.setConfig(requestConfig); StringEntity postEntity = new StringEntity(data, "UTF-8");
httpPost.addHeader("Content-Type", "text/xml");
httpPost.addHeader("User-Agent", USER_AGENT + " " + WxConfig.mchId);
httpPost.setEntity(postEntity); HttpResponse httpResponse = httpClient.execute(httpPost);
HttpEntity httpEntity = httpResponse.getEntity();
return EntityUtils.toString(httpEntity, "UTF-8"); } private static String HMACSHA256(String data, String key) throws Exception {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
sha256_HMAC.init(secret_key);
byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
} private static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
documentBuilderFactory.setXIncludeAware(false);
documentBuilderFactory.setExpandEntityReferences(false); return documentBuilderFactory.newDocumentBuilder();
} private static Document newDocument() throws ParserConfigurationException {
return newDocumentBuilder().newDocument();
} public static Map<String, String> xmlToMap(String strXML) throws Exception {
try {
Map<String, String> data = new HashMap<String, String>();
DocumentBuilder documentBuilder = newDocumentBuilder();
InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
org.w3c.dom.Document doc = documentBuilder.parse(stream);
doc.getDocumentElement().normalize();
NodeList nodeList = doc.getDocumentElement().getChildNodes();
for (int idx = 0; idx < nodeList.getLength(); ++idx) {
Node node = nodeList.item(idx);
if (node.getNodeType() == Node.ELEMENT_NODE) {
org.w3c.dom.Element element = (org.w3c.dom.Element) node;
data.put(element.getNodeName(), element.getTextContent());
}
}
try {
stream.close();
} catch (Exception ex) {
// do nothing
}
return data;
} catch (Exception ex) {
getLogger().warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", ex.getMessage(), strXML);
throw ex;
} }
/**
* 日志
* @return
*/
private static Logger getLogger() {
Logger logger = LoggerFactory.getLogger("wxpay java sdk");
return logger;
} /**
* 判断签名是否正确
*
* @param xmlStr XML格式数据
* @param key API密钥
* @return 签名是否正确
* @throws Exception
*/
public static boolean isSignatureValid(String xmlStr, String key) throws Exception {
Map<String, String> data = xmlToMap(xmlStr);
if (!data.containsKey("sign") ) {
return false;
}
String sign = data.get("sign");
return generateSignature(data, key,SignType.MD5).equals(sign);
} }

3.小程序发起请求 组装发起统一下单所需要的参数

     @PostMapping("/recharge/wx")
public Map recharge(HttpServletRequest request, @RequestParam(value = "vipType",required = true) VipType vipType) throws Exception {
// 本案例是充值会员 用的时候根据实际情况改成自己的需求
Integer loginDealerId = MySecurityUtil.getLoginDealerId();
// 获取ip地址 发起统一下单必要的参数
String ipAddress = HttpUtil.getIpAddress(request);
// 生成预付订单 存入数据库 回调成功在对订单状态进行修改
PrepaidOrder prepaidOrder = payService.recharge(loginDealerId, vipType, ipAddress);
// 组装统一下单需要的数据map
Map<String, String> stringStringMap = prepaidOrder.toWxPayParameters();
// 调起统一支付
Map<String, String> payParameters =WxUtil.unifiedOrder(stringStringMap);
return payParameters;
}

生成预付订单代码(根据实际需求生成,此处只是我这的需求,仅供参考)

 @Service("WXPayService")
@Slf4j
public class PayServiceImpl implements PayService { @Resource
PrepaidOrderDao prepaidOrderDao; @Resource
VipDao vipDao; @Resource
DealerDao dealerDao; @Resource
ApplicationContext applicationContext;
@Override
@Transactional
public PrepaidOrder recharge(Integer dealerId, VipType vipType, String userIp) {
Dealer dealer = dealerDao.getDealerById(dealerId);
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
String newDate = sdf.format(new Date());
Random random = new Random();
String orderNumber = newDate + random.nextInt(1000000);
BigDecimal amount = null;
// 如果不是生产环境 付一分钱
if (!applicationContext.getEnvironment().getActiveProfiles()[0].contains("prod")){
amount = BigDecimal.valueOf(0.01);
}else if (vipType.equals(VipType.YEAR)){
amount= BigDecimal.valueOf(999);
}else {
amount = BigDecimal.valueOf(365);
}
PrepaidOrder prepaidOrder = new PrepaidOrder();
prepaidOrder.setDealerId(dealerId);
prepaidOrder.setOpenId(dealer.getOpenId()); // 这个是微信需要的 openid
prepaidOrder.setVipType(vipType);
prepaidOrder.setUserIp(userIp); // 这个是微信需要的参数 userIp
prepaidOrder.setOrderStatus(OrderStatus.ONGOING);
prepaidOrder.setAmount(amount); // 这个是微信需要的参数 total_fee
prepaidOrder.setOrderNumber(orderNumber); // 这个是微信需要的参数 out_trade_no
// 添加预付订单
prepaidOrderDao.addPrepaidOrder(prepaidOrder);
return prepaidOrder;// 返回预付订单
  }  }

在实体类做最后的参数封装

 @Data
public class PrepaidOrder extends BaseModel {
private String orderNumber;
private Integer dealerId;
private Integer versionNum;
private BigDecimal amount;
private OrderStatus orderStatus=OrderStatus.ONGOING;
private LocalDateTime successTime;
private String userIp;
private String openId;
private VipType vipType; public Map<String, String> toWxPayParameters() throws Exception {
Map map = new HashMap();
map.put("body",getBody()); // 商品名字
map.put("appid", WxConfig.appId); // 小程序appid
map.put("mch_id", WxConfig.mchId); // 商户id
map.put("nonce_str", WxUtil.generateNonceStr()); // 随机字符串
map.put("notify_url", AppConst.host+WxConfig.notifyPath); // 回调地址
map.put("openid",this.openId); // 发起微信支付的用户的openid
map.put("out_trade_no",this.orderNumber); // 订单号
map.put("spbill_create_ip",this.userIp); // 发起微信支付的用户的ip地址
map.put("total_fee",parseAmount()); // 金额 (单位分)
map.put("trade_type",WxConfig.tradeType); // 支付类型
// 数据签名 也是第一次签名
map.put("sign", WxUtil.generateSignature(map, WxConfig.key, SignType.MD5 ));
return map;
} public String getBody(){
if (vipType.equals(VipType.YEAR)){
return "年度会员";
}else {
return "季度会员";
}
} public String parseAmount(){
BigDecimal multiply = amount.multiply(BigDecimal.valueOf(100));
BigDecimal result = multiply;
if (multiply.compareTo(BigDecimal.valueOf(1))==0){
result = BigDecimal.valueOf(1);
}
return result.toString();
} @Override
public String toString() {
return "PrepaidOrder{" +
"orderNumber='" + orderNumber + '\'' +
", dealerId=" + dealerId +
", versionNum=" + versionNum +
", amount=" + amount +
", orderStatus=" + orderStatus +
", successTime=" + successTime +
", userIp='" + userIp + '\'' +
", openId='" + openId + '\'' +
", vipType=" + vipType +
'}';
}
}
 

4.签名类型的枚举类  public enum SignType { MD5, HMACSHA256 }

5.获取用户IP工具类

     public static String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}

本文阅读代码顺序  只针对本文

小程序发起微信支付->controller获取用户必要信息->service生成预付订单->实体类参数封装->WxUtil发起统一下单->返回结果

以上就是发起统一下单的所有流程了 整个流程

springboot+微信小程序实现微信支付【统一下单】的更多相关文章

  1. 微信小程序之微信支付C#后台(统一下单)

    一.微信小程序支付 1.微信小程序端请求支付接口 商户在小程序中先调用该接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易后调起支付.具体可以查看接口示例. 接口传入参数示例: <xm ...

  2. 微信小程序结合微信公众号进行消息发送

    微信小程序结合微信公众号进行消息发送 由于小程序的模板消息已经废弃了,官方让使用订阅消息功能.而订阅消息的使用限制比较大,用户必须得订阅.需要获取用户同意接收消息的权限.用户必须得和小程序有交互的时候 ...

  3. 微信小程序(微信应用号)开发ide安装解决方法

    这两天整个技术圈都炸锅了,微信小程序(微信应用号)发布内测,首批200家收到邀请,但是没受邀请的同学,也不用担心,下面介绍一下解决方法. 首先需要下载ide,昨天只需要下载0.9版本的编辑器并替换文件 ...

  4. 微信小程序(原名微信应用号)开发工具0.9版安装教程

    微信小程序全称微信公众平台·小程序,原名微信公众平台·应用号(简称微信应用号) 声明 微信小程序开发工具类似于一个轻量级的IDE集成开发环境,目前仅开放给了少部分受微信官方邀请的人士(据说仅200个名 ...

  5. 微信小程序之微信登陆 —— 微信小程序教程系列(20)

    简介: 微信登陆,在新建一个微信小程序Hello World项目的时候,就可以看到项目中出现了我们的微信头像,其实这个Hello World项目,就有一个简化版的微信登陆.只不过是,还没有写入到咱们自 ...

  6. 微信小程序(微信应用号)组件讲解[申明:来源于网络]

    微信小程序(微信应用号)组件讲解[申明:来源于网络] 地址:http://www.cnblogs.com/muyixiaoguang/p/5902008.html

  7. 微信小程序和微信公众号的id是一个吗

    首先,简单说下我遇到的问题是我们的程序调用微信小程序得到openid,然后通过openID得到用户的唯一标识,用户得以登录,然而,当我们调用微信公众号也同样的到openid,同一以用户两个不同的ope ...

  8. [转]微信小程序、微信公众号、H5之间相互跳转

    本文转自:https://www.cnblogs.com/colorful-paopao1/p/8608609.html 转自慕课网 一.小程序和公众号 答案是:可以相互关联. 在微信公众号里可以添加 ...

  9. 微信小程序、微信公众号、H5之间相互跳转

    转自慕课网 一.小程序和公众号 答案是:可以相互关联. 在微信公众号里可以添加小程序. 图片有点小,我把文字打出来吧: 可关联已有的小程序或快速创建小程序.已关联的小程序可被使用在自定义菜单和模版消息 ...

  10. 使用Appium 测试微信小程序和微信公众号方法

    由于腾讯系QQ.微信等都是基于腾讯自研X5内核,不是google原生webview,需要打开TBS内核Inspector调试功能才能用Chrome浏览器查看页面元素,并实现Appium自动化测试微信小 ...

随机推荐

  1. C语言I—2019秋作业01

    1您对软件工程专业或计算机科学与技术专业了解是什么? 工程专业将成为一个新的热门专业.软件工程专业以计算机科学与技术学科为基础,突出软件开发的工程性,使学生在掌握计算机科学与技术方面知识和技能的基础上 ...

  2. lua程序设计(一)

    摘要:lua程序设计第二版学习笔记 脚本语言的基础语法大都比较简单,这里只列举一些lua独有,或者需要特别注意的语法点. 书中前三章的内容是一些惯常的引言,基础数据类型,运算符等内容,相对简单,这里就 ...

  3. 读书笔记-《Maven实战》-2018/5/3

    5.7依赖调解 1.当一个项目有以下依赖关系的时候:A->B->C->X(1.0).A->D->X(2.0),X作为A的传递依赖而拥有两个版本,Maven为了解决以上问题 ...

  4. 敏捷宣言(Agile Manifesto)和敏捷开发十二原则

    敏捷宣言 The Agile Manifesto Individuals and interactions over Process and tools 个体与交互 重于 过程和工具 Working ...

  5. 爬虫之CrawlSpider简单案例之读书网

    项目名py文件下 class DsSpider(CrawlSpider): name = 'ds' allowed_domains = ['dushu.com'] start_urls = ['htt ...

  6. CSPS模拟 88

    今天我还是个弟弟. 果然唯有AK不可超越.. T1 决策单调性,暴力上整体二分. 极限数据跑的挺快,可是被n<k的脑残测试点qj了.. T2 又是大模拟! T3 想到剩余同种数量的彩球完全等效 ...

  7. 欧拉路&&欧拉回路

    T1是欧拉路板子,但我不会,直接爆炸.. 这玩意就是个dfs,但我以前一直以为欧拉路只能$O(nm)$求 今天才知道可以$O(n+m)$ 欧拉路判定: 无向:起点终点为奇度点,其余偶度 有向:起点终点 ...

  8. Asp.net Core 系列之--1.事件驱动初探:简单事件总线实现(SimpleEventBus)

    ChuanGoing 2019-08-06  前言 开篇之前,简单说明下随笔原因.在园子里游荡了好久,期间也起过要写一些关于.NET的随笔,因各种原因未能付诸实现. 前段时间拜读daxnet的系列文章 ...

  9. 开启docker中的mongodb认证授权

    前言: 开启MongoDB服务后,默认是没有权限验证的.直接通过IP加端口就可以远程访问数据库,并对数据库进行任意操作.下面介绍一下如何开启docker中MongoDB的权限认证. 安装完MongoD ...

  10. javax.persistence.PersistenceException: org.hibernate.exception.GenericJDBCException: ResultSet is from UPDATE. No Data.

    Java jpa调用存储过程,抛出异常如下: javax.persistence.PersistenceException: org.hibernate.exception.GenericJDBCEx ...