ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot水务管理系统设计与部署实战:从源码到上线全解析

SpringBoot水务管理系统设计与部署实战:从源码到上线全解析 水务管理系统是一个看着不大、实际上业务链条很长的项目。水表档案、抄表、计费、账单、缴费、报装、维修每一块都不能少再加上用户权限、操作日志、统计报表这些通用模块整套做下来足够撑起一个像模像样的毕业设计也够一个小型水务公司日常管理用了。这也是为什么SpringBoot技术栈里这类系统一直很受欢迎——业务需求明确、模块边界清晰、难度适中做出来又有完整的上线演示效果。我最近整理了一套完整的水务管理系统源码和配套文档从数据库建表、后端核心业务代码到部署上线的每一个环节都重新过了一遍。这篇文章就把这套系统的设计和实现拆开来讲重点放在源码结构、部署步骤和代码讲解这三个最容易让人卡住的地方给正在做同类项目、或者想从零搭一个完整后端工程的朋友提供一份可以照着操作的路线图。1. 项目整体设计先搞清楚业务再动手写代码1.1 水务管理系统到底管什么很多人拿到“水务管理系统”这个题目第一反应是懵的不知道从哪里切入。其实你只要把自己代入一个水务公司的日常运营场景需求就清晰了。水务公司的核心业务链路是用户开户建档 → 安装水表 → 定期抄表 → 按用量计费 → 生成账单 → 用户缴费 → 欠费催缴。在这条主链路之外还有报装申请、过户、报修工单、停水通知这些服务类业务以及日常运营所需要的统计分析、营收报表等管理功能。所以这个系统我拆成了六个模块系统管理用户管理、角色权限、菜单配置、操作日志、数据字典。基础档案用水户档案、水表档案用户和表是一对多的关系一个人可以有多块表、区域/片区管理、计费规则配置。抄表业务抄表任务创建、抄表数据录入支持人工录入和远传表自动采集、抄表异常标记、抄表审核。计费与账单按用量自动计算水费支持阶梯水价、催缴管理、缴费登记、缴费记录导出。服务业务报装申请新装、增容、过户申请、报修工单派单与处理。统计报表用水量趋势、营收日报/月报、欠费统计、抄表完成率。把这些模块列出来整个系统的骨架就出来了。不要一上来就写代码先花半天把业务模块梳理清楚后面建表、写接口都会顺很多。1.2 技术选型为什么是SpringBoot技术选型上我用的组合是SpringBoot MyBatis-Plus MySQL Redis JWT Knife4jSwagger增强版前端配的是Vue3 Element Plus。这套组合在中小型管理系统里非常经典设计思路是实用为主尽量不整花活。SpringBoot最大的价值在于自动装配。你引入一个依赖它通过EnableAutoConfiguration把常用的Bean帮你配好比如spring-boot-starter-web自动注入Tomcat和Spring MVCspring-boot-starter-data-redis自动生成RedisTemplate。传统SSM要写一大堆XML配置SpringBoot里只需要在application.yml里写数据源就完事了。这也是为什么招聘里到处都是SpringBoot——它把开发者的精力从繁琐配置中解放出来聚焦在业务代码上。这里顺带提醒一下SpringBoot版本要选好。2.7.x是目前绝大多数第三方框架兼容最好的版本3.x系列虽然更现代但把javax.*包换成了jakarta.*很多老库直接不认而且默认的代理机制也变了。如果你的项目需要整合一些基于2.x开发的工具老老实实待着2.7.x最稳妥没必要追新版。1.3 工程结构规划后端工程我用的是经典的分层架构Controller接收请求、Service处理业务逻辑、Mapper负责数据库操作、Entity对应表结构。再加上config配置、common通用返回结果和异常处理、security认证与权限拦截三个辅助包。com.water ├── WaterApplication.java # 启动类 ├── controller/ # 接口层 │ ├── SystemUserController.java │ ├── MeterController.java │ ├── ReadingController.java │ ├── BillController.java │ └── ReportController.java ├── service/ # 业务层 ├── mapper/ # 数据访问层 ├── entity/ # 实体类 ├── config/ # MyBatis、Redis、跨域等配置 ├── security/ # JWT拦截器、权限注解 └── common/ # Result统一返回、异常全局处理这个结构不是拍脑袋定的它对应了请求处理的完整链路前端调用接口 → Controller做参数校验 → Service处理业务逻辑 → Mapper拼SQL查询 → 数据库返回结果 → 逐层封装返回给前端。只要按这个思路走到一遍看别的SpringBoot项目代码也能快速上手。2. 数据库设计地基打不好后面全要返工2.1 核心表有哪些数据库设计是这类系统的灵魂表建得好不好直接决定后面业务代码写起来费不费劲。我把表分成基础档案类和业务流水类。基础档案类sys_user系统登录用户区分管理员、抄表员、营业员等角色。water_user用水户档案和登录用户是两套数据别混。water_meter水表档案记录安装地址、规格、表类型远传/人工、状态。area片区信息水表可以按片区、街道归类方便抄表任务分配。业务流水类meter_reading抄表记录一条记录就是某块表在某一次抄表周期内的读数。water_bill账单表账期、用水量、应收金额、状态。payment_record缴费记录实收金额、缴费方式、操作人。repair_order维修工单报修地址、故障描述、处理人、处理状态。application_form报装/过户申请记录业务申请信息。2.2 关键字段与设计细节挑几张重点表说几个容易踩坑的字段设计水表档案表water_meterCREATE TABLE water_meter ( id BIGINT PRIMARY KEY AUTO_INCREMENT, meter_no VARCHAR(50) NOT NULL UNIQUE COMMENT 水表编号, user_id BIGINT NOT NULL COMMENT 用水户ID, area_id BIGINT COMMENT 片区ID, meter_type TINYINT COMMENT 表类型 1人工 2远传, spec VARCHAR(50) COMMENT 水表口径规格, install_address VARCHAR(255) COMMENT 安装地址, init_reading DECIMAL(12,2) DEFAULT 0.00 COMMENT 初次安装读数起始表底, max_reading DECIMAL(12,2) COMMENT 表量程上限防止录入异常读数, status TINYINT COMMENT 状态 1正常 2停用 3报废, create_time DATETIME );这里有个容易被忽略的点init_reading初始读数一定要保留。换表、新装表的时候这个值就是计费起点没有它第一期的用水量根本算不出来。抄表记录表meter_readingCREATE TABLE meter_reading ( id BIGINT PRIMARY KEY AUTO_INCREMENT, meter_id BIGINT NOT NULL, meter_no VARCHAR(50) NOT NULL, read_value DECIMAL(12,2) NOT NULL COMMENT 本次读数, last_value DECIMAL(12,2) COMMENT 上次读数, usage_amount DECIMAL(12,2) COMMENT 本次用量, reading_date DATETIME NOT NULL COMMENT 抄表时间, reader_id BIGINT COMMENT 抄表员ID, source TINYINT COMMENT 数据来源 1人工 2远传 3系统生成, status TINYINT COMMENT 状态 1正常 2异常待审核, remark VARCHAR(255) COMMENT 备注异常原因填这里 );抄表记录的核心是read_value和last_value。用量计算公式是usage_amount read_value - last_value。所以插入一条新抄表记录时必须带上上一次的读数否则账单那边没法算。我实际做的时候是在Service层查当前水表的最新一条抄表记录作为last_value不依赖前端传过来的值避免脏数据。账单表water_billCREATE TABLE water_bill ( id BIGINT PRIMARY KEY AUTO_INCREMENT, bill_no VARCHAR(50) NOT NULL UNIQUE COMMENT 账单编号, user_id BIGINT NOT NULL, meter_id BIGINT NOT NULL, period VARCHAR(20) NOT NULL COMMENT 账期如2025-01, usage_amount DECIMAL(12,2) NOT NULL COMMENT 本期用水量, total_amount DECIMAL(12,2) NOT NULL COMMENT 应缴金额, status TINYINT COMMENT 状态 1未缴 2已缴 3已作废, due_date DATETIME COMMENT 缴费截止日期, pay_time DATETIME COMMENT 实际缴费时间 );账单表有个必须加的逻辑同一块表、同一个账期不能生成两条账单。这个约束可以在Service层判断也可以在表上加联合唯一索引(meter_id, period)双保险。2.3 初始化数据的准备我建库脚本里除了建表语句还放了初始化管理员账号、角色、菜单以及基础数据字典。这一步千万别省。没有角色和菜单数据前端登录进去也是个空壳你需要的数据字典包括水表类型、抄表状态、账单状态等。初始化数据写法和测试代码不一样要保证SQL幂等能执行多次而不会因为重复数据报错。常用的做法是插入语句前判断表里是否已经有数据比如INSERT INTO sys_role (role_code, role_name) SELECT ADMIN, 系统管理员 WHERE NOT EXISTS (SELECT 1 FROM sys_role WHERE role_code ADMIN);后来我在部署文档里特意标注了一个坑如果数据库名称不是water_system或者MySQL字符集配置不是utf8mb4初始化脚本里的中文数据会变成乱码。所以建库语句要显式指定字符集CREATE DATABASE IF NOT EXISTS water_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;3. 核心业务代码讲解抄表计费到底怎么实现3.1 抄表与用水量计算抄表这个功能表面上看就是往meter_reading表插一条记录但实际代码里有几个细节处理很关键。第一步查询数据时校验水表状态。水表被停用了或者不存在就不能录抄表数据不然会出现莫名其妙的账单。第二步拿最新的抄表记录当last_value。第三步计算用量并判断异常。Service public class MeterReadingService { Autowired private WaterMeterMapper waterMeterMapper; Autowired private MeterReadingMapper meterReadingMapper; Transactional(rollbackFor Exception.class) public MeterReading addReading(MeterReadingDTO dto) { WaterMeter meter waterMeterMapper.selectById(dto.getMeterId()); if (meter null) { throw new ServiceException(水表不存在); } if (meter.getStatus() ! 1) { throw new ServiceException(水表当前状态不可抄表); } // 查询当前水表最近一次抄表记录作为上次读数 MeterReading last meterReadingMapper.selectLatestByMeterId(dto.getMeterId()); BigDecimal lastValue (last null) ? meter.getInitReading() : last.getReadValue(); BigDecimal currentValue dto.getReadValue(); BigDecimal usage currentValue.subtract(lastValue); if (usage.compareTo(BigDecimal.ZERO) 0) { throw new ServiceException(本次读数小于上次读数请确认后重新录入); } MeterReading reading new MeterReading(); reading.setMeterId(meter.getId()); reading.setMeterNo(meter.getMeterNo()); reading.setReadValue(currentValue); reading.setLastValue(lastValue); reading.setUsageAmount(usage); reading.setReadingDate(new Date()); reading.setReaderId(dto.getReaderId()); reading.setSource(dto.getSource()); reading.setStatus(1); meterReadingMapper.insert(reading); return reading; } }注意Transactional(rollbackFor Exception.class)这个注解保证抄表记录插入失败时不留下半截数据。rollbackFor Exception.class要把异常类型指定全否则某些运行时异常不一定触发回滚。3.2 阶梯水价与账单自动生成水费计费是整个系统里最有“业务含量”的部分。居民用水一般实行阶梯水价比如第一阶梯每吨3.0元、第二阶梯4.5元、第三阶梯6.0元。账单模块要把抄表记录取出来按照阶梯规则分段计算金额。阶梯计费的核心逻辑是这样的假设月用水量是40吨第一阶梯30吨按3.0元算剩下10吨按4.5元算总金额 30×3.0 10×4.5 135元。看起来简单但写代码的时候要注意计费规则不是写死在代码里的而是从charge_rule表里读取的方便运营人员调整价格。Component public class TierPriceCalculator { /** * tiers 例如 [{max:30, price:3.0}, {max:60, price:4.5}, {max:null, price:6.0}] */ public BigDecimal calculate(BigDecimal usage, ListChargeTier tiers) { if (usage null || usage.compareTo(BigDecimal.ZERO) 0) { return BigDecimal.ZERO; } BigDecimal total BigDecimal.ZERO; BigDecimal remaining usage; BigDecimal previousLimit BigDecimal.ZERO; for (ChargeTier tier : tiers) { if (remaining.compareTo(BigDecimal.ZERO) 0) { break; } BigDecimal tierUnit tier.getMax() null ? remaining : tier.getMax().subtract(previousLimit).min(remaining); total total.add(tierUnit.multiply(tier.getPrice())); remaining remaining.subtract(tierUnit); previousLimit tier.getMax() null ? remaining : tier.getMax(); } return total; } }账单生成是典型的“批处理”场景。每月账期到了以后系统要把所有已抄表水表的数据捞出来计算水费再写入water_bill表。生成账单的时间点很讲究我建议不要放在用户查询时才动态计算而是由定时任务自动生成。SpringBoot里用Scheduled定时注解很方便Scheduled(cron 0 30 2 1 * ?) // 每月1号凌晨2点半跑前一个月的账单 public void generateMonthlyBill() { // 1. 查询所有有效水表 // 2. 查出每块表上一个账期的抄表记录 // 3. 调用价格计算器算金额 // 4. 批量插入账单表 }定时任务里有几个细节必须处理不然上线会出大问题幂等生成前先检查该账期是否已经生成过账单如果已经存在跳过或者给出提示。分批水表数量多的时候别一次性全捞用MyBatis-Plus分页插件分段处理。异常记录某块表数据有问题不应该中断整个批处理要catch异常单独标记回头人工处理。3.3 缴费与对账逻辑缴费是营业员在前端录一笔实收金额系统更新账单状态为“已缴”。这块的逻辑不复杂但有个并发问题值得提两个人同时给同一张账单缴费会出现重复支付。解决办法是在更新SQL里带状态条件Update(UPDATE water_bill SET status 2, pay_time NOW(), update_by #{operator} WHERE id #{billId} AND status 1) int markPaid(Long billId, String operator);这里AND status 1是关键。如果返回更新行数为0说明账单已经被操作过或者状态不对Service层直接抛异常提示“账单状态异常请刷新后重试”。这个技巧在处理订单、余额扣减等业务里通用属于数据库层面的乐观锁思想。4. 登录认证与权限控制JWT怎么落进SpringBoot4.1 JWT认证流程登录认证我用的是JWTJSON Web Token。流程很简单用户提交用户名和密码后端校验通过后生成一个包含用户ID、用户名、角色信息的Token返回给前端。前端把Token存在localStorage里每次请求在HTTP头里带上Authorization: Bearer token。后端拦截器拿到Token后解析校验签名和过期时间然后放行请求。Component public class JwtAuthenticationInterceptor implements HandlerInterceptor { Autowired private JwtUtil jwtUtil; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); } if (StringUtils.isBlank(token) || !jwtUtil.validateToken(token)) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\登录已过期请重新登录\}); return false; } // 解析出用户信息放入ThreadLocal供后续业务获取当前用户 LoginUser loginUser jwtUtil.parseToken(token); UserContext.set(loginUser); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); } }UserContext用ThreadLocal保存当前登录用户好处是Service层不需要到处传用户ID参数直接UserContext.get().getId()就拿到了。注意必须在请求结束后清理ThreadLocal否则Tomcat线程复用的时候数据串了。JWT本身是URL安全的但如果前端拿Token的方式不对后端做得再多也没用。我在文档里专门强调前端用Axios拦截器统一添加请求头别在业务代码里重复写。4.2 角色权限怎么控制我的权限设计是经典的RBAC用户表、角色表、菜单表、用户角色关联表、角色菜单关联表五张表。用户登录后通过用户ID查出角色再根据角色查出菜单和接口权限标识。SpringBoot里做接口权限控制我用了简单实用的一招自定义注解RequirePermission(system:user:add)标注在Controller方法上拦截器里判断当前用户是否具备该权限。Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequirePermission { String value(); }拦截器里解析注解的逻辑Method method ((HandlerMethod) handler).getMethod(); RequirePermission rp method.getAnnotation(RequirePermission.class); if (rp ! null) { // 判断当前用户权限集合是否包含该标识 if (!loginUser.getPermissions().contains(rp.value())) { response.setStatus(403); return false; } }用注解的好处是一旦权限含义变化只需要改注解值不需要动业务方法逻辑。数据库里维护一张菜单权限表权限标识就存在菜单表的perms字段比如system:user:add表示系统管理模块的新增用户权限。前端拿到菜单树后还能根据权限标识控制页面按钮显示这样后端防御加前端配合权限才算闭环。4.3 接口数据权限除了操作权限数据权限也很重要。典型场景抄表员登录后应该只能看到自己负责片区的水表和抄表任务不能看到别的地方的数据。我的方案是在水表表上增加area_id字段抄表员创建时绑定负责的片区。查询抄表任务时Service层根据当前登录人的角色去拼条件管理员不过滤抄表员只查area_id in (自己的片区)。这个逻辑不适合写在SQL里因为不同角色条件不同放在Service层用if判断最清楚。当然如果系统复杂到要支持多租户、数据权限规则动态配置可以考虑引入MyBatis拦截器做自动过滤这个属于进阶中小型系统用Service层判断就够了。5. 部署实战指南从本地到服务器一台搞定5.1 环境准备与配置修改部署这块最容易出问题我踩过的坑写出来给大家避雷。先给出一份环境清单JDK 1.8或11别用最新17/21除非你确信代码兼容Maven 3.6MySQL 5.7或8.0Redis 6.xNginx部署前端用服务器Linux配置2核4G就够跑演示环境了后端配置文件的重点在application.ymlspring: datasource: url: jdbc:mysql://localhost:3306/water_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 你自己的密码 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 password: 如果有就填 server: port: 8080serverTimezoneAsia/Shanghai这个参数特别重要不加它MySQL和Java的时区不一致日期数据会差8个小时。useSSLfalse是为了避免本地测试SSL握手报错线上生产如果是内网传输也不需要SSL。5.2 后端多环境配置与打包我习惯做三套配置application.yml公共配置、application-dev.yml开发环境、application-prod.yml生产环境。公共配置放不变的参数比如应用名、MyBatis配置环境配置放数据库地址、Redis地址这些会变的内容。application.yml里不需要指定当前激活哪个环境打包时用Maven参数动态指定# 打包生产环境 mvn clean package -DskipTests -Dspring.profiles.activeprod # 启动时指定环境如果skip了参数 java -jar water-system.jar --spring.profiles.activeprod这里推荐一个习惯打包前先把数据库密码、Redis密码这些敏感信息确认好不要在代码里写死用启动参数覆盖。比如java -jar water-system.jar \ --spring.profiles.activeprod \ --spring.datasource.password实际密码 \ --spring.redis.password实际密码这样即使代码被翻出来生产密码也不会跟着泄露。另外IDEA里调试时如果想临时改端口不用去改配置文件。在Run Configuration的VM options填-Dserver.port8081或者在Program arguments填--server.port8081两者效果一样但Program arguments的优先级更高因为它是SpringApplication从命令行读取的参数。5.3 Docker部署与Nginx反向代理服务器上提前装好Redis和MySQL之后写一个Dockerfile把包塞进去。FROM openjdk:8-jre WORKDIR /app COPY target/water-system.jar app.jar ENV TZAsia/Shanghai EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar, --spring.profiles.activeprod]构建和启动docker build -t water-system:v1.0 . docker run -d --name water-app \ -p 8080:8080 \ -e SPRING_DATASOURCE_PASSWORD实际密码 \ water-system:v1.0前端Vue项目打包后是一堆静态文件用Nginx托管同时做API反向代理解决跨域问题server { listen 80; server_name water.example.com; 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; } location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } }这里最关键的坑是proxy_pass http://127.0.0.1:8080/这行末尾的斜杠不能丢。带了斜杠Nginx会把/api/user/list去掉/api前缀后转发成/user/list不带斜杠转发过去是/api/user/list会直接404。这是最容易出错的Nginx配置细节之一。前端请求接口时要统一写/api/user/list而不是写全地址http://localhost:8080/user/list这样测试环境和生产环境Nginx配置不用改前端代码部署灵活。Vue打包后如果接口地址写死了每次换环境都要重新打包这是很多新手没注意的地方。6. 源码文档与代码讲解拿到一套源码该怎么看6.1 源码结构速览如果读者拿到的是我整理的那套源码压缩包打开之后第一眼看到的是这些目录water-system/ ├── pom.xml ├── sql/ │ └── init.sql # 建库建表初始化数据 ├── src/main/java/com/water/ └── src/main/resources/ ├── application.yml ├── application-dev.yml ├── application-prod.yml ├── mapper/ # MyBatis XML映射文件 └── banner.txt看源码不要乱翻最科学的顺序是先看pom.xml了解依赖再看application.yml了解配置然后看sql/init.sql了解表结构最后按Controller→Service→Mapper的正向链路阅读业务代码。这样走一遍基本就能把项目“跑”进脑子里了。代码讲解文档里我写了一段经验看Controller重点看URL设计和参数接收方式看Service重点看事务和业务规则看Mapper重点看SQL和查询条件。每一层关注点不同不要从Entity开始啃容易被细枝末节带偏。6.2 阅读代码的正确顺序以这个水务系统为例我的建议阅读顺序是这样的WaterApplication.java看启动类理解扫描路径和启动方式。Controller层打开MeterController看它暴露了哪些接口每个接口对应什么URL和参数。Service层打开MeterReadingService看抄表和计费逻辑。Mapper层打开MeterReadingMapper.xml看SQL怎么写的有没有动态条件。对应到项目的“讲解”文档我会给每个核心类配一段说明文字解释三个问题它做什么、为什么这么写、可以怎么改进。比如抄表接口为什么参数要用DTO而不是直接接收散参说明原因是为了接口扩展性——以后抄表可能要传照片、传GPS坐标如果直接散参就不好加了。6.3 配套文档怎么用我打包时把文档分成三类README、部署文档、代码讲解文档。README介绍项目概况和启动步骤部署文档是给运维或者接手的同学看的代码讲解文档面向正在学习SpringBoot的人。如果你是自己做毕设建议把部署文档的每一步都实际跑一遍不要把“运行成功”寄托在别人的机器上。本地、服务器两套环境我都提供示例配置。按我的经验90%的SpringBoot项目部署失败都发生在数据库连接串错误、Redis未启动、端口冲突、前端Nginx代理配置少斜杠。把这些提前写好文档能节省大量时间。7. 常见问题与排查技巧实录7.1 典型问题速查表这部分内容我在部署文档里反复检查过直接给一张排查速查表。如果你按步骤操作遇到问题对着这个表自查一遍比网上乱搜索管用。现象可能原因排查与解决启动报Port 8080 was already in use端口被占用执行netstat -ano提示数据库连接失败账号密码错误、数据库未创建先确认water_system库存在再用命令行连一下测试中文乱码数据库字符集不对或连接串没配字符集连接串加characterEncodingutf8建库时确保utf8mb4登录失败但日志无异常Redis启动失败检查Redis是否运行redis-cli ping返回PONG才行前端页面打开黑屏或404Nginx配置不对或Vue打包路径问题检查index.html能否直接访问确认try_files配置接口返回404后端能启动但请求没到达看控制台有没有对应请求的Mapping日志确认路径和参数7.2 几个特别容易踩的坑这几个坑是我实际测试这套系统时花时间最多的点写出来当提醒。坑一SpringBoot 3.x带来的依赖兼容问题。我用的是SpringBoot 2.7.18所有依赖都是稳定兼容的。如果你用3.x会发现很多老教程的配置写法不适用比如javax.servlet变成jakarta.servlet了。所以网上查资料时先看发布时间3.x相关的资料很多是从英文社区转来的中文资源相对少。坑二JWT过期时间设置不合理。我把Token过期时间设成了24小时结果测试时频繁因为改代码重启导致重新登录。调试阶段建议把过期时间放大一点比如7天。上线前再改回24小时或者更短加上Redis里存一份可刷新机制这样用户使用的体验更好。坑三跨域配置不生效。开发调试时前端在localhost:5173后端在8080跨域是必须解决的。在SpringBoot里写一个CorsFilter配置类很多教程里写的是WebMvcConfigurer的addCorsMappings但拦截器拦截之后跨域配置就不生效了需要在拦截器里放行OPTIONS预检请求。我在拦截器里第一行就加了if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; }这个判断对前后端分离项目特别重要忘了它每一次带Token的请求都会因为预检失败而被拦截。坑四MyBatis-Plus分页不生效。一定记得配置分页拦截器PaginationInnerInterceptor。身边好几个人问我为什么分页查询返回全部数据原因都是注册插件这一步忘了。MyBatis-Plus的分页插件不会自动注册需要手动加这个配置类。Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); interceptor.addInnerInterceptor(pagination); return interceptor; }坑五Linux上时区差了8小时。我在系统里用LocalDateTime存储时间部署到Linux服务器后发现数据库里的时间和服务器时间对不上。这是因为JVM默认读取系统时区服务器没设置时区的话会取UTC。解决办法是启动时加-Duser.timezoneAsia/Shanghai或者在Dockerfile里加ENV TZAsia/Shanghai再配合MySQL连接串的serverTimezoneAsia/Shanghai三处保持一致就稳了。这些坑都写进了部署文档的FAQ部分。把部署文档写得像“排雷手册”是我整理这类项目的一个习惯。毕竟文档的意义不止是让人跑通一个demo更重要的是让接手的人遇到问题时能自己靠自己解决问题。我从一开始做这套系统就坚持把所有文档和源码放在一起每次修改代码的同时更新对应说明。这其实是个朴素的工作习惯项目做完了配套资料也顺手做完了不会出现“代码写好了但看不懂、跑不起来”的情况。打包这个项目的时候我把部署文档、README和源码结构说明统一放在压缩包里就是希望拿到它的人能把它当作一个可以真正落地的东西而不是一份只存在于评分的演示。
返回列表