☰
ASP.NET MVC C# 优惠券领取微信小程序源码解析与实战
2026/10/14 15:06:00 网站建设 项目流程

简介:这是一套面向微信小程序开发者与淘宝客项目实践者的完整源码,基于C#.NET MVC与微信小程序前后端分离架构,实现调用阿里妈妈淘宝客API进行优惠券自助搜索与领取。后台已内置内容管理、会员、订单、微信系统及WAP等模块,可直接作为二次开发基础,适合具备一定.NET基础、希望快速搭建电商导购类小程序的开发者参考学习。资源包共76个文件,约22.8MB,以js、json、wxss、wxml等小程序前端页面与配置文件为主,另含后台压缩包、数据库脚本及说明文档,前台页面与后台代码分层清晰,便于按模块阅读。开发环境为Visual Studio 2010搭配SQLServer2008与.NET 4.0,默认管理员账号及数据库连接字符串修改位置均有说明。目前已有1784人学习下载,读者可从中获取淘宝客API对接思路、小程序页面组织方式与后台管理系统结构,作为完整项目案例进行拆解与二次开发。

1. 从一份2019年的优惠券领取小程序源码说起:它到底能跑通什么

2019年前后,微信小程序生态里最典型的一类练手项目,就是“优惠券领取”。它麻雀虽小,五脏俱全:前端要处理用户授权、券列表渲染、领取按钮状态;后端要处理库存扣减、防重复领取、券码生成;数据库要处理并发下的超卖问题。很多开发者第一次接触“前后端分离 + 小程序”的完整链路,就是拿这类源码开刀的。而标题里这份“ASP.NET MVC C# 优惠券领取微信小程序源码源代码2019完整版”,本质上是一套用 C# 写服务端、用微信小程序写客户端的全栈示例。它适合谁?适合已经会一点 C#、想找一个能跑通“小程序请求 → 后端接口 → 数据库落库 → 返回结果”闭环的练手项目的人。不适合谁?不适合想直接上线运营的人,因为2019年的代码放到今天,微信登录接口、支付接口、甚至小程序基础库的写法都变过,直接部署大概率会在授权环节翻车。但作为学习“优惠券领取”这个业务模型的骨架,它依然有参考价值。这一章先把这套源码的组成和它能解决的核心问题讲清楚,后面几章再拆怎么跑、怎么改、坑在哪。

2. 这套源码的骨架:ASP.NET MVC 后端与小程序前端怎么对上

2.1 后端为什么用 ASP.NET MVC 而不是 Web API

2019年很多 C# 示例项目还在用 ASP.NET MVC 的 Controller 直接返回 JSON,而不是独立建 Web API 项目。原因很实际:MVC 模板自带路由、过滤器、依赖注入的雏形,一个 Controller 里既能返回 View 也能返回 JsonResult,对练手项目来说省事。这套优惠券源码大概率也是这个路子:一个CouponController,里面几个 Action 分别处理“获取券列表”“领取优惠券”“查询我的券”。你要做的第一件事,是确认它用的是 .NET Framework 还是 .NET Core。2019年两者都在用,但配置方式完全不同。如果是 .NET Framework,通常有Global.asax和Web.config;如果是 .NET Core,则是Startup.cs和appsettings.json。打开项目先看根目录有没有Startup.cs,这一步决定了你后面怎么改数据库连接串。

// 典型的 CouponController 结构,返回 JSON 给小程序 public class CouponController : Controller { private readonly CouponService _couponService; public CouponController() { _couponService = new CouponService(); } // 获取可领取的优惠券列表 [HttpGet] public JsonResult List() { var list = _couponService.GetAvailableCoupons(); return Json(new { code = 0, data = list }, JsonRequestBehavior.AllowGet); } // 领取优惠券,传入 couponId [HttpPost] public JsonResult Receive(int couponId) { // 从 Session 或 Token 中取用户标识 var userId = GetUserIdFromToken(); if (userId == null) { return Json(new { code = 401, msg = "未登录" }); } var result = _couponService.ReceiveCoupon(userId, couponId); return Json(result); } }

上面这段代码的关键点有三个:第一,JsonRequestBehavior.AllowGet在 .NET Framework 的 MVC 里必须加,否则 GET 请求会被框架拦掉,这是新手最容易卡住的地方;第二,用户标识不能从小程序明文传 userId,必须走服务端解析 token 或 session;第三,ReceiveCoupon的返回值要包含明确的业务码,而不是只返回 true/false,因为小程序端需要根据码值决定弹什么提示。参数上,couponId建议用 int 而不是 string,避免小程序端传字符串导致模型绑定失败。

