SSM+Flask双后端架构:儿童收养信息管理系统的状态流转与检索推荐落地实践
发布时间:2026/9/30 8:44:46来源:尧图网络
做过几个SSM项目之后你会发现这类老牌技术栈确实稳定但一旦业务场景里塞进了检索、推荐、统计这些偏算法的需求单靠Java硬顶会写得非常别扭。今年帮一个儿童福利方向的机构落地孩童收养信息管理系统我直接采用了JavaSSMFlask的双后端架构SSM负责收养登记、儿童档案、审核流程这些核心业务Flask负责政策检索、收养匹配推荐和数据统计。整套系统跑下来最深的感受是收养管理系统的难点从来不是增删改查而是业务状态怎么流转、两个后端怎么协作、数据怎么保持一致。这篇文章我会从需求拆解、选型逻辑、表设计、核心代码思路到部署踩坑完整过一遍给正在做同类系统尤其是课程设计、毕业设计或者小机构内部信息管理系统的朋友一个可以直接参考的落地方案。1. 这个系统要管的不是儿童档案而是“儿童-家庭-机构”之间的关系流转1.1 三个角色和四条业务主线很多同学拿到“孩童收养信息管理系统”这个题目第一反应就是做一个儿童信息登记表再加个收养家庭申请表然后对着表格写CRUD。但实际把业务捋清楚之后会发现这个系统管理的不只是儿童本身而是儿童、收养家庭、福利机构三者之间的关系变化过程。系统里最基本的角色有三个机构工作人员也就是民政窗口经办人、福利院管理员负责录入儿童档案、审核收养申请、办理收养登记、维护政策文档。收养申请人也就是想要收养儿童的家庭或个人负责提交申请、上传资质材料、查看审核进度、发起政策咨询。系统管理员负责用户分配、角色授权、字典项维护、数据备份这些运维层面的事。围绕这三个角色业务上要打通四条主线第一条是儿童档案从入库、匹配到完成收养的信息变更第二条是收养家庭从提交申请、材料审核、家庭评估到登记办理的完整流程第三条是收养登记完成后的归档与追溯第四条是政策咨询与信息发布。前两条是大家都会做的后两条特别容易在需求设计阶段被漏掉。我当时就是先只写了前两条主线等到需求评审被问了一嘴“收养登记之后档案是删掉还是保留后续回访怎么办”才发现系统必须支持历史追溯。所以后来在儿童表里加了一个状态字段登记完成之后儿童不是被删除而是状态变成已收养所有关联的登记记录都要能按条件查出来。1.2 状态变更比增删改查难得多传统课设项目倾向于把儿童管理做成纯档案管理但我做完这个项目之后可以很明确地说收养系统的核心是状态机不是表单。一个儿童从进入机构到完成收养大致会经历几个状态在院养育刚录入系统待收养公示允许申请人查看和匹配匹配审核中已经有家庭发起申请且进入评估收养登记完成法定登记办结已离院完成交接每一笔状态变更还要记录操作时间、操作人、变更原因。这个“审计要求”一开始我没做后来补了很痛苦因为涉及历史数据的字段改动。建议各位在第一版设计里就给每张核心表加上操作日志表或者至少留够备注字段。2. 技术选型逻辑为什么用SSM管业务用Flask管“偏算法”的活2.1 让Java做它擅长的事让Python做它擅长的事先回答一个很多人会问的问题SSM一个框架都能做为什么要再加一个Flask我的答案是能但会很累。SSM在事务控制、权限拦截、企业级分层上是强项这一点不需要怀疑。但在下面这几个场景里Flask背后Python生态的优势太明显了政策文档检索。Java做全文检索得上Lucene或者Elasticsearch对小系统来说太重了而Python生态里有现成的中文分词库和文本相似度计算工具几十行代码就能做一个够用的检索接口。儿童与收养家庭的匹配推荐。这个功能本质上是打分排序Python里处理数据、写加权算法、快速调整因子都比Java方便。统计报表。福利机构非常看重季度、年度的收养数据统计Flask配合ECharts做数据接口和可视化展示开发效率高出一截。所以整体的架构思路是SSM作为主后端处理用户登录、权限、儿童档案、收养登记流程这些核心业务Flask作为辅助后端处理政策检索、匹配推荐、统计接口。两个后端通过HTTP接口通信部署在同一台服务器或者同一内网环境里前端对用户只暴露SSM的地址Flask接口不直接对外开放安全性和权限管理也都集中在SSM这边。2.2 两个后端之间怎么协作协作模式我最终选的是“SSM转发”而不是“前端双调用”。原因很简单如果前端同时调两个后端第一个要处理跨域第二个要处理两套登录态第三个是权限控制会被打散。全部由SSM转发的话前端只跟SSM打交道Flask只信任来自SSM的请求用内网IP加白名单控制就行。在SSM侧我用RestTemplate封装了一个简单的HTTP客户端工具类服务层需要调用Flask接口时直接注入使用。Flask侧则通过CORS配置允许来自SSM服务端的跨域调用同时用请求头里一个自定义的token做服务间鉴权。这个token在两边配置一致虽然简单但对内网系统来说已经够用了。2.3 技术分工对照表下面的表格是我最后定下来的模块归属写进设计文档里开发时照着分工走避免两个人改到同一块代码功能模块归属后端选型理由用户登录与权限拦截SSMShiro权限框架和SpringMVC拦截器处理起来成熟稳定儿童档案增删改查SSMMyBatis写复杂联表查询很顺手事务可控收养登记流程与审核SSM状态机事务是Java的强项不容易出并发问题政策文档入库与更新SSM只是普通CRUD不需要额外框架政策关键词检索和问答Flaskjieba分词相似度计算代码量小见效快儿童与收养家庭匹配推荐Flask打分算法迭代快调试方便统计报表数据接口Flask数据聚合逻辑和JSON输出简洁前端ECharts直接消费这里有一个前提要提醒这套分工是基于项目体量来的。如果是大型省级平台我不会这么分而是统一用Java微服务体系检索直接上Elasticsearch。但如果是一个市级福利机构或者内部管理系统双后端这种“轻量混搭”反而比引入一堆重型组件更务实。3. 数据库设计五张核心表加一条状态链3.1 核心表结构和字段说明数据库我用的MySQL 8.x字符集utf8mb4。表设计上没有搞太复杂的范式业务上够用就好。下面这几张核心表是这个系统的基础。第一张是儿童档案表字段覆盖基本信息、健康情况、入院信息和当前状态CREATE TABLE t_child ( child_id BIGINT PRIMARY KEY AUTO_INCREMENT, child_no VARCHAR(32) NOT NULL COMMENT 儿童编号, name VARCHAR(50) NOT NULL, gender TINYINT COMMENT 1男 2女, birth_date DATE, health_status VARCHAR(255) COMMENT 健康状况描述, guardian_name VARCHAR(50) COMMENT 入院前监护人, entry_date DATE COMMENT 入院日期, status TINYINT COMMENT 1在院 2待收养 3匹配中 4已收养 5离院, photo_url VARCHAR(255), story_desc TEXT COMMENT 儿童简介, create_by BIGINT, create_time DATETIME, update_time DATETIME, UNIQUE KEY uk_child_no (child_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;第二张是收养申请表存申请家庭的基本信息和材料路径CREATE TABLE t_adoption_apply ( apply_id BIGINT PRIMARY KEY AUTO_INCREMENT, apply_no VARCHAR(32) NOT NULL, adopter_name VARCHAR(50) NOT NULL COMMENT 申请人姓名, id_card VARCHAR(18) NOT NULL, spouse_name VARCHAR(50) COMMENT 配偶姓名, income_level VARCHAR(50) COMMENT 家庭收入等级, family_address VARCHAR(255), house_area DECIMAL(10,2) COMMENT 住房面积, family_member_count INT COMMENT 家庭已有子女数, marriage_cert_url VARCHAR(255), income_cert_url VARCHAR(255), health_cert_url VARCHAR(255), police_cert_url VARCHAR(255), apply_date DATETIME, status VARCHAR(32) COMMENT 申请状态, create_by BIGINT, create_time DATETIME, UNIQUE KEY uk_apply_no (apply_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;第三张是收养登记表用来记录“哪个儿童被哪个家庭收养”这个核心事件CREATE TABLE t_adoption_registration ( registration_id BIGINT PRIMARY KEY AUTO_INCREMENT, registration_no VARCHAR(32) NOT NULL COMMENT 登记编号, child_id BIGINT NOT NULL, apply_id BIGINT NOT NULL, review_date DATETIME COMMENT 审核日期, evaluate_date DATETIME COMMENT 评估日期, match_date DATETIME COMMENT 匹配日期, register_date DATETIME COMMENT 登记办结日期, status VARCHAR(32) COMMENT 登记状态, remark VARCHAR(500), create_time DATETIME, UNIQUE KEY uk_registration_no (registration_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;另外两张表分别是政策文档表和咨询记录表我就不把完整SQL都贴出来了思路都是文档内容加发布时间咨询记录是申请人ID、问题、答复、咨询时间。用户表用常规的id、username、password、role字段就够了密码存MD5加盐后的值。3.2 登记流程的状态链怎么设计收养登记是整个系统里最容易出bug的部分因为每一步都可能驳回、回退、终止。如果你只用一个小数字字段表示状态代码很快就堆成一团乱麻。我是这样定义的状态字段直接用字符串枚举可读性优先PENDING_REVIEW申请人提交等待初审FIRST_PASS初审通过进入家庭评估FAMILY_ASSESS正在做家庭评估可能上门走访PENDING_MATCH评估通过等待匹配儿童MATCHED已匹配儿童等待办理登记REGISTERED登记完成TERMINATED流程终止可能是驳回、放弃或评估不合格状态流转的规则是PENDING_REVIEW可以到FIRST_PASS或TERMINATEDFIRST_PASS可以到FAMILY_ASSESS或TERMINATEDFAMILY_ASSESS可以到PENDING_MATCH或TERMINATEDPENDING_MATCH到MATCHEDMATCHED到REGISTERED。正常流程里不允许跳过中间状态比如不能直接从未审核跳到已登记。这里有一个设计细节很关键t_adoption_registration表里的状态和t_child表里的状态要联动更新。比如匹配成功的那一刻申请表中的状态变成MATCHED同时儿童表中的status要从“待收养”变成“匹配中”。这个联动必须在同一个数据库事务里完成否则就会出现“家庭已经匹配成功了儿童在系统里还是待收养”这种数据不一致。我在第一版就犯了这个错写成了两个独立的方法调用结果测试时发现儿童状态没变查了半天才发现是忘了加事务。后面在4.3节我会详细说这个问题的排查思路。4. SSM侧核心模块落地档案管理、材料上传和审核流控制4.1 儿童档案模块的实现结构SSM侧我按标准的Controller-Service-Mapper三层走Controller层只做参数接收、调用服务、返回ModelAndView或者JSON。Service层写业务逻辑比如状态机判断、事务控制。Mapper层用MyBatis写SQL和动态查询。儿童档案模块本身不难但有两个地方值得注意。第一是列表查询要做分页我用PageHelper插件一行代码搞定count查询和limit拼接第二是儿童照片上传我用的本地磁盘存储路径存数据库没有接OSS这类对象存储。拍照上传后后端做类型校验只允许jpg、png大小限制在2MB以内图片文件按日期目录存放文件名用UUID重新生成避免中文文件名乱码和重名覆盖。4.2 收养申请的材料管理收养申请这一步收集的材料比较碎身份证、结婚证、收入证明、无犯罪记录证明每一种都是图片或者PDF。我统一用一个材料表来管理不硬编码到申请表字段里这样以后扩展材料类型不用改表结构。后端限制单文件不超过5MB做过一个简单的文件大小和类型校验过滤器超出直接返回提示省得之后磁盘被塞满。4.3 审核流控制一个必踩的事务失效坑收养申请审核是整个SSM侧最需要谨慎的地方。审核通过时代码逻辑上要做两件事更新申请表的状态在审核日志表里插入一条审核记录。这两个操作必须同时成功或同时失败所以Service方法上必须有Transactional。Transactional(rollbackFor Exception.class) public void passFirstReview(Long applyId, Long reviewerId, String remark) { AdoptionApply apply adoptionApplyMapper.selectById(applyId); if (apply null || !PENDING_REVIEW.equals(apply.getStatus())) { throw new BusinessException(当前申请状态不可进行初审操作); } adoptionApplyMapper.updateStatus(applyId, FIRST_PASS, remark); auditLogMapper.insert(new AuditLog(applyId, reviewerId, 初审通过, remark)); }看着简单但实际测试的时候我发现状态更新了审核日志却没写进去。排查到最后发现是事务失效原因很经典我在同一个Service里通过内部方法调用updateStatus不经过Spring的代理对象导致Transactional注解没有被激活。解决办法有几个第一是拆分Service让A Service调B Service的方法确保调用经过代理第二是注入自身代理第三是用TransactionTemplate手动控制事务。我最后选择的是拆分Service把审核记录写入放到另一个AuditService里这样代码结构也更清晰。这个坑在SSM项目里非常经典如果你们也是照着源码改一定要注意别在同一个类里“自己调自己”。4.4 权限控制与进度查询权限控制使用的Shiro给工作人员和管理员配了不同的角色。普通申请人登录后只能看到自己提交的申请查询逻辑是MyBatis动态SQL根据当前登录用户ID过滤数据。进度查询接口返回的是一个JSON树里面包含每一轮审核的状态、操作人、时间和备注前端用时间线组件渲染用户能看到申请走到哪一步了。5. Flask侧业务政策检索、匹配推荐和统计接口的实现5.1 政策文档检索不用搜索引擎也能做Flask侧第一个让我觉得“这技术栈没白加”的功能是政策检索。政策文档都是中文长文本直接用SQL的LIKE匹配效果很差比如用户搜“收养需要什么条件”单靠like %收养需要什么条件%什么都查不出来。我用的是jieba分词加余弦相似度import jieba from flask import Flask, request, jsonify app Flask(__name__) def calc_similarity(text1, text2): words1 set(jieba.lcut(text1)) words2 set(jieba.lcut(text2)) if not words1 or not words2: return 0.0 inter words1 words2 score len(inter) / (len(words1) len(words2) - len(inter)) return round(score, 4) app.route(/api/search_policy, methods[POST]) def search_policy(): data request.get_json() question data.get(question, ) policies query_all_policies() scored [] for p in policies: s calc_similarity(question, p[title] p[content]) scored.append({doc_id: p[id], title: p[title], score: s}) scored.sort(keylambda x: x[score], reverseTrue) return jsonify(scored[:5])这里用的是最基础的Jaccard相似度实际效果比LIKE好很多。测试的时候搜“多大年龄可以收养”也能匹配到包含“被收养人年龄”的文档虽然排序可能不是最优但对内部辅助查询来说完全够用了。如果你们想做得更精细可以换TF-IDF向量计算余弦相似度但小系统没必要上向量数据库。5.2 儿童与收养家庭的匹配打分匹配推荐是Flask侧最有业务价值的接口。逻辑不复杂本质是给每个待收养儿童和申请家庭算一个匹配分按分数排序输出候选名单。打分因子我定了三个维度年龄匹配度家庭申请意向年龄段与儿童实际年龄的接近程度越接近得分越高。家庭条件匹配家庭已有子女数、收入等级、居住面积对抚养能力的综合评估分。健康与需求匹配儿童是否有特殊健康需求家庭申请意向里是否明确愿意接受匹配则加分。每个因子分配权重最后算加权总分。算法写在Python里非常直观def match_score(child, family): age_score calc_age_match(child[birth_date], family[preferred_age_range]) family_score calc_family_condition(family) need_score calc_need_match(child[health_status], family[accept_special_need]) return 0.4 * age_score 0.4 * family_score 0.2 * need_score这个接口最初是工作人员手动在儿童列表里挑家庭匹配全靠经验现在系统生成推荐列表后能节省大量筛查时间。第一次联调时我发现Flask返回的JSON里日期字段是对象格式前端解析不稳定后来统一在Flask里转成字符串再返回。5.3 统计报表数据接口一次性输出统计这块Flask做起来确实快。比如季度登记趋势我写一个接口直接GROUP BY月份统计登记数量返回给前端ECharts渲染app.route(/api/stats/registration_trend, methods[GET]) def registration_trend(): rows db.query(SELECT DATE_FORMAT(register_date, %Y-%m) AS month, COUNT(*) AS cnt FROM t_adoption_registration WHERE statusREGISTERED GROUP BY month) return jsonify({months: [r[month] for r in rows], counts: [r[cnt] for r in rows]})因为Flask连接的是同一个MySQL库所以这里可以直接读业务表不需要SSM专门暴露统计接口。ECharts那边拿到这两个数组直接就能画折线图效率很高。6. 联调部署中的踩坑记录6.1 端口规划与跨域问题系统部署在一台内网服务器上SSM运行在8080端口Flask运行在5000端口。最开始开发时前端页面直接访问Flask接口遇到跨域问题我临时在Flask里开了CORS中间件from flask_cors import CORS CORS(app)这个只用于本地开发调试。部署到生产环境后我把Flask的host绑定到127.0.0.1同时关闭CORS所有外部请求都走SSM的8080端口由SSM服务端转发到Flask避免Flask接口被外部直接扫描调用。6.2 日期格式和空值问题联调时遇到的最多问题就是日期格式。Java的LocalDateTime序列化成JSON后默认是一个数组或ISO字符串而Python那边解析时经常出格式错误。后来约定所有接口传递日期时间统一用“yyyy-MM-dd HH:mm:ss”字符串Java侧加JsonFormat注解JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime createTime;建议大家在项目一开始就在接口文档里把这个约定写清楚否则两边各按各的格式解析联调阶段会浪费大量时间。6.3 部署脚本和运维建议最后写了一个简单的启动脚本一条命令拉起两个服务#!/bin/bash nohup java -jar adoption-ssm.jar --server.port8080 logs/ssm.log 21 nohup python app.py logs/flask.log 21 echo services started实测过程中建议把两个服务的日志分开存放出了问题先看Flask日志再看SSM日志排查效率高很多。另一个坑是MySQL连接池配置Flask这边我用的PyMySQL默认连接超时时间比较短长时间空闲后首次请求会报连接丢失解决方案是连接池里加自动重连参数或者每次请求重新获取连接。埋伏了两天才发现的接口幂等最后补充一个印象最深的经验收养登记确认接口如果前端双击提交或者网络重试会出现一条数据被插入两次的情况。处理方式是数据库加唯一约束registration_no全局唯一同时后端业务方法里先查再插用唯一键冲突兜底。这个思路同样可以套用在儿童编号、申请编号这些核心业务字段上比单纯靠前端禁按钮可靠得多。我当时在测试环境里才发现这个问题因为手动点击不容易触发但用脚本模拟并发请求就会遇到。如果你要接手这类系统建议把涉及登记编号、申请编号的插入操作全部检查一遍唯一约束凡是没加的都补上。做完这个系统最大的体会是信息管理系统的复杂度不在代码量而在业务规则的建模和跨技术栈的协作设计。SSM作为Java老牌组合做核心事务流程依然让人放心Flask像一把轻快的小刀正好补上检索、推荐、统计这些Java做起来笨重的场景。如果你也准备做类似的项目可以先照着这个需求拆解思路把自己的业务角色和状态链列清楚再决定哪些功能用Java扛、哪些交给Python最后才会发现联调没有想象中那么可怕。
网站建设高端定制企业官网