1. 项目缘起从单体应用到SaaS化日报管理的思考最近几年我接触了不少中小企业的内部管理系统需求其中“日报/周报管理”几乎是每个团队都绕不开的话题。一开始大家可能就是用Excel表格在群里传来传去或者用共享文档。但随着团队规模扩大这种方式的弊端就暴露无遗格式混乱、难以统计、无法关联任务、领导批阅不便更别提移动端填写了。很多公司会考虑采购现成的OA系统但往往功能臃肿、价格不菲而且一些定制化需求比如和自家项目管理工具打通很难实现。于是自己动手搭建一个轻量、灵活、可多租户使用的SaaS化日报管理系统就成了一个性价比很高的选择。这不仅仅是做一个CRUD增删改查应用更涉及到如何设计一个支持多企业、数据隔离、可配置的后台架构以及如何提供一个体验流畅、响应迅速的前端界面。Vue 3凭借其组合式API带来的极致开发体验和性能优势Spring Boot以其“约定大于配置”的快速开发能力成为了实现这个想法的最佳技术组合。这个项目就是一次将这套技术栈应用于一个真实业务场景的完整实践我会把从技术选型、架构设计到具体编码、部署上线的全过程细节以及其中踩过的坑和总结的经验毫无保留地分享出来。2. 技术栈深度选型为什么是Vue 3 Spring Boot在启动任何项目前理性的技术选型是成功的基石。面对琳琅满目的前端框架和后端技术我最终锚定了Vue 3和Spring Boot这背后是一系列务实的考量而不仅仅是追逐热门。2.1 前端Vue 3的组合式API与生态成熟度首先看前端。Vue 3相较于Vue 2是一次巨大的革新其核心优势在于组合式API。在开发企业级应用尤其是像日报系统这样带有复杂表单、状态管理和交互的模块时选项式API的代码组织方式会显得有些力不从心。相关的逻辑如一个日报表单的验证、提交、草稿保存可能会被分散在data、methods、computed等多个选项中当组件变得复杂时理解和维护成本会急剧上升。组合式API允许我们将与同一个功能相关的所有代码响应式状态、计算属性、方法、生命周期钩子聚合在一个setup函数或script setup语法糖中。例如处理日报提交的逻辑可以封装成一个独立的useDailyReport组合式函数里面包含了表单数据、验证规则、提交方法等。这种基于逻辑而非选项的组织方式使得代码的可读性和可复用性得到了质的提升非常适合中大型项目。其次性能与体积。Vue 3通过重写响应式系统使用Proxy、优化编译器生成更高效的渲染函数和引入Tree-shaking支持使得打包后的体积更小运行速度更快。对于需要快速加载的SaaS应用这一点至关重要。再者完整的TypeScript支持。Vue 3的源码就是用TypeScript重写的提供了完美的类型推断。在日报系统中定义清晰的日报接口、用户接口、审批流接口能极大减少低级错误提升开发效率和代码质量。配合VSCode和Volar插件开发体验非常流畅。最后生态与社区。Vue的生态已经非常成熟对于管理系统UI库有Element Plus、Ant Design Vue等优秀选择路由有Vue Router 4状态管理有Pinia官方推荐比Vuex更简洁。这些库都对Vue 3提供了优先支持整合起来非常顺畅。2.2 后端Spring Boot的“开箱即用”与微服务亲和性后端选择Spring Boot理由同样充分。Spring Boot的核心哲学是约定大于配置它通过自动配置和起步依赖极大地简化了基于Spring的应用初始搭建和开发过程。对于日报系统后端我们可能需要Web服务、数据访问MyBatis/JPA、安全控制Spring Security、缓存Redis、消息队列可选用于异步发送通知等一系列组件。如果从零开始整合这些会耗费大量时间在XML或Java配置上。Spring Boot的spring-boot-starter-*依赖可以让我们一键引入所需功能。例如添加spring-boot-starter-data-jpa和mysql-connector-java配置好数据库连接就可以立刻开始编写实体类和Repository几乎无需额外配置。这让我们能快速将精力聚焦在业务逻辑也就是日报的增删改查、多租户数据隔离、权限校验等核心功能上。其次Spring Boot内嵌了Tomcat、Jetty或Undertow等Servlet容器这意味着我们可以将应用打包成一个可执行的JAR文件部署时只需要Java运行环境简化了部署流程。这对于后期可能的Docker容器化部署也非常友好。更重要的是对微服务架构的天然支持。虽然我们初版的日报系统可能是一个单体应用但SaaS系统随着租户增多、功能复杂未来有向微服务演进的可能。Spring Boot是Spring Cloud微服务套件的基石其简洁的配置和模块化设计使得未来拆分成用户服务、日报服务、审批服务等独立模块时迁移成本相对较低。技术栈协同考量Vue 3构建的前端是纯静态资源可以独立部署在Nginx或CDN上。Spring Boot后端提供纯净的RESTful API。前后端通过HTTP/HTTPS协议通信完全解耦。这种模式便于团队前后端分离开发、独立部署和水平扩展。在日报系统中前端负责渲染复杂的表单、时间线视图后端则专注于数据一致性、业务规则和安全保障。3. 核心架构设计多租户SaaS系统的数据隔离与扩展性搭建一个企业级SaaS系统区别于单企业部署系统的核心在于多租户架构。我们需要让多个企业租户共享同一套应用程序实例但他们的数据必须严格隔离互不可见。同时系统需要具备良好的可配置性和扩展性以应对不同企业的个性化需求。3.1 多租户数据隔离方案选型常见的多租户数据隔离方案有三种独立数据库每个租户拥有自己独立的数据库。安全性最高性能最好但运维成本和资源消耗也最大。共享数据库独立Schema所有租户共享同一个数据库实例但每个租户有自己的一套表Schema。在MySQL中Schema基本等同于Database。这种方式在隔离性和资源利用率间取得平衡。共享数据库共享Schema所有租户的数据都存放在同一套表结构中通过一个tenant_id字段来区分数据。成本最低但数据隔离完全依赖应用层代码设计和查询复杂度较高且有潜在的数据泄露风险。对于中小型日报管理系统我推荐采用方案三即共享Schema通过tenant_id区分。理由如下成本可控初创期租户数量不多数据量不大单一数据库实例完全足够。开发敏捷无需动态创建数据库或Schema简化了租户 onboarding入驻流程。新企业注册无非是在用户表里多了一条带tenant_id的记录。运维简单备份、监控、升级都只需要针对一个数据库。注意选择共享Schema方案必须在每一次数据库查询中都强制带上tenant_id条件。这是一个铁律任何疏忽都可能导致数据越权访问。这需要通过技术手段在框架层面解决而不是依赖开发人员手动添加。3.2 基于Spring Boot的租户上下文管理为了实现透明的、无侵入的租户数据隔离我们需要在请求进入后端时就识别出当前请求属于哪个租户并将这个租户ID存储在一个“上下文”中供后续的数据访问层自动使用。实现思路租户标识获取通常租户信息可以通过以下方式传递子域名如companyA.daily-system.com。利用Nginx或网关解析子域名将其作为租户标识放入请求头如X-Tenant-ID。请求路径如/api/tenant/companyA/daily-reports。但这会污染API路径不推荐。JWT令牌用户登录后颁发的JWT令牌中直接包含tenant_id声明。这是最常用、最安全的方式因为它与用户身份绑定。 在本项目中我采用JWT方案。用户登录时后端根据其所属企业生成包含tenant_id的JWT令牌。前端在后续所有请求的Authorization头中携带此令牌。创建租户上下文定义一个TenantContext类内部使用ThreadLocal来存储当前线程的租户ID。因为Web请求通常是一个线程处理一个ThreadLocal可以完美实现请求级别的租户隔离。public class TenantContext { private static final ThreadLocalString CURRENT_TENANT new ThreadLocal(); public static void setCurrentTenant(String tenantId) { CURRENT_TENANT.set(tenantId); } public static String getCurrentTenant() { return CURRENT_TENANT.get(); } public static void clear() { CURRENT_TENANT.remove(); } }拦截器解析租户创建一个Spring MVC拦截器或Spring Security过滤器在请求到达Controller之前从JWT令牌中解析出tenant_id并调用TenantContext.setCurrentTenant(tenantId)将其存入上下文。数据层自动过滤这是最关键的一步。我们需要让MyBatis或JPA在生成SQL时自动为查询条件加上tenant_id ?。以MyBatis为例可以使用其提供的插件机制。实现一个Interceptor拦截Executor的query和update方法。通过反射获取原始的SQL语句和参数对象。分析SQL语句如果是针对需要租户隔离的表如daily_report,user等则修改SQL在WHERE条件中动态添加tenant_id #{tenantId}。这个tenantId就从TenantContext.getCurrentTenant()获取。同时在插入INSERT数据时插件也需要自动将tenant_id字段的值设置上。通过这套机制业务开发人员在编写DAO层代码时几乎可以完全忘记多租户的存在就像在开发单租户系统一样。所有的数据隔离工作都在框架层面自动、强制地完成了极大地提升了开发效率并降低了出错风险。3.3 前后端分离下的API与状态管理设计前端采用Vue 3我们使用Pinia作为状态管理库。对于日报系统需要全局管理的状态包括用户信息含tenant_id、登录状态、当前企业的配置信息等。API设计遵循RESTful风格但需要为多租户做一点适配。虽然数据隔离由后端自动完成但API路径本身可以保持简洁。例如GET /api/daily-reports获取当前用户其租户上下文已确定的日报列表。POST /api/daily-reports创建一篇日报请求体中包含日报内容租户ID由后端自动从上下文中获取并填充。GET /api/admin/tenants这是一个系统级管理接口只有超级管理员能访问用于管理所有租户企业。这类接口需要特殊的权限控制与租户隔离的业务接口分开。前端通过Axios发起请求并配置请求拦截器自动为每个请求加上携带JWT的Authorization头。响应拦截器则统一处理常见的错误如401未授权/令牌过期跳转登录页403无权限提示用户500服务器错误进行友好提示等。4. 核心功能模块实现详解有了稳固的架构基础我们就可以开始实现具体的业务功能了。日报管理系统的核心模块主要包括用户认证与权限、日报的CRUD与查询、以及审批流程。4.1 用户体系、JWT认证与权限控制用户是系统的入口。我们需要区分系统管理员管理所有租户企业、企业管理员管理本企业内的用户和角色和普通员工。这里采用经典的RBAC基于角色的权限控制模型。后端实现Spring Security JWT实体设计User实体关联Tenant租户Role角色表以及User_Role关联表。Role关联Permission权限表。登录流程用户提交用户名、密码。UserDetailsService根据用户名加载用户信息并验证密码。验证通过后使用JJWT等库生成JWT令牌。令牌的Payload中应包含关键信息userId,username,tenantId,authorities权限列表。将JWT返回给前端同时可以将用户基本信息如昵称、角色也一并返回方便前端直接展示。认证过滤器自定义一个JwtAuthenticationFilter放在Spring Security过滤器链中。它负责从请求头Authorization中提取JWT。验证JWT的签名和有效期。解析JWT重新构建Authentication对象包含用户信息和权限并存入SecurityContextHolder。这样在后续的Controller中就可以通过AuthenticationPrincipal注解获取当前用户。权限校验使用Spring Security的注解式控制。PreAuthorize(hasRole(ADMIN))要求具有ADMIN角色。PreAuthorize(hasAuthority(daily:write))要求具有“日报:写”的权限。对于租户隔离的接口我们不需要在注解中显式检查tenant_id因为数据层插件已经保证了数据安全。但有时需要业务逻辑上的校验例如“企业管理员只能管理自己企业的用户”这需要在Service层代码中比较当前用户的tenantId和操作目标的tenantId是否一致。前端实现Vue 3 Pinia 路由守卫登录与令牌存储用户登录成功后将后端返回的JWT令牌和用户信息存储到Pinia的authStore中同时持久化到localStorage或sessionStorage防止页面刷新后状态丢失。请求拦截在Axios的请求拦截器中从authStore读取token并添加到每个请求的Header中。路由守卫使用Vue Router的导航守卫在进入需要权限的路由前进行检查。// 在路由守卫中 const isAuthenticated authStore.isAuthenticated; // 检查Pinia中是否有token/user const requiredRole to.meta.requiredRole; // 路由元信息中定义的所需角色 if (!isAuthenticated) { // 跳转到登录页 return { path: /login }; } if (requiredRole !authStore.user.roles.includes(requiredRole)) { // 无权限跳转到403页面 return { path: /403 }; }按钮级权限可以封装一个权限判断的函数或自定义指令v-permission在模板中控制按钮或组件的显示/隐藏。4.2 日报实体设计与富文本编辑集成日报的核心是内容。一个典型的日报实体可能包含以下字段public class DailyReport { private Long id; private String tenantId; // 租户ID由插件自动填充 private Long authorId; // 作者ID private String title; // 日报标题 private String content; // 日报内容富文本HTML private LocalDate reportDate; // 汇报日期 private ReportStatus status; // 状态草稿、已提交、已审阅... private Long reviewerId; // 审阅人ID private String reviewComment; // 审阅意见 private LocalDateTime submitTime; // 提交时间 private LocalDateTime reviewTime; // 审阅时间 // ... 其他字段如关联的项目、任务等 }其中content字段存储富文本。前端我们需要一个强大的富文本编辑器。WangEditor或Quill都是不错的选择它们轻量、开源、易于定制。以集成WangEditor V5Vue 3版本为例安装与引入npm install wangeditor/editor wangeditor/editor-for-vue封装成Vue组件创建一个RichTextEditor.vue组件在其setup中初始化编辑器实例并通过v-model与父组件的日报内容数据双向绑定。图片上传处理富文本编辑器通常需要上传图片。我们需要配置编辑器的图片上传菜单将图片上传到后端的一个接口如/api/upload/image。这个接口需要验证用户权限。将图片文件保存到服务器磁盘或对象存储如阿里云OSS、MinIO。生成一个可以公开访问的URL或带有时效性的签名URL返回给前端。前端编辑器用这个URL替换图片的src。重要经验绝对不要让富文本编辑器直接提交Base64格式的图片到content字段这会导致数据库字段巨大且难以管理和迁移。一定要走单独的文件上传接口。内容清理与安全后端在保存content之前必须进行HTML净化防止XSS攻击。可以使用像Jsoup这样的库只允许安全的HTML标签和属性通过。4.3 复杂查询、分页与统计功能日报列表页面通常需要支持多种查询条件按日期范围、按状态我的草稿、已提交、待我审阅、按关键词标题/内容搜索、按提交人等。后端实现使用MyBatis动态SQL在Mapper XML中使用if标签根据传入的参数动态拼接WHERE条件。这是处理复杂查询最灵活的方式。select idselectReportList resultMapReportResultMap SELECT * FROM daily_report WHERE tenant_id #{tenantId} if testparams.authorId ! null AND author_id #{params.authorId} /if if testparams.status ! null AND status #{params.status} /if if testparams.startDate ! null AND report_date #{params.startDate} /if if testparams.endDate ! null AND report_date #{params.endDate} /if if testparams.keyword ! null and params.keyword ! AND (title LIKE CONCAT(%, #{params.keyword}, %) OR content LIKE CONCAT(%, #{params.keyword}, %)) /if ORDER BY report_date DESC, submit_time DESC /select分页使用PageHelper插件。在Service层方法开始处调用PageHelper.startPage(pageNum, pageSize)后续的查询就会自动进行物理分页。返回的对象包含了分页数据列表和分页信息总条数、总页数等非常适合返回给前端。统计功能例如首页需要展示“本周已提交日报数”、“待审阅日报数”。这类统计查询需要编写单独的SQL通常使用COUNT、GROUP BY等聚合函数。为了避免N1查询问题应尽量在一个SQL中完成或者使用MyBatis的关联查询。前端实现查询表单使用Element Plus的Form组件绑定一个查询参数对象。表格与分页使用Element Plus的Table和Pagination组件。表格数据来自后端分页接口的返回。当页码或每页条数改变时重新发起请求。日期范围选择使用el-date-picker组件设置typedaterange可以方便地选择开始和结束日期。提交给后端时需要将其拆分成两个参数。4.4 审批流程的状态机设计日报的审批是一个简单的状态流转草稿 - 已提交 - 已审阅/已驳回。我们可以用一个枚举ReportStatus来定义状态。但更严谨的做法是引入一个轻量级的状态机明确定义状态之间的转换关系和触发条件。后端状态机实现 可以使用Spring State Machine或更轻量的squirrel-foundation。这里以简单的枚举Service层逻辑为例public enum ReportStatus { DRAFT, SUBMITTED, REVIEWED, REJECTED; // 判断是否可以从当前状态转换到目标状态 public boolean canTransitionTo(ReportStatus targetStatus, User currentUser, Report report) { switch (this) { case DRAFT: return targetStatus SUBMITTED report.getAuthorId().equals(currentUser.getId()); case SUBMITTED: return (targetStatus REVIEWED || targetStatus REJECTED) report.getReviewerId().equals(currentUser.getId()); case REVIEWED: case REJECTED: return false; // 终态不允许再转换 default: return false; } } }在Service层的submitReport、reviewReport方法中先获取日报的当前状态然后调用currentStatus.canTransitionTo(targetStatus, currentUser, report)进行校验校验通过才更新状态并记录操作日志。同时状态变更可以触发事件例如当日报被审阅后发送一条系统通知或邮件给提交者。前端状态流转 前端根据日报的status字段显示不同的标签和操作按钮。例如状态为SUBMITTED且当前用户是审阅人时才显示“通过”和“驳回”按钮。点击按钮后调用对应的后端API。5. 开发、部署与运维实战要点5.1 前后端分离的联调与跨域问题开发阶段前端运行在localhost:5173(Vite默认端口)后端运行在localhost:8080。浏览器出于安全考虑会阻止跨域请求。解决方案有两种后端配置CORS在Spring Boot的配置类或全局过滤器中添加CORS配置允许前端的源、方法和头信息。Configuration public class WebConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) // 针对所有/api开头的接口 .allowedOrigins(http://localhost:5173) // 允许的前端地址 .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true); // 允许携带cookie等凭证 } }前端代理推荐在Vite的配置文件vite.config.js中配置代理将/api开头的请求转发到后端服务器。这样前端代码中请求的URL可以写成相对路径/api/xxx避免了硬编码后端地址也更接近生产环境。export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, // rewrite: (path) path.replace(/^\/api/, ) // 如果需要重写路径 } } } })5.2 生产环境部署Nginx配置与HTTPS生产环境部署通常采用以下架构用户 - Nginx (HTTPS/负载均衡/静态资源) - Spring Boot应用 (JAR)前端执行npm run build生成dist目录下的静态文件HTML, JS, CSS。将这些文件放到Nginx的某个目录下如/usr/share/nginx/html/daily-admin。后端使用mvn clean package打包生成可执行的JAR文件如daily-system-1.0.0.jar。通过java -jar命令启动或使用 systemd、Docker 等方式托管。Nginx配置一个关键的Nginx配置示例如下server { listen 80; server_name your-domain.com; # 或 *.your-domain.com 用于多租户子域名 # 强制跳转HTTPS return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; server_name your-domain.com; # SSL证书配置 ssl_certificate /path/to/your/cert.pem; ssl_certificate_key /path/to/your/key.pem; # 前端静态资源 location / { root /usr/share/nginx/html/daily-admin; index index.html; try_files $uri $uri/ /index.html; # 支持Vue Router的history模式 } # 后端API代理 location /api/ { proxy_pass http://localhost:8080; # 后端Spring Boot应用地址 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 如果使用WebSocket可能需要以下配置 # proxy_http_version 1.1; # proxy_set_header Upgrade $http_upgrade; # proxy_set_header Connection upgrade; } # 可能还有上传文件的代理 location /upload/ { proxy_pass http://localhost:8080/upload/; # ... 同上proxy_set_header配置 } }这个配置实现了HTTP自动跳转HTTPS、静态文件服务、API请求转发。如果采用多租户子域名模式需要配置通配符SSL证书并将server_name改为*.your-domain.com并在后端解析请求头中的Host或自定义头来获取租户标识。5.3 数据库优化与缓存策略随着数据量增长一些查询可能会变慢。索引优化在经常用于查询条件的字段上建立索引如daily_report表的(tenant_id, report_date)、(tenant_id, author_id, status)联合索引。使用EXPLAIN命令分析慢查询SQL的执行计划。引入缓存对于不经常变化但频繁访问的数据可以使用Redis进行缓存。例如用户信息缓存用户登录后将其信息缓存一段时间避免每次鉴权都查数据库。企业配置缓存每个租户的系统配置如日报提交截止时间、可用字段等。首页统计看板数据可以定时任务计算并缓存避免每次打开首页都执行复杂的聚合查询。 在Spring Boot中整合Redis并使用Cacheable、CacheEvict注解可以轻松实现声明式缓存。5.4 监控、日志与问题排查一个健壮的系统离不开可观测性。应用日志使用SLF4J Logback合理设置日志级别INFO, WARN, ERROR。日志中要包含请求ID或Trace ID、用户ID、租户ID方便串联一次请求的所有日志。将日志输出到文件并使用Logstash等工具收集到ELKElasticsearch, Logstash, Kibana栈进行集中查看和分析。健康检查Spring Boot Actuator提供了/actuator/health、/actuator/metrics等端点可以监控应用状态、JVM内存、数据库连接池等。在生产环境应通过Spring Security保护这些端点或仅允许内网访问。APM工具对于更深入的性能监控可以考虑集成SkyWalking、Pinpoint等APM工具它们可以追踪分布式请求链路定位慢SQL、慢方法。6. 典型问题排查与进阶思考在实际开发和运维中总会遇到一些“坑”。这里分享几个我遇到过的典型问题及其解决方案。6.1 前端路由在刷新后404的问题这是Vue Router使用history模式部署到Nginx时的经典问题。用户直接访问/daily-reports或刷新该页面时Nginx会把这个路径当作一个真实的文件请求去root目录下查找当然找不到于是返回404。解决方案正如前面Nginx配置所示关键是在处理前端路由的location /块中加入try_files $uri $uri/ /index.html;这行配置。它的意思是先尝试找请求的文件$uri再尝试找对应的目录$uri/如果都找不到最后返回/index.html文件。Vue应用加载后路由器就能根据URL路径正确渲染对应的组件了。6.2 MyBatis多租户插件与复杂SQL的冲突我们实现了自动添加tenant_id条件的MyBatis插件。但在某些复杂场景下插件可能“误伤”或“遗漏”。场景一LEFT JOIN查询。插件可能只在主表的WHERE条件后添加了tenant_id但如果关联的子表也需要租户隔离就会漏掉。解决方案是在插件中更精细地解析SQL识别出所有需要隔离的表别名并为其添加条件。这需要较强的SQL解析能力。场景二UNION查询。插件可能无法正确处理UNION两边的SELECT语句。一个更稳妥的方案是在编写这类复杂SQL时手动在Mapper XML中显式地加上tenant_id #{tenantId}条件并告诉插件跳过对该语句的修改可以通过在Mapper方法上加自定义注解来实现。场景三系统级查询。有些查询需要跨租户比如超级管理员查看所有企业的活跃度统计。这类查询应该使用独立的Mapper方法并显式标注如IgnoreTenantFilter让插件跳过处理。实操心得多租户插件是保证数据安全的利器但必须对其进行充分的单元测试和集成测试覆盖各种复杂的SQL场景。在项目初期可以采取“插件自动过滤 关键处手动校验”的双保险策略。6.3 大文件上传与断点续传日报系统如果支持附件上传可能会遇到大文件如几十兆的视频或设计图上传超时或失败的问题。前端分片使用库如simple-uploader.js将文件切割成多个小块chunk。后端合并为每个文件生成一个唯一标识如UUID。前端上传每个分片时都携带这个标识和分片序号。后端将分片临时存储。全部分片上传完成后前端发送一个合并请求后端按序号将临时文件合并成最终文件。断点续传在上传前前端先计算文件的MD5等哈希值作为标识。上传每个分片前先询问后端该分片是否已上传秒传。这样即使网络中断恢复后也可以只传剩余的分片。Spring Boot后端实现需要两个关键接口/api/upload/chunk(上传分片) 和/api/upload/merge(合并文件)。注意处理并发上传和临时文件清理。6.4 系统扩展性思考从单体到微服务当租户数量达到数百上千日报量巨大时单体应用可能会遇到性能瓶颈。此时可以考虑向微服务演进。拆分维度可以按业务域拆分例如用户中心服务负责认证、授权、用户管理。日报核心服务负责日报的CRUD、查询、统计。审批流程服务负责状态流转、通知发送。文件服务专门处理文件上传、下载、存储。技术挑战分布式事务比如“提交日报”需要同时更新日报状态和发送通知可能涉及两个服务。可以考虑使用最终一致性方案如基于消息队列RabbitMQ/RocketMQ的事件驱动架构。服务发现与通信使用Nacos、Consul作为注册中心服务间通过OpenFeign进行声明式HTTP调用。统一网关使用Spring Cloud Gateway作为API网关统一处理路由、认证、限流、日志。配置中心将各服务的配置如数据库连接、Redis地址统一管理在Nacos Config或Apollo中。 这个演进过程是循序渐进的初期可以先将压力最大的模块如文件服务独立出去逐步积累微服务治理的经验。搭建这样一个系统是一个不断权衡、决策和迭代的过程。没有完美的架构只有适合当前阶段和未来预期的架构。从Vue 3和Spring Boot的基础使用到多租户架构的设计再到生产环境的部署优化每一步都需要结合具体的业务需求和技术团队的实际情况来做出选择。这个项目不仅是一个工具的实现更是一次对现代Web全栈开发、SaaS模式理解的深度实践。希望这些详实的步骤和经验能为你带来切实的帮助。