2.2 小程序端怎么调后端接口

小程序端的核心文件是pages/coupon/list.js和对应的 wxml。2019年的写法通常用wx.request,而不是现在更常见的封装请求库。你要检查的是wx.request的url是否指向了你本地后端的地址,以及header里有没有带content-type: application/json。很多源码里 url 写的是作者当时的服务器地址,你直接跑会请求失败,必须改成你自己的局域网 IP 或本地调试地址。

// pages/coupon/list.js 中领取优惠券的典型逻辑 Page({ data: { coupons: [], loading: false }, onLoad: function () { this.fetchCouponList(); }, fetchCouponList: function () { var that = this; wx.request({ url: 'http://你的后端地址/api/coupon/list', // 必须改成自己的地址 method: 'GET', success: function (res) { if (res.data.code === 0) { that.setData({ coupons: res.data.data }); } } }); }, onReceiveTap: function (e) { var couponId = e.currentTarget.dataset.id; var that = this; if (this.data.loading) return; // 防止连点 this.setData({ loading: true }); wx.request({ url: 'http://你的后端地址/api/coupon/receive', method: 'POST', data: { couponId: couponId }, header: { 'content-type': 'application/json' }, success: function (res) { if (res.data.code === 0) { wx.showToast({ title: '领取成功' }); } else { wx.showToast({ title: res.data.msg || '领取失败', icon: 'none' }); } }, complete: function () { that.setData({ loading: false }); } }); } });

这段代码里有两个容易忽略的点:loading标志位是防连点的第一道防线,但真正防重复领取要靠后端;e.currentTarget.dataset.id取的是 wxml 里>-- 优惠券定义表 CREATE TABLE Coupon ( Id INT PRIMARY KEY IDENTITY, Name NVARCHAR(100), TotalCount INT, -- 总库存 RemainingCount INT, -- 剩余库存 StartTime DATETIME, EndTime DATETIME ); -- 用户领取记录表,必须加唯一索引 CREATE TABLE UserCoupon ( Id INT PRIMARY KEY IDENTITY, UserId NVARCHAR(64), CouponId INT, ReceiveTime DATETIME DEFAULT GETDATE(), Status INT DEFAULT 0 -- 0未使用 1已使用 2已过期 ); -- 关键:防止同一用户重复领取同一张券 CREATE UNIQUE INDEX IX_UserCoupon_User_Coupon ON UserCoupon(UserId, CouponId);

有了这个唯一索引,即使业务层判断漏了,数据库也会在插入时抛异常,你只需要在 C# 里捕获这个异常并返回“已领取”即可。这是最省事的防重方案,比在代码里加锁可靠得多。库存扣减则建议用UPDATE Coupon SET RemainingCount = RemainingCount - 1 WHERE Id = @Id AND RemainingCount > 0,然后判断受影响行数是否为 1,这样能避免超卖。

3. 把源码跑起来:从改配置到本地联调的完整步骤

3.1 后端环境的还原与数据库初始化

拿到源码后,第一步不是急着 F5 运行,而是先还原 NuGet 包和数据库。2019年的项目,packages.config里可能引用了旧版本的 Newtonsoft.Json 或 EntityFramework,如果你的 VS 版本较新,还原时可能会提示版本冲突。常见做法是右键解决方案,选择“还原 NuGet 包”,如果失败,就手动把packages.config里的版本号改成你本地能还原的稳定版。数据库方面,先看项目里有没有.sql脚本或.mdf文件。如果有.mdf,直接附加到 LocalDB 或 SQL Server Express;如果没有,就根据模型类自己建表,或者用 EF 的 Code First 迁移。

# 如果项目使用 EF Code First,可以在 Package Manager Console 中执行 Update-Database # 如果是 .NET Core,用命令行 dotnet ef database update

执行之前,务必检查Web.config或appsettings.json里的连接字符串。2019年的示例常用(LocalDb)\MSSQLLocalDB或.\SQLEXPRESS,如果你机器上没装对应实例,就会报“无法连接”。最稳妥的做法是改成你本地已有的 SQL Server 实例名,并确认账号有建库权限。这一步没有捷径,连接串不对,后面所有接口都会 500。

