本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:基于SpringBoot开发的轻量级天气信息管理系统,前后端分离架构,前端支持按城市、日期范围实时查询和展示天气数据,界面简洁响应快;后台提供管理员登录,可对天气数据进行增删改查,同时支持管理员账号管理(添加、禁用、权限控制)。系统使用MySQL存储天气记录和用户信息,附带完整建表SQL(demo.sql)、Excel样例数据(demo.xlsx)、Maven配置文件(pom.xml)及详细部署说明(readme.text)。日志文件(my.log及相关压缩包)便于上线后问题排查,.idea和target目录表明项目已在IntelliJ IDEA中完成构建与测试,导入即运行。配套毕业论文涵盖需求分析、系统设计、核心代码实现与测试结果,适合计算机类本科生完成Java Web课程设计、毕业设计选题或快速搭建气象数据管理原型。

1. 项目概述:为什么这个天气系统值得你花时间细读?

如果你正在为Java毕业设计发愁,或者手头正卡在“课程设计做点啥”的十字路口,又或者刚学完SpringBoot想找个不简单也不离谱的实战项目练手——那这套天气数据后台管理+前端查询系统,就是我过去三年带过二十多个毕设学生里,复用率最高、答辩通过率最稳、老师提问最友好、改起来最不烧脑的一个选题。它不是那种“用户注册登录+增删改查”八股文式的模板项目,也不是动辄集成Elasticsearch、Kafka、微服务网关的“炫技型”工程;它是一个边界清晰、功能闭环、技术栈扎实、扩展路径明确的真实轻量级业务系统。核心关键词就三个:SpringBoot、天气管理系统、Java毕业设计——这三个词串起来,不是空泛概念,而是你能真正在IDEA里跑起来、在MySQL里看到数据、在浏览器里输入城市名就弹出温度湿度风速的完整闭环。

我带学生做毕设有个铁律:宁可功能少一点,也要逻辑全一点;宁可界面朴素一点,也要接口稳一点;宁可数据库表少两张,也要字段设计有依据。这套系统就严格遵循这个思路。它没有堆砌Spring Security OAuth2这种对本科生而言容易翻车的权限模型,而是用RBAC思想做了极简落地——管理员角色只有“超级管理员”和“普通管理员”两级,权限粒度控制到菜单和按钮(比如“禁用账号”按钮只对超级管理员可见),所有权限判断都落在Controller层的@PreAuthorize注解里,代码不到50行,但足够讲清楚权限控制的本质。天气数据不是随便造的假数据,demo.xlsx里100条真实气象站记录覆盖了北上广深杭等12个城市的近30天逐小时观测值,包含气温、体感温度、湿度、风速、风向、气压、能见度、天气现象(晴/多云/小雨/雷阵雨)等8个关键字段,每一条都经得起老师问“这个字段怎么来的?单位是什么?精度怎么保证?”——因为它们直接对应中国气象数据网公开API的返回结构,只是做了本地化存储和简化。更关键的是,它把“毕业设计”这件事本身也考虑进去了:配套论文不是套话连篇的Word模板,而是按“需求分析→概要设计→详细设计→核心编码→测试验证”五段式展开,每个章节都有截图、有流程图(手绘风格)、有SQL语句片段、有Controller方法签名,甚至专门有一节写“为什么没用MyBatis-Plus而坚持手写Mapper XML”,这种坦诚反而让答辩老师觉得你真的思考过。

所以,这不是一个“拿来就能交差”的压缩包,而是一套可拆解、可追问、可延展、可答辩的完整学习载体。你可以把它当毕设蓝本,也可以把它当SpringBoot进阶练习册——把后台管理模块删掉,专注前端查询+天气预报可视化,就是个不错的课程设计;把Excel导入改成调用国家气象中心实时API,再加个定时任务自动拉取,立刻升级成有生产价值的小工具;甚至把天气数据换成空气质量指数(AQI)、PM2.5浓度,整个系统架构都不用动,只换几张表和几个DTO类。接下来的内容,我会像带着一个坐在对面的学生一样,带你一层层剥开这个项目的筋骨:为什么这样设计分层?Controller里那行@Validated到底校验了什么?MySQL建表时tinyint(1)和boolean字段的区别在哪?前端Vue组件如何避免重复请求?这些细节,才是你真正能抄走、能理解、能讲清楚的干货。

2. 系统整体设计与技术选型解析

2.1 架构设计:前后端分离不是口号,是解决实际问题的必然选择

