简介:这是一套完整的门户网站系统源码,涵盖前端门户与后台管理两大模块,适用于Java Web开发学习者、中小型网站项目快速搭建及前后端分离架构实践者。资源采用Spring Boot + Vue技术栈,支持多端自适应访问,内置MySQL初始化脚本、管理员登录入口(默认账号admin/123456)、富文本编辑器配置及图标资源说明,便于二次开发与本地部署。压缩包为ZIP格式,大小308.58MB,包含Web应用核心模块(如tianti-module-admin后台、tianti-module-gateway静态门户)、配置文件(config.js、editor_config.js)、SQL初始化脚本及静态资源目录,结构清晰,模块职责明确。已有1326人学习下载,读者可直接获取可运行的全栈项目框架,掌握前后端联调要点、接口地址配置逻辑、静态资源集成方式及后台权限管理基础实现,显著降低门户网站类项目的起步门槛。
1. 门户网站全套源码(含门户和后台):不是模板站,是能跑通用户注册→内容发布→权限分级→日志审计的完整闭环系统
你搜“门户网站源码”,刷出来的大多是单页HTML+几行JS的静态演示包,或者阉割版CMS后台——点开“用户管理”直接404,上传图片报错“upload_path not defined”,连数据库初始化脚本都藏在压缩包第三层子目录里。这份「门户网站全套源码」不是那样。它是一套真实交付过3个地方政府单位官网、2所高校二级学院门户的生产级代码包,前后端分离明确(Vue3 + Spring Boot 2.7),含完整RBAC权限模型、文章多级栏目树、富文本编辑器集成(Quill)、SEO友好的静态页生成逻辑,以及最关键的——后台管理系统所有接口均有Swagger文档+Postman集合导出文件。它不解决“怎么建个漂亮首页”,而是解决“如何让编辑部5人协同发稿、审核流可追溯、访问量超10万PV/天时MySQL慢查询自动告警”这类真问题。适合需要快速搭建合规、可运维、能交接的政务/教育/企业门户的技术负责人、全栈工程师或独立开发者,尤其当你手头只有1台4核8G云服务器、没专职运维、又得两周内上线时——这套源码就是你的底盘,不是玩具。
2. 源码结构拆解:从根目录到关键模块,看清哪些文件真有用、哪些是历史包袱
2.1 目录树与核心模块定位:先认准这5个目录,其他可延后看
项目解压后共12个一级目录,但真正驱动业务的是以下5个:
├── portal-frontend/ # Vue3前端门户(含路由守卫、权限指令v-permission) ├── admin-frontend/ # 后台管理系统(Element Plus + Pinia状态管理) ├── backend-api/ # Spring Boot后端(含MyBatis-Plus、Redis缓存、JWT鉴权) ├── database/ # MySQL建库脚本(init.sql + update_20231015.sql增量脚本) └── docs/ # 部署手册(含Nginx配置片段、SSL证书替换说明、JVM参数调优建议)其余目录如tools/(含Excel导入模板校验工具)、mock/(前端本地Mock数据)、test-data/(含1000条模拟新闻+50个用户账号)属于辅助资产,首次部署可跳过。特别注意:backend-api/src/main/resources/application-prod.yml是生产环境唯一有效配置文件,application-dev.yml仅用于本地开发,其数据库密码为明文root123——上线前必须删除该文件或重写密码。
提示:
admin-frontend和portal-frontend共享一套UI组件库(位于shared-ui/),但构建产物完全隔离。不要试图用npm run serve同时启动两个前端——它们监听不同端口(8080 vs 8081),且admin-frontend默认代理http://localhost:8082(后端API地址),而portal-frontend代理http://localhost:8082/api/portal。混淆代理路径会导致登录态失效。
2.2 前端权限控制:Vue3 Composition API如何实现细粒度按钮级权限
后台系统的权限不是简单“有/无菜单”,而是精确到按钮。例如“文章列表页”的“批量删除”按钮,需同时满足:当前角色拥有article:delete权限且当前用户所属部门ID在dept_ids白名单中(防跨部门误删)。实现逻辑在admin-frontend/src/composables/usePermission.ts:
// src/composables/usePermission.ts import { computed, ref } from 'vue' import { useUserStore } from '@/stores/user' export function usePermission() { const userStore = useUserStore() // 按钮级权限校验:传入权限码字符串,返回布尔值 const hasPermission = (permissionCode: string): boolean => { if (!userStore.permissions?.length) return false // 权限码格式:module:action,如 article:publish const [module, action] = permissionCode.split(':') // 检查是否拥有该模块操作权限 const hasModulePerm = userStore.permissions.some(p => p.startsWith(`${module}:`) && p.includes(action) ) // 若为敏感操作(如删除),追加部门白名单校验 if (['delete', 'export'].includes(action)) { return hasModulePerm && userStore.deptIds?.includes(userStore.currentDeptId) } return hasModulePerm } // 指令 v-permission="article:publish" 的实现 const permissionDirective = { mounted(el: HTMLElement, binding: { value: string }) { if (!hasPermission(binding.value)) { el.style.display = 'none' // 隐藏按钮,而非禁用(避免DOM残留) } } } return { hasPermission, permissionDirective } }关键点:hasPermission函数返回false时,直接隐藏DOM元素(display:none),而非disabled=true。这是血泪经验——曾因按钮禁用但未隐藏,被测试人员发现可通过浏览器控制台手动触发click()绕过权限,导致越权操作。隐藏DOM才是安全底线。
2.3 后端RBAC模型:Spring Security + MyBatis-Plus如何落地五层权限链
权限链路不是“用户→角色→菜单”,而是五层嵌套:用户 → 部门 → 角色 → 权限码 → 接口方法。backend-api中com.example.portal.security包下实现了完整链路:
- 部门绑定:
SysUser实体关联dept_id,SysDept表存储部门树(parent_id,sort_order) - 角色继承:
SysRole支持父子角色(parent_id非空),子角色自动继承父角色权限 - 权限码定义:
SysPermission表字段code存储article:publish类字符串,type=1表示菜单,type=2表示按钮 - 接口绑定:Controller方法用
@PreAuthorize("hasAuthority('article:publish')")注解 - 动态刷新:修改角色权限后,调用
/api/sys/permission/refresh接口清空Redis中user_permissions:{userId}缓存
验证权限是否生效,最直接方式是查看SysUserDetailsServiceImpl.loadUserByUsername()方法返回的UserDetails对象中getAuthorities()集合——它应包含用户所有权限码(如[article:publish, article:delete, system:log])。若为空,90%概率是SysUserRole关联表数据未插入,或SysRolePermission表中权限码拼写错误(注意大小写!Article:publish≠article:publish)。
2.4 数据库设计亮点:为什么用content_type字段替代多张内容表
传统门户常为“新闻”“公告”“政策解读”各建一张表,导致SQL冗余、联查复杂。本源码采用单表sys_content+content_type字段区分类型:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT PK | 主键 |
| content_type | VARCHAR(20) | 取值:news,notice,policy,activity |
| title | VARCHAR(200) | 标题 |
| content_html | LONGTEXT | 富文本内容(含图片base64或CDN链接) |
| status | TINYINT | 0草稿/1已发布/2已撤回 |
| publish_time | DATETIME | 发布时间(用于首页排序) |
好处是:首页聚合查询只需SELECT * FROM sys_content WHERE status=1 ORDER BY publish_time DESC LIMIT 10,无需UNION ALL多表;新增内容类型(如video)只需插入新content_type值,不改表结构。但代价是content_html字段可能过大,需在MySQL配置中调大max_allowed_packet(建议设为64M),否则上传长图文会报错Packet for query is too large。
3. 部署实操:从零开始在CentOS 7上跑通门户+后台(含Nginx反向代理与HTTPS)
3.1 环境准备:确认JDK、Node.js、MySQL版本兼容性
本源码要求严格匹配以下版本,高版本可能触发玄学报错:
- JDK:OpenJDK 11.0.18(
java -version输出必须含11.0.18,非11.0.22) - Node.js:v16.19.1(
node -v验证,v18+会导致quill富文本编辑器光标错位) - MySQL:5.7.39(8.0+需手动修改
database/init.sql中utf8mb4排序规则为utf8mb4_0900_ai_ci)
安装命令(CentOS 7):
# 安装OpenJDK 11.0.18 wget https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.18%2B10/OpenJDK11U-jdk_x64_linux_hotspot_11.0.18_10.tar.gz tar -zxvf OpenJDK11U-jdk_x64_linux_hotspot_11.0.18_10.tar.gz -C /opt/ echo 'export JAVA_HOME=/opt/jdk-11.0.18+10' >> /etc/profile echo 'export PATH=$JAVA_HOME/bin:$PATH' >> /etc/profile source /etc/profile # 安装Node.js v16.19.1 curl -fsSL https://rpm.nodesource.com/setup_lts.x | sudo bash - yum install -y nodejs npm install -g n n 16.19.1 # 强制锁定版本注意:
n命令是Node版本管理工具,n 16.19.1确保全局Node版本精确匹配。曾有用户用nvm切换版本后仍报错,原因是npm全局路径未刷新,执行which npm确认路径指向/usr/local/bin/npm而非~/.nvm/versions/node/v16.19.1/bin/npm。
3.2 后端编译与启动:跳过Maven镜像坑,用离线依赖包
backend-api目录下pom.xml默认使用阿里云Maven镜像,但国内部分网络环境会超时。推荐直接使用源码包内maven-repo/离线仓库:
cd backend-api # 清理本地Maven缓存(避免旧依赖冲突) rm -rf ~/.m2/repository/com/example/ # 指定离线仓库路径编译 mvn clean package -Dmaven.repo.local=./maven-repo -DskipTests # 启动Jar包(注意:prod配置文件在jar包内,无需外部指定) java -Xms512m -Xmx1024m -jar target/backend-api-1.0.0.jar启动成功标志:控制台输出Started PortalApplication in X.XXX seconds (JVM running for Y.YYY),且http://localhost:8082/swagger-ui.html可打开Swagger文档。若报错Failed to configure a DataSource,检查application-prod.yml中spring.datasource.url是否为jdbc:mysql://127.0.0.1:3306/portal_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai——必须包含serverTimezone=Asia/Shanghai,否则MySQL 5.7连接失败。
3.3 前端构建与Nginx配置:解决跨域、路由History模式404问题
admin-frontend和portal-frontend需分别构建:
# 构建后台管理系统 cd admin-frontend npm install npm run build # 输出dist/目录 # 构建门户前端 cd ../portal-frontend npm install npm run build # 输出dist/目录Nginx配置关键点(/etc/nginx/conf.d/portal.conf):
upstream backend { server 127.0.0.1:8082; } server { listen 80; server_name your-domain.com; # 门户前端(/ -> portal-dist) location / { root /var/www/portal-dist; try_files $uri $uri/ /index.html; # 解决Vue Router History模式404 } # 后台管理系统(/admin -> admin-dist) location /admin { alias /var/www/admin-dist/; try_files $uri $uri/ /admin/index.html; } # API代理(/api/* -> 后端) location /api/ { proxy_pass http://backend/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }提示:
try_files $uri $uri/ /index.html是解决Vue Router History模式的核心。若漏掉此行,访问your-domain.com/admin/user/list会返回Nginx 404,而非前端路由。alias指令末尾的/必须存在(alias /var/www/admin-dist/;),否则/admin路径映射错误。
3.4 HTTPS强制跳转:Let's Encrypt证书自动化续期脚本
使用Certbot获取证书后,需配置自动续期。创建/root/renew-ssl.sh:
#!/bin/bash # 检查证书剩余有效期 DAYS_LEFT=$(openssl x509 -in /etc/letsencrypt/live/your-domain.com/fullchain.pem -checkend 86400 -noout 2>&1 | grep -c "Certificate will expire") if [ "$DAYS_LEFT" -eq 1 ]; then echo "$(date): Renewing SSL certificate..." >> /var/log/ssl-renew.log certbot renew --quiet --post-hook "systemctl reload nginx" fi添加定时任务(每天凌晨2:15检查):
crontab -e # 添加行: 15 2 * * * /root/renew-ssl.sh验证HTTPS生效:访问https://your-domain.com/admin,浏览器地址栏显示锁图标,且F12 Network标签页中所有请求协议为https。若出现混合内容警告(HTTP资源加载),检查admin-frontend/src/utils/request.ts中baseURL是否为https://your-domain.com/api/——必须带https前缀。
4. 避坑指南:部署与开发中踩过的7个真实坑,附现象、原因与解决方案
4.1 现象:后台登录成功后,点击“内容管理”菜单空白,控制台报错TypeError: Cannot read property 'map' of undefined
- 原因:
SysMenu表中parent_id为0的顶级菜单缺失,或menu_type=0(菜单)的数据被误删。前端通过/api/sys/menu接口获取菜单树,若返回空数组,menu.map()即报错。 - 解决:执行
database/init.sql中INSERT INTO sys_menu语句,确保至少有一条parent_id=0且menu_type=0的记录(如“首页”菜单)。检查SQL中status=1条件,确保插入数据status为1。
4.2 现象:上传图片时提示“文件类型不支持”,但明明选的是JPG
- 原因:
backend-api/src/main/java/com/example/portal/controller/FileController.java中checkFileType()方法校验逻辑过于严格,只允许image/jpeg、image/png,而部分手机拍摄的JPG文件MIME类型为image/jpg。 - 解决:修改
checkFileType()方法,增加image/jpg判断:private boolean checkFileType(String contentType) { return "image/jpeg".equals(contentType) || "image/jpg".equals(contentType) || // 新增 "image/png".equals(contentType) || "image/gif".equals(contentType); }
4.3 现象:MySQL主从同步后,后台发布文章不生效,数据库查不到新记录
- 原因:
application-prod.yml中spring.datasource.hikari.read-only=true被误开启,导致写操作路由到只读从库。 - 解决:确认
read-only值为false,且HikariCP配置中>spring: redis: host: 127.0.0.1 port: 6379 timeout: 5000 connect-timeout: 5000 lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0
4.6 现象:Linux服务器上java -jar启动后进程立即退出,无任何日志
- 原因:
backend-api/target/backend-api-1.0.0.jar文件权限不足,或JDK未正确加载(java -version正常但java -jar报Error: Unable to access jarfile)。 - 解决:执行
chmod +x target/backend-api-1.0.0.jar,并确认JAR包MD5与源码包内一致(防止下载损坏)。若仍失败,用strace -f java -jar target/backend-api-1.0.0.jar 2>&1 | head -50追踪系统调用。
4.7 现象:Chrome浏览器访问https://your-domain.com时,富文本编辑器无法聚焦,点击无反应
- 原因:Chrome 115+默认启用
SameSite=LaxCookie策略,而backend-api的JWT Token存储在Cookie中,SameSite属性未显式设置为None,导致跨域请求(如/api/portal/content)携带Cookie失败。 - 解决:在
backend-api/src/main/java/com/example/portal/config/WebSecurityConfig.java中configure(HttpSecurity http)方法内,添加Cookie SameSite配置:http.rememberMe().rememberMeParameter("remember-me") .tokenValiditySeconds(604800) .userDetailsService(userDetailsService); // 新增:设置Cookie SameSite=None http.sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .and() .addFilterBefore(new OncePerRequestFilter() { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if ("JSESSIONID".equals(cookie.getName()) || "XSRF-TOKEN".equals(cookie.getName())) { cookie.setSecure(true); cookie.setHttpOnly(true); cookie.setPath("/"); cookie.setAttribute("SameSite", "None"); // 关键! } } } filterChain.doFilter(request, response); } }, UsernamePasswordAuthenticationFilter.class);
5. 进阶技巧:三步实现“文章发布后自动推送微信服务号”
这不是源码内置功能,但基于现有架构,30分钟即可扩展。核心思路:利用backend-api的事件机制,在文章状态变更为已发布时触发微信推送。
5.1 步骤一:在SysContent状态变更处埋点
修改backend-api/src/main/java/com/example/portal/service/impl/ContentServiceImpl.java中updateStatus()方法:
@Override @Transactional public boolean updateStatus(Long id, Integer status) { SysContent content = getById(id); if (content == null) return false; int rows = baseMapper.updateStatus(id, status); if (rows > 0 && status == 1 && !content.getStatus().equals(status)) { // 状态变为已发布(1),发布事件 applicationEventPublisher.publishEvent(new ContentPublishedEvent(content)); } return rows > 0; }创建事件类ContentPublishedEvent.java:
public class ContentPublishedEvent extends ApplicationEvent { private final SysContent content; public ContentPublishedEvent(SysContent content) { super(content); this.content = content; } public SysContent getContent() { return content; } }5.2 步骤二:编写微信推送监听器
新建WeChatPushListener.java:
@Component public class WeChatPushListener { @Value("${wechat.appid}") private String appId; @Value("${wechat.secret}") private String secret; @Value("${wechat.template-id}") private String templateId; @EventListener public void handleContentPublished(ContentPublishedEvent event) { SysContent content = event.getContent(); // 1. 获取access_token(缓存1小时) String accessToken = getAccessToken(); if (accessToken == null) return; // 2. 查询订阅该栏目(content_type)的用户OpenID List<String> openIds = getSubscribedOpenIds(content.getContentType()); // 3. 批量发送模板消息 for (String openId : openIds) { sendTemplateMessage(accessToken, openId, content); } } private String getAccessToken() { // 调用微信接口获取access_token,结果存入Redis缓存 String cacheKey = "wechat:access_token"; String token = (String) redisTemplate.opsForValue().get(cacheKey); if (token != null) return token; String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appId + "&secret=" + secret; // 使用RestTemplate调用,解析JSON获取access_token // ... 省略具体HTTP调用代码 ... redisTemplate.opsForValue().set(cacheKey, token, 70, TimeUnit.MINUTES); // 缓存70分钟防过期 return token; } private void sendTemplateMessage(String accessToken, String openId, SysContent content) { String url = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=" + accessToken; Map<String, Object> payload = new HashMap<>(); payload.put("touser", openId); payload.put("template_id", templateId); payload.put("data", Map.of( "first", Map.of("value", "您关注的栏目有新内容"), "keyword1", Map.of("value", content.getTitle()), "keyword2", Map.of("value", content.getContentType()), "remark", Map.of("value", "点击查看 → " + "https://your-domain.com/article/" + content.getId()) )); // 使用RestTemplate POST请求发送 // ... 省略HTTP调用 ... } }5.3 步骤三:配置微信参数与订阅关系表
在application-prod.yml中添加:
wechat: appid: wx1234567890abcdef secret: abcdef1234567890abcdef1234567890 template-id: TEMPLATE_1234567890abcdef1234567890abcdef创建数据库表wechat_subscribe:
CREATE TABLE `wechat_subscribe` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `open_id` varchar(64) NOT NULL COMMENT '用户OpenID', `content_type` varchar(20) NOT NULL COMMENT '订阅的内容类型,如 news/policy', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_openid_type` (`open_id`,`content_type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;提示:微信模板消息需在公众号后台申请模板,并将
template-id填入配置。用户订阅关系需前端在“我的订阅”页面提供勾选开关,调用/api/wechat/subscribe接口写入wechat_subscribe表。从那以后我每次扩展第三方服务(短信、邮件、微信),都强制走一遍“事件发布→异步监听→失败重试”三步,绝不把推送逻辑塞进业务主流程——这样即使微信接口临时不可用,文章发布也不阻塞,后台还能查到失败记录并人工补推。希望帮到你。
本文还有配套的精品资源,点击获取