评论

Asp.Net Core3.1 微信支付、退款v3 JSAPI

记录一下Core3.1里面的微信支付,纸上得来终觉浅。网上关于asp.net 微信支付的资料少。分享一下。

1、前言

看着那么丰厚的奖品我觉得我又行了,想当初开始弄这样一块的时候大多数都是java,php语言的,.Ner Core的少之又少。哈哈自己的东西能跟大家分享也是一种享受。这里后端是Core3.1 Web Api前段时间 vue来实现的。

2、准备、配置

一个非个人性质的公众号跟商户号,并且你自己是管理员,公众号都知道,商户号是专门支付的平台。微信服务号申请地址 ,商户号申请地址

登录公众号后台看左侧菜单公众号设置---->功能设置------>JS安全域名----->设置。添加一个域名,这个域名就是你后台部署程序对应的那个服务器的域名,服务器要ICP备案 能访问80端口。把公众号页面上的文件下载下来复制到这个服务器部署目录的根目录。这里我是Core Api的。发布的时候是访问不到的。我这边的操作就是在项目中新建一个wwwroot文件,在configure里面加上访问静态文件 app.UseStaticFiles()然后把文件丢进去发布就可以了。注意了这里发布都是发布在80端口上面

在JS域名安全域名下面还有一个网页授权域名 一样的设置。 我部署在CentOS上面 看一下文件夹目录,还有一个文件夹里面是是p12文件 后面会提到。这个网页授权意思就是后面要获取到用户的OpenId的时候 要通过这个域名授权。我们就能获取到用户的信息,授权登录这些配置。后面图上还有一个HHhhjZj的文件这个是商户号上面设置的。

在微信商户平台上面选择产品中心---->开发配置,这里面设置支付目录。我这里是设置的一个 ,我也不是申请商户号的人 也没有这个权限 。上面的界面跟上面两步骤差不多就不啰嗦了。

最后在开发-------基本配置--------Ip白名单 ----查看 修改一下 把服务器的Ip弄进去。这个弄了可以后端操作公众号的菜单显示。至于微信商户号key v3Key设置 这里不再重复 参考微信开发文档  微信JSAPI开发接入前准备 https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_1.shtml 设置秘钥 GmQQW594Yl这种来拼够32位就可以了哈哈。

3、Core3.1后端代码 详解

终于到写代码了前面的还是准备工作。坑到处都是写上前面的可能会帮助到你。微信支付的逻辑就是,获取用户的OpenId------->统一下单获取payId-------------->拉起微信支付------------>支付回调接口写逻辑 参考文档 JSAPI支付

