using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Gateway.Payment.Shared; using System; using System.Collections.Generic; using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; namespace PaymentGateway.GatewayApp { public class TongLianAllInPayV2Processor : IPaymentProcessor { private IServiceProvider services; private ILogger logger; public TongLianAllInPayV2Processor(IServiceProvider services) { this.services = services; var loggerFactory = services.GetRequiredService(); this.logger = loggerFactory.CreateLogger("DynamicPrivate_Gateway.Payment"); } public async Task Cancel(PaymentOrder order) { return new GenericProcessResponse() { AllInPayResponse = null }; } public async Task Process(PaymentOrder order) { order.TradeStatus = TradeStatusEnum.PAYERROR; var response = new TongLianResponseV2(); try { if (order.Optional?.ContainsKey("preCreatedBillNumber") ?? false) { var config = (TongLianPayConfigV2)order.Config; response = await QueryTrxResult(order, config.queryInterval, config.queryTimeout); } else { response = QrPay(order); if (IsSuccessful(response)) { order.TradeStatus = TradeStatusEnum.SUCCESS; } if (IsQueryResponse(response)) { this.logger.LogDebug($"Continuous query trx result is ongoing for order: {order.ToSimpleLogString()}"); // 当结果码为“2000”时,商户系统可设置间隔时间(建议10秒)重新查询支付结果,直到支付成功或超时(建议40秒) var config = (TongLianPayConfigV2)order.Config; response = await QueryTrxResult(order, config.queryInterval, config.queryTimeout); this.logger.LogDebug($" Continuous query trx result has done for order: {order.ToSimpleLogString()} with response: {response}"); if (IsSuccessful(response)) { order.TradeStatus = TradeStatusEnum.SUCCESS; } } } } catch (Exception ex) { this.logger.LogError($"Request AllInPayV2 exceptioned: {ex.ToString()}"); } return new GenericProcessResponse() { AllInPayResponseV2 = response }; } private async Task QueryTrxResult(PaymentOrder order, int interval = 10, int dueTime = 60) { this.logger.LogDebug($"AllInPayV2 QueryTrxResult started, query interval = {interval}, dueTime = {dueTime}"); var reponse = new GenericProcessResponse() { AllInPayResponseV2 = new TongLianResponseV2() }; var cts = new CancellationTokenSource(); try { await Task.WhenAny(PeriodicTaskV2.Run(Query, QuitQuerying, order, reponse, cts, TimeSpan.FromSeconds(interval)), Task.Delay(TimeSpan.FromSeconds(dueTime))); cts.Cancel(); } catch (Exception ex) { this.logger.LogError($"QueryTrxResult exceptioned, msg {ex}"); } return reponse.AllInPayResponseV2; } public async Task Query(PaymentOrder order) { order.TradeStatus = TradeStatusEnum.PAYERROR; var response = DoQuery(order); if (IsSuccessful(response)) { order.TradeStatus = TradeStatusEnum.SUCCESS; } return new GenericProcessResponse() { AllInPayResponseV2 = response }; } public async Task Query(PaymentOrder order, GenericProcessResponse reponse, CancellationTokenSource cts) { var response = await Query(order); if (IsSuccessful(reponse.AllInPayResponseV2)) { cts.Cancel(); } return response; } public async Task Return(PaymentOrder order) { order.TradeStatus = TradeStatusEnum.PAYERROR; var response = DoCancel(order); if (IsSuccessful(response)) { order.TradeStatus = TradeStatusEnum.SUCCESS; } if (IsQueryResponse(response)) { this.logger.LogDebug($"Continuous query trx result is ongoing for order: {order.ToSimpleLogString()}"); //当结果码为“2000”时,商户系统可设置间隔时间(建议10秒)重新查询支付结果,直到支付成功或超时(建议40秒) var config = (TongLianPayConfigV2)order.Config; response = await QueryTrxResult(order, config.queryInterval, config.queryTimeout); this.logger.LogDebug($" Continuous query trx result has done for order: {order.ToSimpleLogString()} with response: {response}"); if (IsSuccessful(response)) { order.TradeStatus = TradeStatusEnum.SUCCESS; } } //LogResponse(response); return new GenericProcessResponse() { AllInPayResponseV2 = response }; } public TongLianResponseV2 QrPay(PaymentOrder order) { if (string.IsNullOrEmpty(order.AuthCode)) throw new ArgumentException("Must provide AuthCode for TongLianPayV1"); if (string.IsNullOrEmpty(order.BillNumber)) throw new ArgumentException("Must provide BillNumber for TongLianPayV1"); var config = (TongLianPayConfigV2)order.Config; var paramDic = BuildBasicParam(config); var amount = Convert.ToInt32(order.NetAmount * 100); var bizContent = new BizContent { sysId = config.sysId, bizOrderNo = order.BillNumber, amount = amount, consumeTypes = new List() { new ConsumeType {amount = amount, type = "C0001"} }, oilStation = new OilStation { oilStationNo = config.oilStationNo, }, shiftsTime = DateTime.Now.ToString("yyyyMMdd"), shiftsMask = config.shiftsMask }; bizContent.authCode = order.AuthCode; bizContent.payType = "CODEPAY_VSP"; paramDic.Add("bizContent", bizContent); paramDic.Add("service", "oil.pay.apply"); paramDic.Add("sign", AllInPayAppUtil.signParam(paramDic, config.key, true)); return DoRequest(paramDic, "", config); } public TongLianResponseV2 PlaceUnifiedOrder(PaymentOrder order) { var config = (TongLianPayConfigV2)order.Config; var paramDic = BuildBasicParam(config); var amount = Convert.ToInt32(order.NetAmount * 100); var bizContent = new BizContent { sysId = config.sysId, bizOrderNo = order.BillNumber, amount = amount, consumeTypes = new List() { new ConsumeType {amount = amount, type = "C0001"} }, oilStation = new OilStation { oilStationNo = config.oilStationNo, }, shiftsTime = DateTime.Now.ToString("yyyyMMdd"), shiftsMask = config.shiftsMask }; bizContent.account = order.Optional["mobilePayId"] as string; bizContent.subAppId = config.subAppId; bizContent.amount = amount; bizContent.payType = "WECHATPAY_MINIPROGRAM"; paramDic.Add("bizContent", bizContent); paramDic.Add("service", "oil.pay.apply"); paramDic.Add("sign", AllInPayAppUtil.signParam(paramDic, config.key, true)); return DoRequest(paramDic, "", config); } public TongLianResponseV2 DoCancel(PaymentOrder order) { if (string.IsNullOrEmpty(order.BillNumber)) throw new ArgumentException("Must provide BillNumber for cancel a trx from remote payment provider"); var config = (TongLianPayConfigV2)order.Config; var paramDic = BuildBasicParam(config); var bizContent = new BizContent { sysId = config.sysId, bizOrderNo = SequenceNumber.Next(), oldBizOrderNo = order.BillNumber, amount = Convert.ToInt32(order.NetAmount * 100), consumeTypes = new List() { new ConsumeType {amount = Convert.ToInt32(order.NetAmount * 100), type = "C0001"} }, shiftsTime = DateTime.Now.ToString("yyyyMMdd"), shiftsMask = config.shiftsMask }; paramDic.Add("bizContent", bizContent); paramDic.Add("service", "oil.pay.refund"); paramDic.Add("sign", AllInPayAppUtil.signParam(paramDic, config.key, true)); return DoRequest(paramDic, "", config); } public TongLianResponseV2 DoQuery(PaymentOrder order) { //String reqsn, String trxid var config = (TongLianPayConfigV2)order.Config; var paramDic = BuildBasicParam(config); var bizContent = new BizContent { sysId = config.sysId, bizOrderNo = order.BillNumber }; paramDic.Add("bizContent", bizContent); paramDic.Add("service", "oil.pay.query"); paramDic.Add("sign", AllInPayAppUtil.signParam(paramDic, config.key, true)); return DoRequest(paramDic, "", config); } private Dictionary BuildBasicParam(TongLianPayConfigV2 config) { var paramDic = new Dictionary(); paramDic.Add("appId", config.appId); paramDic.Add("version", config.version); paramDic.Add("charset", config.charset); paramDic.Add("timestamp", DateTime.Now.ToString("yyyyMMddHHmmss")); return paramDic; } private static readonly string DefaultUserAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)"; private static bool CheckValidationResult(object sender, System.Security.Cryptography.X509Certificates.X509Certificate certificate, System.Security.Cryptography.X509Certificates.X509Chain chain, System.Net.Security.SslPolicyErrors errors) { return true; //总是接受 } public String CreatePostHttpResponse(string url, Dictionary parameters, Encoding charset) { System.Net.HttpWebRequest request = null; //HTTPSQ请求 System.Net.ServicePointManager.ServerCertificateValidationCallback = new System.Net.Security.RemoteCertificateValidationCallback(CheckValidationResult); request = System.Net.WebRequest.Create(url) as System.Net.HttpWebRequest; request.ProtocolVersion = System.Net.HttpVersion.Version10; request.Method = "POST"; request.ContentType = "application/x-www-form-urlencoded"; request.UserAgent = DefaultUserAgent; //如果需要POST数据 if (!(parameters == null || parameters.Count == 0)) { StringBuilder buffer = new StringBuilder(); int i = 0; foreach (string key in parameters.Keys) { if (key == "key") { continue; } var value = parameters[key]; if (!(value is string)) { value = System.Text.Json.JsonSerializer.Serialize(value); } if (i > 0) { buffer.AppendFormat("&{0}={1}", key, value); } else { buffer.AppendFormat("{0}={1}", key, value); } i++; } var bufString = buffer.ToString(); this.logger.LogDebug($"AllInPay or AllInPayV2 CreatePostHttpResponse request: {bufString}"); var urlEncodedStr = System.Web.HttpUtility.UrlEncode(bufString); byte[] data = charset.GetBytes(buffer.ToString()); using (System.IO.Stream stream = request.GetRequestStream()) { stream.Write(data, 0, data.Length); } } System.IO.Stream outstream = request.GetResponse().GetResponseStream(); //获取响应的字符串流 System.IO.StreamReader sr = new System.IO.StreamReader(outstream); //创建一个stream读取流 return sr.ReadToEnd(); } private TongLianResponseV2 DoRequest(Dictionary param, string url, TongLianPayConfigV2 config) { var rawRsp = CreatePostHttpResponse(config.apiUrl + url, param, Encoding.UTF8); var response = (TongLianResponseV2)System.Text.Json.JsonSerializer.Deserialize(rawRsp, typeof(TongLianResponseV2)); LogResponse(response); return response; } private void LogResponse(TongLianResponseV2 response) { this.logger.LogInformation($"请求返回数据:AllInPayV2 recieved response: {response}"); } private bool IsSuccessful(TongLianResponseV2 response) { return response.code == "SUCCESS" && response.bizCode == "0000" && response.payStatus == "0000"; } private bool IsQueryResponse(TongLianResponseV2 response) { // 当结果码为“2000”时,商户系统可设置间隔时间(建议10秒)重新查询支付结果,直到支付成功或超时(建议40秒) return response.bizCode == "2000" || response.payStatus == "2000"; } private bool PayFailed(TongLianResponseV2 response) { return response.code == "SUCCESS" && (response.bizCode == "3045" || response.bizCode == "3999" || response.bizCode == "4999" || response.bizCode == "9999" || response.payStatus == "3045" || response.payStatus == "3999" || response.payStatus == "4999" || response.payStatus == "9999"); } private bool QuitQuerying(TongLianResponseV2 response) { return IsSuccessful(response) || PayFailed(response); } public async Task UnifiedOrder(PaymentOrder order) { order.TradeStatus = TradeStatusEnum.PAYERROR; var response = new TongLianResponseV2(); try { response = PlaceUnifiedOrder(order); if (IsSuccessful(response)) { order.TradeStatus = TradeStatusEnum.SUCCESS; } if (IsQueryResponse(response) && !string.IsNullOrEmpty(response.payInfo)) { order.TradeStatus = TradeStatusEnum.SUCCESS; } } catch (Exception ex) { this.logger.LogError($"Request AllInPayV2 exceptioned: {ex.ToString()}"); } return new GenericProcessResponse() { AllInPayResponseV2 = response }; } public async Task Query(PaymentOrder order, int count, int interval) { order.TradeStatus = TradeStatusEnum.PAYERROR; var response = new TongLianResponseV2(); try { // 当结果码为“2000”时,商户系统可设置间隔时间(建议10秒)重新查询支付结果,直到支付成功或超时(建议40秒) var config = (TongLianPayConfigV2)order.Config; response = await QueryTrxResult(order, config.queryInterval, config.queryTimeout); if (IsSuccessful(response)) { order.TradeStatus = TradeStatusEnum.SUCCESS; } } catch (Exception ex) { this.logger.LogError($"Request AllInPayV2 exceptioned: {ex.ToString()}"); } return new GenericProcessResponse() { AllInPayResponseV2 = response }; } } public class PeriodicTaskV2 { public static async Task Run(Func> process, Func predicate, PaymentOrder order, GenericProcessResponse response, CancellationTokenSource cts, TimeSpan period) { while (!cts.Token.IsCancellationRequested) { if (!cts.Token.IsCancellationRequested) { response = await process(order, response, cts); if (response != null && predicate(response.AllInPayResponseV2)) return; } await Task.Delay(period, cts.Token); } } } public class TongLianResponseV2 { public string amount { get; set; } public string bizCode { get; set; } public string bizMsg { get; set; } public string bizOrderNo { get; set; } public string createTime { get; set; } public string code { get; set; } public string finishTime { get; set; } public string msg { get; set; } public string oldBizOrderNo { get; set; } public string orderNo { get; set; } public string payInfo { get; set; } public string payStatus { get; set; } public string payStatusMsg { get; set; } public string payType { get; set; } public string remark { get; set; } public string shiftsMask { get; set; } public string shiftsTime { get; set; } public string sign { get; set; } public string sysId { get; set; } public string tradeDirection { get; set; } public string consumeTypes { get; set; } public override string ToString() { var sb = new StringBuilder(); PropertyInfo[] properties = typeof(TongLianResponseV2).GetProperties(); foreach (PropertyInfo property in properties) { sb.Append($"{property.Name}: {property.GetValue(this)}\n"); } return sb.ToString(); } } public class BizContent { public string account { get; set; } public long amount { get; set; } public string authCode { get; set; } public string bizOrderNo { get; set; } public List consumeTypes { get; set; } public OilStation oilStation { get; set; } public string oldBizOrderNo { get; set; } public string payType { get; set; } public string shiftsMask { get; set; } public string shiftsTime { get; set; } public string subAppId { get; set; } public string sysId { get; set; } } public class ConsumeType { public int amount { get; set; } public string type { get; set; } } public class OilStation { //public string oilGunNo { get; set; } public string oilStationNo { get; set; } //public string oilStationPersonNo { get; set; } } }