ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于SpringBoot的宠物成长监管系统开发实战与踩坑全解析

基于SpringBoot的宠物成长监管系统开发实战与踩坑全解析 “SpringBoot版本太高踩坑”“SpringBoot自动装配原理”——我最近在带学生做项目时发现不少人的毕业设计选题都绕不开“宠物成长监管系统”这类题目。说实话这个题目比传统的“XX管理系统”有意思得多它既有业务深度又有技术广度能把一个开发者对SpringBoot生态的理解完整地暴露出来。这篇文章我就以自己的实际开发经历为蓝本把从0到1搭一套基于SpringBoot的宠物成长监管系统的完整思路、核心实现和踩坑记录都写出来希望能给正在做类似项目的人一些参考。1. 功能定位与总体架构设计1.1 核心需求拆解这个系统到底在管什么宠物成长监管系统重点不在“监管”字面的监视和控制而是在“记录跟踪提醒”。换句话说它要解决的是宠物主人最关心的几个问题我的宠物现在长得多大了、发育得正常不正常、上次打疫苗是什么时候、该不该驱虫了、吃的喝的有没有规律。围绕这些诉求我把它拆成五个核心模块宠物档案管理录入宠物基础信息品种、性别、出生日期、绝育状态等支持一个用户养多只宠物相当于一个“宠物户口本”。成长记录管理定期记录宠物的体重、体长、胸围等身体指标附带照片和文字备注形成一条时间轴式的成长日记。健康管理疫苗记录、驱虫记录、就诊记录每一项都要有执行时间和下次到期时间这是整个系统的核心业务锚点。提醒与预警基于健康管理数据系统定时扫描即将到期或已过期的疫苗、驱虫事项主动推送给主人。数据可视化把成长记录转化为折线图、体重增长曲线和大规模样本的平均值做对比这里可以用简化的参考区间直观展示宠物的生长趋势是否正常。从技术角度上看这个系统本质上是一个“数据采集→数据存储→业务计算→可视化呈现”的闭环。SpringBoot在这里扮演的角色是业务后端和接口中枢所有模块的功能都通过RESTful API暴露给前端Vue或小程序。1.2 为什么选SpringBoot做核心框架这个问题在我带的几个项目里几乎每次都会被问到。选SpringBoot而不是Spring MVC XML配置倒不是因为“大家都用所以我也用”而是因为它真的适合这种中型的业务系统开发。第一自动装配极大降低了配置成本。数据库连接、Redis、定时任务这些组件在传统Spring项目里配一个数据源要写上百行XML而SpringBoot用spring-boot-starter-data-jpa或mybatis-spring-boot-starter就能通过约定大于配置的方式快速集成。开发人员可以把精力集中在业务逻辑上。第二生态成熟踩坑资料丰富。宠物监管系统涉及的Web层、持久层、缓存、权限、定时任务、文件上传每一个环节SpringBoot都有对应的Starter和成熟方案。遇到问题一搜解决方案基本都有对新手非常友好。第三内置Tomcat、打包即运行的方式让部署变得极简。用Maven打个jar包传到服务器上java -jar就能跑不需要额外装Tomcat。在Docker部署场景下也只需要一个基础镜像加上jar文件就能构建。当然SpringBoot也不是没有缺点版本迭代太快本身就是一个大坑——这点我会在后面的常见问题里专门展开讲。1.3 总体技术选型与架构分层这套系统我采用的是经典的前后端分离架构后端用SpringBoot做纯API服务前端用Vue3 Element Plus做管理界面部署时后端jar包和前端dist目录分开。层级技术选型说明前端Vue3 Axios ECharts负责页面渲染和数据可视化接口层SpringBoot RESTful API统一返回Result对象业务层Service ServiceImpl业务逻辑和事务控制持久层MyBatis PlusCRUD和条件构造器数据库MySQL 8.0存储业务数据缓存Redis用户会话和热点数据认证Spring Security JWT无状态登录认证定时任务Spring Task / Quartz扫描提醒事项分层上我沿用了最标准的Controller-Service-Mapper三层结构。有些同学会问我为什么不加一层Manager/Facade我的观点是这个项目的数据量级和复杂度还没到非要再抽一层的程度过度设计反而是伤害。等业务逻辑真到了某个Service过于臃肿的时候再按领域拆分不迟。2. 数据库设计与核心表结构2.1 业务实体的分类与关系梳理数据库设计是整个项目的地基地基打不好后面写代码全是补丁。我当时做设计的时候把业务实体分成三类基础类用户user、宠物pet。用户和宠物是一对多的关系因为一个主人可以养猫养狗养兔子。宠物需要冗余存储主人的user_id这个没什么可说的。 记录类成长记录growth_record、疫苗记录vaccine_record、驱虫记录deworm_record、喂养记录feed_record。所有记录都归属于某一只宠物通过pet_id关联。 设定类提醒配置reminder_config。它记录类型疫苗或驱虫、项目名称、间隔周期用于生成下次提醒时间。这样设计的核心思想是宠物是业务主体记录全部挂在宠物下面用户只作为数据的拥有者存在。系统里没有任何一张表直接“属于”用户所有查询都通过宠物表逐级带出。这样做的好处是数据隔离天然清晰后续如果有权限扩展只需在pet表上增加owner校验其他表都不需要改动。2.2 核心字段设计与类型选择这里我直接给出几张核心表的建表语句和字段说明重点讲几个容易踩坑的字段设计。用户表CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT COMMENT 用户ID, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT BCrypt加密后的密码, nickname varchar(50) DEFAULT NULL COMMENT 昵称, avatar varchar(255) DEFAULT NULL COMMENT 头像URL, phone varchar(20) DEFAULT NULL COMMENT 手机号, email varchar(100) DEFAULT NULL COMMENT 邮箱, status tinyint NOT NULL DEFAULT 1 COMMENT 1-正常 0-禁用, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB COMMENT用户表;宠物表CREATE TABLE pet ( id bigint NOT NULL AUTO_INCREMENT COMMENT 宠物ID, user_id bigint NOT NULL COMMENT 所属用户ID, name varchar(50) NOT NULL COMMENT 宠物昵称, species varchar(20) NOT NULL COMMENT 物种CAT/DOG/RABBIT等, breed varchar(50) DEFAULT NULL COMMENT 品种, gender tinyint NOT NULL COMMENT 0-未知 1-公 2-母, birth_date date DEFAULT NULL COMMENT 出生日期, adoption_date date DEFAULT NULL COMMENT 领养日期, sterilized tinyint NOT NULL DEFAULT 0 COMMENT 0-未绝育 1-已绝育, avatar varchar(255) DEFAULT NULL COMMENT 宠物头像URL, weight decimal(5,2) DEFAULT NULL COMMENT 当前体重kg, status tinyint NOT NULL DEFAULT 1 COMMENT 1-正常 0-已离世/送养, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id) ) ENGINEInnoDB COMMENT宠物信息表;成长记录表CREATE TABLE growth_record ( id bigint NOT NULL AUTO_INCREMENT, pet_id bigint NOT NULL COMMENT 宠物ID, record_date date NOT NULL COMMENT 记录日期, weight decimal(5,2) NOT NULL COMMENT 体重kg, body_length decimal(5,2) DEFAULT NULL COMMENT 体长cm, chest_circumference decimal(5,2) DEFAULT NULL COMMENT 胸围cm, photo_url varchar(255) DEFAULT NULL COMMENT 照片URL, remark varchar(500) DEFAULT NULL COMMENT 备注, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_pet_id_date (pet_id, record_date) ) ENGINEInnoDB COMMENT成长记录表;字段类型选择上我必须提醒一句体重的decimal精度不要拍脑袋写。我见过有人用decimal(10,2)这当然不会报错但会让人误以为这个字段存的是几百公斤的东西。宠物体重的合理范围是0.1kg到100kgdecimal(5,2)完全够用既限制了非法数据的输入又优化了存储空间。2.3 提醒表的设计要点与状态流转提醒是整个系统的灵魂功能它的表设计比看起来要复杂一些。如果你只是简单地存一个“下次提醒时间”然后定时任务扫一遍就发提醒那系统第一次运行没问题但第二次跑的时候会重复提醒。所以我引入了提醒状态机的概念。我设计的提醒表CREATE TABLE reminder ( id bigint NOT NULL AUTO_INCREMENT, pet_id bigint NOT NULL, type varchar(20) NOT NULL COMMENT VACCINE/DEWORM/MEDICAL, item_name varchar(100) NOT NULL COMMENT 项目名称, last_execute_date date DEFAULT NULL COMMENT 上次执行日期, next_execute_date date NOT NULL COMMENT 下次执行日期, status tinyint NOT NULL DEFAULT 0 COMMENT 0-待提醒 1-已提醒 2-已完成 3-已忽略, execute_cycle_days int NOT NULL COMMENT 执行周期天, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_next_date_status (next_execute_date, status) ) ENGINEInnoDB COMMENT提醒表;状态流转规则如下定时任务每天扫描next_execute_date 当前日期 且 status 0 的记录把status改为1已提醒同时推送通知。用户看到提醒后去接种疫苗或驱虫系统根据新的last_execute_date加上execute_cycle_days生成下一条提醒记录status为0。如果用户选择“忽略本次提醒”则status变为3不再扫描。这个设计的好处是提醒记录不是一条“死数据”而是一个有生命周期的“状态机”每次执行疫苗或驱虫动作都会产生一条新的、待触发的提醒记录。这样系统的提醒逻辑始终是干净、可追溯的。3. 认证授权与接口安全设计3.1 JWT无状态认证的实现思路宠物监管系统的前端是SPA应用天然适合JWT这种无状态认证方案。用户登录成功后后端签发一个JWT令牌前端存储后在请求头Authorization里带回来后端通过拦截器或Spring Security过滤器链校验令牌并解析用户信息。这里还需要注意一个问题就是密钥和过期时间。开发环境你可以随便填一个secret但生产环境一定要用环境变量注入不要把密钥硬编码进代码里提交到Git仓库——这是最低级的安全事故。我一般用如下配置jwt: secret: ${JWT_SECRET:your-default-dev-secret-key-please-change} expire-time: 86400000 # 24小时单位毫秒过期时间我建议短期令牌用2小时长期令牌用7天同时配合Redis做“在线状态”管理。JWT本身的特性决定了它无法在服务端主动失效但如果你在Redis里存一个token黑名单登出的时候把token加进去就可以实现“伪注销”了。这个方案虽然牺牲了一点点无状态性但却是实践中体验最好的折中方案。3.2 Spring Security核心过滤器链的配置这一节我直接给出我在项目中使用的Spring Security配置核心部分并对关键点做解释。Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeHttpRequests(auth - auth .requestMatchers(/api/auth/**, /api/pet/**).permitAll() .requestMatchers(HttpMethod.OPTIONS, /**).permitAll() .anyRequest().authenticated() ) .exceptionHandling() .authenticationEntryPoint(restAuthenticationEntryPoint()) .and() .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } }简单解释几个容易混淆的点csrf().disable()SPA前后端分离项目中CSRF防护主要针对Cookie会话JWT模式下请求头携带tokenCSRF攻击面已经大幅缩小所以禁用是合理的。requestMatchers(/api/pet/**).permitAll()宠物档案接口在部分场景下需要匿名访问例如生成分享链接但我并不太推荐对所有宠物接口都放行更合理的做法是只放行/api/pet/share/{id}这一个匿名接口。addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)这一步是核心。JWT过滤器必须在Spring Security内部的认证过滤器之前执行把解析出来的用户信息封装成Authentication对象塞进SecurityContext后续的权限判断才能正常工作。3.3 接口权限控制粒度与角色设计宠物监管系统虽然表面上是个人工具但为了完整性我建议引入“普通用户”和“管理员”两种角色。管理员可以查看系统运行数据、管理所有用户和宠物账号普通用户只能操作自己的数据。PreAuthorize(hasRole(ADMIN)) GetMapping(/admin/users) public ResultListUserVO listUsers() { // 管理员才能访问的接口 }普通用户的接口权限用资源归属校验实现GetMapping(/pet/{petId}/growth-records) public ResultListGrowthRecordVO listGrowthRecords(PathVariable Long petId) { Pet pet petService.getById(petId); if (!pet.getUserId().equals(currentUserId())) { throw new ApiException(403, 无权访问该宠物数据); } // ... }这个小片段是我在项目中反复使用的“资源归属校验”模式——不用等数据返回再过滤而是在查询前就校验用户对资源的拥有权。这样做既保护了数据安全又避免了无谓的数据库查询是一个非常值得记住的习惯。4. 成长记录与健康管理核心功能实现4.1 成长记录的上传与处理流程成长记录里最麻烦的不是数字字段的存储而是照片上传。用户每一条记录可能附带1到3张照片这些照片要处理上传、存储路径、访问权限、缩略图生成等多个环节。我在项目里用的是本地存储方案开发环境 MinIO生产环境。原因很简单本地存储适合快速验证业务逻辑MinIO支持S3协议后续如果想迁移到云存储服务器代码改动几乎为零。先看文件上传的ControllerPostMapping(/api/file/upload) public ResultString upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { throw new ApiException(400, 上传文件不能为空); } String originalFilename file.getOriginalFilename(); String ext StringUtils.getFilenameExtension(originalFilename); if (!Arrays.asList(jpg, jpeg, png, gif, webp).contains(ext.toLowerCase())) { throw new ApiException(400, 不支持的图片格式); } long maxSize 5 * 1024 * 1024; // 5MB if (file.getSize() maxSize) { throw new ApiException(400, 图片大小不能超过5MB); } String url fileStorageService.store(file); return Result.success(url); }fileStorageService是一个接口我提供了两个实现类LocalFileStorageServiceImpl和MinioFileStorageServiceImpl。通过ConditionalOnProperty注解决定哪个生效Service ConditionalOnProperty(name file.storage.type, havingValue local) public class LocalFileStorageServiceImpl implements FileStorageService { // ... }ConditionalOnProperty这个注解是SpringBoot自动配置体系里的一个利器它可以根据配置文件动态决定哪个Bean被加载。很多同学写代码时只知道用Service遇到这种“多实现切换”的需求就靠注释和改代码其实SpringBoot早就把方案给你准备好了。4.2 疫苗与驱虫记录的关联操作疫苗和驱虫的核心逻辑在于“记录一条执行记录”的同时要“生成下一条提醒”。这个操作必须在一个事务里完成否则就会出现“提醒表更新了但接种记录没写入”的数据不一致问题。Transactional(rollbackFor Exception.class) public void addVaccineRecord(VaccineRecordDTO dto) { // 1. 保存疫苗执行记录 VaccineRecord record new VaccineRecord(); BeanUtils.copyProperties(dto, record); vaccineRecordMapper.insert(record); // 2. 更新宠物表的疫苗状态可选 // 3. 生成下一条提醒 Reminder reminder new Reminder(); reminder.setPetId(dto.getPetId()); reminder.setType(VACCINE); reminder.setItemName(dto.getVaccineName()); reminder.setLastExecuteDate(dto.getExecuteDate()); reminder.setNextExecuteDate(dto.getExecuteDate().plusDays(dto.getIntervalDays())); reminder.setStatus(0); reminderMapper.insert(reminder); }Transactional默认只对RuntimeException回滚。如果业务方法里抛的是受检异常Exception事务不会自动回滚这就是很多事务“失效”的根源。我在项目里统一使用rollbackFor Exception.class避免了这个坑。另外事务方法不要同类内部调用否则代理失效也会导致事务不生效这属于Spring AOP的经典问题。4.3 接口返回体设计与前端联调的约定接口返回体的统一性是前后端联调是否顺畅的关键。我见过很多半路交接的项目每个接口返回的数据结构都不一样前端调用的时候各种if else极其痛苦。我项目里统一了这样的返回结构{ code: 200, message: success, data: {} }对应的Java类public class ResultT { private int code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(int code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }这个类虽然简单但它是前后端之间最重要的协议约定。我建议在一开始设计系统时就把这个类定义好并且在全局异常处理器里统一封装错误信息这样前端只需要在Axios的响应拦截器里写一次对code的判断逻辑即可。5. 定时提醒与数据可视化实现5.1 定时任务扫描提醒的两种方案对比宠物监管系统最“活”的功能就是定时提醒它决定了这个系统是“数据记录工具”还是“有粘性的服务”。SpringBoot里实现定时任务有两种主流方案Spring自带的Scheduled注解和Quartz框架。我对它们的选型建议是单机、任务简单用Scheduled需要集群部署、分布式任务调度用Quartz或XXL-Job。Scheduled的用法非常简单Component public class ReminderScanTask { Scheduled(cron 0 0 8 * * ?) public void scanAndNotify() { ListReminder pendingReminders reminderMapper.selectPendingReminders(LocalDate.now()); for (Reminder reminder : pendingReminders) { notifyService.notifyOwner(reminder); reminder.setStatus(1); reminderMapper.updateById(reminder); } } }Cron表达式0 0 8 * * ?表示每天早上8点执行一次。这个时间点是经过考量的——早上8点用户刚起床刷手机的频率最高提醒的触达效果最好。这里有个优化建议不要把提醒推送逻辑写在定时任务里而是把定时任务做得“轻”。具体做法是扫描出待提醒列表后把提醒事件发送到消息队列或直接调用通知服务通知服务再通过邮件、短信、微信模板消息等方式触达用户。这样即使通知服务挂了也不会影响定时的扫描主流程。还有一点要留意单机部署时Scheduled是每个实例都会执行一遍的。如果你部署了两个实例做负载均衡同一个提醒会重复发两次。解决方法是加一个Redis分布式锁只有抢到锁的实例才执行扫描任务。Scheduled(cron 0 0 8 * * ?) public void scanAndNotify() { Boolean lock redisTemplate.opsForValue() .setIfAbsent(reminder:scan:lock, 1, Duration.ofMinutes(10)); if (Boolean.TRUE.equals(lock)) { try { // 执行扫描 } finally { redisTemplate.delete(reminder:scan:lock); } } }5.2 成长曲线图表的后端数据组装成长曲线图是系统最直观的展示层。前端用ECharts画折线图后端需要提供一个接口返回某只宠物所有成长记录的时间点和对应数值。我的接口设计如下GetMapping(/pet/{petId}/growth-trend) public ResultGrowthTrendVO growthTrend(PathVariable Long petId) { ListGrowthRecord records growthRecordMapper.selectByPetIdOrderByDate(petId); GrowthTrendVO vo new GrowthTrendVO(); vo.setDates(records.stream().map(r - r.getRecordDate().toString()).collect(Collectors.toList())); vo.setWeights(records.stream().map(GrowthRecord::getWeight).collect(Collectors.toList())); vo.setBodyLengths(records.stream().map(GrowthRecord::getBodyLength).collect(Collectors.toList())); return Result.success(vo); }前端拿到数据后直接喂给ECharts即可。但这里要注意当记录数量很大的时候比如超过100条全量返回会导致前端图表渲染卡顿。我的处理方案是在SQL层做抽稀如果记录大于100条则按时间区间等间隔取100个点。SELECT * FROM ( SELECT g.*, ROW_NUMBER() OVER (ORDER BY record_date) AS rn, COUNT(*) OVER () AS total_cnt FROM growth_record g WHERE pet_id #{petId} ) t WHERE t.rn % CEIL(t.total_cnt / 100.0) 0 ORDER BY record_date;这个方案用窗口函数在数据库层直接完成抽稀避免了大列表传输到前端再处理的内存浪费。MySQL 8.0以上版本才支持窗口函数如果你的MySQL版本低就只能用Java侧来抽了。5.3 体重标准参考区间的实现思路要让成长曲线“活”起来光画一条线不够最好能给宠物主人一个参考我的猫这个月龄体重应该多少才算正常。宠物标准体重的数据来源其实很难拿到权威的公开数据集但我们可以内置一套基于品种和月龄的简化参考区间。例如猫的体重参考月龄参考体重下限(kg)参考体重上限(kg)10.20.520.51.031.01.862.53.6123.05.0这些数据完全可以用一个配置表维护不在代码里硬编码。系统计算宠物当前月龄后比对配置表返回对应的参考区间。前端把参考区间画成阴影区域主人一眼就能看出自家宠物在哪个百分位。不过需要强调一点参考区间仅是参考不是医学诊断标准。真实项目里最好在界面上加一句声明避免用户因为数据偏差产生不必要的焦虑这也是对用户负责。6. 常见问题与排查技巧实录6.1 SpringBoot版本太高引发的连锁问题做这个项目时如果你直接选了SpringBoot 3.x JDK 21又没有注意新老API的变化会遇到不少莫名其妙的“怪问题”。这是很多人在用SpringBoot时最容易忽视的坑。SpringBoot 2.x和3.x的核心区别在于3.x基于Jakarta EE 9规范包名从javax.*迁移到了jakarta.*。如果项目里引用的第三方库还停留在旧规范上就会直接报ClassNotFoundException: javax.servlet.Filter。解决办法要么降低SpringBoot版本到2.7.x要么升级第三方库版本。我的建议是如果是为了做毕业设计或中小型项目在JDK环境允许的情况下优先使用SpringBoot 2.7.x因为它的社区资料最丰富各种兼容性问题基本都有了现成的解决方案模板。等到项目真正跑通后再考虑要不要升3.x。用新版本看起来很酷但如果遇到一个偏门问题没有人解答你的开发进度会直接卡死。6.2 MyBatis Plus条件构造器使用踩坑MyBatis Plus的LambdaQueryWrapper确实方便但它有个常见的坑多个条件之间用and连接没关系但如果要拼接“带括号的复杂条件”直接链式调用就会出错。比如要查询满足“今天到期或已经过期”且“状态为待提醒”的提醒项你可能会写成LambdaQueryWrapperReminder wrapper new LambdaQueryWrapper(); wrapper.eq(Reminder::getStatus, 0) .le(Reminder::getNextExecuteDate, LocalDate.now());这样写没问题因为eq和le之间本身就是and关系。但如果要查“A或B”且“C”就必须用and(condition - condition.le().or().le())这种方式显式声明括号。很多人在这里踩坑后还以为MyBatis Plus有bug其实是没理解SQL拼接的语义。wrapper.eq(Reminder::getStatus, 0) .and(w - w.le(Reminder::getNextExecuteDate, LocalDate.now()) .or() .eq(Reminder::getNextExecuteDate, today))6.3 时间字段格式化导致前端解析错误LocalDateTime在SpringBoot默认序列化成ISO格式的数组比如[2024, 12, 1, 14, 30, 0]前端直接拿去new Date()会得到一串无法解析的东西。这个问题在前后端联调时几乎必现。解决办法是在application.yml里配置统一的JSON序列化格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8但要注意date-format只对java.util.Date生效对LocalDateTime不生效。如果是Java 8时间类型需要自定义Jackson配置Configuration public class JacksonConfig { Bean public Jackson2ObjectMapperBuilderCustomizer customizer() { return builder - { builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); builder.deserializers(new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); }; } }6.4 跨域问题排查思路前后端分离项目里跨域是躲不开的。常见错误是后端只配了CrossOrigin注解在单个Controller上但前端请求的接口分散在各个Controller漏掉的就直接抛CORS错误。我建议用全局配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }如果同时引入Spring Security还要确保SecurityConfig里的http.cors()被启用否则Security过滤器链会先把OPTIONS请求拦下来CORS配置根本没机会生效。6.5 排查问题日志实用技巧最后分享一个排查问题的实用技巧在Service层的关键业务方法入口和出口打印日志用when:xxx method start, userId{}和when:xxx method end, cost{}ms这种固定格式。前期开发时觉得多余接入线上问题排查后就会发现这是救命稻草。我还习惯在全局异常处理器里把异常堆栈打到日志文件中并带上requestId关联上下文这样每次用户反馈“我的宠物数据哪去了”翻日志用requestId一搜整条链路就出来了。7. 后续扩展与项目经验总结7.1 功能上的三种扩展方向这套系统做完后如果你还想继续丰富它我根据自己的开发经验给你指三条路第一条路是物联网接入。给宠物佩戴智能项圈通过蓝牙或Wi-Fi上报心率、运动步数、睡眠时长等实时数据。后端增加数据接收接口用消息队列缓冲高频数据落库后做统计分析。这是宠物监管系统向智能硬件方向升级的必经之路。第二条路是社区化。增加宠物动态发布、互动评论、互助问答等功能把工具型产品变成社区型产品。这种扩展在技术上不复杂重点是内容审核和用户运营策略。第三条路是多端支持。目前做的是Web端可以扩展到微信小程序和移动App。小程序端的JWT认证和Web端略有不同需要处理wx.login的code2session逻辑但整体架构不需要推翻重来。7.2 从这套系统中沉淀下来的技术经验做这个系统的过程也是我对SpringBoot理解加深的过程。有几点体会我觉得挺有价值的分享给你一是自动装配是SpringBoot的灵魂。绝大多数“莫名其妙的问题”根源都在自动装配的Bean和你预期的不一致。排查思路是先确认Bean是否被加载、加载的是哪个实现类再深入到业务逻辑。SpringBoot的spring.factories和ConditionalOn*注解就是它的“安装说明书”。二是事务边界要分清。我在开发中犯过一次经典错误把文件上传后的缩略图生成逻辑和成长记录保存逻辑放在同一个事务里结果缩略图生成的Io操作太慢数据库连接被长时间占用最终导致连接池耗尽。后来把耗时操作移到事务外或在事务提交后通过事件监听器去执行问题立刻解决。记住事务里只放必须同步的原子操作异步、耗时的尽量踢出去。三是不要为了奇怪的设计过度使用设计模式。大多数业务系统用模板方法模式处理流程类业务、用策略模式处理不同品种的计算逻辑就够了。过度设计会让团队里其他人难以接手也让简单问题复杂化。四是一定要重视数据隔离和权限校验。宠物数据涉及用户隐私虽然功能上看起来只是个“记录工具”但接口如果随意允许跨用户访问那就是数据泄露事故。资源归属校验这个习惯越早养成越好。7.3 给正在选题或做类似项目的朋友的建议最后这个项目其实非常适合做成SpringBoot的练手项目它不像电商系统那样有复杂的并发和订单状态机也不像后台管理系统那样全是纯粹的增删改查。它在业务上有一个很自然的“记录-分析-提醒”闭环在技术上覆盖了SpringBoot生态最常用的几个组件。如果你准备做这个题目我给你一个“三步走”的执行建议第一步先把宠物档案和成长记录这两个模块做扎实它们是最基础的CRUD代码量不大但能让你把SpringBoot项目结构跑通。 第二步接JWT认证和Spring Security权限控制这一步能让你把SpringBoot生态里最核心的安全框架吃透。 第三步加定时提醒和数据可视化这两个功能是系统的亮点也是面试时能拿得出手的案例。做完这三个阶段你对SpringBoot项目的整体认知会明显上一个台阶不管是用来自用还是面试都是很完整的项目经验。而且整个过程你不会觉得枯燥因为每一阶段都会让你看到系统“活”了起来——从能存数据到能认主人再到能主动提醒那种成就感是盯着书本学半个月都换不来的。
返回列表