Spring Boot+uniapp构建居民健康监测系统实战 简介本资源是一套基于Spring Boot后端与uniapp前端的居民健康监测系统源码面向Java全栈开发者、小程序学习者及医疗信息化实践者解决社区级健康数据采集、用户管理与可视化报告等实际场景需求。压缩包共1608个文件涵盖137个Java后端业务与配置类、233个Vue组件及90个WXML/WXSS小程序页面文件、210个JS逻辑脚本、190个PNG图标与161个SVG矢量图支撑完整小程序界面与后台功能另有SQL建表脚本、YML配置、BAT一键部署脚本等实用工程文件整体30.5MB结构清晰便于模块化学习与二次开发。目前已有54人下载学习资源包含可直接运行的管理后台、用户健康档案录入模块、体温/血压/血糖多维数据监测与图表生成能力以及基于本地路径upload的图片上传存储实现适合作为毕业设计、课程实训或健康类小程序快速原型参考。1. 为什么居民健康监测系统必须用 Spring Boot uniapp 双端协同架构一个社区卫生服务中心上线新系统后发现护士每天要手动录入 200 条血压、血糖、心率数据纸质登记本堆满三抽屉漏记率超 18%而老年居民用不了复杂 App微信里点开 H5 页面又无法调用定位和蓝牙连接家用体征设备。这不是个“做个后台加个前端”就能解决的问题——它需要一套能同时满足政务级数据合规性、多端统一业务逻辑、低门槛用户触达的组合方案。Spring Boot 提供了快速构建符合等保要求的 RESTful 接口、JWT 鉴权、MyBatis-Plus 数据审计、以及对接医院 HIS 系统所需的 JDBC 多数据源能力uniapp 则让同一套 Vue 代码编译出微信小程序用于老人子女代操作、H5嵌入公众号免安装、Android/iOS 原生 App社区医护专用且能通过uni.getLocation、uni.getConnectedBluetoothDevices、uni.onBLEConnectionStateChange等 API 直接调用设备传感器。这不是技术炫技而是当「居民在家测一次血压」和「社区医生在 PC 端看到实时预警」必须发生在同一套业务流里时唯一可落地的工程选择。2. Spring Boot 后端四层架构如何支撑健康数据的强一致性与审计追溯居民健康监测系统不是普通 CRUD 应用。一次血压测量值可能触发慢病随访流程一条异常心率记录需联动家庭医生工作站所有操作必须留痕、可回溯、防篡改。Spring Boot 的分层设计不是教科书模板而是应对真实监管要求的技术契约。2.1 四层结构选型依据与包结构映射层级包路径示例核心职责为何不可省略Controllercom.health.controller接收 uniapp 请求JSON、校验基础参数如Valid RequestBody BloodPressureDTO、返回统一响应体含code200/400/500承担第一道防线防止非法字段穿透、控制接口幂等性如PostMapping(value /v1/measure, produces MediaType.APPLICATION_JSON_VALUE)Servicecom.health.service.impl编排业务逻辑校验用户是否绑定家庭医生、判断收缩压是否 ≥140mmHg 触发预警、生成随访任务 ID 并写入消息队列避免 Controller 耦合数据库操作此处是规则引擎入口例如if (bp.getSystolic() 140) { alertService.sendUrgentAlert(userId); }Mapper / Repositorycom.health.mapperMyBatis或com.health.repositoryJPA仅执行单表增删改查MyBatis XML 中强制使用selectKey生成 UUID 主键禁用useGeneratedKeystrue避免 MySQL 自增 ID 泄露数据量满足等保 2.0 对“数据操作最小权限”要求所有 SQL 必须显式声明字段禁止SELECT *Entity / DTO / VOcom.health.entity/dto/voEntity 带Table(name t_blood_pressure)映射物理表DTO 用于接收前端参数含NotNull注解VO 用于返回脱敏数据如mobile: 138****1234实现数据传输对象隔离Entity 不暴露给前端VO 不含敏感字段身份证号经JsonIgnore或Transient过滤提示不要用 Lombok 的Data全局开启 getter/setter——健康数据字段如bloodSugar必须区分读写权限GetterSetter(access AccessLevel.PRIVATE)防止外部误赋值。2.2 关键配置项让健康数据真正“可追溯”Spring Boot 的application.yml中以下配置直接决定系统能否通过卫健部门数据质量检查spring: datasource: url: jdbc:mysql://health-db:3306/health_db?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: health_app password: ${DB_PASSWORD} # 从环境变量注入禁止明文 hikari: connection-timeout: 30000 maximum-pool-size: 20 # 强制开启 SQL 执行日志仅开发/测试环境 >Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { // 插入时自动填充创建人、时间 this.strictInsertFill(metaObject, createdBy, String.class, system); // 默认系统创建 this.strictInsertFill(metaObject, createdTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updatedBy, String.class, system); this.strictInsertFill(metaObject, updatedTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, deleted, Integer.class, 0); // 逻辑删除默认 0 } Override public void updateFill(MetaObject metaObject) { // 更新时只更新 updatedBy 和 updatedTime this.strictUpdateFill(metaObject, updatedBy, String.class, system); this.strictUpdateFill(metaObject, updatedTime, LocalDateTime.class, LocalDateTime.now()); } }这段代码确保每条血压记录入库时created_time字段精确到毫秒、created_by记录操作来源如wechat_mini_program_v2.3而非简单设为new Date()。当卫健监督员抽查某居民 2024 年 Q3 血压趋势时系统能导出含完整操作链路的 Excel谁在何时、通过哪个终端、录入了哪组数值。2.3 健康数据特有的事务边界设计一次完整的“家庭医生上门随访”操作包含① 更新居民体征数据t_blood_pressure② 新增随访记录t_follow_up③ 修改居民档案状态t_resident_profile.status under_care④ 向家属推送微信模板消息调用微信 API这四个动作必须原子性成功或全部回滚。但第④步是外部 HTTP 调用无法纳入本地数据库事务。解决方案是Saga 模式 补偿事务Service Transactional(rollbackFor Exception.class) public class FollowUpService { Transactional(propagation Propagation.REQUIRED) public void startFollowUp(FollowUpRequest request) throws Exception { // 步骤①②③ 在同一事务中执行 bloodPressureMapper.insert(request.getBp()); followUpMapper.insert(request.getFollowUp()); profileMapper.updateStatus(request.getResidentId(), under_care); // 步骤④ 单独异步执行失败则记录补偿任务 try { wechatService.sendTemplateMessage(request.getFamilyOpenId(), request); } catch (Exception e) { // 写入补偿表 t_compensation_task由定时任务重试 compensationTaskMapper.insert(new CompensationTask( SEND_WECHAT_MSG, request.getFamilyOpenId(), JSON.toJSONString(request), 3 // 最多重试 3 次 )); } } }这种设计让系统在微信服务临时不可用时仍能保证核心健康数据一致性同时不阻塞业务主流程——这是居民健康系统区别于电商系统的根本特征数据正确性优先于即时响应。3. uniapp 前端如何用一套代码覆盖微信小程序、H5 公众号、App 三端健康监测场景uniapp 的价值不在“一次开发多端运行”的宣传语而在于它解决了健康监测场景下最棘手的三个矛盾老人不会装 App 但会点微信、社区医生需要离线扫码录入但 H5 无蓝牙权限、子女想实时查看父母数据却受限于不同平台登录态。这些需求无法靠 WebView 或纯 H5 满足必须深度利用 uniapp 的条件编译和原生能力桥接。3.1 条件编译实现三端差异化功能uniapp 的/* #ifdef MP-WEIXIN */和/* #ifdef APP-PLUS */不是锦上添花而是功能落地的前提。以“获取当前位置并显示附近社区卫生站”为例template view classmap-container !-- 微信小程序端使用 wx.getLocation wx.openLocation -- /* #ifdef MP-WEIXIN */ button clickopenWeixinMap打开微信地图/button /* #endif */ !-- H5 端公众号内使用 HTML5 Geolocation API -- /* #ifdef H5 */ button clickopenH5Map定位并查找站点/button /* #endif */ !-- App 端调用 plus.geolocation 获取高精度定位 -- /* #ifdef APP-PLUS */ button clickopenAppMap启动原生地图/button /* #endif */ /view /template script export default { methods: { /* #ifdef MP-WEIXIN */ openWeixinMap() { uni.getLocation({ type: gcj02, // 火星坐标系适配微信地图 success: (res) { uni.openLocation({ latitude: res.latitude, longitude: res.longitude, name: 最近社区卫生站, address: XX街道社区卫生服务中心 }); } }); } /* #endif */ /* #ifdef H5 */ openH5Map() { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( (position) { const lat position.coords.latitude; const lng position.coords.longitude; // 调用高德 JS API 渲染地图 this.initAMap(lat, lng); }, (err) { uni.showToast({ title: 请允许位置权限, icon: none }); } ); } } /* #endif */ /* #ifdef APP-PLUS */ openAppMap() { // plus.geolocation 是 5 Runtime 提供的原生定位 API精度更高 plus.geolocation.getCurrentPosition( (p) { // 启动原生地图应用 plus.runtime.openURL(geo:${p.coords.latitude},${p.coords.longitude}); }, (e) { console.error(定位失败: e.message); } ); } /* #endif */ } }; /script这段代码的关键在于微信小程序用wx.openLocation跳转微信内置地图符合微信生态规范H5 用浏览器标准 API避免被公众号拦截App 端用plus.geolocation获取亚米级精度满足医护上门导航需求。若强行用一套uni.getLocation统一调用H5 端在 iOS 微信中将因 Safari 安全策略无法获取定位导致功能瘫痪。3.2 健康设备直连uniapp 如何驱动蓝牙血压计与 NFC 健康卡居民在家测血压最理想路径是打开微信小程序 → 点击“连接设备” → 自动发现已配对蓝牙血压计 → 一键同步数据到云端。这需要突破 uniapp 默认能力边界步骤 1配置 manifest.json 启用原生权限{ name: 居民健康监测, appid: __UNI__XXXXXXX, description: , versionName: 2.3.0, versionCode: 230, transformPx: false, splashscreen: { alwaysShowBeforeRender: true, waiting: true, autoclose: true, delay: 0 }, permissions: { android: { uses-permission: [ android.permission.BODY_SENSORS, android.permission.ACCESS_FINE_LOCATION, android.permission.BLUETOOTH, android.permission.BLUETOOTH_ADMIN, android.permission.ACCESS_COARSE_LOCATION ] }, ios: { UIBackgroundModes: [bluetooth-central], NSBluetoothAlwaysUsageDescription: 需要蓝牙权限同步血压计数据, NSLocationWhenInUseUsageDescription: 需要定位权限查找附近社区卫生站 } } }注意BODY_SENSORS权限是 Android 10 读取蓝牙医疗设备如血压计、血糖仪的必要条件缺此权限会导致uni.getConnectedBluetoothDevices返回空数组。步骤 2编写蓝牙连接与数据解析逻辑// utils/bluetooth.js export const connectBloodPressureDevice async () { try { // 1. 初始化蓝牙适配器 await uni.openBluetoothAdapter(); // 2. 开始搜索App 端需先申请位置权限 /* #ifdef APP-PLUS */ await new Promise((resolve) { plus.android.importClass(android.Manifest); plus.android.requestPermissions([android.permission.ACCESS_FINE_LOCATION], () { resolve(); }, () { uni.showToast({ title: 请授予定位权限, icon: none }); }); }); /* #endif */ // 3. 搜索指定服务 UUID 的设备血压计通用 UUID const devices await uni.searchBluetoothDevices({ services: [00001810-0000-1000-8000-00805f9b34fb] // Blood Pressure Service }); if (devices.deviceList.length 0) { throw new Error(未发现血压计设备); } // 4. 连接设备并读取特征值 const deviceId devices.deviceList[0].deviceId; await uni.createBLEConnection({ deviceId }); // 5. 启用通知血压计数据通过 notify 通道发送 await uni.notifyBLECharacteristicValueChange({ state: true, deviceId, serviceId: 00001810-0000-1000-8000-00805f9b34fb, characteristicId: 00002a35-0000-1000-8000-00805f9b34fb // Blood Pressure Measurement }); // 6. 监听数据变化实际项目中需解析 IEEE-11073 医疗协议 uni.onBLECharacteristicValueChange((res) { const buffer new Uint8Array(res.value); const systolic buffer[3] | (buffer[4] 8); // 收缩压单位 mmHg const diastolic buffer[5] | (buffer[6] 8); // 舒张压 const pulse buffer[7] | (buffer[8] 8); // 心率 // 上报到 Spring Boot 后端 uni.request({ url: https://api.health.gov/v1/measure/blood-pressure, method: POST, data: { systolic, diastolic, pulse, deviceId }, header: { Authorization: Bearer uni.getStorageSync(token) } }); }); } catch (err) { console.error(蓝牙连接失败:, err); uni.showToast({ title: 连接失败请检查设备电量与蓝牙开关, icon: none }); } };这段代码展示了 uniapp 如何绕过 H5 限制在 App 端实现真正的医疗设备直连。关键点在于notifyBLECharacteristicValueChange启用通知后血压计每测量一次就主动推送二进制数据帧前端无需轮询降低功耗解析逻辑严格遵循 Bluetooth SIG 的 Blood Pressure Service 规范GATT UUID00001810-...确保兼容市面 90% 以上国产血压计。3.3 微信公众号 H5 的特殊处理授权、定位、分享闭环H5 端嵌入微信公众号面临三大限制微信浏览器禁用navigator.geolocationiOS 尤甚公众号菜单跳转 H5 时window.location.href会丢失code参数分享到好友需调用wx.config但签名必须后端生成解决方案是服务端预签名 前端静默授权// main.js 全局初始化 const initWechatConfig async () { try { // 1. 从后端获取签名Spring Boot 提供 /api/wechat/sign?urlxxx const res await uni.request({ url: https://api.health.gov/v1/wechat/sign, data: { url: encodeURIComponent(window.location.href.split(#)[0]) } }); // 2. 配置微信 JSSDK wx.config({ debug: false, appId: res.data.appId, timestamp: res.data.timestamp, nonceStr: res.data.nonceStr, signature: res.data.signature, jsApiList: [getLocation, updateAppMessageShareData, updateTimelineShareData] }); // 3. 静默获取用户 openid无需弹窗授权 const code getQueryString(code); // 从公众号菜单带入的 code if (code !uni.getStorageSync(openid)) { const userRes await uni.request({ url: https://api.health.gov/v1/wechat/user-info, data: { code } }); uni.setStorageSync(openid, userRes.data.openid); uni.setStorageSync(userInfo, userRes.data.userInfo); } } catch (e) { console.warn(微信配置失败降级为普通 H5 功能); } }; // 页面 mounted 时调用 export default { mounted() { initWechatConfig(); } };后端 Spring Boot 对应接口RestController RequestMapping(/api/wechat) public class WeChatController { GetMapping(/sign) public MapString, String getSignature(RequestParam String url) { String jsapiTicket weChatService.getJsapiTicket(); // 从 Redis 缓存获取有效 ticket String nonceStr UUID.randomUUID().toString().replace(-, ); long timestamp System.currentTimeMillis() / 1000; String string1 jsapi_ticket jsapiTicket noncestr nonceStr timestamp timestamp url url; String signature DigestUtils.sha256Hex(string1); MapString, String result new HashMap(); result.put(appId, wx1234567890abcdef); result.put(timestamp, String.valueOf(timestamp)); result.put(nonceStr, nonceStr); result.put(signature, signature); return result; } }这套机制让居民点击公众号菜单进入 H5 后自动完成身份绑定、获取定位通过wx.getLocation、并支持一键分享体检报告到微信群——所有操作在 3 秒内完成无需二次跳转或弹窗极大降低老年用户操作门槛。4. 数据贯通实战Spring Boot 与 uniapp 如何实现“测量-预警-随访”端到端闭环一个高血压患者在家用蓝牙血压计测量数据上传后系统需在 5 秒内完成① 存储原始数据 ② 判断是否超标 ③ 若超标则向家庭医生推送企业微信消息 ④ 向患者家属发送微信模板消息 ⑤ 在医生工作台首页展示待处理预警卡片。这个闭环不是靠增加服务器性能而是靠精准的消息路由与状态机设计。4.1 健康数据上报接口的幂等性与防重设计uniapp 端网络不稳定时用户可能连续点击“同步数据”按钮导致同一组血压值重复提交。Spring Boot 必须拒绝重复请求而非简单去重PostMapping(/v1/measure/blood-pressure) public Result? saveBloodPressure(Valid RequestBody BloodPressureDTO dto, HttpServletRequest request) { // 1. 从请求头提取设备指纹uniapp 端通过 plus.device.uuid 或 wx.getSystemInfoSync().model 生成 String deviceFingerprint request.getHeader(X-Device-Fingerprint); if (StringUtils.isBlank(deviceFingerprint)) { return Result.fail(设备标识缺失); } // 2. 构建业务唯一键设备ID 测量时间戳精确到秒 收缩压值 String uniqueKey String.format(%s_%d_%d, deviceFingerprint, dto.getMeasuredAt() / 1000, // 转为秒级时间戳 dto.getSystolic()); // 3. 使用 Redis SETNX 实现分布式锁有效期 10 秒防死锁 Boolean isLocked redisTemplate.opsForValue() .setIfAbsent(bp:lock: uniqueKey, 1, Duration.ofSeconds(10)); if (!Boolean.TRUE.equals(isLocked)) { return Result.fail(数据已存在无需重复提交); } try { // 4. 执行业务逻辑存储、预警、推送 bloodPressureService.handleNewMeasurement(dto, deviceFingerprint); return Result.success(); } finally { // 5. 释放锁 redisTemplate.delete(bp:lock: uniqueKey); } }注意deviceFingerprint不能直接用plus.device.uuid部分 Android 厂商限制推荐方案是 uniapp 端组合plus.device.uuid plus.system.model Date.now().toString().substr(-6)生成 32 位字符串既保证唯一性又规避隐私风险。4.2 预警规则引擎用 Groovy 脚本动态配置慢病阈值卫健委对不同年龄段、不同基础病患者的血压控制目标不同如糖尿病患者收缩压应 130mmHg。硬编码 if-else 无法应对政策调整。Spring Boot 集成 Groovy 实现规则热加载Service public class AlertRuleEngine { Autowired private ScriptEngineManager scriptEngineManager; // 从数据库读取规则脚本支持动态更新 PostConstruct public void loadRules() { String ruleScript if (bp.systolic 140 || bp.diastolic 90) { return HIGH_RISK; } else if (bp.systolic 130 patient.hasDiabetes) { return MEDIUM_RISK; } else { return NORMAL; }; CompiledScript compiled ((Compilable) scriptEngineManager.getEngineByName(groovy)) .compile(ruleScript); // 缓存 compiled script避免每次执行都编译 } public String evaluateRisk(BloodPressure bp, Patient patient) { ScriptEngine engine scriptEngineManager.getEngineByName(groovy); Bindings bindings engine.createBindings(); bindings.put(bp, bp); bindings.put(patient, patient); try { return (String) engine.eval(if (bp.systolic 140 || bp.diastolic 90) { HIGH_RISK } else if (bp.systolic 130 patient.hasDiabetes) { MEDIUM_RISK } else { NORMAL }, bindings); } catch (ScriptException e) { log.error(规则执行失败, e); return UNKNOWN; } } }当卫健部门下发新文件《关于老年高血压患者管理目标的补充通知》运维只需在管理后台修改数据库中的 Groovy 脚本无需重启服务、无需发版5 分钟内所有新测量数据即按新规判定风险等级。4.3 三端状态同步用 WebSocket 实现实时预警推送医生在 PC 端工作台看到预警卡片家属在微信收到模板消息患者在 App 端看到“您的血压偏高请及时联系医生”提示——这三个动作必须强一致。Spring Boot 使用EnableWebSocketMessageBroker搭建 STOMP 消息总线Configuration EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker(/topic, /queue); // 允许订阅 topic 和 queue config.setApplicationDestinationPrefixes(/app); // 客户端发送消息前缀 config.setUserDestinationPrefix(/user); // 用户专属消息前缀 } Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint(/ws).setAllowedOrigins(*).withSockJS(); // 兼容 IE } } // 推送预警到医生端按科室订阅 Service public class AlertPublisher { Autowired private SimpMessagingTemplate messagingTemplate; public void sendToDoctor(String department, Alert alert) { messagingTemplate.convertAndSend(/topic/doctor/ department, alert); } // 推送消息到指定用户家属微信 openid public void sendToUser(String openid, Object message) { messagingTemplate.convertAndSendToUser(openid, /queue/alert, message); } }uniapp 端连接 WebSocket// store/modules/websocket.js const state { stompClient: null }; const mutations { SET_STOMP_CLIENT(state, client) { state.stompClient client; } }; const actions { connectWebSocket({ commit }) { const socket new SockJS(https://api.health.gov/ws); const stompClient Stomp.over(socket); stompClient.connect({}, () { commit(SET_STOMP_CLIENT, stompClient); // 订阅医生科室预警 stompClient.subscribe(/topic/doctor/cardiology, (message) { const alert JSON.parse(message.body); uni.showToast({ title: 心内科预警${alert.patientName} 血压异常, icon: none }); }); // 订阅个人消息家属端 stompClient.subscribe(/user/queue/alert, (message) { const msg JSON.parse(message.body); uni.showModal({ title: 健康提醒, content: msg.content }); }); }); } };当血压数据入库触发预警Spring Boot 同时向/topic/doctor/cardiology和/user/queue/alert发送消息三端在 200ms 内收到相同事件且 WebSocket 连接断开时自动重连保障关键消息不丢失——这才是居民健康监测系统应有的实时性。5. 生产环境关键技巧如何让 Spring Boot uniapp 系统通过等保三级测评等保三级不是“加个防火墙就完事”而是对数据全生命周期的刚性约束。Spring Boot uniapp 组合在测评中最常被扣分的三个点健康数据传输未加密、日志留存不足 180 天、API 未做细粒度权限控制。以下技巧直接对应测评条款。5.1 HTTPS 强制跳转与敏感字段 AES 加密传输即使 Nginx 已配置 SSLuniapp 端仍可能因开发疏忽调用 HTTP 接口。Spring Boot 必须在网关层拦截Configuration public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authz - authz .requestMatchers(/actuator/**).permitAll() // actuator 开放但需后续加固 .requestMatchers(/v1/measure/**).authenticated() // 健康数据接口需登录 .anyRequest().permitAll() ) .requiresChannel(channel - channel .requestMatchers(r - r.getRequestURI().startsWith(/v1/)) .requiresSecure() // 强制 HTTPSHTTP 请求直接 301 ); return http.build(); } }但仅 HTTPS 不够——等保要求“重要数据传输加密”。血压值、血糖值属于《个人信息安全规范》定义的“个人健康生理信息”需额外 AES 加密// uniapp 端加密使用 crypto-js import CryptoJS from crypto-js; const encryptHealthData (data, key) { const iv CryptoJS.enc.Utf8.parse(1234567890123456); // 16字节 IV const encrypted CryptoJS.AES.encrypt(JSON.stringify(data), key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.toString(); }; // Spring Boot 端解密 Component public class AesDecryptor { private static final String KEY HealthMonitor2024!; // 生产环境从 Vault 获取 private static final String IV 1234567890123456; public BloodPressureDTO decrypt(String encryptedJson) { try { byte[] keyBytes KEY.getBytes(StandardCharsets.UTF_8); SecretKeySpec keySpec new SecretKeySpec(keyBytes, AES); IvParameterSpec ivSpec new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8)); Cipher cipher Cipher.getInstance(AES/CBC/PKCS5Padding); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); byte[] encryptedBytes Base64.getDecoder().decode(encryptedJson); byte[] decryptedBytes cipher.doFinal(encryptedBytes); String decryptedJson new String(decryptedBytes, StandardCharsets.UTF_8); return new ObjectMapper().readValue(decryptedJson, BloodPressureDTO.class); } catch (Exception e) { throw new RuntimeException(AES 解密失败, e); } } }提示AES 密钥KEY绝不能写死在代码中生产环境必须通过 HashiCorp Vault 或阿里云 KMS 动态获取每次请求都调用 KMS Decrypt API 解密密钥。5.2 日志审计用 Logback 实现健康操作日志留存 180 天等保要求“审计日志留存不少于 180 天”。Logback 配置必须禁用滚动策略的maxHistory陷阱很多教程写的maxHistory90实际只存 90 个文件非 90 天!-- logback-spring.xml -- appender nameHEALTH_AUDIT classch.qos.logback.core.rolling.RollingFileAppender filelogs/health-audit.log/file rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy !-- 每天一个文件文件名含日期 -- fileNamePatternlogs/health-audit.%d{yyyy-MM-dd}.%i.log/fileNamePattern !-- 保留 180 天的日志文件 -- timeBasedFileNamingAndTriggeringPolicy classch.qos.logback.core.rolling.SizeAndTimeBasedFNATP maxFileSize100MB/maxFileSize /timeBasedFileNamingAndTriggeringPolicy !-- 关键设置 maxHistory 为 180表示最多保留 180 天的文件夹 -- maxHistory180/maxHistory /rollingPolicy encoder pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/pattern /encoder /appender !-- 审计专用 Logger -- logger namecom.health.audit levelINFO additivityfalse appender-ref refHEALTH_AUDIT/ /logger在业务代码中记录审计日志Service public class BloodPressureService { private static final Logger AUDIT_LOGGER LoggerFactory.getLogger(com.health.audit); public void save(BloodPressure bp) { // ... 保存逻辑 AUDIT_LOGGER.info(HEALTH_DATA_UPLOAD|USER_ID:{}|DEVICE_ID:{}|SYSTOLIC:{}|DIASTOLIC:{}|IP:{}, bp.getUserId(), bp.getDeviceId(), bp.getSystolic(), bp.getDiastolic(), RequestContextHolder.currentRequestAttributes().getAttribute(remoteAddr, RequestAttributes.SCOPE_REQUEST)); } }日志格式严格按HEALTH_DATA_UPLOAD|USER_ID:1001|DEVICE_ID:BP-2024-001|SYSTOLIC:142|DIASTOLIC:92|IP:119.123.45.67输出便于 ELK 做字段提取与关联分析——测评时审计组会随机抽取 3 个用户要求提供其近 180 天所有健康数据操作日志格式错误即判不合格。5.3 API 权限精细化基于 ResolvedPermission 的动态鉴权Spring Security 默认的PreAuthorize(hasRole(DOCTOR))过于粗放。家庭医生只能查看自己签约居民的数据社区护士只能操作本辖区数据这种行级权限必须动态计算Component public class HealthDataPermissionEvaluator implements PermissionEvaluator { Autowired private ResidentService residentService; Override public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) { if (!(targetDomainObject instanceof BloodPressure)) { return false; } BloodPressure bp (BloodPressure) targetDomainObject; String currentUserId authentication.getName(); // 获取当前用户角色 Collection? extends GrantedAuthority authorities authentication.getAuthorities(); boolean isDoctor authorities.stream().anyMatch(a - a.getAuthority().equals(ROLE p a hrefhttps://download.csdn.net/download/2501_90323865/91056397 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p