基于Java+SSM+Django的网络办公系统,是我这段时间梳理过最典型的综合性项目之一。它表面上没有复杂的算法,也没有酷炫的前端交互,但从需求拆解、权限设计、审批流转、双技术栈联调,到最后的打包部署,每一个环节都有能让新手卡上半天的暗坑。这篇内容不打算罗列功能截图,而是专注拆解这套OA系统从需求到实现的关键链路:SSM主业务和Django辅助模块是怎么分工的、核心表如何设计、审批状态机怎么落地、联调部署时最容易踩哪些雷。如果你想做在线办公系统,或者在准备毕业设计答辩、系统学习Java后端开发,这篇文章应该能给你一份有效的参考。
很多同学看到“Java+SSM+Django”这个组合会觉得奇怪,甚至怀疑是不是硬凑技术栈。说实话,这种判断一半对一半错。它确实把Java和Python两套体系拉进了同一个系统,但如果把它们理解成两个独立服务、通过接口协作,这反而是企业中很常见的一种架构形态:核心业务用Java保证稳定,辅助业务用Python快速开发和交付。下面我按一个完整项目的推进顺序,把每一步的设计逻辑和实操细节都摊开讲。
1. 先搞清楚:一套网络办公系统到底要解决什么问题
1.1 办公场景里那些让人崩溃的低效环节
做OA之前,首先得明白它要治什么病。很多需求文档喜欢把办公系统写成“员工管理、通知公告、请假审批、会议室预定、文档管理”,列一堆功能模块,看起来什么都覆盖了,但恰恰漏掉了核心问题。
我跑了几个中小型公司的场景之后,感触很深。没有线上办公系统时,一个员工请假可能要走这样的流程:先找直属领导当面说,再拿纸质请假单找人事,人事再去找部门负责人签字,如果领导出差,这张单子就这么压着。报销也一样,发票贴好送到财务,财务对账的时候才发现缺少审批依据,又退回去重走一遍。会议通知靠微信群发,发出去之后谁看了、谁没看根本不知道,到了时间会议室还被其他部门占了。文档管理就更乱了,合同、方案、制度散落在各台电脑和聊天记录里,找一份半年前的报价单,可能要翻一整天的聊天记录。
这些场景汇总起来就是三个核心痛点:流程没有线上化、信息没有统一入口、权责没有留痕手段。远程办公和混合办公逐渐普及之后,这些问题会被进一步放大——人和人不在同一个物理空间,单纯靠线下沟通已经无法组织起高效的协作,必须有一个统一的“网上办公室”。
1.2 网络办公系统的核心目标:流程在线、信息不落地、权责可追溯
所以我对这套网络办公系统的定位,从来不是“把纸质表单搬到网页上”,而是把业务流程本身数字化。流程一旦在线,审批节点、处理时间、处理人都会被记录下来;信息一旦集中,公告、文档、日程都从同一个平台获取,就不再会有“这件事我不知道”的扯皮;权责一旦可追溯,每一个操作都有操作日志,出了问题时可以查到是谁在什么时间做了什么操作。
这里特别提醒一点:办公系统的“审计能力”非常重要。无论是企业内部的合规要求,还是政务类系统的电子政务场景,流程留痕和操作日志都是底线要求。所以在我设计的系统里,除了业务表本身,还专门设计了操作日志表和审批记录表,任何状态的变更都要追加一条记录。很多人做OA只看重“能不能审批”,忽略了“审完有没有痕迹”,这是设计上的大忌。
1.3 系统面向的用户角色与权限边界
办公系统里至少有三种角色要处理清楚:
- 普通员工:提交请假、报销、用章申请,查看公告和自己的日程,参与会议,维护个人信息。
- 部门主管/审批人:处理下属提交的申请,可以同意或者驳回,查看本部门的工作动态。
- 系统管理员:维护用户、角色、菜单、部门结构,分配权限,查看系统运行日志和数据统计。
这个划分本身不复杂,但紧接着就引出一个老生常谈又容易翻车的问题:数据权限。角色权限通常指的是“能不能点某个菜单、能不能做某个操作”,但数据权限是“能看哪些数据”。比如财务主管可以查看全公司工资数据,部门主管只能看本部门人员,普通员工只能看自己的申请记录。在同一张表、同一个接口的情况下,只判断“是否登录、是否管理员”远远不够,必须把数据范围计算进去。这部分我后面会在表结构和拦截器设计里详细展开。
2. 技术架构拆解:SSM和Django为什么能共存于同一个项目
2.1 SSM三件套:Spring、SpringMVC、MyBatis的分工
既然标题里写着Java+SSM,那就先把SSM的骨架说清楚。SSM是Spring、SpringMVC、MyBatis三件套的简称,在很多Java后端项目里仍然是经典组合。
- Spring负责IoC容器和AOP。所有业务对象通过依赖注入管理,事务、日志、权限校验这些横切逻辑通过AOP统一处理。没有Spring,服务层会变成一堆互相new对象的散装代码。
- SpringMVC负责Web层。它处理前端发过来的HTTP请求,完成参数绑定、校验、路由分发,控制层里一个
@RequestMapping就能把URL映射到具体方法。 - MyBatis负责数据持久化。它把SQL写在Mapper接口或XML里,灵活控制SQL的拼接和结果映射。相比JPA的“全自动”,MyBatis更接近SQL本身,适合复杂查询和报表统计。
一句话总结:Spring管对象的组装和事务,SpringMVC管请求的分发,MyBatis管数据库的访问。三者各司其职,这也是SSM面试里最基础、最常被追问的一组概念。
2.2 Django在这个系统里的定位:辅助业务与数据服务
那Django又是干嘛的?我这里采用的方案是:SSM作为系统的核心,负责人事、权限、审批、公告等主业务;Django作为辅助服务,负责几个典型场景:
- 数据看板和统计报表:基于业务数据做汇总分析,比如出勤率、审批时效、部门工作量。
- 定时数据同步任务:从外部数据源抓取信息,或者定时生成报表并推送通知,Django的Celery和ORM配合起来非常快。
- 内部管理后台:Django自带Admin,改造之后可以快速实现一些低频率的后台配置和数据维护功能。
为什么把辅助模块交给Django而不是全部写在SSM里?核心原因是开发效率。Java要做一套统计报表,Controller、Service、Mapper、XML、前端页面全部要写,而Django基于ORM和模板,默认的Admin后台能省掉一大片重复劳动。把辅助业务拆出去,SSM主工程可以保持相对精简,边界也更清晰。
2.3 异构技术栈之间的通信方案
Java和Python之间怎么通信?这里不引入复杂的服务框架,最稳妥也最容易调试的方案是REST API。SSM作为服务提供方,把用户、审批、部门等核心数据以JSON接口暴露出去;Django作为服务调用方,通过requests或httpx请求这些接口获取数据,再加工成报表。
当然也可以选择共享同一个MySQL数据库,SSM写业务表,Django只读或者只操作自己的辅助表。但我个人不推荐共享库,因为一旦两个技术栈对同一张表做写操作,后期表结构变更、字段类型兼容都会变成灾难来源。我自己采用的是:各自维护独立数据库,业务数据通过SSM提供的接口同步到Django的分析库,辅助模块产出结果再通过接口回写或由SSM去拉取。这样两个服务即便日后要拆分、替换,也不会互相拖累。
2.4 开发环境与依赖版本清单
这是一套可复现的版本组合,我在实际搭建时踩过版本不兼容的坑,所以直接给一份确定可用的清单:
| 组件 | 版本/说明 |
|---|---|
| JDK | 1.8或11,推荐1.8,稳定且兼容旧项目 |
| Maven | 3.6以上 |
| Spring | 5.3.x |
| SpringMVC | 5.3.x |
| MyBatis | 3.5.x |
| MySQL | 5.7或8.0,字符集使用utf8mb4 |
| Tomcat | 9.0 |
| Python | 3.9或3.10 |
| Django | 3.2 LTS或4.2 LTS |
| Redis | 可选,做会话缓存和接口限流 |
定时任务在Django端建议用Celery 5.x,不过如果只是简单报表,直接写Django自带的cron script配合系统定时任务也行,能省掉一堆基础设施。
3. 核心功能与数据模型设计
3.1 功能模块全景
把常用办公系统拆分出来,需要有如下这些模块:组织架构与用户管理、角色与权限管理、审批工作流(请假、报销、用章、出差)、日程与会议管理、公告通知、文档资料库、考勤统计、操作日志与系统监控。
下面用一张表说清楚每个模块的输入输出和关键点:
| 功能模块 | 主要输入 | 关键输出 | 设计注意点 |
|---|---|---|---|
| 用户与部门管理 | 部门信息、员工信息 | 部门树、员工账号 | 部门支持树形层级,员工要有启用/停用状态 |
| 角色与权限管理 | 角色定义、菜单权限 | 角色-菜单映射 | 动态菜单,用户登录后生成可访问菜单树 |
| 审批工作流 | 申请单、审批意见 | 已办任务、待办任务 | 状态机管理,支持同意、驳回、撤销 |
| 会议与日程 | 会议主题、时间、参与人 | 日历视图、会议提醒 | 会议室与时间段冲突检测 |
| 公告通知 | 公告正文、发布范围 | 已读/未读统计 | 记录每个用户的阅读状态,避免“不知道”的扯皮 |
| 文档资料库 | 文件分类、上传附件 | 在线预览、下载 | 大文件上传限制、文件存储路径统一 |
| 考勤统计 | 打卡记录、外勤记录 | 出勤表、异常统计 | 与请假、出差数据打通,避免重复扣减 |
| 操作日志 | 用户操作行为 | 日志列表、审计报表 | 拦截器或AOP统一记录,不能依赖业务代码自觉 |
3.2 RBAC权限模型怎么落地
权限设计我选用了经典RBAC模型:User、Role、Menu三个核心实体,外加UserRole和RoleMenu两张关联表。普通场景足够,而且答辩时也很好讲。五张核心表的关系是:用户属于角色,角色绑定菜单,菜单里的按钮对应操作权限。
菜单权限只是第一层,第二层是操作权限。比如“用户管理”这个菜单下面,管理员能增删改查,普通人力专员可能只能看列表导出Excel。我的做法是在菜单表里增加权限标识字段,比如perm_code,类似system:user:add,然后通过一个自定义@RequiresPermission注解或拦截器统一校验。
真正麻烦的是第三层,数据权限。同一个“查看员工”接口,管理层要看全员,部门主管只能看本部门。我的方案是在查询SQL里根据当前用户动态拼接数据范围条件:管理员不加过滤条件,主管级别加where department_id = 当前部门,普通员工再细化为where user_id = 当前用户。这种设计比每次在Service层硬编码条件要规范得多。
3.3 审批流状态机:从待审批到归档
审批模块是整个系统的核心。我以请假单为例:一张单子从提交到结束,至少要经历“待审批 -> 同意/驳回”两个大状态,再算上“撤销”和“已归档”。更完整的版本还会区分一级审批和二级审批,状态机就要扩展为:待一级审批、一级通过待二级审批、二级通过、已驳回、已撤销、已归档。
我在代码里用int类型作为状态字段,同时配一个常量类来定义可读的状态名称。每次状态变更都往审批记录表写一条记录,包含审批人、审批动作、审批意见和时间。这里有一个非常关键的细节:所有状态变更必须加事务控制,否则一旦审批记录写完而主单状态没更新成功,就会出现“记录说同意了,单子还停在待审批”的脏数据。
3.4 核心表结构设计示例
我自己建表时会先画一个最简版,以用户表、请假单、审批记录为例,SQL大概长这样:
CREATE TABLE t_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录账号', password VARCHAR(100) NOT NULL COMMENT 'SHA256加密密码', real_name VARCHAR(50) NOT NULL, department_id BIGINT COMMENT '所属部门', enabled TINYINT DEFAULT 1 COMMENT '是否启用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表'; CREATE TABLE t_leave ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL COMMENT '申请人', leave_type TINYINT NOT NULL COMMENT '1事假2病假3年假', start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, reason VARCHAR(500), status TINYINT DEFAULT 0 COMMENT '0待一级审批1待二级审批2已同意3已驳回4已撤销5已归档', version INT DEFAULT 0 COMMENT '乐观锁版本号', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_user_id(user_id), KEY idx_status(status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='请假申请表'; CREATE TABLE t_approval_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, biz_type TINYINT NOT NULL COMMENT '业务类型:1请假', biz_id BIGINT NOT NULL COMMENT '业务单ID', approver_id BIGINT NOT NULL COMMENT '审批人', action TINYINT NOT NULL COMMENT '1同意2驳回3转交4撤销', comment VARCHAR(500) COMMENT '审批意见', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_biz(biz_type, biz_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批记录表';设计上的几个通用原则我想强调一下:逻辑删除字段is_deleted要有,但核心流程表里我保留物理删除,原因是审批单属于审计数据,不应该允许用户物理删除;每个表都保留create_time和update_time,排查问题和做统计报表时没有这两个字段会非常痛苦;deleted之外,凡是经常用来过滤的字段都建索引,比如状态字段和用户ID字段。
4. 从零到跑通:核心环节的实操实现
4.1 工程结构与初始化步骤
工程结构上,我建议拆成两个独立项目,不要硬塞在一个Maven工程里。SSM主项目是标准的Maven Web项目,Django辅助服务是独立的Python项目,目录长这样:
oa-system/ ├── oa-backend/ # SSM主工程 │ ├── pom.xml │ └── src/main/ │ ├── java/com/oa/ │ │ ├── controller/ │ │ ├── service/ │ │ ├── mapper/ │ │ ├── entity/ │ │ ├── common/ │ │ └── config/ │ └── resources/ │ ├── mappers/ │ ├── spring/ │ └── jdbc.properties ├── oa-report/ # Django辅助服务 │ ├── manage.py │ ├── requirements.txt │ └── reportapp/ │ ├── models.py │ ├── views.py │ └── urls.py └── sql/ └── init.sql初始化时先把sql脚本执行到MySQL,再启动SSM主工程,最后启动Django服务。顺序不能反,Django如果需要调用SSM接口来初始化基础数据,而SSM服务没起来,就会直接超时报错。
4.2 SSM端关键代码:统一返回结果与控制层
前后端交互统一用一个Result<T>包装类,这样接口返回结构稳定,前端解析起来也省心:
public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> ok() { ... } public static <T> Result<T> error(String message) { ... } }Controller层不要写业务逻辑,只做参数接收、参数校验和结果转换。真正复杂的逻辑放到Service层。下面是一个简单的请假单提交接口:
@RestController @RequestMapping("/api/leave") public class LeaveController { private final LeaveService leaveService; @PostMapping("/submit") public Result<Long> submit(@RequestBody LeaveForm form) { // 参数校验按业务规则处理,比如结束时间必须晚于开始时间 Long leaveId = leaveService.submit(form); return Result.ok(leaveId); } @PostMapping("/approve") public Result<Void> approve(@RequestBody ApproveForm form) { leaveService.approve(form.getLeaveId(), form.getAction(), form.getComment()); return Result.ok(); } }4.3 审批状态流转的实现细节
审批状态流转是必须放在Service层并加事务的。状态编码我建议定义成常量或枚举,不要到处写魔法数:
public class LeaveStatus { public static final int PENDING_FIRST = 0; public static final int PENDING_SECOND = 1; public static final int APPROVED = 2; public static final int REJECTED = 3; public static final int CANCELED = 4; public static final int ARCHIVED = 5; }审批方法的伪代码逻辑:
@Transactional(rollbackFor = Exception.class) public void approve(Long leaveId, Long approverId, int action, String comment) { Leave leave = leaveMapper.selectById(leaveId); if (leave == null) { throw new BusinessException("申请单不存在"); } // 核心校验:当前状态必须是待审批,防止重复审批 if (leave.getStatus() != LeaveStatus.PENDING_FIRST && leave.getStatus() != LeaveStatus.PENDING_SECOND) { throw new BusinessException("当前状态不可审批"); } // 乐观锁更新,避免并发审批同时通过 int rows = leaveMapper.compareAndSetStatus( leaveId, leave.getStatus(), action == 1 ? nextStatus(leave.getStatus()) : LeaveStatus.REJECTED, leave.getVersion()); if (rows == 0) { throw new BusinessException("单据已被其他审批人处理,请刷新后再试"); } // 插入审批记录 ApprovalRecord record = new ApprovalRecord(...); approvalRecordMapper.insert(record); }这里比较关键的是乐观锁写法。有两个审批人同时打开同一张单子,A点同意,B也点同意,如果只简单update status = 2 where id = ...,两条同时执行也都能成功,但业务上就错了。所以更新条件必须带上当前状态和版本号,用compareAndSetStatus这样的原子更新来保证只有一个请求能真正修改成功。
MyBatis的XML里对应的更新语句长这样:
<update id="compareAndSetStatus"> UPDATE t_leave SET status = #{newStatus}, version = version + 1 WHERE id = #{id} AND status = #{oldStatus} AND version = #{version} </update>4.4 Django端模型、查询与删除操作的注意点
Django辅助模块里,我主要做统计和报表,所以模型相对简单。比如要做一个考勤统计结果表:
from django.db import models class AttendanceReport(models.Model): dept_id = models.BigIntegerField() work_date = models.DateField() total_count = models.IntegerField(default=0) leave_count = models.IntegerField(default=0) late_count = models.IntegerField(default=0) status = models.CharField(max_length=16, default='pending') class Meta: db_table = 't_attendance_report'Django的查询一直在被题目中的“django执行查询-删除对象”热点多次提到,这里重点讲两个容易踩的细节。
第一个是查询集取数据时,REST接口返回给Java端的一定要做序列化处理。不要直接返回QuerySet对象,必须先转成列表或字典。比如:
from django.http import JsonResponse from django.db.models import Count def dept_summary(request): dept_id = request.GET.get('dept_id') rows = ( AttendanceReport.objects .filter(dept_id=dept_id, status='done') .values('work_date') .annotate(total=Count('id')) .order_by('work_date') ) data = list(rows) return JsonResponse({'code': 0, 'data': data})第二个是删除对象时的级联问题。Django的ORM里,如果外键设置了on_delete=models.CASCADE,删除主表对象时会自动把关联子表记录也删掉。这看起来方便,但办公系统里很多核心数据不能这么硬删,否则审批记录、日志全没了。所以我建议临时表可以用delete(),核心业务表一般做软删除:在模型里加一个is_deleted字段,默认查询时用管理器统一过滤掉已删除记录。批量删除还有一个坑点:QuerySet.delete()不会触发模型自带的delete()方法和信号,如果依赖信号做日志或通知,就会发现数据没了但日志没写。这一点在面试中也经常被问到。
4.5 打包部署与联调步骤
SSM端打包成WAR包丢到Tomcat的webapps目录,Django端用python manage.py runserver做测试,生产环境则用Gunicorn或uWSGI。整体用Nginx做反向代理,把不同路径转发到不同服务:
server { listen 80; server_name oa.example.com; # SSM主应用 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # Django统计和报表 location /report/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }联调时最容易出问题的是网络超时。SSM接口如果处理慢,Django默认请求超时时间又很短,会出现两者各自运行正常,一调用就报错的诡异问题。解决方案是给Django端的HTTP请求设置明确超时长,比如连接时间3秒、读取时间10秒,并且加对应的错误重试逻辑。
5. 真实开发中踩过的坑和排查方法
5.1 SSM高频运行问题速查表
| 现象 | 常见原因 | 处理建议 |
|---|---|---|
| 启动报“Bean named xxx is currently in creation” | 循环依赖 | 在Setter上添加@Lazy注解,或重构Service依赖方向 |
| 查询实体属性全是null | 数据库字段下划线和Java驼峰没映射 | 开启mybatis.configuration.map-underscore-to-camel-case=true |
| 中文乱码 | 请求和响应编码不一致 | 在web.xml里配置CharacterEncodingFilter,强制UTF-8 |
#{}查不到数据 | 误用了${}拼接导致SQL错误 | 默认用#{}预编译,${}仅用于固定动态表名或排序字段 |
| 接口报404 | Controller路径写错或没加@RequestMapping | 检查HandlerMapping日志,确认请求地址与Controller路径完全一致 |
| 事务失效 | 同一个类内部方法互调,且没有走代理 | 拆分到不同Service,或通过AopContext.currentProxy()调用 |
这里我把“SSM常用注解”这个问题一并说透。控制层常用@Controller、@RestController、@RequestMapping、@ResponseBody、@RequestBody;Service层常用@Service和@Transactional;依赖注入最常用@Autowired或@Resource;MyBatis层面有@Mapper、@Select、@Param。如果你在项目里同时使用注解和XML,记住一个原则:简单SQL用注解,复杂动态SQL用XML,这样程序可读性和可维护性最好。
5.2 Django查询与删除相关的细节问题
Django的删除操作,不同写法效果差异很大。删除单个对象obj.delete()会触发级联和信号;删除QuerySetfilter().delete()是批量删除,不走模型的delete()方法,也不触发每个对象的信号。在办公系统里做清理任务时,我建议分两步走:先把要删的ID查询出来,判断是否有需要归档的关联数据,然后再执行批量删除,并在日志里记录删除数量和类型。下面是一个安全删除示例:
to_delete = TempFile.objects.filter(create_time__lt='2025-01-01', is_archived=True) count_info = to_delete.delete() logger.info(f'清理过期临时文件:{count_info}')另外一个高频坑是Django的时区问题。如果开启了USE_TZ=True,写入数据库的时间是UTC时间,而SSM端存的是本地时间,两边一对比数据会差8小时。处理办法是统一约定:所有接口传输的时间都用字符串并使用ISO格式,数据库存储用北京时间,关闭USE_TZ或配置TIME_ZONE='Asia/Shanghai'并明确一个主时区。
5.3 跨技术栈联调的隐患
Java和Django之间传参,最大的坑是时间格式和数值类型。Java的LocalDateTime默认序列化格式是“2025-01-01T10:00:00”,Django的JSON解析器和前端通常更习惯“2025-01-01 10:00:00”,所以要么在Java端配置Jackson格式化统一为yyyy-MM-dd HH:mm:ss,要么在Django端做字符串兼容转换,最好是在联调前就约定好格式。
然后是跨域问题。如果SSM主应用和Django辅助服务分属不同域名或端口,浏览器直接请求Django接口会触发跨域拦截。Django端需要安装django-cors-headers并配置允许的域名。但如果是后端服务之间互相调用,跨域问题不存在,要关注的反而是防火墙、端口和代理转发是否正确。
5.4 业务逻辑上的并发与数据一致性
办公系统业务层面必须面对并发。请假、报销单据并发审批只是问题之一,会议室预定也会出现两个部门同时订到同一间会议室的情况。处理原则建议统一采用乐观锁或唯一索引来控制。
会议室预定我在表设计时给会议时间加了一个唯一约束,具体做法是会议室ID和会议开始时间组合成唯一索引,插入新会议时如果时间重叠就直接报冲突,这样比查完再插更可靠。审批流并发则用版本号乐观锁,前面审批代码里的version字段就是干这个的。
还有一类问题是数据一致性。Django从SSM拉取数据生成报表时,如果SSM端数据正在修改,拉下来的报表可能是不一致的快照。这种情况不一定要上分布式事务,简单点可以让Django在拉取时对SSM接口传版本号或时间段参数,只统计已结束的业务单,减少中间态数据对报表的影响。
6. 文档编写、演示讲解与答辩准备
6.1 源码+文档的项目该怎么整理
标题里的“源码+LW+调试文档+讲解等”是这类项目最常见的交付形态,其中LW通常指配套论文或设计文档。很多同学源码跑通了,论文和文档却不知道怎么下笔,这里提供一个稳妥的大纲:需求分析、系统总体设计(架构图+功能模块图)、数据库设计(ER图+表结构说明)、模块详细设计(核心流程描述+主要代码片段说明)、系统测试(功能测试用例+结果分析)、部署说明。
调试文档我习惯按环境搭建、启动步骤、常见异常三部分来写。环境搭建要写清楚JDK、MySQL、Tomcat、Python版本,避免读者随便装一个新版本就跑不起来。启动步骤要从导入SQL开始一直到浏览器访问地址,每步都给出预期结果。常见异常则把上面提到的坑整理进去,读者照着排查就能快速解决。
6.2 演示讲解的时间线设计
演示视频或现场答辩时,最容易犯的错误是把所有功能都点一遍,听起来像流水账。我的建议是设计一条有剧情的主线:管理员登录创建部门和三个员工账号 => 配置角色权限 => 员工提交请假审批 => 部门主管审批通过 => 普通用户收到审批结果 => Django报表页面展示考勤统计。这样一套流程走下来,既展示了核心业务闭环,又体现出权限模型和双技术栈协作,全场不会乱。
演示前准备几条典型数据非常关键。不要现场才新增账号,提前把用户、部门、历史审批数据填充好,演示的时候才能从容展示列表分页、状态筛选、统计图表这些亮点。
6.3 面试和答辩阶段的高频问题
结合java面试题的热点方向,我整理了这几个问题,建议大家提前准备:
- SSM中三者的作用分别是什么?请结合项目中的具体业务说明。这个问题要落到实际,不要背定义。
- 权限模型怎么设计的?RBAC之外你如何做数据权限控制?重点讲动态SQL拼接数据范围。
- 为什么用Django?它和SSM之间如何通信?答案围绕服务拆分和API协作展开。
- 审批并发如何保证数据一致性?讲到乐观锁版本控制就足够,再补充一个悲观锁场景对比。
- MyBatis中
#{}和${}的区别?这个问题几乎是SSM岗位必问,务必说清楚预编译和SQL拼接的根本区别。
6.4 从毕设/演示系统走向生产环境的差距
演示系统能跑到生产环境,中间还有一段距离。如果要继续把这套系统往真实项目推,我建议从这几个方向改造。
首先引入Spring Boot而不是手动配置SSM的XML,Spring Boot内置Tomcat、自动配置依赖,开发效率会明显提升。其次把Django和SSM的认证打通,用统一令牌解决跨服务的登录态问题,而不是在Django端另搞一套用户名密码。然后是部署方式迁移到Docker Compose,MySQL、Redis、Tomcat、Django各一个容器,补丁发布和机器迁移都更方便。最后是补齐监控和日志采集,至少做到错误日志集中存储和关键业务指标的监控报警,不然上线之后出了问题只能靠用户跟你说。
这里我可以多聊一句心得:办公系统表面看是“增删改查”,但真正有价值的部分永远在业务抽象和架构边界上。审批状态机、RBAC数据权限、跨服务接口约定,这些才是项目里拿得出手的亮点。
这套项目做下来的整体感受
我真正花时间的部分其实不是SSM的CRUD,也不是Django的报表接口,而是想清楚两个技术栈之间的边界在哪里。同一套系统里同时出现Java和Python,很容易把工程写成一团乱麻,但只要你时刻记得“核心服务负责业务,辅助服务负责效率”,所有决策都会变得清晰。对正在做OA、电子政务、或者任何协同办公类项目的朋友,我再多一句实在的建议:开工之前先画两张图,一张是“角色-数据矩阵”,一张是“审批状态流转图”。这两张图想清楚了,项目的基本盘就已经稳了一半,剩下的实现不过是把这些设计翻译成代码而已。