news 2026/9/11 7:53:37

SpringBoot+Vue3居家办公系统实战:从数据库设计到前后端部署

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Vue3居家办公系统实战:从数据库设计到前后端部署

这两年居家办公从临时应急变成了很多团队必须支持的常态。我当时接到一个很现实的需求:员工分散在家里,怎么收集健康状况、怎么打卡、怎么审批、怎么同步任务?后来我把自己参与设计的这套基于 SpringBoot + Vue3 + MyBatis + MySQL 的疫情居家办公系统源码做了完整整理,前后端分离,接口文档、数据库脚本、部署手册都齐全。这篇文章不是简单放一个演示链接,而是把整个项目从业务分析到落地的关键细节拿出来讲透,尤其是哪些地方最容易写错、哪些设计曾经让我加班到凌晨,这些才是常规教程里不会写的部分。

1. 居家办公系统的真实业务痛点,以及它为什么不是普通CRUD

1.1 分散场景下的信息孤岛问题

集中办公时代,考勤机解决的是“人到没到”的问题;一旦团队分散在家,问题就变成了“人在哪里、身体状态怎么样、今天做什么、审批找谁签”。这四个问题如果全部靠在微信群里接龙,基本会乱成一锅粥。我开发这个系统时,第一件事不是画 ER 图,而是把分散场景下的信息流重新理了一遍:

  • 员工每天需要提交健康状况和体温,并且要能追溯到个人;
  • 上下班不再以打卡机为准,而是线上签到和离岗说明;
  • 工作安排从口头布置变成可跟踪的任务列表;
  • 请假、报销、用章申请等流程都要支持在线审批;
  • 公司公告、防疫通知需要有已读回执。

把这些痛点列出来之后,系统的边界就清晰了。一个居家办公系统首先要解决的是“远程协作 + 健康数据采集”,而不是一上来就做成大而全的 ERP。很多人做这种系统失败,不是因为技术不行,而是因为边界没控制住,把薪资、库存、客户管理全都塞进来,结果半年交付不了。

1.2 从功能清单反推系统边界

理完痛点之后,我很快把系统拆成了六个核心域:用户权限、健康上报、考勤签到、任务管理、审批流程、公告通知。用这六个域去反推数据库表,大概就锁定了二十多张表,规模适中,既有学习价值,又能完整跑通一个真实业务闭环。

管理端和员工端的需求有明显差异。管理端要看统计报表、处理审批、推送公告,所以需要仪表盘和审批列表;员工端则聚焦每天都要用的上报、打卡、任务、申请。这个区别直接影响了前端菜单设计和后端接口设计。比如健康上报的接口,管理员需要“按部门汇总”,员工只需要“提交当天数据”,这两个接口不能混在一起,否则前端要传一堆冗余参数,后端也会越写越乱。

1.3 这套源码适合谁参考

不是所有人都有必要从零写一套这样的系统。如果你是 Java 后端新手,想看 SpringBoot 项目和 Vue3 项目怎么真实对接;如果你要做的毕业设计、公司内部管理后台刚好需要健康上报、在线审批这类模块;或者你想把一个完整项目整理进简历,那这套源码就非常合适。它麻雀虽小但五脏俱全:有 JWT 权限控制、有事务处理、有定时任务、有图表统计、有文件上传、有前后端分离部署,这些点几乎覆盖了 Java 后端岗位面试中会被反复追问的常见场景。

建议先按本文把系统整体结构过一遍,再对照源码去看具体实现。源码不是看了就会,必须能说清楚每个表为什么这么设计、每个接口为什么这么拆,才是真正吃透了。

2. 技术选型复盘:SpringBoot+Vue3+MyBatis这套组合凭什么够用

2.1 后端为什么是 SpringBoot 而不是别的框架

SpringBoot 不是性能最强的框架,但它是“快速交付业务”性价比最高的选择。它把 SpringMVC、事务管理、自动配置打包在了一起,开发时不用再写一堆 XML 配置,内置 Tomcat 直接启动,打成一个 jar 包就能部署。对居家办公系统这种业务逻辑复杂度适中的后台,SpringBoot 2.7.x 足够稳定,搭配 JDK 1.8 或 11 都很顺手。