3.2 小程序端的合法域名与本地调试设置

微信开发者工具默认会校验请求域名是否在后台配置过。你本地跑的后端地址(比如http://192.168.1.100:8080)肯定不在合法域名里,所以必须在开发者工具右上角“详情” → “本地设置”里勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”。这个选项不勾,wx.request会直接失败,而且报错信息不明显,新手容易以为是后端挂了。

// 如果你想让请求地址可配置,可以单独建一个 config.js const config = { baseUrl: 'http://192.168.1.100:8080' // 改成你电脑的局域网 IP }; module.exports = config; // 在页面中引用 const config = require('../../utils/config.js'); wx.request({ url: config.baseUrl + '/api/coupon/list' });

把地址抽出来好处是,换网络环境时只改一个文件。注意,手机预览时不能用localhost,必须用电脑的局域网 IP,且手机和电脑要在同一个 WiFi 下。如果公司网络隔离了设备互访,这一步会卡很久,建议用手机热点给电脑和手机同时供网。

3.3 用 Postman 先验后端,再联调小程序

很多新手一上来就在小程序里点按钮,结果报错了不知道是前端传参问题还是后端逻辑问题。更高效的做法是先用 Postman 把后端接口逐个调通。以领取接口为例,先发一个 POST 请求,Body 选 raw JSON,内容为{"couponId":1},看返回的 code 是不是 0。如果返回 401,说明 token 校验没过,你需要先调登录接口拿到 token,再在 Header 里加Authorization: Bearer xxx。这一步能帮你把后端的问题和小程序的问题隔离开。

// Postman 中测试领取接口的请求体 { "couponId": 1 } // 期望的成功响应 { "code": 0, "msg": "领取成功", "data": { "couponId": 1, "receiveTime": "2025-01-01T10:00:00" } }

如果 Postman 通了,小程序还不通,那问题就在小程序端的 url、header 或参数格式上。常见的是小程序wx.request默认content-type是application/json,但如果你后端用的是[FromForm]接收,就会绑定不上,必须改成[FromBody]或让前端发 form 格式。这个细节在2019年的代码里经常不统一,需要你两边对着看。

4. 避坑与排查:这套2019源码最容易翻车的五个地方

4.1 微信登录接口变更导致授权失败

现象:小程序端调用wx.login拿到 code,传给后端换 openid,后端返回错误码 40029 或 40163。原因:2019年之后,微信对code的使用次数和有效期做了更严格的限制,且部分旧接口路径调整过。旧代码里可能还在用https://api.weixin.qq.com/sns/jscode2session的老参数拼接方式,或者没有正确处理grant_type。解决:确认后端请求的 URL 是https://api.weixin.qq.com/sns/jscode2session,参数必须包含appid、secret、js_code、grant_type=authorization_code。同时检查appid和secret是否填的你自己的小程序账号,而不是源码里作者留下的测试号。测试号早就失效了,这一步不换,登录永远过不去。

4.2 库存扣减在并发下变成负数

现象:压测时发现RemainingCount出现负数,或者同一张券被领超了。原因:旧代码用“先查再减”的两步操作,两个请求同时查到库存为 1,然后都执行减一,结果变成 -1。解决:改成原子操作UPDATE Coupon SET RemainingCount = RemainingCount - 1 WHERE Id = @Id AND RemainingCount > 0,然后判断ExecuteNonQuery返回的影响行数。如果返回 0,说明库存不足,直接返回“已领完”。这个改法不需要加锁,靠数据库行锁就能保证一致性。

4.3 小程序端 token 过期后没有续期逻辑

现象:用户用了一段时间后,点击领取没反应,后端返回 401,但前端没有任何提示。原因:2019年的示例通常把 token 存在wx.setStorageSync里,但没有处理过期。token 一过期,所有请求都失败,用户不知道发生了什么。解决:在wx.request的fail回调或统一请求封装里判断状态码,如果是 401,就清除本地 token 并跳转到登录页重新走wx.login。更优雅的做法是后端返回 401 时附带一个needLogin: true,前端据此决定是否静默重新登录。

4.4 优惠券时间判断用了客户端时间

现象:用户把手机时间改到活动开始前,就能提前领券。原因:旧代码在领取时用DateTime.Now和券的开始/结束时间比较,而DateTime.Now是服务器时间,这本身没问题。但有些源码为了“减轻服务器压力”,把时间判断放在小程序端,用new Date()比较,这就给了用户可乘之机。解决:所有时间判断必须在后端做,前端只负责展示。后端用数据库的GETDATE()或服务器时间,不要信任客户端传来的任何时间戳。

4.5 数据库连接串明文写在代码里

现象:把源码传到代码托管平台后,数据库密码泄露。原因:2019年的示例为了省事,直接把连接串写在Web.config或appsettings.json里,甚至硬编码在 C# 文件中。解决:本地开发可以用Web.config的connectionStrings节点,但上线前一定要改成环境变量或密钥管理服务。如果只是练手,至少不要把包含真实密码的配置文件提交到公开仓库。检查.gitignore有没有排除appsettings.Development.json和Web.config。

5. 从能跑到能用:给这套源码加一层防重与限流的具体技巧

把源码跑通只是第一步,真正让它接近“能用”的状态,还需要在领取接口上加一层防重和限流。我一般会在ReceiveCoupon方法里做三件事:先查缓存判断用户是否已经领过,再用数据库唯一索引兜底,最后加一个基于 Redis 的计数器限制单用户每秒请求次数。缓存用IDistributedCache或直接上 Redis,key 设计成coupon:receive:{userId}:{couponId},设置一个较短的过期时间,比如 5 秒。这样即使用户连点,也只有第一个请求能穿透到数据库。

public ReceiveResult ReceiveCoupon(string userId, int couponId) { var cacheKey = $"coupon:receive:{userId}:{couponId}"; // 尝试写入缓存,如果已存在说明正在处理或已领取 if (!_cache.TryAdd(cacheKey, "1", TimeSpan.FromSeconds(5))) { return new ReceiveResult { Code = 429, Msg = "操作太频繁,请稍后再试" }; } try { // 原子扣减库存 var affected = _db.Database.ExecuteSqlCommand( "UPDATE Coupon SET RemainingCount = RemainingCount - 1 WHERE Id = @p0 AND RemainingCount > 0", couponId); if (affected == 0) { return new ReceiveResult { Code = 400, Msg = "已领完" }; } // 插入领取记录,靠唯一索引防重 _db.UserCoupons.Add(new UserCoupon { UserId = userId, CouponId = couponId }); _db.SaveChanges(); return new ReceiveResult { Code = 0, Msg = "领取成功" }; } catch (DbUpdateException ex) when (IsUniqueConstraintViolation(ex)) { // 唯一索引冲突,说明已领取,回滚库存 _db.Database.ExecuteSqlCommand( "UPDATE Coupon SET RemainingCount = RemainingCount + 1 WHERE Id = @p0", couponId); return new ReceiveResult { Code = 409, Msg = "您已领取过该券" }; } finally { // 可选:成功后保留缓存一段时间,失败则立即删除 // _cache.Remove(cacheKey); } }

这段代码的关键在于TryAdd的原子性,它保证了同一用户同一券在 5 秒内只有一个请求能进入后续逻辑。DbUpdateException的捕获要判断是不是唯一索引冲突,不同数据库的异常码不一样,SQL Server 是 2601 或 2627,MySQL 是 1062。回滚库存这一步不能省,否则用户没领到券,库存却被扣了,活动结束后对账会发现少了很多券。最后,缓存过期时间设 5 秒是个经验值,太短防不住连点,太长会影响用户“先失败后重试”的体验。如果你用的是内存缓存而不是 Redis,多实例部署时这个防重会失效,因为每个实例的缓存是独立的。所以生产环境建议直接上 Redis,用StackExchange.Redis的StringSet带When.NotExists参数,效果和TryAdd一样。

还有一个容易被忽略的点:优惠券的“已领取”状态和“已使用”状态要分开。领取记录表里的Status字段,0 表示未使用,1 表示已使用,2 表示已过期。查询“我的券”时,要根据当前时间和券的有效期动态计算是否过期,而不是只依赖 Status 字段。我见过有人把过期券的 Status 批量改成 2,结果用户第二天打开小程序发现券没了,其实是定时任务跑早了。更稳妥的做法是查询时用WHERE EndTime > GETDATE()过滤,Status 只用来标记使用状态。这些细节在2019年的源码里通常没有,需要你自己补。希望帮到你。

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

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

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

立即咨询