作者:974294341-bit | 技术栈:Spring Boot 3 / Vue3 / MySQL·H2 | 标准:MZ/T 039-2013

  1. 为什么写这篇
    起因很朴素:动手做这个项目前,在 B 站刷到两个"老年人能力评估系统"的视频,一个是微信小程序版、一个是机构 Web 版。看演示挺像回事,但点进去只有解说,没有源码、没有题库、没有打分逻辑。

作为一个想真正搞懂"它到底怎么算出来'失能等级'"的人,光看演示没用。于是我做了三件事:

  1. 逆向这两个系统的设计与架构(设计级,非反编译);

  2. 扒标准——找到民政部《老年人能力评估》MZ/T 039-2013,把"22 题怎么评、等级怎么定"搞透;

  3. 自己落地一套能跑的系统:电脑端管理后台 + 登录鉴权 + 后端评估引擎(手机端原生 App 下轮做)。
    这篇博客就是这条链路的完整记录,重点放在评估算法怎么工程化、登录与权限怎么零依赖实现,以及本机没有 Maven 也把 Spring Boot 跑起来的实战,而不是堆界面截图。

  4. 逆向对象:两个系统的真实面貌
    两个 B 站链接都是搜索聚合落地页,拿不到源码,所以做的是设计级逆向——基于公开描述 + 国标框架还原架构。结论如下:
    系统 A(教学演示向)
    系统 B(机构专业向)
    形态
    微信小程序(C 端)+ Web 后台
    Web 系统(机构/医养场景)
    角色
    老人 / 自评师 / 管理员(3 类)
    评估员 / 管理员
    技术栈
    Spring + Vue3 + Bootstrap + 小程序
    同类 Web(框架未明示)
    评估类型
    单一(国标能力评估)
    多量表(能力/认知/营养…)
    特色
    自评 + 评估师建议 + 统计看板
    AI 处方 + 详细/简易双报告
    定位
    轻量/教学演示
    专业评估/干预闭环
    关键启发来自系统 B:它"支持多种评估类型"的本质,是把量表配置化——一套表结构能吃下所有量表,加新量表 = 插几条数据,不碰代码。这一点我直接吸收进了自己的设计。

  5. 标准深挖:评估到底怎么算
    拿到 MZ/T 039-2013 标准后,核心模型非常清晰,是一个四层结构:
    22 个二级指标(逐项 0~4 评分)
    ↓ 求和
    4 个一级指标
    ↓ 按得分区间划级
    一级指标等级(0 能力完好 ~ 3 重度受损)
    ↓ 组合判定(ADL 权重最高)
    综合能力等级(0 能力完好 ~ 3 重度失能)

2.1 4 个一级指标 + 22 个二级指标
一级指标
满分
二级指标(数量)
日常生活活动(ADL)
40
进食、洗澡、修饰、穿衣、大便控制、小便控制、如厕、床椅转移、平地行走、上下楼梯(10)
精神状态
8
认知功能、攻击行为、抑郁症状(3)
感知觉与沟通
10
意识水平、视力、听力、沟通交流(4)
社会参与
20
生活能力、工作能力、时间/空间定向、人物定向、社会交往能力(5)
多数项目 04 分(4=完好,0=完全丧失);部分损伤类项目(攻击行为、抑郁、视力、听力、意识)只用 02 档。

2.2 综合等级判定(2013 版核心)
不是简单取平均分,日常生活活动(ADL)权重最高。判定规则:

  • 0 级 能力完好:4 个维度全为 0 级
  • 1 级 轻度失能:出现 1 级且无更高
  • 2 级 中度失能:出现 2 级(或 ADL=1 且其他有 ≥1 项为 2 级)
  • 3 级 重度失能:任一维度=3 级(或 ADL=2 且其他 ≥2 项为 2/3 级;或 ADL=3)

