接了个连锁门店的预约小程序,8 家店,3 周上线。做完复盘,发现 80% 的时间花在后端和微信接口上,页面只占很小一块。
这篇文章把最值钱的 4 个接口拆开讲:微信登录、订阅消息、支付回调、时段库存。技术栈 ASP.NET Core 8 + 原生小程序。
一、微信登录:别把 session_key 发给前端
流程是标准的三步:小程序 wx.login() 拿 code → 后端用 code + appid + secret 换 openid → 后端自己签发登录态。
wx.login({
success: async (res) => {
const r = await wx.request({
url: 'https://api.example.com/api/wechat/login',
method: 'POST',
data: { code: res.code }
});
wx.setStorageSync('token', r.data.token);
}
});
code 有效期很短且只能用一次,所以每次登录都要重新调 wx.login(),别缓存。
后端:
var url = "https://api.weixin.qq.com/sns/jscode2session"
+ $"?appid={appId}&secret={secret}"
+ $"&js_code={Uri.EscapeDataString(code)}"
+ "&grant_type=authorization_code";
返回里有 openid、session_key、unionid(满足条件时)。这里两条纪律:
session_key绝不下发客户端 —— 它能解密用户敏感数据,泄露等于数据裸奔。- 别拿
openid当接口鉴权凭证 —— 泄露后无法吊销。
正确做法是后端生成随机 token,把 {openid, session_key} 存 Redis,只把 token 给前端:
var token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
await cache.SetStringAsync($"wx:sess:{token}",
JsonSerializer.Serialize(new { r.OpenId, r.SessionKey }),
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(30) });
return new { token };
前端用 wx.checkSession() 判断 session_key 是否可能过期,过期就重走一遍登录。
二、订阅消息:额度是「攒」出来的,不是「要」来的
这是最容易在报价阶段就说不清的一件事。
- 一次性订阅:用户点一次授权,只能下发一条;只有勾了「总是保持以上选择」,额度才会累积。
- 长期订阅:只对政务民生、医疗、交通等特定行业开放,普通门店拿不到。
既然只能一次性订阅,策略就很明确了:在每个用户主动操作的节点,顺便要一次授权(提交预约后、支付完成后、取消预约时),而不是等要发消息时才发现没额度。
wx.requestSubscribeMessage({
tmplIds: ['TEMPLATE_RESULT', 'TEMPLATE_REMIND'],
success(res) {
// res[tmplId]: 'accept' | 'reject' | 'ban' | 'filter'
}
});
⚠️ 必须在用户点击的回调里调,页面 onLoad 里自动调会被拒。
服务端下发前先拿 access_token(7200 秒有效,有频率限制),必须缓存:
private static readonly SemaphoreSlim _lock = new(1, 1);
public async Task<string> GetAccessTokenAsync(CancellationToken ct)
{
if (_cache.TryGetValue("wx:at", out string? c) && !string.IsNullOrEmpty(c)) return c;
await _lock.WaitAsync(ct);
try
{
if (_cache.TryGetValue("wx:at", out string? a) && !string.IsNullOrEmpty(a)) return a;
var json = await http.GetStringAsync(
$"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appId}&secret={secret}", ct);
using var doc = JsonDocument.Parse(json);
var at = doc.RootElement.GetProperty("access_token").GetString()!;
var exp = doc.RootElement.GetProperty("expires_in").GetInt32();
_cache.Set("wx:at", at, TimeSpan.FromSeconds(exp - 300)); // 提前 5 分钟过期
return at;
}
finally { _lock.Release(); }
}
下发:
await http.PostAsJsonAsync(
$"https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token={at}",
new {
touser = openId,
template_id = templateId,
page = "pages/order/detail?id=123",
miniprogram_state = "formal",
lang = "zh_CN",
data = new {
thing1 = new { value = "染发" },
time2 = new { value = "2026-09-25 14:00" }
}
});
两个细节:模板字段有类型和长度限制(thing 一般 20 字符内),超长直接报错,建议封装 + 截断;返回 43101 不是 bug,是一次性订阅额度用尽。
三、支付回调:签名、验签、解密、幂等
微信支付 v3 有四道坎。
1. 请求签名(签名串每行末尾都要换行,含最后一行):
{HTTP方法}\n{URL}\n{时间戳}\n{随机串}\n{报文主体}\n
var sig = priKey.SignData(Encoding.UTF8.GetBytes(msg),
HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
Authorization 头:WECHATPAY2-SHA256-RSA2048 mchid="...",nonce_str="...",signature="...",timestamp="...",serial_no="..."。
下单 POST /v3/pay/transactions/jsapi,拿到 prepay_id 后再签一次给前端(签名串是 appId\n时间戳\n随机串\npackage\n)。
2. 回调验签:串不一样,是 时间戳\n随机串\n报文主体\n,用平台证书(或商户后台配置的微信支付公钥)。平台证书要定时更新。
3. 解密 resource(AES-256-GCM):
using var gcm = new AesGcm(Encoding.UTF8.GetBytes(apiV3Key), 16);
gcm.Decrypt(Encoding.UTF8.GetBytes(nonce), body, tag, plain,
Encoding.UTF8.GetBytes(associatedData));
4. 幂等:这是最容易漏的一条。微信会重复推送回调直到你正确应答。按 out_trade_no 加唯一约束,重复通知直接返回成功;耗时逻辑丢后台队列,先秒级返回;金额以后端订单为准,不信前端。
四、时段库存:别在应用层加锁
预约系统的经典事故 —— 两人同时抢 14:00 同一技师,都成功。
最可靠的做法是把并发交给数据库:给 (门店Id, 技师Id, 时段) 建唯一索引(对有效状态),插入冲突即「已被预约」。比任何分布式锁都简单。
CREATE UNIQUE INDEX UX_Slot ON Appointment(StoreId, StaffId, SlotStart)
WHERE Status IN (0, 1);
五、开工前那 4 个配置,比代码更影响工期
| 配置 | 坑 |
|---|---|
| 服务器域名 | 必须 HTTPS + 已备案域名,不支持 IP / 自签;改动次数有限 |
| ICP 备案 | 周期不可控,签合同当天就提交,与开发并行 |
| 用户隐私保护指引 | 没配就调不了手机号等接口,审核必驳 |
| 类目 | 与功能不符 = 审核驳回第一名 |
六、3 周排期
- 第 1 周:账号/备案并行;接口设计;
code2session全链路跑通 - 第 2 周:预约主流程 + 后台管理 + 订阅下发定时任务
- 第 3 周:支付联调、体验版走查、提审并应对 1–2 轮驳回
小结
这 4 个接口背后是同一条原则:凡是涉及身份、钱、库存、外部通知的环节,都要假设它会失败、会重复、会并发。按这个前提写出来的后端,才敢在 3 周之后交付。
如果你也在做小程序后端,欢迎在评论区聊聊踩过的坑,觉得有用的话点个关注,后面继续写 .NET 与微信生态对接的实战笔记。