☰
JavaWeb企业门户网站完整项目实战:Spring Boot+MySQL部署全流程
2026/9/26 14:26:43 网站建设 项目流程

企业门户网站是我接触最多的外包项目类型之一,老客户隔三差五就会来一句:“帮我做一套完整的官网,要带后台,能自己发新闻、管产品。”所谓的完整企业门户网站源码项目实战,正是应对这种需求的标准打法。这篇文章我就以一套真实的 JavaWeb 完整项目为例,从需求拆解、数据库设计、后端接口、前台模板渲染,到服务器部署、常见坑位排查,完整过一遍。适合正在做 JavaWeb 课程设计、刚入行想拿完整项目练手、或者小团队需要一套拿来改改就能上线门户系统的朋友,内容偏实操,照着做基本能跑通。

1. 项目整体设计思路与方案选型

1.1 需求梳理:做门户网站到底要做什么

很多人一上来就搜“企业门户网站源码”,结果下载了一堆乱七八糟的残包,要么只有前台没有后台,要么数据库脚本缺失,要么加密代码根本没法改。所以在动手之前,得先把“完整”这两个字定义清楚。

一个能实际交付的企业门户,必须包含这几块:前台展示、后台管理、数据库脚本、部署说明。前台通常有首页、公司简介、新闻资讯、产品中心、在线留言、联系我们这些页面;后台最少要有登录、栏目管理、新闻管理、产品管理、留言管理、基础配置。别小看这些模块,它们几乎覆盖了 JavaWeb 课程设计和中小型外包项目的全部考点:CRUD、分页、文件上传、权限拦截、富文本、数据关联、部署上线。

我这次用的技术组合是 Spring Boot + MyBatis-Plus + MySQL + Thymeleaf。Spring Boot 负责提供接口和页面渲染,MySQL 存数据,Thymeleaf 做服务端模板。选择这套方案的理由很直接:第一,服务端渲染对搜索引擎友好,企业门户最在意的就是官网能有 SEO 效果;第二,单体应用部署简单,一个 jar 包丢到服务器就能启动;第三,JavaWeb 项目完整案例里,这套组合的资料最全,遇到问题随手能搜到答案。

为什么不上 Vue + Spring Boot 的前后端分离?不是不能,而是没必要。门户网站的内容形态以文章、图片、列表为主,服务端渲染天然适合内容展示。前后端分离要多维护一套 Node 构建流程、要处理跨域、要写鉴权接口,对小团队来说反而增加交付成本。等项目后面确实需要做移动端、需要给运营做复杂交互的时候,再把后端 API 化、前端用 Vue 重写也不迟,我后面会单独讲怎么扩展。

1.2 完整项目源码的目录规划与功能边界

拿到源码项目,第一件事不是跑起来,而是看目录。一个标准的 Spring Boot 门户项目,结构应该是这个感觉:

enterprise-portal/ ├── src/main/java/com/example/portal │ ├── common // 统一返回、异常处理、常量 │ ├── config // 拦截器、资源映射、MyBatis-Plus配置 │ ├── controller │ │ ├── admin // 后台管理接口 │ │ └── web // 前台页面接口 │ ├── entity // 数据库实体 │ ├── mapper // MyBatis-Plus Mapper接口 │ ├── service │ │ └── impl │ └── PortalApplication.java ├── src/main/resources │ ├── mapper // XML文件(复杂SQL使用) │ ├── static // JS、CSS、图片 │ ├── templates │ │ ├── admin // 后台Thymeleaf模板 │ │ └── web // 前台模板 │ ├── application.yml │ └── banner.txt ├── sql │ └── portal.sql // 建库建表脚本,附测试数据 ├── docs │ └── 部署文档.md └── README.md

注意我特意加了sql和docs两个目录。很多源码包没有数据库脚本或只有部分建表语句,这是最坑的。一个真正完整的源码项目,应该提供初始化 SQL,包含建库、建表、默认管理员账号和测试数据。没有测试数据,前台页面空空如也,后台也不知道有没有问题,后期联调全靠手工造数据,效率很低的。

