健康服务器与云端服务
健康服务器与云端服务是 JL_Health 应用中负责与杰理健康云平台通信的子系统,通过 jl_health_http SDK(AAR 库)完成健康数据的双向同步:将本地采集的健康数据上报到云端服务器,并从云端下载历史健康数据回本地。
Purpose and Scope
本文档介绍健康应用与云端服务器之间的完整通信链路,包括:
jl_health_httpSDK(jl_health_http_V1.4.0_10311-release.aar)的定位与核心 API(HttpClient、HealthDataApi、请求/响应模型)。ServerHealthDataSyncTask双向同步任务的实际实现:服务器数据下载(syncServerToLocalData)与本地数据上报(syncLocalDataToServer)。- 同步数据的本地持久化模型(
HealthEntity、HealthDao、HealthDataDbHelper)与同步标志位的维护方式。 - 同步触发条件、网络检测、失败处理与边界情况。
以下主题属于兄弟页面,不在本文范围:
- 设备侧健康数据采集与蓝牙通道(参见设备健康数据同步
DeviceHealthDataSyncTask、实时健康数据同步RealTimeHealthDataSyncTask)。 - 本地数据库整体设计(Room 数据库、DAO 层,参见 3.4 体系下的数据持久化相关页面)。
- AI 云消息(
AICloudMessageDao/AICloudMessageEntity)属独立能力,不在本文展开。
Overview
在可穿戴健康设备应用中,健康数据(心率、血氧、运动等)首先由设备端采集并落到本地数据库,随后需要与云端服务器保持一致性。健康服务器与云端服务这一能力解决了三个核心问题:
- 数据备份与跨设备迁移:用户换机或重装应用后,可从云端恢复全部历史健康数据。
- 多端数据一致性:本地与服务器之间互为镜像,任意一端新增的数据都会被同步到另一端。
- 与账号体系解耦:同步以
uid为维度进行,服务器数据按用户隔离,本地写入时会校验uid匹配。
实现上,应用不直接编写 HTTP 代码,而是通过杰理官方封装的 jl_health_http AAR 库(版本 V1.4.0_10311,Release 构建)访问云端 REST API。该库内部基于 Retrofit2 封装,对外暴露 HttpClient.createHealthDataApi() 工厂方法,返回 HealthDataApi 接口,提供 uploadHealthData(批量上报)与 getHealthData(按时间范围下载)两个核心操作。
同步任务 ServerHealthDataSyncTask 属于 AbstractSyncTask 体系,运行在 ThreadManager 线程池中,是设备开机/定时同步链路上负责"本地 ↔ 云端"的一段。
Architecture
flowchart TD
subgraph sg_App["健康应用 (HealthAide)"]
AppVM["HealthAppViewModel<br/>uid / registerTime"]
NetHelper["NetworkStateHelper<br/>网络状态检测"]
Task["ServerHealthDataSyncTask<br/>双向同步任务"]
DB["HealthDataDbHelper / HealthDao<br/>Room 本地库"]
end
subgraph sg_Sdk["jl_health_http SDK (AAR V1.4.0)"]
HttpClient["HttpClient<br/>createHealthDataApi()"]
Api["HealthDataApi<br/>uploadHealthData / getHealthData"]
Models["HealthDataParam / RangeParam<br/>HealthDataRangeResponse / BooleanResponse"]
end
subgraph sg_Cloud["云端服务器"]
Cloud["健康云平台 REST API"]
Store[("云端健康数据存储")]
end
Task -->|"读取 uid / registerTime"| AppVM
Task -->|"检查网络可用性"| NetHelper
Task -->|"读写本地数据 (sync 标志位)"| DB
Task -->|"调用 SDK"| HttpClient
HttpClient --> Api
Api --> Models
Api -->|"Retrofit2 同步请求 (execute)"| Cloud
Cloud --> Store
Store -.->|"按时间范围返回数据"| Cloud
架构说明:
ServerHealthDataSyncTask是这一能力的编排核心,位于tool/watch/synctask包,继承AbstractSyncTask并实现Runnable。它同时承担"上行"(本地→云端)与"下行"(云端→本地)两个方向的数据同步,run()方法中先执行syncServerToLocalData()再执行syncLocalDataToServer()。jl_health_httpSDK 是应用与云端的唯一通信通道,以 AAR 二进制库形式打包在app/libs目录下。SDK 内部用 Retrofit2 定义 REST 接口,应用侧只依赖com.jieli.jl_health_http包下的公开类型,无需关心 URL、鉴权与序列化细节。- 本地数据库(
HealthEntity→HealthDao→HealthDataDbHelper)是同步的落点与源头:下行数据解码后insert入库并标记sync=true;上行数据从库中筛选sync=false的记录批量上报,成功后在同一事务中翻转标志位。 - 云端服务器以
uid为数据隔离维度,返回的每条记录都带uid字段,客户端在写入前会做一致性校验,防止脏数据混入本地库。
核心实现剖析
jl_health_http SDK:云端通信的封装层
jl_health_http 以 AAR 形式提供(jl_health_http_V1.4.0_10311-release.aar),版本号 V1.4.0_10311 中 10311 是内部构建号。应用通过它访问云端,关键类型如下(依据 ServerHealthDataSyncTask 中的实际使用推断其公开契约):
| 类型 | 角色 | 关键成员(按使用点) |
|---|---|---|
com.jieli.jl_health_http.HttpClient | 静态工厂,创建 API 实例 | createHealthDataApi() |
com.jieli.jl_health_http.model.param.RangeParam | 下载请求的时间范围参数 | 构造器 RangeParam(startTime, endTime),时间为服务器日期格式字符串 |
com.jieli.jl_health_http.model.param.HealthDataParam | 单条上报参数 | 构造器 HealthDataParam(type, base64Data, dateString) |
com.jieli.jl_health_http.model.response.HealthDataRangeResponse | 下载响应 | getCode()(0 表示成功)、getT()(数据列表)、内部类 HealthData 含 getUid() / getData()(Base64 字符串) |
com.jieli.jl_health_http.model.response.BooleanResponse | 上报响应 | getCode()(0 表示成功) |
SDK 的调用方式为 Retrofit2 同步调用:HttpClient.createHealthDataApi().getHealthData(...) / uploadHealthData(...) 返回 retrofit2.Response<T>,调用方执行 .execute() 得到响应对象。这决定了同步任务必须运行在后台线程(ThreadManager 线程池),不能在主线程直接调用。
ServerHealthDataSyncTask:双向同步编排
任务继承 AbstractSyncTask 并实现 Runnable,类型为 TASK_TYPE_SYNC_HEALTH_DATA。start() 不直接执行,而是投递到 ThreadManager 线程池:
@Override
public synchronized void start() {
//super.start();
ThreadManager.getInstance().postRunnable(this);
}
Source: ServerHealthDataSyncTask.java
设计意图:start() 被 synchronized 修饰且实际把执行权交给线程池,既避免了同一任务被重复并发启动,又保证同步不阻塞调用方线程(如广播接收器或主线程中的触发点)。
run() 是任务入口,执行顺序与前置条件为:
- 通过
NetworkStateHelper.getInstance().getNetWorkStateModel()检查网络;网络不可用则直接finishListener.onFinish(),不发起任何请求——无网络时静默跳过,避免无谓的超时等待。 - 从
HealthApplication.getAppViewModel().getUid()取用户标识;uid为空同样直接结束——没有合法账号身份就没有同步的意义。 - 先执行
syncServerToLocalData()(下行:云端 → 本地),再执行syncLocalDataToServer()(上行:本地 → 云端)。 - 整个流程包在
try/catch (Exception)中,异常只打印堆栈,最终必然回调finishListener.onFinish(),保证同步任务链不会因单个任务异常而卡死。
下行同步:按时间范围分页下载
syncServerToLocalData() 的核心策略是从本地最新数据时刻开始,按 10 天一个窗口向服务器分页拉取:
- 起点计算:先查询本地数据库最新一条健康数据(
getLastData(uid))。若存在,起点为lastData.time + 1分钟(避免重复拉取同一分钟的数据);若本地为空,则回退到账号注册时间getRegisterTime() - 1天,保证首次使用也能拉全历史数据。 - 分页循环:
for (start = startTime; start < endTime; start += 10天),每次请求的窗口为[start, min(start+10天, now)],时间格式由CalendarUtil.serverDateFormat()统一生成,保证请求串格式与服务器约定一致。 - 每条记录的处理:校验
healthData.getUid().equals(uid)(防串号);对getData()做Base64.decode得到原始字节;HealthEntity.from(src)解析为本地实体;解析成功则setSync(true)、setUid(uid)后insert入库。
设计意图:分页窗口(10 天)是"单次请求数据量与请求次数"之间的折中——窗口过大单次响应体膨胀,过小则请求次数激增。起点从本地最新数据续传,使重复同步时只拉增量,显著降低流量与服务器压力。
上行同步:批量上报与事务性标志位
syncLocalDataToServer() 负责把本地未上报的数据推送到云端:
- 查询
findBySync(uid, false)获取所有sync=false的本地记录;为空则说明全部已上报,直接返回。 - 逐条把
entity.getData()(原始二进制)做Base64.encodeToString,配合entity.getType()与时间字符串构造HealthDataParam,聚合成一个List<HealthDataParam>。 - 一次
uploadHealthData(params)批量上报;HTTP 层失败(!response.isSuccessful())或业务层失败(getCode() != 0)都只记录日志并返回——此时数据库标志位不变,记录会在下一次同步时重试。 - 上报成功后,在
HealthDataDbHelper.getHealthDatabase().runInTransaction(...)事务中把所有记录的sync置为true并重新insert。
设计意图:用 sync 标志位 + 事务实现"至少一次"语义:只有服务器确认成功后标志位才翻转,失败的记录自然保留在待同步集合中。把标志位更新放进数据库事务,避免了批量更新中途崩溃导致部分记录标记成功、部分未标记的不一致状态。
核心流程
双向同步时序
sequenceDiagram
participant TM as ThreadManager
participant T as ServerHealthDataSyncTask
participant NH as NetworkStateHelper
participant VM as HealthAppViewModel
participant SDK as jl_health_http SDK
participant Cloud as 云端服务器
participant DB as HealthDao (Room)
TM->>T: start() -> postRunnable(run)
activate T
T->>NH: getNetWorkStateModel()
NH-->>T: 网络不可用? -> onFinish()
T->>VM: getUid()
VM-->>T: uid 为空? -> onFinish()
T->>DB: getLastData(uid) / 注册时间
loop 每 10 天一个窗口
T->>SDK: getHealthData(RangeParam).execute()
SDK->>Cloud: HTTP GET (按时间范围)
Cloud-->>SDK: HealthDataRangeResponse
SDK-->>T: Response<HealthDataRangeResponse>
T->>T: 校验 code==0 且 uid 匹配
T->>DB: Base64 解码 -> HealthEntity.from -> insert(sync=true)
end
T->>DB: findBySync(uid, false)
DB-->>T: 未上报记录列表
T->>T: 构造 List<HealthDataParam>
T->>SDK: uploadHealthData(params).execute()
SDK->>Cloud: HTTP POST (批量上报)
Cloud-->>SDK: BooleanResponse
SDK-->>T: Response<BooleanResponse>
T->>DB: runInTransaction: 全部置 sync=true
T-->>TM: onFinish()
deactivate T
同步任务状态流转
stateDiagram-v2
[*] --> 检查前置条件: run()
检查前置条件 --> 跳过: 无网络 或 uid 为空
检查前置条件 --> 下行同步: 条件满足
下行同步 --> 上行同步
上行同步 --> 完成
跳过 --> 完成
完成 --> [*]: onFinish()
下行同步 --> 下行同步: 每 10 天窗口循环
上行同步 --> 上行同步: 批量上报失败,标志位不变<br/>下次同步重试
两个方向的失败互不阻塞:下行某个窗口失败只跳过该窗口(continue 到下一窗口);上行失败则记录保留 sync=false 等待下次同步。任务整体保证 onFinish() 必然被调用,同步任务链(设备数据 → 实时数据 → 服务器数据)不会中断。
数据模型与持久化
同步能力直接依赖本地健康数据表,核心链路为:
HealthEntity:健康数据实体,字段至少包含time(毫秒时间戳)、type(数据类型)、data(原始二进制载荷)、uid(所属用户)、sync(是否已上报)。支持HealthEntity.from(byte[])从二进制反序列化,以及getData()/getType()/getTime()等访问器。HealthDao(Room DAO):提供getLastData(uid)(最新一条)、findBySync(uid, false)(未上报列表)、insert(entity)、clean()等操作,是同步任务读写数据库的唯一入口。HealthDataDbHelper/HealthDatabase:单例数据库门面,提供getHealthDao()与getHealthDatabase()(后者用于runInTransaction包裹事务)。
erDiagram
HealthEntity ||--o{ HealthDao : "读写"
HealthDao ||--|| HealthDatabase : "归属"
HealthDataDbHelper ||--|| HealthDatabase : "单例门面"
HealthDataDbHelper ||--|| HealthDao : "获取"
HealthEntity {
long time PK
int type
byte data
string uid
bool sync
}
sync 标志位是整套同步机制的状态核心:false 表示"本地有、云端可能没有",true 表示"云端已确认"。下行数据写入时直接置 true(因为来自云端,必然已存在);上行数据只有在服务器确认后才在事务中置 true。
使用示例
以下代码全部提取自 ServerHealthDataSyncTask.java 的真实实现。
示例一:按时间范围从服务器下载健康数据
下行同步的单窗口实现,展示了 SDK 同步调用、业务码校验、uid 校验与 Base64 解码入库的完整链路:
private void syncServerToLocalDataByRange(String startTime, String endTime) throws IOException {
Response<HealthDataRangeResponse> response = HttpClient.createHealthDataApi().getHealthData(new RangeParam(startTime, endTime)).execute();
if (!response.isSuccessful()) {
JL_Log.w(tag, "syncServerToLocalDataByRange", "下载服务器健康数据失败 -->" + response.message());
return;
}
HealthDataRangeResponse rangeResponse = response.body();
if (rangeResponse == null || rangeResponse.getCode() != 0) {
JL_Log.w(tag, "syncServerToLocalDataByRange", "下载服务器健康数据失败 ------>" + rangeResponse);
return;
}
for (HealthDataRangeResponse.HealthData healthData : rangeResponse.getT()) {
if (!healthData.getUid().equals(uid)) {
JL_Log.w(tag, "syncServerToLocalDataByRange", "异常uid ------>" + healthData.getUid() + "\tuid=" + uid);
continue;
}
byte[] src = Base64.decode(healthData.getData(), Base64.DEFAULT);
HealthEntity entity = HealthEntity.from(src);
if (entity == null) continue;
entity.setSync(true);
entity.setUid(uid);
JL_Log.d(tag, "syncServerToLocalDataByRange", entity.toString());
HealthDataDbHelper.getInstance().getHealthDao().insert(entity);//插入数据库
}
}
Source: ServerHealthDataSyncTask.java
示例二:批量上报本地健康数据到服务器
上行同步的核心:构造参数列表 → 一次批量上传 → 事务内翻转同步标志位:
List<HealthDataParam> params = new ArrayList<>();
for (HealthEntity entity : healthEntities) {
String base64Data = Base64.encodeToString(entity.getData(), Base64.DEFAULT);
String dateString = CalendarUtil.serverDateFormat().format(new Date(entity.getTime()));
HealthDataParam param = new HealthDataParam(String.valueOf(entity.getType()), base64Data, dateString);
params.add(param);
}
Response<BooleanResponse> response = HttpClient.createHealthDataApi().uploadHealthData(params).execute();
if (!response.isSuccessful()) {
JL_Log.w(tag, "syncLocalDataToServer", "健康数据上报失败 -->" + response.message());
return;
}
BooleanResponse booleanResponse = response.body();
if (booleanResponse == null || booleanResponse.getCode() != 0) {
JL_Log.w(tag, "syncLocalDataToServer", "健康数据上报失败 ------>" + booleanResponse);
return;
}
HealthDataDbHelper.getInstance().getHealthDatabase().runInTransaction(() -> {
//更新上传标志位,需要添加事务
for (HealthEntity entity : healthEntities) {
entity.setSync(true);
HealthDataDbHelper.getInstance().getHealthDao().insert(entity);
}
});
Source: ServerHealthDataSyncTask.java
示例三:下行分页起点的增量续传策略
从本地最新数据(或注册时间)开始、按 10 天窗口推进的增量拉取逻辑:
HealthEntity healthEntity = HealthDataDbHelper.getInstance().getHealthDao().getLastData(uid);//读取数据库最新日期的数据
long dayTime = 1000 * 3600 * 24;
if (healthEntity == null) {
long registerTime = HealthApplication.getAppViewModel().getRegisterTime() - dayTime;
calendar.setTimeInMillis(registerTime);//如果没有记录,则从注册时间作为开始时间
} else {
calendar.setTimeInMillis(healthEntity.getTime() + 1000 * 60);
}
long startTime = calendar.getTimeInMillis();
long endTime = Calendar.getInstance().getTimeInMillis();
long spaceTime = 10 * dayTime;
//分页读取数据,暂定10天一个间隔
for (long start = startTime; start < endTime; start += spaceTime) {
String startString = CalendarUtil.serverDateFormat().format(start);
long end = Math.min(start + spaceTime, Calendar.getInstance().getTimeInMillis());
String endString = CalendarUtil.serverDateFormat().format(end);
JL_Log.i(tag, "syncServerToLocalData", "startTime = " + startString + "\tendTime = " + endString);
syncServerToLocalDataByRange(startString, endString);
}
Source: ServerHealthDataSyncTask.java
API Reference
基于 ServerHealthDataSyncTask 的实际调用点整理 SDK 公开 API(SDK 以 AAR 二进制分发,以下签名为使用侧契约):
HttpClient.createHealthDataApi(): HealthDataApi
静态工厂方法,返回健康数据 API 实例。ServerHealthDataSyncTask 中每次调用都直接通过它获取 API,未持有长生命周期实例,说明 SDK 内部应是无状态/线程安全的。
使用位置:ServerHealthDataSyncTask.java 与 L159
HealthDataApi.getHealthData(RangeParam): Response<HealthDataRangeResponse>
按时间范围下载健康数据(Retrofit 同步接口,需 .execute())。
参数:
RangeParam:时间范围参数,构造器签名RangeParam(String startTime, String endTime),时间格式为CalendarUtil.serverDateFormat()生成的服务器日期串。
返回:
retrofit2.Response<HealthDataRangeResponse>:isSuccessful()表示 HTTP 层成功;body().getCode() == 0表示业务成功;body().getT()为HealthDataRangeResponse.HealthData列表,每条含getUid()(数据所属用户)与getData()(Base64 编码的原始健康数据)。
抛出: IOException(网络层/IO 异常,由 .execute() 抛出,调用方在 run() 的 catch 中兜底)。
HealthDataApi.uploadHealthData(List<HealthDataParam>): Response<BooleanResponse>
批量上报健康数据(Retrofit 同步接口,需 .execute())。
参数:
List<HealthDataParam>:上报参数列表,单条构造器签名HealthDataParam(String type, String base64Data, String dateString),分别对应数据类型、Base64 编码的原始数据、服务器日期格式的时间串。
返回:
retrofit2.Response<BooleanResponse>:isSuccessful()表示 HTTP 层成功;body().getCode() == 0表示业务成功(服务器已接收)。
抛出: IOException,语义同上。
配置选项
云端同步能力没有独立配置文件,参数以内置常量/约定形式固化在代码中:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| SDK 版本 | AAR | jl_health_http_V1.4.0_10311-release | 云端通信库,升级需替换 app/libs 下的 AAR 并同步接口契约 |
| 下行分页窗口 | long | 10 * 24h | 单次下载请求的时间跨度,见 spaceTime = 10 * dayTime |
| 下行起点回退 | long | 注册时间 − 1 天 | 本地无数据时的历史拉取起点 |
| 下行续传偏移 | long | 最新数据时间 + 1 分钟 | 避免重复拉取同一分钟数据 |
| 同步前提 | - | 网络可用 且 uid 非空 | 不满足则整次同步跳过 |
| 时间格式 | - | CalendarUtil.serverDateFormat() | 服务器约定的日期字符串格式,上下行共用 |
失败模式、边界情况与并发
失败模式
| 失败场景 | 检测点 | 行为 | 恢复机制 |
|---|---|---|---|
| 无网络 | NetWorkStateModel.isAvailable() | 整次同步跳过,直接 onFinish() | 下次同步任务触发时重试 |
| uid 为空 | TextUtils.isEmpty(uid) | 整次同步跳过 | 用户登录/绑定后重试 |
| HTTP 层失败(4xx/5xx/超时) | response.isSuccessful() | 记录日志,放弃本次窗口/批次 | 上行:标志位不变,下次重传;下行:跳到下一窗口 |
| 业务失败 | getCode() != 0 或 body 为 null | 记录日志并返回 | 同上 |
| 服务器返回异常 uid | healthData.getUid().equals(uid) | 跳过该条记录,防止串号数据入库 | - |
| Base64 解码/解析失败 | HealthEntity.from(src) == null | 跳过该条记录 | - |
| 未知异常 | catch (Exception e) | 打印堆栈 | 任务仍会 onFinish(),链路上其他任务不受影响 |
边界情况
- 首次同步(本地为空):起点回退到注册时间前 1 天,拉取用户全部历史数据。
- 长时间未同步:分页循环按 10 天窗口推进,窗口数量与离线时长成正比,单次任务可能发起多次请求;每个窗口独立失败、互不影响。
- 重复数据防护:下行续传起点为"本地最新数据 + 1 分钟",与上行路径配合,避免同一分钟数据被反复拉取;
insert依赖time主键去重。 - 上报失败的数据:
sync=false记录会反复进入待上报集合,直到服务器确认——这是"至少一次"语义的自然结果,代价是极端情况下可能重复上报同一条数据(服务器端需幂等或容忍重复)。
并发与一致性
- 任务串行化:
start()为synchronized,且实际执行在ThreadManager线程池,避免同一任务实例被并发启动造成双写。 - 数据库事务:上行成功后的标志位翻转包在
runInTransaction中,保证"标记成功"这一批操作原子性;源码注释中的废弃代码块表明该事务是后期为修复一致性问题而引入的。 - 线程约束:SDK 为 Retrofit 同步接口,
run()整体运行在线程池线程,不触碰主线程;UI 层通过SyncTaskFinishListener.onFinish()感知任务结束。
性能与运维
- 批量上报:未同步数据聚合为单个
uploadHealthData请求,而非逐条上报,显著降低请求次数与服务器负载;代价是单请求体随积压数据量增长。 - 增量下行:从本地最新数据续传 + 10 天分页,重复同步时通常只产生少量请求,流量可控。
- 日志可观测性:任务全程使用
JL_Log(tag 统一为tag)记录 start、时间窗口、失败原因与实体内容(debug 级),便于线上排查同步问题。 - AAR 升级:云端接口变更通过替换
app/libs/jl_health_http_V1.4.0_10311-release.aar完成;升级时需核对HealthDataParam/RangeParam/HealthDataRangeResponse/BooleanResponse的契约是否变化。
扩展点
- 新增同步数据类型:
HealthDataParam以字符串type区分数据类型,新增类型只需在本地采集侧产出对应HealthEntity(type+data),上行/下行链路无需改动——类型透传是天然的扩展点。 - 调整同步窗口:分页间隔
spaceTime(10 天)为局部常量,可按服务器能力调整。 - 触发时机:任务由上层同步链(
AbstractSyncTask+SyncTaskFinishListener)调度,可在设备连接、定时器或应用启动等时机复用该任务,无需修改其内部逻辑。
Related Links
- ServerHealthDataSyncTask.java(本页核心源码)
- jl_health_http SDK(AAR 库)
- 设备侧健康数据采集与同步:
DeviceHealthDataSyncTask、RealTimeHealthDataSyncTask(同目录下兄弟任务,属于设备数据通道页面) - 本地健康数据持久化:
HealthEntity/HealthDao/HealthDatabase/HealthDataDbHelper(属于数据持久化页面) - 项目总览:README.md / README_en.md