Client.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using System.Web;
  5. using System.IO;
  6. using System.Threading.Tasks;
  7. using Newtonsoft.Json;
  8. using Alipay.EasySDK.Kernel.Util;
  9. using Tea;
  10. namespace Alipay.EasySDK.Kernel
  11. {
  12. /// <summary>
  13. /// Tea DSL编排所需实现的原子方法
  14. /// </summary>
  15. public class Client
  16. {
  17. /// <summary>
  18. /// 构造成本较高的一些参数缓存在上下文中
  19. /// </summary>
  20. private readonly Context context;
  21. /// <summary>
  22. /// 注入的可选额外文本参数集合
  23. /// </summary>
  24. private readonly Dictionary<string, string> optionalTextParams = new Dictionary<string, string>();
  25. /// <summary>
  26. /// 注入的可选业务参数集合
  27. /// </summary>
  28. private readonly Dictionary<string, object> optionalBizParams = new Dictionary<string, object>();
  29. /// <summary>
  30. /// 构造函数
  31. /// </summary>
  32. /// <param name="context">上下文对象</param>
  33. public Client(Context context)
  34. {
  35. this.context = context;
  36. }
  37. /// <summary>
  38. /// 注入额外文本参数
  39. /// </summary>
  40. /// <param name="key">参数名称</param>
  41. /// <param name="value">参数的值</param>
  42. /// <returns>本客户端本身,便于链路调用</returns>
  43. public Client InjectTextParam(String key, String value)
  44. {
  45. optionalTextParams.Add(key, value);
  46. return this;
  47. }
  48. /// <summary>
  49. /// 注入额外业务参数
  50. /// </summary>
  51. /// <param name="key">参数名称</param>
  52. /// <param name="value">参数的值</param>
  53. /// <returns>本客户端本身,便于链式调用</returns>
  54. public Client InjectBizParam(String key, Object value)
  55. {
  56. optionalBizParams.Add(key, value);
  57. return this;
  58. }
  59. /// <summary>
  60. /// 获取Config中的配置项
  61. /// </summary>
  62. /// <param name="key">配置项的名称</param>
  63. /// <returns>配置项的值</returns>
  64. public string GetConfig(string key)
  65. {
  66. return context.GetConfig(key);
  67. }
  68. /// <summary>
  69. /// 是否是证书模式
  70. /// </summary>
  71. /// <returns>true:是;false:不是</returns>
  72. public bool IsCertMode()
  73. {
  74. return context.CertEnvironment != null;
  75. }
  76. /// <summary>
  77. /// 获取时间戳,格式yyyy-MM-dd HH:mm:ss
  78. /// </summary>
  79. /// <returns>当前时间戳</returns>
  80. public string GetTimestamp()
  81. {
  82. return DateTime.UtcNow.AddHours(8).ToString("yyyy-MM-dd HH:mm:ss");
  83. }
  84. /// <summary>
  85. /// 计算签名,注意要去除key或value为null的键值对
  86. /// </summary>
  87. /// <param name="systemParams">系统参数集合</param>
  88. /// <param name="bizParams">业务参数集合</param>
  89. /// <param name="textParams">其他额外文本参数集合</param>
  90. /// <param name="privateKey">私钥</param>
  91. /// <returns>签名值的Base64串</returns>
  92. public string Sign(Dictionary<string, string> systemParams, Dictionary<string, object> bizParams,
  93. Dictionary<string, string> textParams, string privateKey)
  94. {
  95. IDictionary<string, string> sortedMap = GetSortedMap(systemParams, bizParams, textParams);
  96. StringBuilder content = new StringBuilder();
  97. foreach (var pair in sortedMap)
  98. {
  99. if (!string.IsNullOrEmpty(pair.Key) && !string.IsNullOrEmpty(pair.Value))
  100. {
  101. content.Append(pair.Key).Append("=").Append(pair.Value).Append("&");
  102. }
  103. }
  104. if (content.Length > 0)
  105. {
  106. //去除尾巴上的&
  107. content.Remove(content.Length - 1, 1);
  108. }
  109. return Signer.Sign(content.ToString(), privateKey);
  110. }
  111. private IDictionary<string, string> GetSortedMap(Dictionary<string, string> systemParams,
  112. Dictionary<string, object> bizParams, Dictionary<string, string> textParams)
  113. {
  114. AddOtherParams(textParams, bizParams);
  115. IDictionary<string, string> sortedMap = new SortedDictionary<string, string>(systemParams, StringComparer.Ordinal);
  116. if (bizParams != null && bizParams.Count != 0)
  117. {
  118. sortedMap.Add(AlipayConstants.BIZ_CONTENT_FIELD, JsonUtil.ToJsonString(bizParams));
  119. }
  120. if (textParams != null)
  121. {
  122. foreach (var pair in textParams)
  123. {
  124. sortedMap.Add(pair.Key, pair.Value);
  125. }
  126. }
  127. SetNotifyUrl(sortedMap);
  128. return sortedMap;
  129. }
  130. private void SetNotifyUrl(IDictionary<string, string> paramters)
  131. {
  132. if (GetConfig(AlipayConstants.NOTIFY_URL_CONFIG_KEY) != null && !paramters.ContainsKey(AlipayConstants.NOTIFY_URL_FIELD))
  133. {
  134. paramters.Add(AlipayConstants.NOTIFY_URL_FIELD, GetConfig(AlipayConstants.NOTIFY_URL_CONFIG_KEY));
  135. }
  136. }
  137. /// <summary>
  138. /// 获取商户应用公钥证书序列号,从证书模式运行时环境对象中直接读取
  139. /// </summary>
  140. /// <returns>商户应用公钥证书序列号</returns>
  141. public string GetMerchantCertSN()
  142. {
  143. if (context.CertEnvironment == null)
  144. {
  145. return null;
  146. }
  147. return context.CertEnvironment.MerchantCertSN;
  148. }
  149. /// <summary>
  150. /// 获取支付宝根证书序列号,从证书模式运行时环境对象中直接读取
  151. /// </summary>
  152. /// <returns>支付宝根证书序列号</returns>
  153. public string GetAlipayRootCertSN()
  154. {
  155. if (context.CertEnvironment == null)
  156. {
  157. return null;
  158. }
  159. return context.CertEnvironment.RootCertSN;
  160. }
  161. /// <summary>
  162. /// 将业务参数和其他额外文本参数按www-form-urlencoded格式转换成HTTP Body中的字节数组,注意要做URL Encode
  163. /// </summary>
  164. /// <param name="bizParams">业务参数</param>
  165. /// <returns>HTTP Body中的字节数组</returns>
  166. public byte[] ToUrlEncodedRequestBody(Dictionary<string, object> bizParams)
  167. {
  168. IDictionary<string, string> sortedMap = GetSortedMap(new Dictionary<string, string>(), bizParams, null);
  169. return AlipayConstants.DEFAULT_CHARSET.GetBytes(BuildQueryString(sortedMap));
  170. }
  171. private string BuildQueryString(IDictionary<string, string> sortedMap)
  172. {
  173. StringBuilder content = new StringBuilder();
  174. int index = 0;
  175. foreach (var pair in sortedMap)
  176. {
  177. if (!string.IsNullOrEmpty(pair.Key) && !string.IsNullOrEmpty(pair.Value))
  178. {
  179. content.Append(index == 0 ? "" : "&")
  180. .Append(pair.Key)
  181. .Append("=")
  182. .Append(HttpUtility.UrlEncode(pair.Value, AlipayConstants.DEFAULT_CHARSET));
  183. index++;
  184. }
  185. }
  186. return content.ToString();
  187. }
  188. /// <summary>
  189. /// 生成随机分界符,用于multipart格式的HTTP请求Body的多个字段间的分隔
  190. /// </summary>
  191. /// <returns>随机分界符</returns>
  192. public string GetRandomBoundary()
  193. {
  194. return DateTime.Now.Ticks.ToString("X");
  195. }
  196. /// <summary>
  197. /// 字符串拼接
  198. /// </summary>
  199. /// <param name="a">字符串a</param>
  200. /// <param name="b">字符串b</param>
  201. /// <returns>字符串a和b拼接后的字符串</returns>
  202. public string ConcatStr(string a, string b)
  203. {
  204. return a + b;
  205. }
  206. /// <summary>
  207. /// 将其他额外文本参数和文件参数按multipart/form-data格式转换成HTTP Body中的字节数组流
  208. /// </summary>
  209. /// <param name="textParams">其他额外文本参数</param>
  210. /// <param name="fileParams">业务文件参数</param>
  211. /// <param name="boundary">HTTP Body中multipart格式的分隔符</param>
  212. /// <returns>Multipart格式的字节流</returns>
  213. public Stream ToMultipartRequestBody(Dictionary<string, string> textParams, Dictionary<string, string> fileParams, string boundary)
  214. {
  215. MemoryStream stream = new MemoryStream();
  216. //补充其他额外参数
  217. AddOtherParams(textParams, null);
  218. foreach (var pair in textParams)
  219. {
  220. if (!string.IsNullOrEmpty(pair.Key) && !string.IsNullOrEmpty(pair.Value))
  221. {
  222. MultipartUtil.WriteToStream(stream, MultipartUtil.GetEntryBoundary(boundary));
  223. MultipartUtil.WriteToStream(stream, MultipartUtil.GetTextEntry(pair.Key, pair.Value));
  224. }
  225. }
  226. //组装文件参数
  227. foreach (var pair in fileParams)
  228. {
  229. if (!string.IsNullOrEmpty(pair.Key) && pair.Value != null)
  230. {
  231. MultipartUtil.WriteToStream(stream, MultipartUtil.GetEntryBoundary(boundary));
  232. MultipartUtil.WriteToStream(stream, MultipartUtil.GetFileEntry(pair.Key, pair.Value));
  233. MultipartUtil.WriteToStream(stream, File.ReadAllBytes(pair.Value));
  234. }
  235. }
  236. //添加结束标记
  237. MultipartUtil.WriteToStream(stream, MultipartUtil.GetEndBoundary(boundary));
  238. stream.Seek(0, SeekOrigin.Begin);
  239. return stream;
  240. }
  241. /// <summary>
  242. /// 将网关响应发序列化成Map,同时将API的接口名称和响应原文插入到响应Map的method和body字段中
  243. /// </summary>
  244. /// <param name="response">HTTP响应</param>
  245. /// <param name="method">调用的OpenAPI的接口名称</param>
  246. /// <returns>响应反序列化的Map</returns>
  247. public Dictionary<string, object> ReadAsJson(TeaResponse response, string method)
  248. {
  249. string responseBody = TeaCore.GetResponseBody(response);
  250. Dictionary<string, object> dictionary = JsonConvert.DeserializeObject<Dictionary<string, object>>(responseBody);
  251. dictionary.Add(AlipayConstants.BODY_FIELD, responseBody);
  252. dictionary.Add(AlipayConstants.METHOD_FIELD, method);
  253. return DictionaryUtil.ObjToDictionary(dictionary);
  254. }
  255. /// <summary>
  256. /// 适配Tea DSL自动生成的代码
  257. /// </summary>
  258. public async Task<Dictionary<string, object>> ReadAsJsonAsync(TeaResponse response, string method)
  259. {
  260. return ReadAsJson(response, method);
  261. }
  262. /// <summary>
  263. /// 从响应Map中提取支付宝公钥证书序列号
  264. /// </summary>
  265. /// <param name="respMap">响应Map</param>
  266. /// <returns>支付宝公钥证书序列号</returns>
  267. public string GetAlipayCertSN(Dictionary<string, object> respMap)
  268. {
  269. return (string)respMap[AlipayConstants.ALIPAY_CERT_SN_FIELD];
  270. }
  271. /// <summary>
  272. /// 获取支付宝公钥,从证书运行时环境对象中直接读取
  273. /// 如果缓存的用户指定的支付宝公钥证书的序列号与网关响应中携带的支付宝公钥证书序列号不一致,需要报错给出提示或自动更新支付宝公钥证书
  274. /// </summary>
  275. /// <param name="alipayCertSN">网关响应中携带的支付宝公钥证书序列号</param>
  276. /// <returns>支付宝公钥</returns>
  277. public string ExtractAlipayPublicKey(string alipayCertSN)
  278. {
  279. if (context.CertEnvironment == null)
  280. {
  281. return null;
  282. }
  283. return context.CertEnvironment.GetAlipayPublicKey(alipayCertSN);
  284. }
  285. /// <summary>
  286. /// 验证签名
  287. /// </summary>
  288. /// <param name="respMap">响应Map,可以从中提取出sign和body</param>
  289. /// <param name="alipayPublicKey">支付宝公钥</param>
  290. /// <returns>true:验签通过;false:验签不通过</returns>
  291. public bool Verify(Dictionary<string, object> respMap, string alipayPublicKey)
  292. {
  293. string sign = (string)respMap[AlipayConstants.SIGN_FIELD];
  294. string content = SignContentExtractor.GetSignSourceData((string)respMap[AlipayConstants.BODY_FIELD],
  295. (string)respMap[AlipayConstants.METHOD_FIELD]);
  296. return Signer.Verify(content, sign, alipayPublicKey);
  297. }
  298. /// <summary>
  299. /// 从响应Map中提取返回值对象的Map,并将响应原文插入到body字段中
  300. /// </summary>
  301. /// <param name="respMap">响应Map</param>
  302. /// <returns>返回值对象Map</returns>
  303. public Dictionary<string, object> ToRespModel(Dictionary<string, object> respMap)
  304. {
  305. string methodName = (string)respMap[AlipayConstants.METHOD_FIELD];
  306. string responseNodeName = methodName.Replace('.', '_') + AlipayConstants.RESPONSE_SUFFIX;
  307. string errorNodeName = AlipayConstants.ERROR_RESPONSE;
  308. //先找正常响应节点
  309. foreach (var pair in respMap)
  310. {
  311. if (responseNodeName.Equals(pair.Key))
  312. {
  313. Dictionary<string, object> model = (Dictionary<string, object>)pair.Value;
  314. model.Add(AlipayConstants.BODY_FIELD, respMap[AlipayConstants.BODY_FIELD]);
  315. return model;
  316. }
  317. }
  318. //再找异常响应节点
  319. foreach (var pair in respMap)
  320. {
  321. if (errorNodeName.Equals(pair.Key))
  322. {
  323. Dictionary<string, object> model = (Dictionary<string, object>)pair.Value;
  324. model.Add(AlipayConstants.BODY_FIELD, respMap[AlipayConstants.BODY_FIELD]);
  325. return model;
  326. }
  327. }
  328. throw new Exception("响应格式不符合预期,找不到" + responseNodeName + "节点");
  329. }
  330. /// <summary>
  331. /// 生成页面类请求所需URL或Form表单
  332. /// </summary>
  333. /// <param name="method">GET或POST,决定是生成URL还是Form表单</param>
  334. /// <param name="systemParams">系统参数集合</param>
  335. /// <param name="bizParams">业务参数集合</param>
  336. /// <param name="textParams">其他额外文本参数集合</param>
  337. /// <param name="sign">所有参数的签名值</param>
  338. /// <returns>生成的URL字符串或表单</returns>
  339. public string GeneratePage(string method, Dictionary<string, string> systemParams, Dictionary<string, object> bizParams,
  340. Dictionary<string, string> textParams, string sign)
  341. {
  342. if (AlipayConstants.GET.Equals(method))
  343. {
  344. //采集并排序所有参数
  345. IDictionary<string, string> sortedMap = GetSortedMap(systemParams, bizParams, textParams);
  346. sortedMap.Add(AlipayConstants.SIGN_FIELD, sign);
  347. //将所有参数置于URL中
  348. return GetGatewayServerUrl() + "?" + BuildQueryString(sortedMap);
  349. }
  350. else if (AlipayConstants.POST.Equals(method))
  351. {
  352. //将系统参数、额外文本参数排序后置于URL中
  353. IDictionary<string, string> urlParams = GetSortedMap(systemParams, null, textParams);
  354. urlParams.Add(AlipayConstants.SIGN_FIELD, sign);
  355. string actionUrl = GetGatewayServerUrl() + "?" + BuildQueryString(urlParams);
  356. //将业务参数排序后置于form表单中
  357. AddOtherParams(null, bizParams);
  358. IDictionary<string, string> formParams = new SortedDictionary<string, string>()
  359. {
  360. { AlipayConstants.BIZ_CONTENT_FIELD, JsonUtil.ToJsonString(bizParams)}
  361. };
  362. return PageUtil.BuildForm(actionUrl, formParams);
  363. }
  364. else
  365. {
  366. throw new Exception("_generatePage中method只支持传入GET或POST");
  367. }
  368. }
  369. /// <summary>
  370. /// 生成订单串
  371. /// </summary>
  372. /// <param name="systemParams">系统参数集合</param>
  373. /// <param name="bizParams">业务参数集合</param>
  374. /// <param name="textParams">其他文本参数集合</param>
  375. /// <param name="sign">所有参数的签名值</param>
  376. /// <returns>订单串</returns>
  377. public string GenerateOrderString(Dictionary<string, string> systemParams, Dictionary<string, object> bizParams,
  378. Dictionary<string, string> textParams, string sign)
  379. {
  380. //采集并排序所有参数
  381. IDictionary<string, string> sortedMap = GetSortedMap(systemParams, bizParams, textParams);
  382. sortedMap.Add(AlipayConstants.SIGN_FIELD, sign);
  383. //将所有参数置于URL中
  384. return BuildQueryString(sortedMap);
  385. }
  386. private string GetGatewayServerUrl()
  387. {
  388. return GetConfig(AlipayConstants.PROTOCOL_CONFIG_KEY) + "://" + GetConfig(AlipayConstants.HOST_CONFIG_KEY) + "/gateway.do";
  389. }
  390. /// <summary>
  391. /// AES加密
  392. /// </summary>
  393. /// <param name="plainText">明文</param>
  394. /// <param name="key">密钥</param>
  395. /// <returns>密文</returns>
  396. public string AesEncrypt(string plainText, string key)
  397. {
  398. return AES.Encrypt(plainText, key);
  399. }
  400. /// <summary>
  401. /// AES解密
  402. /// </summary>
  403. /// <param name="chiperText">密文</param>
  404. /// <param name="key">密钥</param>
  405. /// <returns>明文</returns>
  406. public string AesDecrypt(string chiperText, string key)
  407. {
  408. return AES.Decrypt(chiperText, key);
  409. }
  410. /// <summary>
  411. /// 对支付类请求的异步通知的参数集合进行验签
  412. /// </summary>
  413. /// <param name="parameters">参数集合</param>
  414. /// <param name="alipayPublicKey">支付宝公钥</param>
  415. /// <returns>true:验证成功;false:验证失败</returns>
  416. public bool VerifyParams(Dictionary<string, string> parameters, string alipayPublicKey)
  417. {
  418. return Signer.VerifyParams(parameters, alipayPublicKey);
  419. }
  420. /// <summary>
  421. /// 获取SDK版本信息
  422. /// </summary>
  423. /// <returns>SDK版本信息</returns>
  424. public string GetSdkVersion()
  425. {
  426. return context.SdkVersion;
  427. }
  428. /// <summary>
  429. /// 将随机顺序的Map转换为有序的Map
  430. /// </summary>
  431. /// <param name="input">随机顺序的Map</param>
  432. /// <returns>有序的Map</returns>
  433. public Dictionary<string, string> SortMap(Dictionary<string, string> input)
  434. {
  435. //GO语言的Map是随机顺序的,每次访问顺序都不同,才需排序
  436. return input;
  437. }
  438. private void AddOtherParams(Dictionary<string, string> textParams, Dictionary<string, object> bizParams)
  439. {
  440. //为null表示此处不是扩展此类参数的时机
  441. if (textParams != null)
  442. {
  443. foreach (var pair in optionalTextParams)
  444. {
  445. if (!textParams.ContainsKey(pair.Key))
  446. {
  447. textParams.Add(pair.Key, pair.Value);
  448. }
  449. }
  450. SetNotifyUrl(textParams);
  451. }
  452. //为null表示此处不是扩展此类参数的时机
  453. if (bizParams != null)
  454. {
  455. foreach (var pair in optionalBizParams)
  456. {
  457. if (!bizParams.ContainsKey(pair.Key))
  458. {
  459. bizParams.Add(pair.Key, pair.Value);
  460. }
  461. }
  462. }
  463. }
  464. }
  465. }