ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

马拉松报名系统毕设全攻略:微信小程序+Java后端从0到答辩

马拉松报名系统毕设全攻略:微信小程序+Java后端从0到答辩 简介基于微信小程序Java后端的马拉松报名系统毕业设计完整适配计算机专业毕设或课程设计场景。项目采用微信小程序开发工具、Java后端与MySQL数据库实现用户注册登录、赛事查看报名、活动商城购买及管理员后台管理等功能覆盖个人中心、赛事信息管理、订单管理等业务模块提供一套可直接运行并可二次开发的实战源码及开发思路参考。压缩包内含1220个文件约41.42MB以png界面图、js/vue前端逻辑、java后端服务、wxss/wxml小程序页面、json配置、sql数据库脚本及mp4演示视频等类型为主目录结构清晰便于按模块查阅。目前已有300人学习下载附带的说明文档、演示视频与数据库文件可帮助快速部署适合需要完整毕设项目参考或系统学习小程序前后端整合开发的学生。1. 马拉松报名系统毕业设计为什么微信小程序 Java 后端是稳妥的选型做毕业设计最怕的不是不会写代码而是选了一个“看起来容易、做起来没内容”的题目。马拉松报名系统恰好相反它有一套清晰的业务闭环——用户在小程序里浏览赛事、在线报名、模拟支付、查询报名状态管理员在后端发布赛事、管理名额、导出参赛名单。用微信小程序做前端、Java 做后端是这几年毕业设计里最稳的组合小程序端天然适合“报名”这种高频、移动、轻量的场景Java 后端又有成熟的生态和大量可复用的代码参考。这套系统适合计算机、软件工程专业的毕设也适合想完整走一遍前后端项目的同学。下文不假设你已经有源码而是把一套能跑通的方案讲清楚数据库怎么建、接口怎么写、小程序页面怎么做、常见的坑在哪。2. 系统功能拆解与数据库设计从报名到领物先想清楚再写代码2.1 用户端与管理员端的功能边界小程序端该做什么、不该做什么很多毕业生拿到“马拉松报名系统”这个题目后第一反应是把所有功能都塞进小程序里用户报名、管理员审核、赛事发布、数据统计恨不得一个页面全搞定。真这么做大概率会在中期翻车因为小程序端不适合做复杂的后台管理页面结构一旦膨胀微信审核和真机调试都会变成折磨。合理的边界划分是这样的用户端只负责“报名链路”上的事——打开小程序看到赛事列表列表区分“报名中”“即将开始”“已结束”三种状态点进赛事详情能看到比赛时间、地点、组别全程/半程/健康跑、报名费、剩余名额点击报名后填写姓名、身份证、手机、紧急联系人然后提交订单订单进入“待支付”状态后用模拟支付接口标记为“已支付”最后在“我的报名”里看到自己的报名记录和状态。这个链路是小程序的核心体验做到流程顺畅即可。管理员端不要做进小程序用 Spring Boot 自带的接口文档Swagger或者一个极简 HTML 后台就够了。管理员需要发布赛事、设置报名时间与名额、审核特殊组别报名、导出报名名单这些操作在电脑浏览器上效率更高。常见做法是后端提供 REST 接口管理页面只调接口如果你只想省事甚至可以只做接口用 Postman 调一遍当作演示。这样边界一划清数据库设计和后端接口的数量就定下来了用户端需要 5~6 个页面后端需要 8~10 个接口工作量可控。这里要给自己一个提醒不要为了“功能多”去加积分商城、社区动态这类和报名无关的模块。毕业设计答辩看的是业务完整性不是功能数量。马拉松报名系统的核心是“名额有限、先到先得、支付确认”把这条主链路做扎实比堆砌十个无关联的页面更有说服力。2.2 数据库表设计用户、赛事、报名、订单、成绩这五张表怎么建功能边界定好后数据库设计就顺理成章了。我一般会画一张简单的 ER 图不用工具直接在纸上画把实体和关系列出来用户和报名是一对多赛事和报名是一对多报名和订单是一对一。成绩表可做可不做但建议保留因为马拉松系统如果没有“成绩查询”答辩时容易被问“报完名之后呢”。即使不做前端展示后端留一个成绩表也能体现数据闭环。核心表一共五张。用户表存微信登录后的用户信息关键是 openid它是微信用户在小程序内的唯一标识。赛事表存马拉松比赛的元信息包括赛事名称、起止时间、报名时间、地点、报名费、总名额、已报名数。报名表是核心业务表记录某用户对某赛事的报名行为包含组别、参赛人姓名、身份证号、手机号、紧急联系人、报名状态。订单表和报名表一对一存订单号、支付金额、支付状态、支付时间这样做是为了把“报名成功”和“支付成功”两个状态分开。成绩表存完赛成绩和排名如果赛事没有成绩模块可以留空表。这里有两个比较容易想不明白的点。第一为什么用户表里要有“手机号”和“身份证号”而不直接放在报名表里因为同一个用户可能报名多场比赛个人信息应该复用报名表里保存比赛当次的参赛人信息可能是帮朋友报名所以报名表里要有参赛人独立的姓名和证件。第二为什么订单表不直接叫支付表因为订单还要承载支付流水号、退款状态等附加信息单独建表以后扩展方便不会破坏报名表的结构。字段类型上主键用INT UNSIGNED配合AUTO_INCREMENT足够个人项目使用。openid 用VARCHAR(64)因为微信返回的 openid 长度在 28~32 之间留 64 长度安全。价格字段建议用DECIMAL(10,2)不要用FLOAT避免浮点数精度问题。时间字段统一用DATETIME不要用TIMESTAMP存未来几年的赛事时间因为TIMESTAMP有 2038 年上限虽然马拉松赛事不会排到那么远但统一用DATETIME能少解释一轮。2.3 用 SQL 建出可跑通的库建表脚本与关键字段说明数据库选 MySQL 5.7 或 8.0 都可以毕设环境里 5.7 最常见。下面是一份可以直接执行的建表脚本我按上面的设计落了地关键注释写在 SQL 里。-- 创建数据库指定 utf8mb4 字符集防止中文乱码 CREATE DATABASE IF NOT EXISTS marathon_sign DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE marathon_sign; -- 用户表 CREATE TABLE user ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键, openid VARCHAR(64) NOT NULL COMMENT 微信小程序 openid, nickname VARCHAR(64) DEFAULT COMMENT 昵称, avatar VARCHAR(255) DEFAULT COMMENT 头像 URL, phone VARCHAR(20) DEFAULT COMMENT 手机号, id_card VARCHAR(30) DEFAULT COMMENT 身份证号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 赛事表 CREATE TABLE event ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键, name VARCHAR(128) NOT NULL COMMENT 赛事名称, description TEXT COMMENT 赛事介绍, start_time DATETIME NOT NULL COMMENT 比赛开始时间, sign_start_time DATETIME NOT NULL COMMENT 报名开始时间, sign_end_time DATETIME NOT NULL COMMENT 报名截止时间, location VARCHAR(255) DEFAULT COMMENT 比赛地点, distance VARCHAR(50) DEFAULT COMMENT 比赛距离如 42.195KM, fee DECIMAL(10,2) DEFAULT 0.00 COMMENT 报名费, quota INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 总名额, signed_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 已报名人数, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-未开始 1-报名中 2-已结束, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT赛事表; -- 报名表 CREATE TABLE registration ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键, event_id INT UNSIGNED NOT NULL COMMENT 赛事ID, user_id INT UNSIGNED NOT NULL COMMENT 用户ID, group_type VARCHAR(20) NOT NULL DEFAULT 健康跑 COMMENT 组别全程/半程/健康跑, applicant_name VARCHAR(32) NOT NULL COMMENT 参赛人姓名, applicant_id_card VARCHAR(30) NOT NULL COMMENT 参赛人身份证号, applicant_phone VARCHAR(20) NOT NULL COMMENT 参赛人手机号, emergency_contact VARCHAR(32) DEFAULT COMMENT 紧急联系人, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-待支付 1-已支付 2-已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_event_id (event_id), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT报名表; -- 订单表 CREATE TABLE orders ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键, order_no VARCHAR(64) NOT NULL COMMENT 订单号, registration_id INT UNSIGNED NOT NULL COMMENT 关联报名ID, pay_amount DECIMAL(10,2) NOT NULL COMMENT 支付金额, pay_status TINYINT NOT NULL DEFAULT 0 COMMENT 0-未支付 1-已支付, pay_time DATETIME DEFAULT NULL COMMENT 支付时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表; -- 成绩表 CREATE TABLE result ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键, registration_id INT UNSIGNED NOT NULL COMMENT 报名ID, event_id INT UNSIGNED NOT NULL COMMENT 赛事ID, user_id INT UNSIGNED NOT NULL COMMENT 用户ID, finish_time VARCHAR(20) DEFAULT COMMENT 完赛时间如 03:25:47, rank_no INT UNSIGNED DEFAULT NULL COMMENT 名次, PRIMARY KEY (id), KEY idx_event_id (event_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT成绩表;这段脚本有四个地方值得留意。第一user表用 openid 建唯一索引这是微信登录后做“没注册就自动建号”的查重依据。第二event表加了signed_count字段它不是冗余而是报名时用来做“剩余名额”判断的计数如果用COUNT(*)实时查报名表在高并发下压力会更大。第三registration表和orders表拆开是因为“待支付”和“已支付”是两种状态支付前可能取消支付后可能退款拆开表才能记录完整状态流转。第四所有时间字段都用DATETIME字符集用utf8mb4这样从建库开始就避开中文乱码和时区问题。建完表后先插入两条测试数据一条“报名中”的赛事一条“已结束”的赛事后面小程序列表页和详情页都要靠它们调试。不要等后端写完再补数据数据库先行页面渲染才有东西可看。3. 用 Spring Boot 搭出报名系统后端接口、鉴权与并发控制3.1 后端项目结构与依赖一个 Maven 工程该引入哪些东西后端我选 Spring Boot 2.x配合 MyBatis-Plus 操作数据库这是毕业设计里最常见的组合。Spring Boot 负责 HTTP 接口和依赖管理MyBatis-Plus 省去手写大量 SQL尤其适合快速出活。新建一个 Maven 工程pom.xml里最核心的依赖是下面这些。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies !-- Web 接口 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-Plus 和 MySQL 驱动 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version scoperuntime/scope /dependency !-- 简化实体类代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency !-- JWT 做登录态 -- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency /dependencies依赖里有两个容易踩的点。第一Spring Boot 2.7 对应 MyBatis-Plus 3.5.x不要引入 4.x 版本因为 4.x 的包名和配置方式有变化网上很多教程对不上。第二MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver配置文件里必须带serverTimezoneAsia/Shanghai否则连接数据库时会报时区错误。配置写在application.yml里server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/marathon_sign?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case是 MyBatis-Plus 自动把数据库的下划线字段名映射成 Java 驼峰属性比如sign_start_time映射到signStartTime。如果不开这个配置查询结果会大面积赋值为 null。log-impl开启控制台 SQL 日志调试接口时非常有用能看到具体执行的 SQL 和参数。项目内部包结构我习惯这样分controller放接口service放业务逻辑mapper放数据操作entity放实体类common放统一返回结果和异常处理。不要一个 Controller 里写完全部逻辑后面加并发控制和登录鉴权时会改到你怀疑人生。3.2 报名接口的最小实现事务、锁与库存扣减报名接口是整个系统的灵魂也是最容易在答辩时被追问的地方“多个用户同时报名一个赛事你怎么保证不会超卖”如果只是简单查一下signed_count quota然后插入报名记录在并发量稍高的情况下必然翻车。一个可靠的实现需要两步把名额判断和扣减做成原子操作再把插入报名表和订单表包进同一个事务。我用 MyBatis-Plus 的UpdateWrapper写一个“条件更新”来扣减名额。SQL 大致等价于UPDATE event SET signed_count signed_count 1 WHERE id ? AND signed_count quota数据库行锁会保证同一时刻只有一个请求能更新成功。更新影响行数为 1说明名额拿到为 0说明名额已满。这个做法比SELECT ... FOR UPDATE更轻也比外加分布式锁更简单适合毕业设计。Override Transactional(rollbackFor Exception.class) public Result createRegistration(RegistrationRequest request) { Long eventId request.getEventId(); // 扣减名额条件更新只有剩余名额大于 0 时才加 1 UpdateWrapperEvent wrapper new UpdateWrapper(); wrapper.eq(id, eventId) .gt(signed_count, 0) .setSql(signed_count signed_count 1); int rows eventMapper.update(null, wrapper); if (rows 0) { return Result.error(名额已满); } // 构建报名记录状态为待支付 Registration registration new Registration(); registration.setEventId(eventId); registration.setUserId(request.getUserId()); registration.setGroupType(request.getGroupType()); registration.setApplicantName(request.getApplicantName()); registration.setApplicantIdCard(request.getApplicantIdCard()); registration.setApplicantPhone(request.getApplicantPhone()); registration.setEmergencyContact(request.getEmergencyContact()); registration.setStatus(0); registrationMapper.insert(registration); // 生成订单订单号用时间戳加随机数 Orders order new Orders(); order.setOrderNo(M2025 System.currentTimeMillis()); order.setRegistrationId(registration.getId()); order.setPayAmount(request.getFee()); order.setPayStatus(0); ordersMapper.insert(order); return Result.success(registration.getId()); }这个方法有三个关键点。第一Transactional保证“扣名额”和“插报名”“插订单”要么全部成功要么全部回滚如果插入订单时抛异常名额扣减也会回滚不会出现“名额扣了但报名没生成”的脏数据。第二UpdateWrapper里的gt(signed_count, 0)表示只更新剩余名额大于 0 的赛事数据库层面挡住超卖。第三订单号用M2025 System.currentTimeMillis()只是一种简单生成法演示时够用如果要多位用户同时报名订单号可能重复建议再加上随机数或者用雪花算法。这里还要提一个“模拟支付”接口的实现小程序端点击“立即支付”后后端把报名状态从“待支付”改为“已支付”订单状态也改为“已支付”。毕设不需要接真实微信支付否则要商户号、证书、回调地址一套折腾下来能卡你一周。模拟支付在答辩时说明“这里预留了真实支付接口”即可评审老师都懂。3.3 微信登录与 token 签发小程序 wx.login 到后端 code2Session小程序端调用wx.login拿到一个临时code然后把code发给后端后端拿着code去微信服务器换openid和session_key。这是微信小程序登录的标准流程也是必考知识点。Spring Boot 里我用RestTemplate调微信接口代码不复杂但有一个容易忽略的坑code 只能用一次而且 5 分钟内有效调试时经常因为重复使用同一个 code 而报“invalid code”。Override public Result wxLogin(String code) { String url https://api.weixin.qq.com/sns/jscode2session?appid appId secret appSecret js_code code grant_typeauthorization_code; // 用 RestTemplate 请求微信接口 String response restTemplate.getForObject(url, String.class); JSONObject json JSONObject.parseObject(response); String openid json.getString(openid); if (openid null) { return Result.error(登录失败 json.getString(errmsg)); } // 查询或创建用户 LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.eq(User::getOpenid, openid); User user userMapper.selectOne(wrapper); if (user null) { user new User(); user.setOpenid(openid); userMapper.insert(user); } // 生成 JWT放入用户 ID 和过期时间 String token Jwts.builder() .setSubject(String.valueOf(user.getId())) .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, jwtSecret) .compact(); return Result.success(token); }这段代码有两个地方要重点说明。第一appId和appSecret从微信公众平台拿不能写死在代码里提交到公开仓库GitHub 上每天都有扫密钥的机器人一旦泄露你的小程序会被陌生人乱调接口。放在application.yml里并且用ConfigurationProperties读取是相对安全的做法。第二JWT 的secret要单独配置不要用默认值JWT 过期时间我设置成 7 天对应小程序端“一次登录一周内免登录”的体验。小程序每次请求把 JWT 放在请求头Authorization: Bearer token后端用一个拦截器解析 token 并取出用户 ID就能实现登录态。有一个常见的认知误区拿到session_key后要不要解密用户手机号毕设里通常不需要。就算你要实现“一键获取手机号”那是独立的getPhoneNumber接口和wx.login不是一回事。先把 openid 对应的用户体系跑通手机号让用户手动填表单这样工作量小逻辑也更清晰。4. 微信小程序前端落地页面、列表加载更多与兼容性4.1 小程序页面结构赛事列表、详情、报名表单、我的小程序端我建议分成四个 tab赛事列表、赛事详情、报名表单、我的。实际做的时候赛事详情、报名表单、我的报名记录都可以用普通页面跳转首页和“我的”作为 tab 页。页面文件清单大致是这个结构pages/ ├── index/ # 赛事列表首页 │ ├── index.wxml │ ├── index.js │ ├── index.json │ └── index.wxss ├── detail/ # 赛事详情 │ ├── detail.wxml │ ├── detail.js │ ├── detail.json │ └── detail.wxss ├── signup/ # 报名表单 │ ├── signup.wxml │ ├── signup.js │ ├── signup.json │ └── signup.wxss ├── order/ # 订单支付页 │ ├── order.wxml │ ├── order.js │ ├── order.json │ └── order.wxss └── mine/ # 我的 ├── mine.wxml ├── mine.js ├── mine.json └── mine.wxss赛事列表页是最先见的页面它要展示赛事名称、状态标签报名中/已截止、已报名人数和剩余名额。数据来源是后端的GET /api/event/list返回当前所有赛事。赛事的“报名中”状态不要在前端用时间戳自己算因为每个用户手机时间可能不准应该由后端根据sign_start_time和sign_end_time计算好返回status字段前端只负责渲染。这个细节能避免演示时出现“手机时间不对导致按钮状态错误”的尴尬。赛事详情页显示完整介绍底部放一个“立即报名”按钮。如果赛事已结束或者名额满按钮要置灰并显示原因。报名表单页是核心字段包括组别选择、参赛人姓名、身份证号、手机号、紧急联系人提交前要前置校验身份证号 15 位或 18 位手机号 11 位以 1 开头。前端做一层校验能减少后端收到脏数据的概率但后端接口也必须做同样的校验别指望前端拦截就万事大吉。4.2 列表加载更多与顶部导航栏高度两个高频自定义点小程序列表页最常被问到的就是“加载更多”。赛事虽然不会特别多但报名记录可能很多。我习惯在“我的报名记录”页面做分页加载用onReachBottom触底加载下一页。这里有一个很多新手不知道的细节onReachBottom只在页面滚动到底部时触发如果你用的是scroll-view组件那需要监听scrolltolower事件并且要给scroll-view设置一个明确的高度否则永远不触发。// pages/mine/mine.js Page({ data: { registrations: [], page: 1, pageSize: 10, hasMore: true, loading: false }, // 页面触底加载更多 onReachBottom() { if (this.data.hasMore !this.data.loading) { this.loadRegistrations(); } }, async loadRegistrations() { if (this.data.loading) return; this.setData({ loading: true }); const res await wx.request({ url: http://localhost:8080/api/registration/my, data: { page: this.data.page, pageSize: this.data.pageSize }, header: { Authorization: Bearer wx.getStorageSync(token) } }); const list res.data.data.records; this.setData({ registrations: this.data.registrations.concat(list), page: this.data.page 1, hasMore: list.length this.data.pageSize, loading: false }); } });这段代码有三个参数值得注意。pageSize设置为 10一次加载 10 条既能看出“加载更多”的效果又不至于一次性渲染太多hasMore的判断依据是“本次返回条数是否等于 pageSize”如果返回不足一页说明没有更多了loading是防止触底事件连续触发导致重复请求的“节流开关”。这个loading极其重要我在真实开发中见过有人不设它结果一次滚动触发五六次请求数据库被刷出好几条重复数据好在有事务否则更严重。另一个高频自定义点是顶部导航栏高度。小程序的胶囊按钮右上角那个圆形菜单位置在不同机型上不一样如果页面里用了自定义导航栏计算高度时必须动态获取。简单做法是使用官方wx.getWindowInfo()拿到statusBarHeight和menuButtonBoundingClientRect然后算出导航栏高度。直接写死 44px 或 64px在带刘海屏的 iPhone 上会看到顶部的“刘海”挡住页面内容这是答辩演示时最掉分的细节。// 自定义导航栏高度计算 const info wx.getWindowInfo(); const menu wx.getMenuButtonBoundingClientRect(); const navBarHeight (menu.top - info.statusBarHeight) * 2 menu.height; this.setData({ statusBarHeight: info.statusBarHeight, navBarHeight: navBarHeight });这段代码的逻辑是菜单按钮底部到状态栏顶部的距离乘以 2 加上按钮高度就是导航栏的总高度。这个公式是社区里广泛验证过的比单独写机型适配靠谱。在index.json里配置navigationStyle: custom后页面的最顶部留出padding-top: statusBarHeight navBarHeight内容就不会被刘海遮挡了。4.3 从 request 到渲染封装 API 请求与状态管理小程序原生wx.request用起来很繁琐每个页面都要写成功回调、失败回调、错误提示。我一般会封装一个request.js统一处理 baseURL、token 注入和错误提示。这样页面代码看起来干净也方便在开发环境和生产环境之间切换后端地址。// utils/request.js const BASE_URL http://localhost:8080/api; function request(path, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method: method, data: data, header: { Content-Type: application/json, Authorization: Bearer wx.getStorageSync(token) }, success(res) { if (res.data.code 200) { resolve(res.data.data); } else if (res.data.code 401) { // token 失效跳转登录页 wx.navigateTo({ url: /pages/login/login }); } else { wx.showToast({ title: res.data.msg, icon: none }); reject(res.data); } }, fail(err) { wx.showToast({ title: 网络请求失败, icon: none }); reject(err); } }); }); } module.exports { request, BASE_URL };这个封装里有几个设计决策。第一后端返回格式统一为{ code, msg, data }所以这里直接判断code 200若后端返回不规范页面层会很难处理。第二token 从wx.getStorageSync取出每次请求自动带上不需要页面层关心登录态。第三401 表示 token 过期这里跳转登录页如果毕设没有做单独的登录页也可以改成重新调用wx.login静默登录。第四fail里统一弹出“网络请求失败”省得每个页面写重复代码。页面里的调用方式就是单行代码比如赛事列表页const { request } require(../../utils/request.js); Page({ data: { events: [] }, async onLoad() { const events await request(/event/list, GET); this.setData({ events: events }); } });渲染方面用wx:for循环events每一项显示赛事名称、报名状态、名额进度。这里有一个优化点名额进度条的前端计算用“已报名数 / 总名额”的比例如果signed_count接近quota可以把进度条颜色从绿色切到红色给用户紧迫感。这个是马拉松报名系统的常见交互效果直观代码成本又低。页面之间传参也要注意从列表页跳到详情页只需要传eventId详情页再用onLoad(options)接收options.eventId然后请求接口拿完整数据。不要在跳转时把整个赛事对象塞进url因为小程序url有长度限制中文和特殊字符会报错。这是新手最容易犯的错。5. 避坑/常见问题/排查毕业设计跑不起来的 5 个经典现场做这个系统十个里面有九个不是死在业务逻辑上而是死在环境配置和联调细节上。下面这五条都是我自己带学生做毕设时反复看到的现场每条按“现象 → 原因 → 解决”写方便你在踩坑时直接对号入座。5.1 现象小程序 request 到不了本地后端小程序开发工具里网络请求发出去了后端控制台却没有任何日志或者报ERR_CONNECTION_REFUSED。原因微信开发者工具默认不允许访问本机localhost而且从基础库 2.x 开始对未配置域名的请求有强拦截。虽然开发工具可以勾选“不校验合法域名”但很多新手不知道要勾。解决在开发者工具右上角“详情” → “本地设置”里勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”。如果用了真机调试localhost不会再指向电脑真机上的localhost是手机自己此时要把后端接口地址改成电脑的局域网 IP比如http://192.168.1.100:8080并且确保电脑防火墙放行 8080 端口。5.2 现象数据库导入失败中文乱码用 Navicat 导入 SQL 文件表建好了但中文全部变成???或者直接报Unknown character set。原因SQL 文件本身的字符集和数据库连接字符集不一致。常见的坑是数据库是utf8mb4但导入工具连接的字符集还是latin1或者 SQL 文件文本编码是 GBK被工具按 UTF-8 解析。解决建库脚本里已经写了DEFAULT CHARACTER SET utf8mb4这个是第一道保险。导入前在 Navicat 连接属性里把“编码”设为utf8mb4再把 SQL 文件用记事本另存为 UTF-8 编码。如果已经导乱了删库重建一遍不要手动去改乱码数据改不干净的。5.3 现象报名并发时报“名额已满”但明明有名额用 Postman 开并发请求报名接口明明赛事名额还剩十几个接口却返回“名额已满”而且数据库signed_count没有变化。原因十有八九是事务没生效。比如Transactional加在了一个类内部调用的方法上或者方法不是publicSpring AOP 拦截不到还有一种情况是数据库表引擎是 MyISAM不支持行锁条件更新退化成全表锁但逻辑没变化导致更新影响行数判断错误。解决检查Transactional是否加在service类的public方法上并且由外部 Controller 调用检查建表语句里ENGINEInnoDB。最简单的排查方法是在配置里开启 SQL 日志看“扣减名额”的 UPDATE 语句是否真的带上了signed_count 0的条件如果 SQL 是对的但影响行数始终为 0就去检查表引擎。5.4 现象wx.login 拿不到 code 或 code2Session 报错小程序端wx.login在success回调里打印 code 是undefined或者后端调用jscode2session返回40013invalid appid。原因wx.login的 code 只有在success回调里才能拿到不要用同步变量去接40013是因为小程序的AppID和AppSecret不匹配最常见的是把测试号的 AppID 和正式小程序的 AppSecret 混用了。解决真机调试时wx.login一般都能成功开发工具里要先确认不是“未登录”状态。后端报40013时到微信公众平台 → 开发管理 → 开发设置里核对 AppID 和 AppSecret注意不要有多余空格。还有一个隐藏点如果小程序是“测试号”它的 AppSecret 需要在测试号申请页面单独复制不能从公众平台后台拿。5.5 现象演示视频录好了但答辩时环境变了毕业设计通常要交演示视频但答辩现场要用另一台电脑或者重新启动项目结果发现数据库密码不对、后端端口被占用、小程序 AppID 还是别人的。原因项目配置写死了本机路径和密钥没有做环境差异隔离或者演示视频录得很顺但从来没有“从零启动一遍”过。解决准备一个README启动手册写清楚 JDK、Maven、MySQL 的版本数据库导入步骤后端启动命令小程序开发工具里需要填的 AppID 和后端地址。答辩前一天做一次“冷启动演练”关掉所有服务删掉数据库按 README 从零恢复一遍。这个过程能暴露大量“我以为没问题”的问题比答辩时现场收拾强得多。代码里的密钥和密码可以写成环境变量读取但这在毕设里不是强制项至少保证同一个压缩包里交付的配置是自洽的。这五条不是全部但覆盖了 80% 的翻车现场。遇到问题时先去看控制台报错再去查配置不要靠猜。6. 答辩前最后一步用真实数据把系统跑成可演示的状态系统能跑通只是第一步答辩要的是“讲得清、演示得顺”。我习惯在答辩前给三张表里填一组有说服力的数据赛事表里放三场比赛一场“报名中”、一场“未开始”、一场“已结束”报名表里准备两条真实样子的记录一条已支付、一条待支付订单表对应对应支付状态。这组数据能让评审老师一眼看到系统的完整状态不用现场临时录入。注意数据要真实到像有人用过——姓名用“张伟”“李娜”这类普通名字手机号用 13 位合法格式赛事名称写“2025 年城市马拉松”而不是“测试赛”。演示时鼠标点击要比讲解快半拍提前把小程序切到赛事列表页后端服务提前启动数据库连接正常。有一个答辩技巧很多人不知道把“模拟支付”这一步做成显眼的功能点完“立即支付”后弹出一个带订单号的页面告诉评审“这里对接真实微信支付只需替换一个接口”。这句话能直接堵住“你用的是真钱吗”“支付安全怎么保证”这类问题同时显得你理解了接口扩展边界。我自己带过的项目里凡是演示到支付流程的答辩分数都比只讲 CRUD 的高一截。最后说一个血泪经验永远不要在答辩现场现敲代码。哪怕只是改一个按钮颜色也可能因为网络、输入法、演示环境的差异浪费五分钟。所有要展示的内容提前录好备用现场演示只是加分项。代码提交前用 Git 打一个 tag数据库导出一份 SQL 放同目录压缩包命名带日期这些小事能让你在“最后交文件”时不用后悔。希望帮到你。本文还有配套的精品资源点击获取
返回列表