ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue3+MyBatis实战:失踪人员信息发布与管理系统设计与实现

SpringBoot+Vue3+MyBatis实战:失踪人员信息发布与管理系统设计与实现 1. 项目定位失踪人员信息发布与管理系统到底在做什么做这个失踪人员信息发布与管理系统最初是因为一个做公益寻人的朋友找到了我。他们团队一直在微信群里靠人工转发寻人启事信息散落在各个聊天记录和朋友圈里有人提供线索了也只能靠手工记录核对的效率非常低。更麻烦的是寻人启事一旦过期信息就沉底了再要找回来几乎不可能。聊了几轮之后我决定用Java SpringBootVue3MyBatis这一套前后端分离的方案帮他们搭一个真正能跑起来的系统。项目源码交付出来后我把它分享到了技术社区没想到不少做公益组织、救助站、甚至社区警务辅助系统的朋友都来问细节所以干脆把整个设计和实现过程整理成这篇博文。这个系统核心解决的问题其实就三件事第一让失踪人员信息有结构化的录入入口而不是散落在聊天记录里第二让寻人启事能被高效检索和展示方便快速扩散第三让社会公众提供的线索能统一收集、管理、核对形成闭环。系统本身不复杂但麻雀虽小五脏俱全涉及用户权限、信息审核、图片上传、条件检索、线索管理等典型功能非常适合用来学习SpringBootVue3前后端分离项目的完整开发流程。如果你是一个正在学Java全栈的开发者或者你需要给某个组织快速搭建一个信息发布与管理系统这篇文章会很对你有用。我会从需求拆解、技术选型、数据库设计、核心接口实现、前端页面开发到部署排坑一条线讲清楚。文章里所有代码都是实际跑通过的没有删减。我尽量把每一步的“为什么这么做”也讲明白因为光看代码是学不会设计的。2. 需求拆解与功能边界划定动手写代码之前我花了整整两天做需求梳理。这个系统的用户角色其实很清晰就三类系统管理员、信息录入员可以理解为志愿者、普通访客比如看到寻人启事来提供线索的热心人。三类角色对应的需求侧重点完全不同这直接决定了权限模型的设计。2.1 核心角色与权限模型访客端的核心诉求是快速浏览寻人启事、按条件筛选、查看详情以及提交线索。访客不需要注册登录因为寻人平台要降低公众参与门槛一旦要求注册很多热心人就不愿意填了。所以访客走的是公开访问路径接口只开放只读权限加上线索提交。录入员是平台上使用频率最高的角色他们的日常工作就是登记失踪人员信息、上传照片、修改信息、处理自己录入的信息。录入员需要登录但权限范围限制在本人创建的数据上不能看到其他人录入的信息草稿。这就避免了志愿者之间互相干扰。管理员主要做三件事审核录入员提交的信息、管理所有已发布信息下架、编辑、标记已找到、查看线索列表并处理线索。管理员对所有数据有完全权限同时可以管理录入员账号。整个权限模型我最终用RBAC基于角色的访问控制做了三张表用户表、角色表、用户角色关联表没有做按钮级权限因为这类公益系统根本不需要那么重角色到接口层面控制就足够了。2.2 功能模块划分按照上面的角色梳理整个系统拆成了六个功能模块信息登记模块录入员填写失踪人姓名、性别、年龄、身高、体貌特征、失踪时间、失踪地点、联系人、联系电话、照片等。这里有一个关键设计——失踪时间默认取当前时间因为实际场景中很多信息是家属事后才来登记的但是录的时候往往记不清准确时间了所以表单上我加了一个“是否精确到日”的开关如果关闭时间只存年月。信息审核模块录入员提交的寻人信息默认状态是“待审核”不会在前台公开展示。管理员审核通过后才对外发布。这个机制非常重要寻人信息的准确性直接影响公众信任度乱七八糟的信息一旦放出去整个平台的可信度就毁了。信息发布与展示模块前端首页展示审核通过的信息支持按性别、年龄段、失踪地区、失踪时间范围组合筛选支持关键词搜索姓名、特征。列表是分页加载的卡片式布局每条信息展示照片、姓名、年龄、失踪时间和地点。线索管理模块访客在详情页看到寻人信息后可以提交线索比如“我某月某日在某地见过类似的人”。线索提交后会直接关联到对应寻人信息下管理员和录入员可以在后台看到线索列表并标记线索状态待核实、已核实、无效。寻人公告管理模块管理员可以发布寻人进展公告比如“某某已找到”公告会展示在详情页顶部。同时信息会被标记为“已找到”从首页的“寻找中”列表自动移出。统计与数据导出模块管理员可以查看平台数据概览总信息数、已找到人数、待审核数、线索总数。数据导出做成Excel方便组织做月度报告。这些模块看起来多但实际上每个模块的CRUD逻辑都很标准难度主要集中在信息审核的状态流转、线索与寻人信息的关联查询、以及图片上传处理这三个点上。接下来我会一个个展开讲。3. 技术选型为什么是SpringBootVue3MyBatisMySQL这套技术栈放在今天不算新奇但在选型时我是认真对比过的不是随便拼凑。下面把每个组件的取舍逻辑说清楚方便你做技术选型时参考。3.1 后端选型SpringBoot与MyBatisSpringBoot已经是Java后端开发的事实标准这一点没什么争议。我选SpringBoot的一个重要原因是它和Spring Security结合做登录鉴权非常顺而MyBatis则是我故意选的。为什么不选MyBatis-Plus我承认Plus在单表CRUD上确实省事但失踪人员信息查询往往需要多条件动态拼接SQL、多表关联统计在MyBatis里写XML控制SQL会更直观排查问题也更方便。特别是线索按时间段、地区、信息状态多维组合筛选的时候动态SQL用if标签拼起来SQL执行计划自己心里有数。用Plus虽然代码少但一旦业务复杂起来直观性会打折扣。我用的SpringBoot版本是2.7.18这个版本是2.x分支的最终版稳定性和兼容性都有保障。为什么不直接上SpringBoot 3.x因为3.x基于Jakarta命名空间部分老版本的MyBatis Start、其他三方库需要同步升级我们项目里还需要对接一些内部工具用2.7.18会省掉不少兼容性折腾。如果你是全新项目、没有历史包袱直接上3.x也没问题但注意用新版配套的mybatis-spring-boot-starter。Java环境我用的是JDK 8配合SpringBoot 2.7.18这个组合在绝大多数服务器上都能跑部署成本最低。3.2 前端选型Vue3 Vite Element Plus前端技术栈选择Vue3是完全基于生态现状的判断。Vue3的组合式APIComposition API写业务逻辑比Vue2的选项式API清晰很多一个寻人信息表单涉及十几个字段、联动校验和动态图片列表用组合式API组织代码能把“单一职责”体现得很直观。构建工具我选了Vite。Vite在开发环境下基于原生ESM冷启动速度吊打Webpack项目里几十个组件秒开。生产构建用Rollup打包优化也不用操太多心。这套组合在2026年的今天依然是Vue3生态的主流方案无论是学习还是生产使用都不过时。UI组件库用了Element Plus。寻人系统是面向公众的页面不需要花哨但信息展示要清晰、表单操作要顺手。Element Plus在表格、表单、分页、弹窗、消息提示这些场景下非常成熟而且和Vue3契合度高。特别是照片上传组件我直接基于el-upload封装了一层支持图片预览和删除省了很多事。3.3 数据库选型MySQLMySQL在中小型应用里依然是性价比最高的选择没有之一。这个系统的数据量级撑死到十万级记录MySQL加合适的索引完全够用。我选了MySQL 5.7的InnoDB引擎因为5.7在事务支持、行级锁、崩溃恢复方面已经非常成熟而且很多云服务器默认预装的就是5.7或者8.0。逻辑备份用mysqldump物理备份用binlog运维简单直接。字符集我统一用了utf8mb4因为寻人信息里可能包含生僻字、特殊符号utf8mb4才能完整覆盖。排序规则用了utf8mb4_general_ci如果用utf8mb4_unicode_ci在部分MySQL版本下对中文排序会有一些奇怪的行为所以保守起见选了前者。3.4 前后端分离的架构收益前后端分离现在已经是标配我这一套也不例外。后端只提供RESTful API前端完全独立部署静态资源丢到Nginx就能跑。这样做有几个实际好处部署环境隔离后端API可以部署在云服务器前端静态页可以挂CDN流量高峰时不会互相拖累。团队并行开发前后端各有一套代码库后端定义好接口文档后前端同学甚至可以用Mock数据先把页面写出来不用等后端接口就绪。多端复用我后端接口全部是无状态的只靠Token鉴权以后如果要做一个微信小程序版本直接复用同一套API不用改后端逻辑。架构上我画了一幅分层图但这里用文字描述浏览器访问Vue3前端页面前端通过Axios调用后端API后端Controller层接收请求Service层处理业务逻辑Mapper层通过MyBatis操作MySQL数据库跨域通过CORS配置解决登录用JWT做无状态鉴权。这套链路清晰、好排查问题。4. 数据库表设计核心是状态流转与关联查询数据库是这个系统的地基。表设计得好不好直接决定后面写动态SQL时是优雅拼接还是痛苦嵌套。我把核心表的结构分享出来并说明每个字段的思考过程。4.1 失踪人员信息表missing_person这是整个系统的核心表。字段设计如下字段名类型说明idBIGINT(20)主键自增nameVARCHAR(50)失踪人姓名genderTINYINT性别1男2女0未知ageINT失踪时年龄heightINT身高厘米允许为空body_featuresVARCHAR(500)体貌特征描述missing_dateDATE失踪日期missing_locationVARCHAR(200)失踪地点contact_nameVARCHAR(50)联系人姓名contact_phoneVARCHAR(20)联系人电话photo_urlVARCHAR(500)照片地址支持多张用逗号分隔statusTINYINT状态0待审核1已发布2已找到3已下架remarkVARCHAR(500)备注信息create_byBIGINT(20)创建人用户IDcreate_timeDATETIME创建时间update_timeDATETIME更新时间几个关键设计点**photo_url为什么用逗号分隔字符串而不建子表**这是一个典型的取舍。照片如果有多张规范做法是建一张照片子表通过missing_person_id关联。但我实际调研后发现寻人启事绝大多数情况就一到三张照片用子表会产生大量无关紧要的JOIN和冗余的ORM映射。我最终用逗号分隔存在一个字段里前端拿到后split(,)渲染成多图预览。如果以后照片数量可能超过五张我建议还是拆子表现在这个量级这个方案足够。**status字段为什么用TINYINT而不直接用字符串枚举**存储上TINYINT省空间查询上MySQL对整数索引效率更高。业务状态含义我统一放在后端常量类里管理前端通过接口获取状态字典而不是硬编码在页面里。这样后续如果新增状态比如“复核中”前端不用改代码。4.2 线索表clue_info线索是“公众参与”的载体设计线索表的重点是如何高效关联到寻人信息、如何跟踪线索处理状态。字段名类型说明idBIGINT(20)主键missing_person_idBIGINT(20)关联的失踪人员IDclue_contentTEXT线索内容clue_timeDATETIME目睹时间clue_locationVARCHAR(200)目睹地点reporter_nameVARCHAR(50)线索提供人姓名reporter_phoneVARCHAR(20)线索提供人联系方式statusTINYINT状态0待核实1已核实2无效remarkVARCHAR(500)管理员备注create_timeDATETIME提交时间线索表的索引设计是我精心考虑过的。missing_person_id字段必须有索引因为后台按寻人信息查看线索是最高频的操作。create_time也建了索引因为管理员经常按时间倒序看最新线索。status字段单独建索引的意义不大因为它区分度太低和missing_person_id或者create_time做联合索引才有效。4.3 用户表与角色表用户表字段比较常规id、username、passwordBCrypt加密后存储、real_name真实姓名、phone、avatar、status启用/禁用、create_time。密码绝对不允许明文存储必须加密。我用的是BCryptPasswordEncoder每次校验时调用matches方法验证即使数据库泄露密码也无法反向还原。角色表我就设了两种角色ADMIN和OPERATOR录入员。前面说的访客不需要建账号。用户角色关联表user_role记录用户和角色的映射关系。这个设计虽然比直接在用户表存一个role字段多了一张表但扩展性更好以后如果增加“审核员”角色只需要在关联表里插入记录不用动用户表结构。4.4 审核状态流转这条状态流转线是整个业务的命脉录入员创建信息时status0待审核→ 管理员审核通过后status1已发布→ 寻人成功后管理员或录入员将状态改为2已找到→ 出现异常时下架状态改为3已下架。状态流转规则必须在后端Service层强校验。比如录入员无权直接把“待审核”改成“已发布”如果从前端直接调接口传status1后端必须拦截。我实现了一个简单的枚举类封装状态值并在更新方法里校验当前状态和目标状态的合法性。这样做可以有效避免绕过前端页面直接调接口的操作。5. 后端核心接口与MyBatis实现细节后端代码结构我按常见的分层来组织controller、service、mapper、entity、dto、config。这里不打算把所有代码贴出来重点项目讲核心接口的实现逻辑和SQL写法。5.1 寻人信息分页检索接口这是整个系统最核心的接口之一。访客首页要通过条件筛选和关键词搜索找到目标寻人信息。接口设计为GET请求参数包括pageNum、pageSize、keyword匹配姓名和体貌特征、gender、ageMin、ageMax、province、city、startDate、endDate。返回值封装为分页对象总记录数、总页数、当前页数据列表。关键难点在多条件动态拼接SQL。我用的MyBatis动态标签来实现。实际写法如下select idpageList resultTypecom.example.entity.MissingPerson SELECT * FROM missing_person where status 1 if testkeyword ! null and keyword ! AND (name LIKE CONCAT(%, #{keyword}, %) OR body_features LIKE CONCAT(%, #{keyword}, %)) /if if testgender ! null AND gender #{gender} /if if testageMin ! null AND age gt; #{ageMin} /if if testageMax ! null AND age lt; #{ageMax} /if if teststartDate ! null AND missing_date gt; #{startDate} /if if testendDate ! null AND missing_date lt; #{endDate} /if /where ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} /select注意几个细节为什么用where标签而不是手写WHERE 11where标签会自动处理多余的AND关键字SQL日志看起来更干净避免“11”这种丑写法引发审查麻烦。为什么用gt;、lt;而不是直接写、XML文件中和是特殊字符直接写会导致XML解析错误。可以不用转义但为了统一规范我全部用了实体写法。这是一处非常容易踩的坑新手经常在这里被报错卡住半天。LIMIT #{offset}, #{pageSize}用于分页。我没有引入PageHelper插件因为这个系统分页逻辑简单手写LIMIT足够少一个依赖就少一分配置风险。数据量大到几十万级的时候LIMIT深分页比如翻到第200页会变慢到时候可以改成基于游标的方案但目前这个量级不需要。5.2 线索提交与去重逻辑访客提交线索的接口很容易被恶意刷我在Service层做了三重防护第一同一IP限制记录IP地址同一个IP在10分钟内只能提交5条线索超过则拒绝。这个逻辑我用Redis实现但如果你项目里没有Redis用内存Map加过期时间也能凑合单机部署情况下。第二同手机号限制一个手机号在24小时内针对同一条寻人信息只能提交一次线索。实现方式是查库先SELECT COUNT(*)判断再执行插入。在高并发下可能有竞态问题但公益平台线索量有限加一个唯一索引uk_missing_reporter (missing_person_id, reporter_phone)从数据库层面兜底这才是最可靠的方案。第三内容长度校验线索内容不得小于10个字不得包含网址链接。这个是为了防止灌水和垃圾广告正则表达式过滤掉http://和https://开头的文本。5.3 MyBatis关联查询与懒加载在后台列表页管理员需要同时看到寻人信息的基本情况、当前状态、以及收到的线索数。这个线索COUNT不完全适合单独拉一个接口在前端循环调用那样会产生N1问题——列表20条数据就要额外发20次查询严重影响性能。我直接在SQL里用子查询实现select idadminList resultTypecom.example.dto.MissingPersonVO SELECT mp.*, (SELECT COUNT(*) FROM clue_info WHERE missing_person_id mp.id) AS clue_count FROM missing_person mp where if teststatus ! null AND mp.status #{status} /if /where ORDER BY mp.create_time DESC LIMIT #{offset}, #{pageSize} /select这是典型的“以空间换时间”思路一条主查询搞定不用循环调用。对于列表展示类业务能用子查询解决的尽量不要用ORM的懒加载懒加载在循环里触发会非常致命。5.4 JWT登录鉴权实现登录流程用的是JWT无状态方案。用户输入用户名密码后端校验通过后生成一个Token返回给前端。Token中包含用户ID和角色信息并设置7天有效期。前端每次请求时在Header里带上Authorization: Bearer token后端通过拦截器统一解析Token如果过期或非法直接返回401。Spring Security我用了但是简化配置没有引入一堆过滤器链。核心做法是Configuration EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .cors().and() .authorizeRequests() .antMatchers(/api/public/**, /api/auth/login, /api/missing/page).permitAll() .antMatchers(/api/admin/**).hasRole(ADMIN) .antMatchers(/api/operator/**).hasRole(OPERATOR) .anyRequest().authenticated(); } }然后我写了一个JwtAuthenticationFilter继承OncePerRequestFilter在每个请求进来时解析Token并放入SecurityContext。这部分代码比较长核心逻辑就是解析JWT如果拿到了用户ID查出用户信息设置认证态。注意接口路径权限必须配置在后端前端的路由守卫只能算作体验优化不能作为安全边界。6. Vue3前端实现从零搭建到页面交付前端部分我用Vite从零搭建项目没有用vue-cli因为Vite在开发体验上确实好很多。下面按步骤讲清楚整个前端项目的搭建过程和核心代码。6.1 项目初始化与基础配置初始化命令很简单npm create vitelatest missing-person-web -- --template vue cd missing-person-web npm install装完基础依赖后需要额外安装以下几个包npm install vue-router4 pinia axios element-plus sass其中状态管理选了Pinia它是Vue3官方推荐的状态库比Vuex用法更简洁支持组合式API直接写业务逻辑。写了几年Vuex的人上手Pinia基本无障碍而且不需要写那么多mutations同步状态直接改$state很方便。后续归档我利用vue-router配置了三个主要的视图首页HomeView.vue、详情页DetailView.vue、管理后台AdminView.vue。管理后台内部又包含几个子路由信息管理、线索管理、用户管理、数据统计。整体用router-view嵌套渲染侧边栏菜单用Element Plus的el-menu实现。6.2 Axios二次封装与拦截器Axios封装是每个前端项目的第一道工序我习惯把所有请求细节都收到一个文件里管理。核心代码如下import axios from axios import { ElMessage } from element-plus const service axios.create({ baseURL: /api, timeout: 15000 }) // 请求拦截器自动带token service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer ${token} } return config }) // 响应拦截器统一处理错误 service.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { ElMessage.error(登录已过期请重新登录) localStorage.removeItem(token) location.href /login } else { ElMessage.error(error.message || 网络异常) } return Promise.reject(error) } ) export default service这里有一个很有用的细节开发环境下我通过Vite的代理配置解决跨域问题在vite.config.js中添加export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样前端所有请求都走相对路径/api/xxx由Vite开发服务器代理到后端。生产环境则直接把后端的接口路径和前端静态文件一起部署到同一个域名下也不存在跨域问题。这是一个非常省心的策略开发生产都不用为了CORS头疼。6.3 组合式API组织寻人信息表单这里重点讲一个前端核心业务——寻人信息登记表单。这个表单字段多、校验规则多用Vue3的组合式API组织非常合适。// 在组件中组织表单逻辑 const formRef ref(null) const formData reactive({ name: , gender: null, age: null, height: null, body_features: , missing_date: , missing_location: , contact_name: , contact_phone: , photoList: [], status: 0 }) const rules { name: [{ required: true, message: 请输入姓名, trigger: blur }], gender: [{ required: true, message: 请选择性别, trigger: change }], missing_date: [{ required: true, message: 请选择失踪日期, trigger: change }], missing_location: [{ required: true, message: 请输入失踪地点, trigger: blur }], contact_phone: [ { required: true, message: 请输入联系电话, trigger: blur }, { pattern: /^1[3-9]\d{9}$/, message: 手机号格式不正确, trigger: blur } ] } const submitForm async () { await formRef.value.validate() const payload { ...formData, photo_url: formData.photoList.map(item item.url).join(,) } await api.submitMissingPerson(payload) ElMessage.success(提交成功等待管理员审核) resetForm() }照片上传组件这里多说两句。我用el-upload的http-request属性自定义了上传行为因为Element Plus默认的上传行为和我们的业务需求不完全匹配const uploadPhoto async (options) { const formData new FormData() formData.append(file, options.file) const res await api.uploadFile(formData) if (res.code 200) { formData.photoList.push({ url: res.data.url }) ElMessage.success(照片上传成功) } }上传接口由后端处理文件流通过MultipartFile接收重命名后存储在服务器本地指定目录同时生成访问URL。我在这里把文件名改成UUID就是为了避免中文文件名和重复名导致的问题。7. 项目部署与几个不得不踩的坑项目写完不算完能顺利部署上线才算真正结束。我在这里把部署步骤和踩过的坑记录一下帮你省去不必要的折腾。7.1 前后端打包部署流程后端打包很简单在项目根目录执行mvn clean package -DskipTests打包完成后在target目录下会生成一个xxx.jar文件。部署时我建议用java -jar直接启动同时配合nohup做后台运行nohup java -jar missing-person-system-1.0.0.jar --spring.profiles.activeprod 生产环境的数据库连接、文件上传路径、日志级别都要放到application-prod.yml里不要直接改默认配置。前端构建命令是npm run build构建完成后dist目录下就是纯静态文件把它上传到服务器上用Nginx指向这个目录即可。Nginx配置里两个要点一是前端路由使用history模式时必须配置try_files回退到index.html否则直接刷新二级页面会404二是把/api路径反向代理到后端Java进程端口。完整配置片段如下server { listen 80; server_name your-domain.com; root /opt/missing-person-web/dist; index index.html; location / { try_files $uri $uri/ /index.html; } 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; } }7.2 图片上传目录的读写权限问题Linux服务器上部署时经常遇到的一个问题上传图片后页面访问报403或无法显示。原因绝大多数是上传目录没有写权限或者Nginx用户没有读权限。我的解决思路在服务器上创建/data/upload目录然后chmod -R 755 /data/upload同时把SpringBoot的配置指向这个目录file: upload-dir: /data/upload access-prefix: /upload/**然后Nginx需要配置一个静态映射让/upload/**开头的请求直接指向/data/upload目录location /upload/ { alias /data/upload/; }这里有个经验之谈不要图省事把图片交给后端Java处理访问路径直接用Nginx静态文件服务性能是最好的。通过Java IO流去读磁盘文件再输出响应性能和稳定性都要差一些。7.3 MySQL连接参数的坑MySQL连接串的配置看似简单里面藏着一个很实际的坑。如果用默认的时区参数serverTimezoneAsia/Shanghai必须显式配置否则Java程序连接MySQL 8.0以上版本时会报错。5.7版本会宽松一些但也建议显式加上。另外连接池参数也很关键我实际配置如下spring: datasource: url: jdbc:mysql://localhost:3306/missing_person_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: xxxxxx hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000useSSLfalse很重要如果MySQL端没有配置SSL证书这个参数不设会导致连接警告甚至失败。局域网部署场景下SSL加密是可有可无的直接关掉节省握手开销。7.4 日志查看与线上问题排查线上排查问题日志就是你的眼睛。我配置了Logback滚动日志按天生成文件保留30天logging: file: name: /data/logs/missing-person-system.log level: com.example.missingperson.mapper: DEBUG这里特别说明Mapper包级别Debug日志会把MyBatis的SQL和参数打印出来线上排查“查不到数据”或者“数据不对”时这是最直接的线索。但注意生产环境不建议长期开DEBUG日志量会暴涨只在排查问题时临时开启。我踩过的经典坑之一是“明明重启了项目但查询结果还是旧的”。后来定位到是MyBatis一级缓存和二级缓存的问题。MyBatis的本地缓存特性在同一个SqlSession下会缓存查询结果如果项目中某个环节复用了SqlSession拿到的是缓存数据。虽然在Spring整合环境下每次请求都会新建SqlSession默认一级缓存基本不会造成问题但一旦你开了二级缓存又没清楚对应缓存的失效条件就会遇到脏数据问题。我的原则是这个系统里直接关闭二级缓存要缓存就上Redis不要依赖MyBatis的二级缓存机制。7.5 前后端时间格式统一最后说一个非常容易阴沟翻船的问题时间格式。Java后端返回的LocalDateTime默认序列化格式是2026-01-15T10:30:00这个带T的格式前端展示起来非常难看而且用户不习惯。我在后端做了全局Jackson配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai前端接收日期字符串后直接展示不需要再做格式化。但注意如果用LocalDate序列化上面这个配置不生效需要在字段上加JsonFormat(pattern yyyy-MM-dd)注解。这两个坑我都踩过写在这里希望你能少花半天时间。8. 项目实战心得这套系统还能怎么升级系统主体跑通后我后来又做了几轮迭代优化也想清楚了这个项目后续可以怎么扩展。这里简单讲讲我个人的思路。第一个优化点是引入Elasticsearch做全文检索。现在MySQL的LIKE %keyword%在数据量小的时候完全没问题但如果未来数据量增长到几十万甚至百万条这个查询会明显变慢。ES可以做到更快的全文检索和联想搜索甚至“根据体貌特征反查相似人员”。不过引入ES也意味着架构复杂度明显上升不是每个公益组织都有运维能力。按目前绝大多数用户的数据量来看MySQL加索引完全够用这个优化属于“储备方案”而不是“需求”。第二个优化点是增加“相似人员比对”功能。失踪人员特征身高、年龄、体型、失踪地点周边区域可以做成多维度向量利用机器学习做相似度匹配。比如有人报案称在某地见到一个疑似走失人员系统可以自动从数据库中推荐“可能匹配”的失踪人员大幅提升寻人效率。这个功能如果要做后端架构需要引入向量数据库或使用Milvus成本不低但公益价值很大。我当时没有实施主要原因是缺少足够的标注数据做模型训练而且这种功能的误报率如果高反而会误导志愿者精力。第三个优化点是增加公众线索的微信小程序入口。现在前端是PC端网页公众提交线索路径太长了。小程序作为入口更符合公益场景——用户看到寻人启事直接在小程序里拍照上传线索使用门槛极低。由于后端接口本身是前后端分离的复用度很高小程序端只需要重新写一层前端即可。最后说一句我的体会这类系统技术难度不在极致的性能优化而在于把信息流转的状态关系理清楚把权限边界守好把公众侧的体验做简单。技术栈选得再潮如果数据不准、状态混乱、界面难用都白搭。按照SpringBoot后端提供稳定接口、Vue3前端聚焦交互体验、MyBatis精确控制SQL这一套组合在开发效率和可维护性之间取得了比较好的平衡。这篇文章里的所有代码片段和技术决策都来自真实落地项目你照着做至少能少走两三个弯路。
返回列表