Java之微信支付JSAPI


JSAPI支付介绍:

JSAPI支付是用户在微信中打开商户的H5页面,商户在H5页面通过调用微信支付提供的JSAPI接口调起微信支付模块完成支付。
应用场景有:
◆ 用户在微信公众账号内进入商家公众号,打开某个主页面,完成支付。
◆ 用户的好友在朋友圈、聊天窗口等分享商家页面连接,用户点击链接打开商家页面,完成支付。
◆ 将商户页面转换成二维码,用户扫描二维码后在微信浏览器中打开页面后完成支付。

这里使用将商户页面转换成二维码,用户扫描二维码后在微信浏览器中打开页面后完成支付。

一.JSAPI支付前期准备

JSAPI支付需要准备 :公众号和商户号

1.微信支付需要进行申请

在这里插入图片描述

2.公众号平台- 网页授权 和JS授权

网页授权 在这里插入图片描述
JS授权

在这里插入图片描述

3.微信支付商户平台-支付授权目录

**ps:出现添加后页面没有反应。**
 1.添加支付目录后,按流程正确输入操作密码后。界面会跳回来,这个时候发现并没有添加成功。
 2.接下来不要刷新网页,也不要点击别的模块。再添加一次刚才添加的目录。 
 3.这个时候,你会发现不用输入操作密码了,然后,目录也添加成功了

支付授权目录

ps:最好选择为 https//

在这里插入图片描述

4. 关联账号

需关联商户平台,如果是公众号支付就关联公众号,小程序则关联小程序。
在这里插入图片描述

二、获取主要参数

   	微信商户:
   	Appid:xxxx;
   	mch_id:xx
   	APi:xxx;
   
  	公众号:
   	Appid: xx 
   	AppSecret:xx
 

1、商户号平台参数

商户号Appid的获取方法:微信支付商户后台(pay.weixin.qq.com) -> 【账户中心】->【账户设置-商户信息】->微信支付商户号。
在这里插入图片描述
微信支付商户号mch_id

在这里插入图片描述

商户密匙API:在微信支付商户平台(pay.weixin.qq.com),进入【账户中心】->【账户设置-API安全】->【设置密钥】,自助设置32位API密钥即可。如下图所示:
在这里插入图片描述

*注意:请事先将需设置的密钥用文档记录,设置成功后不支持查看,只支持修改重设。

2.公众号参数

Appid:xx
AppSecret:xx

ps:注意设置服务器白名单。

在这里插入图片描述

3.开通JSAPI支付

配置完API密钥后,请记得在【产品中心 – JSAPI支付】,开通JSAPI支付。
在这里插入图片描述

ed:微信支付开通完成。

三、支付和退款

微信支付-》微信支付回调-》验证回调的签名支付信息-》支付成功
微信退款-》微信退款回调-》解密回调信息-》退款成功

1.获取用户的openid

这里使用的是:网页授权获取用户openid
具体而言,网页授权流程分为四步:

  1. 引导用户进入授权页面同意授权,获取code
  2. 通过code换取网页授权access_token(与基础支持中的access_token不同)
  3. 如果需要,开发者可以刷新网页授权access_token,避免过期
  4. 通过网页授权access_token和openid获取用户基本信息(支持UnionID机制)

代码:

//TODO 回调接口
@RequestMapping("/gzh")
public class GzhHtmlController {
    
    
	@RequestMapping(value = "/queryFee")
	public ModelAndView queryFee(String code) {
    
    
		ModelAndView mv = new ModelAndView();
		String openid=openidUtil.getOpenid(code);
		mv.addObject("openid",openid);
		mv.setViewName("gzh/index");
		return mv;
	}
}

getOpenid方法:

	/**
	 * 
	 * @param appid 公众号appid
	 * @param appSecret 公众号appsecret
	 * @param code
	 * @return
	 */
