├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
└── src
├── WeChatRedPacketSample.sln
└── WeChatRedPacketSample
├── App.config
├── CertificateStoreFinder.cs
├── DebugWeChatClient.cs
├── ICertificateFinder.cs
├── IWeChatClient.cs
├── Program.cs
├── Properties
└── AssemblyInfo.cs
├── RedPacket.cs
├── RedPacketSentError.cs
├── RedPacketSentResult.cs
├── RedPacketService.cs
├── WeChatClient.cs
├── WeChatRedPacketSample.csproj
└── packages.config
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.sln.docstates
8 |
9 | # Build results
10 | [Dd]ebug/
11 | [Dd]ebugPublic/
12 | [Rr]elease/
13 | [Rr]eleases/
14 | x64/
15 | x86/
16 | build/
17 | bld/
18 | [Bb]in/
19 | [Oo]bj/
20 |
21 | # Roslyn cache directories
22 | *.ide/
23 |
24 | # MSTest test Results
25 | [Tt]est[Rr]esult*/
26 | [Bb]uild[Ll]og.*
27 |
28 | #NUNIT
29 | *.VisualState.xml
30 | TestResult.xml
31 |
32 | # Build Results of an ATL Project
33 | [Dd]ebugPS/
34 | [Rr]eleasePS/
35 | dlldata.c
36 |
37 | *_i.c
38 | *_p.c
39 | *_i.h
40 | *.ilk
41 | *.meta
42 | *.obj
43 | *.pch
44 | *.pdb
45 | *.pgc
46 | *.pgd
47 | *.rsp
48 | *.sbr
49 | *.tlb
50 | *.tli
51 | *.tlh
52 | *.tmp
53 | *.tmp_proj
54 | *.log
55 | *.vspscc
56 | *.vssscc
57 | .builds
58 | *.pidb
59 | *.svclog
60 | *.scc
61 |
62 | # Chutzpah Test files
63 | _Chutzpah*
64 |
65 | # Visual C++ cache files
66 | ipch/
67 | *.aps
68 | *.ncb
69 | *.opensdf
70 | *.sdf
71 | *.cachefile
72 |
73 | # Visual Studio profiler
74 | *.psess
75 | *.vsp
76 | *.vspx
77 |
78 | # TFS 2012 Local Workspace
79 | $tf/
80 |
81 | # Guidance Automation Toolkit
82 | *.gpState
83 |
84 | # ReSharper is a .NET coding add-in
85 | _ReSharper*/
86 | *.[Rr]e[Ss]harper
87 | *.DotSettings.user
88 |
89 | # JustCode is a .NET coding addin-in
90 | .JustCode
91 |
92 | # TeamCity is a build add-in
93 | _TeamCity*
94 |
95 | # DotCover is a Code Coverage Tool
96 | *.dotCover
97 |
98 | # NCrunch
99 | _NCrunch_*
100 | .*crunch*.local.xml
101 |
102 | # MightyMoose
103 | *.mm.*
104 | AutoTest.Net/
105 |
106 | # Web workbench (sass)
107 | .sass-cache/
108 |
109 | # Installshield output folder
110 | [Ee]xpress/
111 |
112 | # DocProject is a documentation generator add-in
113 | DocProject/buildhelp/
114 | DocProject/Help/*.HxT
115 | DocProject/Help/*.HxC
116 | DocProject/Help/*.hhc
117 | DocProject/Help/*.hhk
118 | DocProject/Help/*.hhp
119 | DocProject/Help/Html2
120 | DocProject/Help/html
121 |
122 | # Click-Once directory
123 | publish/
124 |
125 | # Publish Web Output
126 | *.[Pp]ublish.xml
127 | *.azurePubxml
128 | # TODO: Comment the next line if you want to checkin your web deploy settings
129 | # but database connection strings (with potential passwords) will be unencrypted
130 | *.pubxml
131 | *.publishproj
132 |
133 | # NuGet Packages
134 | *.nupkg
135 | # The packages folder can be ignored because of Package Restore
136 | **/packages/*
137 | # except build/, which is used as an MSBuild target.
138 | !**/packages/build/
139 | # If using the old MSBuild-Integrated Package Restore, uncomment this:
140 | #!**/packages/repositories.config
141 |
142 | # Windows Azure Build Output
143 | csx/
144 | *.build.csdef
145 |
146 | # Windows Store app package directory
147 | AppPackages/
148 |
149 | # Others
150 | sql/
151 | *.Cache
152 | ClientBin/
153 | [Ss]tyle[Cc]op.*
154 | ~$*
155 | *~
156 | *.dbmdl
157 | *.dbproj.schemaview
158 | *.pfx
159 | *.publishsettings
160 | node_modules/
161 |
162 | # RIA/Silverlight projects
163 | Generated_Code/
164 |
165 | # Backup & report files from converting an old project file
166 | # to a newer Visual Studio version. Backup files are not needed,
167 | # because we have git ;-)
168 | _UpgradeReport_Files/
169 | Backup*/
170 | UpgradeLog*.XML
171 | UpgradeLog*.htm
172 |
173 | # SQL Server files
174 | *.mdf
175 | *.ldf
176 |
177 | # Business Intelligence projects
178 | *.rdl.data
179 | *.bim.layout
180 | *.bim_*.settings
181 |
182 | # Microsoft Fakes
183 | FakesAssemblies/
184 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 温海雄
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WeChatRedPacketSample
2 | 微信现金红包接口示例
3 |
--------------------------------------------------------------------------------
/src/WeChatRedPacketSample.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 2013
4 | VisualStudioVersion = 12.0.31101.0
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WeChatRedPacketSample", "WeChatRedPacketSample\WeChatRedPacketSample.csproj", "{ACA5A9D9-B510-44AC-8E91-E78E0F71C745}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {ACA5A9D9-B510-44AC-8E91-E78E0F71C745}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {ACA5A9D9-B510-44AC-8E91-E78E0F71C745}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {ACA5A9D9-B510-44AC-8E91-E78E0F71C745}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {ACA5A9D9-B510-44AC-8E91-E78E0F71C745}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | EndGlobal
23 |
--------------------------------------------------------------------------------
/src/WeChatRedPacketSample/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/WeChatRedPacketSample/CertificateStoreFinder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Security.Cryptography.X509Certificates;
3 |
4 | namespace WeChatRedPacketSample
5 | {
6 | public class CertificateStoreFinder : ICertificateFinder
7 | {
8 | string m_SubjectDistinguishedName;
9 |
10 | public CertificateStoreFinder(string subjectDistinguishedName)
11 | {
12 | if (subjectDistinguishedName == null)
13 | throw new ArgumentNullException("subjectDistinguishedName");
14 |
15 | m_SubjectDistinguishedName = subjectDistinguishedName;
16 | }
17 |
18 | public X509Certificate2 Find()
19 | {
20 | var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
21 | store.Open(OpenFlags.ReadOnly);
22 | //你可以采用别的方式来寻找证书,只要能找到就可以。
23 | var certs = store.Certificates.Find(X509FindType.FindBySubjectDistinguishedName, m_SubjectDistinguishedName, false);
24 | if (certs.Count == 0)
25 | throw new Exception("无法找到微信支付证书");
26 |
27 | return certs[0];
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/WeChatRedPacketSample/DebugWeChatClient.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | namespace WeChatRedPacketSample
4 | {
5 | public class DebugWeChatClient : IWeChatClient
6 | {
7 | public static readonly string DefaultResponse = @"
8 | DebugWeChatClient
9 |
10 |
11 |
12 |
13 |
14 |
15 | 1233006302
16 |
17 |
18 | 375
19 | ";
20 | string m_Result;
21 |
22 | public DebugWeChatClient(string result = null)
23 | {
24 | m_Result = result ?? DefaultResponse;
25 | }
26 |
27 | public Task PostAsync(string data)
28 | {
29 | return Task.FromResult(m_Result);
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/WeChatRedPacketSample/ICertificateFinder.cs:
--------------------------------------------------------------------------------
1 | using System.Security.Cryptography.X509Certificates;
2 |
3 | namespace WeChatRedPacketSample
4 | {
5 | public interface ICertificateFinder
6 | {
7 | X509Certificate2 Find();
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/WeChatRedPacketSample/IWeChatClient.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | namespace WeChatRedPacketSample
4 | {
5 | public interface IWeChatClient
6 | {
7 | Task PostAsync(string data);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/WeChatRedPacketSample/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace WeChatRedPacketSample
4 | {
5 | class Program
6 | {
7 | static void Main(string[] args)
8 | {
9 | //微信支付商户号
10 | var merchantId = "1234567890";
11 | //支付密钥,不是公众号/服务号的密钥,在账户设置-安全设置-API安全中配置。
12 | var payKey = "60BC9982-D893-4AB7-BE8A-FEC3B38FB042";
13 | //这一行是模拟发送
14 | var service = new RedPacketService(merchantId, payKey, new DebugWeChatClient());
15 | //这里是真正发起API调用。
16 | //subjectDistinguishedName是微信提供下载安装的X509证书的主题可分辨名称
17 | //格式严格类似: "SN=10000000, CN=深圳XXXX科技有限公司, OU=MMPay, O=Tencent, L=Shenzhen, S=Guangdong, C=CN"
18 | //注意,CertificateStoreFinder的实现要求证书必须安装在当前用户(应用程序启动的身份)下的个人类别中。
19 | //不了解的请启动命令行,输入certmgr回车,左边树形列表第一个就是。
20 | //当然你也可以安装在其他地方,修改CertificateStoreFinder的实现。
21 | //如果无法在托管系统中安装证书,可以采用微信官方Demo中文件形式访问,这里不再重复。
22 | //基于安全的考虑,墙裂建议把证书安装在证书容器中,并在安装时设置为私钥不可导出。
23 | //var subjectDistinguishedName = "xxxxx";
24 | //var service = new RedPacketService(merchantId, payKey, new WeChatClient(new CertificateStoreFinder(subjectDistinguishedName)));
25 | var redPacket = new RedPacket
26 | {
27 | ActName = "测试",
28 | Amount = 120,
29 | AppId = "1234567890", //微信/服务公众号Id,通过这个公众/服务号下行红包通知,如果用户没关注,通过服务消息(特殊服务号)下行。
30 | //订单号格式: 商户号 + 4位年 + 2位月 + 2位日 + 10位自然日内唯一数字。
31 | BillNumber = merchantId + DateTime.Now.ToString("yyyyMMdd") + GetUniqueId().PadLeft(10, '0'),
32 | IpAddress = "127.0.0.1", //触发操作的客户的IP地址
33 | OpenId = "xxxxxxxxxx", //触发红包的用户在微信号内的Id。
34 | Remark = "备注",
35 | SendName = "显示在红包上的发送者名称",
36 | Wishing = "祝福的话"
37 | };
38 | var result = service.SendAsync(redPacket).Result; //控制台程序可以使用Result属性来访问异步Task结果,在Asp.Net和WinForm、WPF中不要这么做。
39 | var message = string.Format("{0}, {1}", result.Succeeded ? "成功" : "失败", result.Error);
40 | Console.WriteLine(message);
41 | }
42 |
43 | static string GetUniqueId()
44 | {
45 | //可以通过数据库来生成唯一的ID,这个唯一ID不要求全局唯一,只要求在一个自然日内唯一。
46 | //这里用随机数模拟。
47 | var r = new Random();
48 | return r.Next(1, 1000000000).ToString();
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/WeChatRedPacketSample/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("WeChatRedPacketSample")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("WeChatRedPacketSample")]
13 | [assembly: AssemblyCopyright("Copyright © 2015")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid("10ce5da3-f92f-4849-bceb-03193ab70aef")]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Build and Revision Numbers
33 | // by using the '*' as shown below:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("1.0.0.0")]
36 | [assembly: AssemblyFileVersion("1.0.0.0")]
37 |
--------------------------------------------------------------------------------
/src/WeChatRedPacketSample/RedPacket.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace WeChatRedPacketSample
3 | {
4 | public class RedPacket
5 | {
6 | public string BillNumber { get; set; }
7 | public string SendName { get; set; }
8 | public string ActName { get; set; }
9 | public string Wishing { get; set; }
10 | public string Remark { get; set; }
11 | public string AppId { get; set; }
12 | public string OpenId { get; set; }
13 | public string IpAddress { get; set; }
14 | public int Amount { get; set; }
15 |
16 | public bool IsValid() //可以用ModelBinding机制来替换这个。
17 | {
18 | return !string.IsNullOrWhiteSpace(BillNumber) &&
19 | !string.IsNullOrWhiteSpace(SendName) &&
20 | !string.IsNullOrWhiteSpace(ActName) &&
21 | !string.IsNullOrWhiteSpace(Wishing) &&
22 | !string.IsNullOrWhiteSpace(Remark) &&
23 | !string.IsNullOrWhiteSpace(AppId) &&
24 | !string.IsNullOrWhiteSpace(OpenId) &&
25 | !string.IsNullOrWhiteSpace(IpAddress) &&
26 | ((Amount == 0) || (Amount != 0 && Amount >= 100));
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/WeChatRedPacketSample/RedPacketSentError.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace WeChatRedPacketSample
3 | {
4 | public enum RedPacketSentError : byte
5 | {
6 | None = 0,
7 | InternalError = 1,
8 | BalanceNotEnough = 2,
9 | Other = 128
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/WeChatRedPacketSample/RedPacketSentResult.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace WeChatRedPacketSample
3 | {
4 | public class RedPacketSentResult
5 | {
6 | public bool Succeeded { get; set; }
7 |
8 | public string Response { get; set; }
9 |
10 | public RedPacketSentError Error { get; set; }
11 |
12 | public int Amount { get; set; }
13 |
14 | public string BillNumber { get; set; }
15 |
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/WeChatRedPacketSample/RedPacketService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Net;
4 | using System.Net.Security;
5 | using System.Security.Cryptography;
6 | using System.Security.Cryptography.X509Certificates;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 | using System.Xml.Linq;
10 |
11 | namespace WeChatRedPacketSample
12 | {
13 | public class RedPacketService
14 | {
15 | static readonly Random _Random = new Random();
16 | readonly string m_PayKey;
17 | readonly string m_MerchantId;
18 | readonly IWeChatClient m_WeChatClient;
19 |
20 | static RedPacketService()
21 | {
22 | ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(CheckValidationResult);
23 | }
24 |
25 | public RedPacketService(string merchantId, string payKey, IWeChatClient client)
26 | {
27 | if (merchantId == null)
28 | throw new ArgumentNullException("merchantId");
29 | if (payKey == null)
30 | throw new ArgumentNullException("payKey");
31 | if (client == null)
32 | throw new ArgumentNullException("client");
33 |
34 | m_PayKey = payKey;
35 | m_WeChatClient = client;
36 | }
37 |
38 | public async Task SendAsync(RedPacket request)
39 | {
40 | if (request == null)
41 | throw new ArgumentNullException("request");
42 | if (!request.IsValid())
43 | throw new ArgumentException("request");
44 |
45 | var amount = GetAmount(request.Amount);
46 | var amountString = amount.ToString();
47 | var sendData = new SortedList(StringComparer.Ordinal);
48 | sendData.Add("nonce_str", Guid.NewGuid().ToString("N"));
49 | sendData.Add("mch_billno", request.BillNumber);
50 | sendData.Add("mch_id", m_MerchantId);
51 | sendData.Add("wxappid", request.AppId);
52 | sendData.Add("nick_name", request.SendName); //简单起见使用send_name。
53 | sendData.Add("send_name", request.SendName);
54 | sendData.Add("re_openid", request.OpenId);
55 | sendData.Add("total_amount", amountString);
56 | sendData.Add("min_value", amountString);
57 | sendData.Add("max_value", amountString);
58 | sendData.Add("total_num", "1");
59 | sendData.Add("wishing", request.Wishing);
60 | sendData.Add("client_ip", request.IpAddress);
61 | sendData.Add("act_name", request.ActName);
62 | sendData.Add("remark", request.Remark);
63 | var xml = DictionaryToXml(sendData);
64 |
65 | var result = await m_WeChatClient.PostAsync(xml);
66 | var result2 = Parse(result);
67 | result2.Amount = amount;
68 | result2.BillNumber = request.BillNumber;
69 | return result2;
70 | }
71 |
72 | public RedPacketSentResult Parse(string result)
73 | {
74 | try
75 | {
76 | var xml = XDocument.Parse(result).Root;
77 | var returnCode = xml.Element("return_code").Value;
78 | var balanceNotEnough = string.Equals(xml.Element("err_code").Value, "NOTENOUGH", StringComparison.OrdinalIgnoreCase);
79 | var error = balanceNotEnough ? RedPacketSentError.BalanceNotEnough : RedPacketSentError.Other;
80 | if (!string.Equals(returnCode, "SUCCESS", StringComparison.OrdinalIgnoreCase))
81 | return new RedPacketSentResult { Succeeded = false, Response = result, Error = error };
82 |
83 | var resultCode = xml.Element("result_code").Value;
84 | if (!string.Equals(resultCode, "SUCCESS", StringComparison.OrdinalIgnoreCase))
85 | return new RedPacketSentResult { Succeeded = false, Response = result, Error = error };
86 |
87 | return new RedPacketSentResult { Succeeded = true, Response = result }; ;
88 | }
89 | catch (Exception ex)
90 | {
91 | //Logger.WriteError("解析微信返回结果时发生错误", ex);
92 | return new RedPacketSentResult { Succeeded = false, Response = result, Error = RedPacketSentError.InternalError };
93 | }
94 | }
95 |
96 | internal int GetAmount(int amount)
97 | {
98 | var maxAmount = 20000;
99 | if (amount == 0)
100 | return _Random.Next(100, maxAmount);//微信红包必须大于100分。
101 | else
102 | {
103 | return amount > maxAmount ? maxAmount : amount;
104 | }
105 | }
106 |
107 | //默认情况下只能使用受信任的证书,此方法可以干预这个逻辑。
108 | static bool CheckValidationResult(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors)
109 | {
110 | if (errors == SslPolicyErrors.None)
111 | return true;
112 | return false;
113 | }
114 |
115 | string Sign(IDictionary dict)
116 | {
117 | StringBuilder sb = new StringBuilder();
118 | foreach (var item in dict)
119 | {
120 | if (string.IsNullOrEmpty(item.Key) || string.IsNullOrEmpty(item.Value))
121 | continue;
122 |
123 | sb.AppendFormat("{0}={1}&", item.Key, item.Value);
124 | }
125 | sb.Append("key=" + m_PayKey);
126 | var bytesToHash = Encoding.UTF8.GetBytes(sb.ToString()); //注意,必须是UTF-8。
127 | var hashResult = ComputeMD5Hash(bytesToHash);
128 | var hash = BytesToString(hashResult, false);
129 | return hash;
130 | }
131 |
132 | static byte[] ComputeMD5Hash(byte[] input)
133 | {
134 | if (input == null)
135 | throw new ArgumentNullException("input");
136 |
137 | using (var md5 = new MD5CryptoServiceProvider())
138 | {
139 | var result = md5.ComputeHash(input);
140 | return result;
141 | }
142 | }
143 |
144 | public static string BytesToString(byte[] input, bool lowercase = true)
145 | {
146 | if (input == null || input.Length == 0)
147 | return string.Empty;
148 |
149 | StringBuilder sb = new StringBuilder(input.Length * 2);
150 | for (var i = 0; i < input.Length; i++)
151 | {
152 | sb.AppendFormat(lowercase ? "{0:x2}" : "{0:X2}", input[i]);
153 | }
154 | return sb.ToString();
155 | }
156 |
157 | string DictionaryToXml(IDictionary dict)
158 | {
159 | StringBuilder sb = new StringBuilder();
160 | sb.AppendLine("");
161 | foreach (var item in dict)
162 | {
163 | if (string.IsNullOrEmpty(item.Key) || string.IsNullOrEmpty(item.Value))
164 | continue;
165 |
166 | sb.AppendFormat("<{0}>{1}{0}>", item.Key, item.Value);
167 | }
168 | var sign = Sign(dict);
169 | sb.AppendFormat("{0}", sign);
170 | sb.AppendLine("");
171 | return sb.ToString();
172 | }
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/src/WeChatRedPacketSample/WeChatClient.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http;
3 | using System.Security.Cryptography.X509Certificates;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace WeChatRedPacketSample
8 | {
9 | public class WeChatClient : IWeChatClient
10 | {
11 | static readonly string _WeChatRedPacketApiEndpoint = "https://api.mch.weixin.qq.com/mmpaymkttransfers/sendredpack";
12 | ICertificateFinder m_CertificateFinder;
13 |
14 | public WeChatClient(ICertificateFinder finder)
15 | {
16 | if (finder == null)
17 | throw new ArgumentNullException("finder");
18 |
19 | m_CertificateFinder = finder;
20 | }
21 |
22 | public async Task PostAsync(string data)
23 | {
24 | var certHandler = new WebRequestHandler();
25 | certHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
26 | certHandler.UseDefaultCredentials = false;
27 | var cert = m_CertificateFinder.Find();
28 | certHandler.ClientCertificates.Add(cert);
29 | using (var client = new HttpClient(certHandler, true))
30 | {
31 | var response = await client.PostAsync(_WeChatRedPacketApiEndpoint, new StringContent(data, Encoding.UTF8, "application/xml"));
32 | var responseData = await response.Content.ReadAsByteArrayAsync();
33 | var result = Encoding.UTF8.GetString(responseData);
34 | return result;
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/WeChatRedPacketSample/WeChatRedPacketSample.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {ACA5A9D9-B510-44AC-8E91-E78E0F71C745}
8 | Exe
9 | Properties
10 | WeChatRedPacketSample
11 | WeChatRedPacketSample
12 | v4.5
13 | 512
14 |
15 |
16 | AnyCPU
17 | true
18 | full
19 | false
20 | bin\Debug\
21 | DEBUG;TRACE
22 | prompt
23 | 4
24 |
25 |
26 | AnyCPU
27 | pdbonly
28 | true
29 | bin\Release\
30 | TRACE
31 | prompt
32 | 4
33 |
34 |
35 |
36 | False
37 | ..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll
38 |
39 |
40 |
41 |
42 |
43 | False
44 | ..\packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
78 |
--------------------------------------------------------------------------------
/src/WeChatRedPacketSample/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------