拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Java+微信小程序活动报名系统:数据库设计、事务防并发与接口实战

简介这是面向毕业设计场景的微信小程序活动报名管理系统完整Java项目包适合计算机相关专业学生用于课程设计、毕设答辩或小程序开发实战。系统实现用户在小程序端浏览活动信息、在线注册报名后台管理员维护活动分类与内容前后端职责清晰。整个压缩包共422个文件大小约55MB核心内容包括46个Java源码与对应class文件、59个依赖jar包、40个xml配置以及小程序前端wxml、wxss、js源码和SQL数据库脚本还附带说明文档、演示视频与后台管理页面素材目录结构完整方便按模块查阅。目前已吸引1905人浏览学习热度在同类毕业设计中较为突出。下载后能获得可运行的完整工程后端控制器覆盖活动报名、收藏、评论、社团申请等模块可以快速跑通小程序报名流程理解Java后端与微信小程序的数据交互方式也适合在现有功能上做二次扩展或结合源码与文档完成毕业论文撰写和毕设答辩准备。1. 活动报名系统的真实场景与技术框架线下活动报名一直是最容易被低估的流程。用 Excel 收集名单统计人数麻烦重复报名和临时改期更难追踪用问卷星又无法实时展示余量和报名状态。这个基于 Java 后端 微信小程序端的活动报名管理系统把活动发布、分类浏览、报名、收藏、评论都放进同一个闭环。后端源码的类名里能看到ActivityController、ActivityApplyController、BaseDAO这样的分层说明这是很经典的 Controller-DAO 结构配合 MySQL 数据库做持久化。对正在做毕业设计或想厘清小程序与后端数据流的人这套源码的拆解值得完整过一遍尤其是报名人数、唯一索引和事务这几个点。2. 数据表设计与 BaseDAO 层的通用查询封装2.1 根据业务功能拆出核心表从源码里出现的高频类名可以反推业务模块活动、用户、报名、收藏、评论、分类。设计表时按功能拆活动是主表报名和评论是典型从表收藏用于记录用户与活动之间的关注关系。下面是一版适合这个系统的建表脚本。-- 活动分类表 CREATE TABLE activity_category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 分类名称, sort INT DEFAULT 0 COMMENT 排序值越小越靠前 ); -- 活动表 CREATE TABLE activity ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL COMMENT 分类 ID, title VARCHAR(200) NOT NULL COMMENT 活动标题, cover VARCHAR(255) COMMENT 封面图地址, location VARCHAR(200) COMMENT 活动地点, start_time DATETIME COMMENT 开始时间, end_time DATETIME COMMENT 结束时间, max_people INT DEFAULT 0 COMMENT 名额上限0 表示不限制, apply_count INT DEFAULT 0 COMMENT 当前已报名人数, content TEXT COMMENT 活动详情, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 小程序用户表 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) NOT NULL UNIQUE COMMENT 微信 openid, nickname VARCHAR(100) COMMENT 昵称, avatar VARCHAR(255) COMMENT 头像地址, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 报名表 CREATE TABLE activity_apply ( id INT PRIMARY KEY AUTO_INCREMENT, activity_id INT NOT NULL, user_id INT NOT NULL, apply_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_activity_user (activity_id, user_id) );activity_apply表上的联合唯一索引是防重复报名的第一道防线。只靠后端代码先查再插入在快速点击或并发请求下会出现两个请求同时读到“未报名”随后各插一条数据的情况。索引会直接拒绝第二条插入程序捕获到 DuplicateKeyException 后返回“您已报名”即可。这个设计比单纯在 service 里加判断可靠得多。字段含义是否必须关键点max_people活动名额上限否0 或 NULL 表示不限制apply_count当前报名人数计数是每次报名成功 1openid微信用户在系统内唯一标识是一个微信号对应一条 user 记录uk_activity_user活动 用户联合唯一索引是防止同一个人重复报名活动表里还需要冗余一个apply_count如果不冗余报名人数只能通过COUNT(*)统计 activity_apply列表页每次都要做聚合查询数据量变大后响应会变慢。用冗余字段的好处是列表页只查 activity 一张表代价是报名事务里必须同时更新这个字段。2.2 BaseDAO 封装了什么项目里所有 DAO 都应该继承BaseDAO把获取连接、参数绑定、资源释放集中处理而不是每个 DAO 重复写DriverManager.getConnection和while(rs.next())。常见做法是暴露update和query两个通用方法public class BaseDAO { // 实际项目中数据库账号密码建议放到配置文件 private static final String URL jdbc:mysql://localhost:3306/activity_db; private static final String USER root; private static final String PASSWORD 123456; protected Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public int update(String sql, Object... params) throws SQLException { try (Connection conn getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } return ps.executeUpdate(); } } public T ListT query(String sql, RowMapperT rowMapper, Object... params) { ListT result new ArrayList(); try (Connection conn getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } try (ResultSet rs ps.executeQuery()) { while (rs.next()) { result.add(rowMapper.mapRow(rs)); } } } catch (SQLException e) { // 这里不能只打印堆栈需要向上抛出或记录日志 e.printStackTrace(); } return result; } }RowMapper是函数式接口只负责把一行ResultSet转换成业务对象。query方法里用 try-with-resourcesConnection、PreparedStatement、ResultSet都会自动关闭避免连接泄漏。注意用可变参数Object... params时JDBC 的占位符顺序必须和数组顺序一致ps.setObject(i 1, params[i])中i1是因为 JDBC 的索引从 1 开始不是从 0 开始。这个 BaseDAO 在低并发毕业设计场景下够用但接口如果被多人同时访问每次都新建数据库连接会有较大开销。上线前可以把getConnection()换成 Druid 连接池改的是实现细节调用方不用变。3. Controller 层请求链路与活动报名接口实战3.1 用 action 参数做请求分发源码里有ApiController、ActivityController、ActivityApplyController说明系统按模块拆 Controller。常见做法有两种一种是WebServlet(/api/activity)配合actiondetail参数另一种是 REST 风格/api/activity/detail路径参数。这套源码里出现了AutoCodeController自动代码生成器通常更喜欢规则清晰的action分发因为 URL 规则越统一越容易用字符串拼接生成代码。下面是ActivityController的典型骨架。WebServlet(/api/activity) public class ActivityController extends BaseController { private ActivityDAO activityDAO new ActivityDAO(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { String action req.getParameter(action); switch (action null ? : action) { case list: list(req, resp); break; case detail: detail(req, resp); break; default: writeJson(resp, Result.error(未知 action)); } } private void list(HttpServletRequest req, HttpServletResponse resp) throws IOException { String categoryId req.getParameter(categoryId); String keyword req.getParameter(keyword); int page req.getParameter(page) null ? 1 : Integer.parseInt(req.getParameter(page)); int limit 10; StringBuilder sql new StringBuilder( SELECT id, title, cover, location, start_time, apply_count, max_people FROM activity WHERE 11); ListObject params new ArrayList(); if (categoryId ! null !categoryId.isEmpty()) { sql.append( AND category_id ?); params.add(Integer.parseInt(categoryId)); } if (keyword ! null !keyword.isEmpty()) { sql.append( AND title LIKE ?); params.add(% keyword %); } sql.append( ORDER BY create_time DESC LIMIT ?, ?); params.add((page - 1) * limit); params.add(limit); ListActivity rows query(sql.toString(), rs - { Activity a new Activity(); a.setId(rs.getInt(id)); a.setTitle(rs.getString(title)); a.setStartTime(rs.getTimestamp(start_time)); return a; }, params.toArray()); writeJson(resp, Result.ok(rows)); } }BaseController里通常放着query快捷调用和writeJson避免每个 Controller 重复创建 response JSON 的代码。这里的page参数是前端传过来的页码从 1 开始但 SQL 里LIMIT偏移要从 0 开始所以写成(page - 1) * limit。分页参数如果直接拼进 SQL 会有注入风险这里统一走?占位符。参数类型必填说明actionstring是请求动作list / detailcategoryIdint否按分类过滤keywordstring否标题模糊搜索pageint是页码从 1 开始limitint否每页条数默认 10很多新手会把模糊查询写成 AND title LIKE %?%这是错误的?会被当成普通字符串而不是占位符。正确做法是把%keyword%整体作为一个参数传入。3.2 报名接口的事务与防超卖报名是写操作源码里单独拆出ActivityApplyController说明和查询分离。报名逻辑需要做四件事校验登录状态、检查是否已报名、插入报名记录、更新报名人数。其中后两步必须在同一个事务里否则会出现“报名记录插入成功但人数没增加”的脏数据。WebServlet(/api/activity/apply) public class ActivityApplyController extends BaseController { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { int userId getLoginUserId(req); int activityId Integer.parseInt(req.getParameter(activityId)); Connection conn null; try { conn getConnection(); conn.setAutoCommit(false); // 检查是否已报名利用联合索引兜底 String checkSql SELECT COUNT(*) FROM activity_apply WHERE activity_id? AND user_id?; if (queryInt(conn, checkSql, activityId, userId) 0) { writeJson(resp, Result.error(您已报名请勿重复提交)); return; } // 插入报名记录 update(conn, INSERT INTO activity_apply(activity_id, user_id) VALUES (?,?), activityId, userId); // 带条件更新避免超卖 int updated update(conn, UPDATE activity SET apply_count apply_count 1 WHERE id ? AND (max_people 0 OR apply_count max_people), activityId); if (updated 0) { throw new RuntimeException(活动名额已满); } conn.commit(); writeJson(resp, Result.ok(报名成功)); } catch (DuplicateKeyException e) { writeJson(resp, Result.error(您已报名请勿重复提交)); } catch (Exception e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } writeJson(resp, Result.error(e.getMessage())); } finally { try { if (conn ! null) conn.setAutoCommit(true); if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }这里有两个关键细节。第一UPDATE ... WHERE apply_count max_people把“名额是否已满”的判断写进更新语句而不是先SELECT apply_count再判断因为并发环境下两次 select 拿到相同值会导致超卖。第二如果updated 0说明条件不满足此时回滚整个事务insert 进去的报名记录也会撤销。DuplicateKeyException是 Spring 里的异常类如果项目基础版没引入 Spring 依赖可以捕捉SQLIntegrityConstraintViolationException或直接判断SQLException的错误码为 1062。这一点决定了异常处理是否够健壮线上环境不能靠打印堆栈就算完。4. 微信小程序端联调登录态、列表渲染与报名交互4.1 用 wx.login 获取 openid 并建立登录态小程序没有传统用户名密码体系后端识别用户靠 openid。标准流程是wx.login拿到临时 code再把 code 发给后端后端拿 code 调微信接口换取 openid生成自定义 token 返回给小程序。以后每次请求都带上 token后端过滤器解析 token 就知道当前用户是谁。const API_BASE https://api.example.com; function login() { return new Promise((resolve, reject) { wx.login({ success(res) { if (!res.code) { reject(new Error(登录失败)); return; } wx.request({ url: ${API_BASE}/api/login, method: POST, data: { code: res.code }, success(resp) { wx.setStorageSync(token, resp.data.token); resolve(resp.data); }, fail: reject }); } }); }); }后端登录接口拿到 code 后按照appid secret code的规则请求微信接口返回 openid 和 session_key。openid 直接作为 user 表的唯一标识不存在就在 user 表插入新记录存在则直接生成 token。token 可以是简单签名的字符串也可以直接用 JWT推荐 JWT 是因为它自带过期时间后端不需要维护 session 表。字段来源说明codewx.login 返回临时凭证有效期约 5 分钟token后端生成小程序端存入 Storage请求时放入 headeropenid后端从微信接口获得用户唯一标识不可在小程序端直接获取4.2 活动列表页的数据加载与渲染首页加载活动列表核心逻辑是分页请求和触底加载。请求前先判断loading和hasMore避免重复触发。Page({ data: { activities: [], page: 1, hasMore: true, loading: false }, loadActivities() { if (this.data.loading || !this.data.hasMore) return; this.setData({ loading: true }); wx.request({ url: ${API_BASE}/api/activity, method: GET, data: { action: list, page: this.data.page }, header: { Authorization: wx.getStorageSync(token) }, success: (res) { const list res.data.data || []; this.setData({ activities: this.data.activities.concat(list), page: this.data.page 1, hasMore: list.length 10 }); }, complete: () this.setData({ loading: false }) }); } });WXML 里用wx:for渲染列表每个活动卡片显示封面、标题、地点、开始时间和报名按钮按钮上通过>onApply(e) { const id e.currentTarget.dataset.id; if (this.submitting) return; this.submitting true; wx.showLoading({ title: 提交中, mask: true }); wx.request({ url: ${API_BASE}/api/activity/apply, method: POST, data: { activityId: id }, header: { Authorization: wx.getStorageSync(token) }, success: (res) { wx.showToast({ title: res.data.message, icon: none }); }, complete: () { this.submitting false; wx.hideLoading(); } }); }this.submitting不是响应式数据挂在 Page 实例上只用来控制请求期间不能再次进入点击逻辑。mask: true让 loading 层挡住按钮减少误触。这个方案配合后端唯一索引能保证同一个用户在同一场活动里只有一条报名记录。5. 验收清单与联调避坑技巧5.1 用 curl 验证后端接口在联调小程序之前先独立验证后端接口。启动项目后用 curl 请求活动列表确认 JSON 结构和预期一致。curl http://localhost:8080/api/activity?actionlistpage1 \ -H Authorization: test-token-123返回结果里code为 0 时再取一个真实活动 id 测试报名接口。用 curl 连续执行两次相同报名请求第二次应该返回“您已报名”的提示因为联合唯一索引已经把重复数据拦住了。这一步能快速确认数据库索引和事务配置是否生效。5.2 权限边界与事务配置管理员接口和普通用户接口要分开校验。不少项目只在小程序端隐藏管理入口接口本身没有鉴权懂抓包的人可以直接构造请求调用管理接口删除数据。后端过滤器至少要对/api/admin/*做二次校验并记录操作日志。事务方面setAutoCommit(false)必须在 try 之前rollback放在 catch 里finally 中恢复自动提交并关闭连接顺序错了会导致连接状态污染。验证并发场景时打开两个终端同时执行同一个报名请求正常情况只有一个成功。检查activity_apply表只多一条记录apply_count只增加 1。如果出现多人同时报名但报名总数超过设定上限重点排查UPDATE语句里是否带上apply_count max_people条件以及事务是否真的回滚了INSERT。这一条排查路径能覆盖报名系统最常见的数据一致性故障。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门