功能边界划分上,我只做一层权限,也就是后台管理员登录才能管理内容,不搞复杂的 RBAC。企业门户的运营者通常就是几个固定的人,没必要设计角色权限表。如果用 Spring Security 做完整权限体系,这套方案反而会显得臃肿。登录校验用 Session + 拦截器就能解决:放行登录接口、放行静态资源、放行前台页面,其他/admin/**路径都必须要有登录标记。

2. 数据库设计与核心源码实现细节

2.1 表结构设计:一张图想清楚六张表的关系

门户网站的数据模型不复杂,但表结构设计得合理,后面写代码会非常舒服。我通常建议这样设计:

  • news 新闻资讯表:标题、摘要、正文、封面图、作者、发布时间、置顶标识、状态。
  • product_category 产品分类表:分类名称、排序。
  • product 产品表:分类ID、产品名称、封面图、简介、详细内容、排序、状态。
  • message 在线留言表:姓名、电话、邮箱、留言内容、回复内容、是否处理。
  • setting 系统配置表:网站名称、Logo、SEO关键词、备案号、版权信息等。
  • admin_user 管理员表:用户名、密码、盐值、昵称、最后登录时间。

这里给你一段新闻表的建表 SQL,能看出我设计时的习惯:

CREATE TABLE `news` ( `id` INT NOT NULL AUTO_INCREMENT COMMENT '主键', `title` VARCHAR(200) NOT NULL COMMENT '标题', `summary` VARCHAR(500) DEFAULT NULL COMMENT '摘要', `content` TEXT COMMENT '正文', `cover_image` VARCHAR(255) DEFAULT NULL COMMENT '封面图', `author` VARCHAR(50) DEFAULT '' COMMENT '作者', `publish_time` DATETIME DEFAULT NULL COMMENT '发布时间', `is_top` TINYINT DEFAULT 0 COMMENT '是否置顶 0否 1是', `status` TINYINT DEFAULT 1 COMMENT '状态 0草稿 1发布', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_status_top_time` (`status`, `is_top`, `publish_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='新闻资讯表';

这里有三个细节容易忽略。第一,日期字段我用的是DATETIME而不是TIMESTAMP,因为TIMESTAMP从 1970 年开始计数,2038 年会有回绕问题,DATETIME范围更大。第二,时间字段都加上DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,以后更新记录时不需要手动维护update_time,数据排查起来方便很多。第三,状态字段用TINYINT而不是INT,查询性能更好,存储空间也省。

产品表和新闻表结构类似,中间用category_id和product_category表关联。消息表里我通常会加两个字段:is_handle和reply_content,后台管理员看到新留言后可以回复标记,这样运营时不容易漏处理。系统配置表用 key-value 结构,后面改网站标题、底部版权信息,不需要改代码,后台填一下就能覆盖。

2.2 后端接口的统一返回、异常处理与分页查询

完整源码项目一定会有统一返回结构,不然前端对接接口时会疯掉。我习惯定义一个Result<T>对象,所有接口都返回它:

public class Result<T> { private int code; // 200 成功,500 失败 private String message; private T data; public static <T> Result<T> ok(T data) { Result<T> r = new Result<>(); r.setCode(200); r.setMessage("操作成功"); r.setData(data); return r; } public static <T> Result<T> error(String message) { Result<T> r = new Result<>(); r.setCode(500); r.setMessage(message); return r; } }

实际中我会配合@RestControllerAdvice做全局异常拦截,业务代码里不需要到处 try-catch。比如操作数据库失败、参数校验失败,统一返回异常信息,避免 500 页面直接裸露给用户。这样做的好处是,出问题时大家只看返回结构就能定位是业务问题还是系统异常。

分页查询是门户网站最高频的操作,新闻列表、产品列表、后台管理列表全都需要。我这里用 MyBatis-Plus 自带的分页插件,配置一个MybatisPlusInterceptor就可以:

@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }

之后 Service 里直接写Page<News> page = newsMapper.selectPage(new Page<>(current, size), wrapper),分页参数从 controller 接收。注意 current 和 size 要做参数校验,size 最好限制在 1-100,防止有人传一个几万的值把数据库拖垮。

2.3 密码加密:MD5 加盐的完整过程和适用边界

热词里提到“MD5 算法详细完整过程并举例”,这个后面的登录模块一定会用到。我直接分享一个常规做法:用户注册或初始化管理员时,随机生成一个盐值,把密码和盐拼接后做 MD5,最终数据库存salt:hash这样的字符串。登录时取出盐值,重新计算比对。

import java.security.MessageDigest; public static String md5Hex(String text) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] bytes = md.digest(text.getBytes("UTF-8")); StringBuilder sb = new StringBuilder(); for (byte b : bytes) { sb.append(String.format("%02x", b)); } return sb.toString(); } catch (Exception e) { throw new RuntimeException(e); } } // 注册时 String salt = UUID.randomUUID().toString().replace("-", ""); String hash = md5Hex(password + salt); // 存到数据库:admin_user.salt = salt; admin_user.password = hash; // 登录时 String hash = md5Hex(inputPassword + saltFromDb); if (hash.equals(passwordFromDb)) { // 校验通过 }

需要强调一点:MD5 本身已经被证明不安全,直接裸存密码是绝对不可取的。这个案例里用 MD5 加盐,主要目的是演示加密过程、满足课程设计的复杂度要求。真正的生产环境,我建议直接用 Spring Security 的 BCryptPasswordEncoder,或者至少用 SHA-256 加随机盐。大家在写毕设或项目的时候,也要在注释里写清楚这一点,体现出你的安全意识,面试官看到这种细节会很加分。

另外,管理员初始化密码不要写在代码里,正确的做法是数据库初始化脚本里写入一条加密后的数据,并把默认密码写进 README,提醒客户第一次登录后立即修改。这样不会暴露明文,也算一个交付小技巧。

2.4 图片上传与访问映射:后台传图背后的小机关

后台新闻和产品都要传封面图,这就要解决文件上传和图片访问两个问题。我的做法是把图片保存到一个独立上传目录,比如upload/,然后通过 Spring Boot 静态资源映射把这个目录暴露出去。Controller 里接收MultipartFile,文件名用 UUID 重新生成,防止出现中文路径或同名覆盖问题。

@PostMapping("/admin/news/upload") public Result<String> upload(@RequestParam("file") MultipartFile file) throws IOException { if (file.isEmpty()) { return Result.error("上传文件为空"); } String original = file.getOriginalFilename(); String ext = original != null ? original.substring(original.lastIndexOf(".")) : ".jpg"; String newFileName = UUID.randomUUID().toString().replace("-", "") + ext.toLowerCase(); File dir = new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(dir, newFileName)); return Result.ok("/upload/" + newFileName); }

然后写一个WebMvcConfigurer,把/upload/**映射到本地磁盘目录:

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String path = "file:" + uploadDir + separator; registry.addResourceHandler("/upload/**").addResourceLocations(path); }

这个部分最容易出问题的地方是 Windows 和 Linux 的路径分隔符不一致。开发环境是D:/upload/,服务器可能是/opt/enterprise-portal/upload/,如果把路径写死在代码里,部署时必然踩坑。我习惯把上传目录放到application.yml配置项里,通过@Value注入,这样开发和生产环境各配各的路径,不用改一行代码。

还要注意文件类型和大小校验。没有校验的话,用户传一个 .jsp 文件进去,再配上某些服务器的解析配置,就能造成安全风险。最低限度也要限制扩展名在白名单内,比如只允许 jpg/png/gif/webp,上传接口的spring.servlet.multipart.max-file-size默认是 1MB,企业官网图片建议放宽到 5MB 左右,但也不要太大。

3. 完整实操:从开发到上线的关键环节

3.1 环境准备与 spring boot 项目初始配置

先把开发环境定下来,我建议 JDK 1.8、Maven 3.6+、IDEA、MySQL 5.7 或 8.0。Spring Boot 版本我选 2.7.x,不是越新越好,而是 2.7.x 对 JDK 8 的兼容性最好,资料和踩坑记录最多,完全够企业门户这种中小型项目用。如果一上来就上 Spring Boot 3,要求 JDK 17,你的课程设计或者外包交付环境未必跟得上。

项目初始化后,application.yml最核心的是数据源配置:

spring: datasource: url: jdbc:mysql://localhost:3306/enterprise_portal?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false mybatis-plus: mapper-locations: classpath:/mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

有几个配置项必须解释清楚。characterEncoding=utf8是中文不乱码的底线,serverTimezone=Asia/Shanghai是用来解决 MySQL 8 与 JDBC 驱动的时区偏差,useSSL=false是本地开发必须的,不然会提示 SSL 连接错误。thymeleaf.cache=false是开发阶段的标配,改完模板一定要先清浏览器缓存再刷新,否则你以为代码没生效,实际上是浏览器缓存了旧页面。

MyBatis-Plus 打印 SQL 这个配置,开发时开着挺有用,能看到每次执行的真实语句,排查问题效率高。但上线前一定要关掉或改成日志文件输出,否则 SQL 全部打到控制台,既暴露表结构又影响性能。

3.2 前台模板的复用与首页数据聚合

门户网站的前台页面有很多公共元素:导航栏、页脚、侧边栏。如果用每个 HTML 都写一遍,后期改一个版权信息要全局替换,太痛苦了。我用 Thymeleaf 的片段语法解决这个问题,把公共部分抽成fragments.html,页面里直接用th:replace引入。

比如页脚在fragments.html里定义一个片段:

<footer th:fragment="footer"> <p th:text="${setting.copyright}">版权信息</p> </footer>

其他页面需要时写:

<div th:replace="~{fragments :: footer}"></div>

这样基础配置表里存的版权信息、ICP 备案号,全局都能生效,后台改一次所有页面同步更新。

首页是比较特殊的页面,数据来源不止一张表。我通常会在 Service 层写一个聚合查询方法,一次查出:置顶且已发布的新闻 6 条、最新产品 8 条、公司简介摘要、联系信息,统一放到 Model 里返回给模板。注意页面只取必要字段,比如产品不需要 content 大字段,SQL 里就别select *,减轻数据库传输压力。

3.3 后台管理模块的权限拦截与核心功能实现

后台管理虽然功能多,但核心就两件事:登录保护、内容 CRUD。我选择用拦截器做登录保护,代码比 Spring Security 简单得多,也更适合这个项目体量。

public class AdminInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { String uri = request.getRequestURI(); if (uri.startsWith("/admin/login") || uri.startsWith("/admin/css/") || uri.startsWith("/admin/js/")) { return true; } Object loginUser = request.getSession().getAttribute("loginUser"); if (loginUser == null) { response.sendRedirect("/admin/login"); return false; } return true; } }

登录成功后把用户信息放进 Session,同时更新一下最后登录时间。退出登录就是清除 Session 再重定向。这套逻辑简单、直观,而且面试时能讲清楚,比无脑背 Spring Security 配置强得多。

新闻、产品、留言这几个模块的实现套路其实一致:列表页分页查询,新增页跳转,编辑页回显,删除采用逻辑删除。为什么用逻辑删除而不是物理删除?因为客户误删新闻后想恢复,物理删除就彻底没了。MyBatis-Plus 里给字段加@TableLogic注解,删除自动变成 update 语句,查询自动过滤已删除数据,非常省事。

3.4 打包部署:jar 包加 systemd 加 Nginx 的黄金组合

项目开发完,接下来是交付环境部署。现在 Spring Boot 项目打包方式我强烈建议打成 jar 包,配合 Linux systemd 守护进程。部署流程可以归纳成四步:

第一步,执行mvn clean package -DskipTests,生成enterprise-portal.jar。第二步,把 jar 和 SQL 脚本上传到服务器,比如放到/opt/enterprise-portal/,执行source /opt/enterprise-portal/portal.sql初始化数据库。第三步,编写 systemd 服务文件,让程序开机自启、崩了自动拉起。第四步,配置 Nginx 反向代理 80 端口。

systemd 服务文件可以参考下面这个:

[Unit] Description=Enterprise Portal Service After=network.target mysql.service [Service] User=www WorkingDirectory=/opt/enterprise-portal ExecStart=/usr/bin/java -jar /opt/enterprise-portal/enterprise-portal.jar --spring.profiles.active=prod Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target

写好之后执行systemctl daemon-reload && systemctl start enterprise-portal就能启动。注意Restart=on-failure很关键,进程异常退出后 5 秒会自动重启,比 nohup 裸跑靠谱太多。

Nginx 配置上,我只把动态请求反代给 Java 应用,静态上传目录直接由 Nginx 处理:

server { listen 80; server_name your-domain.com; location / { 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; } location /upload/ { alias /opt/enterprise-portal/upload/; expires 30d; } }

这样图片访问不经过 Java 应用,并发高一些也不会卡。上线后务必检查云服务器安全组,只放行 80、443、22 端口,8080 端口不对公网开放,只让 Nginx 通过内网访问,整体安全性会好很多。

3.5 前后端分离改造:门户项目如何升级为 vue 应用

很多公司现在招聘要求里写着“前后端分离项目实战”,如果你手里只有这套门户单体源码,面试时可能会被质疑。其实单体门户转前后端分离并不难,我给你理一下改造思路。

后端方面,把原来返回 ModelAndView 的 controller 全部改成返回 JSON 数据,加一个统一鉴权方案。最简单的做法是引入 JWT,登录接口验证通过后签发 token,其他接口用拦截器校验 token,前端请求头带上Authorization: Bearer <token>。原来的 Session 拦截器直接替换成 JWT 拦截器就行。

前端方面,用 Vue 3 + Vite 搭一套后台管理界面,用 Vue Router 做路由,用 Axios 请求接口。模板页面的写法改成组件化开发:导航、页脚、卡片、分页都是组件,数据通过接口驱动。这样门户网站就从“模板渲染”升级成了“接口驱动”,但核心业务逻辑一点不用动,新闻的 CRUD、产品的关联查询、留言的审核流程全部沿用。

什么时候值得做这个改造?如果你需要同时维护小程序、PC 官网、运营后台多套终端,或者前端交互变得复杂,那前后端分离是正确选择。如果只是做一个纯展示型官网,老老实实用 Thymeleaf 更省心。不要为了技术炫技而脱离实际需求。

4. 常见问题与排查技巧实录

4.1 数据库连接报错的三种典型情况

做 JavaWeb 项目的人,几乎都在数据库连接上栽过跟头。我汇总了三种最高频的报错和对应的排查顺序:

第一,Access denied for user 'root'@'localhost',先核对账号密码,别忽略密码里多一个空格这种低级错误。第二,Unknown database 'enterprise_portal',这是 SQL 脚本没执行或者执行错了库,进去 MySQL 执行show databases;看看有没有这个库。第三,Public Key Retrieval is not allowed for this user,这是 MySQL 8 加密方式导致的,连接串上加allowPublicKeyRetrieval=true&useSSL=false就能解决。

排查顺序也有讲究。看到连接报错,先看控制台异常堆栈,是网络层、认证层还是 SQL 层。网络层去看端口是否通,认证层去核对账号权限,SQL 层去检查语句和字段。不要一上来就重装 MySQL,多数问题只是配置细节。

4.2 上传图片 404 和中文乱码的排查

图片上传成功但访问 404,90% 是映射没生效。检查三处:是不是加了registry.addResourceHandler("/upload/**"),上传目录配置的绝对路径是否正确,目录有没有写权限。特别是服务器上用www用户启动 jar,目录没有给到www写权限,上传会静默失败或者路径错误。

中文乱码的坑也常见。前台页面乱码,先看数据库连接串有没有characterEncoding=utf8;后台提交的中文乱码,看 Tomcat 是否对请求做了 UTF-8 解码。Spring Boot 内置 Tomcat 默认已经处理,但如果你打成 war 包丢到外部 Tomcat,就需要配置URIEncoding="UTF-8"。数据库建表时字符串字段一定要用utf8mb4,表情符号和生僻字才不会乱。

4.3 打包部署后 404 或 500 的快速定位

本地跑得好好的,部署到服务器就 404,常见原因是项目配置了server.servlet.context-path,Nginx 代理时没对应去掉前缀;或者前端页面访问的路径和后台接口不一致。解决方式是把 Nginx 的proxy_pass http://127.0.0.1:8080;最后加不加/搞清楚:加了对接口路径很敏感,不加则保持原路径转发。

500 错误要看日志,这是最直接的办法。spring.profiles.active=prod对应的生产配置里可以临时打开日志输出,用journalctl -u enterprise-portal -e查看 systemd 服务的最近日志。常见原因包括:数据库字段和实体属性没映射上、MySQL 8 的语法兼容问题、上传目录没创建成功。定位后逐个排除,通常不难解决。

4.4 关键配置与排查速查表

最后我把容易踩坑的配置项整理在这张表里,发布前对着检查一遍,能省下一堆远程调试时间:

检查项推荐配置原因
数据库连接串characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai防止中文乱码、SSL 报错、时区偏差
MySQL 8 加密驱动用com.mysql.cj.jdbc.Driver旧驱动无法连接 MySQL 8
Thymeleaf 缓存开发关、生产开开发时改模板立即生效,生产时减少解析开销
上传目录配置项注入,不写死适配 Windows 与 Linux 路径差异
文件大小限制5MB 左右太小封面图传不上,太大服务器压力高
生产环境 SQL 日志关闭或输出到文件防止暴露表结构和拖累性能
云服务器安全组仅开放 22、80、4438080 不暴露公网,减小攻击面
systemd 守护Restart=on-failure崩溃自动重启,减少人工介入

芯片级的细节很多,但记住一个原则:完整源码项目跑通只是起点,能让人不看微信、照着文档独立部署一遍,才算真正有交付价值。我带着学员做这类项目时,最常强调的就是这件事。有一次我把一个门户项目交给客户,客户在服务器上自己照着部署文档一路操作,居然一次都没找我,那一刻比我多写几千行代码都踏实。最后再分享一个小技巧:部署完别忘了把spring.profiles.active=prod里数据库密码改成强密码,并确认后台默认管理员的密码已经改掉。这种安全习惯,才是完整源码项目实战中真正拉开差距的地方。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询