做了几年机房管理系统相关项目,我最大的感受是:上机管理这个事,看着简单,真正做起来涉及的点特别多。很多人一听到“上机管理系统”就以为是给网吧做计时收费,其实在高校机房、培训中心、公司内部共享设备这些场景里,要解决的问题比你想象的多得多。注册与权限、设备分配、计时计费、异常掉线处理、数据统计,每一项都藏着细节。这篇文章我就以一套基于Web的上机管理系统源码为线索,把我自己的设计思路、核心模块实现、数据库方案、踩坑记录一次性讲清楚。无论你是准备拿这类题目做毕业设计,还是工作中需要搭建一套内部上机管理系统,这篇都能给你一个可以直接参考的完整路径。
这个项目用到的关键词挺集中的:Web、上机管理系统、源码。我的讲述也会围绕这三个点展开——为什么选Web方案、系统每个模块怎么设计、哪些代码可以直接复用。
1. 上机管理系统怎么拆需求:机房里的角色、流程与边界
很多毕设选题第一步就卡住了,不是不会写代码,而是不知道要做什么。其实做这类系统,最靠谱的方式是先把自己想象成机房管理员,把一天的工作流程捋一遍,系统功能就自动浮现出来了。
1.1 机房日常管理中真实存在的三个痛点
我走访过几个高校机房和培训机构的实际管理场景,发现不管规模大小,痛点高度一致。
第一个痛点是上机身份确认。早期机房靠管理员登记,学生上机填一张表,下机再填一次,纸质记录又慢又容易丢。后来用Excel,好一点,但多人同时上机时,登记信息经常对不上号——人走了没记录下机时间,或者用了他人的账号。
第二个痛点是计费核算。高校机房虽然有免费时段,但开放给校外人员或培训用途时就要收费。按小时计费还是按次计费?超过一分钟怎么算?包时段怎么处理?这些问题如果全靠人工计算,月末对账时管理员心态会崩。
第三个痛点是设备状态不可见。哪台机器在用、哪台空闲、哪台故障了,管理员只能一台一台去看,或者靠学生口头反馈。人多的时候,设备利用率很难提升,故障设备也不能及时被发现和维修。
这三个痛点对应的正是上机管理系统最基础的三块能力:用户管理、计费管理、设备管理。你去看市面上的上机管理系统,不管界面多花哨,核心永远逃不出这三个模块。
1.2 系统角色与核心业务流程闭环
明确了痛点之后,角色模型就很好确定了。一套完整的Web上机管理系统,角色通常分三种:
| 角色 | 核心操作 | 关注点 |
|---|---|---|
| 管理员 | 用户管理、设备管理、费率设置、查看统计 | 管理效率、数据准确 |
| 普通用户(学生/培训学员) | 注册/登录、自助上机、下机结算、查看消费记录 | 操作便捷、计费透明 |
| 系统审计员(可选) | 查看操作日志、充值日志 | 数据可追溯 |
核心业务流程是一个闭环:
账号登录 → 浏览空闲设备 → 选择设备/座位 → 系统锁定设备并开始计时计费 → 使用完毕下机 → 费用结算(扣费/记录) → 生成上机记录 → 管理员可以查看整体统计。
这个闭环里,最值得关注的边界点是:
- 用户登录后能否同时占用多台设备?我见过的正规系统基本都限制一人一机,避免资源浪费。
- 设备被选中的瞬间遇到其他人同时抢选,怎么处理?这需要数据库层面有一定的锁或唯一约束。
- 断网、断电等异常情况下,计时怎么处理?
- 余额不足时,是强制下机还是允许欠费继续使用?
这些问题如果能在需求分析阶段就明确下来,后面写代码会省掉大量返工。如果你的时间有限,至少先把前两个问题想清楚,因为它们直接影响表结构设计和并发处理逻辑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与整体架构:从机房管理模式说到Web方案
系统是B/S还是C/S,这个抉择直接影响整个项目的工作量。我见过不少毕设选题明明叫“基于Web”,最后交出来却是一个纯本地桌面程序包了一层浏览器壳,这种就属于没想清楚架构。
2.1 为什么选Web而不是桌面客户端
机房上机管理类的系统,早些年确实有很多C/S架构的实现,用一个Delphi或C#客户端连接局域网数据库,配合刷卡器使用。但现在做新系统,我强烈建议选择Web方案,理由很实际:
- 免安装:客户端方案最大的痛点是更新。今天加一个功能,明天改一个Bug,所有客户端都要重新发布。Web方案只需要重新部署服务器,浏览器刷新即用。
- 跨平台:上机管理不只是机房里的固定终端要用,管理员可能用手机看统计数据,学生可能用自己笔记本访问系统。Web天然跨平台。
- 远程管理:Web系统可以通过校园网、局域网甚至公网访问,管理员不在机房也能看到设备状态和上机情况。
- 设备松耦合:机房的终端只需要有一个浏览器,不再受操作系统版本限制,也不需要在每台终端上安装运行库。
基于以上考虑,这个项目确定采用B/S结构。前端浏览器负责交互展示,后端服务负责业务逻辑与数据存取,浏览器与服务器之间通过HTTP协议交互。
需要特别说明的是,上机管理场景里有一个特殊的现实问题:如果机房里的终端也需要被纳管,通常的做法是终端开机自动打开浏览器进入固定页面,或者在终端上装一个小型Agent用于上报开机状态。后者属于设备监控范畴,如果毕设阶段做不完,可以先只做Web端的管理功能,在设备表里手动维护设备在线状态即可。
2.2 整体技术栈选型与分层设计
既然确定了Web方向,技术栈的选择就有了明确的范围。我做这类系统时比较推荐的组合是这样的:
后端:Spring Boot + MyBatis-Plus + MySQL。Spring Boot是目前Java后端最主流的选择,起步快、生态完善、自带Tomcat。MyBatis-Plus在MyBatis之上做了大量增强,单表CRUD基本不用手写SQL,非常适合快速开发。MySQL则是最稳妥的数据库选型,部署文档多,出了问题也容易搜到解决方案。
如果你对Java不是很熟,用Python Flask或Django也能做,不过考虑到这类系统后续要对接校园统一认证、一卡通消费等场景,Java生态的集成案例更多,所以我个人还是倾向Java方案。
前端:如果不做前后端分离,直接用Thymeleaf模板引擎 + Bootstrap + jQuery,开发效率最高,学生也最容易上手。如果时间充裕,想做得更现代一些,可以用Vue 3 + Element Plus做单页应用,后端提供JSON接口。
这里我多说一句:毕设项目不建议为了追求技术新鲜度而选择过重的架构。比如引入微服务、Redis缓存、消息队列这些,如果没有真正用起来的场景,反而会在答辩时被老师追问到漏洞百出。倒不如把Spring Boot + MyBatis-Plus + MySQL这套组合用扎实,把业务逻辑做完整。
动态监控技术:上机管理系统中,设备状态和计时信息需要实时刷新。最简单的方案是前端定时轮询,每隔5到10秒请求一次接口;进阶方案是用WebSocket做服务端推送。如果做毕设,轮询完全够用,而且逻辑简单、容易解释清楚。
2.3 三层架构与项目目录组织
项目的代码组织,我习惯采用经典的三层架构:Controller层接收请求并做参数校验,Service层处理业务逻辑,Mapper层(DAO层)负责数据库交互。这是Spring Boot项目最常规的写法,逻辑清晰,也符合大多数课程设计的验收要求。
实际的包结构大致是这样的:
code复制com.example.labmanage
├── controller // 接口层
│ ├── AuthController.java
│ ├── UserController.java
│ ├── MachineController.java
│ ├── RecordController.java
│ └── StatsController.java
├── service // 业务逻辑层
│ ├── UserService.java
│ ├── AuthService.java
│ ├── MachineService.java
│ ├── RecordService.java
│ └── ChargeService.java
├── mapper // 数据访问层
│ ├── UserMapper.java
│ ├── MachineMapper.java
│ ├── RecordMapper.java
│ └── RechargeLogMapper.java
├── entity // 实体类
│ ├── User.java
│ ├── Machine.java
│ ├── UsageRecord.java
│ └── RechargeLog.java
├── common // 通用工具类、异常处理、返回结果封装
│ ├── Result.java
│ ├── BusinessException.java
│ └── GlobalExceptionHandler.java
├── config // 配置类
│ └── WebConfig.java
└── interceptor // 拦截器
└── LoginInterceptor.java
这样的分层结构,最直接的好处是出了问题能快速定位:接口报错查Controller,业务逻辑有问题查Service,数据不对查Mapper SQL。
3. 核心链路实现:登录、分配、计费、下机的完整代码逻辑
需求清楚了、架子搭好了,接下来就是填充核心功能。这一节我挑几个关键环节,把代码级别的实现逻辑讲透。
3.1 用户认证与权限控制:从登录开始防越权
用户登录是系统的第一道门。做上机管理系统时,认证逻辑要比普通网站多几个心眼:既要防暴力破解,又要防止普通用户越权操作管理员接口。
密码存储方面,千万不能明文存数据库。一种简单可靠的方式是使用BCrypt加密。Spring Boot的spring-security-crypto包内置了BCryptPasswordEncoder,用法非常直接:
java复制// 注册时:加密存储
String encodedPassword = new BCryptPasswordEncoder().encode(rawPassword);
user.setPassword(encodedPassword);
// 登录校验时:只比对密文,不比对明文
boolean matches = new BCryptPasswordEncoder().matches(rawPassword, user.getPassword());
登录成功后,使用Session保存用户信息,同时生成一个会话标识。每次请求进来,通过拦截器统一校验登录状态。
权限控制方面,我建议在拦截器里做两层校验。第一层判断是否登录,第二层判断访问的接口是否与当前角色匹配。可以用一个简单的注解方式实现:
java复制@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireRole {
String value(); // "ADMIN" 或 "USER"
}
然后在拦截器里解析:
java复制public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 1. 处理跨域预检请求
// 2. 从Session中获取登录用户
// 3. 判断方法上的 @RequireRole 注解,角色不匹配则返回403
}
这里要提醒一个容易被忽略的细节:前端隐藏按钮不算权限控制。很多人只把管理员入口链接在页面上隐藏了,结果接口照样能被普通用户猜出来直接调用。真正的权限控制必须发生在服务端,前端隐藏只是体验优化。
3.2 上机/下机流程的状态机设计:避免一个用户占两台设备
上机管理区别于普通业务系统的最典型特征,就是状态流转非常明确。每台设备只有几种状态:空闲、使用中、维修中。每一个上机行为都是一个完整的状态机。
我在设计时把状态表放在设备实体里,同时用上机记录表维护每一次使用的详情:
- 设备状态(0-空闲,1-使用中,2-维修中)
- 上机记录状态(0-进行中,1-已完成,2-异常终止)
上机操作的Service层逻辑,核心代码如下:
java复制@Transactional
public Result startUse(Long userId, Long machineId) {
// 1. 校验用户
User user = userMapper.selectById(userId);
if (user == null || user.getStatus() != 1) {
return Result.fail("用户不存在或已被禁用");
}
// 2. 检查是否已有进行中的上机记录
LambdaQueryWrapper<UsageRecord> activeWrapper = new LambdaQueryWrapper<>();
activeWrapper.eq(UsageRecord::getUserId, userId)
.eq(UsageRecord::getStatus, 0);
if (recordMapper.selectCount(activeWrapper) > 0) {
return Result.fail("当前用户已有使用中的记录,请先下机");
}
// 3. 检查机器状态
Machine machine = machineMapper.selectById(machineId);
if (machine == null || machine.getStatus() != 0) {
return Result.fail("设备不存在或当前不可用");
}
// 4. 抢占设备:通过条件更新防止并发重复分配
int updated = machineMapper.updateStatusWithCondition(machineId, 0, 1);
if (updated == 0) {
return Result.fail("设备刚刚被其他用户使用,请重新选择");
}
// 5. 创建上机记录
UsageRecord record = new UsageRecord();
record.setUserId(userId);
record.setMachineId(machineId);
record.setStartTime(LocalDateTime.now());
record.setStatus(0);
recordMapper.insert(record);
return Result.success(record);
}
这里的核心是第4步的条件更新,用一条SQL完成“检查状态并修改状态”的原子操作:
sql复制UPDATE machine SET status = 1 WHERE id = #{machineId} AND status = 0
这种方法比先查询再更新更安全,能避免两个用户同时抢选一台设备时数据库出现脏数据。如果要更严格一点,还可以在UPDATE之后检查受影响行数,只有受影响行数为1时才算抢占成功。
下机操作与上机相反:设备状态置回空闲,上机记录状态改为已完成,计算本次上机时长和费用。这个操作同样需要事务,确保“设备释放”和“记录更新”要么都成功,要么都失败。
3.3 计费引擎设计:精度、边界与可配置
计费逻辑是最容易出Bug的地方。如果你直接用浮点数做运算,很快会遇到0.1 + 0.2不等于0.3这种问题;如果对不同费率规则不加区分,又可能出现深夜包场费率和普通时段费率混用的情况。
我的建议是:金额统一用分为单位存储,使用整数或BigDecimal运算,最终展示时再转成元。如果是Java后端,BigDecimal配合setScale(2, RoundingMode.HALF_UP)是标准做法。
计费规则需要可配置。我把费率设计成一张fee_config表,支持设置基础单价、免费时长、按时段计费、单日封顶价等选项。
以最简单的按时计费为例:
java复制public BigDecimal calculateCost(UsageRecord record, FeeConfig config) {
long minutes = Duration.between(record.getStartTime(), LocalDateTime.now()).toMinutes();
long billableMinutes = Math.max(0, minutes - config.getFreeMinutes());
BigDecimal cost = BigDecimal.valueOf(billableMinutes)
.multiply(config.getUnitPrice())
.divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP);
// 封顶逻辑
if (config.getDailyCap() != null && cost.compareTo(config.getDailyCap()) > 0) {
return config.getDailyCap();
}
return cost;
}
计费系统中必须处理的边界情况包括:
- 免费时长:不足免费时长的部分直接按0计费,但时长记录要保留。
- 跨天使用:如果配置了每日封顶价,跨天需要按天切割计算,而不是简单算总时长。
- 包时段:比如“18:00到22:00包价20元”,如果用户从19:00用到次日02:00,需要按覆盖规则分段计费。
- 余额不足:建议设计为允许欠费上机,但设置一个最大欠费额度,超过后不可继续上机。
这些规则在写代码前就应该明确,否则后期改规则会牵连到表结构改动,工作量很大。
4. 数据库设计与事务边界:账不能算错,钱不能丢
上机管理系统本质上是一个带有资金交易属性的业务系统。计费、充值、扣费这些操作直接和钱相关,数据库设计的好坏直接决定系统的可靠程度。
4.1 核心表结构与字段解释
我设计的表结构包含五张核心表:用户表sys_user、设备表machine、上机记录表usage_record、充值记录表recharge_log、费率配置表fee_config。下面把每张表的关键字段列出来,并说明设计理由。
sys_user(用户表)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| username | varchar(50) | 登录名(唯一索引) |
| password | varchar(100) | BCrypt加密后的密码 |
| real_name | varchar(50) | 真实姓名 |
| role | varchar(20) | ADMIN / USER |
| balance | int | 余额(单位:分) |
| status | tinyint | 1-正常 0-禁用 |
| created_at | datetime | 创建时间 |
余额用int存分而不是用decimal存元,一方面避免浮点误差,另一方面整数运算速度更快。展示层只需要除以100即可还原成元。
machine(设备表)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| machine_name | varchar(64) | 设备编号(如A区01号机) |
| ip_address | varchar(64) | 设备IP地址 |
| location | varchar(128) | 所在位置描述 |
| status | tinyint | 0-空闲 1-使用中 2-维修中 |
| remark | varchar(255) | 备注 |
usage_record(上机记录表)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id | bigint | 用户ID |
| machine_id | bigint | 设备ID |
| start_time | datetime | 上机时间 |
| end_time | datetime | 下机时间 |
| duration_minutes | int | 本次使用时长(分钟) |
| cost | int | 本次消费金额(分) |
| status | tinyint | 0-进行中 1-已完成 2-异常终止 |
| created_at | datetime | 记录创建时间 |
这里需要特别说明:usage_record中的cost字段是在下机结算时写死的。不要在页面展示时通过费率表实时计算历史记录的费用,因为费率规则将来可能调整,历史记录的金额必须保持当时的结算结果不变。
recharge_log(充值记录表)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id | bigint | 用户ID |
| amount | int | 充值金额(分) |
| pay_type | varchar(20) | 支付方式 |
| created_at | datetime | 充值时间 |
| operator | varchar(50) | 操作人(管理员代充时记录) |
充值操作在真实场景中可能涉及第三方支付对接,如果是毕设或内部系统,做成管理员后台代充或模拟支付即可,但要保留完整的日志表结构,方便将来扩展。
fee_config(费率配置表)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| config_name | varchar(64) | 费率方案名称 |
| unit_price | int | 每小时单价(分) |
| free_minutes | int | 免费时长(分钟) |
| daily_cap | int | 每日封顶价(分) |
| start_time | time | 生效起始时间 |
| end_time | time | 生效结束时间 |
| enabled | tinyint | 是否启用 |
4.2 并发场景下的一致性保障
上机管理系统是典型的高并发写操作场景:高峰时段大量用户同时上机、下机、充值、扣费。这个场景下最容易出现三类数据问题。
第一类是设备抢占冲突。多个用户同时点击同一台空闲设备。解决方案在上面已经提到,用条件更新实现原子操作,核心是不把“读取状态”和“更新状态”分成两步执行。
第二类是余额扣减不一致。用户下机时扣费,如果用户恰好同时充值,可能出现余额扣成了负数,或者充值后余额被旧数据覆盖。解决方案是把“查询余额”和“更新余额”合并为一个带条件的Update语句:
java复制@Update("UPDATE sys_user SET balance = balance - #{cost} WHERE id = #{userId} AND balance >= #{cost}")
int deductBalance(@Param("userId") Long userId, @Param("cost") int cost);
如果返回受影响行数为0,说明余额不足或用户不存在,本次扣费失败,需要走异常处理流程。这种写法天然避免了并发覆盖。
第三类是跨表操作的一致性问题。上机创建记录、修改设备状态、下机结算扣费,这些操作往往涉及多张表,必须用事务包住。Spring Boot里直接在Service方法上标注@Transactional即可,但要注意事务默认只回滚RuntimeException,如果代码里捕获了异常但没有重新抛出,事务就失效了。
4.3 账目核对:每个用户的钱是怎么流动的
为了让系统可审计,每一笔余额变动都应该有对应日志。我在设计中遵循一条原则:余额变更必须有流水溯源。充值记录表记充值流水,扣费通过上机记录的cost字段体现,管理员手动调整余额单独记录adjust_log。这样月末对账时,可以用一条SQL把用户的每一笔余额变动拼起来:
sql复制SELECT
u.username,
u.balance AS current_balance,
IFNULL(r.total_recharge, 0) AS total_recharge,
IFNULL(rec.total_cost, 0) AS total_cost,
IFNULL(r.total_recharge, 0) - IFNULL(rec.total_cost, 0) AS expected_balance
FROM sys_user u
LEFT JOIN (
SELECT user_id, SUM(amount) AS total_recharge
FROM recharge_log GROUP BY user_id
) r ON u.id = r.user_id
LEFT JOIN (
SELECT user_id, SUM(cost) AS total_cost
FROM usage_record WHERE status = 1 GROUP BY user_id
) rec ON u.id = rec.user_id
这条核对SQL非常实用,如果某个用户当前余额和预期余额对不上,说明系统中存在Bug或手工操作,能迅速定位问题。
5. 安全、异常恢复与上线前的自测清单
上机管理系统跑在校园网或局域网里,虽然不像公网系统那样直面攻击,但安全防线依然要做扎实。我把安全重点分成三块:接口安全、数据安全、异常恢复。
5.1 接口安全的三个层面
第一层是身份认证。除了登录拦截器之外,前后端交互建议使用Session或Token机制。如果是纯Web场景,Session + Cookie就够了,但要设置合理的会话超时时间,比如30分钟无操作自动失效。前端的Ajax请求需要带上Session凭证,在拦截器里统一校验。
第二层是参数校验。所有接收前端传参的接口,都要做合法性校验,禁止数据库错误信息直接透出到页面。比如用户ID必须是正数、金额必须在合理范围内、分页参数不能大于1000。这些校验用Spring Boot的@Validated注解配合实体类字段上的校验注解即可完成。
第三层是SQL注入与XSS的防范。MyBatis的#{}语法天然防SQL注入,但要确保写SQL时不使用${}拼接字符串。XSS防护需要在后端对输入内容做转义过滤,尤其是用户真实姓名、备注等可以录入的文本字段。
5.2 断电、断网等异常场景的恢复策略
机房场景里,断电断网是家常便饭。系统必须能在异常情况下保证数据不丢、账目不乱。
最核心的机制是:上机记录一旦创建,哪怕下机操作没有正常完成,记录也不能丢失。我在下机操作中提供两个通道:正常情况下用户在前端点“下机”按钮完成结算;异常情况下(比如终端断电、网络断开),管理员可以在后台看到状态为“使用中”的记录,手动执行“强制下机”操作。
强制下机逻辑:
java复制@Transactional
public Result forceFinish(Long recordId, Long operatorId) {
UsageRecord record = recordMapper.selectById(recordId);
if (record == null || record.getStatus() != 0) {
return Result.fail("记录不存在或已结束");
}
// 以当前时间作为下机时间,计算费用
record.setEndTime(LocalDateTime.now());
record.setDurationMinutes((int) Duration.between(record.getStartTime(), record.getEndTime()).toMinutes());
record.setCost(calculateCost(record, getCurrentFeeConfig()));
record.setStatus(1);
recordMapper.updateById(record);
// 释放设备
machineMapper.updateStatus(record.getMachineId(), 1, 0);
// 扣费
int updated = userMapper.deductBalance(record.getUserId(), record.getCost());
if (updated == 0) {
// 余额不足时允许负余额,但标记告警
userMapper.markNegativeBalance(record.getUserId());
}
return Result.success(record);
}
这个逻辑里我设计了一个“允许负余额但标记告警”的方案。它比强制不让下机更现实——已经发生的上机服务不可能回头,欠费属于后续催缴问题。
5.3 上线前的自测清单
以一个运行过多次的项目案例经验来说,上机管理系统在正式投入使用前,至少要过一遍下面的自测清单:
- 两个用户同时抢同一台空闲设备,只有一个人能成功。
- 同一个用户在上机状态下再次选择新设备,会被拒绝。
- 免费时长内的用户下机,费用为0但记录完整。
- 跨天使用设备时,跨天时段和封顶价计算正确。
- 用户余额不足时下机,扣费失败但系统不崩溃,记录状态为已完成且标记欠费。
- 管理员强制下机后,设备回到空闲状态,用户看到费用已结算。
- 禁用用户后,该用户无法登录,已上机的记录可以正常下机结算。
- 分页查询大数据量上机记录时,响应时间在合理范围内。
这些场景每一条背后都可能藏着一个具体的Bug,我见过不少项目在演示时一切正常,一到真实环境就翻车,原因就是没有提前做这些场景的验证。
6. 实际部署中踩过的坑与优化经验
最后这部分,我把自己在开发和部署这套系统时真实踩过的坑,以及对应的解决方案整理出来。这些问题你在开发过程中大概率也会碰到。
6.1 时间与精度问题:两个最容易翻车的细节
第一个坑是跨天计费的日期处理。如果直接用LocalDateTime比较,跨天场景很容易漏判。比如用户晚上23:50上机,次日00:30下机,如果封顶价是按“自然日”算的,就必须分别计算两天的使用时长。我在实现时写了一个按天拆分工具方法,先算出跨度涉及的天数,然后逐天计算费用再汇总。这里需要注意的是,机房管理员通常认为“一晚上”是连续时段,但数据库存储时必须精确到自然日,否则无法与封顶规则对应。
第二个坑是浮点数参与金额计算。我一开始在费率配置表里用了decimal类型,在Java代码里用double参与运算,结果出现用户充了50元,用了3小时后余额显示少了0.01元的奇怪现象,排查半天才发现是浮点运算精度问题。后来全部改成int分存储,余额计算全程用整数,问题彻底消失。
6.2 查询性能与联调问题
上机记录表的数据量增长非常快,尤其是每天有大量短时上机的场景。如果不做分页和索引优化,页面打开会越来越慢。我在usage_record表上建了联合索引(user_id, status)和(start_time),并按天统计的查询也走了索引。如果数据量到了一定规模,还可以做按月分表,不过毕设和中小规模机房用不上这个方案。
前后端联调时最常遇到的是跨域问题。如果前端和后端分开部署,需要在后端配置CORS。我的做法是在Spring Boot中写一个WebMvcConfigurer配置:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:8081")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true);
}
}
注意,如果使用了Session做登录状态,allowCredentials必须设为true,并且allowedOrigins不能使用通配符,必须写明确的前端地址。
6.3 部署环境里的小细节
- 数据库连接池:如果使用HikariCP,建议把maximum-pool-size设置为10-20,太小高峰时期会排队,太大会浪费数据库资源。
- 时区配置:MySQL连接串上一定要加serverTimezone=Asia/Shanghai,否则服务器时区和数据库时区不一致的时候,插入的时间会差8个小时。
- 图片与静态资源:机房座位图、设备位置图这类静态资源,建议走Nginx直接托管,不经过Java应用服务器,减少后端压力。
根据我的实际部署经验,一台4核8G的云服务器跑Spring Boot + MySQL,支持200台设备、5000个用户的机房完全没问题,瓶颈通常不会出现在性能上,而在于异常情况的处理是否完善。
最后再分享一个我做这套系统时学到的小技巧:每张业务表都加上created_at和updated_at字段,并在插入更新时自动维护。上机管理系统需要频繁排查历史数据问题,有这两个字段,定位问题能省一半时间。MyBatis-Plus的MetaObjectHandler可以自动填充这两个字段,不需要在每个Service方法里手动赋值。你现在开始写代码的时候就把这个机制加上,后面会省很多事。
