拓冰建站拓冰建站
首页 / 资讯中心 / 正文

SpringBoot+微信小程序在线办公:登录态、审批流与部署实战

简介面向高校毕业设计及课程设计场景这是一套基于JAVASpringBootMySQL微信小程序架构的在线办公小程序完整项目适用于需要快速完成可运行演示系统的学生或开发者。包内包含项目源码、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 存员工账号和 openidsys_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 审批单号格式APyyyyMMdd随机四位, 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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 ListOaApproval getPendingApprovals(Long userId) { LambdaQueryWrapperOaApproval 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 方法单表操作不需要写 SQLLambdaQueryWrapper 解决了字段名硬编码问题。但这些内置方法解决不了多表联查比如审批列表需要显示发起人姓名就得自己写一个 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_typeauthorization_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。为什么加锁因为审批场景下两个审批人同时点击「通过」按钮如果不加锁两个请求都会读到 status0都通过校验然后都执行 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_HOMED:\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 里把version3.x.x/version改成version2.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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: yourpassword driver-class-name: com.mysql.jdbc.DriverserverTimezoneAsia/Shanghai是必须的不写会默认取系统时区而 MySQL 5.7 以上默认时区是 UTC导致查询时间差八小时。useSSLfalse是本地开发常见配置避免 MySQL 自签名证书的警告刷屏。最后打包时如果 3-build.bat 在 install 阶段卡住不动多半是 Maven 在下载依赖但网络被墙配置阿里云镜像到settings.xmlmirror idaliyun/id mirrorOfcentral/mirrorOf nameAliyun Maven Mirror/name urlhttps://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这个全局变量做文件缓存和离线存储时比硬编码更稳。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门