# 微信 (18_Wechat)
最后更新: 2024-09-20
# 📚 概述
DarkM 框架的微信模块提供了完整的微信公众号/小程序开发能力。包括消息处理、菜单管理、用户管理、微信支付、JS-SDK 等功能,支持多账号管理。
源代码位置: DarkM/src/Framework/Wechat
# 🏗️ 模块架构
# 项目结构
Wechat/
├── Wechat.Abstractions/ # 抽象层(配置、模型、异常)
│ ├── WechatConfig.cs # 微信配置类
│ ├── WechatAccount.cs # 微信账号配置
│ ├── CacheKeys.cs # 缓存键定义
│ ├── FuncFlag.cs # 功能标识
│ ├── Exception/ # 异常类
│ ├── Module/ # 微信消息模型
│ │ ├── TextMessage.cs # 文本消息
│ │ ├── ImageMessage.cs # 图片消息
│ │ ├── VoiceMessage.cs # 语音消息
│ │ ├── VideoMessage.cs # 视频消息
│ │ ├── LocationMessage.cs # 位置消息
│ │ ├── LinkMessage.cs # 链接消息
│ │ └── EventMessage.cs # 事件消息
│ └── Support/ # 支持类
│ ├── JsSdkConfig.cs # JS-SDK 配置
│ ├── AccessToken.cs # 访问令牌
│ └── JsApiTicket.cs # JSAPI 票据
│
├── Wechat.Core/ # 核心实现
│ ├── WechatProcesser.cs # 微信处理器
│ ├── WechatHandleMiddleware.cs # 微信处理中间件
│ ├── WechatHelper.cs # 微信帮助类
│ ├── Commands/ # 命令处理
│ │ └── EventHandle/ # 事件处理器
│ │ ├── BaseEventHandle.cs # 基础事件处理
│ │ ├── TextEventHandle.cs # 文本事件处理
│ │ └── NotBindedHandle.cs # 未绑定处理
│ └── Support/ # 支持类
│ ├── WechatProvider.cs # 微信提供器
│ └── WechatHandler.cs # 微信处理器
│
├── Wechat.Integration/ # 集成层(DI 注册)
│ └── ServiceCollectionExtensions.cs # DI 扩展
│
└── WxPayAPI/ # 微信支付 SDK(官方)
├── lib/ # 基础库
│ ├── Config.cs # 支付配置
│ ├── WxPayApi.cs # 支付 API
│ ├── Data.cs # 数据结构
│ └── Notify.cs # 通知处理
└── business/ # 业务类
├── JsApiPay.cs # JSAPI 支付
├── NativePay.cs # 扫码支付
├── OrderQuery.cs # 订单查询
├── Refund.cs # 退款
└── RefundQuery.cs # 退款查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 🔧 核心配置类
# WechatConfig(微信配置)
public class WechatConfig
{
/// <summary>
/// 默认的 AppId
/// </summary>
public string Default { get; set; }
/// <summary>
/// 网站地址 (域名)
/// </summary>
public string WebSite { get; set; }
/// <summary>
/// 关注后跳转首页(绑定页面)
/// </summary>
public string HomePage { get; set; }
/// <summary>
/// 是否测试阶段
/// </summary>
public bool Test { get; set; }
/// <summary>
/// 是否维护状态
/// </summary>
public bool Maintenance { get; set; }
/// <summary>
/// 维护页面地址
/// </summary>
public string MaintenancePage { get; set; }
/// <summary>
/// 消息类型配置
/// 0:全部使用小 I 机器人
/// 1:白名单方式使用小 I
/// 10:禁止使用小 I
/// 20:小 I 发生错误时使用默认
/// 99:不回复任何消息
/// </summary>
public int MessageType { get; set; }
/// <summary>
/// 微信账号列表
/// </summary>
public List<WechatAccount> Servers { get; set; }
/// <summary>
/// 获取指定账号
/// </summary>
public WechatAccount GetAccount(string appId)
{
return Servers?.FirstOrDefault(s => s.AppID.Equals(appId));
}
/// <summary>
/// 获取默认账号
/// </summary>
public WechatAccount DefaultServer => GetAccount(Default);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# WechatAccount(微信账号配置)
public class WechatAccount
{
//=======【基本信息设置】=====================================
/// <summary>
/// APPID 对应的公众号/小程序名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// APPID 对应的公众号/小程序描述说明
/// </summary>
public string Label { get; set; }
/// <summary>
/// APPID:绑定支付的 APPID(必须配置)
/// </summary>
public string AppID { get; set; }
/// <summary>
/// APPSECRET:公众帐号 secert(仅 JSAPI 支付时需要)
/// </summary>
public string AppSecret { get; set; }
/// <summary>
/// MCHID:商户号(必须配置)
/// </summary>
public string MchID { get; set; }
/// <summary>
/// 商户支付密钥(必须配置)
/// </summary>
public string PayKey { get; set; }
/// <summary>
/// Token(用于验证微信服务器)
/// </summary>
public string UserToken { get; set; }
/// <summary>
/// 是否小程序账号
/// </summary>
public bool MiniApp { get; set; }
/// <summary>
/// 证书路径(仅退款、撤销订单时需要)
/// </summary>
public string SSlCertPath { get; set; }
/// <summary>
/// 证书密码
/// </summary>
public string SSlCertPassword { get; set; }
/// <summary>
/// 支付结果通知回调 URL
/// </summary>
public string NotifyUrl { get; set; }
/// <summary>
/// 退款通知地址
/// </summary>
public string RefundNotifyUrl { get; set; }
//=======【高级配置】=====================================
/// <summary>
/// APIv3 密钥(v3 接口必填)
/// </summary>
public string V3Key { get; set; }
/// <summary>
/// RSA 公钥(企业付款到银行卡时使用)
/// </summary>
public string RsaPublicKey { get; set; }
/// <summary>
/// 子商户应用号(服务商模式)
/// </summary>
public string SubAppId { get; set; }
/// <summary>
/// 子商户号(服务商模式)
/// </summary>
public string SubMchId { get; set; }
/// <summary>
/// 服务器 IP(可自动获取)
/// </summary>
public string ServerIp { get; set; }
/// <summary>
/// 代理 URL(如有需要才设置)
/// </summary>
public string ProxyUrl { get; set; }
/// <summary>
/// 日志等级
/// 0.不输出日志
/// 1.只输出错误信息
/// 2.输出错误和正常信息
/// 3.输出错误信息、正常信息和调试信息
/// </summary>
public int LogLevel { get; set; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# JsSdkConfig(JS-SDK 配置)
public class JsSdkConfig
{
/// <summary>
/// 公众号的唯一标识
/// </summary>
public string AppId { get; set; }
/// <summary>
/// 时间戳
/// </summary>
public string Timestamp { get; set; }
/// <summary>
/// 随机字符串
/// </summary>
public string NonceStr { get; set; }
/// <summary>
/// 签名
/// </summary>
public string Signature { get; set; }
/// <summary>
/// JS 接口列表
/// </summary>
public string[] JsApiList => JsApi.Split(',');
/// <summary>
/// 默认 JS 接口列表
/// </summary>
public static String JsApi = "JSAPI,onMenuShareTimeline,onMenuShareAppMessage," +
"onMenuShareQQ,onMenuShareWeibo,startRecord,stopRecord,onVoiceRecordEnd," +
"playVoice,pauseVoice,stopVoice,onVoicePlayEnd,uploadVoice,downloadVoice," +
"chooseImage,previewImage,uploadImage,downloadImage,translateVoice," +
"getNetworkType,openLocation,getLocation,hideOptionMenu,showOptionMenu," +
"hideMenuItems,showMenuItems,hideAllNonBaseMenuItem,showAllNonBaseMenuItem," +
"closeWindow,scanQRCode,chooseWXPay,openProductSpecificView,addCard," +
"chooseCard,openCard";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 🏗️ 依赖注入
# 自动注册机制
// Wechat.Integration/ServiceCollectionExtensions.cs
public static class ServiceCollectionExtensions
{
/// <summary>
/// 添加微信相关功能
/// </summary>
public static IServiceCollection AddWechat(
this IServiceCollection services,
IConfiguration cfg)
{
var config = new WechatConfig();
var section = cfg.GetSection("Wechat");
if (section != null)
{
section.Bind(config);
services.AddSingleton(config);
// 加载 Wechat.Core 程序集
var assembly = AssemblyHelper.LoadByNameEndString(
$".Lib.Wechat.Core");
if (assembly == null)
return services;
// 注册微信处理器
var providerType = assembly.GetTypes()
.FirstOrDefault(m => m.Name.EndsWith("WechatProcesser"));
if (providerType != null)
{
services.AddSingleton(providerType);
}
}
return services;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 💡 使用示例
# 1. 配置微信模块
// appsettings.json
{
"Wechat": {
"Default": "wx1234567890abcdef",
"WebSite": "https://www.example.com",
"HomePage": "/wechat/bind",
"Test": false,
"Maintenance": false,
"MessageType": 0,
"Servers": [
{
"AppID": "wx1234567890abcdef",
"Name": "我的公众号",
"Label": "公众号描述",
"AppSecret": "your-app-secret",
"MchID": "1234567890",
"PayKey": "your-pay-key-32-chars",
"UserToken": "your-token",
"MiniApp": false,
"SSlCertPath": "/path/to/apiclient_cert.p12",
"SSlCertPassword": "1234567890",
"NotifyUrl": "https://api.example.com/wechatpay/notify",
"RefundNotifyUrl": "https://api.example.com/wechatpay/refund-notify",
"LogLevel": 2
}
]
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 2. 注册服务
// Program.cs 或 Startup.cs
var builder = WebApplication.CreateBuilder(args);
// 添加微信服务
builder.Services.AddWechat(builder.Configuration);
// 添加微信处理中间件
builder.Services.AddWechatHandleMiddleware();
var app = builder.Build();
// 使用微信处理中间件
app.UseWechatHandleMiddleware();
app.Run();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 3. 微信消息处理
public class WechatMessageController : ControllerBase
{
private readonly WechatProcesser _wechatProcesser;
private readonly WechatConfig _config;
private readonly ILogger<WechatMessageController> _logger;
public WechatMessageController(
WechatProcesser wechatProcesser,
WechatConfig config,
ILogger<WechatMessageController> logger)
{
_wechatProcesser = wechatProcesser;
_config = config;
_logger = logger;
}
/// <summary>
/// 微信服务器验证(GET)
/// </summary>
[HttpGet("wechat/message")]
public string ValidateWechat(
[FromQuery] string signature,
[FromQuery] string timestamp,
[FromQuery] string nonce,
[FromQuery] string echostr)
{
var account = _config.DefaultServer;
if (WechatUtility.VerifySignature(
signature, timestamp, nonce, account.UserToken))
{
return echostr;
}
return "error";
}
/// <summary>
/// 接收微信消息(POST)
/// </summary>
[HttpPost("wechat/message")]
public async Task<string> ReceiveMessage()
{
using var reader = new StreamReader(Request.Body);
var xml = await reader.ReadToEndAsync();
_logger.LogInformation("收到微信消息:{Xml}", xml);
// 处理消息
var response = await _wechatProcesser.ProcessMessageAsync(xml);
_logger.LogInformation("返回微信响应:{Response}", response);
return response ?? "success";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 4. 获取 Access Token
public class WechatTokenService
{
private readonly WechatConfig _config;
private readonly IMemoryCache _cache;
private readonly ILogger<WechatTokenService> _logger;
public WechatTokenService(
WechatConfig config,
IMemoryCache cache,
ILogger<WechatTokenService> logger)
{
_config = config;
_cache = cache;
_logger = logger;
}
/// <summary>
/// 获取 Access Token(带缓存)
/// </summary>
public async Task<string> GetAccessTokenAsync(string appId = null)
{
appId ??= _config.Default;
var account = _config.GetAccount(appId);
if (account == null)
throw new WechatException($"未找到公众号配置:{appId}");
var cacheKey = $"wechat_access_token_{appId}";
return await _cache.GetOrCreateAsync(
cacheKey,
async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(7000);
var url = $"https://api.weixin.qq.com/cgi-bin/token?" +
$"grant_type=client_credential&" +
$"appid={account.AppID}&" +
$"secret={account.AppSecret}";
using var httpClient = new HttpClient();
var response = await httpClient.GetStringAsync(url);
var json = JsonDocument.Parse(response);
if (json.RootElement.TryGetProperty("access_token", out var tokenElem))
{
return tokenElem.GetString();
}
throw new WechatException(
$"获取 Access Token 失败:{response}");
});
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# 5. JS-SDK 签名配置
public class WechatJsSdkService
{
private readonly WechatConfig _config;
private readonly WechatTokenService _tokenService;
public WechatJsSdkService(
WechatConfig config,
WechatTokenService tokenService)
{
_config = config;
_tokenService = tokenService;
}
/// <summary>
/// 获取 JS-SDK 配置
/// </summary>
public async Task<JsSdkConfig> GetJsSdkConfigAsync(
string url,
string appId = null)
{
appId ??= _config.Default;
var account = _config.GetAccount(appId);
// 获取 JsApiTicket
var ticket = await GetJsApiTicketAsync(appId);
// 生成签名参数
var timestamp = WechatUtility.GetTimestamp();
var noncestr = Guid.NewGuid().ToString("N");
// 生成签名
var signature = WechatUtility.CreateJsSdkSignature(
ticket, noncestr, timestamp, url);
return new JsSdkConfig
{
AppId = account.AppID,
Timestamp = timestamp,
NonceStr = noncestr,
Signature = signature
};
}
/// <summary>
/// 获取 JsApiTicket(带缓存)
/// </summary>
private async Task<string> GetJsApiTicketAsync(string appId)
{
var cacheKey = $"wechat_jsapi_ticket_{appId}";
return await _cache.GetOrCreateAsync(
cacheKey,
async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(7000);
var accessToken = await _tokenService.GetAccessTokenAsync(appId);
var url = $"https://api.weixin.qq.com/cgi-bin/ticket/getticket?" +
$"access_token={accessToken}&type=jsapi";
using var httpClient = new HttpClient();
var response = await httpClient.GetStringAsync(url);
var json = JsonDocument.Parse(response);
if (json.RootElement.TryGetProperty("ticket", out var ticketElem))
{
return ticketElem.GetString();
}
throw new WechatException(
$"获取 JsApiTicket 失败:{response}");
});
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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
72
73
74
75
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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
72
73
74
75
# 6. 微信支付(JSAPI)
public class WechatPayService
{
private readonly WechatConfig _config;
private readonly ILogger<WechatPayService> _logger;
public WechatPayService(
WechatConfig config,
ILogger<WechatPayService> logger)
{
_config = config;
_logger = logger;
}
/// <summary>
/// 统一下单
/// </summary>
public WxPayData CreateUnifiedOrder(
string openId,
string outTradeNo,
decimal amount,
string body)
{
var account = _config.DefaultServer;
var req = new WxPayData();
req.SetValue("appid", account.AppID);
req.SetValue("mch_id", account.MchID);
req.SetValue("nonce_str", WxPayApi.GenerateNonceStr());
req.SetValue("body", body);
req.SetValue("out_trade_no", outTradeNo);
req.SetValue("total_fee", (int)(amount * 100)); // 单位:分
req.SetValue("spbill_create_ip", GetClientIp());
req.SetValue("notify_url", account.NotifyUrl);
req.SetValue("trade_type", "JSAPI");
req.SetValue("openid", openId);
// 生成签名
req.SetValue("sign", WxPayApi.GenerateSign(req));
// 调用统一下单 API
var response = WxPayApi.UnifiedOrder(req);
if (response.GetValue("return_code").ToString() != "SUCCESS" ||
response.GetValue("result_code").ToString() != "SUCCESS")
{
throw new WechatPayException(
$"微信支付下单失败:{response.GetValue("return_msg")}");
}
return response;
}
/// <summary>
/// 获取小程序支付参数
/// </summary>
public WxPayData GetJsApiPayData(string prepayId)
{
var account = _config.DefaultServer;
var jsApiPayData = new WxPayData();
jsApiPayData.SetValue("appId", account.AppID);
jsApiPayData.SetValue("timeStamp", WxPayApi.GetTimestamp());
jsApiPayData.SetValue("nonceStr", WxPayApi.GenerateNonceStr());
jsApiPayData.SetValue("package", $"prepay_id={prepayId}");
jsApiPayData.SetValue("signType", "MD5");
// 生成支付签名
jsApiPayData.SetValue("paySign",
WxPayApi.GenerateSign(jsApiPayData));
return jsApiPayData;
}
/// <summary>
/// 查询订单
/// </summary>
public WxPayData QueryOrder(string outTradeNo)
{
var req = new WxPayData();
req.SetValue("out_trade_no", outTradeNo);
return WxPayApi.OrderQuery(req);
}
/// <summary>
/// 申请退款
/// </summary>
public WxPayData Refund(
string outTradeNo,
string outRefundNo,
decimal refundAmount,
decimal totalAmount)
{
var account = _config.DefaultServer;
var req = new WxPayData();
req.SetValue("out_trade_no", outTradeNo);
req.SetValue("out_refund_no", outRefundNo);
req.SetValue("total_fee", (int)(totalAmount * 100));
req.SetValue("refund_fee", (int)(refundAmount * 100));
req.SetValue("op_user_id", account.MchID);
return WxPayApi.Refund(req);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# 🔧 核心功能
# 消息类型
| 类型 | 说明 | 示例 |
|---|---|---|
| 文本消息 | 用户发送的文本 | TextMessage |
| 图片消息 | 用户发送的图片 | ImageMessage |
| 语音消息 | 用户发送的语音 | VoiceMessage |
| 视频消息 | 用户发送的视频 | VideoMessage |
| 位置消息 | 用户发送的位置 | LocationMessage |
| 链接消息 | 用户发送的链接 | LinkMessage |
| 事件消息 | 关注/取消关注等 | EventMessage |
# 事件类型
| 事件 | 说明 | 处理类 |
|---|---|---|
| subscribe | 用户关注 | SubscribeEventHandle |
| unsubscribe | 用户取消关注 | UnsubscribeEventHandle |
| SCAN | 用户扫码 | ScanEventHandle |
| LOCATION | 上报地理位置 | LocationEventHandle |
| CLICK | 点击菜单 | ClickEventHandle |
| VIEW | 点击菜单链接 | ViewEventHandle |
# 支付功能
| 功能 | 说明 | 业务类 |
|---|---|---|
| JSAPI 支付 | 公众号/小程序支付 | JsApiPay |
| 扫码支付 | Native 扫码支付 | NativePay |
| 订单查询 | 查询订单状态 | OrderQuery |
| 申请退款 | 退款操作 | Refund |
| 退款查询 | 查询退款状态 | RefundQuery |
| 下载对账单 | 下载交易对账单 | DownloadBill |
# 💡 最佳实践
# 1. 多账号管理
public class MultiWechatAccountService
{
private readonly WechatConfig _config;
private readonly IMemoryCache _cache;
public MultiWechatAccountService(
WechatConfig config,
IMemoryCache cache)
{
_config = config;
_cache = cache;
}
/// <summary>
/// 根据 AppID 获取账号
/// </summary>
public WechatAccount GetAccount(string appId)
{
return _config.GetAccount(appId);
}
/// <summary>
/// 获取默认账号
/// </summary>
public WechatAccount GetDefaultAccount()
{
return _config.DefaultServer;
}
/// <summary>
/// 获取所有账号
/// </summary>
public List<WechatAccount> GetAllAccounts()
{
return _config.Servers ?? new List<WechatAccount>();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 2. Access Token 缓存
public class WechatTokenCacheService
{
private readonly IMemoryCache _cache;
public WechatTokenCacheService(IMemoryCache cache)
{
_cache = cache;
}
/// <summary>
/// 获取 Token(自动缓存)
/// </summary>
public async Task<string> GetTokenAsync(
string appId,
string appSecret,
TokenType type = TokenType.AccessToken)
{
var cacheKey = $"wechat_{type.ToString().ToLower()}_{appId}";
return await _cache.GetOrCreateAsync(
cacheKey,
async entry =>
{
// AccessToken 有效期 7200 秒,提前 5 分钟刷新
entry.AbsoluteExpirationRelativeToNow =
type == TokenType.AccessToken
? TimeSpan.FromSeconds(7000)
: TimeSpan.FromSeconds(7000);
return await FetchTokenAsync(appId, appSecret, type);
});
}
/// <summary>
/// 清除缓存
/// </summary>
public void ClearCache(string appId)
{
var keys = new[] {
$"wechat_access_token_{appId}",
$"wechat_jsapi_ticket_{appId}"
};
foreach (var key in keys)
{
_cache.Remove(key);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 3. 消息回复模板
public class WechatReplyTemplate
{
/// <summary>
/// 文本消息回复
/// </summary>
public static string CreateTextReply(
string fromUser,
string toUser,
string content)
{
return $@"<xml>
<ToUserName><![CDATA[{fromUser}]]></ToUserName>
<FromUserName><![CDATA[{toUser}]]></FromUserName>
<CreateTime>{DateTimeOffset.Now.ToUnixTimeSeconds()}</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[{content}]]></Content>
</xml>";
}
/// <summary>
/// 图文消息回复
/// </summary>
public static string CreateNewsReply(
string fromUser,
string toUser,
List<NewsArticle> articles)
{
var articlesXml = string.Join("", articles.Select(a => $@"
<item>
<Title><![CDATA[{a.Title}]]></Title>
<Description><![CDATA[{a.Description}]]></Description>
<PicUrl><![CDATA[{a.PicUrl}]]></PicUrl>
<Url><![CDATA[{a.Url}]]></Url>
</item>"));
return $@"<xml>
<ToUserName><![CDATA[{fromUser}]]></ToUserName>
<FromUserName><![CDATA[{toUser}]]></FromUserName>
<CreateTime>{DateTimeOffset.Now.ToUnixTimeSeconds()}</CreateTime>
<MsgType><![CDATA[news]]></MsgType>
<ArticleCount>{articles.Count}</ArticleCount>
<Articles>{articlesXml}</Articles>
</xml>";
}
}
public class NewsArticle
{
public string Title { get; set; }
public string Description { get; set; }
public string PicUrl { get; set; }
public string Url { get; set; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# 🔍 常见问题
# Q1: 微信服务器验证失败?
检查清单:
- ✅ Token 配置是否与微信后台一致
- ✅ 验证算法是否正确(SHA1 排序)
- ✅ 返回 echostr 参数是否正确
- ✅ 服务器是否可被微信访问(80/443 端口)
验证代码:
public static bool VerifySignature(
string signature,
string timestamp,
string nonce,
string token)
{
var arr = new[] { token, timestamp, nonce }.OrderBy(x => x).ToArray();
var hash = string.Join("", arr).ToSHA1();
return hash.Equals(signature, StringComparison.OrdinalIgnoreCase);
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# Q2: Access Token 频繁失效?
解决方案:
- ✅ 使用缓存(有效期 7200 秒,提前刷新)
- ✅ 避免每次请求都获取 Token
- ✅ 多进程共享缓存(使用 Redis)
- ✅ Token 失效时自动重试获取
// 使用 Redis 共享 Token
public class RedisWechatTokenService
{
private readonly IDistributedCache _cache;
public async Task<string> GetTokenAsync(string appId, string appSecret)
{
var key = $"wechat:token:{appId}";
var token = await _cache.GetStringAsync(key);
if (token != null)
return token;
// 获取新 Token
token = await FetchTokenAsync(appId, appSecret);
// 缓存 7000 秒
await _cache.SetStringAsync(
key,
token,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(7000)
});
return token;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Q3: 微信支付证书加载失败?
解决方案:
// 正确加载证书
public X509Certificate2 LoadCertificate(string certPath, string password)
{
if (!File.Exists(certPath))
throw new FileNotFoundException($"证书文件不存在:{certPath}");
return new X509Certificate2(
certPath,
password,
X509KeyStorageFlags.MachineKeySet |
X509KeyStorageFlags.PersistKeySet |
X509KeyStorageFlags.Exportable);
}
// 配置示例
{
"Wechat": {
"Servers": [
{
"SSlCertPath": "/path/to/apiclient_cert.p12",
"SSlCertPassword": "1234567890"
}
]
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 📚 相关文档
# 🔗 参考链接
- 源代码 (opens new window) -
src/Framework/Wechat - 微信公众号文档 (opens new window)
- 微信小程序文档 (opens new window)
- 微信支付文档 (opens new window)
最后更新: 2024-09-20