最近做了一套基于SpringBoot的环保宣传网站系统,前端直接用了HTML页面,没有上前后端分离那套,整个过程踩了不少坑,也沉淀了一些经验。今天把整个设计思路、核心实现和排查细节拆开来讲,特别是后端怎么和HTML页面配合、数据库怎么设计、还有那些在文档里根本不会写的实际问题。这个项目适合正在做课程设计、毕业设计,或者想快速上手SpringBoot+静态页面整合的朋友参考,看完基本能复现一版完整可运行的环保网站。
1. 项目背景与核心需求拆解
1.1 环保网站到底要解决什么实际问题
不要一听到“环保网站”就急着写代码。先想清楚,一个没有商业公司支撑的宣传类网站,本质需求就三块:内容展示、互动收集、后台管理。
内容展示指的是环保新闻、环保知识、政策动态这些信息类页面,访客不需要登录就能浏览,这也是网站的门面。互动收集指的是留言板或者反馈表单,让访客能提交自己的意见、建议,有些需求还会加上在线举报乱排乱放的功能。后台管理则是让管理员能发布内容、审核留言、维护用户信息。
如果没有这些分析,很容易把网站做成一个纯静态的展示页面,没有任何动态能力,后期维护内容只能改HTML文件,那这套系统就失去了存在的意义。所以我在项目一开始就把需求敲定为:前台展示加后台管理的双层结构,用户、新闻、知识库、留言四大核心模块。
1.2 为什么选SpringBoot加HTML这套组合
现在很多人一问就是前后端分离,SpringBoot搭个后端接口,前端用Vue或React。但这套方案对于一个偏内容展示的环保网站来说有点用力过猛,尤其是开发周期短、人手少的时候。
选SpringBoot的理由很直接:内置Tomcat,打成jar包就能跑,省去配置外部容器的麻烦;Spring Data JPA或者MyBatis都能快速接入MySQL;Starter机制让依赖管理变得特别简单。这几条就能把开发环境搭建的时间压缩到半小时以内。
前端用HTML而不是Vue,并不是说静态页更好,而是这个场景下“够用”比“花哨”更重要。环保网站的访客主要就是普通浏览用户,他们打开页面、看新闻、翻知识库、发留言,需要的交互复杂度很低。用HTML加少量JavaScript,配合后端返回的JSON或直接模板渲染,完全能覆盖这些需求。而且纯HTML页面在搜索引擎收录方面更直接,维护门槛也低,找一个会写基础前端的人就能改页面。
1.3 功能需求优先级怎么排
我按使用频率和重要程度把事情拉了一个清单:
| 模块 | 功能点 | 优先级 |
|---|---|---|
| 用户模块 | 注册、登录、退出、查看个人信息 | 高 |
| 内容展示 | 环保新闻列表、详情、分页 | 高 |
| 知识库 | 分类展示环保常识、科普文章 | 高 |
| 互动反馈 | 留言提交、留言列表展示 | 中 |
| 后台管理 | 内容发布、留言审核、用户管理 | 中 |
| 辅助功能 | 搜索、热门排行、访问统计 | 低 |
这里要特别注意,用户模块虽然叫“用户”,但普通访客并不强制登录才能看内容,登录只是为了留言的时候可以关联身份,后台管理员则要校验权限。我最后把登录状态用Session保存,没有引入复杂的Spring Security,因为项目只需要区分“普通访客”和“管理员”两种角色,手写拦截器就可以搞定,引入Security反而增加学习成本。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与架构设计
2.1 后端框架选型背后的小九九
SpringBoot版本我用了2.7.x,这个版本在2024年之前都算长期维护版本,稳定性好,网上资料也最多,遇到问题能搜到大量解决方案。JDK选的是1.8,虽然已经比较老,但胜在兼容性极高,很多事业单位的服务器环境还在用老版本,如果你打算部署到云服务器,1.8几乎不会出问题。
数据访问层我选了Spring Data JPA。很多做单体项目的人习惯用MyBatis,但我在这类内容管理型系统里更推荐JPA。原因很简单:表结构相对规整,实体对象和数据库字段的映射关系比较简单,JPA的自动建表功能非常方便。你只要定义好Entity类,启动项目的时候表结构会自动生成,省去了手写建表SQL的步骤,这对原型开发阶段尤其友好。
当然,JPA的复杂查询确实不如MyBatis灵活,所以我只在需要拼接多条件搜索时才用“@Query”写原生SQL,其余简单查询全部用方法名派生。
2.2 前端页面为什么坚持纯HTML
这里说的HTML并不仅仅是裸的HTML文件,而是“HTML + CSS + JavaScript + jQuery”。我没有用Thymeleaf模板引擎,也没有把所有数据都渲染到服务端页面。设计思路是这样的:
页面主体框架是静态HTML文件,放在src/main/resources/static目录下。动态数据的获取通过AJAX请求后端接口,然后把返回的数据拼接到页面上。这种思路的好处是页面文件可以直接用浏览器打开调试,后端接口完全独立,即便前端先写好了,后端还没完成,也不影响UI展示。
有人可能问,那不还是前后端分离吗?区别在于这里不涉及Node.js、构建工具、前端路由那一整套体系。HTML文件直接由SpringBoot的静态资源处理器托管,不需要额外配置。对于这种展示型网站,页面数量一般在二十个以内,不会出现模块复用和组件化压力,手写HTML反而更透明,出问题一眼就能看到。
2.3 数据库表结构如何设计
我最终设计了五张表:用户表、新闻表、知识库表、留言表、管理员表。后三张表和用户表之间用外键逻辑关联,但没有在数据库层面强制建外键,因为删除操作会受约束,处理起来麻烦。
用户表的核心字段:
sql复制CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(100) NOT NULL,
`nickname` varchar(50) DEFAULT NULL,
`email` varchar(100) DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
密码字段我存的是MD5值,没有引入BCrypt,主要考虑到很多刚入门的朋友用MD5工具类更熟悉。但正式生产的话,建议至少把密码加上随机盐再做一次哈希。
新闻表存储环保行业动态:
sql复制CREATE TABLE `news` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(200) NOT NULL,
`content` text,
`cover_image` varchar(255) DEFAULT NULL,
`type` varchar(20) DEFAULT '环保新闻',
`views` int(11) DEFAULT 0,
`publish_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这里views字段用来做点击量排序,我后续实现了一个简单的热门新闻推荐,就是按views倒序排。cover_image存的是图片上传后的访问路径,不存二进制文件本身,这是很多新人容易犯的错误——直接把图片塞到数据库里,最后数据库膨胀到几百兆,性能急剧下滑。
留言表需要挂接用户信息:
sql复制CREATE TABLE `message` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) DEFAULT NULL,
`content` varchar(500) NOT NULL,
`status` tinyint(1) DEFAULT 1,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
status字段表示留言是否可见,管理员在后台审核的时候可以临时隐藏一些垃圾留言,不用删除数据,留着审计用。
3. 后端核心模块设计与实现
3.1 登录注册与权限拦截的轻量方案
用户模块是后面所有操作的基础。注册接口接收username和password,先检查用户名是否重复,然后插入用户记录。登录成功后把用户ID和用户名扔进Session。
真正需要注意的拦截器。我写了一个LoginInterceptor,通过HandlerInterceptor接口实现。拦截器里只做两件事:判断Session里有没有登录标记,判断请求路径是不是/admin/**。如果是/admin下面的路径并且当前用户不是管理员,直接重定向到登录页。
拦截器注册的时候要特别注意排除静态资源:
java复制registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "/js/**", "/images/**", "/login", "/user/login", "/register", "/news/**", "/knowledge/**", "/message/save");
第一次做的时候我把静态资源忘了,结果登录页的CSS样式全被拦截,整个页面光秃秃的。后来在排除列表里加上了/css/**这些路径才算正常。这个细节一定要记住。
3.2 环保资讯模块:从发布到展示全流程
资讯模块是网站访问量最大的地方,功能上包括后台发布新闻、前台分页列表、详情页阅读、点击量自增。
后台发布的时候,我提供了一个简单的富文本编辑区域,没有接富文本编辑器,就是普通的textarea,内容用HTML格式存储。这样在前台详情页可以直接通过${news.content}或者响应JSON中的content字段展示成带格式的文本。如果你想做得更完善,可以接一个轻量编辑器,但是后端存储方案不需要变,还是HTML字符串。
分页查询是整个模块的难点。我直接用JPA的分页接口:
java复制Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "publishTime"));
Page<News> pageData = newsRepository.findAll(pageable);
这里有个很关键的地方:前端传过来的页码从0开始还是从1开始,一定要约定清楚。我的HTML页面点击分页按钮时传的是第1页、第2页这种对用户友好的概念,而JPA的PageRequest.of里的page参数是从0开始的,所以接口里做了一步转换:
java复制int jpaPage = (page <= 1) ? 0 : page - 1;
很多小兄弟第一次写这个会出现首页是空的情况,就是没做这个减一操作。
详情页的浏览量自增,我采用了直接更新法:
java复制newsRepository.updateViews(id);
对应的JPQL:
java复制@Modifying
@Query("update News n set n.views = n.views + 1 where n.id = ?1")
int updateViews(Integer id);
注意这里必须要标注@Modifying,并且调用的时候要加事务注解,否则会报TransactionRequiredException。别问我怎么知道的,这个问题排查了整整一个下午。
3.3 环保知识库:分类与搜索的实现细节
知识库和新闻的区别是,知识库更偏向科普性质,内容相对稳定,一般不改动。所以我给它加了分类字段,比如“大气保护”、“水资源”、“垃圾分类”、“绿色生活”等。
分类的实现很简单,就是category字段存字符串。列表页根据用户点击的分类查询:
java复制List<Knowledge> list = knowledgeRepository.findByCategoryOrderByCreateTimeDesc(category);
搜索功能我用了一个带条件判断的原生SQL查询。为什么不用方法名派生?因为搜索条件可能为空,方法名派生写不出“关键字为空则忽略这个条件”的逻辑。所以写了这样一段:
java复制@Query(value = "select * from knowledge where (?1 is null or title like concat('%', ?1, '%')) and (?2 is null or category = ?2) order by create_time desc", nativeQuery = true)
List<Knowledge> searchKnowledge(String keyword, String category);
传入null就不过滤对应条件,这个写法在新闻搜索里也能复用。很多人在写模糊搜索的时候会写成like '%?1%',在JPA原生SQL中这样是错的,必须用concat拼接,我一开始就是踩了这个坑,查出来的数据永远为空。
3.4 留言反馈模块:异步提交与展示
留言功能是最能体现“HTML+AJAX”交互的地方。前台留言页面有一个输入框,用户输入留言内容后点击提交,我用jQuery发起POST请求:
javascript复制$.ajax({
url: '/message/save',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({content: msgContent, userId: currentUserId}),
success: function(res) {
if(res.code === 200) {
alert('提交成功,审核后展示');
location.reload();
}
}
});
后端接口接收JSON格式的请求体,用@RequestBody绑定参数:
java复制@PostMapping("/save")
public Result save(@RequestBody MessageRequest request, HttpSession session) {
Message message = new Message();
message.setContent(request.getContent());
message.setUserId((Integer) session.getAttribute("userId"));
message.setStatus(1);
messageRepository.save(message);
return Result.success();
}
这里要注意,如果页面用了form表单提交而不是AJAX,后端参数接收方式完全不同,要么用HttpServletRequest拿参数,要么用@RequestParam。我建议在这个场景统一用AJAX+JSON,因为数据量小,不需要整个页面刷新。
留言展示列表我设置了只查status=1的记录,也就是审核通过的,这个把关功能就是后台管理的核心价值之一。
4. 前端页面实现关键点
4.1 页面目录结构与资源组织方式
SpringBoot访问静态页面的默认根路径是classpath:/static/。我把所有HTML页面直接放在这个目录下,CSS和JS单独分文件夹管理。目录结构是:
text复制src/main/resources/static/
├── css/
│ └── style.css
├── js/
│ └── common.js
├── images/
│ └── banner/
├── index.html
├── news-list.html
├── news-detail.html
├── knowledge.html
├── message.html
└── admin/
├── admin-login.html
└── admin-news.html
这样每个页面都是独立的HTML文件,访问http://localhost:8080/index.html就能直接打开。后台管理页面单独放到admin子目录里,在URL结构上和前台做区分,也给拦截器的路径匹配提供了便利。
4.2 用AJAX拉取数据并动态渲染
首页最新新闻版块,我是通过页面加载完成后发AJAX请求来拿数据的:
javascript复制$.get('/news/latest', function(data) {
var html = '';
for(var i = 0; i < data.length; i++) {
html += '<li><a href="news-detail.html?id=' + data[i].id + '">' + data[i].title + '</a></li>';
}
$('#newsList').html(html);
});
这个模式决定了后端接口直接返回JSON数组,不需要依赖任何模板引擎。优点很明显,前端逻辑清晰,页面渲染不阻塞,后端接口可以做单元测试。缺点也有,如果页面需要SEO,搜索引擎爬虫拿到的只是空框架,内容要靠JS异步加载,收录效果会差一些。好在环保网站通常不是靠SEO盈利的,对这一点不太敏感。
4.3 色彩搭配与样式细节
环保网站给人的第一印象就是“绿色”。我主色调用的是一套自然绿色系,主色#2E8B57,辅助色#B0E0E6,文字色#333333。背景色用了浅灰白,避免大面积绿色造成视觉疲劳。
这里要说一个做前端样式的核心原则:不要所有元素都用一种绿,会显得很死板。比如导航栏用深绿,按钮用亮绿,提示文字用墨绿,同一个色系的不同深浅才能营造层次感。我建议使用CSS变量统一管理:
css复制:root {
--primary-color: #2E8B57;
--secondary-color: #66CDAA;
--bg-light: #F5F5F5;
}
之后所有页面引用这些变量,后期想改主题色,只需要改一处。实测这个细节对整体观感提升很大,评审老师一眼就能看出页面是有设计感的。
布局上我使用了栅格系统,但没有引入Bootstrap,而是自己写了一个简单的12栅格工具类。主要是为了减少外部依赖,也让HTML结构更干净。响应式方面用@media做断点,平板和手机能自动从多列流式布局变为单列。对于一个展示型网站,能达到这个程度就已经合格了。
5. 核心功能实操细节与踩坑记录
5.1 数据库连接池配置的隐藏坑
SpringBoot 2.x默认使用HikariCP连接池,性能很强。我以为配置一个application.yml就够了,结果本地运行一切正常,部署到服务器之后频繁出现“Connection is not available, request timed out”的报错。
后来排查发现,问题出在MySQL的wait_timeout默认是8小时,如果一段时间没有数据库访问,连接会被服务器断开,但连接池里的连接对象还认为自己是可用的。解决这个问题最简单的办法是给数据源配置增加一条:
yaml复制spring:
datasource:
hikari:
connection-test-query: SELECT 1
这条配置让连接池每次拿连接前先执行一个探活SQL,发现死连接就丢弃重连。加了之后服务稳定运行了几周没再报错。另外要注意数据库URL里指定时区参数serverTimezone=Asia/Shanghai,否则日期时间会出现8小时偏移。
5.2 分页查询导致的性能问题
上线之后发现新闻列表页打开速度不稳定,尤其是翻到后面几页时响应明显变慢。我看了一下慢查询日志,发现全表扫描了。原因是没有给publish_time字段建索引。
JPA的自动建表功能不会自动优化索引,需要手动加一个:
sql复制ALTER TABLE news ADD INDEX idx_publish_time (publish_time);
加上索引后,排序查询速度提升了一个数量级。这种内容展示型网站的数据量虽然不特别大,但如果没有索引,等到数据积累到几万条,体验会越来越糟。
5.3 JSON返回时的时间格式问题
前后端通过JSON通信后,时间的显示格式变成了"2024-05-18T10:30:00.000+00:00"这种带T的UTC格式,页面直接显示出来特别难看。
解决办法是在实体类的时间字段上加@JsonFormat注解:
java复制@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date publishTime;
如果项目里有很多时间字段,也可以在application.yml里全局配置:
yaml复制spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
建议两种都用,全局兜底加字段注解兜底,双保险。
5.4 图片上传与访问路径映射
后台发布新闻需要上传封面图,前端用的是<input type="file">,通过FormData表单提交。后端接口接收MultipartFile文件,然后保存到本地磁盘。
这里有一个特别容易踩的坑:保存目录如果写成绝对路径,比如E:/upload/,开发环境可能没问题,但部署到Linux服务器就找不到这个目录了。我的做法是在配置文件里定义好上传路径,并且使用相对当前项目运行目录的路径:
yaml复制file:
upload-dir: ./upload/
然后写一个静态资源映射类,把/upload/**路径映射到磁盘上的实际目录。这样不管jar包在哪跑,都统一使用jar包所在目录下的upload文件夹。如果忘了做映射,页面<img src="/upload/xxx.jpg">会一直404。
6. 常见问题与排查技巧实录
6.1 页面中文乱码
这个问题出现在Windows环境下,HTML文件用GBK编码保存,结果浏览器用UTF-8解析,全部变成乱码。排查思路很简单,所有HTML文件统一用UTF-8编码保存,并且在<head>里加上:
html复制<meta charset="UTF-8">
如果你用的是IDEA,右下角可以看到当前文件编码格式,直接把它改掉再复制内容就行。后端返回的JSON里如果有中文,也要确认响应头里带了Content-Type: application/json;charset=UTF-8,SpringBoot默认会带,但如果你手动设置过HttpServletResponse可能就会覆盖。
6.2 后端接口明明返回了数据,前端就是渲染不出来
这类问题我遇到多次,最典型的表现是Network面板里能看到接口返回200,数据也有,但页面列表区域是空的。原因是前端遍历数据时用了错误的字段名。比如后端返回publishTime,前端写成了publish_time,JavaScript拿不到undefined,拼接出来的HTML就成了空。
解决方案是约定API文档,后端实体类的JSON字段用驼峰命名,前端也统一用驼峰。如果不想改,可以用@JsonProperty("publish_time")让后端输出下划线格式。但最推荐的做法是前后端统一,这个项目里就统一成驼峰。
6.3 刷新后台页面出现404
后台管理页面如果在/admin目录下,链接跳转和页面刷新会出现404或者跳到前台登录页。根本原因是路径匹配问题。比如我在后台页面用相对链接跳转news-list.html,实际URL变成了/admin/news-list.html,这个文件确实存在,但被拦截器拦了。或者我用了绝对路径/news-list.html,又因为没带上管理员验证,被重定向回登录页。
最好是在后台页面的所有请求URL里带上公共前缀,比如/admin/news/list这种接口路径,不要让页面访问和接口访问混在一起。接口路径统一加上/api前缀,页面路径就只管页面跳转,这样就不会打架。
6.4 用户注册时出现SQL插入异常
注册功能上线后,有用户输入了一个包含单引号的昵称,结果直接报SQL语法错误。这是典型的SQL注入风险。虽然我用的是JPA,参数绑定机制会避免这个问题,但在一些自定义原生SQL里必须警惕。比如我写的搜索SQL用了参数绑定?1,安全;但如果在代码里自己拼接字符串,就会出问题。
给用户输入加一层校验总是无害的,我从前端到后端都做了长度限制和非法字符过滤。前端限制输入长度防止恶意提交,后端再校验一次防止绕过前端,双重保障。
7. 这套系统还能怎么扩展
做完这个环保网站之后,我最大的体会是:一个项目的价值不在于用了多少新技术,而在于把基础的东西做得够不够扎实。像这个系统里用的SpringBoot、JPA、MySQL、jQuery,都是非常成熟的技术,但整合起来需要考虑的问题仍然不少。如果你对这套项目感兴趣,有几个方向可以继续往下走。
一个是把访问统计做完整。现在只做了新闻浏览量,可以加上全站PV/UV统计,用拦截器或者过滤器记录每次请求,存到一张单独的表里,然后做一个简单的后台图表展示。
另一个是增加数据导出功能。后台留言审核列表可以加一个导出Excel的接口,用Apache POI把数据写成表格文件,这样管理人员不用登录数据库就能做数据分析。
再就是内容审核机制。现在留言只有“上架/下架”两态,可以扩展成“待审核、通过、拒绝”三态,并且支持管理员回复留言。这些改动都不涉及底层框架变化,只是在现有模块上增加字段和页面按钮的事。
最后想说的是,我在这个项目里最开始特别追求用最新的技术,比如想上Redis做缓存、用Vue重写前端、加消息队列,后来被导师一句话点醒:网站的核心是内容能正常展示、用户能正常留言、管理员能正常管理,先把这些做到极致,比堆技术名词重要得多。事实证明,这套简洁的方案在性能和可维护性上都表现不错,部署到一个1核2G的云服务器上,稳定运行了几个月没有出过事故。如果你也在做类似的环保或者内容展示类网站,希望这篇分享能让你少踩几个坑。
