
简介这是一份基于C# ASP.NET MVC开发的淘宝客优惠券领取微信小程序完整源码面向需要快速搭建优惠券返利类小程序的开发者或刚接触微信小程序与.NET后台对接的学习者。前台小程序支持自助搜索淘宝商品优惠券后台通过调用阿里妈妈淘宝客API实现返利逻辑并预留内容管理、会员、订单、微信系统等模块便于二次扩展。资源包共76个文件大小约22.8MB其中微信小程序前端以js、json、wxml、wxss文件为主另有后台MVC工程压缩包TBKAdmin.zip、数据库脚本及说明文本目录结构按功能模块拆分便于检索学习。该资源已有1784人浏览学习适合用来研究整体接口调用流程、前后台数据交互以及ASP.NET MVC后台架构。随包附带数据库脚本和默认登录账号方便本地部署运行与二次开发验证。1. 2019年的优惠券小程序源码放到今天还值不值得碰第一次接手这类“完整版源码”时我心里是犯嘀咕的ASP.NET MVC、C#、IIS、SQL Server、微信小程序几个关键词凑在一起信息量不小却透着一股老技术栈的味道。真正打开包一看内容比预想全小程序端页面、MVC后端控制器、数据库建表脚本、登录配置说明都齐。这套代码解决的问题很具体运营团队要拉起一个优惠券领取小程序用户进来能看到券列表点一下领取后端校验登录态、防重复领取、扣减库存再在“我的券”里展示已领到的券。适合两类人一类是接手的开发者想快速把老项目跑起来做二次开发另一类是准备做同类活动、想复用成熟逻辑的个人或小团队。作为项目它结构不复杂却把微信小程序和 .NET 后端的配合讲得很典型。能学到东西也能直接用前提是知道它哪里容易翻车。2. 先别急着看代码优惠券小程序的链路、选型和数据表2.1 一次领券请求的完整链路从点按到数据库要经过哪些步骤一个用户在小程序首页看到优惠券点“立即领取”这个动作不是往数据库里插一行记录那么简单。完整链路大致是小程序端用 wx.request 发起 POST 请求到后端地址形如 https://你的域名/Coupon/Receive后端 Controller 接收到参数后先解析请求里携带的登录态 token从 token 中找到用户 OpenId然后查 Coupon 表判断这张券是否在有效期内、库存是否大于 0再查 CouponRecord 表判断这个用户是否已经领过全部通过后在一个数据库事务里同时做库存扣减和领取记录插入最后把领券结果以 JSON 返回给小程序端小程序拿到结果后弹一个“领取成功”提示。这条链路里最容易出问题的环节是登录态。2019 年比较规范的做法是小程序启动时调用 wx.login 拿到一个临时 code把 code 传给后端的 Login 接口后端拿 code 去微信接口换 OpenId 和 SessionKey然后自己生成一个 token 返回给小程序端保存。后面用户再请求领券、列表时都带这个 token后端就不需要每次都请求微信了。这个设计在今天依然是可靠的轻量方案只是微信侧有些接口参数和返回字段的细节变了核心思路没变。2.2 为什么这套源码选了 MVC 的 JsonResult 而不是 Web API如果你现在用 .NET 6 或 .NET 8 做新项目首选自然是 Web API 加最小接口。但 2019 年的项目里用 ASP.NET MVC 的 Controller 直接返回 JsonResult 非常常见。原因不复杂这类源码一般同时包含面向用户的小程序接口和面向运营人员的管理后台页面。MVC 一个管道两种输出都支持既能返回 View 的 cshtml 页面也能用 JsonResult 输出给小程序的数据一套路由配置到底维护成本低。我一般不建议接手老项目时立刻推翻重写成 Web API除非你准备把所有后台页面一起重构。判断标准很简单项目里有没有后台管理页面。有页面就用 MVC只做纯接口才需要新建 WebApi。老源码里的路由配置通常长这样RegisterRoutes 里 MapRoute 一个默认路由Controller 名加 Action 名就能定位比如 /Coupon/Receive 会命中 CouponController 的 Receive 方法。这种约定式路由的好处是接口地址可读性强坏处是稍微不注意命名就容易和页面路由冲突后面避坑章节会提。2.3 数据表怎么设计才能扛住领券场景三张表加两个约束看完整版源码先看数据库脚本这是个好习惯。优惠券领取这类场景数据模型至少需要三张表而不是一张表硬扛。最基础的设计如下-- 优惠券模板表一张券的公共信息 CREATE TABLE Coupon ( Id INT IDENTITY(1,1) PRIMARY KEY, Name NVARCHAR(50) NOT NULL, -- 券名称例如“满100减30” Amount DECIMAL(10,2) NOT NULL, -- 面额 Stock INT NOT NULL DEFAULT 0, -- 剩余库存 Total INT NOT NULL DEFAULT 0, -- 总投放量 Status TINYINT NOT NULL DEFAULT 1, -- 1上架 0下架 StartTime DATETIME NOT NULL, -- 领取开始时间 EndTime DATETIME NOT NULL -- 领取结束时间 ); -- 微信用户表OpenId是业务主键 CREATE TABLE WxUser ( Id INT IDENTITY(1,1) PRIMARY KEY, OpenId NVARCHAR(64) NOT NULL UNIQUE, CreatedTime DATETIME DEFAULT GETDATE() ); -- 领取记录表用户和券的对应关系 CREATE TABLE CouponRecord ( Id INT IDENTITY(1,1) PRIMARY KEY, WxUserId INT NOT NULL, CouponId INT NOT NULL, ReceiveTime DATETIME DEFAULT GETDATE(), Status TINYINT NOT NULL DEFAULT 0 -- 0未使用 1已使用 );三张表的职责很清楚Coupon 表存券模板WxUser 表存用户CouponRecord 表存谁在什么时候领了哪张券。关键点有两个。第一不要把领取状态直接加在 Coupon 表上否则一张券被一万人领取后库存和历史记录全混在一起数据会乱。第二CouponRecord 表是中间表既关联用户也关联券后续查“某个用户领了哪些券”就是一次 Join 的事。我一般建议在 CouponRecord 表里冗余存储券名称和面额字段这样用户已领列表查询时不用再去 Join Coupon 表而且在运营改了券名或者下架券之后历史记录仍然保留用户领取时的权益快照。这个细节很多老源码没做等你在生产环境遇到“券改名后用户历史卡券显示错误”时就会明白它的价值。3. 本地联调从打开解决方案到小程序首页出现券列表3.1 改配置数据库连接串、AppId、Secret一个都不能错拿到源码第一步不是编译而是先打开 Web.config 把三处配置改对。老项目的坑在于配置经常散落在多个文件里Web.config、App.config、甚至代码里有写死的值。优先找连接字符串和微信相关配置!-- 数据库连接串Server 改成你的数据库实例名 -- connectionStrings add nameCouponDbContext connectionStringServer.;DatabaseCouponDb;User Idsa;Password你的密码; providerNameSystem.Data.SqlClient / /connectionStrings !-- 小程序配置换成你自己申请到的 AppId 和 Secret -- appSettings add keyWxAppId valuewx1234567890abcdef / add keyWxSecret valueabcdef0123456789abcdef0123456789 / /appSettings连接字符串里的 Server. 代表本机默认 SQL Server 实例密码不要用图示里这种占位符要换成真实密码。注意如果 SQL Server 是命名实例要写成 Server计算机名\实例名。WxAppId 和 WxSecret 从微信小程序平台后台获取这两个值属于敏感信息不要提交到公开代码仓库更不要写进小程序前端代码里。我见过有人把 Secret 放在小程序 js 文件里等于把钥匙挂在门口。3.2 发布到本机 IIS应用程序池选集成模式绑定 HTTPS改完配置后先编译一遍确认没有缺文件再发布。Visual Studio 里右键项目选“发布”目标选“文件夹”得到一个包含 bin 和 Views 的发布目录。接着在本机 IIS 里添加网站# 场景发布目录在 C:\wwwroot\coupon # IIS 中添加网站 # 网站名称CouponMvc # 物理路径C:\wwwroot\coupon # 绑定https 443 主机名 api.example.com # 应用程序池CouponPool.NET CLR 版本 v4.0托管管道集成 # 如果只是本地调试可以先绑定 http://localhost:8080应用程序池一定要选“集成模式”这是新手翻车最多的地方。经典模式会把请求直接交给静态文件处理器导致 /Coupon/Receive 这类路由访问时直接 404。IIS 里创建网站后右键默认应用程序池把托管管道模式改为“集成”.NET CLR 版本选 v4.0。本地调试时可以先不配 HTTPS用 http://localhost:8080 跑通业务但正式上线前必须在 IIS 绑定里改成 HTTPS 并导入证书。提示本地联调的核心目标是先让“小程序 → IIS → 数据库”这条链路通起来。只要列表接口能从数据库返回数据这一步就算成功了。3.3 小程序端把地址切到本地先跑通“请求通”这件事后端跑起来后打开小程序工程找到配置文件统一维护接口地址。老源码里最常见的坏习惯是每个页面都把请求地址写死遇到这种情况先收敛到一个变量里// config.js 里统一维护环境地址 module.exports { API_BASE: http://localhost:8080 // 本地调试 // API_BASE: https://api.example.com // 正式环境 };接下来在小程序开发者工具右上角点“详情”找到“本地设置”勾选“不校验合法域名”。只有这样才能在开发者工具里请求 http://localhost 这类非 HTTPS 地址。注意这个勾选项只对开发者工具预览生效真机预览时依然会走微信的域名校验。调试过程中如果看到控制台报“不在以下合法域名列表中”先确认这个勾有没有选上再确认 API_BASE 拼写对不对。3.4 第一个联调动作小程序页面调通列表接口跑通链路最简单的验证方式是打开小程序首页看券列表能否正常显示。首页一般长这样onLoad 里发请求请求 Coupon/List 接口返回 JSON 后 setData 渲染列表。如果列表有数据但样式乱那是 wxml 的问题如果列表空先去数据库确认 Coupon 表里插了测试数据如果请求直接失败浏览器里直接访问接口地址看能否返回 JSON。常见做法是先在浏览器里访问 https://localhost:8080/Coupon/List返回 JSON 说明后端没问题问题一定在小程序端请求地址或参数。如果浏览器访问也失败那就是 IIS 配置或路由问题按 3.2 排查应用程序池。记得在数据库里插几条测试券数据状态设 1开始时间设为昨天结束时间设为明天否则列表接口即使通了也是空数据。4. 把核心接口抄明白登录、列表和领券三段代码的细节4.1 微信登录code 换 OpenId 的 C# 实现登录是全部业务的前提。小程序端调用 wx.login 拿到 code传到后端 Login 接口后端拿 code 请求微信接口换取 OpenId。下面是老源码里最典型的写法我把它整理成可直接理解的版本[HttpPost] public async TaskJsonResult Login(string code) { // 微信小程序登录凭证校验code 只能使用一次5分钟内有效 var appId ConfigurationManager.AppSettings[WxAppId]; var secret ConfigurationManager.AppSettings[WxSecret]; var url string.Format( https://api.weixin.qq.com/sns/jscode2session?appid{0}secret{1}js_code{2}grant_typeauthorization_code, appId, secret, code); using (var http new HttpClient()) { http.Timeout TimeSpan.FromSeconds(10); // 避免微信接口慢导致线程阻塞 var json await http.GetStringAsync(url); var result JsonConvert.DeserializeObjectNewtonsoft.Json.Linq.JObject(json); // 微信返回了 errcode 说明 code 无效记录日志便于排查 if (result[errcode] ! null) return Json(new { code 1, msg 登录失败 }); string openid result[openid].ToString(); // 按 openid 查 WxUser 表没有则插入 // 生成 token用 Guid 或随机字符串更新到用户记录 return Json(new { code 0, data token }); } }这段代码有几个细节值得注意。HttpClient 每次 new 一个新实例不算优雅但老代码里常见改造时把它提成静态单例更合理。超时时间要设置否则微信接口出问题时请求会一直挂着。code 具有一次性和 5 分钟有效期所以拿到后要立刻使用不要把它存进数据库等之后再处理。返回结构统一用 code 字段区分成功失败data 或 msg 承载业务数据这个约定前后端都要遵守。4.2 防超发和防重复领取先扣库存再写记录事务包住领券接口是整个系统的核心也是最容易在生产环境出事故的地方。老代码里常见的错误是先查库存判断大于 0再执行 Update 扣库存最后 Insert 记录。这种做法在并发时一定会超发——两个请求同时读到库存 1都判断大于 0然后都执行了更新和插入。正确做法是让数据库的更新语句本身承担判断职责-- 单条事务逻辑扣减库存成功才插入领取记录 DECLARE Result INT; BEGIN TRAN; -- 先扣库存Stock 大于 0 才允许扣减ROWCOUNT 为 0 说明库存不足或券失效 UPDATE Coupon WITH(ROWLOCK) SET Stock Stock - 1 WHERE Id CouponId AND Stock 0 AND EndTime GETDATE(); IF ROWCOUNT 0 BEGIN ROLLBACK; SET Result -2; -- 库存不足或已过期 END ELSE BEGIN BEGIN TRY INSERT INTO CouponRecord(WxUserId, CouponId) VALUES(UserId, CouponId); COMMIT; SET Result 1; -- 领取成功 END TRY BEGIN CATCH ROLLBACK; SET Result -1; -- 唯一索引冲突说明重复领取 END CATCH END SELECT Result AS Result;这个写法的精妙之处在于“先更新后判断”Update 语句本身带 Stock 0 条件库存不够时影响行数就是 0直接返回失败不需要提前 SELECT。ROWLOCK 锁提示保证同一时刻只有一个事务能更新同一行。CouponRecord 表上要加一个 WxUserId CouponId 的唯一索引这样即使并发请求同时插入数据库也会拒绝第二个CATCH 里捕获重复键错误返回 -1。提示我在这个场景里更信任数据库的唯一索引而不是 IF EXISTS 先查再插。先查再插在并发下总有窗口期唯一索引才是最终防线。4.3 优惠券列表要与时间联动状态位、开始结束时间一起过滤列表接口相对简单但有个边界容易写错只过滤 Status 而忘记过滤时间。用户能看到哪些券不仅要看这张券是否上架还要看当前时间是否落在领取时间范围内SELECT TOP 50 Id, Name, Amount, Stock, StartTime, EndTime, ImageUrl FROM Coupon WITH(NOLOCK) WHERE Status 1 AND StartTime GETDATE() AND EndTime GETDATE() ORDER BY SortRank ASC, Id DESC这里用 WITH(NOLOCK) 是因为列表查询允许脏读拿到稍旧的数据对用户无影响但可以避免阻塞正在写入的领券事务。写操作的事务里千万别加 NOLOCK读操作可以。StartTime 用 EndTime 用 这样结束时间刚好到点的那一秒就刷掉用户不会再看到过期券。还需要注意时区问题。服务器如果设在非东八区GETDATE() 返回的是服务器本地时间可能导致券提前或延后过期。我处理这类老项目时会先检查数据库时间偏移如果服务器时区不对最简单又稳妥的做法是代码里统一用 DateTime.Now 从应用层传入不让数据库自己去猜时区。4.4 前后端返回格式先约定 code再谈功能老源码里最让人头疼的“黑匣子”问题是前后端返回格式不统一。有的接口失败时返回 HTTP 404有的返回 HTML 错误页有的在 JSON 里放一个 message 字段小程序端根本不知道该怎么统一处理。我接手后做的第一件事就是梳理所有接口的返回格式并和前端对齐一张状态码表code含义小程序端处理0成功展示数据或提示成功401未登录或登录态过期重新 wx.login 换 token-1已领取过该券按钮置灰提示“已领取”-2库存不足或已过期提示“手慢了下次早点来”有了这张表之后小程序端只需要在 wx.request 的 success 回调里判断 res.data.code零散的特殊判断全部去掉。调试成本会明显下降。老代码里对 HTTP 状态码的使用通常是随手写的200 和 500 混着用但这种业务成功失败用 code 表达、HTTP 状态只表示“请求通了没有”的做法才是微信小程序前后端分离的正确姿势。5. 避坑排查部署这套源码最容易翻车的五个常见问题5.1 真机预览提示“不在以下合法域名列表中”现象开发者工具里一切正常扫码真机预览后页面空白控制台提示“https://api.example.com 不在以下合法域名列表中”。这是微信小程序上线前最多见的报错没有之一。原因小程序正式环境强制校验 request 域名。开发者工具里勾选“不校验合法域名”只能临时绕过工具的限制真机上微信会严格校验请求地址是否已在小程序平台后台完成配置。如果域名没有备案信息、不是 HTTPS、或证书链不完整都会报这个错。解决登录小程序平台后台找到服务器域名配置把接口域名加入 request 合法域名列表。域名必须是 HTTPS证书要完整。很多人只填了主域名忽略了证书链中间证书缺失的问题Android 真机上会表现为偶发请求失败。我一般会用证书检测工具查一下证书链确保完整再提交体验版。5.2 列表正常一点“领取”就报 JSON 循环引用现象首页优惠券列表加载正常点领取后请求失败后端日志里出现“检测到循环引用”或 Newtonsoft.Json.JsonSerializationException 异常。原因Controller 直接返回了 EF 查询出来的数据库实体。优惠券实体里有导航属性导航属性又引用了用户或领取记录集合实体之间互相引用形成循环JSON 序列化时无法正常输出。解决不直接 return 数据库实体改用匿名类型只投影需要的字段。老代码里凡是报这个错的接口基本都是用了实体搞懒加载或导航属性。我习惯把所有接口返回都改成手动投影既解决循环引用也避免把不该暴露的字段发给小程序端。5.3 IIS 发布后接口 404站点目录却能打开现象本地 VS 运行一切正常发布到本机 IIS 后访问 /Coupon/List 返回 404但直接访问站点根目录能打开。这种问题常让人怀疑是代码问题实际上大多数情况是 IIS 配置问题。原因最常见的是应用程序池选了“经典”托管管道模式。经典模式下 ASP.NET MVC 的路由不会生效请求被当成静态文件处理找不到物理文件就返回 404。另一个可能原因是发布目录缺少 bin 文件夹或者应用程序池的 .NET CLR 版本选错。解决右键应用程序池把托管管道模式改为“集成”确认 .NET CLR 版本是 v4.0。如果改了还是 404检查发布时是否勾选了“预编译”以及右键站点是否给应用程序池账号分配了读取权限。这类问题在我排查过的老项目里出现频率很高往往不是玄学就是这几个配置点没对齐。5.4 领券成功但库存变成负数现象某张热门券发放时运营反馈领取数量超过投放量查询数据库发现 Coupon 表的 Stock 字段变成负数CouponRecord 表里出现大量记录。原因老代码用的是“先查库存再判断再更新”的逻辑。两个并发请求同时读到库存为 1都认为可以领取先后执行了更新和插入库存被扣到负数。这是典型的并发超发事故一旦发生没有后悔药只能人工补数据。解决按 4.2 的 SQL 逻辑改造把 Stock 0 条件写进 Update 语句扣减成功才插入记录并且整个包在事务里。同时给 CouponRecord 加唯一索引防重复领取。改造完用并发工具验证验证方法在最后一章会说。5.5 微信 code 报 40029 或 40163登录偶发失败现象用户登录时偶尔失败后端日志记录微信接口返回 errcode 40029code 无效或 40163code 已被使用。这种情况在开发调试时很常见线上偶尔也会出现。原因code 是一次性的同一个 code 只能换一次 OpenId。小程序端如果同时发起多个 Login 请求或者某个页面的 onLoad 里重复调用 wx.login就会导致同一个 code 被提交多次。后端拿到 code 后没有及时缓存结果而是让后续请求继续用旧 code也会出现这个问题。解决小程序端把 wx.login 的逻辑收敛成一个 Promise确保只调用一次然后把 code 传给后端后端拿到 code 后立即换取 OpenId不要把它存起来后续再用。登录接口的日志里不要完整打印 code只记录返回的 errcode 和耗时就够了否则日志系统里全是敏感凭证。6. 三个低成本小改造让这套老源码在真实流量下更稳6.1 用 ab 把并发问题提前暴露出来老源码上线前我习惯先做一轮最简单的并发验证。不用装复杂工具用 ab 就能模拟多个用户同时领券# 模拟 50 个并发总共发出 200 个领券请求 ab -n 200 -c 50 -T application/x-www-form-urlencoded \ -p receive.txt https://api.example.com/Coupon/Receivereceive.txt 里是要 POST 的参数比如 couponId 和 token。跑完后看两个结果ab 报告里的 Complete requests 和 Failed requests再查数据库Coupon 表的 Stock 减量是否等于 CouponRecord 表的记录数。如果不一致说明并发保护没做好。我一般要求两者完全相等哪怕差一条也要排查。6.2 把登录接口改成 async 并复用 HttpClient老代码里常见的问题是每请求一次就 new 一个 HttpClient接口还是同步阻塞的。在小程序高峰时段大量登录请求会占满 IIS 线程池。改造成本很低声明一个静态 HttpClient 字段登录方法改成 async原来用 .Result 或 .Wait() 的地方改成 await。改完后用压测对比线程数和响应时间会发现明显改善。这属于风险最小的优化却常被忽略。6.3 给领券记录加 RequestId前端重试时不再怕重复领取有些用户领券时网络抖动小程序端会自动重发请求结果一张券被领两次或者后端返回“已领取”但用户实际没拿到。我在 CouponRecord 表里加了一个 RequestId 字段前端每次领券时先生成一个 GUID重试时带着同一个 GUID后端插入记录前先按 RequestId 查一次如果存在就幂等返回成功。配合唯一索引这个方案能把网络重试造成的重复彻底消掉。前几年我接手过一套非常类似的老项目上线当晚领券接口超时查下来是两个问题叠在一起SQL 里没有防超发逻辑登录接口又是同步阻塞。那次之后我形成了习惯任何老源码上线前先跑并发再改代码。老项目不是不能救而是要带着生产环境的眼光去验证然后再动手。希望帮到你。本文还有配套的精品资源点击获取