public String getOpenid(String appid, String appSecret, String code) {
    
    
		try {
    
    
			RestTemplate restTemplate = new RestTemplate();

			String url = String.format(
					"https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code",
					appid, appSecret, code);
			String res = restTemplate.getForObject(url, String.class);
			ObjectMapper mapper = new ObjectMapper();
			@SuppressWarnings("rawtypes")
			HashMap m = mapper.readValue(res, HashMap.class);
			String openid = (String) m.get("openid");
			return openid;
		} catch (Exception e) {
    
    
			e.printStackTrace();
			return null;
		}

网站链接转二维码地址:https://cli.im/

urlEncode 处理地址:http://www.jsons.cn/urlencode/

回调地址:https://open.weixin.qq.com/connect/oauth2/authorize?appid=appid&redirect_uri=redirect_uri&response_type=code&scope=snsapi_base&state=123#wechat_redirect

地址中的参数:
1.appid=公众号的appid
2.redirect_uri= https:域名/项目名/gzh/queryFee 地址需使用urlEncode 对链接进行处理
3.response_type=code&scope=snsapi_base&state=123#wechat_redirect 默认

通过地址生成的二维码用微信扫码进入,后台就获取到了openId。

2.支付代码

工具类WXPayUtil是官方的sdk包中,下载导入。
在这里插入图片描述

参考:https://blog.csdn.net/daotiao0199/article/details/85284038

2.1 准备好11个参数:

appid:商家平台ID。在微信的平台上有 
body:商品描述。
mch_id:商户ID。在微信的平台上有
nonce_str:随机字符串,UUID就好了。
openid:用户标识。因为这边是用户已经登录成功了。所以在session中就能拿到。
out_trade_no:商户订单号
spbill_create_ip:终端IP。这个可以从请求头中拿到
total_fee:支付金额。单位是分。
trade_type:交易类型。这里我填JSAPI
notify_url:通知地址。就是用户支付成功之后,微信访问你的哪个接口,跟你传递支付成功的相关信息。

生成的签名:

sign:签名。这个签名它是由上面的10个参数计算得出的。

2.2 支付代码
wxpay类也是微信的sdk


	private final Logger log = LoggerFactory.getLogger(CreatePaymentController.class);
	
/**
	 * @Description 微信浏览器内微信支付/公众号支付(JSAPI)
	 * @param request
	 * @param code
	 * @return Map
	 */
	@RequestMapping(value = "orders")
	public  Map<String, String> orders() {
    
    
		try {
    
    
			 Map<String, String> paraMap= wxpay.unifiedOrder(data);
			String openId = "用户的openid";
			paraMap.put("appid", AuthUtil.APPID); // 商家平台ID
			paraMap.put("body", "纯情小店铺-薯条"); // 商家名称-销售商品类目、String(128)
			paraMap.put("mch_id", AuthUtil.MCHID); // 商户ID
			paraMap.put("nonce_str", WXPayUtil.generateNonceStr()); // UUID
			paraMap.put("openid", openId);
			paraMap.put("out_trade_no", UUID.randomUUID().toString().replaceAll("-", ""));// 订单号,每次都不同
			paraMap.put("total_fee", "1"); // 支付金额,单位分
			paraMap.put("trade_type", "JSAPI"); // 支付类型
			paraMap.put("notify_url", "用户支付完成后,你想微信调你的哪个接口");// 此路径是微信服务器调用支付结果通知路径随意写
			String sign = WXPayUtil.generateSignature(paraMap, AuthUtil.PATERNERKEY);
			paraMap.put("sign", sign);
			//业务处理  生成商户订单
		    Payment payment =xxx;
		 
			// 统一下单
			Map<String, String> result= wxpay.unifiedOrder(paraMap);
			System.out.println("result为:" + result.toString());
			// 判断是否成功 并返回给前端,前端调用微信的组件进行支付
			Map<String, String> data = new HashMap<>();
			if (result.get("return_code").equals("SUCCESS")) {
    
    
				data.put("appId", result.get("appid"));
				data.put("timeStamp", WXPayUtil.getCurrentTimestamp() + "");
				data.put("nonceStr", WXPayUtil.generateNonceStr());
				data.put("package", "prepay_id=" + result.get("prepay_id"));
				data.put("signType", "HMACSHA256");
				data.put("paySign", WXPayUtil.generateSignature(data, config.getKey(), SignType.HMACSHA256));
				// 返回SUCCESS,注意只有签名之后才能设置
				data.put("returnCode", "SUCCESS");
				data.put("outTradeNo", payment.getOutTradeNo());
		} else {
    
    

			data.put("returnCode", "FAIL");
			data.put("returnMsg", "创建微信订单失败,错误为:" + result.get("return_msg"));
		}
		log.debug(data.toString());
		return data;
	}

	/**
	 * @Title: callBack
	 * @Description: 支付完成的回调函数
	 * @param:
	 * @return:
	 */
	@RequestMapping("/notify")
	public String callBack(HttpServletRequest request, HttpServletResponse response) {
    
    
		// System.out.println("微信支付成功,微信发送的callback信息,请注意修改订单信息");
		InputStream is = null;
		try {
    
    

			is = request.getInputStream();// 获取请求的流信息(这里是微信发的xml格式所有只能使用流来读)
			String xml = WXPayUtil.InputStream2String(is);
			Map<String, String> notifyMap = WXPayUtil.xmlToMap(xml);// 将微信发的xml转map
			
			System.out.println("微信返回给回调函数的信息为:"+xml);
			
			if (notifyMap.get("result_code").equals("SUCCESS")) {
    
    
				String ordersSn = notifyMap.get("out_trade_no");// 商户订单号
				String amountpaid = notifyMap.get("total_fee");// 实际支付的订单金额:单位 分
				BigDecimal amountPay = (new BigDecimal(amountpaid).divide(new BigDecimal("100"))).setScale(2);// 将分转换成元-实际支付金额:元

				/*
				 * 以下是自己的业务处理------仅做参考 更新order对应字段/已支付金额/状态码
				 */
				System.out.println("===notify===回调方法已经被调!!!");
				
			}
			
			// 告诉微信服务器收到信息了,不要在调用回调action了========这里很重要回复微信服务器信息用流发送一个xml即可
			response.getWriter().write("<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>");
		} catch (Exception e) {
    
    
			e.printStackTrace();
		} finally {
    
    
			if (is != null) {
    
    
				try {
    
    
					is.close();
				} catch (IOException e) {
    
     
					e.printStackTrace();
				}
			}
		}
		
		return null;
	}

3.退款代码

/**
     * 根据支付记录,进行预退款
     * @param outRefundNo parkingpay系统中的退款订单号
     * @param paymentId 该支付记录的id
     * @param refundFee 退款金额
     * @return
     * @throws Exception
     */ 
	@RequestMapping("/doRefund")
	@Transactional
	// TODO 增加一个调用安全控制
	public Map<String, Object> Refund(String outRefundNo, Long paymentId, BigDecimal refundFee) throws Exception {
    
    
		Map<String, Object> resp = new HashMap<>();
		resp.put("returnCode", "SUCCESS");

		log.debug("paymentId:{}", paymentId);
		// 检查payment是否存在
		Payment payment = paymentMapper.selectByPrimaryKey(paymentId); // 根据系统内支付ID查询
		if (payment == null) {
    
    
			log.debug("无效支付订单号");
			resp.put("returnCode", "FAIL");
			resp.put("returnMsg", "无效支付订单号");
			return resp;
		}
		// 检查payment状态是否为支付成功
		if (!payment.getStatus().equals(PaymentStatus.SUCCESS)) {
    
    
			log.debug("订单状态:{} 不正确", payment.getStatus());
			resp.put("returnCode", "FAIL");
			resp.put("returnMsg", "订单状态不正确");
			return resp;
		}
		// 检查是否存在退款订单
		Refund refund = refundMapper.selectByOutRefundNo(outRefundNo);
		if (refund == null) {
    
    
			log.debug("退款申请不存在");
			resp.put("returnCode", "FAIL");
			resp.put("returnMsg", "退款申请不存在");
			return resp;
		}
		// 检查退款订单状态是否为申请退款
		if (!refund.getStatus().equals(RefundStatus.CREATED)) {
    
    
			log.debug("退款申请状态:{} 不正确", refund.getStatus());
			resp.put("returnCode", "FAIL");
			resp.put("returnMsg", "退款申请状态不正确");
			return resp;
		}
		// 判断退款订单金额与付款金额大小
		if (refundFee.compareTo(payment.getFee()) == 1) {
    
    
			log.debug("申请退款:{} > 实际缴纳:{}", refundFee, payment.getFee());
			resp.put("returnCode", "FAIL");
			resp.put("returnMsg", "退款申请金额大于已付金额");
			return resp;
		}

		log.debug("准备退款参数");

		// 必要参数
		Map<String, String> parameters = new HashMap<>();
		parameters.put("appid", appid); // 微信小程序appid
		parameters.put("mch_id", mch_id); // 微信支付的商户号
		parameters.put("nonce_str", WXPayUtil.generateNonceStr()); // 随机字符串
		parameters.put("out_trade_no", payment.getOutTradeNo()); // 商户订单号
		parameters.put("out_refund_no", refund.getOutRefundNo()); // 商户退款单号
		parameters.put("total_fee", payment.getFee().multiply(BigDecimal.valueOf(100)).intValue() + ""); // 订单金额
		parameters.put("refund_fee", refundFee.multiply(BigDecimal.valueOf(100)).intValue() + ""); // 退款金额。由于可以分批退款,所以退款金额<=订单金额
		// 非必要参数
		parameters.put("sign_type", "HMACSHA256"); // 签名类型 默认MD5
		parameters.put("refund_fee_type", "CNY"); // 退款货币种类 默认CNY 可以不填
		parameters.put("notify_url", notifyUrl); // 回调url
		// 根据以上数据生成sign签名
		parameters.put("sign", WXPayUtil.generateSignature(parameters, key, SignType.HMACSHA256));// 工具类的方法,key就是平台上获取的
		log.debug("退款数据:{}", parameters.toString());

		// 向微信发起退款申请并获取 response
		WXPay wxPay = new WXPay(smallConfig);
		log.debug("调用退款");
		Map<String, String> returnMap;
		try {
    
    
			returnMap = wxPay.refund(parameters);
		} catch (Exception e) {
    
    
			e.printStackTrace();
			throw e;
		}
		log.debug("退款申请返回:{}" + returnMap);

		// retrunMap 为微信返回的 response body 中的 data
		if ("SUCCESS".equals(returnMap.get("return_code")) && "SUCCESS".equals(returnMap.get("result_code"))) {
    
    
			log.debug("更新退款申请(id:{}),状态为退款中", refund.getId());
			// 修改退费申请的状态与实际退费金额
			refund.setStatus(RefundStatus.REFUNDING);
			refundMapper.updateByPrimaryKeySelective(refund);
			resp.put("returnCode", "SUCCESS");
			resp.put("returnMsg", "退款成功");
			return resp;
		} else if ("SUCCESS".equals(returnMap.get("return_code")) && "FAIL".equals(returnMap.get("result_code"))) {
    
    
			// 请求发出,但微信端正常拒绝(接口调用正常,但业务不对:退款金额大于支付金额等)
			log.debug("退款失败(接口调用正常)");
			log.debug("失败原因:{}", returnMap.get("err_code_des"));
			resp.put("returnCode", "FAIL");
			resp.put("returnMsg", "退款失败,原因:" + returnMap.get("err_code_des"));
			return resp;
		} else {
    
    
			// 请求错误(接口调用失败:签名失败、参数格式错误等)
			// 失败关闭退款订单-删除 防止重复退款请求
			log.debug("退款失败(接口调用出错)");
			resp.put("returnCode", "FAIL");
			resp.put("returnMsg", "退款失败,原因:" + returnMap.get("return_msg"));
			return resp;
		}
	}

	/**
	 * 退款回调
	 * 
	 * @param request
	 * @return
	 * @throws Exception
	 */
	@RequestMapping("/notify")
	public String finishPaymentOrder(HttpServletRequest request) throws Exception {
    
    
		Map<String, String> respData = new HashMap<>();

		String resXml = IOUtils.toString(request.getInputStream());
		log.debug("退款返回:{}", resXml);
		Map<String, String> map = WXPayUtil.xmlToMap(resXml);
		log.debug("退款回调:{}" + map);

		// 判断退款是否成功!
		if ("SUCCESS".equals(map.get("return_code"))) {
    
    
			log.debug("退款成功");
			String req_info = map.get("req_info"); // 获取加密信息
			// 解密 req_info
			String decoded = AESUtil.decryptData(req_info);
			log.debug("解密后信息为:{}", decoded);
			Map<String, String> mapInfo = WXPayUtil.xmlToMap(decoded);
			String outRefundNo = mapInfo.get("out_refund_no"); // 微信退款订单号
			String refund_status = mapInfo.get("refund_status"); // 退款状态
			String refundFee = mapInfo.get("refund_fee"); // 实际退款金额
			Refund refund = refundMapper.selectByOutRefundNo(outRefundNo);
			// 退款状态判断
			if ("SUCCESS".equals(refund_status)) {
    
    
				BigDecimal refundFeeDecimal = BigDecimal.valueOf(Long.valueOf(refundFee))
						.divide(BigDecimal.valueOf(100));
				// 保存实际退款金额
				refund.setRefundFee(refundFeeDecimal);
				refund.setStatus(RefundStatus.SUCCESS);
			} else {
    
    
				log.debug("退款失败");
				refund.setStatus(RefundStatus.FAIL);
			}
			log.debug("更新refund");
			refundMapper.updateByPrimaryKey(refund);
		} else {
    
    
			log.debug("退款失败,原因:{}", map.get("return_msg"));
			// 除了日志还能干吗?
		}
		// 若执行到这里必然给微信返回成功
		// 微信就不在会继续调用此接口
		respData.put("return_code", "SUCCESS");
		respData.put("return_msg", "OK");
		log.debug("支付回调处理完毕");
		return WXPayUtil.mapToXml(respData);
	}

退款回调解密:

AESUtil.java

package cn.edu.cqu.paymentservice.utils.jmutil;

import cn.edu.cqu.paymentservice.utils.wxpay.WXPayUtil;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.util.Map;

/**
 * @author jing.huang
 * @function
 * @date 2018年1月10日
 * @version
 */
public class AESUtil {
    
    
	/**
	 * 密钥算法
	 */
	private static final String ALGORITHM = "AES";
	/**
	 * 加解密算法/工作模式/填充方式
	 */
	private static final String ALGORITHM_MODE_PADDING = "AES/ECB/PKCS5Padding";
	/**
	 * 生成key
	 */
	private static SecretKeySpec key = new SecretKeySpec(
			MD5.MD5Encode("api密匙").toLowerCase().getBytes(), ALGORITHM);

	/**
	 * AES加密
	 *
	 * @param data
	 * @return
	 * @throws Exception
	 */
	public static String encryptData(String data) throws Exception {
    
    
		// 创建密码器
		Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING);
		// 初始化
		cipher.init(Cipher.ENCRYPT_MODE, key);
		return Base64Util.encode(cipher.doFinal(data.getBytes()));
	}

	/**
	 * AES解密
	 *
	 * @param base64Data
	 * @return
	 * @throws Exception
	 */
	public static String decryptData(String base64Data) throws Exception {
    
    
		Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING);
		cipher.init(Cipher.DECRYPT_MODE, key);
		return new String(cipher.doFinal(Base64Util.decode(base64Data)));
	}

	public static void main(String[] args) throws Exception {
    
    

		String A = "<xml><return_code>SUCCESS</return_code><appid><![CDATA[wx6ee8f89d58e108ad]]></appid><mch_id><![CDATA[1389309602]]></mch_id><nonce_str><![CDATA[6c1060a9bce50ae1dbb99185a3addc2a]]></nonce_str><req_info><![CDATA[5HOIoMIqBE6kfuOI96rWcq7egyPUHd0wPscqU2ADA/TKchAZIbUQQbGGPbLvaBq2q3p4oyoowc53seV/I+F977v+IA8C7BAExbftBVIhuKo9C3ii6j8gDmkB3fuc31lLdcQoQqiZWO46voDH+Uc6P4+Q3chOqlZlKITQZ+4YxCe898UBCymgQNEO9LNW3gR6s1cIykfMjgeGjUJ89ugdI3tAIs9nEXvWQmzFSKLurFTn6Juj5B6lf4TwpgKzIZxS6zAM5G3opUU0JeoBwMUyUet49w3yoR+HkvPZr1SWiO5ufXYF41b1IzQh9TBL70byL9/o1s+rxTefOY4JRvbMkN139xoXBe1OWH0cJdux56J1XwV4bdiuk0Eh+FxGtABBzEJxZi5w5nara5VRRka+t+B2MSI0fyOwcYZGJEtMKkQlC1MvST6ht45S/midObIY49uCOuq92VuC/HrdmM88Ge881h4T4ccM4ViLTFW2NrKYxFIDFEp3iwhMWY1FpX7A5pIf1i+FLPUlnp9fvqMurG1gYmc61Etvz6HHlohrS+4uItlCLDkJ/O3ZMHFffmta6J2sxOhMOMq3mkkd8LGYan7InznOWQY/XiAxTO/Txxe3s2RJrbBn+BrmQa3Y2Ps2q5BcxMstH0Ln5rV3qG43cpI2zmVtfL0SFBAKn2IB4AdyaiARp8fwPM+PIt7jd215MDTOw5ac4ZppZ6GoS7jUORIO6DVyBlUwfkkIT8fyDutVtp2NZv4X+9rEpp9nH9hgAUU4OdbFVicv3TgLaNJjzpqIu0114c6mVwVavB6ah37+Qyg2PTvTPcGL3+vqXkfW7jnqc/hRBXAQBITyhAIiZnUV8XsU3pFNvDithFqAOBJiQok0JfdPO8uQSIinxuOyTZsnqBpG/1qSeYrzmQKWJda+TKsoBwlsHG+3oXLITyZ416vQJX6Bvof/5tpofuqekVl6GepRkz6ADkwEet/A7Jsp7+KrQwMyF4cWVra69loNJhpnji4qVTHMbPuyStLfngjzmZdA/aL5qvFYRnAIhOEm9WARwyw9QDY01hdleedm6yghd9FRZam7i9cJIA5q]]></req_info></xml>\n";
		Map<String, String> map = WXPayUtil.xmlToMap(A); //微信sdk工具方法
		String req_info = map.get("req_info");

		// 解密 req_info
		String B = AESUtil.decryptData(req_info);
		System.out.println(B);
		Map<String, String> mapInfo = WXPayUtil.xmlToMap(B);
		System.out.println(mapInfo.get("refund_status"));

		// 加密B
//        System.out.println(encryptData(B));
	}

}

