简介:一套完整的门户网站前后端源码包,同时提供后台管理模块与前台展示门户,支持多端自适应,适合需要快速搭建企业或政务门户的开发者,也适合Java Web学习者研究前后端分离架构。项目基于JDK 1.7及以上环境,采用MySQL数据库,后台登录入口、初始管理员账号(admin/123456)、数据库初始化脚本以及编辑器配置等关键信息均在源码中清晰标注,可减少摸索成本。压缩包以zip格式发布,整体体积约308.58MB,内含后端管理工程和可直接部署到Nginx等容器的静态前端工程,并配有SQL脚本、config.js等配置资源,目录结构明确,导入开发工具后即可按说明启动调试。目前已有1336人学习/下载,这份资源整合了从数据库初始化到前后台联调所需的核心配置与代码框架,能帮助开发者快速理解门户系统中门户与后台的协作方式,并为二次功能扩展提供较为完整的起始工程。
1. 门户网站全套源码:一份工程同时管住门户页和后台,拿来就能跑
先别急着写代码。把一份带后台的门户源码跑起来,比写一个新首页更像硬仗。这套门户网站全套源码(含门户和后台)走的是“访客展示端 + 运营管理端”的组合:访客看到的是栏目、文章、搜索和留言;运营进的是登录、栏目维护、内容发布、附件上传和用户授权,两套页面由同一个后端进程支撑。
拿到的目录里既有数据库初始化脚本,也有完整的后台管理代码,工程结构按 portal 和 admin 两个命名空间切开,接口路径和页面模板一一对应。要接手的开发者、要做课程设计的在校生、以及想把旧站从黑匣子状态里救回来的维护者,都可以把它当成样板工程来拆开研究。
2. 项目全貌:一套工程如何同时支撑门户展示与后台管理
2.1 门户站点与后台管理的功能对照
这套源码从形态上分成两块:一块是 portal 门户端,门户首页、栏目列表、文章详情、站内搜索、留言提交都从这里输出;另一块是 admin 后台端,管理员登录、栏目管理、文章管理、留言审核、系统用户和系统参数配置集中在这里。
普通用户访问门户地址/portal/index,运营同事访问/admin/login进后台发布内容。两边访问人不同,但背后读的是同一套数据库。下面这张表把模块和主要数据表对应起来,拿到源码后可以按这个表去定位功能。
| 模块 | 可见端 | 主要数据表 | 典型功能 |
|---|---|---|---|
| 栏目导航 | 门户 + 后台 | category | 栏目树新增、修改、上移下移、启用停用 |
| 内容发布 | 门户 + 后台 | article | 文章新增、编辑、下架、列表分页、详情展示 |
| 站内搜索 | 门户 | 基于 article | 标题模糊检索 |
| 留言反馈 | 门户 + 后台 | leave_message | 访客提交留言、后台审核与删除 |
| 系统用户 | 后台 | sys_user、role | 管理员登录、角色分配、密码修改 |
| 附件图库 | 门户 + 后台 | 文件系统加路径记录 | 封面上传、附件下载 |
真正写进表的业务数据量不大,剩余部分是常见的系统支撑功能。这类门户源码的价值不在表多,而在把“用户看得见的页面”和“运营要维护的页面”之间的数据流跑通。你改任何一张业务表时,都能推断它影响前端哪一块渲染。
2.2 技术选型:为什么这套组合适合门户场景
这套源码用的是 Spring Boot 做后端底座,MyBatis 做持久层,MySQL 存数据,前台模板采用服务端直出而非前后端分离。门户页面用 Bootstrap 改过的站点模板,后台页面用开箱即用的后台管理前端,整体对二次开发相当友好。
选服务端直出的原因很直接——门户站点看重搜索引擎收录和首屏速度。如果全靠 JS 渲染,抓下来的可能只是空壳。Spring Boot 又帮你起一个内嵌容器,配好数据库后直接就能跑,适合“一套代码管两套页面”的工程。
MyBatis 在这类项目里比 JPA 顺手。文章列表往往是多条件组合查询,栏目、关键词、发布时间、上下架状态摞在一起;动态 SQL 写起来直观,想调整索引也方便。后台界面用现成后台前端省掉大量表格样式工作,这也是常见门户后台的真实画风。
2.3 目录结构速览:拿到源码先看这几个位置
工程根目录并不复杂,第一次打开不要急着点开所有文件,先按这份清单确认几个关键入口。
portal-full-source/ ├── pom.xml # Maven 依赖与构建配置 ├── sql/ │ └── init_schema.sql # 建库建表脚本加初始管理员账号 ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/example/portal/ │ │ │ ├── App.java # 启动类 │ │ │ ├── config/ # 拦截器、资源映射配置 │ │ │ ├── controller/ │ │ │ │ ├── portal/ # 门户接口:首页、栏目、文章 │ │ │ │ └── admin/ # 后台接口:登录、增删改 │ │ │ ├── service/ # 业务逻辑层 │ │ │ └── dao/ # MyBatis Mapper 接口 │ │ └── resources/ │ │ ├── application.yml # 数据源、上传目录、端口 │ │ ├── mapper/ # SQL XML 文件 │ │ ├── static/ │ │ │ ├── portal/ # 门户静态资源 │ │ │ └── admin/ # 后台静态资源 │ │ └── templates/ │ │ ├── portal/ # 门户模板页面 │ │ └── admin/ # 后台管理页面 └── README.md这套写法把同类的控制器收进controller/portal和controller/admin两个包,模板对应放在templates下的同名目录。我看到 URL 就能反推出代码文件的位置。我一般拿到源码先打开init_schema.sql看表注释,再打开application.yml确认端口和数据库名,两个文件看完基本能判断这套代码能不能起来。
2.4 一次请求的完整链路:从后台发布到门户展示
理解这套项目,重点在于一次请求怎么从浏览器走进后端,又怎么在前后台之间切换。以“后台管理员发布一篇文章”为例捋链路。
管理员先访问/admin/login,页面是 Spring MVC 渲染出的登录模板,输入账号密码后登录方法去dao/SysUserDao查表,核对成功就把用户对象放进 session,并跳转后台首页。接着管理员填标题、栏目、封面、正文并提交,请求进到controller/admin/ArticleController的保存方法,把表单封装成article实体,通过 mapper 写入数据库。
访客在门户看到新文章是第三步。访客请求/portal/list?categoryId=3时,controller/portal/ArticleController从同一张 article 表取数,交给templates/portal/list.html渲染列表。整个过程里,门户端和后台端没有独立服务,只是同一应用里两类控制器各管一段 URL 前缀。后台改了文章标题,前台下一次刷新就生效,不存在多服务数据同步的问题。
这个设计的复杂度压得很低。守住“后台写数据、门户读数据”这条主线,无论扩展栏目还是增加模块,都不会把工程改乱。
3. 本地跑通指南:从 JDK 配置到门户首页亮起来
3.1 环境准备:JDK、MySQL、Maven 的版本怎么对齐不翻车
跑这套源码需要三样基础环境:JDK、MySQL 服务、Maven。如果你平时用 IDE,版本选择尽量贴近工程本身:JDK 用 8 或 11,MySQL 用 5.7 或 8.0,Maven 配置好中央仓库即可。
翻车多半发生在版本跨度太大。比如更高版本的 JDK 配老一些的 Spring Boot 工程,就会因为模块拆分出现奇怪的加载失败;MySQL 8 如果连接串没带时区参数,启动时也会抛时间相关异常。我建议先看工程pom.xml里的<java.version>和依赖坐标,按工程原来标注的版本准备,不要盲目上新。
如果本机装了多个版本,务必确认 IDE 的项目 SDK 和系统 JAVA_HOME 指向同一个 JDK。这个坑经常在spring-boot:run启动到一半时报版本错误才暴露,前面时间全白花。
3.2 初始化数据库:先建库,再导数据,最后确认表
源码包sql目录下放的是init_schema.sql,里面把数据库、业务表、初始管理员账号一次定义完。先用命令行连进 MySQL 执行脚本:
mysql -uroot -p < sql/init_schema.sql执行前建议把脚本打开看一眼,确认库名是不是你熟悉的,比如portal_db。第一次执行看到任何一行报错就停下检查,重点看两个位置:脚本开头有没有CREATE DATABASE IF NOT EXISTS,建表语句有没有指定DEFAULT CHARACTER SET utf8mb4。utf8mb4 不是小事,文章内容里有生僻字或表情符号,用老旧的 utf8 字符集会直接落库失败或变乱码。
脚本执行成功之后,可以顺手确认表都建出来了:
mysql -uroot -p -e "USE portal_db; SHOW TABLES;"正常会看到category、article、sys_user这些业务表。初始化阶段就确认 root 密码和连接主机,应用里配置的数据源要和这次命令行用的账号密码一致,否则启动第一个异常就是 Access denied。
3.3 修改配置:数据源、上传目录、端口都在 application.yml
数据库初始化完成之后,下一步改src/main/resources/application.yml。这个配置基本决定本地能否启动,字段不算多,按注释逐项确认。
server: port: 8088 # 门户和后台共用的访问端口 servlet: context-path: / # 保持根路径,改动会影响所有 URL spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/portal_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 # 改成你 MySQL 的真实密码 servlet: multipart: max-file-size: 20MB # 单文件大小上限 max-request-size: 50MB # 单次请求总大小上限 thymeleaf: cache: false # 改模板后不用重启即可看到效果 upload: dir: D:/portal-upload # 上传文件的本地落盘目录,先建好说明几个常用参数的取舍。server.port决定门户与后台同一入口,改成 8080 前要确认没被其他进程占用;datasource.url里的serverTimezone对 MySQL 8 是必须的,不给很可能报时区错误;upload.dir是上传落盘目录,建议启动前先建好,避免用到上传功能才报错。
开发阶段把 Thymeleaf 缓存关掉能省很多重启时间。改完配置,先确认password是数据库真实密码,再注意context-path若改成/xxx,后面所有访问路径都要带前缀,登录跳转地址也要同步改。
3.4 启动与验证:后台登录、门户展示、数据链路依次测
配置没问题就在工程根目录执行构建启动:
mvn clean spring-boot:run这个命令会先下载依赖,再启动内嵌容器。看到日志出现Started表示启动成功。第一次启动慢是正常的,依赖没拉全时耐心等 Maven 跑完。
验证分三步走。第一步访问后台登录页http://localhost:8088/admin/login,用init_schema.sql预置的管理员账号登录,能进后台说明数据源和 session 正常。第二步回到门户http://localhost:8088/portal/index,确认栏目导航和已有文章渲染出来。第三步在后台新增一篇测试文章,保存后刷新门户首页,如果新文章出现在列表里,那“后台写、门户读”的链路就通了。
启动过程报错时,不要盯着满屏异常硬猜。先把日志第一段Caused by复制出来搜索,数据库连接失败、Tomcat 端口冲突、配置项写错这几类问题,报错信息里都有明确关键词,顺着关键词改配置比瞎试快得多。
4. 核心模块拆解:权限、栏目树、内容发布与上传的代码长什么样
4.1 登录与权限守卫:后台接口如何把请求挡在门外
后台的安全防护,这套源码用的是传统也最好懂的 session 拦截模式。一个拦截器拦截所有/admin/**请求,看 session 里有没有登录用户,没有就直接重定向到登录页。核心代码在config/AdminAuthInterceptor.java:
package com.example.portal.config; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 后台登录拦截器:除登录页外,其余后台路径都要过这道关卡 */ @Component public class AdminAuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // adminUser 是登录成功后写入 session 的用户对象 Object admin = request.getSession().getAttribute("adminUser"); if (admin == null) { // 没登录就送回登录页,带 error 参数提示 response.sendRedirect(request.getContextPath() + "/admin/login?error=1"); return false; } return true; // 已登录则放行到对应的 Controller } }代码本身不难,关键在注册拦截器时对放行路径的写法。通常在配置类里这样注册:
registry.addInterceptor(adminAuthInterceptor) .addPathPatterns("/admin/**") .excludePathPatterns("/admin/login");excludePathPatterns放行登录页本身,否则登录页被拦死就会跳转死循环。还有细节是登录成功后往 session 放对象的 key,一定要和拦截器取的一致。有些工程随手写了user,拦截器里取的是adminUser,就会出现登录成功跳转后立刻被弹回登录页的奇怪现象,这篇文章的避坑章节还会再提。
4.2 登录方法的实现:密码校验与 session 写入
拦截器只负责守门,真正的进门动作在AdminLoginController。一个直接实用的登录方法:
@PostMapping("/admin/login") public String doLogin(String username, String password, HttpSession session) { SysUser user = userService.findByUsernameAndPassword(username, password); if (user != null) { session.setAttribute("adminUser", user); return "redirect:/admin/index"; } return "redirect:/admin/login?error=1"; }参数说明:username和password是登录表单提交的字段,Spring MVC 按参数名自动绑定;HttpSession由容器注入,不需要自己创建。findByUsernameAndPassword在 service 层做一件事:把密码做一次哈希处理后传给 dao,查询sys_user表;查不到就返回null。
这种写法不复杂,但区分两件事:判断是否登录用 session,判断密码对不对用数据库查询。在这套工程里扩展“锁定失败次数”“角色权限”都容易,直接在登录方法前后加逻辑即可,不用动页面结构。登录成功后不要用forward回首页,用redirect刷新地址栏,避免用户刷新时表单重复提交。
4.3 栏目树建模:parent_id 递归构造门户导航
门户前端最重要的导航结构,后台用一张category表撑起来。关键字段是id、parent_id、name、sort、status,父子关系全靠 parent_id 表达。栏目层级不深时,这样最好维护。
后台维护栏目需要树形结构,service 里常见的递归写法:
public List<Category> buildTree(Integer pid) { List<Category> children = categoryDao.findByParentId(pid); for (Category c : children) { // 对每个子节点继续往下找,直到没有下级为止 c.setChildren(buildTree(c.getId())); } return children; }首次调用 pid 传 0,表示从顶级栏目开始。递归逻辑不复杂,但有两个地方要守住:避免parent_id指向自身形成死循环,这是初始化数据时的底线;栏目深度超过三四层时建议改为循环加栈,防止递归层数太深引发栈溢出。门户导航渲染时循环递归出的集合,有 children 的节点就渲染成下拉菜单即可。
4.4 内容发布与列表查询:动态 SQL 和手动分页的写法
文章表article是数据量最大的表,字段一般包括id、title、category_id、cover、content、status、publish_time。后台文章列表是典型多条件查询,MyBatis XML 用<where>把可选条件叠起来:
<select id="findPage" resultType="com.example.portal.entity.Article"> SELECT id, title, category_id, cover, status, publish_time FROM article <where> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> AND title LIKE CONCAT('%', #{keyword}, '%') </if> </where> ORDER BY publish_time DESC LIMIT #{offset}, #{pageSize} </select>这里分页没引插件,手动传offset和pageSize,分别表示跳过的记录数和每页条数。查询前要先执行一条SELECT COUNT(*)统计总数,否则前端分页器算不出总页数。一个常见错误就是只写 List 查询不写统计总数,导致列表永远只显示第一页。二次开发时记得两条 SQL 要成对出现。
4.5 文件上传:上传目录与访问路径的映射关系
门户封面上传、后台编辑器插图,最后都走 MultipartFile 上传。Controller 取出文件后重新生成文件名再落盘:
@PostMapping("/admin/upload") @ResponseBody public Map<String, Object> upload(@RequestParam("file") MultipartFile file) { Map<String, Object> result = new HashMap<>(); if (file.isEmpty()) { result.put("code", 500); return result; } // 原始文件名不能直接用,避免中文和特殊符号带来访问问题 String filename = UUID.randomUUID().toString().replace("-", ""); String ext = file.getOriginalFilename() == null ? "" : file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(".")); String path = uploadDir + "/" + filename + ext; file.transferTo(new File(path)); result.put("code", 0); result.put("url", "/upload/" + filename + ext); return result; }代码里放弃原文件名,改用 UUID 生成唯一文件名,这样不同用户上传同名文件也不会互相覆盖。要记住的是,落盘目录不能直接写进门户静态目录,否则上传文件会和应用打包在一起,重部署文件就丢了。配置类里需要加一段虚拟路径映射,让/upload/**指向真实磁盘目录,也就是下面避坑章节里要展开的那个问题。
5. 避坑记录:门户与后台工程里常见的 6 个翻车点
常年在门户项目里摸爬的开发者都知道,这种工程真正的门槛不是功能多复杂,而是环境与路径里那几颗螺丝有没有拧紧。下面这 6 条都是我整理这类项目时反复遇到的,每一条都能定位到具体原因。
5.1 数据库连接拒绝:Access denied 和 Communications link failure 怎么区分
现象:启动日志里要么出现Access denied for user,要么出现Communications link failure,后台直接起不来。
原因:Access denied是用户名密码不对,或账号没有目标库的权限;Communications link failure是数据库服务没启动,或连接串里的 IP 端口写错。这两个异常长得完全不一样,但很多人不区分就直接去改密码,越改越乱。
解决:先把 MySQL 服务启动确认一遍,再用命令行实际连一次排除数据库本身问题;然后对照application.yml的url、username、password三件套逐项检查。本地开发建议给账号授全权,避免因为授权粒度太细导致诡异连接失败。
5.2 登录成功后跳回登录页:session 里的 key 不一致
现象:输入正确的账号密码,页面闪了一下后台首页,立刻又回到登录页,重复多次都一样。
原因:典型的跨文件 key 不一致。登录方法写的是session.setAttribute("adminUser", user),拦截器检查时查别的 key;或者处理流程里多执行了一次session.invalidate(),导致新 session 里根本没放对象。
解决:在工程里全文搜索setAttribute和getAttribute,把所有 key 统一到一个常量类里;同时确认拦截器的 excludePathPatterns 放行了/admin/login。登录成功跳转前打一行日志,输出当前 session id,对比跳转后拿到的 session id,能很快确认是不是同一个会话。
5.3 门户样式全丢:静态资源路径前缀对不上
现象:后台能登录,门户页面也能打开,但整个页面的 CSS、JS 全裸奔,布局完全错乱。
原因:页面上引用的静态文件路径是/static/portal/css/xxx.css,而项目实际静态资源在classpath:/static/下,Spring Boot 默认映射对不上;或者某个拦截器把.css、.js请求也拦了。
解决:把模板里的资源路径改成与静态目录直接对应的写法,比如/portal/css/xxx.css;如果自定义过拦截器,记得在 addPathPatterns 之外加一个静态资源排除,把/static/**、/portal/**这类资源请求从拦截范围里踢出去。
5.4 上传能存文件但图片打不开:虚拟路径映射没配对
现象:后台封面上传提示成功,数据库也存了/upload/xxx.jpg,但浏览器访问图片 404。
原因:文件写进了本地磁盘目录,比如D:/portal-upload,但请求到达应用时,Spring MVC 没把/upload/**映射到那个目录,所以找不到。这是文件访问链路里最容易被忽略的一环。
解决:给 WebMvc 配置类补一段资源映射:
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:D:/portal-upload/"); }注意 Windows 下要写file:D:/portal-upload/,结尾斜杠不能丢;Linux 下对应/home/user/portal-upload/。上线后文件目录迁移,这段映射要同步改,否则之前传的历史图片全部失效。
5.5 中文乱码:数据库、连接串、页面标签三个环节要统一
现象:门户新闻标题正常,正文里的生僻字变成问号,或者文章里带表情符号时保存失败。
原因:建表时用了 utf8 而非 utf8mb4,或者连接串没设characterEncoding=utf8,又或者页面缺失 meta charset。只要三个环节里有一处不一致,就会出现保存和读取乱象。
解决:数据库初始化统一用utf8mb4;datasource.url保留useUnicode=true&characterEncoding=utf8;模板页面 head 区保持<meta charset="utf-8">。三处对齐后,中文乱码基本绝迹。注意,改完数据库字符集后要重新导数据,旧表已写入的乱码不会自动变回正常。
5.6 改了配置不生效:target 目录里跑着旧文件
现象:明明改完application.yml的端口或数据库名,重启后还是旧端口旧库,甚至代码改了也不生效。
原因:使用 IDE 直接运行或打包之后没有 clean,target 里保留了旧编译产物和配置文件,Spring Boot 优先加载了旧的target/classes/application.yml。这类问题最迷惑,因为它不是代码逻辑错误,是构建缓存。
解决:改配置后统一用mvn clean spring-boot:run重新构建,彻底清掉 target 再启动。如果加过-o离线参数跑,依赖拉不到新版本时也会出现类似旧包复活的假象,先不要加离线参数。
6. 二次开发:按最小改动思路新增一个后台模块
如果想在这套门户源码上练习二次开发,最安全的做法不是从门户首页改起,而是先选一个后台小模块照葫芦画瓢。后台改动不污染前台现有展示,改动范围可控,验证也直接。
以新增“通知公告”模块为例。先复制一套 article 的实体、mapper、service、controller,全局把Article改成Notice,表名换成notice。后端控制器结构几乎不变:
// 结构与 ArticleController 一致,只替换业务对象和页面路径 @Controller @RequestMapping("/admin/notice") public class NoticeController { @Autowired private NoticeService noticeService; @GetMapping("/list") public String list(Integer page, Integer pageSize, Model model) { model.addAttribute("pageInfo", noticeService.findPage(page, pageSize)); return "/admin/notice/list"; } @PostMapping("/save") public String save(Notice notice) { noticeService.save(notice); return "redirect:/admin/notice/list"; } }新技术框架一件都不用加,装完整个模块大约只要改四处:实体类字段、XML 里的表名、Controller 请求路径、后台侧边栏菜单里加一个入口。跑起来后在后台点一遍新增、保存、删除,确认分页正常,再回到门户端加一个公告列表页去读同一条数据。
这个流程跑通后,你基本就摸清了工程里模块之间是复制的语义还是互不相关的耦合。真正的微创新都藏在这类细节里:比如把上传路径改成按栏目分目录存储,比如给栏目表加一个单页类型让它支持自定义页面。改之前先画一遍数据链路,从菜单入口到 SQL 表,再到门户模板的取值位置,链路能走通再动代码。
从那以后我每次拿到这类带后台的门户源码,都强制花一两个小时把登录、栏目、文章、上传四个主流程完整走一遍,确认没有隐藏的拦截器或资源映射问题,再开始动功能代码;希望帮到你。
本文还有配套的精品资源,点击获取