3.1、微信请求类

 public class HttpClientFactoryHelper
    {
        private readonly IHttpClientFactory _httpClientFactory;
        private IWebHostEnvironment _webHostEnvironment;
        public HttpClientFactoryHelper(IHttpClientFactory httpClientFactory, IWebHostEnvironment webHostEnvironment)
        {
            _httpClientFactory = httpClientFactory;
            _webHostEnvironment = webHostEnvironment;
        }
       
        public void SaveLog(string text)
        {
            string thisTime = DateTime.Now.ToString("yyyyMMdd");
            var path = _webHostEnvironment.ContentRootPath + $"/ApiInterfaceErrorLog/";//绝对路径
            string dirPath = Path.Combine(path, thisTime + "/");//绝对径路 储存文件路径的文件夹
            if (!Directory.Exists(dirPath))//查看文件夹是否存在
                Directory.CreateDirectory(dirPath);
            string splitLine = "============================下一条==============================";
            string timeNow = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
            using StreamWriter file = new StreamWriter(dirPath + thisTime + ".txt", true);
            file.WriteLine(timeNow+text);
            file.WriteLine(splitLine);
        }
        #region //微信支付请求
        /// 
        /// 微信请求Post
        /// 地址
        /// 参数
        /// 私有秘钥 p12文件
        /// 商户号
        /// 商户证书号
        /// 
        public async Task WeChatPostAsync(string url,string requestString, string privateKey, string merchantId, string serialNo)
        {
            try
            {
                var client = _httpClientFactory.CreateClient();
                var requestContent = new StringContent(requestString);
                requestContent.Headers.ContentType.MediaType = "application/json";
                var auth = BuildAuthAsync(url, requestString, privateKey, merchantId, serialNo,"POST");
                string value = $"WECHATPAY2-SHA256-RSA2048 {auth}";
                client.DefaultRequestHeaders.Add("Authorization", value);
                client.DefaultRequestHeaders.Add("Accept", "application/json");
                client.DefaultRequestHeaders.Add("User-Agent", "WOW64");
                client.Timeout = TimeSpan.FromSeconds(60);
                var response = await client.PostAsync(url, requestContent);
                if (response.IsSuccessStatusCode)
                {
                    var result = await response.Content.ReadAsStringAsync();
                    return result;
                }
                else
                {
                    return $"接口【{url}】请求错误,错误代码{response.StatusCode},错误原因{response.ReasonPhrase}具体\n{JsonConvert.SerializeObject(response)}";
                }
            }
            catch(Exception ex)
            {
                SaveLog($"接口【{DateTime.Now +url}】请求错误,错误代码{ex.Message}具体/n{ex.StackTrace}");
                return ex.Message + ex.StackTrace;
            }
        }
        /// 
        /// 微信请求get
        /// 
        /// 地址
        /// 参数
        /// 私有秘钥 p12文件
        /// 商户号
        /// 商户证书号
        /// Get或者Post
        /// 
        public async Task WeChatGetAsync(string url, string privateKey, string merchantId, string serialNo)
        {
            try
            {
                var client = _httpClientFactory.CreateClient();
                var auth = BuildAuthAsync(url, "", privateKey, merchantId, serialNo,"GET");
                string value = $"WECHATPAY2-SHA256-RSA2048 {auth}";
                client.DefaultRequestHeaders.Add("Authorization", value);
                client.DefaultRequestHeaders.Add("Accept", "*/*");
                client.DefaultRequestHeaders.Add("User-Agent", "WOW64");
                client.Timeout = TimeSpan.FromSeconds(60);
                var response = await client.GetAsync(url);
                if (response.IsSuccessStatusCode)
                {
                    var result = await response.Content.ReadAsStringAsync();
                    return result;
                }
                else
                {
                    return $"接口【{url}】请求错误,错误代码{response.StatusCode},错误原因{response.ReasonPhrase}";
                }
            }
            catch (Exception ex)
            {
                SaveLog($"接口【{DateTime.Now + url}】请求错误,错误代码{ex.Message}具体/n{ex.StackTrace}");
                return ex.Message+ ex.StackTrace;
            }
        }
        protected string BuildAuthAsync(string url,string jsonParame ,string privateKey, string merchantId,string serialNo,string method="")
        {
            string body = jsonParame;
            var uri = new Uri(url);
            var urlPath = uri.PathAndQuery;
            var timestamp = DateTimeOffset.Now.ToUnixTimeSeconds();
            string nonce = Path.GetRandomFileName();

            string message = $"{method}\n{urlPath}\n{timestamp}\n{nonce}\n{body}\n";
            //string signature = Sign(message, privateKey);
            var path = _webHostEnvironment.WebRootPath + "/arsjkll/apiclient_cert.p12";
            string signature = Sign(message,path, "商户号");
            return $"mchid=\"{merchantId}\",nonce_str=\"{nonce}\",timestamp=\"{timestamp}\",serial_no=\"{serialNo}\",signature=\"{signature}\"";
        }
        /// 
        /// 签名(CentOs 不支持 换了下面的)
        /// 签名内容
        /// 秘钥
        /// 
        public string Sign(string message,string privateKey)
        {
            byte[] keyData = Convert.FromBase64String(privateKey);
            using CngKey cngKey = CngKey.Import(keyData, CngKeyBlobFormat.Pkcs8PrivateBlob);
            using RSACng rsa = new RSACng(cngKey);
            byte[] data = System.Text.Encoding.UTF8.GetBytes(message);
            return Convert.ToBase64String(rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
        }
        /// 
        /// 获取签名证书私钥
        /// 证书文件路径
        /// 密码
        /// 
        private static RSA GetPrivateKey(string priKeyFile, string keyPwd)
        {
            var pc = new X509Certificate2(priKeyFile, keyPwd, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet);
            return (RSA)pc.PrivateKey;
        }
        /// 
        /// 根据证书签名数据   后面要做成配置在数据库中
        /// 
        /// 要签名的数据
        /// 证书路径
        /// 密码
        /// 
        public string Sign(string data, string certPah, string certPwd)
        {
            var rsa = GetPrivateKey(certPah, certPwd);

            var rsaClear = new RSACryptoServiceProvider();

            var paras = rsa.ExportParameters(true);
            rsaClear.ImportParameters(paras);

            var signData = rsa.SignData(Encoding.UTF8.GetBytes(data), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
            return Convert.ToBase64String(signData);
        }
        #endregion

    }
    public class CustomerHttpException : Exception
    {
        public CustomerHttpException() : base()
        { }
        public CustomerHttpException(string message) : base(message)
        {
        }
    }
    public class ReturnData
    {
        /// 
        /// 返回码
        /// 
        public int Code { get; set; }
        /// 
        /// 消息
        /// 
        public string Msg { get; set; }
        /// 
        /// 是否成功
        /// 
        public bool IsSuccess { get; set; }
        /// 
        /// 返回数据
        /// 
        public object Data { get; set; }
        /// 
        /// 成功默认
        /// 

        /// 
        /// 
        public static ReturnData ToSuccess(object data, string msg = "sussess")
        {
            var result = new ReturnData
            {
                Code = (int)HttpStatusCode.OK,
                IsSuccess = true,
                Msg = msg,
                Data = data
            };
            return result;

        }
        public static ReturnData ToFail(string msg)
        {
            var result = new ReturnData
            {
                Code = (int)HttpStatusCode.BadRequest,
                IsSuccess = false,
                Msg = msg
            };
            return result;
        }
        /// 
        /// 异常
        /// 

        /// 
        /// 
        public static ReturnData ToError(Exception ex, object data = null)
        {
            var result = new ReturnData
            {
                Code = (int)HttpStatusCode.InternalServerError,
                IsSuccess = false,
                Msg = "异常" + ex.Message,
                Data = data
            };
            return result;
        }
        /// 
        /// 未经授权
        /// 
        /// 
        public static ReturnData ToNoToken(object data = null)
        {
            var result = new ReturnData
            {
                Code = (int)HttpStatusCode.Forbidden,
                IsSuccess = false,
                Msg = "未经授权不能访问",
                Data = data
            };
            return result;
        }
        public static ReturnData Instance(object data, string msg, int code)
        {
            var result = new ReturnData
            {
                Code = code,
                IsSuccess = false,
                Msg = msg,
                Data = data
            };
            return result;

        }
    }

Sign签名官方给的只能在IIS上面运行 那是通过直接用私钥签名,我在CentOS上面不行,以前在IIS上面也是 但是这个只要配置IIS应用程序池,把加载用户配置文件改成true就可以了。CentOS上面就不行了。后来我还是把p12文件放在了跟验证域名的那个位置,通过读取文件获取私钥。这个问题搞了我2天。。。不能跨平台。或者是我配置不对,后面有时间在研究。

3. 2、获取用户的OpenId 

在用户统一下单的时候需要用户的OpenId。这个用户在这个公众号下面的一个身份号码,关没关注获取了就不会变,所以我就是没调用统一下单之前就获取了保存在数据库中。  参考公众号网页授权我这里的逻辑就是获取过了直接数据库获取没有获取过的微信授权获取。 如果用户没有授权实际上这个接口要访问2次的 第一次code没有值,第二次微信授权后通过redirect_uri带着code回来就获取到了用户的OpenId信息。

        private const string AuthorUrl = "https://open.weixin.qq.com/connect/oauth2/authorize?";
        private const string Oauth = "https://api.weixin.qq.com/sns/oauth2/access_token?";
        private const string GetUserInfo = "https://api.weixin.qq.com/sns/userinfo?";
        public string GetAuthorizeUrl(string appId, string redirectUrl, string state = "state", string scope = "snsapi_userinfo", string responseType = "code")
        {
            redirectUrl = HttpUtility.UrlEncode(redirectUrl, System.Text.Encoding.UTF8);
            object[] args = new object[] { appId, redirectUrl, responseType, scope, state };
            return string.Format(AuthorUrl + "appid={0}&redirect_uri={1}&response_type={2}&scope={3}&state={4}#wechat_redirect", args);
        }
        private string GetOpenIdUrl(string appId, string secret, string code, string grantType = "authorization_code")
        {
            object[] args = new object[] { appId, secret, code, grantType };
            string requestUri = string.Format(Oauth + "appid={0}&secret={1}&code={2}&grant_type={3}", args);
            return requestUri;
        }
        public GetOpenIdDto GetOpenid(string appId, string secret, string code, string grantType = "authorization_code")
        {
            string requestUri = GetOpenIdUrl(appId, secret, code, grantType);
            var responseStr = _httpClientFactoryHelper.GetAsync(requestUri, null, 120).Result;
            var obj = JsonConvert.DeserializeObject(responseStr);
            var getUserInfoUrl = GetUserInfo + $"access_token={obj.Access_Token}&openid={obj.OpenId}&lang=zh_CN";
            var responseUser = _httpClientFactoryHelper.GetAsync(getUserInfoUrl, null, 120).Result;
            SaveLog("OpenDetails", responseUser);
            var objUser = JsonConvert.DeserializeObject(responseUser);
           
            
            return objUser;
        }
        //控制器里面的写法
        /// 储存用户所在公众号下面的OpenId
        /// 医院|公众号编码
        /// 用户Id(登录的那个)
        /// 微信服务器返回的code不用填
        /// 跳转的returnUrl
        [HttpGet]
        public IActionResult SaveHospPatirntOpenId(string hospCode, int userId, string code = "")
        {
            var modelOpenId = _weChatPayService.IsSaveHospPatientOpenId(hospCode, userId);
            var model = _weChatPayService.GetHospInfo(hospCode);
            var modelNew = _weChatPayService.GetHospNewInfo(hospCode);
            var returnUrl = $"http://网址/#/subSite?hospCode={model.HospCode}&hospId={modelNew.Id}";
            if (modelOpenId != null)
                return Redirect(returnUrl);
            else
            {
               
                if (string.IsNullOrEmpty(code))
                {
                    var redirectUrl = _weChatPayService.GetAuthorizeUrl(model.WxAppid, $"http://网址/api/WeChatPay/SaveHospPatirntOpenId?hospCode={hospCode}&userId={userId}");
                    return Redirect(redirectUrl);
                }
                else
                {
                    //根据code和微信参数得到openid
                    var openIdModel = _weChatPayService.GetOpenid(model.WxAppid, model.WxAppsecret, code);
                    //业务处理
                    var modelOId=_weChatPayService.HospPatirntOpenIdSaveAsync(hospCode, userId, openIdModel).Result;//数据库保存openId
                    
                    return Redirect(returnUrl);
                }
            }
        }

   3.3、统一下单

                    var path = RequestUrl.TRANSACTIONS;//https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi
                    var timeOut = DateTime.Now.AddMinutes(10);
                    var address = $"{model.Address}/api/WeChatPay/NotifySuccess";//这个是微信支付状态返回发到你的接口上的地址
                    var reqDto = new
                    {
                        appid = model.WxAppid,
                        mchid = model.WxMchid,
                        description = req.Desc,
                        out_trade_no = troNo,
                        //time_expire = timeOut,
                        attach = regOrderModel.RegId.ToString(),
                        notify_url = address,
                        amount = new
                        {
                            total = req.Total,
                            currency = "CNY"
                        },
                        payer = new
                        {
                            openid = modelOpenId.OpenId
                        }
                    };
                    var reqJson = JsonConvert.SerializeObject(reqDto);
                   
                    var ret = _httpClientFactoryHelper.WeChatPostAsync(path, reqJson, model.PrivateKey, model.WxMchid, model.CardNo).Result;
                    var result = JsonConvert.DeserializeObject(ret);
                    if (!string.IsNullOrEmpty(result.Prepay_Id))
                    {
                        //时间戳
                        DateTimeOffset dto = new DateTimeOffset(DateTime.Now);
                        var unixTime = dto.ToUnixTimeSeconds().ToString();
                        //随机数
                        var nonMun = Guid.NewGuid().ToString("N").Substring(0, 30);
                        var pck = "prepay_id=" + result.Prepay_Id;
                        //签名
                        string message = $"{model.WxAppid}\n{unixTime}\n{nonMun}\n{pck}\n";
                        var pathfile = _webHostEnvironment.WebRootPath + "/arsjkll/apiclient_cert.p12";
                        string keyRsa = _httpClientFactoryHelper.Sign(message, pathfile, "密码咯");
                        //var keyRsa = _httpClientFactoryHelper.Sign(message, model.PrivateKey);
                        //构建JSAPI拉取支付的参数 匿名参数
                        var requestParam = new
                        {
                            appId = model.WxAppid,
                            timeStamp = unixTime,
                            nonceStr = nonMun,
                            package = pck,
                            signType = "RSA",
                            paySign = keyRsa
                        };
                        return Result.ToSuccess(requestParam);
                    }
                    else
                    {
                        return Result.ToFail("prepay_id获取失败----------" + ret);
                    }

上面统一下单获取到prepay_id在构造JSAPI拉取微信支付的参数返回到前端。签名上面有代码就不贴了。

3. 4、支付回调接口

       public Result NotifySuccess(NotifyDto ret)
        {
            SaveLog("NotifyParame", JsonConvert.SerializeObject(ret));
            //ResourceASC
            if (ret.Event_type == "TRANSACTION.SUCCESS")//支付成功
            {

                //解密数据报文
                var dataJson = AesGcmHelper.AesGcmDecrypt(ret.Resource.Associated_data, ret.Resource.Nonce, ret.Resource.Ciphertext);
                //转换对象接受
                var data = JsonConvert.DeserializeObject(dataJson);
                //获取当前订单记录实体
              //自己的业务逻辑
            }
            else
            {
                SaveLog("NotifyFaile", JsonConvert.SerializeObject(ret));
            }
            return Result.ToSuccess("");
        }
    /// 
    /// 支付结果回调接收参数
    /// 
    public class NotifyDto
    {
        /// 
        /// 通知ID通知的唯一ID  
        /// 示例值:EV-2018022511223320873
        /// 
        public string Id { get; set; }
        /// 
        /// 通知创建时间  通知创建的时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE,YYYY-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss.表示时分秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。例如:2015-05-20T13:29:35+08:00表示北京时间2015年05月20日13点29分35秒。
        /// 示例值:2015-05-20T13:29:35+08:00
        /// 
        public string Create_time { get; set; }
        /// 
        /// 通知类型  通知的类型,支付成功通知的类型为TRANSACTION.SUCCESS
        /// 示例值:TRANSACTION.SUCCESS
        /// 
        public string Event_type { get; set; }
        /// 
        /// 通知数据类型  通知的资源数据类型,支付成功通知为encrypt-resource
        /// 示例值:encrypt-resource
        /// 
        public string Resource_type { get; set; }
        /// 
        /// 通知数据 通知资源数据
        /// json格式,见示例
        /// 
        public Resource Resource { get; set; }
        /// 
        /// 回调摘要 
        /// 示例值:支付成功
        /// 

        public string Summary { get; set; }
    }
    public class AesGcmHelper
    {
        private static string ALGORITHM = "AES/GCM/NoPadding";
        private static int TAG_LENGTH_BIT = 128;
        private static int NONCE_LENGTH_BYTE = 12;
        private static string AES_KEY = "v3秘钥";

        public static string AesGcmDecrypt(string associatedData, string nonce, string ciphertext)
        {
            GcmBlockCipher gcmBlockCipher = new GcmBlockCipher(new AesEngine());
            AeadParameters aeadParameters = new AeadParameters(
                new KeyParameter(Encoding.UTF8.GetBytes(AES_KEY)),
                128,
                Encoding.UTF8.GetBytes(nonce),
                Encoding.UTF8.GetBytes(associatedData));
            gcmBlockCipher.Init(false, aeadParameters);

            byte[] data = Convert.FromBase64String(ciphertext);
            byte[] plaintext = new byte[gcmBlockCipher.GetOutputSize(data.Length)];
            int length = gcmBlockCipher.ProcessBytes(data, 0, data.Length, plaintext, 0);
            gcmBlockCipher.DoFinal(plaintext, length);
            return Encoding.UTF8.GetString(plaintext);
        }
    }

 

3.5、Vue前端调用拉起支付

export default {
  data() {
    return {
      // cardNo: '',
      // desc: '',
      // hospCode: '',
      // pointId: '',//号点Id
      total: '',//支付费用
      countDownTime: 5 * 60 * 1000,//支付剩余毫秒数
      radio: '1',
      appId: '',
      timeStamp: '',
      nonceStr: '',
      package: '',
      signType: '',
      paySign: '',
      btnText: '立即支付',
      btnIsable: true,//支付按钮是否可用,true:可用 false:不可用
      payType: [{
        icon: '#icon-weixinzhifu',
        title: '微信支付',
        name: '1',
      }],
    }
  },
  methods: {
    onSubmit() {
      let vm = this;
      let obj = {
        "appId": vm.appId,//公众号名称
        "timeStamp": vm.timeStamp,//时间戳,自1970年以来的秒数
        "nonceStr": vm.nonceStr,//随机串
        "package": vm.package,
        "signType": vm.signType,
        "paySign": vm.paySign //签名 
      };

      function onBridgeReady() {
        WeixinJSBridge.invoke('getBrandWCPayRequest', obj,
          function (res) {
            if (res.err_msg == "get_brand_wcpay_request:ok") {
              // 使用以上方式判断前端返回,微信团队郑重提示:
              //res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
              vm.btnIsable = false;
              vm.closePage('支付成功');//后期跳转到挂号记录页面
            }
          });
      }
      if (typeof WeixinJSBridge == "undefined") {
        if (document.addEventListener) {
          document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
        } else if (document.attachEvent) {
          document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
          document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
        }
      } else {
        onBridgeReady();
      }
    },
    chkCheck: (el) => {
      return el.name;
    },
    countDownFinish() {
      //倒计时结束销号,不提交支付
      this.btnIsable = false;
      this.closePage('支付超时');
    },
    clickLeft() {
      this.$dialog.confirm({
        title: '提示',
        message: '支付尚未完成,是否继续支付',
      })
        .then(() => {
          // on confirm
        })
        .catch(() => {
          this.$router.go(-1);
        });
    },
    getBaseData() {
      // 获得支付信息
      let vm = this;
      let p = vm.$route.params;
      if (JSON.stringify(p) != "{}") {
        this.appId = p.appId;
        this.total = p.fee;
        this.nonceStr = p.nonceStr;
        this.package = p.package;
        this.paySign = p.paySign;
        this.countDownTime = p.paymentDeadline - vm.$moment().valueOf();
        this.signType = p.signType;
        this.timeStamp = p.timeStamp;
      } else {
        this.closePage('无效请求');
      }
    },
    closePage(text, num = 5, route = 'home') {
      this.btnIsable = false;
      let lock = setInterval(() => {
        num--;
        this.btnText = `${text}${num}秒后关闭`;
        if (num == 0) {
          clearInterval(lock);
          this.$router.push({ path: route });
        }
      }, 1000);
    },
  },
  mounted() {
    this.getBaseData();
  }
}



看一下支付成功返回的东西

4、退款

这里退款v2的v3的我都写了,刚写的时候是v2的写好了v3版本的就来了 天妒英才啊。

4.1、v2请求退费请求类

这个就写在上面的请求类里面。现在就是加了认证文件,p12文件路径跟密码,一般密码都是商户号。这就是微信文档说的双向验证。    拼接请求参数,官网文档有验证这个的。排序的话我自己手动固定排序的。 请求这里,只要Return_code等于SUCCESS下面就可以做自己的业务逻辑了。里面返回的字段肯定有一个是你自己写了传进去的。通过这个字段查询数据库完成自己的逻辑。

           /// 
           ///postXML请求
          /// 

          /// 地址
          /// 参数(json格式)
          /// p12文件路径
          /// 密码
          /// string
          public string PostXml(string url, string requestString,string path, string certPwd)
          {
              var handler = new HttpClientHandler
              {
                  ClientCertificateOptions = ClientCertificateOption.Manual,
                  SslProtocols = SslProtocols.Tls12,
                  ServerCertificateCustomValidationCallback = (x, y, z, m) => true,
              };
  
              //var path = Path.Combine(AppContext.BaseDirectory, "cert\\iot3rd.p12");
              handler.ClientCertificates.Add(new X509Certificate2(path, certPwd));
  
              var client = new HttpClient(handler);
  
              var content = new StringContent(requestString);
              content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded;charset=utf-8");
              //GetAwaiter().GetResult();
              var httpResponseMessage = client.PostAsync(url, content).Result.Content.ReadAsStringAsync().Result;
              return httpResponseMessage;
          }
         
        /// 退费需要的字符
        private string RequestRetreatParmar(Entity.Models.HospInfo hospInfo, RegOrder regOrder)
        {
            var guid = Guid.NewGuid().ToString("N").Substring(0, 30);
            var retreatNo = OrderHelper.GenerateNo("CFTF");
            int money = Convert.ToInt32(regOrder.OwnFee * 100);
            //签名
            string message = $"appid={hospInfo.WxAppid}&mch_id={hospInfo.WxMchid}&nonce_str={guid}&out_refund_no={retreatNo}&refund_desc=客户预约挂号退款&refund_fee={money}&total_fee={money}&transaction_id={regOrder.PlatformTradeId}&key={hospInfo.WxKey}";
            var signMd5 = SecurityHelper.MD5EncrytString(message).ToUpper();
            var dto = new
            {
                xml= new
                {
                    appid = hospInfo.WxAppid,
                    mch_id = hospInfo.WxMchid,
                    nonce_str = guid,
                    sign = signMd5,
                    transaction_id = regOrder.PlatformTradeId,
                    out_refund_no = retreatNo,
                    total_fee = money,
                    refund_fee = money,
                    refund_desc = "客户预约挂号退款",
                }
            };
            var json = JsonConvert.SerializeObject(dto);
            
            return json;
        }
        /// md5加密
        /// 
        /// 字符串
        /// 加密过的字符串(不可以解密)
        public static string MD5EncrytString(string inputString)
        {
            MD5 md5 = System.Security.Cryptography.MD5.Create();
            byte[] buffer = Encoding.UTF8.GetBytes(inputString);
            byte[] md5Buffer = md5.ComputeHash(buffer);
            md5.Clear();
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < md5Buffer.Length; i++)
            {
                sb.Append(md5Buffer[i].ToString("x2"));
            }
            return sb.ToString();
        }

4.2、v2退费

退费我这里没有用notify_url返回接口 直接得到结果。下面是接收的Dto

                 //微信退费操作
                    var url = RequestUrl.PAYREFUND;
                    var json = RequestRetreatParmar(hospInfoMoidel, regOrderModel);
                    var xml = JsonConvert.DeserializeXmlNode(json);
                    var postData = xml.InnerXml;
                    var pathfile = _webHostEnvironment.WebRootPath + "/arsjkll/apiclient_cert.p12";
                    var wxPostResult = _httpClientFactoryHelper.PostXml(url, postData, pathfile, hospInfoMoidel.WxSslcertpassword);
                    SaveLog("WeChatTuiFei", wxPostResult);
                    var resultRep = wxPostResult.Replace("", "");
                    XmlDocument doc = new XmlDocument();
                    doc.LoadXml(resultRep);
                    string jsonText = JsonConvert.SerializeXmlNode(doc);
                    SaveLog("WeChatTuiFei", jsonText);
                    var resultModel = JsonConvert.DeserializeObject(jsonText);
                    var resultModelXml = resultModel.Xml;
                    if (resultModelXml.Result_Code == "SUCCESS")
                    {
                        regOrderModel.RefundNo = resultModelXml.Out_Refund_No;
                        regOrderModel.RefundState = 1;
                        regOrderModel.RefundResult = resultModelXml.Return_Code;
                        regOrderModel.RefundTime = DateTime.Now;

                        payLogModel.RefundNo = resultModelXml.Out_Refund_No;
                        payLogModel.RefundState = 1;
                        payLogModel.RefundResult = resultModelXml.Return_Code;
                        payLogModel.RefundTime = DateTime.Now;
                        await _db.SaveChangesAsync();
                        return Result.ToSuccess(resultModel);
                    }
                    else
                        return Result.ToFail(resultModelXml.Return_Msg);
     public class RetreatNoResulDtoXml
    {
        public RetreatNoResulDto Xml { get; set; }
    }
    /// 
    /// 退号|退费结果
    /// 
    public class RetreatNoResulDto
    {
        /// 
        /// 返回状态码
        /// SUCCESS:退款申请接收成功,结果通过退款查询接口查询
        /// FAIL:提交业务失败
        /// 此字段是通信标识,非交易标识,交易是否成功需要查看result_code来判断
        /// 示例值:SUCCESS
        /// 
        public string Return_Code { get; set; }
        /// 
        /// 返回信息
        /// 返回信息,如非空,为错误原因 
        /// 签名失败
        /// 参数格式校验错误
        /// 示例值:签名失败
        /// 
        public string Return_Msg { get; set; }

        //返回状态码(return_code)为SUCCESS的时候,包含以下字段
        /// 
        /// 业务结果
        /// SUCCESS/FAIL
        /// 示例值:SUCCESS
        /// 

        public string Result_Code { get; set; }
        /// 
        /// 错误代码
        /// 示例值:SYSTEMERROR
        /// 

        public string Err_Code { get; set; }
        /// 
        /// 错误代码描述
        /// 结果信息描述
        ///示例值:系统错误
        /// 
        public string Err_Code_Des { get; set; }
        public string Appid { get; set; }
        public string Mch_Id { get; set; }
        public string Sub_Appid { get; set; }
        public string Sub_Mch_Id { get; set; }
        public string Nonce_Str { get; set; }
        /// 
        /// 签名
        /// 
        public string Sign { get; set; }
        public string Transaction_Id { get; set; }
        public string Out_Trade_No { get; set; }
        public string Out_Refund_No { get; set; }
        public string Refund_Id { get; set; }
        public int Refund_Fee { get; set; }
        public int Settlement_Refund_Fee { get; set; }
        public int Total_Fee { get; set; }
        public int Settlement_Total_Fee { get; set; }
        public string Fee_Type { get; set; }
        public int Cash_Fee { get; set; }
       
    }



返回结果跟官网文档一样滴

4.1、v3退费

        public async Task RetreatNov3Async(RetreatNoDto req)
        {
            var hospInfoMoidel = _db.HospInfo.AsNoTracking().FirstOrDefault(x => x.HospCode == req.HospCode);
            var regOrderModel = _db.RegOrder.FirstOrDefault(x => x.HospCode == req.HospCode && x.UserId == req.UserId && x.HisClinicNo == req.RegNo);
            var payLogModel = _db.PayLog.FirstOrDefault(x => x.TradeNo == regOrderModel.TradeNo);
            //医院退号
            var retreatDto = new DoReleaseRegReq
            {
                CompanyCode = hospInfoMoidel.CompanyCode,
                HospitalCode = hospInfoMoidel.HospCode,
                SignDate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
                RegNo = regOrderModel.HisClinicNo,
                CardNo = regOrderModel.CardNo,
                TradeNumber = "",
                TradeSerialNumber = regOrderModel.TradeNo
            };
            var result = await _registerService.DoReleaseRegAsync(retreatDto);
            if (result.ErrCode == ErrEnum.E0000000)
            {
                if (req.IsFree)//免费
                    return Result.ToSuccess(result);
                else//有费用
                {
                    //微信退费操作
                    var url = RequestUrl.PAYREFUNDV3;
                    int money = Convert.ToInt32(regOrderModel.OwnFee * 100);
                    var retParamar = new
                    {
                        transaction_id=regOrderModel.PlatformTradeId,
                        //out_trade_no=regOrderModel.TradeNo,
                        out_refund_no= OrderHelper.GenerateNo("CFTF"),
                        reason="客户预约挂号退款,公众号退款",
                        amount= new
                        {
                            refund= money,
                            total=money,
                            currency="CNY"
                        }
                    };
                    var reqJson = JsonConvert.SerializeObject(retParamar);
                    var ret = _httpClientFactoryHelper.WeChatPostAsync(url, reqJson, hospInfoMoidel.WxSslcertpassword, hospInfoMoidel.WxMchid, hospInfoMoidel.WxSslcertpath, hospInfoMoidel.CardNo).Result;
                    SaveLogPay("WeChatTuiFeiV3", ret);
                    var resultModel = JsonConvert.DeserializeObject(ret);
                    if (resultModel.Status== "SUCCESS" || resultModel.Status == "PROCESSING")
                    {
                        regOrderModel.RefundNo = resultModel.Out_Refund_No;
                        regOrderModel.RefundState = 1;
                        regOrderModel.RefundResult = resultModel.Status;
                        regOrderModel.RefundTime = DateTime.Now;
                        regOrderModel.IsValid = 0;

                        payLogModel.RefundNo = resultModel.Out_Refund_No;
                        payLogModel.RefundState = 1;
                        payLogModel.RefundResult = resultModel.Status;
                        payLogModel.RefundTime = DateTime.Now;
                        await _db.SaveChangesAsync();
                        return Result.ToSuccess(resultModel);
                    }
                    else
                        return Result.ToFail("微信退款失败,请联系院方订单号:"+ regOrderModel.TradeNo);
                }
            }
            else
                return Result.ToFail(result.Err);
        }


看一下日志结果

v2的时候看着几行代码 还给我排队等了好久终于问人工解决的,之前都没说请求头里面加载用户证书,可能是v3出来了没太在意吧。后面看看 v3接口 看样子应该跟统一下单一样了吧只要调用请求那个方法就可以了! 

最后一次编辑于  2021-05-10  
点赞 4
收藏
评论

5 个评论

  • 魏杨杨
    魏杨杨
    2021-04-14

    沙发自己来站一下

    2021-04-14
    赞同 1
    回复
  • 邓少初
    邓少初
    发表于移动端
    2022-03-02
    F f774/84478482444 84211754
    2022-03-02
    赞同
    回复
  • 可
    2021-06-07

    部署到linux报错

    System.PlatformNotSupportedException: Operation is not supported on this platform.

    解决方案:

    protected string Sign(string message)
    {
        // NOTE: 私钥不包括私钥文件起始的-----BEGIN PRIVATE KEY-----
        //        亦不包括结尾的-----END PRIVATE KEY-----
        string privateKey = "{你的私钥}";
        var rsa = RSA.Create();
        rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(privateKey), out _);
        var signbytes = rsa.SignData(Encoding.UTF8.GetBytes(message), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        return Convert.ToBase64String(signbytes);
    }
    


    2021-06-07
    赞同
    回复 3
    • ㅤㅤㅤㅤ
      ㅤㅤㅤㅤ
      2021-07-16
      楼主写的代码依赖于 RSACng 这个类,这个类只支持 Windows;如需跨平台可以参考我这篇:https://developers.weixin.qq.com/community/develop/article/doc/00020aadc384a0a5f01c3526b56813
      2021-07-16
      1
      回复
    • 魏杨杨
      魏杨杨
      2021-11-21
      ///
              /// 获取签名证书私钥
              /// 证书文件路径
              /// 密码
              ///
              private static RSA GetPrivateKey(string priKeyFile, string keyPwd)
              {
                  var pc = new X509Certificate2(priKeyFile, keyPwd, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet);
                  return (RSA)pc.PrivateKey;
              }
              ///
              /// 根据证书签名数据 后面要做成配置在数据库中
              ///
              /// 要签名的数据
              /// 证书路径
              /// 密码
              ///
              public string Sign(string data, string certPah, string certPwd)
              {
                  var rsa = GetPrivateKey(certPah, certPwd);
                  var rsaClear = new RSACryptoServiceProvider();
                  var paras = rsa.ExportParameters(true);
                  rsaClear.ImportParameters(paras);
                  var signData = rsa.SignData(Encoding.UTF8.GetBytes(data), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
                  return Convert.ToBase64String(signData);
              }
      2021-11-21
      回复
    • 魏杨杨
      魏杨杨
      2021-11-21
      用这个 上面我写了  用下面的
      2021-11-21
      回复
  • 青寒
    青寒
    2021-04-17

    点赞~

    2021-04-17
    赞同
    回复
  • 大稳·杨
    大稳·杨
    发表于移动端
    2021-04-14
    666
    2021-04-14
    赞同
    回复
登录 后发表内容