☰
C#微信小程序优惠券源码拆解:登录、并发与部署避坑
2026/10/15 15:18:23 网站建设 项目流程

简介:这是一套以微信小程序为前台、ASP.NET MVC为后台的淘宝客优惠券自助领取系统源码,适合.NET开发者、小程序学习者以及想搭建优惠券/返利站点的个人站长。后台已完成阿里妈妈淘宝客API对接,并内置会员、订单、微信、WAP等可拓展模块,便于二次开发成完整网站。压缩包共76个文件,包含22个JS、17个JSON、17个WXSS、16个WXML等小程序页面与逻辑文件,以及后台工程TBKAdmin.zip和数据库脚本,整体22.8MB,目录结构清晰。已有1784人学习下载,适合有一定C#基础、希望快速上手微信小程序与API联调的开发者。通过源码可了解小程序前端请求、后台接口封装、数据库配置等完整链路,并可直接部署运行。开发环境为VS2010/SQLServer2008,默认账号admin/admin888,配置好连接字符串即可进入后台体验完整流程。

1. 一套2019年的ASP.NET MVC C#优惠券微信小程序源码,今天拆给你看

如果你在找C#微信小程序开源的完整案例,这套ASP.NET MVC C#优惠券领取微信小程序源码是很有参考价值的。它不是一个只给你看界面的Demo,而是把小程序端到后端数据库的完整链路都串起来了:微信登录、openid绑定、优惠券库存、领取记录、后台API。2019年的版本虽然界面风格偏老,但核心的领券逻辑、接口设计和部署方式放到现在依然能当脚手架用。适合刚接触.NET微信小程序开发的人,也适合想从单体ASP.NET MVC迁移到前后端分离的人。本文不评价代码好坏,只讲怎么把它跑起来,以及你在复现时最可能踩到的坑。

2. 从微信小程序到ASP.NET MVC:先搞清请求链路和登录态,再谈领券

2.1 小程序端为什么不能直接信任前端传来的用户信息

很多第一次做微信小程序的人会犯一个错误:在小程序里直接拿wx.getUserInfo返回的昵称和头像传给后端,然后后端把这些数据当成用户唯一标识。这在2019年的老项目里尤其常见,因为当时微信官方还在鼓励开发者用wx.getUserInfo拿到用户资料。

但这套源码的设计思路是符合标准做法的:小程序端只把wx.login拿到的临时code发给后端,后端通过这个code调用微信的jscode2session接口,换取openid和session_key。openid才是用户在微信生态下的唯一ID,昵称头像随时能改,拿它当账号主键必翻车。

// Controllers/WxAuthController.cs 中典型的 code2session 流程 public async Task<ActionResult> Login(string code) { string appId = ConfigurationManager.AppSettings["WxAppId"]; string appSecret = ConfigurationManager.AppSettings["WxAppSecret"]; string url = $"https://api.weixin.qq.com/sns/jscode2session?appid={appId}&secret={appSecret}&js_code={code}&grant_type=authorization_code"; using (var http = new HttpClient()) { string response = await http.GetStringAsync(url); // 返回格式: {"openid":"xxx","session_key":"xxx","unionid":"xxx"} WxSessionResult result = JsonConvert.DeserializeObject<WxSessionResult>(response); if (string.IsNullOrEmpty(result.openid)) { return Json(new { code = 40001, msg = "code无效或已被使用" }); } // 查库或创建用户 User user = db.Users.FirstOrDefault(u => u.OpenId == result.openid); if (user == null) { user = new User { OpenId = result.openid, CreatedAt = DateTime.Now }; db.Users.Add(user); db.SaveChanges(); } // 生成自定义登录态,返回给小程序 string token = Guid.NewGuid().ToString("N"); Session["token_" + token] = user.Id; Session.Timeout = 60; return Json(new { code = 0, data = new { token = token } }); } }

这段代码的逻辑核心是:前端传code,后端换openid,再生成一个随机token存在Session里。为什么不用session_key当登录态?因为session_key是微信会话密钥,本不该泄露给客户端,而且它有过期机制。很多2019年的源码直接把session_key返回前端,这是不规范的。这里用Session存token是当年单体应用的常见做法,配合Session.Timeout = 60控制会话长度,小程序端每次请求带这个token就能识别身份。