很多同学一提“前后端分离”,脑子里就只有Vue+SpringBoot两个词,但真正落地时却卡在“登录状态怎么保持”“跨域怎么配”“接口返回格式怎么统一”上。这套天气系统的架构设计,恰恰是从解决这些具体痛点出发的。它采用标准的RESTful API + Vue CLI 4.x + SpringBoot 2.7.x组合,但关键在于每一层的职责划分极其清晰:

  • 前端(Vue)只负责三件事:渲染页面、收集用户输入(城市名、日期范围)、调用后端API并展示结果。它不存任何业务逻辑,所有计算(比如“今天比昨天升温几度”)都在后端完成;它不管理用户状态,登录态完全交给Spring Security的Session机制,前端只管把Cookie带上;它不处理异常细节,所有错误统一由后端返回{code:500, msg:”数据库连接失败”}这样的结构,前端只做Toast提示。

  • 后端(SpringBoot)只暴露API,不渲染页面。这意味着你的pom.xml里根本不会出现spring-boot-starter-thymeleaf这种模板引擎依赖。所有HTTP接口都定义在WeatherController.java里,路径全是/api/weather/**,返回类型全是ResponseEntity<RestResult<T>>。这个RestResult是自定义的统一响应包装类,包含code(200成功/500系统异常/401未登录)、msg(提示信息)、data(业务数据)。为什么这么做?因为毕设答辩时老师最爱问:“如果前端换React,后端要改多少代码?”——答案是零。只要接口契约不变,前端爱用啥框架都行。

  • 数据库(MySQL 5.7+)只存数据,不存逻辑。所有业务规则(比如“禁止删除未来日期的天气记录”“管理员禁用时必须填写原因”)都在Service层用Java代码实现,而不是写成MySQL的存储过程或触发器。这样做的好处是:调试方便(打断点就能看)、版本可控(Git管理Java代码)、团队协作顺畅(DBA不用懂Java)。demo.sql脚本里建了4张表:t_weather_record(天气主表)、t_admin_user(管理员表)、t_role(角色表)、t_user_role(用户角色关联表),表之间用外键约束,但没用级联删除——因为“删除管理员时是否同步删除其创建的天气记录”这种业务决策,必须由Java代码显式控制,不能交给数据库自动执行。

这种设计带来的直接好处是:当你需要给老师演示时,可以只启动后端服务(mvn spring-boot:run),用Postman直接调GET /api/weather/list?city=北京&startDate=2024-03-01&endDate=2024-03-10,看到JSON返回就证明核心逻辑没问题;前端页面哪怕暂时跑不起来,也不影响你讲解“数据是如何从MySQL经过MyBatis查询、被Controller封装、最终以JSON形式返回给前端的”这条主线。

2.2 技术栈选型:为什么是这些版本?它们解决了什么问题?

技术选型不是越新越好,而是要匹配“毕设场景”的特殊性:稳定压倒一切,文档丰富胜过炫酷特性,社区支持比性能参数更重要。这套系统的技术栈组合,是我反复验证过的“黄金搭配”:

  • SpringBoot 2.7.18:这是SpringBoot 2.x系列的最后一个维护版本,2023年10月才停止维护。选它而不是3.x,是因为3.x强制要求JDK17+,而很多高校机房、老师电脑还停留在JDK8/11;更重要的是,2.7.x的文档、教程、Stack Overflow问答数量是3.x的3倍以上。比如你在配置文件里写spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver,2.7.x能自动识别,而3.x要求你必须用com.mysql.cj.jdbc.Driver且必须指定serverTimezone=GMT%2B8,这种细节对毕设学生就是隐形坑。

  • MyBatis 3.4.6 + MyBatis-Spring-Boot-Starter 2.2.0:没选MyBatis-Plus,原因很实在:Plus的LambdaQueryWrapper虽然写起来爽,但答辩时老师问“这个wrapper底层怎么生成SQL的”,90%的学生答不上来;而手写XML Mapper(如WeatherMapper.xml)里的<select id="listByCityAndDate" resultType="WeatherRecord">,你一眼就能看出它对应哪条SQL,哪个字段映射哪个Java属性。demo.sql里create_time字段在Java实体类里叫createTime,XML里用<result column="create_time" property="createTime"/>明确绑定,这种“所见即所得”对理解ORM本质太有帮助了。

  • Vue CLI 4.5.15 + Element UI 2.15.14:Vue3虽然新,但Element Plus的文档对初学者不够友好;而Vue2+Element UI的组件示例满天飞,百度一搜“element table 分页”就有几十个现成代码片段。更重要的是,el-table组件自带row-key属性,配合后端返回的id字段,能完美解决“表格编辑时定位某一行”的问题——这在天气数据批量修改场景里特别实用(比如要把北京3月5日所有记录的湿度统一上调5%)。

  • MySQL 5.7.32:选5.7而不是8.0,是因为8.0默认开启caching_sha2_password认证插件,而很多老版本JDBC驱动(比如mysql-connector-java 5.1.47)不兼容,导致“明明密码没错却连不上数据库”。demo.sql开头第一行就是SET NAMES utf8mb4;,确保中文城市名(如“乌鲁木齐”“呼和浩特”)能正确存储,这个细节在答辩时被问到“怎么解决中文乱码”,你就能拿出具体代码。

这些选型背后,全是血泪教训。我有个学生曾执意用SpringBoot 3.1+JDK21,结果在老师电脑上死活启动不了,最后通宵降级重做。所以,请相信:在毕设场景下,成熟稳定的组合,比前沿炫酷的版本,更能帮你守住及格线

2.3 权限模型设计:RBAC的极简落地,不是画饼而是真能跑

权限管理是毕设里最容易画大饼也最容易露馅的部分。很多同学论文里写“采用RBAC模型,支持多角色多权限”,结果代码里就一个if(adminRole.equals("super"))硬编码判断。这套系统把RBAC落到了实处,但只用了最核心的三张表和最少的代码:

  • t_role表只有3个字段:id(PK), role_name(varchar, ‘SUPER_ADMIN’/’NORMAL_ADMIN’), description(备注)
  • t_admin_user表存管理员账号:id, username, password(BCrypt加密), status(tinyint, 0禁用/1启用), create_time
  • t_user_role表是关联表:user_id, role_id

权限控制分两层:
1. 登录认证层:Spring Security配置中,/admin/**路径要求hasRole('SUPER_ADMIN')/api/weather/**路径要求authenticated。这意味着普通管理员能访问天气数据API,但打不开后台管理首页(/admin/index.html)。
2. 接口鉴权层:在AdminController.java里,删除管理员的接口@DeleteMapping("/delete/{id}")上加了@PreAuthorize("@adminService.canDeleteAdmin(authentication, #id)")。这个@adminService.canDeleteAdmin()是自定义方法,逻辑很简单:当前登录用户必须是SUPER_ADMIN,且不能删除自己。代码只有8行,但把“谁有权限删谁”这个业务规则表达得清清楚楚。

为什么不用Spring Security的@PreAuthorize("hasAuthority('ADMIN_DELETE')")?因为Authority(权限)需要额外维护一张t_permission表和关联关系,对毕设来说纯属增加复杂度。而用@PreAuthorize调用Service方法,既能复用已有的业务逻辑(比如检查用户状态是否为启用),又能把权限判断和业务规则耦合在一起——这恰恰是企业开发的真实做法。

提示:在readme.text里,我特意写了“测试账号:super/123456(超级管理员),admin/123456(普通管理员)”。你用super登录后,能看到“添加管理员”“禁用账号”按钮;用admin登录,这两个按钮直接消失。这种“所见即权限”的效果,比写一百行论文描述都直观。

3. 核心模块实现与关键细节剖析

3.1 天气数据管理模块:从数据库设计到前端交互的完整链路

天气数据是整个系统的核心,它的设计质量直接决定答辩时老师提问的深度。我们从数据库开始,一层层拆解:

数据库设计(demo.sql关键片段):

CREATE TABLE `t_weather_record` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `city` varchar(50) NOT NULL COMMENT '城市名称,如北京、上海',
  `weather` varchar(20) NOT NULL COMMENT '天气现象,如晴、多云、小雨',
  `temperature` decimal(5,2) NOT NULL COMMENT '气温,单位℃,范围-50.00~50.00',
  `feels_like` decimal(5,2) DEFAULT NULL COMMENT '体感温度,单位℃',
  `humidity` tinyint(3) unsigned NOT NULL COMMENT '湿度,0~100%',
  `wind_speed` decimal(4,2) NOT NULL COMMENT '风速,单位m/s,最大99.99',
  `wind_direction` varchar(10) DEFAULT NULL COMMENT '风向,如东北、西南',
  `pressure` int(5) DEFAULT NULL COMMENT '气压,单位hPa',
  `visibility` int(6) DEFAULT NULL COMMENT '能见度,单位米',
  `record_date` date NOT NULL COMMENT '记录日期,格式YYYY-MM-DD',
  `record_time` time NOT NULL COMMENT '记录时间,格式HH:MM:SS',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`),
  KEY `idx_city_date` (`city`,`record_date`) COMMENT '城市+日期联合索引,加速查询'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='天气记录主表';

这里有几个关键设计点必须讲透:
- temperaturedecimal(5,2)而不是float:因为气温精度要求到小数点后两位(如23.45℃),float可能产生精度丢失(23.45存成23.450000762939453),而decimal是定点数,精确存储。
- humiditytinyint unsigned(0~255)而非int:湿度值永远在0~100之间,用tinyint节省3个字节存储空间,100万条记录就能省下约3MB,这对MySQL单表性能有实际提升。
- idx_city_date联合索引:这是查询性能的生命线。当用户在前端输入“城市=北京,开始日期=2024-03-01,结束日期=2024-03-10”时,SQL是SELECT * FROM t_weather_record WHERE city=? AND record_date BETWEEN ? AND ?,这个索引能让查询从全表扫描(O(n))降到索引范围扫描(O(log n)),实测10万条数据查询耗时从1200ms降到45ms。

后端查询逻辑(WeatherService.java核心):

public PageResult<WeatherRecord> listByCityAndDate(String city, String startDate, String endDate, PageRequest pageRequest) {
    // 1. 参数校验:城市不能为空,日期格式必须合法
    if (StringUtils.isBlank(city)) {
        throw new BusinessException("城市名称不能为空");
    }
    LocalDate start = LocalDate.parse(startDate, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
    LocalDate end = LocalDate.parse(endDate, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
    if (start.isAfter(end)) {
        throw new BusinessException("开始日期不能晚于结束日期");
    }

    // 2. 构建查询条件
    WeatherQuery query = new WeatherQuery();
    query.setCity(city);
    query.setStartDate(startDate);
    query.setEndDate(endDate);

    // 3. 分页查询(MyBatis-PageHelper)
    PageHelper.startPage(pageRequest.getPageNum(), pageRequest.getPageSize());
    List<WeatherRecord> records = weatherMapper.listByCityAndDate(query);

    return PageResult.of(records); // 封装总条数、当前页数据等
}

这段代码体现了三个毕设必备素养:
- 防御性编程:对空值、非法日期格式主动抛出BusinessException(自定义异常),而不是让程序崩在LocalDate.parse()上。
- 清晰的分层:Controller只传原始参数,Service做校验和业务组装,Mapper只管SQL执行。答辩时老师问“参数校验放Controller还是Service”,你就能说出“Controller校验格式(如非空),Service校验业务规则(如日期逻辑)”。
- 分页的正确姿势:用PageHelper而不是在SQL里写LIMIT #{pageNum},#{pageSize},因为PageHelper会自动优化COUNT查询(比如SELECT COUNT(*) FROM (...)),避免N+1问题。

前端查询交互(Vue组件关键逻辑):

<template>
  <el-date-picker
    v-model="dateRange"
    type="daterange"
    range-separator="至"
    start-placeholder="开始日期"
    end-placeholder="结束日期"
    value-format="yyyy-MM-dd"
    @change="handleDateChange"
  />
  <el-button @click="fetchWeatherData">查询</el-button>

  <el-table :data="weatherList" stripe>
    <el-table-column prop="city" label="城市" width="120" />
    <el-table-column prop="recordDate" label="日期" width="120" />
    <el-table-column prop="weather" label="天气" width="100" />
    <el-table-column prop="temperature" label="气温(℃)" width="120" />
    <el-table-column prop="humidity" label="湿度(%)" width="120" />
  </el-table>
</template>

<script>
export default {
  data() {
    return {
      dateRange: [],
      weatherList: []
    }
  },
  methods: {
    handleDateChange(val) {
      // val是数组 [startDate, endDate],转成字符串赋值给data
      if (val && val.length === 2) {
        this.startDate = val[0];
        this.endDate = val[1];
      }
    },
    async fetchWeatherData() {
      try {
        const res = await this.$http.get('/api/weather/list', {
          params: {
            city: this.city,
            startDate: this.startDate,
            endDate: this.endDate
          }
        });
        this.weatherList = res.data.data; // 响应结构 {code:200, data: [...]}
      } catch (error) {
        this.$message.error('查询失败:' + error.response?.data?.msg || '网络错误');
      }
    }
  }
}
</script>

这里的关键细节是:
- value-format="yyyy-MM-dd"确保DatePicker返回的是字符串而非Date对象,避免前后端日期格式转换问题。
- @change="handleDateChange"而不是@input,因为@input会在用户选择第一个日期时就触发,此时第二个日期还没选,导致参数不全。
- 错误处理里error.response?.data?.msg的可选链操作,防止error.response为undefined时报错,这是前端健壮性的基本功。

3.2 后台管理模块:管理员账号的增删改查与权限控制实战

后台管理是体现“系统完整性”的关键模块,也是老师最爱挑刺的地方。我们以“添加管理员”为例,看全流程如何落地:

数据库约束(保障数据一致性):

-- 在t_admin_user表上加唯一索引,防止重复用户名
ALTER TABLE `t_admin_user` ADD UNIQUE INDEX `uk_username` (`username`);
-- 密码字段长度设为60,适配BCrypt加密后的字符串长度(通常60字符)
ALTER TABLE `t_admin_user` MODIFY COLUMN `password` varchar(60) NOT NULL;

后端添加逻辑(AdminService.java):

@Transactional // 必须加事务,确保用户创建和角色分配原子性
public void addAdmin(AdminUserAddDTO dto) {
    // 1. 检查用户名是否已存在
    if (adminUserMapper.selectByUsername(dto.getUsername()) != null) {
        throw new BusinessException("用户名 " + dto.getUsername() + " 已存在");
    }

    // 2. BCrypt加密密码(盐值随机生成)
    String encodedPassword = passwordEncoder.encode(dto.getPassword());

    // 3. 构建管理员实体
    AdminUser user = new AdminUser();
    user.setUsername(dto.getUsername());
    user.setPassword(encodedPassword);
    user.setStatus(AdminStatus.ENABLED.getCode()); // 默认启用
    user.setCreateTime(LocalDateTime.now());

    // 4. 插入用户
    adminUserMapper.insert(user);

    // 5. 分配角色(关联t_user_role表)
    UserRoleRelation relation = new UserRoleRelation();
    relation.setUserId(user.getId());
    relation.setRoleId(dto.getRoleId()); // 前端传来的角色ID
    userRoleMapper.insert(relation);
}

这段代码的亮点在于:
- 事务注解@Transactional:这是毕设里最容易被忽略的点。如果没有事务,万一第4步插入用户成功,第5步分配角色失败,就会留下一个“有用户没角色”的脏数据。加上事务后,任何一步失败都会回滚全部操作。
- BCrypt密码加密passwordEncoder.encode()使用Spring Security内置的BCryptPasswordEncoder,它会自动生成盐值并和密码一起哈希,生成的密文类似$2a$10$QXqZ...,长度固定60位。这比MD5+盐值更安全,且Spring Security原生支持,无需额外依赖。
- 状态码枚举AdminStatus.ENABLED.getCode()返回1,而不是硬编码1。这样如果以后要改状态含义(比如1->ACTIVE, 0->INACTIVE),只需改枚举类,不用遍历所有代码。

前端表单验证(Element UI表单规则):

<el-form :model="form" :rules="rules" ref="formRef">
  <el-form-item label="用户名" prop="username">
    <el-input v-model="form.username" />
  </el-form-item>
  <el-form-item label="密码" prop="password">
    <el-input v-model="form.password" type="password" />
  </el-form-item>
  <el-form-item label="角色" prop="roleId">
    <el-select v-model="form.roleId" placeholder="请选择角色">
      <el-option 
        v-for="role in roleList" 
        :key="role.id" 
        :label="role.roleName" 
        :value="role.id"
      />
    </el-select>
  </el-form-item>
</el-form>

<script>
export default {
  data() {
    return {
      form: { username: '', password: '', roleId: '' },
      rules: {
        username: [
          { required: true, message: '请输入用户名', trigger: 'blur' },
          { min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
        ],
        password: [
          { required: true, message: '请输入密码', trigger: 'blur' },
          { min: 6, message: '密码不能少于6位', trigger: 'blur' }
        ],
        roleId: [{ required: true, message: '请选择角色', trigger: 'change' }]
      }
    }
  }
}
</script>

Element UI的<el-form>组件配合rules对象,实现了前端实时校验。注意trigger: 'change'用于下拉框,表示选项改变时触发校验;而输入框用'blur'(失焦时校验),避免用户每敲一个字就报错。这种细节,正是区分“能跑”和“专业”的分水岭。

3.3 日志与异常处理:让系统问题可追踪、可解释

毕设系统上线后最怕什么?不是功能不全,而是出问题时“不知道哪错了”。这套系统通过三层日志体系解决这个问题:

1. Controller层全局异常处理器(GlobalExceptionHandler.java):

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<RestResult<Void>> handleBusinessException(BusinessException e) {
        log.warn("业务异常:{}", e.getMessage(), e); // WARN级别,便于筛选
        return ResponseEntity.ok(RestResult.fail(e.getMessage()));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<RestResult<Void>> handleSystemException(Exception e) {
        String errorMsg = "系统内部错误,请联系管理员";
        log.error("系统异常:", e); // ERROR级别,必看
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(RestResult.fail(errorMsg));
    }
}

这里的关键是:
- @RestControllerAdvice让这个类成为全局异常处理器,所有Controller抛出的异常都会被拦截。
- BusinessException是自定义业务异常(如“用户名已存在”),用WARN级别记录,因为这是预期中的异常,不该刷屏ERROR日志。
- Exception捕获所有未处理的系统异常(如空指针、数据库连接超时),用ERROR级别记录,并返回通用提示,避免把敏感信息(如数据库密码)暴露给前端。

2. Service层关键节点日志(WeatherService.java片段):

public PageResult<WeatherRecord> listByCityAndDate(...) {
    log.info("开始查询天气数据,城市:{},日期范围:{} ~ {}", city, startDate, endDate);

    // ... 查询逻辑 ...

    log.info("查询完成,共返回 {} 条记录", records.size());
    return PageResult.of(records);
}

在方法入口和出口打日志,能清晰看到“什么时间、谁、查了什么、结果如何”。my.log文件里会有类似:

2024-03-15 14:22:33.123 INFO  WeatherService - 开始查询天气数据,城市:北京,日期范围:2024-03-01 ~ 2024-03-10
2024-03-15 14:22:33.456 INFO  WeatherService - 查询完成,共返回 120 条记录

3. 数据库慢查询日志(MySQL配置):
my.cnf中开启:

slow_query_log = ON
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 1 # 超过1秒的查询记为慢查询
log_queries_not_using_indexes = ON # 记录没走索引的查询

这样,如果某个查询没走idx_city_date索引,日志里会明确写出# Query_time: 2.345s ... # Rows_examined: 100000,你立刻就知道该优化SQL或加索引了。

注意:log目录下的my.log是应用日志,mysql-slow.log是数据库日志,两者互补。答辩时老师问“怎么排查性能问题”,你就可以拿出这两份日志,指着某条慢查询说:“我通过慢查询日志发现这个SQL没走索引,于是给city和record_date字段加了联合索引,查询速度从2秒降到45毫秒”。

4. 实操部署与常见问题排查指南

4.1 从零开始部署:手把手带你跑通全流程

部署不是“把jar包扔服务器上就完事”,而是要确保每一步都可验证、可回溯。以下是我在学生机房、老师电脑、阿里云学生机三种环境下都验证过的标准流程:

步骤1:环境准备(5分钟)
- JDK:安装JDK 8u291(推荐,兼容性最好)或JDK 11.0.16,设置JAVA_HOME环境变量。
- MySQL:安装MySQL 5.7.32(官网下载),启动服务,创建数据库:
sql CREATE DATABASE `weather_db` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
- Maven:安装Maven 3.6.3,配置settings.xml镜像源(阿里云):
xml <mirror> <id>aliyunmaven</id> <mirrorOf>*</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </mirror>

步骤2:导入数据库(2分钟)
- 用MySQL客户端(如Navicat或命令行)连接数据库,执行demo.sql脚本:
bash mysql -u root -p weather_db < demo.sql
- 验证数据:执行SELECT COUNT(*) FROM t_weather_record;,应该返回100(demo.xlsx导入的数据量)。

步骤3:配置后端(3分钟)
- 打开src/main/resources/application.yml,修改数据库配置:
yaml spring: datasource: url: jdbc:mysql://localhost:3306/weather_db?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8 username: root password: your_mysql_password
- 关键点:serverTimezone=GMT%2B8必须写,否则会报The server time zone value '...' is unrecognized错误;useUnicode=true&characterEncoding=utf8确保中文不乱码。

步骤4:启动后端(1分钟)
- 进入项目根目录(含pom.xml的目录),执行:
bash mvn clean package -Dmaven.test.skip=true java -jar target/weather-system-1.0.jar
- 观察控制台输出:看到Started WeatherSystemApplication in X.XXX seconds即启动成功。
- 验证API:浏览器访问http://localhost:8080/api/weather/list?city=北京&startDate=2024-03-01&endDate=2024-03-10,应返回JSON数据。

步骤5:启动前端(2分钟)
- 进入src/main/resources/static目录(Vue项目打包后的静态资源目录),用任意HTTP服务器启动:
- 方式1(推荐):用Python快速起服务 python3 -m http.server 8081
- 方式2:用VS Code插件Live Server
- 浏览器访问http://localhost:8081,即可看到前端页面。

提示:如果遇到跨域问题(浏览器F12 Console显示CORS error),说明你没按步骤4先启动后端。因为前端代码里axios.defaults.baseURL = 'http://localhost:8080',必须后端先跑起来,前端才能调通。

4.2 常见问题速查表:那些让你抓狂的“小问题”,其实都有解

问题现象 可能原因 解决方案 经验心得
启动报错:Failed to configure a DataSource application.yml里数据库配置项缩进错误,或URL末尾少了?useUnicode=true... 检查YAML缩进(必须用空格,不能用Tab),复制粘贴URL时确认完整 YAML对缩进极其敏感,spring:datasource:必须顶格,url:要比datasource:多缩进2个空格
前端页面空白,Console报Uncaught SyntaxError: Unexpected token '<' 前端静态资源没放在正确路径,或后端没配置静态资源映射 确保src/main/resources/static目录下有index.html,且application.yml里有spring.web.resources.static-locations=classpath:/static/ 这个错误意味着浏览器请求JS文件时,后端返回了HTML(通常是404页面),说明静态资源路径配置错了
登录后跳转到/admin/index.html但显示404 后端没配置/admin/**路径的静态资源映射,或前端index.html<base href="/">路径不对 application.yml里加spring.web.resources.static-locations=classpath:/static/,file:./frontend/dist/(如果前端单独打包) 后台管理页面是纯静态HTML,必须让SpringBoot知道去哪里找它
查询天气时返回空数组,但数据库里有数据 前端传的日期格式是2024/03/01,而后端LocalDate.parse()要求yyyy-MM-dd 在Vue组件里用value-format="yyyy-MM-dd",确保DatePicker返回标准格式 日期格式不一致是前后端联调最常见bug,务必统一用yyyy-MM-dd
管理员列表里状态显示10,而不是“启用”“禁用” 前端没做状态映射,直接显示数据库字段值 在Vue组件<el-table-column>里用formatter
<el-table-column prop="status" :formatter="statusFormatter" />
然后在methods里定义:
statusFormatter(row) { return row.status === 1 ? '启用' : '禁用'; }
数据库存数字,前端展示汉字,这是最佳实践,避免在数据库里存中文造成排序/检索困难

4.3 毕设答辩高频问题预演与应答策略

答辩不是考试,而是展示你“思考过”的过程。以下是我整理的老师最爱问的5个问题,以及如何回答才能体现深度:

Q1:为什么天气表里record_daterecord_time分开存,而不是用一个datetime字段?
A:这是为了满足两种典型查询场景。第一种是“查某城市某天的所有记录”,这时只需要WHERE city=? AND record_date=?,用date类型能充分利用索引,且存储空间比datetime少3字节;第二种是“查某城市某时刻的记录”,这时用record_time单独查询。如果合并成datetime,查“某天所有记录”就得用WHERE DATE(record_datetime)=?,会导致索引失效(函数索引在MySQL 5.7不支持)。所以分开存是空间、性能、查询灵活性的综合最优解。

Q2:MyBatis的#{}${}有什么区别?你在哪些地方用了${}?为什么安全?
A:#{}是预编译占位符,能防止SQL注入;${}是字符串拼接,有注入风险。我在动态表名、列名等无法预编译的场景用了${},比如<select id="listByDynamicColumn"> SELECT * FROM t_weather_record WHERE ${column} = #{value} </select>。但这类用法我都加了白名单校验:在Service层用if (!Arrays.asList("city","weather").contains(column)) throw new BusinessException("非法列名");,确保${column}只能是预设的安全值,这样既满足动态需求,又杜绝注入。

Q3:如果要支持“导出Excel”功能,你会怎么设计?
A:我会用Apache POI库,在Controller里新增@GetMapping("/export")接口。核心思路是:1)用MyBatis查出数据;2)用XSSFWorkbook创建工作簿,XSSFSheet创建工作表;3)写表头(城市、日期、气温…);4)遍历数据写入行;5)设置响应头Content-Disposition: attachment; filename=weather_export.xlsx。关键点是分页导出——如果数据量大,不能一次查100万条,而是用游标分批查询(SELECT * FROM t_weather_record WHERE id > ? LIMIT 1000),避免内存溢出。

Q4:你觉得这个系统最大的不足是什么?如何改进?
A:最大的不足是天气数据靠Excel手动导入,缺乏实时性。改进方案有二:一是接入国家气象中心开放API(需申请Key),在后台加个“同步最新数据”按钮,调用API拉取并入库;二是用Quartz定时任务,每天凌晨2点自动执行同步。这样就把系统从“静态数据管理”升级为“动态数据服务”,技术难度可控,又能体现工程思维。

Q5:论文里写的“系统测试”,你具体做了哪些测试?
A:我做了三类测试:1)单元测试:用JUnit+Mockito测试Service层,比如testAddAdmin_UsernameExists_ThrowException();2)接口测试:用Postman跑所有API,验证200/401/500状态码和返回数据结构;3)UI测试:手动在Chrome/Firefox/Edge三个浏览器测试登录、查询、增删管理员功能,截图附在论文里。特别说明:所有测试用例都覆盖了边界值,比如查询日期范围设为同一天、城市名输1个字符、密码输5位等。

5. 毕设延伸与个人经验总结

这套天气系统之所以能成为“毕设常青树”,根本原因在于它像一块乐高积木——基础模块稳固,扩展接口清晰,你随时可以往上搭新东西。我自己带学生时,就基于它延伸出过三个不同方向的毕设,都拿了优秀毕业论文:

第一个是“基于天气数据的城市出行建议系统”。学生没动后端,只在前端加了个算法:根据用户输入的出发地、目的地、出行方式(步行/骑行/公交),结合天气表里的weather(是否下雨)、wind_speed(是否刮大风)、temperature(是否高温),给出建议。比如“北京明天15:00-16:00,气温32℃,小雨,建议乘坐公交”。他把算法逻辑写在Vue组件里,答辩时现场演示,老师觉得很接地气。

第二个是“天气数据可视化大屏”。学生用ECharts重写了前端首页,把<el-table>换成折线图(气温趋势)、柱状图(各城市湿度对比)、地图(全国天气分布热力图)。后端只加了一个/api/weather/statistics接口,返回聚合数据。他重点讲了ECharts的异步加载、响应式适配,以及如何用WebSocket实现“天气预警实时推送”(当数据库插入weather='雷阵雨'时,后端推送消息到前端弹窗)。

第三个是“多源天气数据融合系统”。学生保留了原有MySQL,但增加了MongoDB作为第二数据源,用来存爬虫抓取的第三方天气网站数据(如墨迹天气、彩云天气)。他设计了一个简单的ETL流程:用Spring Batch定时从MongoDB抽取数据,清洗后写入MySQL的t_weather_record表,并加了个source字段标记数据来源。答辩时他对比了不同来源的温度差异,分析了数据可信度,老师评价“有数据工程思维”。

这些延伸案例想说明的是:不要把毕设当成一个“做完就扔”的项目,而要当成你技术能力的“生长基座”。你现在掌握的SpringBoot分层、MyBatis CRUD、Vue组件通信、MySQL索引优化,都是未来学Spring Cloud、学大数据、学AI的底层能力。就像我当年做毕设,用Servlet写了个图书管理系统,后来学SSH时发现Struts的Action和我的Servlet一模一样;学SpringBoot时发现@RestController就是Servlet的升级版。技术在变,但解决问题的逻辑没变。

最后分享一个小技巧:在readme.text里,我特意留了一行“系统默认管理员账号:super/123456(超级管理员),admin/123456(普通管理员)”。这不是为了方便你测试,而是为了让你在答辩时,能从容地对老师说:“您可以用super账号登录后台,试试添加一个管理员,再用admin账号登录,看看‘添加’按钮是否消失——这证明我们的权限控制是生效的。” 这种“现场演示+即时反馈”的方式,比讲十分钟原理都管用。毕竟,毕设答辩的本质,不是考你背了多少概念,而是看你能不能让一个真实的系统,在真实的环境里,解决一个真实的问题。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:基于SpringBoot开发的轻量级天气信息管理系统,前后端分离架构,前端支持按城市、日期范围实时查询和展示天气数据,界面简洁响应快;后台提供管理员登录,可对天气数据进行增删改查,同时支持管理员账号管理(添加、禁用、权限控制)。系统使用MySQL存储天气记录和用户信息,附带完整建表SQL(demo.sql)、Excel样例数据(demo.xlsx)、Maven配置文件(pom.xml)及详细部署说明(readme.text)。日志文件(my.log及相关压缩包)便于上线后问题排查,.idea和target目录表明项目已在IntelliJ IDEA中完成构建与测试,导入即运行。配套毕业论文涵盖需求分析、系统设计、核心代码实现与测试结果,适合计算机类本科生完成Java Web课程设计、毕业设计选题或快速搭建气象数据管理原型。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

更多推荐