这里我要专门提醒一个环境问题:如果你本机 JDK 版本太高,比如 17 甚至 21,某些旧版依赖会报错。项目里我锁定了 SpringBoot 2.7.6 + JDK 1.8,就是为了避免无谓的兼容性问题。很多人一上来就装最新的 JDK,结果 SpringBoot 启动时报UnsupportedClassVersionError,然后开始怀疑代码有问题,其实只是版本不匹配。

2.2 前端为什么选 Vue3 而不是 Vue2

Vue3 的组合式 API 写业务比 Vue2 的 Options API 清晰,尤其是一个功能涉及多个状态时,可以按逻辑聚合,而不是散落在datamethods里。配合 Vite 作为构建工具,冷启动速度和热更新体验比 Webpack 时代的 Vue2 舒服很多。UI 组件库我选了 Element Plus,它在 Vue3 生态里最成熟,表单、表格、弹窗这些后台管理高频组件基本开箱即用。

居家办公系统前端大多是表格和表单页面,比如健康上报列表、审批表单、任务面板。这类界面用 Vue3 + Element Plus 开发效率很高。需要注意,Vue3 的响应式原理基于 Proxy,和 Vue2 的Object.defineProperty不一样,直接通过下标修改数组元素的操作在 Vue2 里可能不触发视图更新,在 Vue3 里是没问题的,但也不能因此随意改数据结构,嵌套过深的对象依然要考虑性能。

2.3 为什么在这个项目里坚持用原生 MyBatis

很多朋友看到标题里有 MyBatis 就会问,既然都已经用 SpringBoot 了,为什么不用 MyBatis-Plus?我在这个项目里刻意保持了原生 MyBatis,只用官方提供的 Mapper 扫描和 XML 映射方式。原因很简单:原生 MyBatis 能让你更清楚 SQL 是怎么写的,尤其是复杂多表查询、统计报表这类场景,自己写 SQL 反而更好控制。

MyBatis-Plus 的 BaseMapper 确实能省掉很多单表 CRUD,但在本项目里,像“员工每日健康上报统计”“部门考勤汇总”这类统计 SQL,最终还是得靠自定义 SQL 完成。如果你追求开发效率,引入 MyBatis-Plus 完全没问题;但如果你想搞清楚 MyBatis 的核心原理,强烈建议先拿原生版本写一个完整项目。这个项目里的UserMapper.xmlReportMapper.xml都是手写的动态 SQL,面试时被问到 MyBatis 动态 SQL、一级缓存、二级缓存,你都能拿真实代码举例,而不是背八股。

2.4 MySQL 8.0 连接配置的几个坑

数据库我选的是 MySQL 8.0。相比 5.7,8.0 的默认字符集已经是 utf8mb4,对中文和表情符号支持更好,WITH递归查询、窗口函数这些能力也更强。但 8.0 和 5.7 在驱动、时区、密码加密方式上都有差异,最容易踩的有三个:

  • 连接串里一定要加serverTimezone=Asia/Shanghai,否则会差 8 小时;
  • 驱动类名是com.mysql.cj.jdbc.Driver,不是老版本的com.mysql.jdbc.Driver
  • MySQL 8.0 默认使用caching_sha2_password认证,如果连接工具版本太老,可能报认证插件错误,需要创建用户时指定mysql_native_password

这些都是看起来很小的配置,但几乎每个用 MySQL 8.0 的人都在这里卡过几小时。后面我会在部署章节再展开一次时区问题。

3. 数据库设计:从 ER 模型到建表语句的取舍

3.1 用户、角色、部门三张基础表不要省