注意:jscode2session的code只能用一次。如果你在测试时发现有时能登录有时不能,先排查是不是同一个code被重复用了。另外,微信小程序开发工具里的“不校验合法域名”开关只对开发调试有效,真机预览必须配HTTPS域名,这点后文避坑章节再展开。

2.2 小程序端登录调用与请求拦截

小程序端不是每次打开都重新wx.login,而是先检查本地有没有token,没有才走登录流程。这套源码的做法是:在app.js里定义一个全局请求方法,统一带token,收到401或业务错误码时自动重新登录。

// app.js 片段 App({ globalData: { token: '', userId: 0 }, onLaunch: function () { let token = wx.getStorageSync('token'); if (token) { this.globalData.token = token; } else { this.login(); } }, login: function () { let that = this; wx.login({ success: res => { wx.request({ url: 'https://yourdomain.com/api/wxauth/login', data: { code: res.code }, success: resp => { if (resp.data.code === 0) { that.globalData.token = resp.data.data.token; wx.setStorageSync('token', that.globalData.token); } } }); } }); } });

这段代码的关键点是:登录态持久化用wx.setStorageSync,下次启动直接读。实际生产环境里token有过期时间,你还需要在request方法里统一处理“token过期”的情况。给个建议:后端返回401时,主动清除本地token并重新调用login(),避免用户看到白屏。很多2019年的老代码没做这层处理,导致会话断了之后接口全挂,体验很差。

小程序端request封装时,公共header里要加Authorization: Bearer token,或者按源码里习惯用token字段。后端MVC里从Request.Headers读取时要注意大小写问题,建议统一小写处理。

2.3 数据库表设计:用户表、优惠券表、领取记录表

这套源码的核心表一般有三张:User、Coupon、CouponLog。CouponLog是用来记录谁在什么时候领了哪张券,同时也是做“一人一券”约束的保障。

-- 核心建表脚本(精简版) CREATE TABLE [dbo].[User]( [Id] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY, [OpenId] [nvarchar](100) NOT NULL, [NickName] [nvarchar](200) NULL, [CreatedAt] [datetime] NOT NULL DEFAULT GETDATE() ) CREATE TABLE [dbo].[Coupon]( [Id] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY, [Title] [nvarchar](200) NOT NULL, [Stock] [int] NOT NULL, [StartTime] [datetime] NULL, [EndTime] [datetime] NULL, [Status] [int] NOT NULL DEFAULT 0 -- 0:可领 1:下架 ) CREATE TABLE [dbo].[CouponLog]( [Id] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY, [UserId] [int] NOT NULL, [CouponId] [int] NOT NULL, [ReceiveTime] [datetime] NOT NULL DEFAULT GETDATE(), [Status] [int] NOT NULL DEFAULT 0 -- 0:正常 1:已使用 -1:过期 )

CouponLog表上最值得注意的约束是应该是唯一索引:(UserId, CouponId)组合唯一,这样数据库层面就杜绝了一人多领。可惜很多2019年的源码没建这个索引,只在应用层用if (log != null)判断,并发一高就会出现同一用户领到两张同款券的情况。后面避坑章节我会专门说这个。

Coupon表的Stock字段是库存,领券时要做原子扣减,千万不能在应用层“先查库存再减”,不然并发请求会把库存扣成负数。

3. 优惠券领取核心逻辑:一人一券、库存扣减与并发控制的落地写法

3.1 用数据库更新而不是先查后改:库存扣减的原子操作

优惠券领取最常见的翻车点是超发。你写if (coupon.Stock > 0) { coupon.Stock--; db.SaveChanges(); },看起来没毛病,但两个用户同时请求时,两个请求都读到Stock = 1,然后都执行扣减,最终库存变成 -1。

正确的做法是在SQL层面做条件更新。这套源码里如果没做好,你应该自己动手改成下面这种写法。

// 事务里先扣库存,再写领取记录,最后校验影响行数 using (DbContextTransaction tx = db.Database.BeginTransaction()) { string sql = "UPDATE Coupon SET Stock = Stock - 1 WHERE Id = @id AND Stock > 0"; int rows = db.Database.ExecuteSqlCommand(sql, new SqlParameter("@id", couponId)); if (rows == 0) { tx.Rollback(); return Json(new { code = 40003, msg = "优惠券已抢光" }); } // 再检查这个用户是否已经领过 bool exists = db.CouponLogs.Any(l => l.UserId == userId && l.CouponId == couponId); if (exists) { tx.Rollback(); return Json(new { code = 40002, msg = "您已领取过该优惠券" }); } db.CouponLogs.Add(new CouponLog { UserId = userId, CouponId = couponId, ReceiveTime = DateTime.Now }); db.SaveChanges(); tx.Commit(); }