2.3 一个易踩的版本坑
MZ/T 039-2013(2013 版)和 GB/T 42195-2022(2022 版)不是一回事:

  • 2013 版 = 22 题、能力分 4 级(0~3)
  • 2022 版 = 26 题、能力分 5 级(新增"完全失能"4 级)
    拿源码后第一件事,就是确认它吃的是哪一版,决定题库是 22 还是 26、等级是 4 还是 5 档。我这里先按 2013 版实现。
  1. 关键决策:不要小程序,要原生 App
    项目硬性要求:电脑端 + 手机端 App,不要微信小程序。
    手机端技术选型当时对比了三种:
    方案
    优点
    缺点
    结论
    Flutter
    一套代码出 Android+iOS 真机,最像"正经 App"
    多学一门 Dart
    最省事但非首选
    Android Kotlin 原生
    与 Java 后端生态近、项目演示有说服力
    只有 Android,无 iOS
    选定(下轮做)
    React Native
    前端 React 复用
    与现成 Vue 后台不共用
    排除
    最终落地节奏:后端 Spring Boot + 电脑端 Vue 管理后台 + 登录鉴权先跑通,手机端 Android(Kotlin + Retrofit + Compose)复用同一套 REST API,不改后端。

  2. 工程落地(核心)
    项目路径:elder-assessment-engine/,结构如下:
    elder-assessment-engine/
    ├── pom.xml # Spring Boot 3 / Java 17
    ├── resolve_cp.mjs # 免 Maven 依赖解析器(见 §6)
    ├── sql/elder_assessment_mzt039_2013.sql # 生产建表 + 22 题选项 JSON
    ├── src/main/java/com/example/elder/
    │ ├── assessment/AssessmentEngine.java # ★ 评估算法核心
    │ ├── entity/ # Elder, Scale, ScaleItem, ScaleResult, ScaleAnswer, User...
    │ ├── repo/ # Spring Data JPA
    │ ├── service/ # 评估业务 + 统计
    │ ├── controller/ # /api/auth, /api/scale, /api/elders, /api/assessments, /api/stats
    │ ├── config/ # CORS + 拦截器 + 启动种子数据
    │ └── dto/
    └── frontend/ # Vue3 + Vite + Element Plus + ECharts
    └── src/views/ # Login / ElderList(建档) / Assessment(22题表单) / Report(双报告) / Statistics

4.1 评估引擎(最重要的一块)
把"四层模型"直接写成纯 Java 方法,不依赖框架,方便独立测试。维度满分硬编码为 DIM_MAX = {40, 8, 10, 20},22 题按题号映射到 4 个维度:
// 题号(1~22) → 维度下标:1-10→ADL, 11-13→精神, 14-17→感知, 18-22→社会
private static int[] buildItemDim() {
int[] a = new int[23];
for (int i = 1; i <= 10; i++) a[i] = 0; // 日常生活活动
for (int i = 11; i <= 13; i++) a[i] = 1; // 精神状态
for (int i = 14; i <= 17; i++) a[i] = 2; // 感知觉与沟通
for (int i = 18; i <= 22; i++) a[i] = 3; // 社会参与
return a;
}
综合定级就是标准里那段组合规则的直接翻译,ADL 权重最高:
public static int comprehensiveLevel(int[] dimLevels) {
if (Arrays.stream(dimLevels).allMatch(l -> l == 0)) return 0; // 全 0 → 完好
int adl = dimLevels[0];
if (adl == 3) return 3; // ADL 重度 → 综合重度
if (Arrays.stream(dimLevels).anyMatch(l -> l == 3)) return 3; // 任一维度重度
int othersGE2 = 0;
for (int i = 1; i < 4; i++) if (dimLevels[i] >= 2) othersGE2++;
if (adl == 2 && othersGE2 >= 2) return 3; // ADL 中度 + 其他≥2项中度
if (Arrays.stream(dimLevels).anyMatch(l -> l == 2)) return 2; // 任一中度
if (adl == 1 && othersGE2 >= 1) return 2;
if (Arrays.stream(dimLevels).anyMatch(l -> l == 1)) return 1; // 任一轻度
return 0;
}
主入口 assess(answers, thresholds):题号→分值 + 各维度等级阈值 → 返回四个维度原始分、维度等级、综合等级与名称。

4.2 后端 Spring Boot

  • 表设计沿用"多量表配置化"思路:scale / scale_dimension / scale_item / scale_result / scale_answer / scale_dimension_level / user。当前只挂了 MZ/T 039-2013 一张量表,但结构能无缝扩展认知、营养等量表。
  • REST API(前端直接消费):
    • POST /api/auth/login —— 登录换 token
    • GET /api/scale —— 拉取评估表单(维度+题目+选项)
    • GET/POST/PUT /api/elders —— 老人建档管理
    • POST /api/assessments —— 提交 22 题答案,返回定级结果
    • GET /api/assessments/{id}/report —— 详细/简易双报告
    • GET /api/stats/overview —— 统计概览
  • dev 用 H2 内存库 + 启动自动种子(4 维度 / 22 题 / 等级阈值 / 演示老人 / 登录账号),开箱即跑;application-prod.yml 切 MySQL。
  1. 登录与权限:零依赖实现
    参考演示系统的"欢迎页 → 主界面"流程,我把登录鉴权也做进去了。但不引 Spring Security、不引 jjwt——全部用 JDK 内置能力实现,既减少依赖又能在"无 Maven"环境里编译通过。

