简介:这是一套面向微信小程序开发者与淘宝客业务学习者的完整源码包,基于C#.NET MVC与微信小程序前后端分离架构,实现调用阿里妈妈淘宝客API进行优惠券自助搜索与领取。后台采用ASP.NET MVC框架,已内置内容管理、会员、订单、微信及WAP等模块,便于在此基础上二次开发成完整站点。资源共76个文件,以js、json、wxss、wxml等小程序页面与逻辑文件为主,另含数据库脚本、配置说明及后台压缩包,整体约22.8MB,前台页面与TBKAdmin后台源码分开放置,结构清晰。开发环境为Visual Studio 2010搭配SQLServer2008与.NET 4.0,默认管理员账号admin/admin888,数据库连接字符串在web.config中修改,DataBase文件夹提供建库脚本。目前已有1784人学习下载,适合想研究小程序对接淘宝客API、或需要一套可运行后台框架进行二次开发的中级开发者参考。
1. 2019 版 ASP.NET MVC 优惠券领取小程序:一套老源码为什么还值得翻出来读
2019 年前后上线的微信小程序里,优惠券领取是最典型的一类业务:用户点开小程序、授权登录、看到券列表、点“领取”、后台扣库存、写领取记录、回传结果。这套流程看着简单,但真正落到代码里,涉及小程序端请求封装、ASP.NET MVC 的控制器路由、C# 侧的库存并发控制、以及数据库事务边界。标题里这套“ASP.NET MVC C# 优惠券领取微信小程序源码源代码2019完整版”,本质就是一份把上述链路全部串起来的后端 + 小程序端参考实现。
它适合谁?一是手里有老项目要维护、需要对照 MVC 三层架构补业务逻辑的 C# 开发者;二是想拿一个真实业务练手、把微信小程序请求打到自建后端的新手;三是需要快速搭一套领券 Demo 去验证库存扣减方案的人。不适合指望直接上线生产的人——2019 年的依赖版本、鉴权写法、并发处理都需要按现在的标准重写。下面按“先看懂结构、再跑通链路、最后处理并发和坑”的顺序拆开讲。
2. 先看清这套 MVC 源码的分层:控制器、服务、数据访问各管什么
拿到一份 2019 年的 ASP.NET MVC 源码,第一件事不是急着 F5 运行,而是把目录结构和分层关系理清楚。这套优惠券项目的典型结构是:Controllers放接口入口,Services放领券业务逻辑,Models放实体和视图模型,DAL或Repositories放数据访问,小程序端单独一个目录放pages和utils。分层清不清楚,直接决定你后面改库存逻辑时会不会牵一发动全身。
2.1 MVC 三层架构在这套领券项目里的实际落点
MVC 设计模式落到优惠券业务上,Controller 只做三件事:接参数、调 Service、包返回。真正的“能不能领、库存够不够、是不是重复领”全部应该在 Service 层判断。我见过太多老项目把库存判断写在 Controller 里,结果小程序端一改请求参数就绕过了校验。正确的落点是:
CouponController:暴露/api/coupon/list、/api/coupon/receive两个动作CouponService:GetAvailableCoupons()、ReceiveCoupon(userId, couponId)CouponRepository:DecreaseStock(couponId)、InsertReceiveRecord(...)
这样分的好处是,库存扣减和记录写入可以放进同一个事务,Controller 不掺和业务规则。C# 里判断字符串、集合操作这些基础能力在这里会频繁用到,比如用List<Coupon>承载券列表、用string.IsNullOrEmpty校验 openid,都是绕不开的基本功。
2.2 小程序端请求是怎么打到 MVC 控制器的
小程序端不认 MVC 的视图,只认 JSON 接口,所以这套源码里 Controller 返回的应该是JsonResult而不是ViewResult。小程序用wx.request发 POST,后端用[HttpPost]接收。一个最小可跑通的请求封装长这样:
// utils/request.js —— 小程序端统一请求封装 const BASE_URL = 'https://your-domain.com'; // 换成你自己的后端域名 function post(url, data) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, method: 'POST', data: data, header: { 'content-type': 'application/json' }, success: (res) => { // 后端约定 code=0 为成功,其余为业务失败 if (res.data && res.data.code === 0) { resolve(res.data.data); } else { reject(res.data || { msg: '未知错误' }); } }, fail: reject }); }); } module.exports = { post };逻辑说明:把wx.request包成 Promise,是为了在页面里用async/await写领券流程,避免回调地狱。参数说明:BASE_URL必须换成已在小程序后台配置过 request 合法域名的地址,否则真机调试直接报域名不合法;header里的content-type要和后端[HttpPost]的绑定方式对上,用application/json时后端参数要加[FromBody],这是新手最常翻车的地方。
2.3 后端接收领券请求的控制器写法
对应上面的请求,MVC 侧的控制器动作应该这样写:
// Controllers/CouponController.cs public class CouponController : Controller { private readonly CouponService _service = new CouponService(); [HttpPost] public JsonResult Receive([FromBody] ReceiveRequest req) { // req 里至少要有 userId 和 couponId if (req == null || string.IsNullOrEmpty(req.UserId) || req.CouponId <= 0) { return Json(new { code = 1, msg = "参数不合法" }); } var result = _service.ReceiveCoupon(req.UserId, req.CouponId); return Json(new { code = result.Success ? 0 : 2, msg = result.Message }); } } public class ReceiveRequest { public string UserId { get; set; } public int CouponId { get; set; } }逻辑说明:[FromBody]是配合application/json的关键,漏了它req会是 null,表现为“参数永远不合法”。参数说明:UserId实际项目里应该由后端从登录态里取,而不是前端传,这里为了讲清链路先显式传;CouponId用 int,是因为券 ID 通常自增。返回统一用code字段区分成功和业务失败,小程序端才能统一处理。
3. 把领券链路跑通:库存扣减、防重复领和事务边界
链路能跑通只是及格线,优惠券业务真正的难点在“高并发下不超发、同一用户不重复领”。2019 年的老源码很多用“先查再扣”的写法,单机测试没问题,一上量就超发。这一章讲清楚正确的扣减姿势和事务边界,也是这套源码最值得你重写的地方。
3.1 库存扣减为什么不能“先查再扣”
“先查库存够不够,再更新库存减一”这个写法在并发下必然超发:两个请求同时查到库存为 1,都判断够,都去减,结果库存变成 -1。正确做法是把判断和扣减合并成一条带条件的 SQL,让数据库来保证原子性:
-- 只有库存大于 0 时才扣减,返回受影响行数 UPDATE Coupon SET Stock = Stock - 1 WHERE Id = @CouponId AND Stock > 0;逻辑说明:这条语句的WHERE Stock > 0是并发安全的核心,数据库执行时会对该行加锁,两个请求串行执行,第二个请求受影响行数为 0,直接判定领取失败。参数说明:@CouponId是券 ID,执行后要检查ExecuteNonQuery的返回值,等于 1 才算扣减成功,等于 0 说明库存已空。这一步是整套源码里最该改对的地方。
3.2 防重复领:唯一索引比代码判断更可靠
同一用户重复领同一张券,靠 Service 层“先查有没有领过”同样有并发漏洞。最稳的做法是在领取记录表上加唯一索引,让数据库兜底:
-- 领取记录表加联合唯一索引,防止同一用户重复领同一张券 CREATE UNIQUE INDEX UX_User_Coupon ON CouponReceive (UserId, CouponId);逻辑说明:有了唯一索引,即使两个请求同时通过了代码层的检查,插入时也只有一个能成功,另一个抛唯一约束冲突,捕获后返回“已领取”。参数说明:UserId和CouponId的组合必须唯一;如果业务允许同一用户领多张同款券,就要改成(UserId, CouponId, BatchNo)这种带批次的组合。C# 侧捕获SqlException判断错误号 2601/2627 即可识别唯一冲突。
3.3 扣库存和写记录必须在同一个事务里
扣了库存但写记录失败,用户没领到券库存却少了;写了记录但扣库存失败,用户领到了券但库存没减。这两种脏数据都源于没加事务。正确写法:
// Services/CouponService.cs public ReceiveResult ReceiveCoupon(string userId, int couponId) { using (var conn = new SqlConnection(ConnStr)) { conn.Open(); using (var tran = conn.BeginTransaction()) { try { // 第一步:原子扣减库存 var affected = DecreaseStock(conn, tran, couponId); if (affected == 0) { tran.Rollback(); return new ReceiveResult { Success = false, Message = "库存不足" }; } // 第二步:写领取记录,唯一索引兜底防重复 InsertReceiveRecord(conn, tran, userId, couponId); tran.Commit(); return new ReceiveResult { Success = true, Message = "领取成功" }; } catch (SqlException ex) when (ex.Number == 2601 || ex.Number == 2627) { tran.Rollback(); return new ReceiveResult { Success = false, Message = "您已领取过" }; } } } }逻辑说明:扣库存和写记录共用一个SqlTransaction,任一步失败整体回滚,保证数据一致。参数说明:ex.Number == 2601/2627是 SQL Server 唯一约束冲突的错误号,捕获它就能把“重复领取”转成友好提示而不是 500 错误。注意事务范围要尽量小,别把网络请求、日志写库这些慢操作包进去,否则会拖长锁持有时间。
4. 避坑与排查:这套 2019 源码最容易翻车的 5 个地方
老源码跑不起来,八成不是逻辑错,而是环境和配置对不上。下面这 5 条是我实际接手这类项目时踩过的,按“现象 → 原因 → 解决”列清楚,照着排查能省不少时间。
4.1 小程序真机请求报“不在以下 request 合法域名列表中”
现象:开发者工具里能跑,真机预览就报域名不合法。原因:小程序对 request 域名有白名单限制,工具里可以勾选“不校验合法域名”,真机不行。解决:在小程序后台把后端域名配进 request 合法域名,必须是 HTTPS 且已备案;本地调试阶段可以先用工具的不校验选项,但上线前必须配好。
4.2 后端接口收到参数全是 null
现象:小程序明明传了userId,后端req却是 null 或字段为空。原因:content-type和参数绑定方式不匹配,用application/json却没加[FromBody],或者用了表单格式却按 JSON 解析。解决:JSON 请求统一加[FromBody],表单请求用[FromForm],两边约定死一种格式,别混用。
4.3 压测时库存变成负数
现象:单机测试正常,一并发就超发。原因:用了“先查再扣”的非原子写法。解决:改成UPDATE ... WHERE Stock > 0的原子扣减,并检查受影响行数;同时确认数据库隔离级别没有把这条更新降级成快照读。
4.4 领取记录出现同一用户多条
现象:用户反馈领了一次却有多条记录。原因:只靠代码层查重,并发下两个请求都通过了检查。解决:加(UserId, CouponId)联合唯一索引,代码层捕获唯一冲突错误号返回友好提示,双保险。
4.5 事务里报“连接已关闭”或超时
现象:领券偶发失败,日志里是连接相关异常。原因:SqlConnection被提前释放,或者事务持有时间过长导致锁等待超时。解决:确保conn、tran用using正确嵌套,事务内不做耗时操作;必要时给命令设置合理的CommandTimeout,别用默认的无限等待。
5. 从能跑到敢用:给老源码补上接口防刷和幂等
把链路跑通、坑排完,这套源码离“敢用”还差一步:防刷和幂等。热搜里常出现“mvc 防止接口快速提交”,放到领券场景就是防止脚本高频刷券。最省事的做法是给领券接口加一层基于用户维度的频率限制,再配合前端按钮防抖。
5.1 用内存缓存做单机频率限制
单机部署时,用MemoryCache按用户维度限流就够了:
// 在 ReceiveCoupon 入口处加频率限制 private static readonly MemoryCache _cache = MemoryCache.Default; public ReceiveResult ReceiveCoupon(string userId, int couponId) { var key = "receive_" + userId; if (_cache.Contains(key)) { return new ReceiveResult { Success = false, Message = "操作太频繁,请稍后再试" }; } // 5 秒内同一用户只允许请求一次 _cache.Set(key, 1, DateTimeOffset.Now.AddSeconds(5)); // ... 后续扣库存、写记录逻辑 }逻辑说明:以userId为 key,5 秒内重复请求直接拒绝,能挡住大部分脚本高频提交。参数说明:AddSeconds(5)是限流窗口,按业务调整,领券这种低频操作 3 到 5 秒足够;多机部署时MemoryCache不共享,要换成 Redis 之类的集中式缓存,否则限流形同虚设。
5.2 幂等:让重复请求返回同一结果
防刷解决的是“频率”,幂等解决的是“同一请求重复到达”。领券接口天然适合用(UserId, CouponId)做幂等键:第一次请求正常扣减并返回成功,后续重复请求因为唯一索引冲突,直接返回“已领取”,而不是报错。这样即使小程序端因为网络重试发了两次,用户看到的也是一致结果。
5.3 上线前值得做的三个验证
第一,用并发工具(如 Apache Bench 或自己写多线程脚本)对领券接口打 100 并发,确认库存不为负、记录不重复。第二,模拟小程序端断网重试,确认幂等生效。第三,把限流窗口调小做压测,确认限流真的拦住了高频请求。这三点过了,这套 2019 年的源码才算真正能撑住一个小型领券活动。
我自己接手这类老项目时有个习惯:先不改业务逻辑,只把库存扣减和唯一索引这两处补上,跑一遍并发验证,再动其他代码。因为这两处是超发和重复领的根,根没扎稳,后面加再多防刷都是白搭。希望帮到你。
本文还有配套的精品资源,点击获取