小程序做内部工具,这几年确实是很多公司和高校毕设的首选。报销审批这种场景,业务逻辑清晰、角色分明、流程固定,用来练手或落地都非常合适。这个标题里信息量其实不少,除了免费源码这个卖点,真正有价值的是“怎么从零设计一个多角色审批流”,以及“Java、PHP、Python、C#这些后端语言怎么选”。基于我们长期做这类项目的经验,我把从需求拆解到技术选型,再到数据库设计、接口实现、权限控制、上线部署的完整链路写出来,争取让看到这篇内容的朋友少走弯路。
1. 项目概述与核心需求拆解
1.1 这个项目到底在解决什么问题
企业内部报销审批系统,本质上是一个流程管理工具。线下报销的痛苦相信很多人都经历过:填纸质单子、找领导签字、财务复核、出纳打款,中间任何一环人不在工位,整个流程就卡住了。尤其是几十人以上的公司或部门,报销周期动不动就拖一两周。
换成微信小程序来做,核心价值就四个字:移动、留痕。员工在手机上填单、拍照上传发票、提交审批,领导在微信里点开小程序就能批,财务在后台批量复核,所有操作自动记录时间、操作人、审批意见。整个过程不用装额外的App,微信扫一扫就能用,对非技术背景的同事非常友好。
从毕设或求职项目角度来说,这类系统的教学价值也很高。它里面涉及用户体系、角色权限、文件上传、流程状态机、数据统计这些通用模块,基本上把后端开发的常见知识点都串起来了。做完这一个项目,再去理解电商系统、工单系统、甚至简单的OA系统,逻辑都是相通的。
1.2 目标用户画像与使用场景分析
我把这个系统的主要使用者分成三类,对应三种不同的权限和操作界面:
| 角色 | 核心诉求 | 主要操作 |
|---|---|---|
| 普通员工(报销申请人) | 快速填单、随时查进度 | 提交报销单、上传发票、查看审批状态 |
| 审批人(部门/财务) | 高效处理待办、信息充分 | 查看明细、通过/驳回、填写意见 |
| 系统管理员 | 配置规则、管控全局 | 部门管理、用户管理、审批流配置、数据统计 |
实际使用场景通常是这样:员工出差回来,在手机上打开小程序,选择报销类型(差旅费、餐费、办公用品、交通费等),填写金额和事由,拍下发票照片上传,选择对应的审批流程,提交。部门经理收到微信通知(订阅消息),点开小程序看到申请单详情,有疑问就驳回并填写意见,没问题就通过。单据流转到财务人员那里,财务核对发票和金额,确认无误后通过,进入打款环节。全程无纸化,链条清晰,每一笔钱花在哪里都查得到。
1.3 系统边界与功能分层梳理
我给这类系统做功能规划时,习惯用前后端分离的思想来划分模块,哪怕是一个单体项目也要保持清晰的边界:
- 基础支撑层(不可或缺):微信登录注册(获取微信头像昵称)、用户身份认证(Token鉴权)、图片上传服务(发票拍照)。
- 业务核心层(这块是重点):报销单CRUD、审批流引擎(流转逻辑)、通知推送(订阅消息)、金额统计与筛选。
- 管理配置层(隐藏难点):多级审批规则配置、部门/岗位用户管理、流程节点可视化配置、操作日志记录。
- 数据报表层(加分项):按部门/人员/时间维度统计报销金额,月度趋势,预算预警提醒。
很多初学者做这类系统,容易把注意力全放在“报销单的增删改查”上,忽略了审批流和权限控制,结果做出来像个简易记账本。实际上审批流程引擎才是这个项目的灵魂,后面我会专门拆开讲。
2. 技术选型与架构设计思路
2.1 前端:为什么选择微信小程序原生框架
微信小程序目前有原生框架、第三方跨端框架(如Taro、uni-app)等选择。对于企业内部工具,我的建议是优先使用原生框架,原因很实际:
- 体积小、启动快。原生小程序没有额外的框架层开销,体验更接近原生应用。
- 上手门槛低。只要会HTML、CSS、JavaScript,基本上几个小时就能上手,不需要引入新的编译链。
- 调试工具成熟。微信开发者工具的报错信息、性能面板、真机调试,对新手非常友好。
- 第三方跨端框架在维护成本上更有优势:如果项目本身需要覆盖多端(小程序端、App端),可以考虑,但很多在微信小程序端会出现兼容性磨人的问题。
如果团队之后想扩展更多端,再引入跨端框架从原生迁移也是可行的路径,但最初设计时建议保持“原生优先”,这也是目前主流技术社区认可的做法。
项目在目录组织上,我一般会用这种结构:
pages/ login/ # 登录页 index/ # 首页(报销列表) submit/ # 提交报销单 detail/ # 报销单详情 approve/ # 审批操作页 mine/ # 个人中心 admin/ # 管理端(部分页面可复用) utils/ request.js # 封装 wx.request auth.js # Token 管理 components/ image-uploader/ # 图片上传组件 status-tag/ # 状态标签组件注意一个小细节:登录页和首页要做分离。不要让用户每次都走微信授权登录,而是通过静默登录(wx.login获取code)换取后端Token,这样用户打开小程序就是已登录状态,几乎无感知,体验会好很多。
2.2 后端语言选型:Java、PHP、Python、C# 怎么选
标题里提到了Java、PHP、Python、C#四种后端语言,我分别说一下适用场景,大家可以根据自己的熟悉程度和学习目标来决定:
| 技术栈 | 优点 | 适用场景 |
|---|---|---|
| Java(Spring Boot) | 生态最完整、市场招聘需求大、企业级标准 | 最推荐作为毕设/求职项目,简历上写出来的价值最高 |
| Python(Flask/Django) | 开发效率极高、代码量少、适合快速出效果 | 适合非计算机背景跨专业、时间紧的同学 |
| PHP(ThinkPHP/Laravel) | 部署简单、运维成本低、老项目兼容性强 | 适合熟悉PHP、追求快速上线的小团队 |
| C#(ASP.NET Core) | 性能好、与Windows生态集成度高 | 适合目标企业用.NET技术栈的场景 |
这里面没有绝对的“最好”,只有“最匹配你目标”的选择。如果从求职角度考虑,Java(Spring Boot)可能是最稳妥的选项,因为它的知识体系(Spring IoC/AOP、Maven、MyBatis等)是很多公司技术面必考的内容;如果纯粹是想快速把一个东西跑起来,Python(Flask)写起来确实是最快的,整个后端所有接口加起来可能只需一两天就能搭完。我下面统一用Java(Spring Boot)+ MyBatis Plus + MySQL来做核心示例,原因是它最主流、最容易找到对应资料。
2.3 整体架构图与数据流转链路
系统整体架构可以这样看:
- 接入层:微信小程序前端通过HTTPS请求调用后端API(域名需要在小程序后台配置白名单)。
- 网关层:Spring Boot拦截器统一处理Token校验、跨域、参数校验,拦截非法请求。
- 服务层:用户服务、单据服务、审批流服务、文件服务、统计服务。
- 数据层:MySQL存储业务数据(用户、审批单、流程记录),对象存储(如阿里云OSS/MinIO)存放发票图片,Redis缓存用户Token信息(也可用内嵌内存缓存替代,小项目不必强依赖Redis)。
数据流转用日常例子来说就是:员工填报的报销单,在数据库里是一条带状态字段的记录;当它被提交后,系统通过审批流引擎找到当前节点对应的审批人,并把单据ID和状态推送到审批人的待办列表;审批人执行通过操作后,系统更新单据状态并记录一条审批历史,再推送到下一个节点。整个过程本质就是一个状态机的状态迁移,这个思想大家一定要抓住。
3. 数据库设计与核心表结构
3.1 数据库表整体设计思路
报销审批系统的数据量并不大,单表记录数通常在几千到几万级别,但表之间的关联关系比较典型,适合用来理解数据库范式和外键关联设计。建议从以下几张表入手:
- 用户表(sys_user):存用户基本信息、微信openid(唯一标识)、部门ID、角色类型。
- 部门表(sys_dept):公司组织架构的简化版,便于按部门统计报销金额。
- 角色表(sys_role):员工、部门主管、财务、管理员四种角色。
- 报销类型表(expense_type):差旅费、餐费、办公用品、交通费等基础配置数据。
- 报销单主表(expense_order):核心业务表,每条记录对应一笔报销申请。
- 报销单明细表(expense_item):一张报销单可能对应多笔费用明细(如一次出差含交通+住宿+餐费)。
- 审批流配置表(approve_flow_config):定义不同报销类型对应几级审批、各级审批角色是谁。
- 审批记录表(approve_record):记录每一步审批的操作人、时间、意见、结果。
- 图片附件表(expense_attachment):发票原始图片的存储路径,关联到报销单或明细。
这种表结构保证了一类相对清晰的边界:主表存状态公共信息,子表存明细数据,流程表存审计记录。即使项目规模不大,这种分层也方便后续扩展(比如增加付款记录表、预算表)。
3.2 报销单与审批记录的核心字段设计
报销单主表(expense_order)的字段设计,我调过不少版本,现在稳定下来基本就是这样:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_no | varchar(32) | 报销单号(格式:BX + 日期 + 随机数,唯一索引) |
| user_id | bigint | 申请人ID |
| dept_id | bigint | 申请人所属部门ID |
| expense_type_id | bigint | 报销类型ID |
| total_amount | decimal(10,2) | 报销总金额 |
| status | tinyint | 状态:0草稿、1审批中、2已通过、3已驳回、4已打款、5已撤销 |
| current_approver_id | bigint | 当前待审批人ID(关键字段,查待办用) |
| apply_time | datetime | 提交时间 |
| update_time | datetime | 最后更新时间 |
| description | varchar(500) | 报销事由/说明 |
审批记录表(approve_record)相对简单,我更习惯这样设计:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_id | bigint | 关联报销单ID |
| approver_id | bigint | 审批人ID |
| approve_level | tinyint | 审批级数(第几级) |
| approve_action | tinyint | 操作:1通过,2驳回 |
| approve_comment | varchar(500) | 审批意见 |
| create_time | datetime | 操作时间 |
这里要特别提一下current_approver_id这个字段的设计。很多人会困惑:审批流一看就是用流程配置表动态计算出来的,为什么还要在报销单上存一个当前审批人?原因很简单,就是查询性能。当上级领导打开小程序“待办审批”列表时,本质上就是一条SQL:SELECT * FROM expense_order WHERE current_approver_id = #{userId} AND status = 1,这条查询走索引秒出结果。如果不冗余这个字段,那每次都要先把流程配置和审批记录拼一遍再筛选,SQL复杂不说,数据量上来之后性能会明显下降。
3.3 审批流程配置表:灵活是设计出来的
这一块是很多教程里讲得最少的,但实际开发中往往是最容易返工的部分。如果审批逻辑是硬编码的(比如代码里写死“金额大于1000需要二级审批”),那上线后业务方提需求说“我们部门想改成三级审批”,你就得改代码重新发布。
更好的做法是把流程规则配置化,用一张表(approve_flow_config)来存储规则:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| expense_type_id | bigint | 报销类型ID(不同类型可以走不同流程) |
| min_amount | decimal(10,2) | 区间最小金额 |
| max_amount | decimal(10,2) | 区间最大金额 |
| approve_levels | tinyint | 审批总级数 |
| node_config_json | text | 每个级次的审批角色配置(JSON格式) |
node_config_json字段里存的东西长这样(MySQL 5.7+直接支持JSON类型):
[ {"level": 1, "role": "dept_manager", "description": "部门经理审批"}, {"level": 2, "role": "finance_staff", "description": "财务复核"} ]那审批流引擎拿这个配置做事情,逻辑就很清晰了:提交报销单时,根据报销类型和金额,查配置表拿到审批链;将第一条节点记录的审批角色对应的实际用户写到报销单的current_approver_id字段;审批人通过后,按节点顺序往后推进;如果当前节点是最后一个节点,直接把状态置为“已通过”。这样设计之后,业务调整流程只需要改数据库里的配置,完全不用动代码。
4. 后端核心接口与业务逻辑实现
4.1 登录鉴权:微信登录流程与Token无感知刷新
微信小程序登录和网页登录不同,它没有传统的用户名密码输入,核心流程是:
- 前端调用
wx.login(),拿到一个临时凭证code。 - 前端把
code发送到后端提供的登录接口。 - 后端拿
code去请求微信的code2Session接口,换取openid和session_key。 - 后端拿着
openid去数据库查用户,如果不存在则自动注册(新用户)。 - 后端生成一个自定义Token字符串,把它返回给前端并存入小程序的storage。
- 前端后续所有请求都在header里带上这个Token,后端根据Token识别用户身份。
这里我踩过一个坑:直接用openid做用户标识传到小程序端。看起来没毛病,但openid如果暴露在前端,相当于把用户的唯一标识暴露给所有能看到请求的人,存在伪装风险。正确的做法是后端生成一个随机Token(可以用UUID),服务端维护Token到用户ID的映射关系,前端永远只拿Token说话。Token可以设置有效期(比如7天),过期之后前端再用wx.login换新code重新获取,用户无感知。
Java端用拦截器(HandlerInterceptor)统一做Token解析,核心逻辑大致是:
@Component public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains("/login")) { return true; } String token = request.getHeader("Authorization"); @SuppressWarnings("unchecked") Map<String, Object> tokenInfo = redisUtil.get("TOKEN_" + token); if (tokenInfo == null) { throw new BusinessException(401, "登录状态已过期,请重新登录"); } // 通过ThreadLocal把当前用户上下文传给业务层 UserContext.set(tokenInfo); return true; } }这里的逻辑很直白:每个需要登录的请求进来,先去Redis里查这个Token还能不能用;能就放行,不能就返回401,前端再触发wx.login换新Token。用Redis缓存Token而不是每次都查数据库,是为了减少数据库压力,当然如果项目规模很小、没有Redis环境,用一个内存ConcurrentHashMap也可以顶住,只是重启会丢,要接受这个取舍。
4.2 报销单提交:事务、状态机与并发控制
报销单提交这个接口是整个系统里最容易出问题的地方。因为在一次提交操作里,它需要同时干好几件事:
- 保存报销单主表数据(state为“审批中”)。
- 保存报销单明细数据(多张子表记录)。
- 根据金额和类型查审批流配置,生成审批链。
- 把第一级审批人的ID写到主表的current_approver_id。
- 记录一条审批历史(提交动作),向审批人发送订阅消息通知。
这五步任意一步失败,都不能让数据处于“半提交”状态,所以必须用@Transactional事务把整个过程包起来。Spring Boot里面这很简单,就是在一个方法上加注解。但我提醒一下,事务必须在最外层入口类方法上加,如果加在内部调用方法上,可能因为方法自调用绕过AOP代理,导致事务失效。这是一个很经典也很隐蔽的问题。
并发控制同样重要。用户可能双击提交按钮,或者手机网络延时导致前端重试,同一个报销单瞬间发来两个POST请求。如果后端不做幂等控制,数据库里就会多出一条重复的报销单。我的解决方案是:在提交接口入参里加一个clientRequestId(前端每次进入提交页生成一个UUID),后端在Redis里用SETNX命令把该ID标记为“已处理”,重复请求直接被拦截。操作顺序大致是:
// 伪代码 if (redis.setIfAbsent("CLIENT_REQ_" + clientRequestId, "1", 10分钟)) { // 第一次进入,正常的保存逻辑 } else { // 重复请求,直接返回"已受理,请勿重复提交" }4.3 审批流转:状态机的核心代码实现
审批通过和驳回是整个业务逻辑的重头戏。我在实际项目里把这段逻辑抽象成了一个独立的ApprovalFlowService,核心代码逻辑大致如下(简化版,实际还要加各种校验):
public void approve(ApprovalRequest req) { ExpenseOrder order = expenseOrderMapper.selectById(req.getOrderId()); // 校验状态:只有审批中才能继续 if (order.getStatus() != ExpenseOrderStatus.PROCESSING) { throw new BusinessException("当前单据状态不允许审批操作"); } // 校验权限:当前登录人必须是当前待审批人 if (!order.getCurrentApproverId().equals(UserContext.getUserId())) { throw new BusinessException("您不是当前节点的审批人"); } // 查询该单据的审批链(也可以从缓存读) FlowConfig flowConfig = getFlowConfig(order.getExpenseTypeId(), order.getTotalAmount()); List<NodeConfig> nodes = parse(flowConfig.getNodeConfigJson()); Integer newStatus = null;// 后续要改成的状态,先声明 int currentLevel = getCurrentLevel(order.getId()); if (req.getAction() == ApprovalAction.REJECT) { // 驳回:无论当前是第几级,状态都回到驳回 newStatus = ExpenseOrderStatus.REJECTED; } else { // 通过:判断是否还有下一节点 if (currentLevel >= nodes.size() - 1) { newStatus = ExpenseOrderStatus.APPROVED; // 最后一级也通过了 } else { newStatus = ExpenseOrderStatus.PROCESSING; // 把待审批人推进到下一级 NodeConfig nextNode = nodes.get(currentLevel + 1); order.setCurrentApproverId(findUserIdByRole(nextNode.getRole())); } } // 更新单据状态 order.setStatus(newStatus); expenseOrderMapper.updateById(order); // 记录审批历史 ApproveRecord record = new ApproveRecord(); record.setOrderId(order.getId()); record.setApproverId(UserContext.getUserId()); record.setApproveLevel(currentLevel); record.setApproveAction(req.getAction()); record.setApproveComment(req.getComment()); approveRecordMapper.insert(record); // 如果最终驳回或通过,可以发通知给申请人 sendResultNotify(order, newStatus); }说一下这段代码里比较关键的两个校验点。第一个是状态校验,只有status为“审批中”的单据才允许审批,避免出现“已驳回的单子又被审批人通过”这种逻辑上的矛盾。第二个是权限校验,当前登录人必须等于单据的currentApproverId,这个校验必须放在后端,不能只在前端做按钮隐藏。很多安全漏洞都是因为后端没校验,前端隐藏了按钮但接口照样能被调用。
4.4 图片上传与静态文件服务的处理
发票拍照上传是报销场景的刚需,处理不好会成为整个体验的短板。微信小程序上传文件和H5不太一样,它有自己的wx.uploadFile接口,需要在小程序端调用:
wx.chooseMedia({ count: 9, mediaType: ['image'], sourceType: ['camera', 'album'], success(res) { const tempFilePath = res.tempFiles[0].tempFilePath; wx.uploadFile({ url: 'https://api.example.com/api/upload', filePath: tempFilePath, name: 'file', formData: { token: getToken() }, success(uploadRes) { const data = JSON.parse(uploadRes.data); // data.url 就是上传后的图片地址 } }); } });后端接收文件后,有几个规范建议:
- 重命名:不要直接用原始文件名,而是用“日期+随机字符串”重命名,避免中文文件名和路径遍历问题。
- 限制文件类型和大小:只允许jpg/png/webp,单张不超过5MB,前端
chooseMedia有sizeType限制,后端也要二次校验,防止绕过前端直接传大文件。 - 存储位置:如果是独立服务器,建议存到本机磁盘上一个专门的目录,不要写进项目发布目录里面,这样重新部署时不会丢文件;发布目录在部署时会被覆盖,到时候好不容易收集的照片全没了,我实际见人踩过这坑。
- 访问路径:需要给静态资源目录配一个HTTP访问路径(如
/upload/**),并且设置跨域配置,否则真机预览时图片可能加载不出来。
如果公司有条件用阿里云OSS等对象存储,那就更省心,后端只负责生成签名URL,前端直传/回显都走云存储,服务器压力小很多。
5. 小程序端核心页面与交互设计
5.1 登录页与身份识别:头像昵称和普通人要做区分吗
小程序登录页设计很有意思。市面上很多教程直接推荐用wx.getUserProfile拉取头像昵称,但后来微信官方调整了规则,用户拒绝授权时依然可以正常使用。所以更合理的做法是:
- 不强制用户必须“授权头像昵称”,优先用
wx.login静默登录。 - 用户基本信息(姓名、部门)最好是由管理员在后台统一录入或让用户首次填个简单表单,而不是依赖微信提供的昵称。
- 头像可以用微信默认,也可以后续在个人中心让用户自愿设置。
为什么这样建议?因为企业内部系统最看重的是真实身份。微信昵称可能叫“我是xxx”、“A小王”这类乱七八糟的,后端如果拿昵称来做审批人判断,很容易出问题。正确的做法是:微信openid只用来做登录凭证和账号关联,真实姓名、工号、部门通过企业人事数据导入或用户自助填写维护。
5.2 首页报销列表:状态视觉化与下拉刷新
首页的设计决定第一印象。一个简洁有力的方案是:
- 顶部放一个“我要报销”的按钮,引导提交。
- 下面是历史报销单列表,用四个状态维度去展示:待审批、已通过、已驳回、全部。
- 每一条报销单卡片上显示:时间、报销类型、金额、当前状态标签。
- 状态标签用不同颜色区分:审批中是黄色,已通过是绿色,已驳回是红色,已打款是蓝色。
还有一个容易被忽略的细节:列表一定要支持下拉刷新和触底加载。企业用户在公司里用手机网络环境不稳定,如果数据没有及时刷新,用户会认为“我提交的单子是不是丢了”。下拉刷新直接调用wx.startPullDownRefresh配合onPullDownRefresh生命周期,成本很低,但能明显提升安全感。
审批人的待办列表其实可以复用这套列表组件,只需要在请求参数上区分:?status=待审批&approverId=当前用户ID,不需要重新写一套页面。这里就是前面数据结构设计得好的红利。
5.3 表单提交页:金额格式、必填校验与草稿保存
报销表单的字段不用太多,但有几个细节要注意:
- 金额输入:小数位限制两位,单位默认“元”,别让用户自己输小数点以外的内容,用
type="digit"调起数字键盘。 - 费用明细:支持动态添加多行(出差涉及交通+住宿+餐饮就很常见),每一行包含费用类型、金额、说明。总金额由前端实时汇总显示,提交时后端再计算校验一次,防止前端计算错误。
- 发票必传:报销单原则上必须上传发票图片,后端接口加个非空校验,前端用组件提示。但注意有些场景(比如无票补贴)需要豁免,可以在后端加一个
hasInvoice字段来标记,而不要把所有单子都卡死。 - 草稿保存:这是一个很提升好感度的功能。用户填到一半被别的事情打断是家常便饭,如果页面一关数据全没,体验非常糟糕。做法是:表单组件在
onUnload时把当前页面所有表单值存到本地storage,下次进入如果检测到草稿且不是刚提交过,就询问用户“发现草稿,是否恢复?”这个逻辑不算复杂,但对用户感知的提升非常明显。
5.4 审批操作页:详情、意见与图片预览
审批人点开一个待办,需要能看到什么?我认为至少包括:
- 报销单基本信息(单号、类型、申请人、部门、总金额)。
- 费用明细列表(每一项的名称、金额、说明)。
- 发票图片缩略图,点开可放大预览(
wx.previewImage)。 - 申请人的提交备注(如果填了的话)。
- 这个单据之前所有审批节点的流转记录(级别、审批人、时间、意见)。
在审批操作上,推荐“通过”和“驳回”使用两个不同方向的按钮,驳回时强制必填原因,这样能避免审批人说一句“不同意”就让员工一脸懵地猜原因。实际到前端实现的时候,弹层对话框里加个必填校验就好。
6. 系统权限设计与多级审批策略
6.1 基于角色的访问控制(RBAC)设计
这个系统里存在不同角色,权限控制的核心是RBAC(Role-Based Access Control),即把“权限”绑定到“角色”上而非直接绑定到“用户”上。具体落地到功能侧:
- 普通员工角色:只能查自己的单子,只能新增报销单,不能看到其他人的单据。
- 部门主管角色:能查看本部门所有单据,能处理“部门经理审批”节点的待办。
- 财务角色:能查看所有状态为“审批中/已通过”的单据,能处理“财务复核”节点的待办,且能看到发票图片。
- 管理员角色:可以管理用户、部门、审批流程配置、查看所有单据和数据统计。
在后端实现上,我建议用一张简单的用户角色映射表,然后在Service层通过@RequiresRole之类的注解做拦截。如果没用现成框架,可以自己写一个方法校验:checkRole(UserContext.getUser().getRole(), "finance"),不满足就抛403异常。这里想强调的一点是:前端根据角色隐藏按钮只是体验层的优化,真正的安全防线永远在后端接口的权限校验。即使是毕设项目,也要有这个意识,因为面试官非常容易问“你这个系统的权限是怎么控制的,前端隐藏和后端校验有什么区别”。
6.2 多级审批的可视化与流转规则
多级审批最常见的是这两种模式:
- 固定级次:比如金额无论多少,固定“部门经理->财务”两级审批。
- 金额区分级次:金额小于1000元只要部门经理审批;1000到5000需要部门经理+财务;超过5000还需要总经理。
实现上,第二种更接近真实企业的需求,也能在答辩或演示时讲出亮点。做法就是我前面提到的审批流配置表approve_flow_config,用费率区间和级次配置组合就能满足。
需要特别注意的是审批流转中“撤回”和“驳回后重新提交”的处理。真实的业务场景中,员工提交错了金额或者填错了类型,希望撤回;或者单据被驳回后,员工改完信息再重新提交。这两块逻辑如果处理不好,会在演示时露馅。我的经验是:
- 撤回:只有单据状态为“审批中”,且当前待审批节点还是第一级(还没被人处理过),才允许申请人撤回;一旦有任何人操作过,就不允许撤回了。
- 驳回后重新提交:把整个审批链重置,重新从第一级开始走。相当于一份新单据但保留了原始的单据号和历史记录,这样审计的时候能看到“这张单子在历史上被驳回过一次”。
6.3 操作日志:没有日志的系统不敢上线
日志这个点容易被新手忽略,但它对报销系统的价值非常重要。财务系统最怕扯皮:为什么这笔报销过了?是谁批的?批准的时候有没有附件?如果系统里只存最终状态,出了纠纷根本说不清。
所以我在设计时加了一张operate_log表,里面存:操作人ID、操作人姓名、操作类型(提交/审批通过/驳回/撤回/修改)、目标单据号、操作时间、IP地址(可选)。前端和后端各自记录一层,后端用AOP切面统一记录,简单可靠。
还有一个实用小技巧:日志表的数据不跟着业务数据删掉。哪怕财务漏录了一张单子或者管理员删除了某张测试单,日志里也要保留痕迹。所以删除报销单的功能,要么不做(只做状态流转),要么做成逻辑删除(加个deleted字段),尽量不物理删除。
7. 测试、部署与上线踩坑实录
7.1 本地开发环境搭建:从下载工具到跑通登录
一个典型的最小开发环境,需要准备这些东西:
- 微信开发者工具:从官网下载,用自己微信号扫码即可登录调试小程序。
- JDK 8+:Java后端本地开发环境。
- MySQL 5.7+:数据库,建表导入sql脚本。
- Redis(可选):用于Token和缓存,不装也可以用内存替代。
- 内网穿透工具:用于手机上真机预览时,让小程序能访问到你电脑上的后端服务。
这里有个常见坑:微信开发者工具的“不校验合法域名”选项。开发阶段勾上它,请求任意域名的接口都能通;一旦“上线”按钮提交审核时,这个选项就不生效了,必须在小程序后台配置HTTPS合法域名。很多新手在开发环境跑得很欢,一到真机预览连接口都连不上,就是没分清开发环境校验和上线校验的区别。
还有一个小建议:后端如果放在本机,Swagger接口文档一定要开。springfox或者springdoc依赖一引入,访问/doc.html就能看到全量接口列表,前后端联调效率翻倍。毕设答辩的时候打开Swagger页面,评审老师一眼就能看到你定义了哪些接口,效果比口述强得多。
7.2 云开发版与自建后端:取舍分析
提到小程序,很多人自然会想到微信云开发(CloudBase)。云开发提供了云数据库、云函数、云存储,可以完全不搭后端。那它适合这个项目吗?我的分析如下:
- 云开发版:开发极快,云函数写逻辑,数据库零运维,一个多月就能上线。但云开发和传统后端语言的生态差异大,如果目标是求职Java/Python开发,写云函数对你简历帮助有限;如果只是产品Demo或课程设计,那云开发准没错。
- 自建后端版:用Java/Python写一套标准后端,需要自己搞定服务器、数据库、部署,过程更“工程化”,知识迁移性强。面试时可以明确说“我完整实现了一套前后端分离的API服务”,说服力更强。
如果做毕设答辩,我建议自建后端;如果你只是帮某个小公司快速落地一个工具,云开发效率高得多。这两种路径做的系统,对外展示时完全可以各取所长。
7.3 常见错误排查速查表(呕心沥血整理)
这部分是我实际联调过程中最常遇到的问题,压箱底的东西直接公开。
| 症状 | 可能原因 | 排查步骤 |
|---|---|---|
| wx.request报406/403 | 前端请求头未带Token或Token过期 | 先断点看请求header,再查Redis里Token是否存在、是否过期 |
| 登录后每次刷新都掉线 | Token全局变量存得不对或接口路径拼错 | 检查Token有没有写入storage,检查请求拦截器有没有统一读取header |
| 真机预览图片加载不出来 | HTTPS域名未配置合法域名 | 去小程序后台把图片所在域名加入downloadFile合法域名 |
| 提交报销单提示“系统繁忙” | 后端事务回滚 | 看异常日志,多半是审批流配置解析失败或明细插入为空 |
| 待办审批列表显示为空 | status字段值不对或currentApproverId没更新 | 手动在数据库里UPDATE一条订单,改到正确的节点人,再查列表是否显示 |
| 订阅消息发不出去 | 模板ID配置错误或用户未订阅 | 检查小程序订阅消息模板ID、用户是否点击过订阅授权 |
| 部署后接口可以访问但小程序请求失败 | 跨域或HTTPS证书问题 | 后端加CORS配置,域名证书确认是有效HTTPS |
这里尤其想讲一下订阅消息的问题。微信的订阅消息和公众号模板消息不一样,不是随便就能推的。用户在触发某个事件时必须主动授权订阅,且授权一次只能发一条。所以合理的交互是:在用户提交报销单的按钮上,让用户同时点击“订阅审批结果通知”,点击量允许订阅几次,才能保证后续发通知的次数足够。另外订阅通知经常有1分钟以上的延迟,测试时不要以为没发出来就是代码有问题,等一会再看。
7.4 演示录像的制作小技巧
如果这个项目是拿来面试演示或当作品集展示的,我强烈建议大家录一个3分钟短视频,而不是让人直接扫小程序码。视频脚本可以这样安排:
- 前10秒:首页展示报销列表和核心功能入口。
- 30秒:演示提交一个报销单,包含上传发票、填明细、提交。
- 40秒:切换账号到审批人视角,演示待办审批、通过/驳回。
- 30秒:演示财务角色复核通过。
- 最后30秒:展示“我提交的单据的审批状态流转记录”,说明每一步都有日志可追溯。
录屏时记得把网络请求面板打开(或把Charles等工具的请求抓包窗口放在副屏),评审的人看到正常的HTTP请求、后端日志、数据库变化,会觉得这个系统的完整性远超普通课程设计。
8. 如何基于这个项目快速变体延伸
8.1 从报销系统扩展成通用审批平台
报销单只是审批流的一个业务载体。当我把审批流引擎独立出来之后,其实可以直接换成请假单、采购申请单、用款申请单。
做法上只需要抽象几张表:业务表单表(存各类表单的公共字段:表单类型、表单标题、状态、创建人等)+表单明细JSON(不同类型表单的字段用JSON存)+审批流配置表(按表单类型区分流程)。这样做完,用户就可以在同一个系统里发起不同类型的审批,而审批引擎完全通用。
这条扩展路径的实际价值很高:公司内部原来有三套系统分别管报销、请假、采购,如果能看到一个系统把它们统一成一站式审批工作台,那项目含金量会明显上升。
8.2 增加数据统计与预算控制
企业报销系统如果只有审批流,会显得有点单薄。加一个统计看板之后,价值提升会很明显:
- 按部门统计:本月各部门累计报销金额,柱状图对比。
- 按人员统计:谁报销最多、哪类费用超标。
- 按费用类型统计:差旅费占比、餐费异常走高预警。
- 预算联动:如果某个部门本月报销金额已经超过预算的80%,提交新报销单时前端给出提示;超过100%则不能提交(或需要更高级别审批)。
统计接口不用复杂,一个SQL中的GROUP BY就能实现:
SELECT dept_id, SUM(total_amount) AS total FROM expense_order WHERE status IN (2, 4) AND DATE_FORMAT(apply_time, '%Y-%m') = #{month} GROUP BY dept_id;重点是把数据用echarts等图画出来。小程序原生不建议直接引全量echarts,体积太大,用ec-canvas那个组件包刚好合适。图表呈现出来的效果,在答辩或演示时是很加分的,因为它让人一眼看出来“这不是一个简单CRUD”。
8.3 移动端适配与性能优化
企业用户使用的手机终端差异比较大,从iPhone 12到几百块的安卓千元机都存在。为了保证体验一致,有两个优化点值得做:
- 图片压缩:发票图片通常像素很高,但上传5MB到服务器既慢又耗流量。小程序端
wx.compressImage压缩一下(质量80%、宽1500px),一张图基本能压到300KB以内,体验差距非常明显。 - 列表分页:前端滚动加载(每页20条),后端LIMIT分页或游标分页。不要一次把几千条记录全查出来,小程序setData大数据量会明显卡顿。
- 骨架屏:首次进入首页时,用一个简单骨架屏组件占位(灰色块模拟卡片),等数据返回后替换,视觉上比一直转圈好很多。实现上就是一个动态伸缩的div加上
wx.showSkeleton这类简易组件,成本很低。
9. 风险提示、避坑心得与项目学习路线
9.1 合规与个人隐私数据保护的注意事项
企业内部系统必然涉及员工个人信息和财务数据,有几个合规红线要特别注意:
- 不得采集与业务无关的个人信息。小程序端如果不需要真名,就不要强制收集身份证号等信息,避免触碰个保法边界。
- 发票图片属于敏感数据:上传后要设置访问权限,不能让人通过“猜URL”的方式浏览到别人的发票;存储目录按日期分层、文件名随机化是很好的防遍历手段。
- 系统上线前做好等保自评估:如果数据是真实的财务数据,不能随便用个免费服务器裸奔;有条件就加HTTPS、数据库备份、安全组白名单。
毕设项目如果用真实数据演示,建议把发票图片换成打码或者“示例发票”的图片,脚本数据单独建一套测试数据,避免真实数据泄露风险。
9.2 毕设避坑:这些问题最容易导致返工
基于我这些年见过很多学生项目的经验,最容易踩坑的地方集中在以下几处:
- 表结构设计阶段没人思考审批流,后面写代码时才发现流程和业务全耦合,代码写来写去不可维护。
- 不做权限校验,页面全靠按钮隐藏来“保护”,抽查接口时一抓一个准。
- 照片上传路径写死在生产目录,重新部署就丢文件,也很容易踩。
- 没有做日志,被问“谁在什么时间改了这条数据”就答不上来。
- 多级审批节点配置硬编码在代码里,改个审批级次要重新改代码发版。
- 异常体系不统一:有的接口返回JSON,有的直接500,前端统一拦截无从下手。
这些问题看起来都是细节,但它们几乎决定了这份代码在职场面试官或答辩老师心中属于“认真做过”还是“随便糊弄”。我的建议是:哪怕时间再紧,也把审批流引擎、日志、异常处理这三样先落地,其他可以慢慢补。
9.3 面向求职的学习路线建议
如果你的目标是拿这个项目去找开发岗位,在完整实现的基础上,我建议再从三个维度加深理解:
- 把项目文档写好:README里写清楚架构图、接口文档、部署步骤、核心难点。有条件的可以画一张简洁的架构图,让面试官30秒就能看懂。
- 针对一个点深入:比如审批流引擎的状态机设计,可以拿它跟Activiti/Flowable等成熟工作流引擎做个横向对比;能说清楚自己实现和Flowable这类引擎的差别、优缺点,面试深度马上不一样。
- 准备3-5个“亮点问答”:例如“你的审批流如果并发通过会怎样?”“你的Token过期了怎么刷新?”“如果报销单涉及预算控制,怎么设计?”这些问题的答案在这篇文章里其实都覆盖到了,关键是自己要能用口头表达出来。
9.4 最后的一点个人体会分享
做了不少企业内部工具之后,我个人最大的体会是:这类项目表面上是在写代码,实际上是在管理“期望和不确定性”。员工希望报销尽快到账,领导希望审批流程严格合规,财务希望账目清晰可查。写代码的人如果没有从这三类角色的角度审视系统,做出来的工具一定在某处别扭。
我记得第一次做一个审批系统的时候,忽略了一个很小的场景:报销人被驳回后,改动数据重新提交,历史审批记录被我清空了。结果财务复核时看到一张单子金额很大却没有历史痕迹,当场质疑有没有走完流程。后来我把“历史版本保留”和“驳回记录不删除”当成铁律写进开发规范里,后面所有版本都没再被这个问题困扰过。
如果你是第一次做这类管理系统,不要焦虑,先按照需求梳理、表结构、后端接口、小程序页面这个顺序稳步推进,遇到不会的模块先画出来再动手写,基本两周左右就能跑通全套流程。报销审批系统作为练手项目,难度适中、覆盖面广,非常值得认真做一遍。祝所有读者都能顺利跑通自己的版本。