5.1 密码:PBKDF2 + 每用户独立加盐
// PasswordUtil:JDK 内置 PBKDF2WithHmacSHA256,65536 次迭代,256 位密钥
private static final int ITERATIONS = 65536;
private static final int KEY_LENGTH = 256;
private static final String ALGO = "PBKDF2WithHmacSHA256";

public static String generateSalt() { // 16 字节随机盐
byte[] salt = new byte[16];
new SecureRandom().nextBytes(salt);
return Base64.getEncoder().encodeToString(salt);
}
public static String hash(String password, String salt) { /* PBEKeySpec + 哈希 */ }
public static boolean matches(String password, String salt, String storedHash) {
return storedHash != null && storedHash.equals(hash(password, salt));
}
数据库只存 salt + hash,即使泄露也无法反推明文。

5.2 令牌:HMAC-SHA256 自签 JWT
不引 jjwt,自己拼 header.payload.signature:
// JwtUtil:HMAC-SHA256 自签,payload 仅含 sub/role/exp
private static final long EXPIRE_MS = 1000L * 60 * 60 * 8; // 8 小时

public static String generate(String username, String role) {
String header = b64url("{"alg":"HS256","typ":"JWT"}"…);
String payload = b64url("{"sub":""+username+"","role":""+role+"","exp":"+exp+"}"…);
String sig = hmacSha256(header + "." + payload, SECRET); // JDK Mac
return header + "." + payload + "." + sig;
}
public static Map<String,Object> parse(String token) { /* 验签 + 解 payload */ }
注意:SECRET 目前是代码里的演示默认值,上线应改环境变量或配置项;token 里只放用户名/角色/过期时间,无敏感信息。

