做线下门店类外包,核销是绕不开的需求。本文把一套最小可用的闭环拆开:小程序负责扫、.NET WebAPI 负责判、WPF 负责看和打。
一、为什么是三端,而不是一个后端搞定
- 顾客/店员在手机上操作 → 必须小程序(扫码、
wx.login、订阅消息都是小程序能力)。 - 判定「码是否有效、扫过没」→ 必须服务端,且要扛并发。
- 门店电脑要看实时数据、打小票 → 桌面端最稳,断网也能查本地记录。
三端解耦后,哪块要迭代都不动另外两块。
这里插一句前端工程上的小事:小程序原生 wx.request 是回调式的,多人协作建议包一层 Promise,统一带 Authorization 和错误提示,否则每个页面都写一遍 wx.showLoading/hideLoading 很快会乱。我习惯在项目里放一个 request() 封装,所有页面只调它,核销、登录、订阅下发走同一套错误处理。
二、小程序端:扫完调用接口
js
Page({
scan() {
wx.scanCode({
onlyFromCamera: true,
scanType: ['qrCode', 'barCode'],
success: (res) => this.verify(res.result)
})
},
async verify(code) {
const token = wx.getStorageSync('token')
const { data } = await request({
url: 'https://api.yourdomain.com/api/verify',
method: 'POST',
header: { Authorization: 'Bearer ' + token },
data: { code }
})
wx.showToast({ title: data.ok ? '核销成功' : (data.msg || '失败'), icon: data.ok ? 'success' : 'none' })
}
})
授权用 wx.login 拿 code,后端走 code2session 换 openid,session_key 只留服务端。订阅消息在核销成功后 wx.requestSubscribeMessage 申请,后端用缓存的 access_token 下发。
三、核心:.NET 核销接口的幂等
重复核销 = 同码并发或连扫两次。用「行锁 + 状态位」解决:
csharp
[HttpPost("api/verify")]
public async Task<IActionResult> Verify([FromBody] VerifyDto dto)
{
await using var tx = await _db.Database.BeginTransactionAsync();
var c = await _db.Coupons
.FromSqlInterpolated($"SELECT * FROM Coupons WITH (UPDLOCK, HOLDLOCK) WHERE Code={dto.Code}")
.FirstOrDefaultAsync();
if (c == null) return NotFound(new { msg = "核销码不存在" });
if (c.Status == 1) return Conflict(new { msg = "该码已核销" }); // 第二次扫直接拦截
c.Status = 1; c.VerifiedAt = DateTime.Now; c.Operator = dto.Operator;
await _db.SaveChangesAsync();
await tx.CommitAsync();
_ = SendVerifyNotice(c.OpenId);
return Ok(new { ok = true, no = c.No });
}
SQL Server 用 UPDLOCK + HOLDLOCK,MySQL 用 FOR UPDATE,SQLite 单机靠唯一约束 + 事务。返回 409 给前端提示「已核销」,比笼统的「失败」体验好很多。
顺手说一下怎么验证幂等真的生效:本地起两个线程或用一个接口压测工具,对同一 Code 同时发十次 POST,正确结果是恰好一次 ok=true、其余九次都是 409。如果十次都成功,说明锁没生效或事务边界没包住判断------这是上线前必测的一项,很多团队跳过这步,上线当天被顾客连扫两下就露了馅。
四、WPF 门店看板 + 小票
门店网络不稳,本地 SQLite 存一份记录:
csharp
public List<VerifyLog> Today() => new SQLiteConnection("Data Source=store.db;")
.Query<VerifyLog>("SELECT * FROM VerifyLog WHERE date(VerifiedAt)=date('now') ORDER BY VerifiedAt DESC")
.ToList();
小票走串口 ESC/POS(热敏机通用指令):初始化 \x1B\x40,切纸 \x1D\x56\x00。
桌面端在这里的价值很直接:扫码、看数、出纸全在一台门店电脑上闭环,不依赖浏览器、不依赖网速,开机就能干活。
五、数据表与部署要补的细节
核销接口依赖一张券表,最小字段就够了:
sql
CREATE TABLE Coupons (
Id INT IDENTITY PRIMARY KEY,
Code NVARCHAR(64) UNIQUE NOT NULL, -- 核销码,全局唯一
No NVARCHAR(32),
Status TINYINT NOT NULL DEFAULT 0, -- 0 未核销 / 1 已核销
OpenId NVARCHAR(64),
Operator NVARCHAR(32),
VerifiedAt DATETIME
);
Code 上加唯一约束,是从数据库层面给幂等再上一道保险:即使应用层锁失效,重复写入也会抛唯一冲突。小程序端还要在后台把接口域名加进 request 合法域名白名单(本地可勾「不校验合法域名」调试),并填好隐私指引声明收集 openid 的用途。WebAPI 部署到公网 HTTPS 即可,回调地址必须外网可达。
六、上线前两个最容易踩的坑
- 幂等没测透:拿两个线程同时打同一码,看是否只有一个生效。
- 小程序提审被驳:类目不符、诱导分享、域名未备案、没填隐私指引,是最常见的四条驳回理由。