)
系统摘要随着移动互联网与微信生态的普及传统 Web 端投票系统已难以满足用户随时随地的参与需求。本文以在线投票管理系统为研究对象在既有 Spring Boot 后端与 Vue 管理端基础上依据 uni-app 小程序开发规范设计并实现了面向普通用户的微信小程序端。系统采用前后端分离架构小程序端使用 uni-app 3、Vue 3 Composition API 与 uView Plus 组件库通过 RESTful 接口与后端通信。针对“展示功能免登录、交互功能需登录”的业务要求后端新增 /api/public 公开浏览接口小程序在首页统计、公告浏览、投票结果查询等模块无需认证即可访问而投票提交、资料修改等操作则通过 JWT 鉴权保障安全。数据库采用 MySQL包含用户、投票、选项、记录、公告等六张核心表通过外键与唯一约束保证数据一致性。全文共七章从需求、设计、数据库、实现、测试到总结完整记录软件工程各阶段成果。测试表明小程序功能完整、交互流畅有效拓展了投票系统的使用场景。技术栈 Spring Boot3uni-appVue3uViewPlusViteMybatsiPlusEcharts微信小程序数据库表6张文末获取联系文末获取联系作者介绍专注于计算机课设、毕设辅导本人开发原创代码一题一稿绝不撞题坚持原创个人创作非工作室源码全网唯一。✅原创唯一个人原创开发独立设计数据库与业务逻辑拒绝工作室代码改造✅技术主流SpringBootVueuni-app前后端分离MySQLEcharts可本地运行✅配套资料源码 数据库 实验报告/论文 答辩 PPT部署演示远程调试问题解答技术范围SpringBoot、Vue、数据可视化、小程序、HLMT、Nodejs、uni-app、MySQL数据库、ElementUi等设计与开发。适用范围软件工程、软件技术、数据库课程设计、计算机科学与技术、数据库系统原理、JavaWeb开发、JavaEE、Java、Web应用开发、动态网页设计的课程设计、课设、大作业、课程实验、期末作业实验报告参考内容实验报告可供大家参考使用用户端功能小程序管理员端功能后台数据库设计系统数据库设计为admins管理员表结构字段名数据类型说明约束idBIGINT主键自增非空usernameVARCHAR(50)登录用户名唯一非空passwordVARCHAR(100)登录密码BCrypt 加密非空real_nameVARCHAR(50)真实姓名可空phoneVARCHAR(20)联系电话可空enabledTINYINT启用状态默认 1created_atDATETIME创建时间可空users用户表结构字段名数据类型说明约束idBIGINT主键自增非空usernameVARCHAR(50)登录用户名唯一非空passwordVARCHAR(100)登录密码BCrypt 加密非空real_nameVARCHAR(50)真实姓名可空phoneVARCHAR(20)联系电话可空departmentVARCHAR(100)所属部门可空enabledTINYINT启用状态默认 1created_atDATETIME创建时间可空announcements公告表结构字段名数据类型说明约束idBIGINT主键自增非空titleVARCHAR(200)公告标题非空contentTEXT公告内容可空statusVARCHAR(20)发布状态PUBLISHED/DRAFTcreated_atDATETIME创建时间可空updated_atDATETIME更新时间可空polls投票表结构字段名数据类型说明约束idBIGINT主键自增非空titleVARCHAR(200)投票标题非空descriptionTEXT投票说明可空vote_typeVARCHAR(20)投票类型SINGLE/MULTIPLEmax_choicesINT多选最多可选默认 1statusVARCHAR(20)投票状态DRAFT/OPEN/CLOSEDstart_timeDATETIME开始时间可空end_timeDATETIME结束时间可空created_atDATETIME创建时间可空updated_atDATETIME更新时间可空poll_options投票选项表结构字段名数据类型说明约束idBIGINT主键自增非空poll_idBIGINT所属投票外键→polls.id非空contentVARCHAR(200)选项内容非空sort_orderINT排序序号默认 0image_urlVARCHAR(500)选项图片可空created_atDATETIME创建时间可空vote_records投票记录表结构字段名数据类型说明约束idBIGINT主键自增非空poll_idBIGINT所属投票外键→polls.id非空option_idBIGINT所选选项外键→poll_options.id非空user_idBIGINT投票用户外键→users.id非空created_atDATETIME投票时间可空主要数据表关系说明主表从表关系说明pollspoll_options1:N一个投票活动包含多个选项pollsvote_records1:N一个投票活动可有多条投票记录usersvote_records1:N一个用户可参与多次不同投票poll_optionsvote_records1:N一个选项可被多名用户选择多选场景系统架构Controller及Service层核心代码写法Service RequiredArgsConstructor public class PollService { private final PollMapper pollMapper; private final PollOptionMapper pollOptionMapper; private final VoteRecordMapper voteRecordMapper; public PageResultPoll list(String keyword, String status, int page, int size) { var wrapper Wrappers.PolllambdaQuery() .eq(StringUtils.hasText(status), Poll::getStatus, status) .and(StringUtils.hasText(keyword), w - w .like(Poll::getTitle, keyword) .or().like(Poll::getDescription, keyword)) .orderByDesc(Poll::getId); PagePoll result pollMapper.selectPage(new Page(page, size), wrapper); result.getRecords().forEach(this::enrichSummary); return PageResult.of(result); } public PageResultPoll listOpenForUser(int page, int size) { Long userId AuthContext.getUserId(); var wrapper Wrappers.PolllambdaQuery() .eq(Poll::getStatus, OPEN) .orderByDesc(Poll::getId); PagePoll result pollMapper.selectPage(new Page(page, size), wrapper); result.getRecords().forEach(p - { enrichSummary(p); p.setHas_voted(hasUserVoted(p.getId(), userId)); p.setOptions(loadOptions(p.getId(), false)); }); return PageResult.of(result); } public Poll detail(Long id) { Poll poll pollMapper.selectById(id); if (poll null) throw new RuntimeException(投票不存在); enrichSummary(poll); poll.setOptions(loadOptions(id, false)); Long userId AuthContext.getUserId(); if (userId ! null) poll.setHas_voted(hasUserVoted(id, userId)); return poll; } Transactional public Poll create(PollDTO dto) { validatePollDto(dto); Poll poll new Poll(); applyDto(poll, dto); poll.setStatus(DRAFT); pollMapper.insert(poll); saveOptions(poll.getId(), dto.getOptions()); return detail(poll.getId()); } Transactional public Poll update(Long id, PollDTO dto) { Poll poll pollMapper.selectById(id); if (poll null) throw new RuntimeException(投票不存在); if (!DRAFT.equals(poll.getStatus())) throw new RuntimeException(仅草稿状态可编辑); if (voteRecordMapper.selectCount(Wrappers.VoteRecordlambdaQuery().eq(VoteRecord::getPoll_id, id)) 0) { throw new RuntimeException(已有投票记录无法编辑); } validatePollDto(dto); applyDto(poll, dto); pollMapper.updateById(poll); pollOptionMapper.delete(Wrappers.PollOptionlambdaQuery().eq(PollOption::getPoll_id, id)); saveOptions(id, dto.getOptions()); return detail(id); } Transactional public void batchDelete(ListLong ids) { if (ids null || ids.isEmpty()) throw new RuntimeException(请选择要删除的数据); pollMapper.deleteBatchIds(ids); } Transactional public Poll open(Long id) { Poll poll requirePoll(id); if (!DRAFT.equals(poll.getStatus()) !CLOSED.equals(poll.getStatus())) { throw new RuntimeException(当前状态无法开启); } long optionCount pollOptionMapper.selectCount(Wrappers.PollOptionlambdaQuery().eq(PollOption::getPoll_id, id)); if (optionCount 2) throw new RuntimeException(请至少配置两个投票选项); validateTimeRange(poll.getStart_time(), poll.getEnd_time()); poll.setStatus(OPEN); pollMapper.updateById(poll); return detail(id); } Transactional public Poll close(Long id) { Poll poll requirePoll(id); if (!OPEN.equals(poll.getStatus())) throw new RuntimeException(仅进行中的投票可结束); poll.setStatus(CLOSED); pollMapper.updateById(poll); return detail(id); } public ListPollOption loadOptions(Long pollId, boolean withStats) { ListPollOption options pollOptionMapper.selectList(Wrappers.PollOptionlambdaQuery() .eq(PollOption::getPoll_id, pollId) .orderByAsc(PollOption::getSort_order) .orderByAsc(PollOption::getId)); if (withStats) { long total voteRecordMapper.selectCount(Wrappers.VoteRecordlambdaQuery().eq(VoteRecord::getPoll_id, pollId)); for (PollOption opt : options) { long cnt voteRecordMapper.selectCount(Wrappers.VoteRecordlambdaQuery() .eq(VoteRecord::getPoll_id, pollId) .eq(VoteRecord::getOption_id, opt.getId())); opt.setVote_count(cnt); opt.setVote_percent(total 0 ? 0.0 : Math.round(cnt * 10000.0 / total) / 100.0); } } return options; } public boolean hasUserVoted(Long pollId, Long userId) { if (userId null) return false; return voteRecordMapper.exists(Wrappers.VoteRecordlambdaQuery() .eq(VoteRecord::getPoll_id, pollId) .eq(VoteRecord::getUser_id, userId)); } private Poll requirePoll(Long id) { Poll poll pollMapper.selectById(id); if (poll null) throw new RuntimeException(投票不存在); return poll; } private void enrichSummary(Poll poll) { ListVoteRecord records voteRecordMapper.selectList(Wrappers.VoteRecordlambdaQuery() .eq(VoteRecord::getPoll_id, poll.getId())); poll.setTotal_votes((long) records.size()); poll.setVoter_count(records.stream().map(VoteRecord::getUser_id).filter(Objects::nonNull).distinct().count()); } private void saveOptions(Long pollId, ListPollOptionDTO options) { int order 0; for (PollOptionDTO dto : options) { PollOption opt new PollOption(); opt.setPoll_id(pollId); opt.setContent(dto.getContent()); opt.setSort_order(dto.getSort_order() ! null ? dto.getSort_order() : order); opt.setImage_url(dto.getImage_url()); pollOptionMapper.insert(opt); } } private void applyDto(Poll poll, PollDTO dto) { poll.setTitle(dto.getTitle()); poll.setDescription(dto.getDescription()); poll.setVote_type(StringUtils.hasText(dto.getVote_type()) ? dto.getVote_type() : SINGLE); poll.setMax_choices(dto.getMax_choices() ! null ? dto.getMax_choices() : 1); poll.setStart_time(dto.getStart_time()); poll.setEnd_time(dto.getEnd_time()); if (MULTIPLE.equals(poll.getVote_type()) poll.getMax_choices() 2) { poll.setMax_choices(2); } if (SINGLE.equals(poll.getVote_type())) poll.setMax_choices(1); } private void validatePollDto(PollDTO dto) { if (dto.getOptions() null || dto.getOptions().size() 2) throw new RuntimeException(请至少添加两个投票选项); if (MULTIPLE.equals(dto.getVote_type())) { int max dto.getMax_choices() ! null ? dto.getMax_choices() : 2; if (max 2 || max dto.getOptions().size()) { throw new RuntimeException(多选最多可选数量须在2到选项数之间); } } validateTimeRange(dto.getStart_time(), dto.getEnd_time()); } private void validateTimeRange(LocalDateTime start, LocalDateTime end) { if (start ! null end ! null !end.isAfter(start)) { throw new RuntimeException(结束时间必须晚于开始时间); } } }博主擅长功能设计、开题报告、任务书、中期检查PPT、系统功能实现、代码开发、论文辅导、论文降重、长期答辩答疑辅导、腾讯会议一对一专业讲解辅导答辩、模拟答辩演练、和理解代码逻辑思路等。更多个人原创作品原创课程设计大全✅原创毕业设计集合✅获取联系项目功能完整可在本地运行并可远程调试确保运行顺利查看获取联系方式课程设计获取联系https://blog.csdn.net/qq_59059632/article/details/163685632?spm1011.2415.3001.5331