郑州这个项目前后磨了差不多两个月,从最开始的微信群接龙报名,到最后整套 Spring Boot 公益网站跑起来,中间踩的坑比想象中多得多。项目名里的"绿城"是郑州的别称,至于末尾那串数字 11795,是当时立项时候的流水号,没什么特殊含义。这篇就完整复盘一下这个基于 Spring Boot 的爱心公益网站是怎么从零做出来的,涉及哪些模块、怎么设计数据库、怎么解决报名并发和定时任务这些实操问题,最后附上完整部署经验。建议正在做 Spring Boot 课设、毕设,或者想给本地志愿团队搭一个轻量公益平台的朋友认真看看,尤其是版本选型部分,能帮你少走很多弯路。
1. 这个公益网站项目要做成什么样
1.1 需求是从哪来的
做这个项目之前,我先花了一周时间跟着郑州本地几个志愿团队的负责人聊了一圈,发现他们的日常管理方式相当原始:公益项目发布靠微信群公告,志愿者报名靠接龙,物资捐赠靠私聊登记,活动结束后的照片和反馈散落在各个群聊里,根本谈不上沉淀和统计。
所以说白了,这个爱心公益网站要解决的问题就三个:活动信息有地方统一发布、志愿者能在线报名、爱心物资捐赠有据可查。考虑到使用人群里有不少中老年志愿者,操作必须足够简单,功能逻辑要直白,不能搞一堆花里胡哨的交互。
1.2 功能边界怎么划
很多同学一上来就想做大而全的东西,支付、社交、积分商城全都要。我接手这个项目之后,跟团队负责人反复确认,最后把功能收敛成了五块:
- 公益项目展示:首页按时间倒序列出正在招募的公益活动,点进去能看到活动详情、时间地点、招募人数;
- 志愿者在线报名:注册用户可以直接报名参与活动,报名前能看到剩余名额;
- 物资捐赠登记:爱心人士可以登记捐赠意向,填写物资类型、数量、联系方式;
- 求助信息发布:公益组织可以发布求助信息,比如某社区需要御寒衣物、某学校缺图书;
- 后台管理:管理员对所有内容进行审核、编辑、下架,导出报名名单,查看捐赠统计。
这个范围对于团队内部使用来说完全够用。技术层面,后端用 Spring Boot,前端用 Vue,数据库 MySQL,缓存 Redis,部署走 Docker。整个系统里没有特别高深的技术,但把每个功能做到顺手、稳定、不出错,才是这类工具型网站最核心的价值。
提示:这给所有做类似项目的朋友一个建议——先跟真实用户聊需求,再动手写代码。哪怕你是做课设,也要有一个像样的需求分析,答辩的时候老师问起来你才能答得有理有据。
1.3 角色与权限的划分
整个网站分三种角色:普通访客、注册用户(志愿者)、系统管理员。访客只能浏览首页项目列表和活动详情,注册用户可以报名和登记捐赠,管理员在后端管理页面做审核、统计、内容维护。
这块我最开始也纠结过要不要上 Spring Security 做细粒度权限控制,后来权衡了一下,这种小体量的公益网站用拦截器加用户角色字段就够了,Spring Security 那一套配起来成本高,而且误配置反而容易把路由全部锁死。最终方案是登录成功后把用户信息存到 Redis,自定义一个拦截器校验请求头的 Token,后台接口额外校验角色字段。
2. Spring Boot 版本选择与骨架搭建里的坑
2.1 版本选型要谨慎
项目启动第一件事就是选 Spring Boot 版本。我当时在 3.1.5 和 2.7.17 之间犹豫了很久,最后选了Spring Boot 2.7.17 + JDK 8。原因很现实:
一是团队的服务器还是 CentOS 7,上面已经跑着好几个老项目,JDK 8 是最稳妥的环境,不想为了一个新项目升级系统环境导致老项目受到影响;二是 Spring Boot 3.x 全面切换到了 Jakarta EE 命名空间,原来 Spring Boot 2.x 用的javax.*系列包全部换成了jakarta.*,这意味着网上大量基于 Spring Boot 2.x 的老教程和代码片段都没法直接用,排查问题的时候非常痛苦;三是当时项目用到的一些第三方依赖,比如用于导出 Excel 的 EasyExcel,对 Spring Boot 3.x 的适配还不够完善,硬上的话还得自己改源码。
这个决策现在回头看依然正确。项目上线之后一切稳定,而且团队成员普遍熟悉 Spring Boot 2.x 的写法,接手成本低。
2.2 项目目录结构怎么组织
我之前见过不少 Spring Boot 项目把所有类一股脑丢进controller、service、dao三个包里,业务一大就乱成一团。这次我特意按功能模块分包,结构如下:
com.greencity ├── common // 通用:返回结果封装、异常处理、常量 │ ├── result │ ├── exception │ └── constant ├── config // 配置类:Redis、跨域、拦截器、定时任务 ├── controller // 接口层 ├── service // 业务层 │ └── impl ├── mapper // MyBatis-Plus Mapper ├── entity // 数据库实体 ├── dto // 入参出参对象 ├── vo // 视图对象 └── utils // 工具类:日期、文件上传、导出等这里特别强调一下dto和vo分离的好处。以前我写项目喜欢直接把entity丢给前端,结果发现数据库字段一旦调整,前端接口也跟着变动,联调阶段每天都在改字段名。这次所有接口的入参都用dto承接,出参用vo组装,前端拿到的永远是干净的数据结构,数据库怎么改都不会直接影响到接口。
2.3 多环境配置的经验
application.yml里我拆分成了三份:application-dev.yml(本地开发)、application-test.yml(测试环境)、application-prod.yml(生产环境)。通过启动参数--spring.profiles.active=prod来切换环境。
这个多环境配置看着简单,但你架不住每个环境里还有细节差异。比如生产环境的数据库密码不能明文写在配置文件里,我这边是通过环境变量的方式注入:${DB_PASSWORD},部署的时候在 Docker Compose 里把环境变量传进去。开发环境则可以直接写测试库的密码,方便本地跑通流程。
2.4 依赖版本统一管理
项目依赖统一使用spring-boot-starter-parent做版本仲裁,额外引入的第三方依赖我会在properties节点里显式声明版本号。这里有个细节:千万别直接写版本号而不经过统一管理,尤其是 MyBatis-Plus 和 第三方工具类,版本冲突的时候排查起来非常头疼。我这个项目最终用的关键依赖如下:
| 依赖 | 版本 | 用途 |
|---|---|---|
| Spring Boot | 2.7.17 | 基础框架 |
| MyBatis-Plus | 3.5.3.2 | ORM 与分页插件 |
| Redis (spring-boot-starter-data-redis) | 内置管理 | 缓存与 Token 存储 |
| EasyExcel | 3.3.2 | 导出报名名单 Excel |
| HuTool | 5.8.22 | 日期、文件等工具方法 |
| Lombok | 内置管理 | 简化实体代码 |
3. 核心功能模块拆解:从报名到捐赠的完整链路
3.1 公益项目发布与状态流转
公益项目是整个网站的核心实体。一个项目从创建到结束,状态要经过好几个阶段:草稿、待审核、招募中、进行中、已结束、已下架。
管理端创建项目时先保存为草稿,提交之后进入待审核状态,管理员审核通过后项目状态变为招募中,开始在前台展示并且允许报名。到了活动开始时间,定时任务自动把状态改成进行中,活动结束后改成已结束。这个状态流转我觉得是这个系统里比较核心的代码逻辑,因为所有前台展示和报名逻辑都依赖状态字段来判断。
用 MyBatis-Plus 实现状态更新非常简单,但要注意一点:所有状态更新都必须通过 Service 层方法完成,禁止在 Controller 里直接操作实体状态。不然状态流转逻辑散落在各个地方,后面加新状态的时候到处翻代码,很容易漏改。
@Override @Transactional(rollbackFor = Exception.class) public void changeProjectStatus(Long projectId, Integer targetStatus) { Project project = this.getById(projectId); if (project == null) { throw new ServiceException("项目不存在"); } // 校验状态流转是否合法 if (!ProjectStatus.canTransfer(project.getStatus(), targetStatus)) { throw new ServiceException("非法的状态流转"); } project.setStatus(targetStatus); this.updateById(project); }这里的状态流转合法性校验是我觉得比较容易被忽略的设计点。比如一个"已结束"的项目不能被重新置为"招募中",这种业务规则如果只在页面上做限制,后台接口被人手动调用就会绕过校验。在 Service 层再做一次校验,才是真正把规则落到了代码层面。
3.2 志愿者报名:防重复与名额控制
报名的核心难点在于并发控制。想象一下,活动发布出来,50 个名额,瞬间涌进来 200 个人点报名,如果不做控制,可能 200 个人全部报名成功。
我的方案是两层控制:
第一层,前端限制。报名按钮在名额满了之后直接置灰,后端接口返回异常信息时也在页面提示。
第二层,后端加锁。数据库层面,报名记录表针对user_id和project_id建了唯一索引,重复报名直接被数据库拒绝;名额控制则用 Redis 的分布式锁,锁住project:signup:{projectId}这个 key,先查当前报名人数,小于名额才允许插入报名记录。
public synchronized void signUp(Long projectId, Long userId) { String lockKey = "project:signup:" + projectId; boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS); if (!lock) { throw new ServiceException("系统繁忙,请稍后再试"); } try { Integer count = signUpRecordMapper.selectCount( new LambdaQueryWrapper<SignUpRecord>() .eq(SignUpRecord::getProjectId, projectId) ); Project project = projectMapper.selectById(projectId); if (count >= project.getMaxPeople()) { throw new ServiceException("名额已满"); } // 插入报名记录 } finally { redisTemplate.delete(lockKey); } }不过这里有个小问题,synchronized只对单机部署有效,如果后面部署到多实例,得换成 Redis 的setIfAbsent或者 Redisson 的分布式锁。我这套目前只部署单实例,所以用synchronized配合数据库唯一索引已经足够稳了。
3.3 物资捐赠与信息脱敏
捐赠登记功能相对简单,用户填写物资类型、数量、联系人、手机号和备注,提交之后管理员后台能看到清单并主动联系对接。这里我做了两件事:
- 联系方式在前台列表页部分脱敏,比如 "138****1234",保护捐赠者隐私;
- 捐赠登记之后自动给用户发送一条站内消息,告知"捐赠意向已收到,工作人员将在 2 个工作日内联系您"。
3.4 后台管理端的实现
后台管理端的页面是独立的一套 Vue 页面,接口走/admin/**前缀,拦截器校验管理员 Token。功能包括:
- 项目管理:审核、编辑、下架、置顶操作;
- 报名管理:查看报名列表、一键导出 Excel;
- 捐赠管理:查看捐赠登记、标记"已联系""已收到"状态;
- 用Excel导出报名名单,用了 EasyExcel,导出的时候要考虑字段格式,手机号要设置成文本格式。
@GetMapping("/admin/export") public void exportSignUpList(HttpServletResponse response, Long projectId) throws IOException { List<SignUpExcelVO> list = signUpService.getSignUpListForExport(projectId); response.setContentType("application/vnd.ms-excel"); response.setCharacterEncoding("utf-8"); response.setHeader("Content-Disposition", "attachment;filename=signup.xlsx"); EasyExcel.write(response.getOutputStream(), SignUpExcelVO.class) .sheet("报名名单") .doWrite(list); }导出功能做出来之后,团队负责人的接受度一下子提升了很多,因为之前他们最头疼的就是对着 Excel 表格手动数人头、打电话通知。软件做得再花哨,不如解决一个他们真正的痛点。
4. 表结构设计:公益场景下的数据模型怎么落地
4.1 数据表总览
整个项目一共设计 8 张表:用户表、公益项目表、报名记录表、捐赠记录表、求助信息表、公告表、系统消息表、审核日志表。数据库名greencity_charity,统一 utf8mb4 字符集,时间字段都用datetime。
字段设计的核心原则是三句话:状态字段一律用tinyint加数据字典说明;金额和数量字段类型要准确,数量用int或decimal(10,2);所有表都带create_time、update_time和deleted逻辑删除标记。
4.2 核心表的字段设计
公益项目表的结构大致如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| title | varchar(100) | 项目标题 |
| cover_image | varchar(255) | 封面图路径 |
| content | text | 项目详情 |
| location | varchar(200) | 活动地点 |
| start_time | datetime | 开始时间 |
| end_time | datetime | 结束时间 |
| max_people | int | 招募名额 |
| status | tinyint | 0草稿 1待审核 2招募中 3进行中 4已结束 5已下架 |
| create_time | datetime | 创建时间 |
报名记录表核心字段包括project_id、user_id、sign_time,唯一的联合索引uk_project_user(project_id, user_id)用来防重复报名。这里有个实践经验:字段类型宁可多花一点存储空间,不要为了省空间导致后续扩展困难。比如状态字段用tinyint而不是bit,虽然只有一个字节和半个字节的差别,但tinyint可以扩展更多状态,而bit只能表示 0 和 1,改动表结构非常麻烦。
4.3 索引设计的心得
索引是整个表结构里每张表最值得花时间想的地方。我的设计逻辑是:先列出查询最频繁的 SQL,再针对 WHERE 条件建索引。
以公益项目列表为例,前台按"招募中 + 发布时间倒序"查项目:
SELECT * FROM project WHERE status = 2 AND deleted = 0 ORDER BY create_time DESC LIMIT 10;这种查询建一个(status, create_time)的联合索引就能直接命中。报名记录表高频查询是"某个项目有多少人报名"和"某个人报名了哪些项目",所以除了上面说的唯一索引外,还单独为user_id建了一个索引。
提示:索引不是越多越好,每个索引都会拖慢写入速度。我的经验是最初按需建索引,等数据量大了再通过慢查询日志去补,绝不提前做过度设计。
4.4 数据字典与状态枚举
状态字段直接在代码里用枚举类管理,避免魔法数字散落各处。比如项目状态:
@Getter public enum ProjectStatus { DRAFT(0, "草稿"), PENDING_REVIEW(1, "待审核"), RECRUITING(2, "招募中"), ONGOING(3, "进行中"), FINISHED(4, "已结束"), OFFLINE(5, "已下架"); private final int code; private final String desc; public static boolean canTransfer(int from, int to) { // 状态流转规则 } }前台页面的展示文本直接通过枚举的desc输出,没有另外维护一张数据字典表。这个小体量项目这么做完全够用,省了字典表的关联查询。如果后面系统变复杂、状态枚举变多,再考虑把枚举挪到数据库字典表也不迟。
5. 定时任务、消息提醒与站内通知的实现
5.1 定时任务的使用场景
公益网站里定时任务的场景还挺多的,这也是 Spring Boot 里不太起眼但非常实用的能力。我这个项目里用到了三类:
- 每天凌晨 2 点,自动将已过结束时间的项目状态改为"已结束";
- 活动开始前 1 天,给所有报名志愿者发送站内消息提醒"明天活动请准时参加";
- 每 10 分钟扫描一次求助信息,对超过 7 天未处理的求助自动标红置顶。
Spring Boot 自带的@Scheduled注解就能实现,不用引入额外的调度框架。
5.2 @Scheduled 单线程的坑
实现定时任务之后,我第一时间发现一个问题:多个定时任务如果执行时间都卡在同一个点,后面的任务会等前面的执行完才开始。因为 Spring 默认的@Scheduled任务是单线程执行的。
解决办法是实现SchedulingConfigurer接口,配置一个自定义的线程池:
@Configuration public class SchedulingConfig implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5)); } }这个坑网上有很多人踩过,属于 Spring Boot 定时任务最经典的性能问题之一。如果你不配置线程池,一旦某个任务执行时间过长(比如调用了外部接口等待响应),其他任务就会被阻塞,排查这个问题真的要花不少时间。
5.3 站内消息的实现方案
消息提醒这块我没有集成消息中间件,虽然热搜词里很多人提到 Spring Boot 整合 ActiveMQ,但从实际需求来看,这种每天几十条消息的场景,用一张数据库表存站内消息完全够用,没必要为了技术而技术。
系统消息表结构很简单:user_id(接收者)、title、content、is_read(是否已读)、create_time。定时任务扫描到需要提醒的用户后,往表里批量插入记录即可。前端页面有个消息铃铛,展示未读数量。
如果项目体量再大一些、消息上万级,我会考虑引入消息队列来削峰和异步解耦。但对你来说,最要紧的是先跑通业务逻辑,而不是让架构一步到位。
5.4 为什么当前不用 ActiveMQ
顺带说一句很多人关心的事:Spring Boot 整合 ActiveMQ 是不是必须掌握的技能?答案是看场景。ActiveMQ 这种队列最擅长的是异步削峰、解耦高并发请求、保证消息不丢失。而公益网站最核心的报名场景,500 人同时报名也远没有到要用消息队列来削峰的地步。数据库加个唯一索引、代码加个锁,就轻松扛住了。真要硬上 ActiveMQ,反而增加了部署和运维复杂度,没什么好处。
如果你的项目后面有复杂通知流,比如报名的同时要通知管理员、要发短信、要写日志,这几个动作可以塞进队列异步执行,那再考虑整合 ActiveMQ 也不迟。
6. 宝塔 Docker 部署上线与运维备份
6.1 部署方案的选择
项目部署我最终选了宝塔面板 + Docker Compose。这个组合在中小型项目里相当省心:宝塔负责管理 Nginx、MySQL、Redis 这些基础服务,Docker 只用来跑 Spring Boot 的 Java 应用镜像。说实话,如果你没有专门的运维人员,这个方案是成本最低、维护最简单的。
用宝塔部署 Docker 其实没你想的复杂,安装好宝塔之后,在软件商店装好 Docker 管理器,直接docker-compose up -d就能拉起来。
6.2 Dockerfile 与 Compose 文件
Spring Boot 项目打成 jar 包之后,写一个简单的 Dockerfile:
FROM openjdk:8-jre-alpine MAINTAINER greencity ENV TZ=Asia/Shanghai RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone WORKDIR /app COPY greencity.jar /app/greencity.jar EXPOSE 8080 ENTRYPOINT ["java", "-Xms256m", "-Xmx512m", "-jar", "greencity.jar", "--spring.profiles.active=prod"]这里有两个细节:
- 设置时区
TZ=Asia/Shanghai,否则容器内默认是 UTC 时间,定时任务会比北京时间晚 8 小时执行,这不是 bug,是时区没设置对; - 限制 JVM 内存
-Xms256m -Xmx512m,防止容器把宿主机内存吃满,尤其是云服务器只有 2G 内存的时候。
docker-compose.yml 里我只把 jar 包的容器编排进去,MySQL 和 Redis 直接用宝塔装的,容器内通过宿主机 IP 访问:
version: '3' services: greencity: image: greencity:latest container_name: greencity-app restart: always ports: - "8080:8080" environment: - DB_HOST=172.17.0.1 - DB_PORT=3306 - DB_NAME=greencity_charity - DB_USERNAME=root - DB_PASSWORD=${DB_PASSWORD} - REDIS_HOST=172.17.0.1 - REDIS_PORT=6379172.17.0.1是 Docker 默认网桥的宿主机地址,容器内可以通过这个地址访问宿主机的 MySQL。这个细节不知道的话,你会碰到容器里连不上数据库的问题,排查很久也找不到原因。这个问题在我帮别人排错时经常遇到。
6.3 数据库备份与恢复
公益网站的数据不像电商数据那样高频变动,但报名记录、捐赠记录都属于宝贵数据,备份策略必须要做。我用的是宝塔自带的计划任务,每天凌晨 3 点执行一次数据库备份,保留最近 7 天的备份文件:
mysqldump -uroot -p${DB_PASSWORD} greencity_charity > /backup/greencity_$(date +%Y%m%d).sql find /backup -name "greencity_*.sql" -mtime +7 -exec rm {} \;恢复的时候更简单:
mysql -uroot -p${DB_PASSWORD} greencity_charity < /backup/greencity_20250115.sql备份这事没什么技术含量,但丢了数据再回来抢救,那才叫欲哭无泪。我一个朋友的项目就是忘了配备份,数据库磁盘坏了,两年的用户数据全没了,最后只能重来。
6.4 前端 Vue 的部署与 Nginx 配置
前端 Vue 项目npm run build之后生成 dist 目录,放到宝塔网站的根目录下,再配置 Nginx 反向代理,把/api开头的请求转发到后端 8080 端口:
server { listen 80; server_name charity.example.com; root /www/wwwroot/greencity/dist; index index.html; # 前端路由 history 模式配置 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; } }前端开发环境的话,直接在 Vite 或 Vue CLI 的配置里设置开发代理,避免开发时跨域问题。这个我在下面踩坑清单里会细说。
7. 全流程踩坑清单:这些坑值得单独拿出来说
7.1 时区偏移导致定时任务延迟八小时
这个坑发生在上线第二天,定时任务本该凌晨 2 点跑,结果早上 10 点才发现数据还没更新。查了日志没有任何报错,最后定位到是容器时区问题。
Docker 容器默认的时区是 UTC,比北京时间晚 8 个小时,所以定在凌晨 2 点的任务实际上在上午 10 点才执行。解决办法是 Dockerfile 里显式设置时区:
ENV TZ=Asia/Shanghai RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone另外,JDBC 连接串还要加上serverTimezone=Asia/Shanghai,MySQL 驱动连接时如果没有指定时区,产生的日期时间数据会有偏差。
7.2 跨域配置:开发环境与生产环境分开处理
Vue 前端开发时跑在http://localhost:8081,后端接口http://localhost:8080,浏览器会有跨域问题。一开始我在 Spring Boot 里加了全局 CORS 配置,开发联通之后上生产又出问题——Nginx 反代之后不需要后端跨域了,但配置了也不影响,然而前端带着 Cookie 跨域时,allowedOrigins不能写*,必须写具体域名,否则请求会丢 Cookie 导致登录态失效。
最后的做法是:开发环境前端用代理转发,后端可以不用配置 CORS;生产环境用 Nginx 反代,天然同源。
7.3 中文乱码:从请求到数据库全链路排查
出现过一次前端提交中文数据,后台存进去变成问号的乱码问题。排查思路从浏览器请求头开始,再到 Tomcat 配置、数据库连接参数、MySQL 表字段字符集,最后发现是两处字符集没对齐:
- 数据库连接串没带
characterEncoding=utf8,导致驱动写入时用系统默认字符集编码; - 表的字符集虽然是 utf8mb4,但个别字段是老的 utf8 字符集,改表的时候没统一。
解决方案两条都补齐:
spring: datasource: url: jdbc:mysql://172.17.0.1:3306/greencity_charity?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghaiutf8和utf8mb4的区别很多人不清楚,简单说,utf8 最多支持 3 字节字符,四字节的 emoji 表情存不进去;utf8mb4 是 MySQL 8.0 之后的推荐字符集,能存 emoji 和生僻字。建议所有表直接建库的时候就用 utf8mb4。
7.4 Vue 上传图片返回 404 的问题
公益项目需要上传封面图,本地开发时图片传到本地磁盘没问题,上生产之后图片在服务器上传成功,但访问图片的 URL 一直 404。
排查之后发现问题出在 Nginx 配置:/uploads路径没有映射到后端上传目录。在 Nginx 里加一条:
location /uploads/ { alias /www/wwwroot/greencity/uploads/; }这类问题本质是前端请求路径用http://域名/uploads/xxx.jpg访问图片,Nginx 不知道该去哪里找文件。部署文档里务必写清楚所有静态资源目录的映射关系。
7.5 前端表单校验和后端安全校验的差距
Vue 中表单校验做得再严谨,也只是一种体验优化,真正的安全边界要由后端兜底。比如用户提交捐赠登记时,前端限制手机号长度,但有人直接调用接口,传一个乱七八糟的手机号也能入库。
我给后端接口补了参数校验,用 Spring Boot 的@Validated+javax.validation注解:
@Data public class DonationDTO { @NotBlank(message = "物资类型不能为空") private String goodsType; @NotBlank(message = "联系人不能为空") private String contactName; @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") private String contactPhone; }很多入门教程会忽略这块,但参数校验是接口开发里最基础也最不能省的一层。校验框架本身就是为这种场景设计的,别自己去手写 if 判断。
7.6 Redis 缓存穿透与击穿
项目列表页在一段时间内访问量很高,我加了 Redis 缓存来减轻数据库压力。但上线后发现一个典型问题:如果有人恶意用不存在的 ID 去刷新项目详情接口,Redis 里查不到就老是穿到数据库,高并发下会把数据库打垮。
我的处理方案是两种策略同时用:
- 空值缓存:查不到数据时,把空值也缓存 60 秒,防止恶意请求直接打到数据库;
- 布隆过滤器:这个方案更彻底,但如果项目数据量小,空值缓存就够了,没必要引入额外的中间件。
8. 关于这个项目想再聊几句
做完整个项目之后,我最大的感受是:这种爱心公益类型的网站,技术难度真的不高,真正的精力都花在了理解使用场景和打磨操作细节上。
比如报名页面,我开始设计的是表格形式,字段很多,后来实地测试的时候发现很多年纪大的志愿者根本填不完那么长的表单。最后改成极简模式:姓名 + 手机号 + 报名人数,就这三项,体验立刻好了很多。
还有一个细节让我记忆很深:团队负责人跟我说过,每次活动结束之后都要手动整理一张"感谢爱心人士捐赠清单",发到群里表达谢意。我就加了一个简单的功能,管理员在捐赠列表里勾选要感谢的条目,一键生成一张感谢图。这个功能我前后只写了不到两个小时,但团队里的人从此记着这个系统好。
说到底,技术永远是为业务服务的。如果你也在做类似的项目,建议从真实的使用者那边挖需求,把最基础的功能做扎实,比堆砌一堆用不上的高级特性要强得多。
这个项目做完之后,还有几个可以继续深化的方向:对接微信公众号模板消息,让志愿者在微信里直接收到活动提醒;再就是报名签到环节,做成二维码签到,活动当天扫一下就能统计到场人数。这些我后续会慢慢补上,有空的话也会继续写文章分享具体实现。