Base64Util.java

package cn.edu.cqu.paymentservice.utils.jmutil;


import java.util.Base64;

/**
 * @author jing.huang
 * @function jdk8支持
 * @date 2018年1月10日
 * @version
 */
public class Base64Util {
    
    


    public static byte[] decode(String encodedText){
    
    
        final Base64.Decoder decoder = Base64.getDecoder();
        return decoder.decode(encodedText);
    }

    public static String encode(byte[] data){
    
    
        final Base64.Encoder encoder = Base64.getEncoder();
        return encoder.encodeToString(data);
    }




}

MD5.java

package cn.edu.cqu.paymentservice.utils.jmutil;

import java.security.MessageDigest;



public class MD5 {
    
    
    private final static String[] hexDigits = {
    
    "0", "1", "2", "3", "4", "5", "6", "7",
            "8", "9", "a", "b", "c", "d", "e", "f"};

    /**
     * 转换字节数组为16进制字串
     * @param b 字节数组
     * @return 16进制字串
     */
    public static String byteArrayToHexString(byte[] b) {
    
    
        StringBuilder resultSb = new StringBuilder();
        for (byte aB : b) {
    
    
            resultSb.append(byteToHexString(aB));
        }
        return resultSb.toString();
    }

    /**
     * 转换byte到16进制
     * @param b 要转换的byte
     * @return 16进制格式
     */
    private static String byteToHexString(byte b) {
    
    
        int n = b;
        if (n < 0) {
    
    
            n = 256 + n;
        }
        int d1 = n / 16;
        int d2 = n % 16;
        return hexDigits[d1] + hexDigits[d2];
    }

    /**
     * MD5编码
     * @param origin 原始字符串
     * @return 经过MD5加密之后的结果
     */
    public static String MD5Encode(String origin) {
    
    
        String resultString = null;
        try {
    
    
            resultString = origin;
            MessageDigest md = MessageDigest.getInstance("MD5");
            resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
        return resultString;
    }



}

ps:2021.07.19更新
以上支付是公众号网页版支付。
如需小程序支付

猜你喜欢

转载自blog.csdn.net/qq_40286424/article/details/108335411