☰
老源码新用:ASP.NET MVC优惠券小程序部署与避坑指南
2026/10/10 12:32:19 网站建设 项目流程

简介:这是一份基于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 name="CouponDbContext" connectionString="Server=.;Database=CouponDb;User Id=sa;Password=你的密码;" providerName="System.Data.SqlClient" /> </connectionStrings> <!-- 小程序配置:换成你自己申请到的 AppId 和 Secret --> <appSettings> <add key="WxAppId" value="wx1234567890abcdef" /> <add key="WxSecret" value="abcdef0123456789abcdef0123456789" /> </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 Task<JsonResult> 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_type=authorization_code", appId, secret, code); using (var http = new HttpClient()) { http.Timeout = TimeSpan.FromSeconds(10); // 避免微信接口慢导致线程阻塞 var json = await http.GetStringAsync(url); var result = JsonConvert.DeserializeObject<Newtonsoft.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 40029(code 无效)或 40163(code 已被使用)。这种情况在开发调试时很常见,线上偶尔也会出现。

原因: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/Receive

receive.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 里没有防超发逻辑,登录接口又是同步阻塞。那次之后我形成了习惯:任何老源码上线前先跑并发,再改代码。老项目不是不能救,而是要带着生产环境的眼光去验证,然后再动手。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询