简介:面向高校毕业设计及课程设计场景,这是一套基于JAVA+SpringBoot+MySQL+微信小程序架构的在线办公小程序完整项目,适用于需要快速完成可运行演示系统的学生或开发者。包内包含项目源码、MySQL数据库脚本、运行脚本及配套论文,前后端代码齐全,开发环境建议为IDEA、微信开发者工具与Navicat,已通过导师指导并确保调试可运行。
资源共1273个文件,整体约21.05MB。主要文件类型包括png图片(界面资源)、vue/js/java(前后端逻辑)、wxml/wxss与json(小程序页面与配置)、sql脚本(数据库结构)以及docx论文等,rar压缩包结构清晰,还附带bat启动脚本便于一键部署。
目前已有81人学习下载。基于SpringBoot的自动配置与MySQL的稳定存储,较适合作为毕业设计或课程设计范本,也可帮助企业快速搭建轻量级办公管理场景。完整源码、数据库脚本与论文共同构成了从设计到实现的可复用方案,可在实际学习中对照阅读并二次开发。
1. 在线办公小程序的登录爆量与审批超时,问题出在架构分层上
接手这个基于 SpringBoot + MySQL + 微信小程序的在线办公项目时,我第一反应是看它的请求链路。多数人会把这类系统当成「CRUD + 微信登录」的简单组合,但实际跑起来,真正决定体验的往往是三件事:小程序端的静默登录态如何维持、审批流这类有状态业务在无状态接口里怎么落地、以及 MySQL 在高频读写下索引和事务隔离级别会不会拖后腿。这个项目的代码结构在这一点上做得比较规矩,后端没有把业务逻辑堆在 Controller 里,小程序端也没有到处写 wx.request,而是把请求封装在独立的 util 层。如果你的课程设计或毕设需要一份能讲清楚「为什么要这样分层」的参考实现,这个包里的源码、数据库脚本和论文能直接拿来对照阅读,而不是只给一个能跑的黑盒。全文我会按数据模型、登录态、审批流、文件处理、部署排错这条线拆开讲,每个环节都给出可复现的代码和参数。
2. 从 .classpath 看工程结构,SpringBoot 项目的分层与数据模型设计
拿到压缩包解压后,不要急着点 1-install.bat。先看根目录下那些 .bak 文件——main.css.bak、IndexMain.vue.bak 这些是微信开发者工具编译前的备份,说明前端在小程序端做过多次样式和组件结构调整。真正的后端工程在另一个目录里,标准的 SpringBoot 结构:controller、service、mapper、entity 四层。我建议你按这个顺序读代码,因为它的数据访问层用的是 MyBatis-Plus,不是原生 MyBatis,这意味着你在论文里可以强调「利用 MyBatis-Plus 的 LambdaQueryWrapper 减少 SQL 编写量」,这是当前企业开发的主流写法。
2.1 MySQL 表结构设计:审批流与任务分配的核心表
在线办公系统的数据模型通常围绕「用户-角色-任务-审批」四条线展开。数据库脚本里会看到这几张核心表:sys_user 存员工账号和 openid,sys_role 和 sys_user_role 做权限模型,oa_task 存任务,oa_leave 或 oa_approval 存审批单。以审批表为例,我建议字段设计如下:
CREATE TABLE `oa_approval` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `approval_no` varchar(32) NOT NULL COMMENT '审批单号,格式:AP+yyyyMMdd+随机四位', `user_id` bigint(20) NOT NULL COMMENT '发起人ID', `approval_type` tinyint(4) NOT NULL COMMENT '1-请假 2-报销 3-用章', `title` varchar(100) NOT NULL COMMENT '审批标题', `content` text COMMENT '审批内容', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0-待审批 1-通过 2-驳回 3-撤回', `current_approver` bigint(20) DEFAULT NULL COMMENT '当前审批人ID', `apply_time` datetime NOT NULL COMMENT '发起时间', `finish_time` datetime DEFAULT NULL COMMENT '完成时间', PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批表';这个表设计的要点在于:status 单独建索引是为了支撑列表页的「我的待办」「已办」「发起」三个 Tab 切换——这三个查询的过滤条件都是 status 加 user_id 的组合;approval_no 用业务号而不是自增 id,是为了将来对接财务系统时能直接通过单号追溯,这个细节在毕业答辩时可以重点讲。utf8mb4 是必须的,因为微信小程序的用户昵称里经常有 emoji 表情,utf8 会报 Incorrect string value 错误,这是常见的 MySQL 安装配置教程里很少提到的坑。另外注意所有表都加create_time和update_time两个通用字段,MyBatis-Plus 的自动填充注解可以直接帮我们维护它们,避免在业务代码里到处写 new Date()。
2.2 MyBatis-Plus 的条件构造器,替代繁琐的 XML 映射
这个项目的数据访问层没有把 SQL 写在 XML 里,而是用 MyBatis-Plus 的 BaseMapper 接口加 LambdaQueryWrapper 实现。例如查询当前用户待审批列表,代码如下:
@Override public List<OaApproval> getPendingApprovals(Long userId) { LambdaQueryWrapper<OaApproval> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(OaApproval::getStatus, 0) .eq(OaApproval::getCurrentApprover, userId) .orderByAsc(OaApproval::getApplyTime); return oaApprovalMapper.selectList(wrapper); }这段代码的逻辑说明:eq方法生成等于条件,第一个参数是实体类的 Lambda 方法引用,这样做的好处是编译期就能检查字段名是否正确,不会出现 XML 里 SQL 写错列名但启动时才报错的情况。orderByAsc按申请时间升序,保证先提交的审批单排在前面。如果你是做 java 面试八股文复习,这里对应的是「MyBatis-Plus 和 MyBatis 的区别」这种问题——回答要点是:BaseMapper 内置了 CRUD 方法,单表操作不需要写 SQL,LambdaQueryWrapper 解决了字段名硬编码问题。但这些内置方法解决不了多表联查,比如审批列表需要显示发起人姓名,就得自己写一个 VO 类然后用selectMaps或自定义 Mapper 方法,这是分层的边界所在,不要为了省事全用 Wrapper。
3. 微信小程序登录态与授权流程,从 wx.login 到后端 Session 管理
小程序端的登录是整个系统的入口,也是每年答辩时老师必问的环节。微信小程序的授权机制是:通过wx.login()获取临时 code,后端拿这个 code 去微信接口服务换 openid 和 session_key。openid 是用户在小程序生态里的唯一标识,而 session_key 用于解密手机号等敏感数据。
3.1 后端登录接口,用注解校验参数并用 Redis 管理会话
我自己在写 SpringBoot 项目时很少在登录接口里直接操作 Redis,但这个项目的资源包里可能没有集成 Redis,所以用 ConcurrentHashMap 或数据库表存 token 也是常见做法。实际生产环境一般用 Redis,但作为毕业设计,把 token 直接存在内存里,答辩时会被问「重启后 token 失效怎么办」。我的建议是退一步,用 JWT 无状态方案,把用户 id 和角色编码进 token。核心代码如下:
@PostMapping("/login") public Result login(@RequestBody LoginRequest request) { // 1. 校验参数 if (!StringUtils.hasText(request.getCode())) { return Result.error("code不能为空"); } // 2. 调用微信接口服务,获取 openid String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appId + "&secret=" + appSecret + "&js_code=" + request.getCode() + "&grant_type=authorization_code"; String response = restTemplate.getForObject(url, String.class); // 3. 解析返回,取出 openid JSONObject json = JSONObject.parseObject(response); String openid = json.getString("openid"); if (openid == null) { return Result.error("微信登录失败,请检查appid和secret"); } // 4. 查数据库,不存在则注册新用户 SysUser user = sysUserService.getByOpenid(openid); if (user == null) { user = new SysUser(); user.setOpenid(openid); user.setNickname("微信用户" + openid.substring(0, 8)); sysUserService.save(user); } // 5. 生成 JWT String token = JwtUtil.createToken(user.getId(), user.getRoleId()); return Result.success(token); }这段代码把登录流程拆成了五个清晰的步骤,每一步都有明确的日志输出点,排错时不容易迷路。注意第 2 步的 URL 拼接,appid 和 secret 在微信公众平台的开发管理页面可以看到,但不要把这两个值写死在代码里,而应该放到 application.yml 里用@Value注入。调试时最常遇到的坑是:code 只能用一次,五分钟过期,如果在测试工具里连续点两次登录按钮,第二次就会返回40029 code无效错误。对应到 java 面试题目,这就是「为什么需要 code2session 而不是直接返回 openid」——直接暴露 openid 会被恶意调用接口刷用户数据,加一层 code 交换相当于临时凭证,这是微信设计的闭环。
3.2 小程序端请求封装,拦截器统一处理 token 过期
小程序端的代码里,你会在 utils 目录下找到一个 request.js 之类的文件,它用 Promise 封装了 wx.request。注意到重点在于:request 里先检查本地缓存是否有 token,没有就跳转登录页,有则在 header 里带上。这里有一个细节值得借鉴——要设置请求拦截,不能每次调用 wx.request 都重复写 getStorageSync。
const request = (url, method, data) => { return new Promise((resolve, reject) => { const token = wx.getStorageSync('token'); wx.request({ url: baseUrl + url, method: method, data: data, header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, success: (res) => { if (res.statusCode === 401) { // token过期,清空缓存跳转登录页 wx.removeStorageSync('token'); wx.removeStorageSync('userInfo'); wx.reLaunch({ url: '/pages/login/login' }); reject(new Error('登录已过期')); } else { resolve(res.data); } }, fail: (err) => reject(err) }); }); };这段封装的关键在于:把 baseUrl 抽成常量,这样部署到不同环境(测试服、正式服)时只需改一个变量。另一个关键点是 401 的统一处理,如果不在这里集中处理,每个页面的 onLoad 里都要写一套 token 过期的判断,代码冗余且容易漏。微信小程序里顶部导航栏高度在不同机型上不统一,所以在处理登录页跳转时,不要用wx.navigateTo的默认动画,直接wx.reLaunch清空页面栈,避免用户按返回键退回已失效的页面。
4. 审批流的实现:状态字段驱动的业务闭环
审批是这个在线办公系统的核心业务,也是论文里最能体现设计深度的章节。在线审批需要解决的问题,一句话概括:一条审批单如何在多个审批人之间流转,每一步操作后状态如何变化,以及并行审批和会签如何实现。
4.1 状态机设计,用常量类管理状态流转
我在这个项目里看到的状态管理方式是普通整数加注释,这里我建议参照工作流引擎的思路,用状态字段加事件驱动。状态机定义如下:
public class ApprovalStatus { // 待审批 public static final int PENDING = 0; // 审批通过 public static final int APPROVED = 1; // 已驳回 public static final int REJECTED = 2; // 已撤回 public static final int WITHDRAWN = 3; // 审批中(已转交) public static final int TRANSFERRED = 4; }状态流转的合法路径是:PENDING -> APPROVED、PENDING -> REJECTED、PENDING -> TRANSFERRED、APPROVED -> WITHDRAWN(仅限发起人在对方审批前撤回)。具体实现时,在 Service 层写一个approve()方法,参数包括审批单 id、当前用户 id、审批结果和备注:
@Transactional(rollbackFor = Exception.class) public void approve(Long approvalId, Long approverId, Integer result, String comment) { // 1. 查单,加锁防止并发重复审批 OaApproval approval = oaApprovalMapper.selectByIdForUpdate(approvalId); if (approval == null) { throw new BusinessException("审批单不存在"); } // 2. 校验当前用户是否为审批人 if (!approval.getCurrentApprover().equals(approverId)) { throw new BusinessException("你不是当前审批人"); } // 3. 校验状态 if (!approval.getStatus().equals(ApprovalStatus.PENDING)) { throw new BusinessException("该审批单已处理"); } // 4. 更新状态 approval.setStatus(result == 1 ? ApprovalStatus.APPROVED : ApprovalStatus.REJECTED); approval.setFinishTime(new Date()); // 5. 写入审批记录 approvalLogService.record(approvalId, approverId, result, comment); oaApprovalMapper.updateById(approval); }这里selectByIdForUpdate是 MyBatis-Plus 提供的行级锁查询,对应 MySQL 的SELECT ... FOR UPDATE。为什么加锁?因为审批场景下,两个审批人同时点击「通过」按钮,如果不加锁,两个请求都会读到 status=0,都通过校验,然后都执行 update,最终状态还是 PENDING 的覆盖风险。加了FOR UPDATE后,第二个事务会被阻塞到第一个事务提交才继续,此时重新读到状态已经是 APPROVED,就走到第 3 步的校验里抛异常了。这个细节答辩时讲出来,分数会明显不一样——这是并发编程的具体应用,比单纯背 java 面试八股文里的 synchronized 例子更有说服力。
4.2 多级审批与条件分支
实际业务里,请假审批超过三天可能要部门主管和总经理两级审批。为了实现这个,oa_approval 表需要增加level和max_level两个字段。level记录当前审批层级,max_level记录总层级。审批通过后判断:如果level + 1 <= max_level,就把current_approver更新为下一级审批人的 id,状态保持 PENDING;否则置为 APPROVED。
if (approval.getLevel() < approval.getMaxLevel()) { approval.setLevel(approval.getLevel() + 1); Long nextApprover = userService.getNextApprover(approval.getApprovalType(), approval.getLevel()); approval.setCurrentApprover(nextApprover); approval.setStatus(ApprovalStatus.PENDING); } else { approval.setStatus(ApprovalStatus.APPROVED); approval.setFinishTime(new Date()); }这一段逻辑要注意的是:审批人的查询不能和审批操作放在同一个事务里,否则如果getNextApprover查出来是 null(比如没有配置上级),整个事务会回滚,审批单停留在 PENDING,但用户那边已经收到「审批通过」的成功提示,造成数据不一致。我的建议是:先查审批人,查不到就抛异常提示管理员配置审批链,查到再走事务。
5. 小程序端首页动态加载与文件上传的临时路径问题
首页的动态加载、附件管理、文件上传这几个模块放在一起讲,因为它们都涉及同一个前端问题:微信小程序的本地文件系统是沙盒机制,wx.chooseMessageFile或wx.chooseMedia选出来的临时文件路径(形如http://tmp/xxx.wxfile)只在当前会话有效,必须主动调用wx.uploadFile上传到服务器。
5.1 修改首页加载页面的入口
项目里有一个需求是「修改刚进入的加载页面」,这在微信小程序里对应app.json的pages数组。排在第一项的路径就是小程序的启动页。很多时候我们开发完登录页,想把首页改成启动页,但直接换路径会导致登录逻辑执行时机变化。正确做法:
{ "pages": [ "pages/index/index", "pages/login/login", "pages/task/task", "pages/approval/approval", "pages/mine/mine" ], "window": { "navigationBarTitleText": "在线办公", "navigationBarBackgroundColor": "#2b4b6f", "navigationBarTextStyle": "white" } }对应的场景说明:微信小程序加载页面路径是 pages 数组第一个元素,而上一次用户已经登录且 token 未过期时,应该直接进首页,反之则跳登录页。在pages/index/index.js的onLoad里先检查 token,不存在就wx.reLaunch到/pages/login/login。注意不要用wx.navigateTo,它只能跳转非 tabBar 页面,且会保留当前页面在栈里,用户按返回键会退回到空白页。如果用 tabBar 页面做首页,wx.switchTab是唯一合法跳转方式,这是小程序开发里很容易踩的 API 边界问题。
5.2 文件上传,后端用 MultipartFile 接参
审批单里上传附件,小程序端代码和微信开发者工具里调试最常见的报错是uploadFile:fail。原因多半是后端接口的 url 路径没对上,或请求头少带了 token。后端代码:
@PostMapping("/api/file/upload") public Result uploadFile(@RequestParam("file") MultipartFile file, @RequestParam("bizType") String bizType) { if (file.isEmpty()) { return Result.error("上传文件不能为空"); } // 校验大小,限制10MB if (file.getSize() > 10 * 1024 * 1024) { return Result.error("文件大小不能超过10MB"); } String originalFilename = file.getOriginalFilename(); String extName = originalFilename.substring(originalFilename.lastIndexOf(".")); // 生成新文件名,防止重名覆盖 String newFileName = System.currentTimeMillis() + "_" + UUID.randomUUID().toString().replace("-", "") + extName; // 存储路径按日期分目录 String datePath = new SimpleDateFormat("yyyyMMdd").format(new Date()); String dirPath = uploadPath + "/" + datePath; File dir = new File(dirPath); if (!dir.exists()) { dir.mkdirs(); } String fullPath = dirPath + "/" + newFileName; file.transferTo(new File(fullPath)); // 返回访问路径 return Result.success("/upload/" + datePath + "/" + newFileName); }这段代码的逻辑讲解:@RequestParam("file")必须和小程序端wx.uploadFile的name字段一致,否则后端拿不到文件,微信不上传 body。bizType参数用来区分这个附件属于审批单还是任务,后续下载时可以做权限校验。transferTo方法的底层是 Java NIO 的文件复制,如果目标目录不存在会抛 IOException,所以要先mkdirs()建目录。文件名用时间戳加 UUID,确实是避免重名的稳妥策略。要注意的是:不要把上传目录放在项目 resources 目录下,否则重新打包部署时文件会丢失,应该配置一个绝对路径,比如/data/oa/upload,并在 application.yml 里用${file.upload-path}注入。
6. 从 bat 脚本到生产可用:部署参数、静态资源映射与常见报错排查
解压目录里有三个 bat 文件:1-install.bat、2-run.bat、3-build.bat,对应 Windows 环境下的安装依赖、启动项目、打包三个步骤。这三个脚本说明作者用的是 Maven 构建,打包产物是一个可执行的 jar 包。在 IDEA 里创建 SpringBoot 项目时,如果 springboot 版本太高(比如 3.x),它默认依赖 Jakarta EE 包,但很多教程还在用 javax 开头,这里有一个兼容性大坑,下面讲启动排查时会说到。
6.1 启动脚本与静态资源路径映射
先看 2-run.bat 的内容,通常长这样:
@echo off chcp 65001 set JAVA_HOME=D:\Java\jdk1.8 set PATH=%JAVA_HOME%\bin;%PATH% java -jar -Xms256m -Xmx512m target\oa-server-1.0.0.jar pause参数说明:-Xms256m和-Xmx512m指定堆内存初始值和最大值,办公类系统单机部署这个配置够用。-Xmx设太大会浪费服务器内存,太小会导致高并发下 Full GC 频繁。chcp 65001是为了让控制台正确显示 UTF-8 日志,不然中文全变乱码。启动成功后访问http://localhost:8080,如果页面 404,检查是否配置了 SpringBoot 对静态资源的映射规则,因为上传的文件存在外部目录,不在 classpath 下,SpringBoot 默认不识别。需要在配置类里加一个映射:
@Configuration public class WebConfig implements WebMvcConfigurer { @Value("${file.upload-path}") private String uploadPath; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // /upload/** 是访问前缀,file: 后是磁盘绝对路径 registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + uploadPath + "/"); } }这段代码解决的核心问题是:浏览器访问http://localhost:8080/upload/20240601/xxx.jpg时,SpringBoot 怎么知道去磁盘的哪个目录找这个文件。addResourceHandler定义 URL 匹配规则,addResourceLocations定义磁盘路径,file: 前缀表示读文件系统,不加这个前缀会被当成 classpath 资源。注意结尾的/必须有,否则路径拼接会出错。
6.2 高版本 SpringBoot 迁移与 MySQL 驱动报错
网络检索热词里反复出现springboot版本太高,这里给一个排查思路。如果 3-build.bat 打包时报错或启动报ClassNotFoundException: javax.servlet.Filter,说明你本地 Maven 仓库里拉到了 SpringBoot 3.x 的依赖,但它需要 Tomcat 10 的 BaseServlet API,不是原来 Tomcat 9 支持的 javax 包。两个解决办法:一是用 SpringBoot 2.7.x 版本,pom.xml 里把<version>3.x.x</version>改成<version>2.7.18</version>,同时注意 MyBatis-Plus 要用 3.5.3 以上版本兼容;二是保留 3.x,但把所有import javax.servlet.*改成import jakarta.servlet.*,同时 MySQL 驱动改为com.mysql.cj.jdbc.Driver。
MySQL 5.7 版本下,连接串的时区参数是很常见的报错源头。如果启动时报The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized,这是乱码显示的中国标准时间,时区设置不识别。解决方法是连接串后面加参数:
spring: datasource: url: jdbc:mysql://localhost:3306/oa_office?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: yourpassword driver-class-name: com.mysql.jdbc.DriverserverTimezone=Asia/Shanghai是必须的,不写会默认取系统时区,而 MySQL 5.7 以上默认时区是 UTC,导致查询时间差八小时。useSSL=false是本地开发常见配置,避免 MySQL 自签名证书的警告刷屏。最后,打包时如果 3-build.bat 在 install 阶段卡住不动,多半是 Maven 在下载依赖但网络被墙,配置阿里云镜像到settings.xml:
<mirror> <id>aliyun</id> <mirrorOf>central</mirrorOf> <name>Aliyun Maven Mirror</name> <url>https://maven.aliyun.com/repository/central</url> </mirror>换源后重新执行mvn clean package -DskipTests,跳过测试能省 30% 的构建时间。项目跑起来之后,建议用微信开发者工具导入小程序端目录,记得在app.js里把baseUrl从http://localhost:8080改成局域网 IP,手机预览才能访问到后端接口。不要用http://127.0.0.1,真机测试时 127.0.0.1 指向手机自己,会报ERR_CONNECTION_REFUSED,同时在小程序后台把「不校验合法域名」开关打开,这是本地联调的标配操作。附件保存路径wx.env.user_data_path也可以关注一下,它是小程序本地文件目录的根路径,用wx.env这个全局变量做文件缓存和离线存储时比硬编码更稳。
本文还有配套的精品资源,点击获取