这段代码里有三个关键点。第一,UPDATE ... WHERE Stock > 0是原子操作,SQL Server会锁行,两个并发请求只有一个能影响1行,另一个影响0行,直接返回抢光。第二,检查是否领取过放在扣库存之后,虽然逻辑上应该先检查,但为了减少锁持有时间,这里先扣了再回滚也安全,只是会短暂占用一次库存。第三,用ExecuteSqlCommand直接执行原生SQL,绕开EF的乐观并发,这样最可控。

如果你用的是EF6以上,也可以用rowversion做乐观并发,但原生SQL在2019年老项目里最直接。注意事务里别写日志文件写入、外部HTTP请求这类耗时操作,会拉长事务和锁的持有时间。

3.2 幂等判断:领取记录是唯一的,别依赖应用层

除了库存,一人一券的约束必须落到数据库唯一索引上。很多源码在应用层用Any判断,但并发场景下两个请求同时通过判断、同时写记录,就会重复。正确做法是给CouponLog表加唯一约束。

-- 在 CouponLog 上建唯一索引,防止同一用户同一优惠券重复领取 IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'UQ_CouponLog_UserId_CouponId') BEGIN CREATE UNIQUE INDEX UQ_CouponLog_UserId_CouponId ON CouponLog(UserId, CouponId); END

加了唯一索引后,即使应用层判断漏了,数据库也会抛唯一键冲突异常。后端代码里要把这个异常捕获并转成友好提示。

try { db.CouponLogs.Add(new CouponLog { UserId = userId, CouponId = couponId }); db.SaveChanges(); } catch (DbUpdateException ex) { if (ex.InnerException != null && ex.InnerException.Message.Contains("UQ_CouponLog_UserId_CouponId")) { tx.Rollback(); return Json(new { code = 40002, msg = "您已领取过该优惠券" }); } throw; }

这个做法的好处是:数据库帮你兜底,不管并发怎么冲,最终只有一条记录。这也是生产环境必须做的保障。我在给某开发者排查线上问题时就遇到过只靠应用层判断的“老司机”源码,10个用户同时点领券,结果8个都成功了,后台数据一查,同一个用户领了三张同券。从那以后我每接一个优惠券项目,第一件事就是检查有没有唯一索引。

3.3 后端API的参数校验与错误码约定

ASP.NET MVC里如果接口不做校验,客户端传个空couponId,你int.Parse就得炸。2019年的老项目喜欢用int.Parse,但更稳妥的是TryParse或者直接用EF的int?参数。

// 领券接口入口 public async Task<ActionResult> Receive(string token, int? couponId) { if (!couponId.HasValue || couponId.Value <= 0) { return Json(new { code = 40000, msg = "优惠券ID不能为空" }); } // 校验token并获取userId int? userId = GetUserIdByToken(token); if (!userId.HasValue) { return Json(new { code = 40100, msg = "登录态已过期" }); } // 调用核心领券逻辑... }

这里的token参数其实是从Header里取的更规范,但老项目往往直接放查询字符串,为了兼容可以先查Query再查Header。错误码建议统一格式:40000参数错误、40002重复领取、40003库存不足、40100未登录。小程序端拿到错误码后展示对应提示,而不是把后端异常文本直接吐给用户。

老项目还有个通病:MVC Action没有添加[HttpPost]限制,导致GET也能调领券接口。建议给所有写操作加上[HttpPost]或[AcceptVerbs(HttpVerbs.Post)],防止被恶意利用。

4. 把源码跑起来:环境准备、数据库脚本和微信小程序配置

4.1 环境清单与版本选择

这套源码是基于2019年的技术栈写的,建议你安装匹配的版本,别拿最新版硬跑。我常用的组合是:

组件推荐版本说明
Visual Studio2017/20192019版本对ASP.NET MVC 5支持完好
.NET Framework4.7.2老项目几乎都是Framework,别用.NET Core硬改
SQL Server2016/2017/2019脚本是T-SQL,2008以上基本都能跑
微信开发者工具最新稳定版即可用于打开小程序目录,但调试需关域名校验
IIS ExpressVS自带本地调试用,但真机预览需外网HTTPS

