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

资讯详情

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

微信小程序+SSM投票系统源码解析:从登录态到防刷机制

微信小程序+SSM投票系统源码解析:从登录态到防刷机制 简介基于微信小程序的投票系统完整源码采用“前台微信小程序后台SSM”前后端分离架构适合计算机相关专业学生用于课程设计、毕业设计与初期项目演示。资源共70个文件压缩包仅848KB包含PNG图片、JS脚本、JSON配置、WXSS样式及WXML页面等既有页面结构与交互逻辑也有project.config.json、network.js等配置和请求封装。系统涵盖用户注册登录、投票创建与参与、结果统计、个人中心等模块页面使用wxcharts图表库展示票数分布后台SSM提供接口支撑可快速跑通完整业务流程。目前已有362人学习浏览整体代码组织清晰pages、utils、images等目录分工明确便于按模块阅读和二次开发。对于需要课设或毕设起步框架的同学这套代码有较强参考和复用价值。1. 微信小程序投票系统的整体形态一份zip源码里的前后端分工投票系统在业务上不算重但落到微信小程序和SSM后台这套组合里涉及的技术点恰好覆盖了小程序开发最核心的部分登录态怎么建立、前端怎么渲染动态列表、后台怎么设计表结构来防重复投票、并发下票数怎么保证准确。标题里“前台微信小程序后台ssm”这句话本质是在说一个前后端分离的完整项目小程序端负责用户界面和交互SSM后端负责业务逻辑、数据持久化和接口输出两者通过HTTPJSON通信。这份源码zip适合三类人正在做毕业设计需要在此基础上扩展业务模块的学生接外包需要快速交付投票类小程序的全栈工程师以及想系统理解小程序登录流程和SSM接口设计的初级开发者。下面从后台表结构、后台接口、小程序前端实现、防刷设计、最后落地运行这条线展开。2. SSM后台投票系统的表结构设计与管理接口落地2.1 投票业务的实体关系三张表和三个状态投票系统最基础的模型是“投票主题”和“选项”的父子关系。常见做法是设计三张表vote主题表、vote_option选项表、vote_record投票记录表。主题表存投票的标题、开始时间、结束时间、类型单选/多选、状态和创建人选项表通过vote_id关联主题存选项内容和排序记录表存每次投票行为关联openid、主题ID、选项ID和投票时间。为什么需要三张表而不是把选项存在JSON字段里因为投票结束后需要按选项分组统计关系型查询比解析JSON字段高效得多而且SSM的MyBatis对一对一、一对多映射有天然支持直接resultMap嵌套就能查出来。三张表的建表语句如下CREATE TABLE vote ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL COMMENT 投票标题, type TINYINT DEFAULT 1 COMMENT 1单选 2多选, status TINYINT DEFAULT 0 COMMENT 0草稿 1进行中 2已结束, start_time DATETIME, end_time DATETIME, create_by VARCHAR(50) COMMENT 创建人openid或admin, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE vote_option ( id INT PRIMARY KEY AUTO_INCREMENT, vote_id INT NOT NULL, content VARCHAR(200) NOT NULL COMMENT 选项文字, sort INT DEFAULT 0, vote_count INT DEFAULT 0 COMMENT 冗余计数实时展示用, KEY idx_vote_id (vote_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE vote_record ( id INT PRIMARY KEY AUTO_INCREMENT, vote_id INT NOT NULL, option_id INT NOT NULL, openid VARCHAR(64) NOT NULL COMMENT 微信用户标识, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_openid_vote (vote_id, openid), KEY idx_option_id (option_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;说明一下几个关键设计。vote_option里的vote_count是冗余计数每次投票时在这个字段上自增前台展示结果直接查该字段不需要实时COUNT聚合性能好得多。vote_record里的唯一键uk_openid_vote是防重复投票的第一道屏障同一个openid在同一场投票里只能有一条记录数据库层面就挡掉了重复插入。status字段是投票状态机的最低实现0草稿、1进行中、2已结束前台列表只查status1的投票后台管理端可以修改状态来控制投票的生命周期。2.2 Controller层三个接口覆盖投票主流程后台只需要三个核心接口查询投票列表、查询投票详情含选项、提交投票。管理端的创建投票和结果导出不是主流程但源码里通常也会带上。Controller层写法遵循SSM的标准分层下面是核心代码Controller RequestMapping(/api/vote) public class VoteController { Autowired private VoteService voteService; ResponseBody GetMapping(/list) public Result list(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size) { PageHelper.startPage(page, size); ListVoteVO votes voteService.getActiveVotes(); return Result.ok(votes); } ResponseBody GetMapping(/detail/{voteId}) public Result detail(PathVariable Integer voteId) { VoteDetailVO detail voteService.getVoteDetail(voteId); return detail null ? Result.error(投票不存在或已结束) : Result.ok(detail); } ResponseBody PostMapping(/submit) public Result submit(RequestBody SubmitDTO dto, RequestAttribute(openid) String openid) { // openid由拦截器从token解析后放入request域 return voteService.vote(dto.getVoteId(), dto.getOptionIds(), openid); } }这个Controller有三个值得注意的细节。第一RequestAttribute(openid)里的openid不是前端传来的而是拦截器先校验自定义token再把token里解析出的openid塞进request域防止用户伪造身份。第二detail接口返回的是VoteDetailVO里面嵌套了选项列表对应MyBatis的resultMap一对多映射。第三submit接口的入参optionIds是List 天然支持多选投票一次提交多个选项IDService层要保证批量插入和多选项计数的原子性。2.3 提交投票的Service层先校验再插入最后更新计数提交投票是整个后台业务逻辑最集中的地方。顺序应该是校验投票状态、校验选项归属、插入vote_record、更新vote_count。这个顺序不能乱前两步是逻辑校验后两步是数据变更如果先更新计数再插入记录一旦插入失败就会出现计数虚高。代码如下Service Transactional public class VoteServiceImpl implements VoteService { Override public Result vote(Integer voteId, ListInteger optionIds, String openid) { Vote vote voteMapper.selectByPrimaryKey(voteId); if (vote null || vote.getStatus() ! 1) { return Result.error(投票不存在或不在进行中); } ListVoteOption options voteOptionMapper.selectByVoteId(voteId); SetInteger validIds options.stream() .map(VoteOption::getId) .collect(Collectors.toSet()); if (!validIds.containsAll(optionIds)) { return Result.error(包含无效选项); } // 唯一索引兜底重复投票会抛DuplicateKeyException voteRecordMapper.batchInsert(voteId, optionIds, openid); for (Integer optId : optionIds) { voteOptionMapper.increaseCount(optId); } return Result.ok(); } }这里用Transactional把插入和更新包在同一个事务里任何一个失败都整体回滚。increaseCount对应的SQL是update vote_option set vote_count vote_count 1 where id ?天然是行级原子操作不需要先查再写避免了并发下丢失更新。batchInsert用一条SQL插入多条记录减少数据库往返次数。关于事务边界有个常见误用是把这个方法拆成无事务的多次调用或者把查询逻辑也放进事务里其实查询部分不需要在事务内执行只对写操作开启事务就够了。3. 微信小程序前台投票列表到提交的完整链路3.1 小程序的登录态wx.login到自定义token微信小程序不能像Web端一样用用户名密码它的登录流程是固定的wx.login取code传给后台后台拿code去微信接口换openid再生成自定义token返回前端。这个流程在app.js的onLaunch里执行是投票系统识别“用户身份”的基础。App({ onLaunch() { wx.login({ success: (res) { if (res.code) { wx.request({ url: https://your-api.com/api/auth/login, method: POST, data: { code: res.code }, success: (resp) { wx.setStorageSync(token, resp.data.token); } }); } } }); } });code不能直接在后台当用户标识来用它是一次性的有效期五分钟。后台拿到code后通过jscode2session接口换取openid和session_keyopenid是用户在小程序生态里的唯一标识同一个微信用户对同一个小程序永远不变这是投票系统识别“人”的基础。token生成后存到小程序storage里后续所有请求的header带上它后台根据token解析出openid就有了完整的身份链路。开发阶段经常遇到code失效常见原因有两个后台配置的appid和小程序的appid不一致或者开发者工具里没勾选“不校验合法域名”。3.2 投票列表页加载页面改成自己的业务数据拿到一份zip源码后首先要做的事就是把默认的加载页面替换成自己的业务页面。修改刚进入的加载页面核心是调整app.json的pages数组第一项就是小程序启动后显示的首页。投票系统的首屏应该是投票列表页。{ pages: [pages/vote/list, pages/vote/detail, pages/vote/result], window: { navigationBarTitleText: 投票活动, navigationBarBackgroundColor: #1e90ff, navigationBarTextStyle: white } }投票列表页的数据渲染用wx.request GET请求/api/vote/list拿数据。这里有个开发中容易忽略的细节列表页的数据加载应该放在onShow而不是onLoad因为用户从详情页返回到列表时投票状态可能已变化onShow每次进入都会触发保证数据新鲜。onLoad只在页面创建时执行一次适合放不需要刷新的初始配置。网络热词里“修改刚进入的加载页面”和“顶部导航栏高度”实际在源码里的对应位置就是app.json的window配置。如果业务方要求自定义导航栏需要设置navigationStyle为custom然后通过wx.getMenuButtonBoundingClientRect动态计算胶囊按钮位置这个操作更适合在组件封装避免每个页面重复写。3.3 投票详情页单选框组件的受控用法投票详情页是交互核心。网络热词里的“微信小程序单选框”指的就是这里的radio-group组件。单选投票用radio-group多选投票用checkbox-group两者都要绑定change事件来收集选中项。view classvote-card wx:for{{options}} wx:keyid label classoption-item bindtaptoggleSelect>submitVote() { if (this.data.selectedIds.length 0) { wx.showToast({ title: 请先选择选项, icon: none }); return; } if (this.data.submitting) return; this.setData({ submitting: true }); wx.request({ url: https://your-api.com/api/vote/submit, method: POST, data: { voteId: this.data.voteId, optionIds: this.data.selectedIds }, header: { token: wx.getStorageSync(token) }, success: (res) { if (res.data.code 0) { this.setData({ voted: true }); this.loadResult(); } else { wx.showToast({ title: res.data.msg, icon: none }); } }, complete: () this.setData({ submitting: false }) }); }wx.request的header里放token而不是放在data里是前后端约定的规范写法后台拦截器从header取token解析openid。结果页用进度条展示每个选项的投票数和占比占比数据是后台算好在VoteDetailVO里返回的计算公式是每个选项vote_count除以主题总票数四舍五入保留一位小数。进度条动画可以通过CSS transition实现不涉及额外请求。4. 投票系统的防刷机制、并发控制与状态机设计4.1 防刷三道防线openid去重、频率限制、IP维度补充投票系统最容易被攻击的点就是刷票。生产环境里常规的安全加固有三层zip源码里通常至少实现第一层。第一层是openid维度依赖vote_record的唯一索引一个人只能投一次。第二层是频率限制用Redis记录某个openid在单位时间内的投票次数比如一分钟内最多投10次超过就拒绝。第三层是IP维度因为攻击者可以通过换微信号绕过前两层但IP相对固定。Component public class VoteRateLimiter { Autowired private StringRedisTemplate redisTemplate; public boolean isAllowed(String openid) { String key vote:rate: openid : System.currentTimeMillis() / (60 * 1000); Long count redisTemplate.opsForValue().increment(key); if (count 1) { redisTemplate.expire(key, 60, TimeUnit.SECONDS); } return count 10; } }这段代码用的是固定窗口算法key加上了当前分钟的时间戳所以每分钟自动换一个key。increment是原子自增第一次自增时给key设置60秒过期。limit设为10也就是每分钟最多10次投票请求正常用户的投票频次远低于这个值刷票脚本通常会直接触发拦截。这个方案代码量小适合业务量不大的投票场景不需要引入Redisson这类重型分布式限流器。如果需要更平滑的限流可以换成滑动窗口用Redis的ZSET按时间戳记录每次请求删除窗口外的旧记录后统计数量但代码复杂度会高一些。4.2 并发下的计数一致性唯一索引和行级自增的组合高并发投票时最怕出现“幽灵票”票数加了但记录没有或者记录存在但票数没加。前面的方案里已经设计了核心机制vote_record上建唯一索引(vote_id, openid)vote_option的vote_count用自增SQL。两者配合在并发场景下大部分脏数据能自动挡掉。假设10个并发请求同时提交同一场投票数据库层面会做什么批量插入SQL走唯一索引InnoDB加锁的粒度是索引行10个请求里只有一个能成功插入其余九个会抛DuplicateKeyException事务回滚对应的vote_count自增跟着回滚计数不会虚高。这里有个编码细节不要把DuplicateKeyException直接抛给前端应该在Service层捕获后返回“您已经投过票了”的友好提示。关于锁和事务还有一个进阶处理要点业务校验代码不要放在事务里过长执行。上面Service代码里先查vote再查options再批量插入再自增整个事务持锁时间包含两次select。并发高的时候select不会加锁RR隔离级别下普通select是快照读真正加锁的只有batchInsert时的唯一索引检查锁以及update时的行锁所以持锁时间很短死锁风险不大。但如果在事务里加了select ... for update操作就需要注意加锁顺序确保多个资源的锁获取顺序一致否则可能死锁。4.3 状态机从草稿到结束的生命周期管理投票的status字段用数字表示状态但业务上应该有清晰的生命周期创建投票时是草稿点击发布变为进行中到达end_time自动变为已结束。源码里常见的做法是在Service层提供publishVote和closeVote两个方法配合Spring定时任务扫描到期投票。// 定时任务每分钟扫描一次到期的投票 Scheduled(fixedRate 60000) Transactional public void autoCloseExpiredVotes() { ListVote expiredVotes voteMapper.selectExpiredVotes(new Date()); for (Vote vote : expiredVotes) { vote.setStatus(2); voteMapper.updateByPrimaryKeySelective(vote); } }注意selectExpiredVotes的SQL条件必须带上status 1否则已经关闭的投票会被重复扫描产生无效的update操作。定时任务的频率设为每分钟一次足够因为结束时间精度不需要精确到秒。如果想要更精确的自动关闭可以在查询时把status字段所在行锁住再更新但投票系统通常不需要这么强的实时性。另一个状态机的细节是草稿状态的投票应该允许创建者通过openid查看和编辑所以后台查询列表的接口需要支持按openid过滤。4.4 避免常见误用事务里查了不该查的东西一个常见的坑是把select count查询也放进Transactional方法里。比如提交投票前先select count判断是否已投票再走insert。这在并发下会有竞态问题两个请求同时查出count0然后都执行insert虽然唯一索引最终还是挡住了一个但多了一次无意义的查询。正确做法是去掉这个count校验直接insert然后根据DuplicateKeyException判断结果不但更快逻辑也更简单。另一个坑是批量更新计数时逐条update开销大。多选投票可能有5个选项就是5条update语句。如果票数实时性要求不是特别高可以考虑异步的方式投票成功先把记录写进一张待统计表后台定时任务每分钟汇总一次更新到vote_option把写入路径缩短。这个方案的取舍是投票结果有最多一分钟的延迟但对绝大多数投票系统完全够用而且能够大幅降低数据库压力。5. 拿到zip源码后从解压到运行的5个关键操作5.1 第一步解压前先看目录结构下载的zip压缩包先别急着解压。常规源码包含三个部分小程序前端目录含pages、utils、app.js、SSM后台工程目录含src、pom.xml或web.xml、数据库初始化脚本vote.sql。用压缩工具直接预览zip内部结构确认pom.xml和app.json都在再解压。特别留意zip密码的问题有些源码包带密码保护网络热词里“zip压缩包密码破解工具”和“zip密码移除”暗示了这类情况实际开发中遇到带密码的源码包先联系分享者索取密码自己破解既费时间而且也不建议对来源不明的压缩包执行未知破解工具。5.2 按顺序执行五步替换第一步解压后先导入数据库脚本在Navicat或命令行执行vote.sql确认三张表都能建出来导入成功后手动插入一条测试投票数据。第二步用IDEA导入后台maven工程执行mvn clean package编译如果报错先检查JDK版本SSM框架通常要求JDK 8Maven仓库配置用阿里云镜像加速。第三步修改数据库连接配置jdbc.properties里的用户名、密码和URL注意URL带serverTimezone和useSSLfalse参数避免告警。第四步小程序工程用微信开发者工具导入选择“导入项目”而不是“新建项目”填入自己的AppID本地调试阶段勾选“不校验合法域名”。第五步全局替换后台接口地址小程序代码里所有https://your-api.com改成你的局域网IP或公网域名本地调试可以用http://localhost:8080但必须勾选不校验合法域名。5.3 验证连通性和发布前必改配置验证是否跑通的方法很直接开发者工具里打开投票列表页如果能拉到数据库里的测试投票数据说明前后端已经连通。再走一遍投票流程提交后到数据库里查看vote_record记录是否插入、vote_option的vote_count是否1。最后检查开发者工具Network面板里每个请求的响应码重点看token校验和状态码为401的请求是否被拦截器正确放行或拦截。正式发布前还有三个配置必须改第一到微信公众平台后台配置request合法域名必须是https且经过ICP备案开发者工具的“不校验合法域名”只能用于本地调试。第二前端代码里的appid要换成正式小程序的appid否则获取不到openid。第三定时任务相关配置确认生产环境的时区正确以免投票结束时间因时区偏差导致提前或延后关闭。完成这三项配置后投票系统就可以进入正式使用阶段了。本文还有配套的精品资源点击获取
返回列表