云计算百科
云计算领域专业知识百科平台

SpringBoot+Redis+AI旁路,打造可复用的赛事系统毕设

做赛事类系统时,最容易陷入两种极端:一是只做后台 CRUD,页面像后台表格;二是堆概念——微服务、网关、真实支付——演示时全线崩盘。

「发令台 GUNSTART」(仓库名 marathon_system)走的是第三条路:业务闭环优先,外部能力可插拔,新技术旁路接入。它覆盖公开报名与组委会管理两条门户,串起报名、模拟支付、通知、取消退款、成绩排名,再把赛事助手拆成独立 AI 服务。这篇文章记录技术选型、关键设计和几处刻意取舍——适合当作毕设复盘,也适合当作同类管理系统的起步模板。

访问入口大致是:

角色

地址

说明

跑者公开平台

`http://localhost:8081/`

报名、我的报名、排名、赛事助手

组委会管理台

`http://localhost:8081/admin.html`

赛事/选手/成绩/导出

接口文档

`http://localhost:8081/doc.html`

Knife4j

AI 旁路

`http://localhost:8082`

Spring AI 赛事助手

一、我们要解决什么问题

马拉松报名不是「插一条 registration」那么简单。真实链路里至少有这些约束:

  • **名额与防重**:同人同赛不能重复报名,满额要挡掉。
  • **支付与状态机**:未支付、已支付、取消、退款窗口,状态要可解释。
  • **赛后资产**:成绩、分段、公开排名,读多写少,适合缓存。
  • **两端体验**:跑者要轻;组委会要批量与导出。
  • **演示可信度**:支付、短信往往接不到真实商户号,但不能假装「没这块」。
  • 因此系统目标被收敛成一句话:

    用单体后端托住完整业务叙事;支付/短信用可审计的 mock;AI 用旁路服务增强体验,但不绑架主工程版本。

    二、技术栈与总体架构

    版本与环境清单

    组件

    版本 / 约定

    说明

    主工程

    Java 8 · Spring Boot 2.7.14 · MyBatis 2.3.1

    端口 8081

    数据库

    MySQL 8 · 库名 `marathon_system`

    基线 `docs/数据库脚本/`,演进 `sql/`

    缓存

    Redis(Lettuce + commons-pool2)

    本地 `localhost:6379`

    鉴权

    Apache Shiro 1.11 + JJWT 0.11.5

    管理端无状态 JWT

    接口文档

    Knife4j 4.4(OpenAPI2 / Swagger2)

    `http://localhost:8081/doc.html`

    导出

    Apache POI 5.2.5

    报名/成绩 Excel

    静态前端

    原生 HTML/JS,无构建

    `static/index.html` + `admin.html`

    AI 服务

    Java 21 · Spring Boot 3.4.5 · Spring AI 1.0.9 · `qwen-plus`

    端口 8082,OpenAI 兼容

    主工程(赛事核心)

    Java 8 + Spring Boot 2.7.14,MyBatis 注解 SQL + MySQL 8;Shiro + JJWT 做管理端鉴权;Redis(Lettuce)管 OTP 与赛事/排名缓存;Knife4j 出接口文档;POI 出 Excel;前端两套静态页同 jar 托管,免 CORS 心智负担。

    图 1  主工程分层架构

    AI 旁路(赛事助手)

    Java 21 + Spring Boot 3.4 + Spring AI,通过 @Tool 调用主工程 /api/public/** 查赛事,结构化输出(赛事卡片 + 是否 grounded),前端直接渲染。

    图 2  AI 旁路数据流

    为什么拆开?

    主工程锁在 Boot 2.7 / Java 8 时,硬升 Spring AI 成本极高。旁路隔离让「核心业务稳定」和「AI 能力尝鲜」可以并行——这是我认为最值得写进简历的一刀。

    三、鉴权:管理端与跑者端故意不同

    管理端:Shiro 过滤器链 + JWT

    管理台走前后端分离:登录签发 JWT,请求头携带,Shiro 自定义 JwtFilter / JwtRealm 校验。过滤器链用 LinkedHashMap 保证顺序敏感(ShiroConfig 中真实配置的缩写版):

    filterChainDefinitionMap.put("/api/login", "anon");       // 登录接口本身
    filterChainDefinitionMap.put("/api/public/**", "anon");   // 选手端公开接口
    filterChainDefinitionMap.put("/doc.html", "anon");        // Knife4j 文档
    filterChainDefinitionMap.put("/webjars/**", "anon");      // Swagger 静态资源
    filterChainDefinitionMap.put("/uploads/**", "anon");      // 封面 <img> 不带 token
    filterChainDefinitionMap.put("/api/**", "jwt");            // 其余 API 必须 JWT
    filterChainDefinitionMap.put("/**", "anon");               // 静态页面放行

    好处是:过滤器链对「哪些匿名、哪些鉴权」一目了然;管理操作有明确身份边界;和 Knife4j、静态资源共存时规则好讲。

    当前实现仍是**明文比对管理员密码**(演示友好)。生产必须上 BCrypt/Argon2,并把 JWT secret 挪出仓库——这点「不足」章节也会写到。

    跑者端:手机号 OTP + Redis

    跑者不适合强行注册「账号体系」。公开端用短信验证码登录(本地模式写 sms_log,可配置 expose-code 方便答辩),会话用 X-Runner-Token 轻量凭证,绑定手机号后访问「我的报名」。核心逻辑(PublicController 真实片段):

    // 发码:Redis 存 5 分钟 TTL;local 通道落 sms_log
    redisService.set("public:otp:" + phone, code, 5, TimeUnit.MINUTES);
    smsService.send(phone, "otp_login", noticeContent, noticeTitle);
     
    // 登录:校验后签发 runnerToken(UUID),Redis 映射选手 ID,7 天有效
    redisService.set("public:runner:" + token, runner.getId(), 7, TimeUnit.DAYS);

    这是体验优先的取舍:降低跑者门槛,同时用 Redis TTL 管验证码过期。若上线,OTP 必须关回显、加频控与风控。

    四、业务闭环:从报名到排名

    一条可演示的主路径:

    组委会创建赛事(封面上传)
        → 跑者浏览报名中赛事
        → 提交报名(名额 / 防重校验)
        → 模拟收银台确认支付(写 payment_log)
        → 站内通知 / 确认函信息
        →(可选)取消与退款窗口
        → 赛后录入成绩
        → 公开排名(Redis 缓存)

    几个实现上的「小心机」:

    1. 名额防重:先查后插 + 事后回写(当前实现)

    报名核心校验在 RegistrationServiceImpl.submit():

    if (registered >= quota)
        throw new RuntimeException("赛事名额已满,报名失败");
    if (registrationMapper.selectByEventAndRunner(eventId, runnerId) != null)
        throw new RuntimeException("该选手已报名此赛事,请勿重复报名");
    registrationMapper.insert(registration);
    eventMapper.increaseRegisteredCount(eventId);   // registered_count + 1
    eventMapper.syncRegisteredCount(eventId);        // 用 COUNT(status=1) 回写兜底

    当前是「先查后插」:判断与自增不是原子操作,并发下存在名额漂移风险;syncRegisteredCount 的 COUNT 回写能在大部分场景兜底,但严格并发仍建议改造成条件更新(不足表第 4 行的落地版):

    — 把「判断 + 自增」合成一条原子语句,受影响行数为 0 即名额已满
    UPDATE marathon_event
    SET registered_count = registered_count + 1
    WHERE id = #{eventId}
      AND registered_count < quota
      AND status = 0;

    2. 支付与短信:mock,但留审计表

    配置里是 marathon.pay.gateway: mock、marathon.sms.provider: local。

    关键不是「假装接了微信」,而是:

    • 支付确认会改报名状态,并落 `payment_log`(含渠道、凭证、金额);
    • 短信验证码与通知落 `sms_log`;
    • 商户号、签名等配置位预留,真实 SDK 可替换。

    答辩时这样表述通常比「已经对接微信支付」更经得起追问。

    3. 封面上传:本地目录映射

    marathon.upload.path 指向 uploads/,WebConfig 把 /uploads/** 映射出去;Shiro 对该路径匿名,否则 <img src> 带不上 token。类型与大小在上传接口校验,和 multipart 限制对齐。

    现状核对:`uploads/` 目录与 `cover_image` 字段(`sql/add_cover_image.sql`)均已就绪,但示例素材与种子脚本尚未落地(见第七节待办)。

    4. Redis:不只是「会用缓存」

    赛事列表、公开排名读多写少,适合缓存;赛事变更时按模式失效(SCAN + 删除),避免只记得 set 忘了 del。OTP 同样吃 Redis TTL。这些点很适合面试追问「缓存一致性你怎么做的」。

    5. 导出与统计

    管理台需要「能交差」的运营能力:报名名单、成绩导出(POI),以及简单统计接口。对毕设来说,这比再堆一个微服务更有说服力。

    五、前端:两套门户,一个静态资源目录

    当前真正在用的是:

    • `src/main/resources/static/index.html` + `public-app.js`:公开品牌站「发令台」
    • `src/main/resources/static/admin.html` + `admin-app.js`:管理台

    同 jar 托管的好处是部署简单、联调零跨域。代价是前端工程化弱(无打包流水线、无组件化测试),属于明确的下一阶段优化点。

    docs/前端源文件/ 下的单文件旧稿是归档对照,不参与运行——版本管理的价值,往往在这种「两套前端」时才体现出来。

    六、AI 助手:让模型「接地气」

    大模型最怕一本正经地胡说。赛事助手的策略是:

  • **Tool Calling**:先调主工程公开 API 拿真实赛事与名额;
  • **结构化输出**:返回 `answer` + `events[]` + `grounded`;
  • **前端可降级**:AI 服务挂了,公开报名主流程仍可用。
  • @Tool 的定义(ai-service 中 EventQueryTools 真实片段):

    @Tool(description = "查询正在报名中的马拉松赛事列表。当用户问「有什么比赛」「还能报名吗」时使用。")
    public String listOpenEvents() {
        return toJson(marathonEventClient.listOpenEvents());  // 走主工程 /api/public/**
    }
     
    @Tool(description = "按赛事 ID 查询单场赛事详情(报名费、剩余名额、地点、日期、规则等)。")
    public String getEventById(@ToolParam(description = "赛事数字 ID") Long eventId) {
        EventDetailView detail = marathonEventClient.getEventDetail(eventId);
        return detail == null ? "{\\"error\\":\\"赛事不存在\\"}" : toJson(detail);
    }

    API Key 放在 ai-service/application-local.yml(gitignore),支持 DeepSeek / 通义等 OpenAI 兼容端点,换模型只改 base-url 与 model。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » SpringBoot+Redis+AI旁路,打造可复用的赛事系统毕设
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!