尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

在线投票系统开发:Yii 2.0与Vue 2.0前后端分离实战

在线投票系统开发:Yii 2.0与Vue 2.0前后端分离实战 简介一套基于Vue 2.0、element-UI、Yii 2.0与PHP开发的在线投票系统完整源码附带项目文档面向高校毕业设计、课程设计及需要快速搭建投票功能的开发者。压缩包共286个文件大小约1.25MB涵盖52个PHP后端脚本、22个Vue组件、63个JavaScript文件、SQL数据库脚本及md项目文档等整体分为PHP后端、前端后台管理、前台投票页三部分前后端分层清晰便于按模块查阅和二次开发。功能上支持免登录匿名投票并依据IP限制重复投票也可登录后参与后台系统可创建与修改投票主题设置起止时间、单选/多选、多选最大数及最大投票数还可对主题进行激活、取消与完成操作每个用户或IP仅能对同一主题投一次。数据库由user、theme、record三表构成源码已经严格测试导入vote.sql即可运行通过md文档可快速理解投票计数与数据关联逻辑适合在此基础上延申实现问卷、评选、表决等场景。目前已有87人学习下载是学习Vue前端与PHP/Yii后端协作开发的合适样例。1. 在线投票系统用这套技术栈重点不在页面在后端怎么接住并发很多人拿到「在线投票系统」这个题目第一反应是先把 Vue 2.0 和 element-UI 的页面做漂亮结果做到一半才发现投票按钮点了之后数据往哪里送、同一台设备能不能连续投、开票瞬间一堆人同时点会不会把票数加乱这些问题比前端难回答得多。这套技术栈里 Yii 2.0 才是整个系统的骨架Vue 2.0 负责把骨架露出来的接口变成能点的页面JavaScript 和 PHP 分别在这两层里补业务细节。适合做课程设计和毕业设计的人也适合想补一套完整前后端协作经验的前端开发。下面按后端、前端、部署、文档的顺序把这套系统讲透。2. 先定分工Vue 2.0、element-UI、Yii 2.0 和 PHP 各自该管哪一层2.1 别把 Yii 2.0 当普通 PHP它管的是路由、模型和事务Yii 2.0 是 PHP 生态里 MVC 结构完整的框架内置 ActiveRecord 数据表映射、Gii 代码生成器、事务与行为Behavior机制。对个人开发和课设项目来说它最大的价值在于不需要自己写「数据库连接 拼 SQL 做参数过滤」那一套重复代码数据表生成模型之后普通增删改查就是几行方法调用。另一个好处是自带 URL 规则前端请求 /api/vote/submit后端控制器就能按规则自动匹配到 actionSubmit参数再经 rules() 校验能省掉一大段手写判断。Vue 2.0 和 element-UI 负责从接口数据到用户界面的那一段。Vue 2.0 的组件化让「投票活动列表」「投票详情」「结果看板」拆成独立组件element-UI 提供现成的表格、表单、单选框、消息提示。JavaScript 在这套方案里有两层含义一层是 Vue 2.0 底层的运行基础另一层是组件代码里真正要写的业务逻辑比如把后端返回的数组重新组装成表单需要的数据结构、把两个对象合并之后传给请求体。2.2 从 composer 和 vue create 把两套开发环境搭起来常规的搭建顺序是先在项目目录里创建 Yii 2.0 高级模板再在旁边用 Vue CLI 创建前端工程。命令如下# 创建 Yii 2.0 高级项目模板包含 backend、frontend、api、console 四个入口 composer create-project --prefer-dist yiisoft/yii2-app-advanced vote-system # 进入项目根目录初始化运行环境配置dev 或 prod php init # 在项目根目录旁创建 Vue 2.0 前端工程 npx vue/cli create vote-web # 选择 Vue 2.x 预设然后装两个核心依赖 npm install element-ui axios第一条 composer 命令拉下来的是 Yii 2.0 高级模板它比基础模板多了 api 入口正好适合本系统Vue 只消费接口后台维护投票活动、管理候选人用 Yii 自带的视图渲染一个项目两种实现方式都覆盖了。后面两条 npm 相关命令给前端装 element-ui 组件库和 axios 请求库Vue 2.0 时期把公共组件打包进 element-ui初版跑通更快后续想减体积再按需加载。2.3 目录结构怎么分整个项目的维护边界才清楚我一般把系统分成前端工程和 Yii 工程两套代码Yii 后端按模块归功能目录可以照这样排目录归属放什么内容vote-web/src/viewsVue 2.0 前端页面列表页、投票页、结果页vote-web/src/apiVue 2.0 请求封装按后端接口粒度封装的 axios 方法api/modules/voteYii 2.0 接口模块投票活动、候选人的接口控制器backend/views/voteYii 2.0 管理后台后台维护活动选项、查看投票结果console/controllersYii 2.0 命令行定时汇总票数等脚本这样分的好处是写文档时能画一张清晰的数据流向图前端 axios 请求到 api 模块api 控制器调 Active Record 模型查询数据库结果以 JSON 返回。答辩被问到「某条数据从哪里来到哪里去」时按这张图的路径讲一遍就足够清楚。前端不做数据库操作后端不进模板写逻辑职责边界不模糊后续扩展手机端页面不需要动另一端。3. 后端先落地Yii 2.0 PHP 把投票接口和防重复校验写清楚3.1 数据表设计三张表把「活动、选项、投票记录」分开在线投票核心数据有三类投票活动本身、活动里的候选项、每一次投票行为。拆成三张表是标准的做法CREATE TABLE vote_activity ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT 活动ID, title VARCHAR(200) NOT NULL COMMENT 活动标题, begin_time INT NOT NULL COMMENT 开始时间unix时间戳, end_time INT NOT NULL COMMENT 结束时间unix时间戳, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0停用 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT投票活动表; CREATE TABLE vote_option ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, activity_id INT UNSIGNED NOT NULL COMMENT 所属活动, name VARCHAR(100) NOT NULL COMMENT 候选名, votes INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 票数冗余字段 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT投票选项表; CREATE TABLE vote_log ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, activity_id INT UNSIGNED NOT NULL, option_id INT UNSIGNED NOT NULL, user_ip VARCHAR(46) NOT NULL, user_token VARCHAR(128) NOT NULL COMMENT 设备指纹, created_at INT NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT投票日志表;表里最值得说明的是 vote_option.votes 这个冗余字段。统计票数可以从 vote_log 里 count但投票高峰期 count 语句会反复扫描日志表保留冗余票数字段投票成功时「日志插入 票数 1」放在同一个事务里查询结果直接读 votes开销小得多。活动起止时间用时间戳不用 datetime前后端传参和比较都少一层转换。3.2 用 Gii 生成模型再在控制器里写提交投票接口数据表建好之后打开 Yii 2.0 的 Gii 模块分别把三张表生成 Model再在 api/modules/vote/controllers 下写控制器。投票接口是整套系统里最关键的流程public function actionSubmit() { $request Yii::$app-request; $activityId (int)$request-post(act_id); $optionId (int)$request-post(option_id); $token $request-post(user_token, ); $activity VoteActivity::findOne($activityId); if (!$activity || $activity-status ! 1) { return $this-fail(活动不存在或未启用); } $now time(); if ($now $activity-begin_time || $now $activity-end_time) { return $this-fail(不在投票时间内); } $option VoteOption::findOne([id $optionId, activity_id $activityId]); if (!$option) { return $this-fail(候选选项不存在); } $log VoteLog::find() -where([activity_id $activityId, user_token $token]) -one(); if ($log || $this-isIpLimit($request-userIP, $activityId)) { return $this-fail(你已经投过票了); } $transaction Yii::$app-db-beginTransaction(); try { $newLog new VoteLog(); $newLog-setAttributes([ activity_id $activityId, option_id $optionId, user_ip $request-userIP, user_token $token, created_at $now, ]); $newLog-save(); VoteOption::updateAllCounters([votes 1], [id $optionId, activity_id $activityId]); $transaction-commit(); return $this-success([votes $option-votes 1]); } catch (\Exception $e) { $transaction-rollBack(); return $this-fail(投票失败请稍后再试); } }这里的执行顺序是有讲究的先验证活动和选项是否存在再校验时间最后查重复记录任何一个条件不过都直接返回不给数据库增加无效写入。事务把「插入日志」和「票数递增」绑在一起任何一个失败都能整体回滚避免出现投了票但票数没加的脏数据。success 和 fail 是控制器基类里封装好的统一响应方法返回 code、message、data 三段结构。3.3 接口参数和防重复的取舍参数表加几种常见限制参数类型校验规则说明act_idint必填、大于 0、必须是有效活动定位投票活动option_idint必填、属于该活动定位候选选项user_tokenstring长度 16-128前端生成的设备指纹不需要登录态user_token 常见做法是用户第一次进入投票页时前端生成一段随机字符串存入 localStorage在没有账号体系的场景下靠它判断重复。接口内部再做 IP 层辅助限制比如配置每个 IP 单日 50 次上限超出返回错误。这套组合不是绝对防刷但对课程设计评委关注的方向已经能说清防重思路。提示不要只查 user_ip 判断重复同一个局域网内几个人共用出口 IP 的情况很常见IP 只做辅助限制。4. 前端对接Vue 2.0 页面、element-UI 表格和路由参数逐个打通4.1 路由参数列表页把活动 ID 带到详情页前端页面之间传参常见的是路由参数方式。列表页点击某个活动跳转到详情页// src/router/index.js 里的路由定义 const routes [ { path: /activity/:id, name: ActivityDetail, component: ActivityDetail } ];列表页跳转this.$router.push({ name: ActivityDetail, params: { id: row.id } });详情页在 created 钩子里读取参数const id this.$route.params.id; this.fetchDetail(id);路由定义组件内取值适用场景path 带 :id$route.params.id详情页、编辑页query: { id: 1 }$route.query.id列表筛选、分享链接params name 组合$route.params编程式跳转路由参数用 params 传递时 URL 里会出现实际值刷新页面后数据不会丢这点比 query 传对象更适合传递业务主键。Vue 2.0 里同一个路由带不同参数跳转时组件会被复用created 不会每次触发要么在 watch 里监听 $route.params.id要么跳转时给组件加 :keyid两种做法选一种别等到现场再踩这个坑。4.2 投票组件element-UI 表单校验加 JavaScript 对象合并投票页是整套系统里交互最集中的地方用 element-UI 的 el-radio-group 绑定选中项el-form refvoteForm :modelvoteForm :rulesrules el-form-item propoptionId el-radio-group v-modelvoteForm.optionId el-radio v-foropt in options :keyopt.id :labelopt.id {{ opt.name }} /el-radio /el-radio-group /el-form-item /el-formexport default { data() { return { // Object.assign 把接口数据和默认字段合并成表单初始值 voteForm: Object.assign({ optionId: null }, { actId: this.$route.params.id }), rules: { optionId: [{ required: true, message: 请选择候选人, trigger: change }] } }; }, methods: { async submitVote() { await this.$refs.voteForm.validate(); const payload Object.assign({}, this.voteForm, { user_token: localStorage.getItem(vote_token) || this.makeToken() }); const res await this.$http.post(/vote/submit, payload); if (res.data.code 0) { this.$message.success(投票成功); this.$emit(voted, res.data.data); } } } };rules 校验触发时机选 change用户切换单选项时如果没有选中组件会立即提示提交按钮再兜底校验一次。请求体里通过 Object.assign 把用户 token 合并进 payload这是前端常见「合并两个对象」的场景Vue 2.0 里不要把合并结果直接改到 data 上先展开成新对象再发数据流更干净。4.3 element-UI 表格里面的行可以收起来展开行展示投票明细后台需要一个「活动列表」页面每行显示活动标题、状态、总票数点一下展开当前活动所有候选人的得票明细。element-UI 的展开行功能正好解决「表格行收起/展开」el-table :dataactivityList row-keyid el-table-column typeexpand template #default{ row } el-table :datarow.options el-table-column propname label候选人/el-table-column el-table-column propvotes label票数/el-table-column /el-table /template /el-table-column el-table-column proptitle label活动名称/el-table-column el-table-column proptotal_votes label总票数/el-table-column el-table-column propstatus label状态/el-table-column /el-table展开列放在表格第一列每个候选人的明细作为当前行的展开内容不用新开页也不会撑爆列表高度。注意展开列内嵌套 el-table 时内层表格固定列、特殊样式容易出问题初版直接用普通列展示。row 的 options 字段可以在列表接口里一次性返回也可以展开事件里按需请求推荐后者列表页接口响应体积更小。4.4 axios 封装和 PHP 跨域CORS 预检请求响应头Vue 前端开发服务器默认是 localhost:8080后端接口在同机 8010 端口浏览器会发起跨域请求。封装一个统一的 axios 实例再在后端补上 CORS 响应头// src/api/http.js import axios from axios; const http axios.create({ baseURL: /api, timeout: 10000 }); http.interceptors.request.use(config { config.headers[X-Token] localStorage.getItem(vote_token) || ; return config; }); export default http;Yii 2.0 控制器基类里用行为挂 CORSpublic function behaviors() { return [ corsFilter [ class \yii\filters\Cors::class, cors [ Origin [*], Access-Control-Request-Method [GET, POST, OPTIONS], Access-Control-Allow-Headers [X-Token, Content-Type], ], ], ]; }浏览器在 POST 前会先发 OPTIONS 预检Yii 的 Cors 过滤器会自动处理并返回允许跨域的头。需要提的是前端 Vue 带了自定义请求头 X-Token后端 Access-Control-Allow-Headers 里必须明确列出否则预检不通过接口报错时控制台里全是 CORS 字样实际是配置缺失而不是代码逻辑问题。5. 票数统计和打包部署从本地跑通到放进 U 盘演示5.1 统计接口聚合计算和冗余字段一起用投票结果页一般要同时展示「每个选项票数」和「总票数」不需要反复查日志表直接从选项表聚合public function actionResult($id) { $activity VoteActivity::findOne($id); if (!$activity) { return $this-fail(活动不存在); } $options VoteOption::find() -where([activity_id $id]) -select([id, name, votes]) -asArray() -all(); $total array_sum(array_column($options, votes)); return $this-success([ total $total, options $options ]); }array_column 把选项数组里的 votes 字段全部取出来array_sum 求和PHP 里做这步开销很小不需要额外 SQL。峰值时段的统计可以让 console 控制台跑定时任务把结果缓存到 Redis 直接读这时候 Yii 框架自带的队列组件就能承担异步刷新看板的活。5.2 vue 打包后布局异常八成是 publicPath 没配置Vue 2.0 默认打包生成的静态资源引用路径是根目录放在项目子目录访问时CSS 和 JS 找不到页面布局就会错乱。解决办法是改打包配置// vue.config.js module.exports { publicPath: ./, outputDir: dist, assetsDir: static };配置项默认值建议值作用publicPath/./静态资源引用方式outputDirdistdist打包输出目录assetsDir空static静态资源子目录productionSourceMaptruefalse关闭 source mappublicPath 改成相对路径后打包产物无论放进 nginx 子路径还是直接打开 dist/index.html资源都能找到。还有一个常见陷阱是 history 路由刷新 404课设演示阶段直接用 hash 模式带 # 号能避开服务端回退配置线上部署再升级 history。5.3 nginx php-fpm 跑 Yii 2.0 和 Vue 静态页面最简单的部署方式是 nginx 同时托管 Vue 静态文件和 PHP 接口server { listen 80; server_name vote.local; root /data/www/vote-system/vote-web/dist; # Vue 静态资源与页面路由回退 location / { try_files $uri $uri/ /index.html; } # API 请求转发给 php-fpm location /api/ { alias /data/www/vote-system/api/web/; try_files $uri $uri/ /index.php?$query_string; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $request_filename; fastcgi_pass 127.0.0.1:9000; } }注意 Yii 2.0 的 api/web 才是接口入口root 指向的是 Vue 的 dist 目录/api 前缀的请求单独用 alias 指向 api/web两种路径写法要分清。部署完成后的状态是访问站点看到投票列表页点击投票发送 /api/vote/submit 请求php-fpm 进程返回 JSON。6. 课程设计和毕业设计版源码与文档怎么组织6.1 文档必写四部分课程设计和毕业设计评分里文档和源码同等重要。文档至少覆盖需求分析、数据库设计、接口清单、测试记录四部分章节内容要点页数参考需求分析三类角色、投票流程用例、状态图4-6数据库设计ER 图、三张表字段说明4-5接口设计每个接口的方法、参数、返回 JSON 示例6-8测试记录正常投票、重复投票、活动结束投票三个用例2-3源码包里的 README 不要只写安装步骤把「三张表的关系」和「投票接口时序」画成图放进去评委翻资料时一眼能看到设计重点。6.2 答辩追问的三个方向追问一防重复怎么防答案围绕 user_token 加 IP 辅助说明各自动态。追问二并发下数据一致怎么保证答案是事务 原子自增 updateAllCounters。追问三如果访问量继续变大怎么办答案往队列异步写日志、Redis 读票数、WebSocket 推送实时票数这个方向说不需要真实现能讲清思路就够。6.3 验收现场用一组 curl 自证# 1. 查询活动详情 curl http://你的地址/api/vote/detail?act_id1 # 2. 提交投票第二次执行同一命令应返回你已经投过票了 curl -X POST http://你的地址/api/vote/submit \ -d act_id1option_id2user_tokentest_token_001 # 3. 查看结果统计 curl http://你的地址/api/vote/result?act_id1这三条命令覆盖了系统的主流程、防重复校验和统计输出演示前先在命令行里自己跑一遍比现场点页面更能说明接口设计是完整的。本文还有配套的精品资源点击获取
返回列表