1. 项目概述与技术栈选型
大学城水电管理系统是一个面向高校后勤管理的数字化解决方案,旨在解决传统人工抄表、费用计算和账单管理效率低下的问题。这个基于SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0的全栈项目,采用了当前主流的技术组合,实现了水电数据的自动化采集、费用计算和统计分析功能。
技术栈选择上,后端采用SpringBoot2框架,这是目前Java领域最流行的微服务开发框架,相比传统SSM架构,它简化了配置,内置Tomcat服务器,支持快速启动和部署。前端选用Vue3组合式API开发模式,相比Vue2在性能上有显著提升,特别是Composition API让代码组织更加灵活。持久层使用MyBatis-Plus,它在MyBatis基础上增强了CRUD操作,减少了大量模板代码。数据库采用MySQL8.0,充分利用其JSON支持、窗口函数等新特性。
提示:在实际项目开发中,SpringBoot2.x版本需要特别注意与JDK版本的兼容性问题,推荐使用JDK11或JDK17以获得最佳支持。
2. 系统架构设计与核心模块
2.1 整体架构设计
系统采用前后端分离架构,后端提供RESTful API接口,前端通过axios进行调用。这种架构使得前后端可以独立开发和部署,提高了开发效率。后端采用经典的三层架构:
- Controller层:处理HTTP请求,参数校验
- Service层:业务逻辑实现
- DAO层:数据库操作
前端采用模块化设计,主要分为:
- 用户认证模块
- 仪表盘展示模块
- 水电数据管理模块
- 费用计算模块
- 报表统计模块
2.2 数据库设计关键点
MySQL8.0数据库设计中,有几个关键表需要特别注意:
- 用户表(users):存储系统用户信息,包括管理员和普通用户
- 水电表(meters):记录每个房间的水电表信息
- 读数记录(readings):存储定期采集的水电读数
- 费率表(rates):存储不同时段的水电费率
- 账单表(bills):存储生成的账单信息
sql复制CREATE TABLE `readings` (
`id` bigint NOT NULL AUTO_INCREMENT,
`meter_id` bigint NOT NULL COMMENT '水电表ID',
`reading_value` decimal(10,2) NOT NULL COMMENT '读数',
`reading_time` datetime NOT NULL COMMENT '抄表时间',
`reader_id` bigint DEFAULT NULL COMMENT '抄表人ID',
PRIMARY KEY (`id`),
KEY `idx_meter_id` (`meter_id`),
KEY `idx_reading_time` (`reading_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
3. 核心功能实现细节
3.1 水电数据采集与处理
水电数据采集是系统的核心功能之一。我们设计了两种数据采集方式:
- 人工录入:通过Web界面手动输入水电表读数
- 设备对接:通过API接口接收智能电表自动上报的数据
对于自动采集的数据,我们实现了数据校验机制:
java复制public Reading validateReading(ReadingDTO dto) {
if (dto.getMeterId() == null) {
throw new BusinessException("水电表ID不能为空");
}
if (dto.getValue() == null || dto.getValue().compareTo(BigDecimal.ZERO) < 0) {
throw new BusinessException("读数必须为非负数");
}
Meter meter = meterService.getById(dto.getMeterId());
if (meter == null) {
throw new BusinessException("水电表不存在");
}
// 获取上次读数
Reading lastReading = readingMapper.selectLastReading(dto.getMeterId());
if (lastReading != null && dto.getValue().compareTo(lastReading.getValue()) < 0) {
throw new BusinessException("当前读数不能小于上次读数");
}
// 构建实体
Reading reading = new Reading();
BeanUtils.copyProperties(dto, reading);
reading.setReadingTime(new Date());
return reading;
}
3.2 费用计算引擎实现
费用计算是系统的另一个核心功能。我们设计了一个灵活的费用计算引擎,支持:
- 阶梯计价:不同用量区间采用不同费率
- 分时计价:不同时段采用不同费率
- 特殊折扣:对特定用户或特定时段给予折扣
费用计算的核心逻辑:
java复制public BigDecimal calculateBill(Long meterId, Date startDate, Date endDate) {
// 获取费率规则
List<Rate> rates = rateService.getRatesByMeter(meterId);
// 获取期间读数
List<Reading> readings = readingMapper.selectReadingsBetween(meterId, startDate, endDate);
if (readings.size() < 2) {
throw new BusinessException("计算期间内读数不足");
}
// 排序读数
readings.sort(Comparator.comparing(Reading::getReadingTime));
BigDecimal totalAmount = BigDecimal.ZERO;
Reading prevReading = readings.get(0);
// 计算每个区间的费用
for (int i = 1; i < readings.size(); i++) {
Reading currentReading = readings.get(i);
BigDecimal usage = currentReading.getValue().subtract(prevReading.getValue());
Date usageTime = currentReading.getReadingTime();
// 查找适用的费率
Rate applicableRate = findApplicableRate(rates, usageTime);
// 计算该区间费用
BigDecimal intervalAmount = usage.multiply(applicableRate.getRateValue());
totalAmount = totalAmount.add(intervalAmount);
prevReading = currentReading;
}
return totalAmount;
}
4. 前端实现与优化
4.1 Vue3组合式API实践
前端采用Vue3的组合式API,相比Options API更加灵活。以下是仪表盘组件的一个示例:
javascript复制<script setup>
import { ref, onMounted, computed } from 'vue'
import { useStore } from 'vuex'
import { getDashboardData } from '@/api/dashboard'
const store = useStore()
const loading = ref(true)
const dashboardData = ref(null)
const userRole = computed(() => store.state.user.role)
const fetchData = async () => {
try {
loading.value = true
const response = await getDashboardData()
dashboardData.value = response.data
} catch (error) {
console.error('获取仪表盘数据失败:', error)
} finally {
loading.value = false
}
}
onMounted(() => {
fetchData()
})
</script>
4.2 数据可视化实现
系统使用ECharts实现水电消耗数据的可视化展示。我们封装了一个可复用的图表组件:
javascript复制<template>
<div ref="chartRef" style="width: 100%; height: 400px;"></div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
import * as echarts from 'echarts'
const props = defineProps({
option: {
type: Object,
required: true
},
theme: {
type: String,
default: 'light'
}
})
const chartRef = ref(null)
let chartInstance = null
const initChart = () => {
if (chartInstance) {
chartInstance.dispose()
}
chartInstance = echarts.init(chartRef.value, props.theme)
chartInstance.setOption(props.option)
// 响应式调整
const resizeHandler = () => chartInstance.resize()
window.addEventListener('resize', resizeHandler)
onBeforeUnmount(() => {
window.removeEventListener('resize', resizeHandler)
chartInstance.dispose()
})
}
onMounted(initChart)
watch(() => props.option, initChart, { deep: true })
watch(() => props.theme, initChart)
</script>
5. 系统安全与性能优化
5.1 安全防护措施
- 认证与授权:采用JWT进行用户认证,结合Spring Security实现基于角色的访问控制
- 数据校验:前后端都进行严格的数据校验,防止非法输入
- SQL防护:使用MyBatis-Plus的Wrapper避免SQL注入
- XSS防护:前端使用DOMPurify对用户输入进行净化
Spring Security配置示例:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.antMatchers("/api/**").authenticated()
.anyRequest().permitAll()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
5.2 性能优化实践
-
数据库优化:
- 合理设计索引
- 使用MyBatis-Plus的二级缓存
- 对大表进行分区
-
接口优化:
- 使用Spring Cache缓存频繁访问的数据
- 对大数据量接口实现分页查询
- 使用DTO减少不必要的数据传输
-
前端优化:
- 组件懒加载
- 路由懒加载
- 使用keep-alive缓存组件状态
缓存配置示例:
java复制@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats());
return cacheManager;
}
}
6. 项目部署与运维
6.1 后端部署方案
后端采用Docker容器化部署,便于扩展和维护。Dockerfile示例:
dockerfile复制FROM openjdk:11-jre-slim
WORKDIR /app
COPY target/water-electricity-system.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
使用docker-compose编排服务:
yaml复制version: '3'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/water_electricity?useSSL=false
- SPRING_DATASOURCE_USERNAME=root
- SPRING_DATASOURCE_PASSWORD=yourpassword
depends_on:
- mysql
mysql:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=yourpassword
- MYSQL_DATABASE=water_electricity
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:
6.2 前端部署方案
前端使用Nginx作为Web服务器,配置如下:
nginx复制server {
listen 80;
server_name yourdomain.com;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend: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;
}
}
7. 常见问题与解决方案
7.1 开发环境问题
问题1:SpringBoot2启动报错找不到application.properties
解决方案:
- 确保配置文件放在src/main/resources目录下
- 检查文件名是否正确,包括大小写
- 如果是多环境配置,确保激活的profile有对应的配置文件
问题2:Lombok在IDE中不生效
解决方案:
- 在IDE中安装Lombok插件
- 确保项目中包含Lombok依赖
- 在IDE设置中启用注解处理
7.2 生产环境问题
问题1:MySQL8.0连接问题
解决方案:
- 确保MySQL用户有远程连接权限
- 检查连接URL中的时区设置,建议添加
serverTimezone=Asia/Shanghai - 对于SSL连接问题,可以添加
useSSL=false
问题2:Vue3路由问题
解决方案:
- 确保路由模式与部署环境匹配(history模式需要服务器支持)
- 检查路由守卫中的逻辑是否正确
- 对于动态导入的组件,确保路径正确
8. 项目扩展与二次开发
8.1 功能扩展建议
- 移动端支持:开发微信小程序或APP版本,方便学生和教职工随时查询
- 智能预警:实现水电异常消耗的自动检测和预警
- 第三方支付:集成支付宝、微信支付,实现在线缴费
- 数据分析:增加更丰富的数据分析功能,如同比环比分析
8.2 技术升级路径
- 微服务化:将系统拆分为多个微服务,提高可扩展性
- 前后端分离:可以考虑采用GraphQL替代RESTful API
- CI/CD:引入Jenkins或GitHub Actions实现自动化构建部署
- 监控系统:集成Prometheus和Grafana实现系统监控
在实际开发中,我发现MyBatis-Plus的Lambda表达式能极大提高开发效率,特别是在复杂查询场景下。例如,下面的代码展示了如何使用Lambda表达式构建动态查询条件:
java复制public Page<Bill> queryBills(BillQueryDTO queryDTO) {
return billMapper.selectPage(new Page<>(queryDTO.getPage(), queryDTO.getSize()),
Wrappers.<Bill>lambdaQuery()
.eq(queryDTO.getUserId() != null, Bill::getUserId, queryDTO.getUserId())
.eq(queryDTO.getStatus() != null, Bill::getStatus, queryDTO.getStatus())
.ge(queryDTO.getStartDate() != null, Bill::getCreateTime, queryDTO.getStartDate())
.le(queryDTO.getEndDate() != null, Bill::getCreateTime, queryDTO.getEndDate())
.orderByDesc(Bill::getCreateTime)
);
}
对于Vue3项目,组合式API确实让代码组织更加灵活,但需要注意避免过度拆分逻辑,否则反而会降低可读性。我建议将相关逻辑组织在同一个文件中,使用良好的命名约定来保持代码清晰。
