ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot全家桶实战:校园综合服务平台完整落地

SpringBoot全家桶实战:校园综合服务平台完整落地 1. 一个校园综合服务平台为什么最终选了SpringBoot全家桶接手这个校园综合服务平台项目的时候需求其实不算复杂学生要能在上面查课表、看成绩、充值校园卡、在线报修老师要能发布通知、管理活动后勤那边要能处理工单、导出报表。听起来像是个普通的管理系统但真正做起来就会发现所谓综合两个字才是最磨人的地方。它不是某一个单一业务而是多个业务线的叠加用户角色分学生、教师、管理员、后勤人员数据维度涉及教务、财务、后勤、社团活动各个模块之间还有交叉引用。比如学生报修之后要通知后勤人员维修完成之后要给学生反馈评分评分数据又要汇总到后勤的月度报表里。这种业务复杂度如果还按传统的JSPServlet那一套来写后期维护成本会高到让人想辞职。选SpringBoot不是因为它新而是因为它能把一套复杂的业务系统拆得足够清爽。自动配置帮我们省掉了大量XML配置起步依赖让版本管理变得无脑内嵌Tomcat让部署从装环境、配服务器、丢war包变成扔一个jar包上去就跑。而且SpringBoot的生态足够完整安全认证有Spring Security接口文档有Swagger缓存有Redis Starter消息队列有ActiveMQ Starter文件存储有MinIO客户端报表对接也有成熟的HTTP方案。对于一个要涵盖多个业务域、由好几名同学/同事协作开发的平台来说SpringBoot最合适的一个特点就是——它把大家的工作方式统一了配置规范了接口风格也规整了省去很多你怎么写成这样的扯皮。按项目的实际需求技术选型最终定下来是这样一套核心框架SpringBoot 2.7.x持久层MyBatis-Plus带分页插件数据库MySQL 8.0缓存Redis文件存储MinIO消息队列ActiveMQ报表锐浪报表服务器GridReport前端Vue 3 Element Plus部署Docker Docker Compose这篇文章我会按照完整的项目实现顺序把从架构设计到具体功能落地的过程、踩过的坑、改过的方案都写一遍重点放在那些搜索热度一直很高的话题上SpringBoot自动装配原理、全局过滤器处理上传PDF的XSS攻击、MinIO整合、ActiveMQ整合、锐浪报表深度整合、Docker部署。2. 模块边界与数据模型从综合二字开始拆业务项目启动的时候我们第一件做的事不是写代码而是画业务边界。校园综合服务平台最容易犯的错误就是把所有功能塞进一个巨型单体用户表后缀越加越多订单表和服务表纠缠不清最后变成一个大泥球。我们最后把平台拆成了六个核心模块模块核心功能主要用户用户中心登录认证、角色权限、个人信息全体用户教务服务课表查询、成绩查询、考试安排学生、教师校园生活校园卡充值、消费流水、失物招领学生后勤报修在线报修、工单派发、维修进度学生、后勤活动管理社团活动发布、在线报名、签到学生、社团管理员统计报表各类业务数据的汇总与分析管理员、后勤这个拆分不是随便分的依据是一个模块内部是高内聚的模块之间通过接口通信绝不直接互相查表。比如后勤报修模块产生工单数据统计报表模块需要维修完成率那报表模块就通过报修模块提供的接口拿数据而不是自己去查工单表。这样后面每个模块独立迭代、独立优化都不会影响其他人。数据模型上我最想强调的一点是不要一上来就设计二十多张表。我们第一版只设计了与用户和权限直接相关的核心表以及每个模块自己的主业务表。用户表结构大致如下CREATE TABLE sys_user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 学号/工号, password varchar(100) NOT NULL COMMENT BCrypt加密存储, real_name varchar(50) DEFAULT NULL, role tinyint(4) NOT NULL DEFAULT 3 COMMENT 1-管理员 2-教职工 3-学生, dept_id bigint(20) DEFAULT NULL COMMENT 所属院系, phone varchar(20) DEFAULT NULL, email varchar(100) DEFAULT NULL, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 1-正常 0-禁用, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;业务表这里可以多说两句。报修工单表是各模块中比较有代表性的它涉及状态流转我们把状态设计成一个小整数配合一个状态机来管理。状态码从0到5依次是待派单、处理中、待验收、已完成、已取消、已驳回。每转一次状态都要在工单记录表里留一条轨迹这样后勤管理员能看到这个工单什么时候派的、什么时候修的、谁验收的出了问题能追溯。这里还涉及到一个SpringBoot开发者应该很熟悉的问题就是自动装配到底帮我们做了什么。以MyBatis-Plus为例引入mybatis-plus-boot-starter之后SpringBoot通过spring.factories或AutoConfiguration.imports机制扫描到MybatisPlusAutoConfiguration这个配置类上有一堆ConditionalOnClass、ConditionalOnMissingBean注解只有当你classpath里有对应类、容器里没有用户自定义的SqlSessionFactory时它才自动帮你创建SqlSessionFactory、SqlSessionTemplate这些Bean。这就是为什么我们只需要在application.yml里配好数据源和Mapper扫描路径MyBatis-Plus就能直接用的根本原因。理解了这个机制遇到为什么我引入了依赖但是没生效的问题时第一反应应该是去检查是不是条件注解没满足而不是怀疑人生。3. SpringBoot登录认证与权限控制Shiro还是SpringSecurity登录认证这个环节我们开始也纠结过到底是Spring Security还是Shiro。校园平台有个特点角色分明但不是特别复杂权限控制主要靠角色接口级别做拦截不需要特别细粒度的数据权限。考虑到团队对SpringBoot的熟悉程度我们最后用了Spring Security JWT的方案。说下大概思路用户登录成功后后端签发一个JWT令牌前端存在localStorage里每次请求在Authorization头带上。后端写一个过滤器解析JWT把用户信息塞进SecurityContext里然后走Spring Security的过滤链完成权限校验。核心配置类如下Configuration EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers(/api/auth/login, /api/auth/register).permitAll() .antMatchers(/api/admin/**).hasRole(ADMIN) .antMatchers(/api/repair/**).hasAnyRole(ADMIN, STUDENT) .anyRequest().authenticated() .and() .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } }按SpringBoot 2.7的习惯我是继续用了WebSecurityConfigurerAdapter如果项目用的是SpringBoot 3.x就得改成SecurityFilterChain的Bean写法了这是升级时最容易踩的坑之一。JWT这块有个点要提醒不要把敏感信息放进token里。我们最初为了省事把用户手机号也塞进去了虽然JWT是签名防篡改的但默认是不加密的Base64解码就能看到内容放在前端浏览器里等于裸奔。后面我们把token里只保留userId、username、role这三个必要字段其余信息需要时再查Redis或者数据库。权限上真正要花功夫设计的是接口粒度。校园平台里很多接口不是管理员就能看这么简单的。比如成绩查询接口学生只能看自己的成绩教师能看自己授课班级的成绩教务处能看全部成绩。这种场景单靠hasRole已经不够了我们在Service层做了数据归属校验学生请求时从SecurityContext里拿当前userId查询条件强制带上自己的id不允许前端传入别人的id去查。这里记住一个原则前端传参只是辅助真正的权限过滤必须在后端强制完成。我们把所有查询接口的参数对象里都加了一个当前用户的隐式条件从源头杜绝了越权查询。4. 全局过滤器处理上传PDF的XSS攻击一次从PDF/JS漏洞开始的深度排查这个环节是项目里最有意思的坑也是搜索热度里非常靠前的一个话题。项目上线前做安全测试我们一开始重点检查的是表单输入框有没有XSS注入防护字符串过滤做得挺到位。结果测试组拿了一个PDF文件上传之后浏览器居然弹出了alert框。当时我们都懵了——PDF文件怎么会触发XSS后来才明白原因PDF文件本身可以内嵌JavaScript脚本浏览器内置的PDF阅读器在渲染PDF时会执行其中的JS代码。也就是说即便你的系统对所有文本输入都做了过滤在文件上传场景里攻击者可以构造一个携带恶意脚本的PDF上传到平台然后诱导其他用户点击预览或下载。浏览器打开PDF的那一刹那脚本就在当前域下执行了完全可以窃取Cookie、劫持会话。这比普通反射型XSS隐蔽得多。问题定位之后我们做了一套完整的防御方案。核心分两层第一层对所有上传的文件做类型校验不能只看扩展名要读取文件头部魔数判断真实类型第二层增加一个全局过滤器统一处理请求中的参数清洗。过滤器代码如下Component public class XssFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; // 只处理需要清洗的路径 if (req.getRequestURI().contains(/api/)) { chain.doFilter(new XssHttpServletRequestWrapper(req), response); } else { chain.doFilter(request, response); } } }重点在这个XssHttpServletRequestWrapper它重写了getParameter、getParameterValues、getHeader对所有读到的字符串做HTML转义清洗public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { public XssHttpServletRequestWrapper(HttpServletRequest request) { super(request); } Override public String getParameter(String name) { String value super.getParameter(name); return cleanXss(value); } Override public String[] getParameterValues(String name) { String[] values super.getParameterValues(name); if (values null) { return null; } String[] cleaned new String[values.length]; for (int i 0; i values.length; i) { cleaned[i] cleanXss(values[i]); } return cleaned; } private String cleanXss(String value) { if (value null || value.isEmpty()) { return value; } // 使用HtmlUtils进行HTML转义同时处理常见的XSS关键字 String cleaned HtmlUtils.htmlEscape(value); cleaned cleaned.replaceAll((?i)javascript:, ); cleaned cleaned.replaceAll((?i)onerror, on_error); cleaned cleaned.replaceAll((?i)onclick, on_click); cleaned cleaned.replaceAll((?i)onload, on_load); return cleaned; } }这里有个非常重要的细节也是我们排查过程中最头疼的问题HttpServletRequest的InputStream只能读取一次。如果请求是multipart/form-data文件上传类型过滤器里一旦调用了getInputStream()去读内容做检查后面Controller里SpringMVC的文件解析就拿不到数据了。我们第一个版本的过滤器就是在这个问题上翻车的所有带文件的上传接口全部报错。解决办法是定义一个ContentCachingRequestWrapper把请求体缓存到内存里后续的getInputStream()和getReader()返回的都是缓存副本。具体就是用一个ByteArrayOutputStream缓存body所有读取方法都从这个缓存里取数据。对于JSON请求体还需要把body解析出来对JSON里的字符串值逐个清洗再重新写回。这一套做完文件上传接口的参数清洗才能正常工作。另外针对PDF这种文件类的XSS过滤器是管不了的因为恶意脚本在文件内容里而不是请求参数里。所以必须再加一层文件存储前用PDF解析库扫描脚本。我们用的方案是Apache PDFBox把PDF文本抽取出来用正则扫描可疑的/JavaScript、/OpenAction这类关键字命中就直接拒绝存储。但这招只对文本可抽取的PDF有效如果有密码加密或者扫描型PDF那就只能靠浏览器端的沙箱策略兜底了。最后还做了一道兜底所有涉及文件预览的接口响应头强制加上Content-Disposition: attachment让浏览器以附件形式下载而不是直接打开预览。你要问我文件预览功能还要不要做我的建议是如果没有很强的需求校园平台这类系统不要轻易做在线预览这是风险最高、收益最低的功能。5. MinIO文件存储与动静分离落地校园平台的图片、附件、头像、报修照片这些文件我们用的是MinIO。为什么不用FastDFSFastDFS部署复杂度实在太高要配tracker和storage两个角色环境一换就各种问题而且社区活跃度也一般。为什么不用阿里云OSS项目部署在学校内网服务器没有外网环境OSS根本连不上。综合下来MinIO最合适部署就是Docker跑一个容器的事S3兼容API是行业标准自带Web管理界面单机模式完全够校园平台这种并发量。MinIO和SpringBoot整合其实很简单引入MinIO Java SDK后配置类如下Configuration public class MinioConfig { Value(${minio.endpoint}) private String endpoint; Value(${minio.access-key}) private String accessKey; Value(${minio.secret-key}) private String secretKey; Value(${minio.bucket-name}) private String bucketName; Bean public MinioClient minioClient() { return MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); } }application.yml里对应的配置minio: endpoint: http://192.168.10.20:9000 access-key: campus secret-key: campus-secret bucket-name: campus-files上传的核心方法public String uploadFile(MultipartFile file, String module) throws Exception { String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); String objectName module / UUID.randomUUID().toString().replace(-, ) suffix; minioClient.putObject( PutObjectArgs.builder() .bucket(bucketName) .object(objectName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build() ); return objectName; }这里objectName设计成了模块名/UUID.后缀的结构比如repair/20240512103012345.jpg、avatar/xxxxx.png。这样做的好处是同一类业务的文件都在同一个目录前缀下后面做生命周期管理或者定期清理非常方便。比如失物招领图片30天后自动清理直接按前缀扫描对象就完事。关于MinIO有个非常关键的坑内网地址和浏览器访问地址的区分。MinIO生成的预签名URL默认用的是endpoint配置的地址如果endpoint配成了http://192.168.10.20:9000而用户浏览器访问的是http://campus.example.edu.cn那前端拿到的文件URL就打不开。我们的做法是服务器内部上传走内网endpoint但生成访问URL时用配置文件单独指定一个minio.public-endpoint前端拼URL的时候覆盖默认host。很多团队第一次整合MinIO都在这上面栽跟头提前配置好能省很多事。对于大文件上传比如学生上传的视频作业我们做了分片上传。前端用plupload或vue-upload-component把文件切成5MB一片每次上传带一个uploadId后端用MinIO的composeObject把分片合并。一开始我们以为分片上传没什么难度真正做起来才发现并发合并的顺序、分片信息的缓存、失败重试的幂等性每一个细节都能写出好几屏代码。如果项目时间紧张可以直接用MinIO SDK自带的MultipartUpload封装或者干脆限制单文件大小校园场景能覆盖90%以上的需求。6. 锐浪报表服务器与SpringBoot的深度整合报表模块是这个平台里最容易被低估的部分。校园平台辛苦沉淀了一学期的报修数据、充值数据、活动报名数据总得有个出口让管理层看得到。锐浪报表GridReport在国内很多政务、教育项目里都有在用部署整套报表服务器和SpringBoot对接我们的做法是SpringBoot只出数据锐浪负责呈现。锐浪报表服务器的调用方式是HTTP接口。报表模板设计好之后上传到报表服务器SpringBoot后端要做的事情就是把业务数据转换成报表需要的格式推过去。这里有两个坑要用经验去填。第一个坑锐浪报表的数据源格式。锐浪报表支持SQL直连、JSON、XML三种数据源方式。SQL直连最简单但校园网环境下报表服务器和业务数据库的网络隔离往往是说不清的事而且直接让报表服务器连业务库会带来安全隐患数据库账号密码暴露在报表服务器上总不是个事。我们最后选的是JSON数据源接口方式SpringBoot写一个数据接口接收报表模板编号和参数返回JSON格式的数据集锐浪报表服务器通过HTTP请求拉取JSON再套用报表模板渲染。接口大概长这样RestController RequestMapping(/api/report/data) public class ReportDataController { Resource private RepairReportService repairReportService; PostMapping(/repair-monthly) public ResultReportDataset repairMonthly(RequestBody ReportQueryParam param) { // 按月份、院系汇总维修数据 ListRepairSummaryVO list repairReportService.summaryByDept(param.getMonth()); ReportDataset dataset new ReportDataset(); dataset.setColumns(Arrays.asList(deptName, totalCount, finishedCount, avgHours)); dataset.setRows(list); return Result.success(dataset); } }前端调用锐浪报表的呈现思路是网页端嵌入一个锐浪报表的ActiveX或HTML5控件访问Java服务提供的数据接口获取数据然后通过ReportDataSource属性绑定数据。这套方案虽然多了一步网络传输但职责清晰数据权限控制在业务层报表服务器只做模板渲染后续数据库换掉也不影响报表。第二个坑报表数据权限。报表直连数据库的话很难做行级权限控制同一个报表模板院长想看全院的数据系主任只能看自己系的。走JSON接口方案之后权限过滤直接在Controller层做从SecurityContext里取出当前用户的deptId查询条件动态拼接。系主任调这个接口拿到的数据就自动限制在自己系范围内权限控制和业务接口一样走Spring Security的过滤器链整套逻辑是通的。锐浪报表还有一个比较头疼的点是模板的参数传递。报表模板里如果有日期范围、院系编号这类筛选参数SpringBoot端一定要定义好参数格式和默认值。我们第一版上线时前端报表页面能正常打开但一旦切换院系报表数据出不来排查了半天发现是参数名对不上——模板里用的是deptId后端返回数据的字段叫dept_id锐浪报表控件不认下划线命名导致参数传了但模板取不到。解决方法是统一用驼峰命名前端传参、后端返回、模板字段三处保持一致这个问题就再也没出现过。关于锐浪这套东西还有一个要说的校园里的旧项目很多还在用锐浪的老版ActiveX控件只支持IE浏览器。如果学校浏览器还用着老IE内核那报表模块大概率跑不起来。我们的处理是前端做了控件版本检测和降级提示同时开发了一套PDF导出方案报表服务器渲染PDF用户直接用浏览器打开PDF文件。这个兜底方案虽然交互体验差一点但至少功能上不缺失在兼容性上稳得多。7. ActireMQ异步通知报修工单和消息推送的解耦校园平台里有一个很典型的异步场景学生提交报修工单之后系统要立刻响应提交成功同时要给后勤管理员发通知维修完成后要给学生发评价提醒。如果这些动作全部同步做一次提交请求可能要等通知发送完才返回用户体验会很差。我们用ActiveMQ做消息中间件把耗时操作全部异步化。SpringBoot整合ActiveMQ的过程引入spring-boot-starter-activemq依赖配置连接工厂和Queue然后写生产者和消费者。spring: activemq: broker-url: tcp://192.168.10.20:61616 user: admin password: admin pool: enabled: true max-connections: 10生产者代码Component public class RepairMessageProducer { Resource private JmsTemplate jmsTemplate; private static final String REPAIR_NOTIFY_QUEUE repair.notify.queue; public void sendRepairNotify(RepairNotifyMessage message) { jmsTemplate.convertAndSend(REPAIR_NOTIFY_QUEUE, message); } }消费者代码Component public class RepairMessageConsumer { JmsListener(destination repair.notify.queue) public void handleRepairNotify(RepairNotifyMessage message) { // 解析消息调用消息推送服务 notifyService.pushToRepairMan(message.getRepairManId(), 您有新的报修工单 message.getRepairTitle()); } }这里有几个ActiveMQ和SpringBoot整合的实战问题持久化必须开。ActiveMQ默认的消息存储是内存的Broker一重启消息就丢了。校园平台务必改成KahaDB持久化存储在activemq.xml里配置persistenceAdapter为kahaDB。不然一台服务器重启未消费的报修通知全部消失工单就漏了。消费失败重试配置。JmsListener默认在消费端抛出异常后会进入重试默认重试次数是6次超过就转入死信队列。我们有一次把消息发到Queue消费者因为数据库临时连接不上消费失败重试6次后消息直接进了死信队列后勤管理员那条报修通知就丢了。所以要给死信队列配置告警同时在消费逻辑里捕获异常记录日志确保每条消息的消费都有据可查。消息体序列化。JmsTemplate默认用的Java原生序列化消息头里有类型信息但两端类完全一致才行。如果后面要做系统间解耦或者消息要跨服务传递推荐直接把消息体转成JSON字符串发送消费者端再用Jackson反序列化成自己的内部对象。这样生产者和消费者各自维护自己的类类型变更互不影响。消息队列还有一个好处是把高峰流量削平。校园报修工单的特点是平时很平稳但开学季或设施检修周会突然暴增比如某天全校统一出现宿舍空调漏水这一类工单。如果所有工单让报表模块同步去汇总高峰期报表接口会拖累报修主流程。我们把工单数据进入报表统计这个动作也丢进了MQ报表模块按自己的节奏消费高峰期不崩低峰期也不闲着。8. Docker部署从开发机到服务器的一键发布部署这块项目用的是Docker Compose把MySQL、Redis、MinIO、ActiveMQ和SpringBoot应用编排起来一起跑。开发机上能跑服务器上也能跑环境差异基本消除。SpringBoot应用的Dockerfile写的是多阶段构建先用Maven镜像编译再用JRE镜像精简运行环境FROM maven:3.8-openjdk-8 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests FROM openjdk:8-jre-alpine WORKDIR /app COPY --frombuilder /app/target/campus-platform.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]为什么用多阶段构建因为最终运行镜像里不需要Maven和源码多阶段构建能把镜像体积从700MB降到150MB左右内网服务器磁盘吃紧的时候这个优化很值。另外mvn dependency:go-offline这一步是把依赖先拉一遍做缓存层这样后续只有源码变更时Maven那步会命中缓存构建速度快很多。docker-compose.yml关键内容version: 3.8 services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: campus_db volumes: - mysql-data:/var/lib/mysql ports: - 3306:3306 redis: image: redis:7-alpine ports: - 6379:6379 minio: image: minio/minio command: server /data --console-address :9001 environment: MINIO_ROOT_USER: campus MINIO_ROOT_PASSWORD: campus-secret volumes: - minio-data:/data ports: - 9000:9000 - 9001:9001 activemq: image: rmohr/activemq ports: - 61616:61616 - 8161:8161 app: build: . depends_on: - mysql - redis - minio - activemq environment: SPRING_PROFILES_ACTIVE: prod ports: - 8080:8080 volumes: mysql-data: minio-data:这段配置里有几个经验可以分享SpringBoot应用的depends_on只是控制启动顺序不保证MySQL已经就绪。MySQL容器起来后可能还要十几秒才能接受连接SpringBoot启动时连接数据库失败就直接退出。我们加了重试机制在启动命令前面用一个小shell脚本循环检测MySQL端口通了再启动应用或者更简单的办法是在application.yml里配置spring.datasource连接重试但实测下来还是脚本检测最稳。镜像的时区问题一定要处理。Java应用的默认时区和宿主机不一定一致如果不设置日志里的时间和数据库时间、真实时间会差8小时报修工单的时间统计全部错乱。在Dockerfile里加上ENV TZAsia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo $TZ /etc/timezone另外docker-compose里所有中间件的数据都挂在了volume里这样容器删了重建数据不丢。MySQL的mysql-data卷里存的是整个数据目录MinIO的minio-data里存的是对象文件。定期把这两个卷备份到另一台机器上比备份容器本身实在得多。9. 一些在项目收尾阶段的扩展想法项目核心功能做完之后我们又做了几个增强功能这里简单说下思路。全文检索的改造。校园平台的失物招领和通知公告是典型的全文检索场景。早期版本用的是SQL的LIKE %关键词%查询数据量一上来尤其是失物招领表积累了一两年的数据后查询明显变慢。我们后续接入了Elasticsearch把失物招领、公告、社团活动这些热数据同步到ES里用中文分词器做全文检索查询响应从1秒以上降到了几十毫秒。如果不想引入额外的搜索引擎也可以用MySQL全文索引凑合但中文分词效果确实不如专用搜索引擎。Redis缓存的引入。课表查询和成绩查询是校园平台的最高频接口学生一天能刷好多次。这些数据的特点是变动频率极低一学期可能就变一两次但查询频率极高。我们把这些热数据在Redis里缓存24小时过期后自动从数据库加载同时每次教务管理员更新课表后主动删除对应缓存一下就把数据库的压力降下来了。SpringBoot整合Redis就是加一个spring-boot-starter-data-redis依赖用StringRedisTemplate轻松搞定。审批流的引入。活动管理模块里社团发起活动申请要经过指导老师、团委、后勤多级审批最开始我们用的是自己在数据库里设计状态字段一步一步往下走。后来审批节点多了比如涉及经费、场地状态机就变得特别复杂我们改用了Flowable工作流引擎把审批流程做成BPMN流程图SpringBoot整合Flowable官方有现成的Starter配置好数据库表就能跑。如果一开始就预计审批链路会经常调整建议直接上工作流引擎比硬编码状态机省太多事。我从这个项目里最大的体会是校园综合服务平台看似业务简单但真正做完了才发现难度不在某个单独的技术点而是如何把所有模块整合在一个统一的技术体系里不打架。SpringBoot全家桶的价值正在于此——它提供了一套统一的技术规范让团队成员写出来的代码风格一致、配置文件风格一致、接口风格一致整个项目的可维护性因此提高了好几个档次。另外安全这块千万不要抱有侥幸心理我们这次要不是安全测试那关根本不会发现PDF文件还能携带XSS脚本的问题。线上环境挂了的代价远比开发时多写几层过滤代码要大得多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进