几乎所有管理系统都从用户表开始。但很多人为了图省事,只建一张sys_user,把部门名、角色名直接写成字符串字段放进去,这样做原型演示可以,真实项目会很难维护。我的设计是三张基础表加两张关联表:

  • sys_user:用户主表,包含用户名、密码、姓名、手机号、邮箱、部门 ID、状态;
  • sys_dept:部门表,包含父部门 ID、部门名称、排序;
  • sys_role:角色表,包含角色编码、角色名称;
  • sys_user_role:用户和角色的多对多关联;
  • sys_dept_relation:如果要做树形权限范围,会用到部门层级关系。

用户表里不建议直接存角色 ID,因为一个用户可能有多个角色。居家办公系统里,管理员可能同时是部门负责人,普通员工也可能兼任考勤统计员,这种场景通过多对多关系表才能灵活处理。建表时给逻辑外键加上索引,但不一定要物理外键,物理外键在分库分表和测试数据清理时会非常痛苦。

3.2 健康上报表的唯一约束设计

健康上报是这个系统里最核心的一张表,我把它命名为health_report。字段包括:用户 ID、上报日期、体温、健康状况、是否接触疑似病例、当前所在城市、详细地址、备注、创建时间。

这里有一个非常关键的设计:(user_id, report_date)必须加唯一索引。原因很简单,员工每天只需要提交一条记录,如果没有唯一索引,应用层就算做了判断,并发请求下依然可能插入重复数据。有了唯一索引,即使前端连点两次提交,或者有人直接调接口重复提交,数据库也会拒绝第二条,报DuplicateKeyException。后端再捕获这个异常,把业务提示改成“今日已上报,无需重复提交”,一个完整的幂等方案就出来了。

体温字段我建议用DECIMAL(4,1)而不是DOUBLE。因为体温只需要保留一位小数,DECIMAL能避免浮点数精度问题,也方便在 SQL 里直接做AVGMAX统计。状态字段如健康状况可以用TINYINT存储数字编码,不要直接用中文字符串,否则统计时写一堆CASE WHEN既不优雅也不好维护。

3.3 考勤签到表与审批表的状态机设计

考勤表attendance我设计的字段包括:用户 ID、签到时间、签退时间、工作地点、工作时长、备注。居家办公和公司打卡最大的区别是地点不固定,所以“工作地点”字段建议允许用户手动填写,同时记录经纬度。经纬度只是辅助参考,不要一开始就想做电子围栏,涉及地图定位会显著增加开发成本。

审批表approval是这个系统里另一个容易设计过度的地方。我建议用一张大表统一管理请假、报销、用章申请,通过approval_type字段区分业务类型,而不是每种申请各建一张表。审批状态机就四个状态:待审批、通过、驳回、已撤销。

为什么这么设计?居家办公系统的审批流程相对固定,没有复杂的条件网关,一张表加一个状态字段足够应付。如果每种申请都建一张表,前端要做多个列表页,后端要写多套接口,维护成本翻倍。后续真要扩展时,只需要加一个approval_type编码,不会影响已有业务。

3.4 SpringBoot + MyBatis 当表不存在自动建表的实现

网上经常搜到“SpringBoot + MyBatis 当表不存在自动建表”的话题,我在项目开发早期也研究过。这个需求其实是希望项目启动时自动初始化数据库表结构,避免手动导入 SQL。实现方式有两种常见思路:

第一种是用 SpringBoot 自带的 SQL 初始化能力,在application.yml里配置:

spring: sql: init: schema-locations: classpath:sql/schema.sql mode: always

这种方式适合 Simplest 项目,但要注意mode: always会在每次启动都执行,所以schema.sql里必须写CREATE TABLE IF NOT EXISTS

第二种更可控,我在项目里用了一个TableInitRunner,实现ApplicationRunner接口,在项目启动后执行一次检查逻辑。核心思路是用JdbcTemplate查询information_schema.TABLES,判断表是否存在,不存在就执行classpath:sql/init.sql中的建表语句。

这种自动建表方案在开发测试环境很方便,但生产环境我强烈建议换成 Flyway 或者手动执行 SQL。自动建表最大的风险是:如果线上数据库里已经存在一部分老表,新脚本又修改了字段,直接执行 CREATE TABLE 是会报错的。生产环境的表结构变更应该走专门的迁移脚本,而不是靠启动时自动执行。我已经在这个坑上吃过亏,希望大家不要重蹈覆辙。