如果你电脑已经装了VS2022,也能打开这个老项目,但可能需要安装额外的“ASP.NET和Web开发”工作负载,并且项目文件里的版本号会提示升级,建议保留原项目格式别自动升级,以免路由或配置被改写。

4.2 数据库脚本导入与连接字符串修改

一般源码包里带一个database.sql或db_coupon.sql脚本。用SQL Server Management Studio (SSMS) 打开,先建一个库,比如CouponDB,然后执行脚本。脚本里可能包含创建表的语句和基础数据,但不会有视图或存储过程的话,直接执行就行。

执行后,打开Web.config找到connectionStrings,修改连接字符串:

<connectionStrings> <add name="DefaultConnection" connectionString="Data Source=.;Initial Catalog=CouponDB;User ID=sa;Password=你的密码;MultipleActiveResultSets=True" providerName="System.Data.SqlClient" /> </connectionStrings>

Data Source=.表示本机默认实例,如果你装了命名实例,改成localhost\SQLEXPRESS这种。MultipleActiveResultSets=True在EF里比较重要,否则某些嵌套读操作会报“MARS error”。如果SQL Server开启了远程可访问,测试时直接连也行,但部署到服务器时建议改成服务器的IP和端口。

4.3 小程序端 app.js 与 request 封装调整

小程序端工程里找到utils/request.js或app.js,里面会有apiBaseUrl这样的配置。2019年的源码通常写死一个IP或localhost,你需要改成自己的开发环境地址。

// config.js 示例 module.exports = { BASE_URL: 'http://localhost:5200', // 本地调试地址,真机预览时改成局域网IP // 生产环境必须是HTTPS域名,如 https://api.coupon.com };

请求封装时,注意小程序wx.request的url不支持带端口之后的直接拼接,建议统一用BASE_URL + '/api/xxx'的方式。另外,wx.request的method默认是GET,领券接口必须指定method: 'POST',并且正确设置header: { 'content-type': 'application/json' }。

如果后端返回的JSON里有code字段,小程序端要做个全局解包,统一处理code === 0的成功分支和错误分支。这块源码里一般有,但2019年的写法可能比较粗,你可以参考以下模式:

