2024年我在贵州那边做县域体育赛事信息化,接到了一个村超和民运会赛务报名管理系统的活儿。这类系统听起来不大,真做起来才发现,从运动员资格审核到赛程编排,从报名截止控制到成绩录入汇总,每一环都有人会在线上等着催你改需求。后来整个项目基于 Java + Vue + Spring Boot 这套组合落地,前端用 Vue 做单页应用,后端用 Spring Boot 提供接口,数据库走 MySQL,整个开发周期压缩到了三周半,上线后扛住了几百支队伍同时报名的并发压力。今天就把这个系统的设计与实现思路拆开来讲,包括需求拆解、数据模型、核心代码、部署踩坑,给正在做同类信息系统的人一个可以直接参考的样板。
我自己做这类信息服务系统最深的体会是,村超、民运会这种基层赛事,它不是没有报名流程,而是流程散落在微信接龙、纸质表格、Excel 和电话里边。系统最核心的价值不是炫技,而是把“谁在什么时候能报什么项目、报完怎么分组、比赛时成绩谁说了算”这串链条捋清楚。所以这篇文章会围绕这几块展开:先说业务建模的思路,再讲数据库和接口设计,然后落到前后端具体实现细节,最后把上线过程中最容易翻车的几个问题列出来。不管你是学生做毕业设计,还是刚工作的开发人员接了类似项目,都可以照着自己改一版。
1. 项目整体设计与思路拆解
1.1 村超/民运会的赛务场景到底有哪些痛点
村超这几年火遍全网,本质上是乡村足球超级联赛,但“村超”这两个字已经变成了一种基层群众体育赛事的代名词。民运会则是少数民族传统体育运动会,比赛项目就更杂了,有竞赛类的,有表演类的,还有民族式摔跤、押加这类特色项目。这两种赛事放到一起做系统,最大的难题是“赛事规则不统一”。同一个系统里,可能这边是足球队(11人制)在报名,那边是押加个人赛在报名,还有一支表演队只报一个节目、三个人上场,根本没法用一套固定的字段模板去套。
我在做需求调研时,组委会提得最多的三个痛点是:
第一,报名信息反复核对太累。参赛队伍领队交上来的报名表,有的用 Excel,有的用手机拍照,有的直接微信发一段文字。工作人员要把这些信息重新录入电脑,一不小心就会把身份证号输错一位,等做秩序册的时候发现运动员名字和证件对不上,又是连环电话。
第二,项目冲突和资格审核靠人眼。个人项目里的运动员经常同时报多个项目,比如一个押加运动员可能同时报 68 公斤级和 76 公斤级,但赛事规程规定每人只能报一个级别,这种逻辑靠人工检查很容易漏,必须靠系统规则去卡。
第三,赛程和成绩的数据孤岛。报名系统往往和检录、计分、成绩公布是脱节的,报名数据导出来给裁判组以后,裁判组统计完成绩又要手动录回系统。我在设计时干脆把报名和成绩放在同一个系统里,比赛当天可以直接在终端上录入成绩,实时生成积分榜,省掉中间导表格的环节。
1.2 技术选型为什么是 Spring Boot + Vue 这套组合
市面上做信息管理系统有成型的低代码平台,也可以直接用 PHP 或者 Python 快速搭。但这里有几个硬性条件:学校、县政府信息中心那边希望系统能部署在他们的内网服务器上,对数据库和中间件有明确的国产化或者说自主可控要求;后期可能要接入人脸识别检录、大屏数据展示这类硬件设备,系统必须提供标准接口;开发周期又紧,前后端不能互相等。
Spring Boot 在这种项目里优势很明显。它基于 Spring 生态,自动装配机制把大量繁琐的 Bean 配置省掉了,我只需要在 pom 里引入对应 starter,再写几个配置项就能把 Web、持久层、缓存、安全这些能力组合起来。更关键的是,Spring Boot 的社区资料极度丰富,遇到问题搜索引擎一找一大片,这对小团队来讲就是保命符。Vue 作为前端框架,单页应用的开发体验比传统 JSP 厚模板舒服得多,组件化拆分开发现场报名信息录入界面非常高效,而且 Vue 的学习曲线平缓,招人也好招。
数据库我选了 MySQL 8.0,ORM 用 MyBatis-Plus。选 MyBatis-Plus 而不是 JPA,是因为这类系统的 CRUD 操作太多了,而且会经常写一些多表关联加聚合统计的 SQL,MyBatis-Plus 既保留了 MyBatis 灵活写 SQL 的能力,又提供了 BaseMapper 这种开箱即用的单表操作封装。缓存的引入是上线前临时决定加的,报名高峰 Close 之前那一下,报名接口的 QPS 会突然暴涨,我用 Redis 缓存赛事基础配置和运动员资格校验结果,实测效果非常明显。
2. 核心细节解析与实操要点
2.1 系统角色与功能边界划分
赛务报名系统跟普通的企业 OA 不一样,它的角色天然就是多端的。我在设计权限模型时划分了五个端:
- 系统管理员:管账号分配、赛事创建、系统配置,相当于技术后台的超级管理员。
- 组委会/赛务组:负责发布赛事规程、审核报名信息、编排赛程、录入成绩、发布公告。
- 领队/教练:代表队伍操作,可以创建队伍、添加运动员、选择参赛项目、提交报名。
- 运动员:查看自己的报名状态、赛程安排、成绩结果。
- 裁判/检录员:移动端或者电脑端录入成绩,打印检录表。
这里有个容易忽略的点,领队和运动员并不是天然分开注册的。我设计的是“先注册账号,后申请领队身份”的流程。运动员注册之后可以绑定到某个队伍,这时候他自动成为该队伍的队员,而领队身份需要通过队伍创建或管理员授权才能获得。这样能避免所有人都能随便建队伍造成的数据污染。
前端路由和后端接口权限是两个层面的事情。前端通过路由守卫控制页面能不能进入,后端通过拦截器注解校验接口权限。我用了自定义注解 @RequiresRole,AOP 拦截器里判断当前登录人的角色编码是否在允许列表里,如果不在就直接返回 403。这样就算有人绕过前端直接调接口,也拿不到其他角色的数据。
2.2 数据库表结构与字段设计思路
这类系统的表设计其实比很多企业系统有意思,因为它有“赛事—项目—队伍—运动员”这套多对多的嵌套关系。我最终落地的核心表如下:
赛事表(event)主要存赛事名称、举办时间、状态、报名开始/结束时间。这里的状态字段我用了枚举值表示:1 未开始,2 报名中,3 报名截止,4 进行中,5 已结束。为什么不用字符串直接存?因为后续状态流转要做条件查询,比如WHERE event.status = 2,如果存的是中文,还得先处理编码,字符串大小写不统一也是坑,直接用 int 加枚举类在 Java 层面对应,简洁高效。
项目表(competition_item)要解决“项目类型不一致”的问题。我是这样设计的:项目归属赛事,项目有名称、项目类别(竞赛类/表演类/民族式摔跤/押加)、项目类型(个人/团体)、限制人数(男/女)、参赛级别、报名费用。团体项目会在关联表里说明“每队最少人数/最多人数”,个人项目则用级别字段区分。
队伍表(team)记录参赛单位信息,比如代表队名称、单位类型(乡镇/学校/社会团体)、领队信息。运动员表(athlete)是核心,姓名、身份证号、性别、民族、出生日期、照片、所属队伍。这里身份证号我做了唯一索引,同一个身份证号不允许注册到两个不同队伍。报名表(registration)是业务的核心表,记录队伍针对某个赛事报了哪个项目,包含审核状态:0 待提交,1 待审核,2 已通过,3 已驳回。如果报名被驳回,驳回原因字段必须保留,不然领队不知道哪里错了。
把报名拆成“报名单主表 + 报名明细表”是这类系统比较稳妥的做法。队伍先创建一张报名单,然后在明细里添加运动员,对于团体项目还需要指定队长和上场位置。这样一张报名单就是一个完整的申报包,组委会审核时只需要对单审批,不需要在多个界面来回跳。
2.3 报名状态机的设计与流程控制
报名状态的推进是整个系统最容易出 bug 的地方。我见了太多系统把状态存在一个字段里,然后用 if-else 到处判断,最后状态改来改去自己都搞晕了。这里我直接用状态机模式,给报名单定义清晰的状态流转路径:草稿 → 已提交 → 审核中(提交后进入) → 已通过 / 已驳回。驳回后领队可以编辑重新提交,重新提交后状态回到审核中。
我在接口层做了状态流转的校验方法,用一组 Map 定义允许的流转路径,比如草稿状态只允许执行“提交”操作,已驳回状态只允许执行“编辑”或“重新提交”操作,已通过状态不允许任何修改。这种写法的好处是,后面加新需求,比如“报名截止后组委会可以强制退回某条报名”,只要在这张流转表里加一条路径,改起来非常快。
报名人数限制的控制也有讲究。比如一个团体项目限报 8 人,实际提交时发现人数超了,此时应该在前端就拦截住。前端的校验依赖比赛项目配置里的人数上限,而后端提交接口也必须再查一次数据库实时校验,因为存在多个领队同时操作的并发场景,前端校验只是体验层面的保障,后端校验才是正确性的兜底。具体做法是在提交报名的事务里加上SELECT ... FOR UPDATE行锁,锁住报名单记录,然后再判断明细表当前人数,防止超限。
3. 实操过程与核心环节实现
3.1 Spring Boot 工程结构与关键依赖
我习惯用 Spring Initializr 创建工程,Java 版本用 17,Spring Boot 版本一开始用的 3.2.x。这里要提醒一件事,Spring Boot 3.x 对 Java 版本有硬性要求,最低是 Java 17,如果你本地环境还在用 Java 8,那就老老实实选 Spring Boot 2.7.x。不同版本对应的组件坐标也不一样,比如 Spring Boot 3.x 里很多 spring 官方库的 groupId 从org.springframework.boot调整或升级了版本,网上搜资料时看到 2.x 的解决方案可能直接套不上,这是“springboot版本太高”这个热搜词最常见的来源。
核心依赖我会贴一下 pom.xml 片段:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.25</version> </dependency> </dependencies>Redis 在这里不是必须的,但如果你打算做验证码、防重复提交、缓存热数据,提前引入准没错。Hutool 是我个人很喜欢的一个工具库,身份证校验、日期处理、Excel 导出这些功能都有封装,能省不少重复代码。
application.yml 里比较重要的配置项是数据源、Redis 连接、MyBatis-Plus 的驼峰映射和逻辑删除配置。这里我直接贴一份实际用的配置:
server: port: 8080 servlet: context-path: /api spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/sport_register?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 data: redis: host: localhost port: 6379 database: 0 timeout: 3000ms mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0关于逻辑删除,这类赛事系统不建议对运动员报名记录做物理删除。比赛数据是要归档留痕的,出了问题要看历史记录,物理删除会留下无法弥补的审计盲区。用逻辑删除字段标注,所有的查询都默认过滤掉 deleted=1 的数据,既保证界面看不到废数据,又保住了历史。
3.2 Vue 工程搭建与前端路由设计
前端用 Vite + Vue 3 的组合,包管理工具用 npm。Vue 环境配置这件事在热词里反复出现,其实要点就几个:Node.js 版本必须满足 Vite 的要求(我用的 Node 18 LTS),npm install 时如果遇到权限问题就试试npm install --registry=https://registry.npmmirror.com,装完以后跑npm run dev能起服务就算基础环境通了。
项目结构我这么拆:
src ├── api # 接口请求封装 │ ├── auth.js │ ├── event.js │ ├── registration.js │ └── result.js ├── assets ├── components # 公共组件 │ ├── StatusTag.vue │ └── PaginationTable.vue ├── router │ └── index.js ├── store │ └── user.js ├── views │ ├── login.vue │ ├── event │ ├── registration │ ├── schedule │ └── result └── utils └── request.js路由设计这里有个关键点:村超/民运会这种多角色系统,前端路由不应该全部静态写死。我在 router/index.js 里把每一个页面都登记了 meta 信息,包含 requireAuth、roles 两个字段,然后在全局前置守卫里检查。
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requireAuth && !token) { next({ path: '/login', query: { redirect: to.fullPath } }) return } const userRole = store.state.user.role if (to.meta.roles && !to.meta.roles.includes(userRole)) { next({ path: '/403' }) return } next() })Vue 路由传参在报名详情页跳转时最常用。我跳到一个报名单详情页面时用的是/registration/detail?id=123这种方式,然后this.$route.query.id拿参数。为什么不用 params?因为 params 传参在刷新页面后会丢失,用 query 才是可刷新、可收藏的链接。这在订单详情、报名详情这类需要分享给其他人查看的页面里非常重要。
3.3 登录鉴权与验证码实现
登录这块我没有引入 Spring Security,因为这个项目需要管理的用户角色简单,引入 Security 只会增加配置复杂度。我用拦截器 + JWT 的方式实现。用户输入账号密码后,后端校验通过返回一个 token,前端把 token 存在 localStorage,每次请求在 axios 拦截器里把 token 塞进请求头。后端拦截器从请求头取 token,解析成功才算认证通过。
验证码我用的 Redis 存。给一个 key(比如captcha:userId)设置 5 分钟过期,用户提交登录时把验证码一起提交,后端比对 Redis 里的值和用户输入值,相等才算通过。为什么不把验证码存 Session?因为这个后端接口可能会被手机端复用,Session 在跨端场景下管理维护麻烦,Redis 天然支持过期时间,而且多个后端实例部署时共享一份缓存,没有 Session 同步问题。
JWT 生成和解析我用的是 jjwt 库,代码不长:
public String createToken(User user) { return Jwts.builder() .setSubject(user.getId().toString()) .claim("username", user.getUsername()) .claim("role", user.getRole()) .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }这里 token 有效期设了 12 小时,因为比赛期间领队们基本上整天都在用系统,太短会频繁被踢下线,太长又有安全风险。如果要做更完善的控制,可以在 Redis 里存一份 token 的 jti,实现主动踢人下线,但当前项目没到那个复杂度,后续可以扩展。
登录接口的另一个安全细节是防暴力破解。我在 Redis 里记录同一个账号连续输错密码的次数,超过 5 次之后锁定 15 分钟。底层原理其实就是最简单的一个计数器 + 过期时间,代码量很少,但能挡住很多低级扫描。
3.4 队伍报名核心接口的代码实现
报名提交接口是整个系统业务逻辑最重的地方,我把它单独讲。前端领队选择赛事项目后,创建报名单,再往报名单里添加运动员。前端页面长这样:左边是项目树,右边是当前报名单的运动员列表。保存草稿时只把报名单主记录和明细记录插入数据库;提交审核时,后端先做完整性校验,再做资格校验,最后改状态为待审核。
核心方法大致是这样:
@Transactional(rollbackFor = Exception.class) public Long submitRegistration(RegistrationSubmitDTO dto) { Registration reg = registrationMapper.selectByIdForUpdate(dto.getRegistrationId()); if (reg == null || !reg.getStatus().equals(RegistrationStatus.DRAFT.getCode())) { throw new BizException("报名单不存在或状态不允许提交"); } List<RegistrationItem> items = registrationItemMapper.selectList( new LambdaQueryWrapper<RegistrationItem>() .eq(RegistrationItem::getRegistrationId, reg.getId())); if (CollectionUtils.isEmpty(items)) { throw new BizException("报名明细不能为空"); } // 校验项目人数限制 CompetitionItem item = itemMapper.selectById(reg.getItemId()); if (items.size() > item.getMaxAthletes()) { throw new BizException("报名人数超过项目上限"); } // 校验是否重复报名:同一赛事、同一队伍、同一运动员、同一项目 long repeatCount = registrationItemMapper.selectCount( new LambdaQueryWrapper<RegistrationItem>() .eq(RegistrationItem::getAthleteId, reg.getAthleteId()) .eq(RegistrationItem::getEventId, reg.getEventId()) .eq(RegistrationItem::getItemId, reg.getItemId()) .ne(RegistrationItem::getRegistrationId, reg.getId())); if (repeatCount > 0) { throw new BizException("该运动员在此项目中已存在报名记录"); } reg.setStatus(RegistrationStatus.PENDING.getCode()); registrationMapper.updateById(reg); return reg.getId(); }有些比赛的报名要交纸质材料,比如体检证明、免责声明。我在系统里提供了附件上传功能,用本地磁盘存储,上传成功后在数据库存文件路径。附录的体检表往往是组委会审核时重点看的内容,所以上传附件字段设计成了非必填,但如果项目类型是“体能竞赛类”,后端会自动校验附件列表不能为空。
3.5 赛程编排与成绩录入的联动实现
赛程编排这里,一般的做法是导入秩序册 Excel,然后系统根据项目、组别、日期生成比赛场次。我用的是“先有参赛名单,再按规则生成对阵”的思路。足球项目需要先分组抽签,抽签做在系统里,点击“自动分组”后,系统把报名队伍按地域和种子队伍分成若干小组。分组算法其实不复杂,就是先排种子队,再轮转填组。摔跤、押加这类个人项目则生成淘汰表,首轮轮空规则系统能自动算。
成绩录入端我单独做了一个适配手机和平板布局的页面,裁判只需要先选比赛项目,再选场次,然后录入运动员得分或者胜负关系。录入完成后,系统根据规则自动计算每个单位的总分,并实时刷新到成绩榜页面。这个功能是赛务系统里最容易出性能问题的地方,因为大屏成绩榜页面每隔几秒就要刷新一次。我的做法是成绩榜接口加了 Redis 缓存,成绩录入后主动删除对应缓存,而不是让前端盲目轮询数据库。
与之配套的还有检录表打印。每个比赛项目开赛前,检录员要打印带有二维码的检录表,运动员报到时扫码确认身份并签到。二维码内容存的是比赛场次 ID,后端提供查询接口返回该场次的运动员名单。这个环节能把“人到了没有”变成系统实时数据,对组委会统筹比赛进度帮助很大,是我这个项目里让用户印象最深的功能权之一。
4. 常见问题与排查技巧实录
4.1 Spring Boot 版本过高导致的匹配问题
做这个项目时我踩过一个很典型的坑。项目开始时选的 Spring Boot 3.2.2,Java 17,开发环境一切正常。等部署到服务器上,运维那台机器装的是 JDK 8,结果 java -jar 启动直接报UnsupportedClassVersionError。后来一查,Spring Boot 3.x 编译产物是 Java 17 字节码,JDK 8 根本跑不了。解决办法有两个,要么把服务器 JDK 升到 17,要么把项目降级到 Spring Boot 2.7 系列 + Java 8。最终我给运维重新装了 JDK 17,问题解决。
另一个和版本相关的问题是“springboot版本太高”带来的配置项变更。Spring Boot 2.7 里spring.redis.*的配置前缀,到 Spring Boot 3.x 变成了spring.data.redis.*;javax.annotation包里的注解在 Spring Boot 3.x 中变成了jakarta.annotation。如果你从旧项目复制一份配置过来,最容易忘记改这两个地方。排查方法很简单,看控制台启动日志,配置项无法识别时会打印 warning,别忽略它。
4.2 Vue 打包后布局异常与刷新 404
开发环境 npm run dev 一切正常,npm run build之后打开 dist/index.html 页面空白,或者刷新某个路由直接 404,这是 Vue 项目最常见的问题。原因我知道的人都懂,但第一次遇到还是会慌。
页面空白通常是因为资源路径写成了绝对路径。默认 Vite 构建的 base 是/,部署到服务器根目录没问题,但如果放到子目录比如http://ip:8080/web/,资源路径就会找错。解决方法是调整 vite.config.js 里的 base:
export default defineConfig({ base: './', plugins: [vue()], })刷新 404 是因为 Vue Router 默认用的是 history 模式,路径是真实的路由路径,而服务器上并没有对应的物理文件,刷新时服务器去查这个路径,发现不存在就返回 404。解决方案有两种:一是把路由改成 hash 模式,URL 里会多个#,不美观但没有配置成本;二是在 Nginx 里做 try_files 回退:
location / { try_files $uri $uri/ /index.html; }我项目最终用的是 Nginx 回退方案,因为 URL 干净,而且通过 Nginx 反向代理到后端接口也很方便。另外还有一类 Vite 打包后特性丢失的问题,比如按键回车登录失效,可能是默认公共组件打包时被 tree-shaking 掉了,这种情况要看具体代码,不能一概而论。
4.3 跨域问题与前端请求封装
前后端分离部署必然遇到跨域。开发阶段我用的 Vite 代理:
server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: path => path.replace(/^\/api/, '') } } }生产环境则通过 Nginx 统一反向代理,前端访问/api时由 Nginx 转发到后端服务,这样浏览器看到的还是同源请求。跨域问题能不能在服务端直接加@CrossOrigin解决?能,但生产环境不推荐,因为你把接口暴露给任何域名的前端都能调用,安全风险大。统一走 Nginx 反代是标准的做法。
前端请求封装我在 utils/request.js 里基于 axios 做了统一拦截器,请求时带上 token,响应时统一处理业务码。这里有个细节:如果后端返回 401,前端应该清除本地 token 并跳转登录页,同时要防止多个接口同时返回 401 导致重复跳转。我的做法是加一个标志变量,保证同一时间只跳转一次。
4.4 并发报名时人数超限的兜底方案
线上运营时遇到过一次真实的并发问题。某个热门项目报名开放时间是早上 9 点,结果 9 点整几十支队伍同时点提交,数据库的报名明细表瞬间出现同一运动员多条记录,后台上看到一个人报了三次同一个项目。原因就是前端校验通过后,后端校验重复和人数限制时没有加锁,两条请求同时查到人数未满,同时插入成功。
我是这么修的:在项目表上加一个 current_count 字段,提交报名时用乐观锁或者UPDATE ... SET current_count = current_count + 1 WHERE current_count < max_count这种原子操作去更新,影响行数为 0 就说明人数已满,直接返回报错。这个方案比纯靠 SELECT 判断再 INSERT 稳得多,实际测试 500 并发下没有超限记录。
还有一个跟并发相关的点,就是热词里说的“java线程等待都完成”。比如生成一个大型赛事的秩序册,要异步拉取几十个项目的数据再合成 PDF,我用了 CompletableFuture 并行查询,然后用allOf().join()等待全部完成。这里要注意,join 等待的任务如果某个子任务抛出异常,会导致整个流程卡住或异常,所以在子任务里要 catch 掉所有异常并记录日志,绝不能让它把主流程带崩。
5. 部署上线与维护扩展
5.1 打包部署与命令记录
后端打包依然是标准的mvn clean package -Dmaven.test.skip=true,打完的 jar 放在服务器上用 systemd 守护运行。我贴一个自己常用的 systemd 服务配置,避免进程被意外杀掉后无人重启:
[Unit] Description=Sport Register Server After=network.target [Service] Type=simple User=root WorkingDirectory=/opt/sport-register ExecStart=/usr/bin/java -Xms512m -Xmx1024m -jar sport-register.jar SuccessExitStatus=143 Restart=always RestartSec=10 [Install] WantedBy=multi-user.target前端打包后 dist 目录传到服务器,Nginx 的配置如下:
server { listen 80; server_name your-domain.com; root /opt/sport-register-web/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }注意 proxy_pass 最后那一段有没有斜杠,语义不一样。如果后端接口路径是/api/auth/login,而 proxy_pass 写的是http://127.0.0.1:8080,那么请求会被转发到http://127.0.0.1:8080/api/auth/login,后端 context-path 是/api时就正好匹配。如果写反了,后面会出现 404 或者接口路径多一层少一层的恶心问题。
5.2 数据备份与日志处理
赛事数据的重要性不用多说。MySQL 我每天凌晨 3 点跑一次全量备份,备份文件保留最近 14 天。shell 备份脚本的核心就一行命令:
mysqldump -uroot -p'password' sport_register | gzip > /backup/mysql/sport_register_$(date +%Y%m%d_%H%M%S).sql.gz日志方面,Spring Boot 默认输出到控制台,用 systemd 管理后 journald 会收集日志,用journalctl -u sport-register -f可以实时看。但如果日志量太大,建议在 application.yml 里配置 logback 滚动日志文件,按天切分,保留 15 天。赛务系统比赛当天日志量特别大,不做轮转会把磁盘撑满。
5.3 后续功能扩展的三个方向
系统上线后能扩展的地方其实非常多。第一是移动端适配,现在前端是响应式的,但很多领队习惯用手机操作,加一个小程序或者 H5 端体验会好很多。第二是赛事直播和视频回放,如果后续要做直播,前端要处理 m3u8 流的播放,通常会用到 video.js 加 hls.js 的方案,这不是必须但现在球迷呼声很高。第三是数据大屏,对接 LED 大屏实时展示各项目奖牌榜、积分榜,后台接口已经具备数据基础,只需要再写一个面向大屏适配的只读接口,前端用 ECharts 把数据动态渲染出来即可。
另外如果领导们要求生成各种统计报告,比如“某乡镇报名了多少人、某少数民族项目参与率如何”,就需要把报名表数据再做一层宽表聚合。我在数据库里预留了统计视图,比如v_event_athlete_stats,这里直接用 SQL 的 GROUP BY 生成统计结果,然后导出 Excel。热词里提到 java 相关基础八股,其实这类需求考的就是 SQL 和集合处理的基本功,真用起来会发现任何框架都替代不了这些基础能力。
6. 个人实操体会与建议
做完这个村超民运会赛务报名管理系统,我最大的体会是:技术选型反过来受用户的使用习惯影响很大。这些领队、裁判不少是乡镇干部或者学校体育老师,他们不会像互联网产品用户一样自己去摸索系统,需要系统把流程收窄到“下一步”、“返回”这样的一步步引导。所以在页面设计上,我让报名向导分四步走:选项目,添运动员,传附件,确认提交。每一步都有明确的错误提示,尽量不让用户思考“我该点什么”。
另一个让我印象很深的点是:尽量在系统里减少人工录入的字段,能下拉选的不让手填,能根据身份证自动带出的不让人再输一遍。现在身份证号一输,出生日期、性别、年龄都能解析出来,Hutool 的 IdcardUtil 一把梭,用户的录入成本降低了,错误率也降了。
最后再给打算做类似系统的同学一条建议:这类信息管理系统的开发难度不在编码,在需求拆解。动手写代码之前,多花两天时间找几个真实用户聊一遍流程,把边界场景都列出来,画清楚状态图,后面写出来的代码会少改很多轮。我这次项目里,光是“团体项目能不能替换队员”“报名截止后能不能补报”这两个问题,需求阶段反复确认了三遍,事实证明每一遍都是值得的。系统上线后,组委会最满意的一句话是“原来要干两周的报名统计,现在一天就出汇总表了”。对一个开发者来说,这一句话比任何技术上的赞美都有成就感。