4. 后端接口实现:登录鉴权、业务事务与统计 SQL

4.1 JWT + Redis 的登录态管理

后端接口设计里,登录鉴权是最先要确定的模块。居家办公系统是前后端分离部署,前端在浏览器,后端接口在服务器上,天然就是无状态服务,所以我用了 JWT + Redis 的方案:

  1. 用户登录成功后,后端生成一个 token,把用户 ID、用户名、角色信息写入 token 的 claims 里;
  2. 再将 token 以key: login:token:用户ID的形式存入 Redis,设置比如 8 小时过期;
  3. 前端每次请求在 Header 里带Authorization: Bearer token
  4. 后端拦截器解析 token,校验签名和有效期,再查 Redis 确认该 token 是否有效。

为什么要存 Redis 而不用纯 JWT 的无状态方案?因为纯 JWT 无法处理“用户修改密码后让旧 token 失效”“管理员强制用户下线”这类需求。只要 Redis 里有记录,就可以随时删除。相比之下,纯 JWT 虽然省了 Redis 查询,但遇到权限变更只能等 token 自然过期,这在企业管理后台里不可接受。

核心拦截器逻辑可以这样理解:

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("Authorization"); if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { Claims claims = JwtUtil.parseToken(token.substring(7)); if (claims != null) { UserContext.set(claims); return true; } } response.setStatus(401); return false; }

这里有个小细节:UserContext一定要用ThreadLocal实现,并且在请求结束后的拦截器afterCompletion里执行UserContext.clear()。否则线程池复用线程时,会拿到上一个请求的用户信息,造成数据越权。这个 bug 很隐蔽,不是追查日志根本发现不了。

4.2 健康上报接口的幂等与事务处理

健康上报接口的完整流程是:解析当前登录用户,校验参数,检查今日是否已上报,然后插入一条记录,最后给前端返回成功。前面说过,唯一索引已经保证了幂等,所以在代码里只需要捕获异常:

@Transactional(rollbackFor = Exception.class) public ReportResult submitReport(ReportDTO dto) { HealthReport report = new HealthReport(); report.setUserId(UserContext.get().getUserId()); report.setReportDate(LocalDate.now()); report.setTemperature(dto.getTemperature()); report.setHealthStatus(dto.getHealthStatus()); // 省略其他字段 try { healthReportMapper.insert(report); } catch (DuplicateKeyException e) { throw new BusinessException("今日已上报,请勿重复提交"); } return ReportResult.success(); }

方法上加了@Transactional(rollbackFor = Exception.class),表示任何异常都回滚事务。这里的rollbackFor很重要,Spring 默认只对 RuntimeException 回滚,如果你抛出的是自定义异常但没继承 RuntimeException,事务不会回滚,数据就可能出现半写入状态。编码规范上,我习惯所有业务异常都继承 RuntimeException,再在全局异常处理器里转换成对应的 HTTP 状态码。

4.3 MyBatis 动态 SQL 实现多条件筛选

居家办公系统里的列表页特别多,比如用户列表要支持按部门、姓名、状态筛选;上报记录要支持按日期、部门、健康状态筛选;审批列表要支持按类型、状态筛选。如果每种组合都写一条 SQL,代码量会爆炸。MyBatis 的<where><if>标签就是为这个场景设计的。

以用户列表查询为例,Mapper XML 是这样写的:

<select id="selectUserPage" resultType="com.example.entity.UserVO"> SELECT u.id, u.username, u.real_name, u.status, d.dept_name FROM sys_user u LEFT JOIN sys_dept d ON u.dept_id = d.id <where> <if test="query.deptId != null and query.deptId != ''"> AND u.dept_id = #{query.deptId} </if> <if test="query.realName != null and query.realName != ''"> AND u.real_name LIKE CONCAT('%', #{query.realName}, '%') </if> <if test="query.status != null"> AND u.status = #{query.status} </if> </where> ORDER BY u.create_time DESC </select>

<where>标签会自动处理第一个条件前面的AND,所以即使deptId为空,后面的realName条件也能正确拼接。不要自己去手动拼 SQL 字符串,容易漏空格或者多逗号,而且存在 SQL 注入风险。另外,分页我用了 PageHelper 插件,在业务代码里只要PageHelper.startPage(pageNum, pageSize),再执行查询,返回结果就是一个带总数和页码的 Page 对象,非常方便。

4.4 统计报表接口的 SQL 写法

管理端首页需要展示今日上报人数、部门上报率、近七日体温趋势、任务完成率等图表数据。这些数据最好在后端都聚合好,前端只负责渲染。一个典型的统计 SQL 是:

SELECT department_name, COUNT(DISTINCT u.id) AS total_people, COUNT(DISTINCT CASE WHEN hr.id IS NOT NULL THEN u.id END) AS reported_people, COUNT(DISTINCT CASE WHEN hr.id IS NOT NULL THEN u.id END) * 100.0 / COUNT(DISTINCT u.id) AS report_rate FROM sys_user u LEFT JOIN sys_dept d ON u.dept_id = d.id LEFT JOIN health_report hr ON hr.user_id = u.id AND hr.report_date = #{date} GROUP BY d.id, d.dept_name

这里我用了DISTINCT CASE WHEN而不是SUM(CASE WHEN ... THEN 1 ELSE 0 END),因为要防止同一个人在同一天有多条关联记录导致重复计数。虽然唯一索引已经保证了同一个人同一天最多一条记录,但统计 SQL 里养成使用DISTINCT的习惯,能在多表关联时避免很多隐性错误。图表接口返回的数据结构要简洁,例如近七日趋势直接返回[{date: "2024-01-01", count: 120}, ...],前端 ECharts 拿来就能用。

5. Vue3 前端工程:请求封装、路由守卫与权限落地

5.1 从 Vite 创建项目到接入 Element Plus

前端工程结构我建议直接用 Vite 官方脚手架创建:

npm create vite@latest home-front -- --template vue cd home-front npm install npm install element-plus axios vue-router pinia echarts

这里注意,Vite 创建项目后默认没有装 router 和 pinia,需要自己加。为了让 Element Plus 的按需引入和图标能正常使用,我在vite.config.js里配置了unplugin-vue-componentsunplugin-auto-import两个插件,这样组件和 API 会自动按需导入,打包体积会小不少。

目录结构上,我习惯把src/api放接口定义、src/router放路由、src/stores放 Pinia 状态、src/utils放请求封装和工具函数、src/views放页面。源码里每个模块对应一个目录,比如views/health放健康上报相关页面,views/approval放审批页面。如果全部页面都堆在views根目录,项目一大会非常难维护。

5.2 Axios 拦截器与 Token 失效处理

axios 封装是整个前端工程的地基。我在utils/request.js里创建了一个 axios 实例,并设置了基础路径:

const service = axios.create({ baseURL: '/api', timeout: 15000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('access_token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res.data }, error => { if (error.response && error.response.status === 401) { ElMessage.warning('登录已过期,请重新登录') localStorage.removeItem('access_token') router.push('/login') } return Promise.reject(error) } )

这里我把后端返回结构统一为{ code, message, data },拦截器里直接解包data,业务页面拿到的就是接口真正的返回值,不用每个页面都写res.data.data。遇到 401 状态码时统一跳转登录页,而不是让每个接口单独处理。

有一个容易被忽略的细节:如果后端接口返回 401,同时当前已经在登录页,跳转会冗余。所以我在跳转前判断了router.currentRoute.value.path !== '/login',避免无限循环。这个处理虽然只有几行代码,但真实项目里没有全局考虑,就会出现诡异的白屏问题。

5.3 路由守卫与按钮级权限实现

居家办公系统的页面分两类:管理员页面和员工页面。我在路由表里给每个路由配置了meta: { roles: ['admin'] },然后在全局前置守卫里做校验:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('access_token') if (!token) { if (to.path === '/login') { next() } else { next('/login') } } else { const userRole = useUserStore().roleCode if (to.meta.roles && !to.meta.roles.includes(userRole)) { next('/403') } else { next() } } })

按钮级权限相对复杂一点。我用的是一个自定义指令v-permission,指令里根据当前用户权限码判断是否移除 DOM 元素。比如删除按钮只有管理员能看到,页面里就写<el-button v-permission="'system:user:delete'">删除</el-button>。权限码在后端登录接口返回,存入 Pinia 和 localStorage。这种方式比做动态路由简单,也足够覆盖居家办公系统的权限场景。

5.4 数据可视化页面的实现方式

管理端首页的仪表盘用 ECharts 展示。ECharts 在 Vue3 里使用很简单,先安装echarts,然后在组件里引入:

import * as echarts from 'echarts' const chartDom = ref() const myChart = ref() onMounted(() => { myChart.value = echarts.init(chartDom.value) loadChartData() }) const loadChartData = async () => { const data = await reportApi.getTrend() myChart.value.setOption({ xAxis: { data: data.map(item => item.date) }, series: [{ type: 'line', data: data.map(item => item.count) }] }) }

需要注意容器高度问题。ECharts 的容器必须有明确高度,否则图表不显示。我在样式里给chartDom设置了height: 300px,然后在窗口大小变化时调用myChart.resize()。如果不做 resize,浏览器窗口缩放后图表会变形,这个细节在初期很容易被忽略。

6. 联调与部署阶段的高频问题排查

6.1 前后端字段命名对不上的问题

前后端联调时最常遇到的就是字段名对不上。Java 后端习惯用驼峰命名userName,MySQL 表字段习惯用下划线user_name。如果 MyBatis 没有开启驼峰映射,返回给前端的对象属性就是下划线格式,前端却按驼峰取数据,显示当然为空。

解决办法是在application.yml里配置:

mybatis: configuration: map-underscore-to-camel-case: true

但如果某些查询用了自定义统计 SQL,返回的reportRate之类的字段不会自动映射,需要在 SQL 里起别名,或者在 resultMap 里显式映射。联调时如果表格数据正常但某些列是空的,优先排查是不是字段名映射问题,而不是怀疑接口挂了。我习惯在开发环境开启 MyBatis SQL 日志,一眼就能看到返回列名和前端字段是否一致。

6.2 MySQL 时区导致的时间差 8 小时问题

这个坑几乎人人都会踩。现象是数据库里存的时间是对的,但后端查询出来传给前端,前端显示的时间比实际慢了或者快了 8 小时。原因通常是 MySQL 连接串没有指定时区,JDBC 驱动使用了服务器默认时区,然后LocalDateTime序列化时又叠加了一次本地时区偏移。

排查和解决路径是这样的:

  • 连接串里必须明确serverTimezone=Asia/Shanghai
  • SpringBoot 的spring.jackson.time-zone=GMT+8也要配上;
  • 数据库连接驱动用com.mysql.cj.jdbc.Driver
  • 日期字段建议统一使用DATETIME而不是TIMESTAMPTIMESTAMP会受数据库会话时区影响,DATETIME存什么就返回什么,逻辑更可控。

如果已经部署到服务器,还要检查服务器时区是不是 UTC。可以用date -R命令看,如果是 UTC,建议通过timedatectl set-timezone Asia/Shanghai改成本地时区。整个过程就是在“数据库—连接串—后端—前端”整条链路里把所有时区统一成东八区,缺一个环节就会偏差。

6.3 开发环境开启 MyBatis SQL 日志

联调时看不到 SQL 是最难受的。后端接口报错,如果你不知道 SQL 实际执行了什么,排查效率会极低。我在项目里做了两个配置:

logging: level: com.example.mapper: debug mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

这样控制台会打印完整的 SQL 语句和参数列表。比如一条SELECT查不出数据,看日志就能确定是条件条件拼接错误还是参数传成了 null。生产环境不建议开stdout,否则日志量太大;但开发环境强烈建议打开。还有一个技巧:如果只想看某个 mapper 的 SQL,可以把logging.level.com.example.mapper.HealthReportMapper设为debug,其他 Mapper 保持默认,减少噪音。

6.4 前后端分离部署与 Nginx 代理配置

部署是最后一个大头。后端打成 jar 包,直接java -jar home-server.jar运行;前端执行npm run build生成dist目录,放到 Nginx 的html目录下。由于前后端端口不同,存在跨域问题,但我不推荐在后端代码里写@CrossOrigin或者全局 CORS 配置,更好的方式是用 Nginx 转发。

Nginx 配置的核心是 location 前缀区分:

server { listen 80; server_name your.domain.com; root /usr/share/nginx/html; index index.html; 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; } }

这里比较关键的是try_files $uri $uri/ /index.html。如果写成try_files $uri $uri/ =404,刷新前端/health页面时 404,因为 Nginx 找不到对应的真实文件。加上/index.html后,所有刷新请求都回到前端路由,由 Vue Router 自己处理。proxy_pass http://127.0.0.1:8080/;末尾的斜杠表示把/api/前缀去掉转发,后端接口里就不需要额外配置 context-path,当然这也取决于你后端 ServerContextPath 的设置,前后要对应。

部署完成后,还有一个安全细节:后端服务不要直接暴露公网,只允许 Nginx 通过内网端口访问。如果服务器上开了防火墙,只开放 80/443 就够了,8080 端口别对外开放。

我现在回头看这套系统的维护过程,体会最深的不是某个框架或者某个接口写得多精彩,而是“边界控制”和“统一规范”这两件事。居家办公系统看起来简单,但一旦把业务边界划清楚,所有技术选型都围绕边界来做,代码结构自然就稳定了。你要是准备拿这套源码做二次开发,建议先从数据库表和部署流程看起,跑通之后再去改功能,会比从登录页一步一步点要快得多。后面我还会再整理一份关于这套系统里缓存策略和消息通知扩展的实战记录,到时候可以接着聊。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 7:51:21

编程Agent平台全盘点:17款工具分类详解与选型指南

2022年底我在老项目里第一次接触AI编程补全时&#xff0c;内心其实很平静&#xff1a;无非是多按几次Tab&#xff0c;少敲几个样板函数。可到了2024年下半年&#xff0c;事情开始变得不对劲——GitHub Copilot开始在多文件里连续修改&#xff0c;Cursor能用自然语言把整个支付模…

作者头像 李华
网站建设 2026/9/11 7:48:26

Android心率监测系统开发:BLE通信、PPG信号处理与实时波形实现

简介&#xff1a;一套面向Android毕业设计与课程设计的完整心率监测系统方案&#xff0c;覆盖安卓客户端、服务端与数据库三大模块。安卓端实现用户登录注册、通过手机摄像头实时测量心率、测试历史记录&#xff0c;并将结果上传至网站服务器&#xff1b;网站端支持管理员登录、…

作者头像 李华
网站建设 2026/9/11 7:47:17

ESP32蓝牙Beacon嵌入式测距实战:RSSI动态建模与精度优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 7:45:38

51单片机+ADS1110电子秤设计与实战

简介&#xff1a;本资源是一套完整的基于51单片机的电子秤设计开发包&#xff0c;面向嵌入式初学者、课程设计学生及单片机实践爱好者&#xff0c;解决重量检测系统从原理设计到仿真验证的全流程学习需求。资源共43个文件&#xff0c;涵盖Proteus仿真工程&#xff08;.pdsprj/.…

作者头像 李华
网站建设 2026/9/11 7:42:53

Claudian 响应慢怎么办:3 个高收益设置让它快回来

Claudian 响应慢怎么办&#xff1a;3 个高收益设置让它快回来 【免费下载链接】claudian An Obsidian plugin that embeds Claude Code/Codex as an AI collaborator in your vault 项目地址: https://gitcode.com/GitHub_Trending/cl/claudian 在 Obsidian 里用 Claudi…

作者头像 李华