function request(url, data, method = 'GET') { return new Promise((resolve, reject) => { wx.request({ url: getApp().globalData.baseUrl + url, data: data, method: method, header: { 'token': wx.getStorageSync('token') || '', 'content-type': 'application/json' }, success: (res) => { if (res.data.code === 0) { resolve(res.data.data); } else if (res.data.code === 40100) { // token过期处理 getApp().login(); reject(res.data); } else { wx.showToast({ title: res.data.msg, icon: 'none' }); reject(res.data); } }, fail: reject }); }); }

这段封装做了三件关键事:自动带token、解包数据、全局处理登录失效。很多项目里能跑通但体验差,就是因为在每个页面里重复写错误弹窗,改成这个模式后,页面代码会清爽很多。

5. 配置与部署避坑:API 404、HTTPS、跨域、Session 丢失的真实排查记录

5.1 现象:小程序真机预览,所有接口请求404

本地调试用localhost:5200,开发者工具里勾选“不校验合法域名”能跑通。换到真机预览后,手机访问电脑的IP,比如http://192.168.1.10:5200/api/...,结果全部404。

原因:小程序真机预览不允许访问HTTP域名,官方强制要求HTTPS。其次,你的电脑防火墙可能挡了5200端口,手机根本访问不到。还有一层:IIS Express默认只绑定localhost,不接受局域网IP访问。

解决:第一,开发阶段可以用ngrok之类的内网穿透工具把本地HTTP转成HTTPS外网地址,但注意免费版域名会变化,适合临时调试。第二,修改IIS Express的applicationhost.config,把绑定地址改成*:5200,并允许局域网访问。第三,正规做法是后端部署到带SSL证书的服务器上,小程序后台配置合法域名。

提示:2019年的老源码大量使用了HTTP地址,直接上线是不可能通过微信审核的。你至少需要一个备案域名和SSL证书,这是小程序的基本门槛。

5.2 现象:同一用户短时间内重复点击“领取”,成功领取了两次

页面没做按钮禁用,又没限制点击频率,用户双击两次,前端发了两个请求。后端应用层判断没有唯一索引,两个请求同时到达,都通过了!exists检查,数据库里插入了两条记录。

原因:没有数据库级唯一约束,也没有对请求做防重处理。解决:按前文3.2节的方法给CouponLog表加唯一索引;前端在按钮点击后立即禁用并加防抖;后端接口里可以做基于userId + couponId的分布式锁或事务内重复判断。三层防线最好都上,最低成本的是数据库唯一索引。

5.3 现象:部署到IIS后,接口返回500,但本地调试正常

本地IIS Express跑得好好的,发布到Windows Server IIS后,访问领券接口直接500。打开浏览器开发者工具,发现请求头里多了一条Authorization,或者可能是静态资源404。

原因:常见的有三个。第一,IIS没有安装ASP.NET MVC依赖,可能是缺少ASP.NET角色功能。第二,老项目的bin目录里缺少System.Web.Mvc.dll等必需DLL,发布时需要勾选“在发布前删除所有文件”并包含所有依赖。第三,IIS应用池的.NET CLR版本设置成了“无托管代码”,应该改成.NET CLR v4.0.30319。

解决:在服务器上打开“服务器管理器-添加功能”,勾选“.NET Framework 4.7 功能”,重启IIS;确认应用池的“托管管道模式”为“集成”,.NET CLR版本为v4.0。如果还报错,看事件查看器的应用程序日志,里面有详细异常堆栈。我遇到过某开发者在服务器上只复制了站点文件,忘记注册ASP.NET的情况,处理了一整天。

5.4 现象:Session里的token经常失效,用户玩着玩着就被踢下线

小程序端刚登录完,领券接口调用顺利,过几分钟再领一张,后端提示“登录态已过期”。查代码,登录时设置了Session["token_"+token],但读取时没读到。

原因:ASP.NET MVC的Session默认存在进程内(InProc),应用池被回收、服务器重启、多个服务器负载均衡时都会丢。2019年的老项目通常没配置Redis或SQLServer作为Session状态存储。

解决:如果你只是单机部署,可以把Session模式改成StateServer,在Web.config的system.web节点下配置:

<system.web> <sessionState mode="StateServer" stateConnectionString="tcp=127.0.0.1:42424" cookieless="false" timeout="60"> </sessionState> </system.web>

这需要服务器上启动ASP.NET状态服务。更稳妥的方案是改用JWT,把token签名后发放,后端无状态验证。不过这套老源码在单体应用里直接用Session也够了,只要别给用户设置太短的超时时间,并处理进程回收问题。

5.5 现象:登录成功后,个别用户头像昵称显示不全

原因:小程序改版后wx.getUserInfo不再弹授权框,用户头像昵称需要通过open-type="getUserInfo"或新版头像昵称填写能力获取。2019年老代码调用的wx.getUserProfile(这是2021年才出现的)或者没做兼容处理,导致部分老基础库没有这个方法。

解决:源码里如果写死wx.getUserInfo,建议升级为wx.getUserProfile,或者干脆用微信的“头像昵称填写能力”组件,让用户手动选择头像和昵称。后端保存时做一次转义,防止XSS。这个坑不算致命,但会影响用户体验,需要你改一下小程序端的用户授权逻辑。

6. 从领券小程序延伸到多门店系统:加一份操作日志和并发压测

当你能跑通这套源码后,建议别急着搬上线,先做两件事:加操作日志,再搞一次简单的并发压测。操作日志不是只记录谁领了券,而是要把每一次请求的关键链路都记下来:请求参数、token对应用户、领券结果、数据库影响行数、耗时。我一般会在领券接口里加一个Logger,把couponId、userId、resultCode写入单独的日志表或文件,排错时能直接定位问题。

并发压测方面,用JMeter或哪怕是一个小小的控制台程序并发发10个请求,观察Stock最终值。如果你没做事务扣库存,压测后库存可能变成负数。我第一次做这个压测时,因为没用唯一索引,一个人领了5张券的系统也跑出来了,从那以后我每次接手优惠券类项目,都强制先检查着三样:库存扣减SQL、领取记录唯一索引、Session或token过期策略。压测通过后,再考虑数据库连接池大小和接口超时设置。

此外,多门店场景下,你需要在Coupon表上加StoreId字段,领取逻辑里也要校验用户当前店铺范围,这就得改WHERE条件。这套源码作为一个200多行的核心逻辑框架,扩展点都在可控范围内。希望这份拆解能帮你在自己的项目里少绕一些弯路。

希望这些血泪经验能帮到你。

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

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

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

立即咨询