5.3 两角色 + 拦截器
AuthInterceptor 拦截 /api/**(放行 /api/auth/login),从 Authorization: Bearer 解出 sub/role,写入请求上下文。种子账号:
角色
用户名
密码
说明
管理员
admin
admin123
系统管理员
评估员
evaluator
eval123
评估员小张
实测坑提醒:评估员密码是 eval123,不是管理员的 admin123——填错就会出现"用户名或密码错误"(401)。

  1. 踩坑与校准(干货)

6.1 定级阈值不能拍脑袋 ⚠️
维度原始分 → 维度等级,需要阈值(如 ADL ≥36 为 0 级)。我代码里留了注入点,种子数据用的默认近似:
ADL: 0级≥36 1级≥30 2级≥20 3级≥0
精神: 0级≥7 1级≥6 2级≥4 3级≥0
感知: 0级≥9 1级≥8 2级≥5 3级≥0
社会: 0级≥18 1级≥15 2级≥10 3级≥0
上线前必须做:用标准附录《一级指标等级划分》的精确分值覆盖 scale_dimension_level 表,否则定级结果可能与官方不一致。这是整个系统最容易"看起来对、实际错"的地方。

6.2 本机没有 Maven,照样把 Spring Boot 跑起来 🔧
这是本次最有价值的一段。本机没有 Maven 可执行文件,且代理拦截装不上;更糟的是本地 .m2 被两个 Spring Boot 版本(3.2.5 和 4.1.0)污染,全量收集 jar 必然版本冲突。
我的解法是写了一个依赖解析器 resolve_cp.mjs:

  1. 以 spring-boot-dependencies 3.2.5 的 BOM 为权威版本表,递归解析传递依赖;
  2. 处理 BOM 的 import 嵌套(spring-framework-bom / jackson-bom 等)、链式属性 ${a.b}、省略 groupId 的继承;
  3. 从 .m2 缓存里收集精确版本的 jar,复制进 lib/ 目录;
  4. 用 javac -cp 'lib/' 编译、java -cp 'out;lib/' 主类 直接运行,完全绕过 Maven 打包。
    途中还补齐了 .m2 缺失/选错的 byte-buddy(Hibernate 字节码增强必需,锁定 1.14.13)、lombok、h2、logback。
    最终验证:POST /api/auth/login 用 admin/admin123、evaluator/eval123 均 200 返回 token,真实后端在 8080 端口稳定运行。

6.3 "AI 处方"是医疗敏感词
系统 B 的卖点"AI 处方",在医养语境下本质是干预/照护方案,不是开药。若要做:

  • 干预项来自人工维护的目录(可审计),LLM 只做自然语言润色,不自主诊断;
  • 输出需人工审核留痕(auditor_id),无资质场景不做诊断。否则合规风险很大。

6.4 界面美化:欢迎页 + 2.5D 插画
登录页做了视觉升级:左侧 2.5D 等距插画背景 + 蓝紫渐变,登录卡片居中浮于图层之上(毛玻璃质感);主界面去掉了左侧深灰导航栏,改为顶部导航。这些不影响逻辑,但让演示更有说服力。

  1. 验证结果
    验证项
    结果
    评估引擎独立编译(JDK 17, UTF-8)
    ✅ 通过
    前端 npm install + vite build(1666 模块)
    ✅ 通过
    真实 Spring Boot 后端运行 + 登录鉴权
    ✅ 通过(免 Maven 方式,8080 端口实测)
    22 题评估 → 综合定级
    ✅ 通过
    多条件查询 / 统计 / 导入导出
    ✅ 通过

  2. 本地运行
    方式一:标准(需要 Maven + JDK 17)

后端(默认 H2 内存库,:8080)

cd elder-assessment-engine
mvn spring-boot:run

电脑端(:5173,已配 /api 代理到 8080)

cd elder-assessment-engine/frontend
npm install && npm run dev
方式二:本机无 Maven(已验证)
依赖已解析进 lib/、编译产物在 out/,直接运行主类即可:
cd elder-assessment-engine
java -cp 'out;lib/*' com.example.elder.ElderAssessmentApplication --server.port=8080
注:application.yml 里的 server.port 在本环境没被 Spring 读到,用 --server.port=8080 命令行固定即可,功能不受影响。
体验路径:打开 http://localhost:5173/login → 用 admin/admin123(或 evaluator/eval123)登录 → 老人管理 → 新建 → 去评估 → 答 22 题 → 提交 → 看详细/简易报告。

  1. 按课堂任务补齐的功能(WEB 端)
    项目实际是依据一份课堂任务实现的,要求"开发 WEB + 手机端两套评估系统",并明确了 5 项功能。WEB 端已补齐:
    任务要求
    实现方式
    ① 老人注册/信息导入
    单条建档 + CSV 批量导入(含模板下载)
    ② 能力定期评估
    22 题表单,同一老人可多次评估,历史记录可查、可再次评估
    ③ 多条件查询
    按姓名/性别/年龄/最近评估等级/评估时间筛选
    ④ 能力数据统计
    统计页:等级分布饼图、各等级人数、评估次数趋势(ECharts)
    ⑤ 能力数据导出
    老人数据、评估记录一键导出 CSV(Excel 可直接打开)
    导入/导出统一用零依赖 CSV(UTF-8 BOM),不引入 POI,规避本机构建风险;手机端按任务要求另做原生 App(下轮)。

  2. 后续规划

  3. 手机端 Android 原生 App(Kotlin + Retrofit + Compose):复用现有 /api/*,无需改后端,下一轮做。

  4. 扩展多量表 + AI 处方引擎:接系统 B 的设计——规则引擎出干预项、LLM 做文案润色、人工审核留痕。

  5. 升级 GB/T 42195-2022:若目标系统实为 2022 版,扩题库到 26 题、等级到 5 档。

  6. 阈值校准:用标准附录精确分值替换默认近似阈值。

  7. 小结
    这次从"看两个视频"到"自己写出能跑的评估引擎 + 登录鉴权",最大的体会是:
    项目里的"智能评估"剥掉包装,核心就是"一套标准 + 一张题库表 + 一段定级函数"。 把国标吃透、把算法写成可单测的纯函数,比堆界面有价值得多;而"没有 Maven 也能把 Spring Boot 跑起来",逼着我真正理解了 classpath、BOM 版本仲裁和字节码增强这些被构建工具藏起来的东西。
    逆向的价值不在"抄",在于逼自己把别人的演示翻译成可验证的工程实现——然后你会发现,很多"高大上"的系统,骨架也就这么大。

posted on 2026-11-03 20:59  chenyun_922  阅读(3)  评论(0)    收藏  举报