资讯动态

双后端架构实践:村级办公系统中SSM与Django的整合之道

发布时间:2026/10/14 11:44:21 来源:尧图企业网站定制
接手这个项目的时候我第一眼看到技术栈写着“JavaSSMDjango”心里其实打了个问号——一个村级办公管理系统为什么会同时出现两套后端框架等项目资料翻完又去现场跑了一趟这个疑问才慢慢变成“原来如此”。整套系统从前端到后端再到部署交付整个流程走下来我最大的感受是这类系统的难点从来不在技术本身而在于你怎么把“村级办公”里那些散落在线下、口头、纸质台账里的流程翻译成清晰的数据模型和稳定的功能模块。这篇文章把整个项目的梳理过程写出来包括需求拆解、两套框架的职责划分、表结构设计、双后端联调、调试文档怎么用以及最后部署到真实办公环境时容易踩的坑。给正在做同类管理系统、或者准备接手类似“小体量、多功能、多框架并存”项目的朋友一些参考。1. 需求盘点村级办公系统真正要管的几件事1.1 现场调研里看到的真实痛点去现场之前我以为村级办公管理系统无非就是“公告板台账”的线上化但跑了一圈之后发现问题比想象中具体得多。村级日常办公典型的状态是文件收发靠纸质登记本会议记录写在笔记本上村民档案散在多个Excel表里资产台账更是只有一张手写表格年底统计时几个人要翻几天柜子。平时最常用的通讯工具是微信群但群消息三天后就沉底了真要追溯“某条通知到底有没有通知到位”没有任何依据。这些痛点汇总起来其实指向同一个本质村级办公不缺“做事的人”缺的是把“做过的事”沉淀下来的载体。所以系统的核心价值不只是“录入数据”而是把日常动作变成可查询、可统计、可追溯的记录。这个定位决定了后续功能设计的方向——绝不能做成一个花架子展示平台而要做成每天都要打开使用的工具。1.2 功能边界怎么划和负责人聊完又看了他们正在用的手工台账我把需求收敛成五个核心模块公文管理收文登记、发文拟稿、传阅记录、归档查询村民档案户册信息、成员信息、补贴记录、走访记录会议管理会议计划、参会人登记、会议纪要、决议事项跟踪资产管理资产登记、领用归还、报修报废、盘点记录通知公告发布、已读回执、按人群定向推送每个模块背后都对应着真实的办公场景。比如公文管理村里每天要处理的上级来文不少收进来之后要给相关人传阅传阅结果要留痕再比如资产办公电脑、打印机、会议室设备都要记录归属谁领走了、什么时候维修过这些信息以前都靠脑子记人一换岗就全断了。1.3 不该做和暂时不做的功能需求调研阶段最容易犯的错就是“什么都想要”。我在这里主动砍掉了几块内容一是复杂的工作流引擎村级公文流转层级简单用状态字段就能覆盖上一套审批流反而增加使用成本二是移动端App村里干部普遍靠微信交流做个H5适配比单独开发App实际得多三是和上级系统的数据对接当时接口规范不明朗做成手工导入导出更稳妥。这个“砍需求”的过程后来被证明是整个项目里最值钱的决策。系统上线后使用频率最高的就是公文登记、通知发布、村民档案三个模块正好是当初定义得最清晰的三块。做管理系统尤其是村级这种用户群体固定、操作场景固定的小体量系统功能越多越容易变成负担。2. 技术选型复盘SSM和Django为什么能共存2.1 双后端架构的真实成因标题里同时出现SSM和Django很多人的第一反应是“这不是重复造轮子吗”。我最初也这么想但深入项目之后发现两套后端各自承担着不同的职责属于典型的“按需混用”。项目的核心业务层用的是Java SSMSpring、SpringMVC、MyBatis负责公文、档案、会议、资产这些强事务、强关系的数据操作。这个选择很务实SSM在国内管理系统开发里非常成熟事务管理、权限框架、运维排障的资料都多接手的团队不容易被技术栈卡住。同时项目里还有一部分偏“计算密集”和“定时任务”的功能比如月底统计报表、补贴发放清单生成、周期性的数据汇总导出这部分用了Django来做。为什么用Django承担这部分原因很直接Django自带Admin后台、ORM、模板引擎和成熟的定时任务生态做报表生成、数据导出这一类“数据聚集型”功能开发效率比在SSM里堆工具类要高很多。而且Python生态里处理Excel、PDF的库非常顺手办公室需要的各种导出格式能快速实现。两套后端通过HTTP接口通信各自做好各自的事互不干扰。2.2 目录结构与工程组织方式双后端项目最怕的就是“两个团队各写各的代码像两座孤岛”。这个项目在工程组织上做了一个约定代码仓库分成三个顶层目录。project-root/ ├── backend-java/ # SSM主工程核心业务接口 │ ├── src/main/java │ ├── src/main/resources │ └── pom.xml ├── backend-python/ # Django子服务报表/导出/定时任务 │ ├── office_service/ │ ├── apps/ │ └── manage.py └── docs/ # 调试文档、SQL脚本、部署脚本这个结构的好处是边界清晰Java工程不需要import任何Python代码Python工程也不直接访问数据库表一切数据交互都走接口。部署的时候可以拆开部署也可以一起部署在同一台机器上灵活性很高。从维护角度看这种“语义化目录分离”也比“一个仓库里混着各种语言的文件”要好得多。后来配合的同事打开仓库就知道去哪找代码不用先猜半天。2.3 为什么不用Spring Boot统一既然SSM用的Java为什么不干脆换成Spring Boot把Django的活也用Java干完这个问题我在技术方案评审时被问过。原因有两个第一团队的技术构成决定了不可能“用一套语言解决所有问题”。项目组里有人擅长Java有人日常用Python写数据处理脚本强行统一意味着部分人的生产力会下降学习成本会摊到项目工期上。第二从时效上看报表、导出这些功能用Django写几天就能出成果用Java从建工具类到调POI周期至少翻倍。技术选型不能只看“哪套技术更先进”更要看“当前这批人、这个工期、这些需求哪种组合产出最稳定”。所以最后的结论是不强求统一让合适的工具干合适的活。这个原则也延续到了数据库和中间件的选型上——MySQL存业务数据Redis做缓存和定时任务锁没有引入任何复杂组件。3. 数据建模村里的事务怎么落成表3.1 从“户册”到“村民档案”的设计思路村民档案是整个系统的地基这块没做好后面所有统计都会歪。我设计时的核心模型是“户—人—事件”三层结构。户册表登记的是“家庭”这个单位字段包括户编号、户主姓名、联系电话、户籍地址、家庭人数、是否低保户、是否建档立卡户等。成员表挂在户册之下记录每个家庭成员的姓名、性别、出生日期、身份证号、与户主关系、政治面貌、学历、务工状态等信息。事件表则是动态的记录补贴发放、走访记录、大病救助、耕地补贴申报这类发生在“人”身上的事情。CREATE TABLE household ( id BIGINT PRIMARY KEY AUTO_INCREMENT, household_code VARCHAR(32) NOT NULL COMMENT 户编号, head_name VARCHAR(64) NOT NULL, phone VARCHAR(20), address VARCHAR(255), member_count INT DEFAULT 0, is_low_income TINYINT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE household_member ( id BIGINT PRIMARY KEY AUTO_INCREMENT, household_id BIGINT NOT NULL, member_name VARCHAR(64) NOT NULL, id_card VARCHAR(18), gender TINYINT, birth_date DATE, relation VARCHAR(16), education VARCHAR(32), work_status VARCHAR(32) ); CREATE TABLE member_event ( id BIGINT PRIMARY KEY AUTO_INCREMENT, member_id BIGINT NOT NULL, event_type VARCHAR(32) NOT NULL, event_date DATE, event_detail VARCHAR(500), operator_id BIGINT );这种设计的好处是查询“某户有几口人”一个count就能出来统计“全村有多少低保户”一条SQL就能搞定追溯“这个人的补贴领取记录”按member_id查事件表就行。当时也考虑过直接做一张宽表把所有信息拍平但考虑到字段经常要加比如临时新增一个“是否参加新农合”的字段宽表的扩展成本太高最后还是用了这个经典的三层结构。3.2 公文、会议、资产三张业务表怎么定状态公文书、会议纪要、资产台账这三块是办公系统里最常用的业务数据它们的表结构设计有共同点每张表都要有“状态字段”和“操作记录”因为线下办公最缺的就是“事情进行到哪一步了”的可视化。以公文表为例我设计了status字段取值有待收登记、已登记待传阅、传阅中、已归档、已退回。每次状态变化都写一条operation_log记录谁在什么时间做了什么操作。会议表类似一个会议从“计划中”到“已召开”再到“决议事项跟踪中”状态流转要清晰。资产表则需要加一个current_holder当前保管人字段因为资产流转的核心问题就是“这东西现在在谁手里”。CREATE TABLE official_doc ( id BIGINT PRIMARY KEY AUTO_INCREMENT, doc_no VARCHAR(64) NOT NULL, title VARCHAR(200) NOT NULL, source_unit VARCHAR(200), receive_date DATE, doc_type VARCHAR(16), urgency TINYINT DEFAULT 0, status VARCHAR(16) DEFAULT pending, file_path VARCHAR(255), created_by BIGINT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE doc_operation_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, doc_id BIGINT NOT NULL, operator_id BIGINT NOT NULL, action VARCHAR(32) NOT NULL, remark VARCHAR(500), operate_at DATETIME DEFAULT CURRENT_TIMESTAMP );这个“一主表一操作日志表”的模式在整个系统里被大量复用。它带来的最直接好处是领导问“上周那个文件到底传给谁看了”不用靠回忆查一张表就出来了。系统上线后这个设计得到的反馈是最好的因为没有哪个村干部愿意天天在系统里编“工作总结”。3.3 软删除与归档策略村级办公系统的数据有一个特点不能真删。哪怕是录错了的公文也得留痕迹因为年底可能有审计核查。所以所有的业务表都保留了is_deleted字段默认0删除操作一律改成update为1列表查询默认过滤掉。管理员可以“彻底清除”回收站里的数据但这个操作会被记录到系统日志里。归档策略上按年份做逻辑归档。每年年初把上一年的公文、会议、资产盘点记录批量打上archive_year标记查询时默认只查当年数据想看历史数据时单独切年份。这个设计避免了一个很实际的性能问题数据量再小五六年后查列表也会明显变慢加一个归档年份字段做索引查询效率会好很多。4. 双后端联调SSM和Django之间的接口约定4.1 统一返回结构与错误码两个后端各自有自己的接口风格如果不做约束对接时一定会乱套。项目启动时我就定了一个统一标准所有接口返回结构固定为JSON包含code、message、data三个字段。Java端写了一个统一响应类Python端用一个JsonResponse包装函数。{ code: 0, message: success, data: { list: [], total: 100 } }错误码也提前约定好范围0表示成功1000-1999是参数错误2000-2999是权限错误3000-3999是业务错误5000-5999是系统错误。为什么要把错误码设计成范围制因为两边的开发是平行的如果错误码是零散的Java端写了10001Python端也写了10001联调时就得逐个人工核对范围制一眼就能看出错的等级排查问题快很多。4.2 接口鉴权token从哪来怎么验因为两套后端都在同一个内网一开始有人提议“接口不鉴权算了反正外面访问不到”。这个提议被否了。村级系统的用户权限虽然不像企业级那么复杂但至少有“普通工作人员”和“管理员”的区分而且有些报表数据涉及村民隐私不能裸奔。设计方案是登录认证统一走Java主工程的SSM端。用户输入账号密码登录成功后Java端签发一个token用JWT生成这个token本身带userId和角色信息同时存一份在Redis里设置过期时间。Django端的所有业务接口在接收到请求时先从Header里取Authorization字段然后调用Java端暴露的一个校验接口确认token有效拿到了用户信息再继续处理业务。# Django端的简单鉴权装饰器示例 import requests from functools import wraps from django.http import JsonResponse AUTH_CHECK_URL http://backend-java:8080/api/auth/check def check_token(view_func): wraps(view_func) def wrapper(request, *args, **kwargs): token request.headers.get(Authorization, ) if not token.startswith(Bearer ): return JsonResponse({code: 2001, message: missing token}, status401) resp requests.post(AUTH_CHECK_URL, json{token: token[7:]}, timeout3) if resp.status_code ! 200 or resp.json().get(code) ! 0: return JsonResponse({code: 2001, message: invalid token}, status401) request.user_info resp.json()[data] return view_func(request, *args, **kwargs) return wrapper这个方案看起来多了一次HTTP调用但实际压力很小。内网延迟基本在毫秒级而且报表类接口调用本身就不频繁。对于这个体量的系统完全够用也省去了在Django里单独维护一套用户表的麻烦。4.3 跨域和端口规划开发和测试阶段前端页面跑在Vite的Dev Server上端口是5173Java后端是8080Django是8000前端的接口请求要同时打到两个不同的源上所以CORS必须处理。Java端写了一个CORS过滤器允许本地开发源的请求Django端用django-cors-headers这个中间件配置了白名单。部署阶段Nginx统一监听80端口把/api/java路径反向代理到Tomcat的8080把/api/python路径反向代理到Gunicorn监听的8000前端页面和静态文件也由Nginx托管这样浏览器端永远只接触同一个源完全绕开了跨域问题。server { listen 80; server_name office.example.local; location /api/java/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /api/python/ { proxy_pass http://127.0.0.1:8000/; proxy_set_header Host $host; } location / { root /data/office-front/dist; try_files $uri $uri/ /index.html; } }这个端口规划在调试文档里特意用一页写了清楚因为后来接手部署的同事照着文档一步步操作基本上没有遇到“怎么端口不通”的疑问。5. 调试文档的价值从“跑不起来”到“半小时部署”5.1 项目交付时调试文档包含什么标题里提到了调试文档这个文档在交付时和源码、演示视频是同等重要的。很多开发者觉得文档是“写给外人看的”实际经历后我强烈建议把它当成交付的核心部分来写。这个项目的调试文档大致包含五块内容环境清单JDK版本、Maven版本、Python版本、MySQL版本、Redis版本每项精确到小版本号避免版本不一致导致的隐性兼容问题初始化步骤建库SQL、初始数据SQL、配置文件修改点数据库连接、Redis连接、文件上传路径启动顺序先启MySQL和Redis再启Java工程最后启Django工程。顺序错了Java启动时连不上数据库会直接报错接口验证用例登录、获取token、调用各模块核心接口的curl命令示例以及预期的返回结果方便验证服务是否真的通了常见启动报错对照表列出项目里出现过的典型报错、原因、解决办法调试文档不是写论文而是要保证“一个之前完全没碰过这个项目的人照着文档能在半天内跑起来前后端”。这个标准虽然朴素但对小体量项目来说非常关键因为这类项目大概率不是原作者一直维护后面总会有接手的人。5.2 一个典型的启动报错排查过程我在调试Django报表服务时踩过一个印象深刻的坑Django服务单独启动一切正常但只要通过Java后端调用它的导出接口就会被Nginx返回504超时。排查链路是这样的先看Django日志发现请求其实已经进来了但是要执行的报表SQL非常慢。再看MySQL慢查询日志定位到一条统计全村补贴发放金额的SQL没走索引因为补贴发放日期字段没建索引。加上索引之后单次查询时间从15秒降到80毫秒。但问题还没完导出Excel时数据量一大同步处理还是会超过Nginx默认的60秒超时时间于是再把耗时较长的导出逻辑改用异步任务先把生成请求放入Redis队列Django后台定时任务处理生成完成后把文件路径写回数据库前端轮询结果。这个过程在调试文档里被完整记录下来包括每个排查步骤和对应的日志片段。后来另一个同事在处理类似的慢请求问题时直接翻文档就找到了思路省了半天的排查时间。5.3 讲解视频和演示数据的准备标题里还提到了“讲解”这个交付物。项目管理系统的演示环节最忌讳用空数据演示。我准备了一套完整的演示数据几十户村民档案、近三个月的公文记录、几次会议纪要、一批资产台账全部做成SQL脚本一键初始化。演示时直接演示真实操作流程比如新建一条收文记录、给村民档案加一条补贴记录、生成月度统计报表。这样用户在屏幕上看到的不是光秃秃的表单而是能理解“这系统能帮我干什么”的场景。有些开发者对“讲解”这个交付物不重视觉得代码写好就行。做这种面向办公场景的管理系统使用者的操作水平普遍不高讲解视频是让他们快速接受新系统的关键一环。我录了三个十分钟左右的短视频系统管理员功能、日常办公操作、报表导出操作都是对着真实页面操作加语音说明最后和源码一起交付。6. 部署与日常维护中真正需要盯住的细节6.1 村级环境的硬件与网络村级办公环境的硬件条件普遍比企业环境紧张。这个项目部署时服务器用的是一台普通的PC主机4核CPU、8G内存装了Ubuntu系统。这台机器同时跑Tomcat、Gunicorn、MySQL、Redis还要做Nginx静态文件服务压力主要集中在内存上。调优时把JVM堆内存控制在2GMySQL的innodb_buffer_pool_size设为1GDjango那边因为只是报表服务Gunicorn开两个worker就够了。网络方面村级办公地点的上行带宽往往不稳定。远程运维时必须确保SSH连接不断建议部署时配置好失败重连和断线后自动重连。给这台服务器配上固定IP和开机自启服务脚本断电重启后系统能自己拉起来不需要专门的人到现场敲命令。6.2 数据初始化最容易被低估的一步系统开发完、准备正式上线的那天最大的坑反而不是技术问题而是“老数据怎么进去”。村级办公系统有大量的历史数据沉在线下纸质公文登记本、Excel村民台账、手写的资产表这些都要在系统启用前完成初始化。实际操作比想象中费时间得多。村民档案这块原始Excel里同一户的人名写法很不规范“张伟”和“张 伟”“王秀兰”和“王秀蘭”都出现了补贴记录里有大量身份证号位数不对的数据。折腾这些脏数据花掉了整整两天。我的建议是上线前至少预留一周做历史数据治理并且一定要让熟悉业务的村干部在场逐条核对因为只有他们能判断“这个人是搬迁走了还是登记错误”。初始化数据的导入脚本也要小心。Django端写了一个导入脚本逐行校验身份证号格式、家庭成员关系合法性校验不通过的行单独导出到一个“待确认清单”文件由管理员人工处理。宁可导入得慢一点也不能静默吞掉错误数据。6.3 备份与定时任务的日常维护备份策略上项目设置了每天凌晨两点用mysqldump全量备份数据库保留最近7天的备份文件外加每月1号做一次冷备份把备份文件同步到另一台远程机器。第一次部署时我就把这个脚本写好并挂在crontab里了。到了第二个月复盘时发现备份文件一直正常生成这个心里才算踏实。定时任务的坑主要集中在Django端日报表统计、补贴发放提醒、会议计划到期提醒这几个用Django自带的Cron库在跑。需要特别注意的是定时任务和主服务时区必须一致否则可能出现“每天凌晨两点跑一次但跑的是前一天数据”这种边界问题。另外定时任务执行前要加分布式锁避免上一次任务没跑完下一次又启动了。这个锁用Redis的SETNX实现非常简单但有效。后记做完这个项目之后我最大的体会是技术栈是不是最新、是不是统一其实都不是这类系统成败的关键。两套后端并存确实会增加一些联调工作但换来的是“每个人都能在自己最擅长的领域发挥效率”。真正决定项目顺不顺利的是需求边界有没有划清楚、数据结构有没有贴近使用场景、配套文档能不能让下一个人快速接手。如果你也在做类似的办公管理系统我的建议是多花时间在现场了解台账怎么填、文件怎么传这些细节最后都会转化成系统的核心竞争力。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