
简介这是基于ASP.NET开发的手机端问卷调查平台源码面向需要快速搭建在线问卷系统的开发者和企业支持问卷创建、发布、管理、结果收集与导出Excel等完整流程也适用于学习Web表单设计与移动端适配的学员。源码包共1232个文件压缩包大小38.43MB涵盖CS后端逻辑、ASPX/ASHX页面处理、JS/CSS前端交互、PNG/JPG/GIF界面素材以及DLL运行库等类型并含数据库备份文件结构完整便于部署与二次开发。资源提供用户注册登录、个人中心问卷管理、分享链接投票、结果数据查看与下载等功能模块另附演示视频和说明文档方便对照理解。目前已有838人学习下载适合有一定ASP.NET基础、希望研究问卷类系统前后端实现细节的开发者参考。1. 免费问卷的尽头是自建H5手机端与ASP.NET源码这套组合的价值做运营的朋友常来问一句话为什么还要自己搭问卷答案藏在标题的后半句里——免费、H5手机端、aspnet源码。SaaS问卷平台在免费档位会限答卷数、挂水印、导数据要付费而一套ASP.NET问卷系统的源码放到自己手里只需要一台能跑IIS的Windows机器页面就能在微信、手机浏览器、App内嵌WebView里同时打开数据全落自己的数据库。这套东西最适合三类人被免费问卷平台额度卡住的活动运营需要把问卷嵌进自研H5页面或公众号菜单的前端以及想用真实业务练手ASP.NET MVC的初级开发。说“免费”不等于免运维而是源码和基础运行环境零授权费起步后续加题、改样式、接企业微信都不受第三方限制。2. 先把边界定清楚ASP.NET平台下的H5问卷系统该做什么、不做什么2.1 为什么这些年还是有人用ASP.NET做H5问卷新项目我们当然可以用前后端分离Vue或React写SPA配上.NET 8 Web API也能跑。但回到“问卷”这个具体场景有个反直觉的事实问卷页面的交互深度非常浅无非是单选、多选、填空、滑块提交一个表单看结果。这种场景用Razor服务端渲染能少踩大量兼容性坑——移动浏览器版本杂老iPhone、安卓低版本WebView里SPA打包后的JS报错不好排查而服务端直出的HTML在微信内置浏览器里打开永远是稳定的第一档体验。这类带源码的项目里最常见的组合是ASP.NET MVC 5加上Razor视图旧一点的项目跑在.NET Framework 4.7.2新的会迁到.NET 6/8再套一个最小API。接到手先看一眼目标框架再定依赖不要上来就升级老源码里Response.Redirect配合IIS Rewrite的写法在跨版本迁移时很容易碎。2.2 功能边界发布、填写、统计三件事一套能用起来的H5问卷系统功能边界要克制。我一般会把“我们要做”和“我们不做什么”摆到桌面上一起讨论。核心三件事创建问卷并生成带ID的短链接手机端打开链接后完成作答后台按问卷ID聚合出回收量和选项占比。至于题目拖拽排序、多语言、分页逻辑、问卷逻辑跳转都属于“第二版再说”的部分第一版引入这些只会让源码里的坑成倍增长。只在本地存数据不导出Excel的项目迟早会被运营追着要求补导出功能。所以数据模型设计时就要留好冗余字段答题明细表里同时存选项文本和题目ID导出时才能直接生成可读的Excel不用再去连表翻译。2.3 数据模型四张表的字段取舍问卷域的数据模型比大多数业务系统都要规矩四张表能覆盖九成需求领域模型的映射也干净。public class Survey { public int Id { get; set; } public string Title { get; set; } public string Description { get; set; } public bool IsPublished { get; set; } public DateTime CreatedAt { get; set; } public ListQuestion Questions { get; set; } } public enum QuestionType { SingleChoice 0, MultiChoice 1, TextAnswer 2 } public class Question { public int Id { get; set; } public int SurveyId { get; set; } public string Content { get; set; } public QuestionType Type { get; set; } public int Sort { get; set; } public ListOption Options { get; set; } }Question里的Sort字段不能省它是手机端H5渲染时题目顺序的唯一依据。数据库里不要靠主键排序问卷题目有插入、上移、下移操作主键顺序和展示顺序几乎一定不一致。public class Answer { public int Id { get; set; } public int SurveyId { get; set; } public int QuestionId { get; set; } public string Value { get; set; } public string SubmitId { get; set; } public DateTime SubmittedAt { get; set; } }Answer表里特意放了一个SubmitId字段它是一份问卷唯一提交标识前端打开问卷时生成一次提交时随表单带过来后端靠它做防重。这个字段比用IP、Cookie防重复可靠得多——手机网络的出口IP经常是同一个微信内置浏览器的Cookie政策也在收紧。表名核心字段作用SurveysId, Title, IsPublished, CreatedAt问卷主表QuestionsSurveyId, Type, Sort, Content题目表OptionsQuestionId, Content选项表文本题不写AnswersSubmitId, SurveyId, QuestionId, Value答卷明细SubmitId做唯一索引大字段不要塞在表里用户答卷时花几分钟输入的一段长文本用nvarchar(max)存没问题但在统计列表页不要默认聚合出来。独立出一个AnswerDetail表或者用ReadModel按需读取都会更省事。不过如果只是为了几百份问卷的量级单表也跑得很稳不需要过度设计。3. 最小可复现从空项目到ASP.NET MVC问卷核心的落代码路径3.1 项目骨架与连接配置用Visual Studio Community版新建ASP.NET MVC项目即可框架版本不必纠结老环境的选.NET Framework 4.7.2能装.NET SDK的就直接上.NET 8。问卷核心代码的写法在两个框架下几乎一致区别主要在Startup配置方式。先建好实体类和DbContextDbContext里只注册四个DbSet。public class PhoneSurveyContext : DbContext { public DbSetSurvey Surveys { get; set; } public DbSetQuestion Questions { get; set; } public DbSetOption Options { get; set; } public DbSetAnswer Answers { get; set; } }连接串放在Web.config或appsettings.json里指向SQL Server或LocalDB都可以。启动初期用LocalDB零配置就能跑但部署到服务器后一定要换正式SQL Server并顺手把连接串里的密码加密或者至少用独立帐号而不是sa。connectionStrings add namePhoneSurveyContext providerNameSystem.Data.SqlClient connectionStringserver.;databasePhoneSurvey;uidsurvey_user;pwdyour_password;MultipleActiveResultSetstrue / /connectionStringsMultipleActiveResultSetstrue在这个场景很关键。问卷提交接口里可能连续执行多次查询其中一次遍历Question列表拉取Options时连接若被前一个DataReader占住会直接报“已有打开的与此连接相关联的DataReader”。这个坑在手机端弱网环境下出现的概率比局域网调试高得多。3.2 发布与填写一个Controller两个Action问卷页面的访问路径直接设计成短链接形态/s/5代表问卷ID为5这个链接用二维码工具转成码贴到海报上顺手也能统计扫码来源。Controller里两个Action就够——Fill负责渲染H5表单页Submit负责接收提交。public class SurveyController : Controller { private readonly PhoneSurveyContext db new PhoneSurveyContext(); public ActionResult Fill(int id) { var survey db.Surveys .Include(s s.Questions.Select(q q.Options)) .FirstOrDefault(s s.Id id s.IsPublished); if (survey null) return HttpNotFound(); return View(survey); } [HttpPost] [ValidateAntiForgeryToken] public JsonResult Submit(int id) { var submitId Request.Form[submitId]; if (string.IsNullOrEmpty(submitId)) return Json(new { ok false, msg 缺少提交标识 }); var rows new ListAnswer(); foreach (var key in Request.Form.AllKeys) { if (!key.StartsWith(q_)) continue; var qid int.Parse(key.Substring(2)); var value Request.Form[key]; if (string.IsNullOrWhiteSpace(value)) continue; rows.Add(new Answer { SurveyId id, QuestionId qid, Value value, SubmitId submitId, SubmittedAt DateTime.Now }); } if (rows.Count 0) return Json(new { ok false, msg 没有有效作答内容 }); using (var tx db.Database.BeginTransaction()) { db.Answers.AddRange(rows); try { db.SaveChanges(); tx.Commit(); return Json(new { ok true, msg 提交成功 }); } catch (DbUpdateException) { tx.Rollback(); return Json(new { ok false, msg 请勿重复提交 }); } } } }Submit的解析逻辑走的是q_题目ID命名规约表单里所有带此前缀的字段都会被收集。这里有一点要提醒如果提交里混入题目ID但选项值为空直接跳过不参与入库避免半空的脏数据。防重复提交靠的是数据库侧的唯一索引不能只靠前端禁用按钮用户快速双击、微信内慢网络下重复POST都会走到后端。在Answers表执行下面这条SQL再配合事务重复提交到第二次时SaveChanges会抛DbUpdateException直接捕获后提示即可。CREATE UNIQUE INDEX IX_SurveySubmit ON Answers(SurveyId, SubmitId);3.3 H5作答页Razor直出加原生JS提交作答页用Razor循环题目选项用input标签渲染这样做的好处是服务端输出HTML手机上打开不依赖任何前端框架加载即渲染。核心交互逻辑只有两段收集表单数据、以FormData方式POST提交。model Demo.PhoneSurvey.Models.Survey form idsurveyForm foreach (var q in Model.Questions.OrderBy(x x.Sort)) { div classquestion pq.Content/p if (q.Type Demo.PhoneSurvey.Models.QuestionType.SingleChoice) { foreach (var o in q.Options) { label input typeradio nameq_q.Id valueo.Content / o.Content /label } } else if (q.Type Demo.PhoneSurvey.Models.QuestionType.MultiChoice) { foreach (var o in q.Options) { label input typecheckbox nameq_q.Id valueo.Content / o.Content /label } } else { textarea nameq_q.Id rows3 stylewidth:100%/textarea } /div } button typebutton idsubmitBtn提交问卷/button /form注意多选和单选共用同一个字段名q_题目ID时FormData会把多个值拼成逗号分隔的字符串。后端解析不需要额外处理直接存Value字段统计时再做拆分。这比生成q_3_0、q_3_1这种复杂命名方式简单得多也保留了原始选项文本用于导出。var btn document.getElementById(submitBtn); btn.addEventListener(click, function () { btn.disabled true; var form document.getElementById(surveyForm); var fd new FormData(form); fd.append(submitId, localStorage.getItem(survey_submit_id) || generateId()); localStorage.removeItem(survey_submit_id); fetch(/s/ Model.Id /submit, { method: POST, body: fd, headers: { RequestVerificationToken: $(input[name__RequestVerificationToken]).val() } }).then(function (r) { return r.json(); }).then(function (d) { if (d.ok) { localStorage.removeItem(survey_draft); alert(提交成功感谢参与); } else { alert(d.msg); btn.disabled false; } }).catch(function () { alert(网络异常请稍后重试); btn.disabled false; }); }); function generateId() { return Date.now().toString(36) _ Math.random().toString(36).slice(2, 8); }提交成功或失败时都要把按钮的disabled状态还原不然弱网环境第一次请求超时用户会被卡死在“不可点击”的页面上。generateId里用时间戳加随机串在生产环境如果要严格唯一可以把用户ID或设备标识一并拼进去但问卷场景下不必过度设计。提示ValidateAntiForgeryToken需要配合前端带上Token请求头上面的示例用jQuery取值只是为了示意原生写法是document.querySelector(input[name__RequestVerificationToken]).value。老源码项目里如果报“防伪令牌验证失败”先查这一步。3.4 统计一把梭用SQL数清楚回收与选项占比问卷后台的统计页面我通常不写复杂ORM联表直接让DbContext执行业务SQL结果映射到可读模型。运营要看的不是原始答卷而是“发了多少份、收回来多少、哪个选项选的人多”。SELECT q.Id, q.Content, COUNT(DISTINCT a.SubmitId) AS AnswerCount FROM Questions q LEFT JOIN Answers a ON a.QuestionId q.Id WHERE q.SurveyId surveyId GROUP BY q.Id, q.Content;按题目拆选项占比时核心匹配条件是把Answer的Value和Option的Content做等值连接。单选、多选都适用多选时同一条答案里如果有多个选项文本需要先在应用层按逗号拆分再分别计数。SELECT o.Content, COUNT(a.Id) AS Cnt FROM Options o LEFT JOIN Answers a ON a.QuestionId o.QuestionId AND a.Value LIKE % o.Content % WHERE o.QuestionId questionId GROUP BY o.Content ORDER BY Cnt DESC;LIKE匹配只对逗号拆分的多选场景够用万一用户填空题里恰好包含某个选项文本会被误计。更严谨的做法是在写入Answer时就把多选值拆成多行每条一个选项。这里不硬推取决于你对数据准确性的容忍度问卷场景我见过不少直接用LIKE的数据量小时误差可接受。4. 手机端H5体验的四个必调项适配、触控、真机调试与微信内嵌WebView4.1 viewport与最小宽度思维H5问卷页面真正开在什么环境里比设计稿长什么样重要得多。朋友圈分享的问卷页三成在微信内置浏览器打开四成在手机浏览器剩下的可能在企业微信、钉钉或自研App的WebView里。第一行viewport就是生死线。meta nameviewport contentwidthdevice-width, initial-scale1.0, maximum-scale1.0, user-scalableno /widthdevice-width让页面宽度等于设备独立像素宽度initial-scale1.0保证默认不缩放。user-scalableno会禁用双指缩放能防止用户在答题时误触放大导致页面横向漂移但苹果的Safari从iOS 10开始忽略这个属性真机上仍可能缩放所以还要配合CSS里font-size: 16px以上保证输入框聚焦时不自动放大。页面的最小布局宽度按320px来做基准这个数字覆盖了iPhone SE和大量安卓机型的竖屏宽度。PC浏览器打开时居中加一个max-width容器宽度超过640px就显示浅灰背景不要试图做成全宽表格。4.2 触控区域与软键盘的坑手机端问卷最容易被忽略的是可点击区域大小。选项label区域至少44像素高这是触控规范里的最小安全值低于这个尺寸在安卓上误触概率会明显上升。单选radio本身的可点区域只有20像素左右正确的做法是把整个label设为块级点文字即选项而不是要求用户精准点到那个小圆圈上。文本输入框的font-size不要小于16px否则iOS聚焦时自动放大页面用户答一道题要缩放一次体验灾难。下拉选择、日期这类原生控件在安卓和iOS上样式不一致问卷场景尽量少用让用户点选项比让ta滚动选择器可靠。草稿保存是手机端问卷体验的分水岭。用户答到一半接到电话切回来页面全空直接流失。用一个change事件监听配合localStorage把表单状态实时存住var form document.getElementById(surveyForm); form.addEventListener(change, function () { var fd new FormData(form); var obj {}; fd.forEach(function (v, k) { obj[k] v; }); localStorage.setItem(survey_draft, JSON.stringify(obj)); });回填逻辑在页面加载时读取把radio和checkbox的checked状态还原。提交成功后立即removeItem否则用户第二天再点链接进来看到的还是上一次的残留数据。这个细节经常被当成“灵异问题”其实是草稿没有按提交标识清除干净。4.3 把项目跑给手机看局域网IP与二维码开发时老是盯着浏览器模拟器会漏掉一半移动端问题。让手机和电脑连同一个Wi-Fi把项目跑起来后用本机IP加端口直接访问。Visual Studio默认的IIS Express只监听localhost需要先绑定本机IP。netsh http add urlacl urlhttp://192.168.1.10:5000/ userEveryone然后在项目属性的“调试”里把启动URL改成这个地址或者直接用IIS Express的applicationhost.config里binding节点加一条。IP地址用ipconfig查拿到后手机浏览器直接输入。要注意Windows防火墙默认会拦掉外部的5000端口入站请求顺手放行netsh advfirewall firewall add rule nameSurveyDev5000 dirin actionallow protocolTCP localport5000访问通之后找任意一个二维码生成工具把URL转成二维码微信扫码打开填问卷F12看Network面板。真机调试能同时暴露三个问题页面的F12模拟宽度和真实屏占比的偏差、弱网下的请求超时时间、微信内置浏览器的缓存策略。4.4 微信内置浏览器与App内嵌WebView的适配问卷链接在微信里打开第一个拦路虎是缓存。改完CSS或JS重新扫码经常看到旧版本原因是微信WebView对静态资源做了强缓存。要么在静态资源URL后面加版本号参数要么在输出HTML时给meta标签加禁止缓存meta http-equivCache-Control contentno-cache, no-store, must-revalidate /真正在公众号菜单里挂问卷还要走微信网页授权拿用户身份这需要JS接口安全域名和OAuth2的redirect_uri做域名校验H5页面和ASP.NET后台必须在同一域名下。定位功能同理微信里调起定位接口要求HTTPS并且配置JS安全域名。开发阶段可以用微信开发者工具里的“公众号网页”模拟扫码调试比一遍遍发预览省时间。嵌入到自研App的WebView时注意不要加window.opener相关的跳转逻辑部分安卓WebView对其支持不完整。App内页面需要拿到用户ID时常见做法是客户端通过URL拼接参数传进来后端在Load时读取并隐藏到隐式域提交时随表单带回不要依赖CookieWebView的Cookie策略各家差异很大。5. 从免费部署到二次开发这套问卷源码的下一刀切在哪5.1 局域网起步验证第一跑先不要折腾公网把发布包部署到本机IIS用局域网访问验证核心链路。发布前把Views编译选项打开避免线上首次访问动态编译造成的卡顿。站点绑定IP加端口物理路径指向发布目录应用池选.NET v4.0或.NET CLR版本与目标框架一致。访问/s/1走一遍创建问卷、打开、填写、提交、看统计的完整流程。这一关过得了系统的80%就算稳定运行了。5.2 低成本上公网正式上公网推荐云主机加已备案域名一台最低配实例跑问卷系统绰绰有余把数据库也放在同一台机器上省去内网互通配置。公网网关上把80端口转发到站点即可。如果你只是短暂联调或给客户演示用内网穿透工具起个临时域名也够用比如frp把本机端口暴露到带公网IP的服务器上配置里写清楚映射关系。[common] server_addr your_server_ip server_port 7000 token your_token [survey-h5] type http local_ip 127.0.0.1 local_port 5000 custom_domains survey.example.com要注意穿透工具适合联调不适合长期生产速度和稳定性受限于两端带宽而且免费的穿透域名经常被微信安全策略拦截。长期用还是得备案域名加云主机正规且省心。5.3 二次开发的第一刀导出、对接、回收率第二刀优先砍导出。运营最频繁的动作是把答卷明细变成Excel用ClosedXML这个库几行代码就能生成不需要服务器装Office。using ClosedXML.Excel; var wb new XLWorkbook(); var ws wb.Worksheets.Add(答卷); ws.Cell(1, 1).Value 提交时间; ws.Cell(1, 2).Value 题目内容; ws.Cell(1, 3).Value 答案; // 遍历Answer明细写行 wb.SaveAs(HttpContext.Server.MapPath(~/exports/answers.xlsx));第三刀对接企业微信或钉钉。问卷埋到企业微信工作台里需要配置可信域名和应用的可见范围提交接口加一个agentId参数做租户隔离。这一刀切完问卷系统就从个人玩具变成了组织内部的正式工具。最后补一个回收率统计在Global.asax的Application_BeginRequest里对/s/路径做访问计数按天存到缓存再和当日SubmitId的数量对比就能算出“访问了多少人、填了多少份、流失率在哪一步”。这个指标比问卷内容本身更能指导运营复盘。提示PDFs导入也不难问卷导出从Excel起步改HTML模板套Razor再走浏览器打印为PDF是成本最低的路径。发布两周后你会收到第一个改造需求——大概率是“能不能加一道图片上传题”。到这一步这套ASP.NET问卷源码才算真正长出了你想要的样子一切边界、参数、统计口径都在自己手里改一行重新发布不用等任何第三方审核。本文还有配套的精品资源点击获取