HealthDataModuleDesign.md 32 KB

健康数据模块设计文档

概述

本设计文档针对慢病APP增加病人用户健康数据模块的需求进行详细设计。模块包含身高、体重、BMI(自动计算)、血压、血糖(空腹、早餐后、午餐前、午餐后、晚餐前、晚餐后、睡前)、心率等健康指标的记录和管理功能。

重要更新:根据需求,每种类型的健康数据需要独立存储和独立的接口。因此,健康数据模块将被拆分为以下4个独立子模块:

  1. 体格数据模块:身高、体重数据存储,BMI自动计算
  2. 血压数据模块:收缩压、舒张压数据存储
  3. 血糖数据模块:空腹血糖、早餐后血糖、午餐前血糖、午餐后血糖、晚餐前血糖、晚餐后血糖、睡前血糖数据存储
  4. 心率数据模块:心率数据存储

每个子模块独立设计,包含独立的数据库表、数据模型、接口和服务。

需求分析

功能需求

  1. 数据录入:支持用户录入各项健康数据
  2. 数据查询:支持查询历史健康数据,支持分页查询
  3. BMI计算:根据身高体重自动计算BMI,不存储计算结果
  4. 数据验证:对输入数据进行合理性校验
  5. 权限控制:基于用户角色控制数据访问权限

数据项说明

  • 体格数据
    • 身高:单位cm,范围100-250cm
    • 体重:单位kg,范围20-300kg
    • BMI:自动计算,身高体重/100^2,不存储
  • 血压数据
    • 收缩压:单位mmHg,范围60-250
    • 舒张压:单位mmHg,范围40-150
  • 血糖数据
    • 空腹血糖:早餐前测量,单位mmol/L,范围1.0-30.0
    • 早餐后血糖:早餐后2小时测量,单位mmol/L,范围1.0-30.0
    • 午餐前血糖:午餐前测量,单位mmol/L,范围1.0-30.0
    • 午餐后血糖:午餐后2小时测量,单位mmol/L,范围1.0-30.0
    • 晚餐前血糖:晚餐前测量,单位mmol/L,范围1.0-30.0
    • 晚餐后血糖:晚餐后2小时测量,单位mmol/L,范围1.0-30.0
    • 睡前血糖:睡前测量,单位mmol/L,范围1.0-30.0
  • 心率数据
    • 心率:单位次/分钟,范围30-200

设计原则

遵循规范

  • 严格遵循项目结构规范(02-项目结构规范.md)
  • 遵循API设计规范(03-API设计规范.md)
  • 遵循数据库规范(07-数据库规范.md)
  • 遵循安全规范(08-安全规范.md)

设计原则

  1. 模块独立性:每个健康数据类型独立存储和管理
  2. 数据完整性:确保数据录入的准确性和完整性
  3. 性能优化:合理设计索引,支持高效查询
  4. 扩展性:预留扩展空间,支持未来增加更多健康指标
  5. 安全性:基于角色权限控制数据访问
  6. 血糖分类:按照医疗标准对血糖进行精细分类,便于医生分析病情

血糖测量类型枚举

// BloodGlucoseType.java
public enum BloodGlucoseType {
    FASTING("空腹", "早餐前测量"),
    AFTER_BREAKFAST("早餐后", "早餐后2小时测量"),
    BEFORE_LUNCH("午餐前", "午餐前测量"),
    AFTER_LUNCH("午餐后", "午餐后2小时测量"),
    BEFORE_DINNER("晚餐前", "晚餐前测量"),
    AFTER_DINNER("晚餐后", "晚餐后2小时测量"),
    BEFORE_BED("睡前", "睡前测量");

    private final String name;
    private final String description;

    BloodGlucoseType(String name, String description) {
        this.name = name;
        this.description = description;
    }

    // getters
}

模块架构

总体架构

健康数据模块
├── 体格数据子模块 (PhysicalData)
│   ├── 数据库表:t_physical_data
│   ├── PO:PhysicalData
│   ├── VO:AddPhysicalDataRequest, PhysicalDataResponse等
│   ├── Controller:PhysicalDataController
│   ├── Service:PhysicalDataService
│   └── Mapper:PhysicalDataMapper
├── 血压数据子模块 (BloodPressureData)
│   ├── 数据库表:t_blood_pressure_data
│   ├── PO:BloodPressureData
│   ├── VO:AddBloodPressureDataRequest, BloodPressureDataResponse等
│   ├── Controller:BloodPressureDataController
│   ├── Service:BloodPressureDataService
│   └── Mapper:BloodPressureDataMapper
├── 血糖数据子模块 (BloodGlucoseData)
│   ├── 数据库表:t_blood_glucose_data
│   ├── PO:BloodGlucoseData
│   ├── VO:AddBloodGlucoseDataRequest, BloodGlucoseDataResponse等
│   ├── Controller:BloodGlucoseDataController
│   ├── Service:BloodGlucoseDataService
│   └── Mapper:BloodGlucoseDataMapper
└── 心率数据子模块 (HeartRateData)
    ├── 数据库表:t_heart_rate_data
    ├── PO:HeartRateData
    ├── VO:AddHeartRateDataRequest, HeartRateDataResponse等
    ├── Controller:HeartRateDataController
    ├── Service:HeartRateDataService
    └── Mapper:HeartRateDataMapper

公共组件

  • 统一的查询请求基类:BaseQueryRequest
  • 统一的分页响应基类:BasePageResponse<T>
  • 统一的业务异常处理
  • 统一的用户权限验证

数据库设计

体格数据表 (t_physical_data)

CREATE TABLE t_physical_data (
    id BIGINT PRIMARY KEY COMMENT '主键ID',
    user_id BIGINT NOT NULL COMMENT '用户ID',
    height DECIMAL(5,2) COMMENT '身高(cm)',
    weight DECIMAL(5,2) COMMENT '体重(kg)',
    measure_time DATETIME NOT NULL COMMENT '测量时间',
    create_user BIGINT COMMENT '创建者ID',
    create_time DATETIME COMMENT '创建时间',
    update_user BIGINT COMMENT '更新者ID',
    update_time DATETIME COMMENT '更新时间',
    version INT DEFAULT 0 COMMENT '乐观锁版本号'
) COMMENT '体格数据表';

-- 索引设计
CREATE INDEX idx_physical_user_measure_time ON t_physical_data(user_id, measure_time);
CREATE INDEX idx_physical_user_id ON t_physical_data(user_id);

血压数据表 (t_blood_pressure_data)

CREATE TABLE t_blood_pressure_data (
    id BIGINT PRIMARY KEY COMMENT '主键ID',
    user_id BIGINT NOT NULL COMMENT '用户ID',
    systolic_pressure INT COMMENT '收缩压(mmHg)',
    diastolic_pressure INT COMMENT '舒张压(mmHg)',
    measure_time DATETIME NOT NULL COMMENT '测量时间',
    create_user BIGINT COMMENT '创建者ID',
    create_time DATETIME COMMENT '创建时间',
    update_user BIGINT COMMENT '更新者ID',
    update_time DATETIME COMMENT '更新时间',
    version INT DEFAULT 0 COMMENT '乐观锁版本号'
) COMMENT '血压数据表';

-- 索引设计
CREATE INDEX idx_bp_user_measure_time ON t_blood_pressure_data(user_id, measure_time);
CREATE INDEX idx_bp_user_id ON t_blood_pressure_data(user_id);

血糖数据表 (t_blood_glucose_data)

CREATE TABLE t_blood_glucose_data (
    id BIGINT PRIMARY KEY COMMENT '主键ID',
    user_id BIGINT NOT NULL COMMENT '用户ID',
    measure_time DATETIME NOT NULL COMMENT '测量时间',
    type VARCHAR(50) NOT NULL COMMENT '血糖测量类型',
    value DECIMAL(5, 2) NOT NULL COMMENT '血糖值',
    version INT DEFAULT 0 COMMENT '乐观锁版本号'
) COMMENT '血糖数据表';

-- 索引设计
CREATE INDEX idx_bg_user_measure_time ON t_blood_glucose_data(user_id, measure_time);
CREATE INDEX idx_bg_user_id ON t_blood_glucose_data(user_id);
CREATE INDEX idx_bg_type ON t_blood_glucose_data(type);

心率数据表 (t_heart_rate_data)

CREATE TABLE t_heart_rate_data (
    id BIGINT PRIMARY KEY COMMENT '主键ID',
    user_id BIGINT NOT NULL COMMENT '用户ID',
    heart_rate INT COMMENT '心率(次/分钟)',
    measure_time DATETIME NOT NULL COMMENT '测量时间',
    create_user BIGINT COMMENT '创建者ID',
    create_time DATETIME COMMENT '创建时间',
    update_user BIGINT COMMENT '更新者ID',
    update_time DATETIME COMMENT '更新时间',
    version INT DEFAULT 0 COMMENT '乐观锁版本号'
) COMMENT '心率数据表';

-- 索引设计
CREATE INDEX idx_hr_user_measure_time ON t_heart_rate_data(user_id, measure_time);
CREATE INDEX idx_hr_user_id ON t_heart_rate_data(user_id);

字段说明

  • id:主键,使用雪花算法生成
  • user_id:关联用户ID
  • measure_time:数据测量时间
  • 各模块特有字段:
    • 体格数据:height(身高), weight(体重)
    • 血压数据:systolic_pressure(收缩压), diastolic_pressure(舒张压)
    • 血糖数据:type(血糖测量类型), value(血糖值)
    • 心率数据:heart_rate(心率)
  • 继承BaseEntity的公共字段

数据模型设计

PO (持久化对象)

体格数据PO

// PhysicalData.java
@TableName("t_physical_data")
public class PhysicalData extends BaseEntity {
    @TableField("user_id")
    private Long userId;

    @TableField("height")
    private BigDecimal height;

    @TableField("weight")
    private BigDecimal weight;

    @TableField("measure_time")
    private LocalDateTime measureTime;

    // getters and setters
}

血压数据PO

// BloodPressureData.java
@TableName("t_blood_pressure_data")
public class BloodPressureData extends BaseEntity {
    @TableField("user_id")
    private Long userId;

    @TableField("systolic_pressure")
    private Integer systolicPressure;

    @TableField("diastolic_pressure")
    private Integer diastolicPressure;

    @TableField("measure_time")
    private LocalDateTime measureTime;

    // getters and setters
}

血糖数据PO

// BloodGlucoseData.java
@TableName("t_blood_glucose_data")
public class BloodGlucoseData extends BaseEntity {
    @TableField("user_id")
    private Long userId;

    @TableField("measure_time")
    private LocalDateTime measureTime;

    @TableField("type")
    private String type;

    @TableField("value")
    private BigDecimal value;

    // getters and setters
}

心率数据PO

// HeartRateData.java
@TableName("t_heart_rate_data")
public class HeartRateData extends BaseEntity {
    @TableField("user_id")
    private Long userId;

    @TableField("heart_rate")
    private Integer heartRate;

    @TableField("measure_time")
    private LocalDateTime measureTime;

    // getters and setters
}

VO (视图对象)

公共查询请求基类

// BaseQueryRequest.java
@Schema(description = "基础查询请求")
public class BaseQueryRequest {
    @Schema(description = "页码", minimum = "1", defaultValue = "1")
    @Min(1)
    private Integer pageNum = 1;

    @Schema(description = "每页大小", minimum = "1", maximum = "100", defaultValue = "10")
    @Min(1) @Max(100)
    private Integer pageSize = 10;

    @Schema(description = "开始时间")
    private LocalDateTime startTime;

    @Schema(description = "结束时间")
    private LocalDateTime endTime;

    // getters and setters
}

体格数据VO

// AddPhysicalDataRequest.java
@Schema(description = "添加体格数据请求")
public class AddPhysicalDataRequest {
    @Schema(description = "身高(cm)", minimum = "100", maximum = "250")
    @DecimalMin("100") @DecimalMax("250")
    private BigDecimal height;

    @Schema(description = "体重(kg)", minimum = "20", maximum = "300")
    @DecimalMin("20") @DecimalMax("300")
    private BigDecimal weight;

    @Schema(description = "测量时间")
    @NotNull
    private LocalDateTime measureTime;

    // getters and setters
}

// PhysicalDataResponse.java
@Schema(description = "体格数据响应")
public class PhysicalDataResponse {
    @Schema(description = "记录ID")
    private Long id;

    @Schema(description = "身高(cm)")
    private BigDecimal height;

    @Schema(description = "体重(kg)")
    private BigDecimal weight;

    @Schema(description = "BMI")
    private BigDecimal bmi;

    @Schema(description = "测量时间")
    private LocalDateTime measureTime;

    @Schema(description = "创建时间")
    private LocalDateTime createTime;

    // getters and setters
}

血压数据VO

// AddBloodPressureDataRequest.java
@Schema(description = "添加血压数据请求")
public class AddBloodPressureDataRequest {
    @Schema(description = "收缩压(mmHg)", minimum = "60", maximum = "250")
    @Min(60) @Max(250)
    private Integer systolicPressure;

    @Schema(description = "舒张压(mmHg)", minimum = "40", maximum = "150")
    @Min(40) @Max(150)
    private Integer diastolicPressure;

    @Schema(description = "测量时间")
    @NotNull
    private LocalDateTime measureTime;

    // getters and setters
}

// BloodPressureDataResponse.java
@Schema(description = "血压数据响应")
public class BloodPressureDataResponse {
    @Schema(description = "记录ID")
    private Long id;

    @Schema(description = "收缩压(mmHg)")
    private Integer systolicPressure;

    @Schema(description = "舒张压(mmHg)")
    private Integer diastolicPressure;

    @Schema(description = "测量时间")
    private LocalDateTime measureTime;

    @Schema(description = "创建时间")
    private LocalDateTime createTime;

    // getters and setters
}

血糖数据VO

// AddBloodGlucoseDataRequest.java
@Schema(description = "添加血糖数据请求")
public class AddBloodGlucoseDataRequest {
    @Schema(description = "测量时间")
    @NotNull
    private LocalDateTime measureTime;

    @Schema(description = "血糖测量类型")
    @NotNull
    private String type;

    @Schema(description = "血糖值", minimum = "1.0", maximum = "30.0")
    @DecimalMin("1.0") @DecimalMax("30.0")
    private BigDecimal value;

    // getters and setters
}

// BloodGlucoseDataResponse.java
@Schema(description = "血糖数据响应")
public class BloodGlucoseDataResponse {
    @Schema(description = "记录ID")
    private Long id;

    @Schema(description = "测量时间")
    private LocalDateTime measureTime;

    @Schema(description = "血糖测量类型")
    private String type;

    @Schema(description = "血糖值")
    private BigDecimal value;

    @Schema(description = "创建时间")
    private LocalDateTime createTime;

    // getters and setters
}

心率数据VO

// AddHeartRateDataRequest.java
@Schema(description = "添加心率数据请求")
public class AddHeartRateDataRequest {
    @Schema(description = "心率(次/分钟)", minimum = "30", maximum = "200")
    @Min(30) @Max(200)
    private Integer heartRate;

    @Schema(description = "测量时间")
    @NotNull
    private LocalDateTime measureTime;

    // getters and setters
}

// HeartRateDataResponse.java
@Schema(description = "心率数据响应")
public class HeartRateDataResponse {
    @Schema(description = "记录ID")
    private Long id;

    @Schema(description = "心率(次/分钟)")
    private Integer heartRate;

    @Schema(description = "测量时间")
    private LocalDateTime measureTime;

    @Schema(description = "创建时间")
    private LocalDateTime createTime;

    // getters and setters
}

接口设计

体格数据模块接口

Controller层

@RestController
@RequestMapping("/physical-data")
@Tag(name = "体格数据管理", description = "病人体格数据相关接口")
public class PhysicalDataController {

    @Autowired
    private PhysicalDataService physicalDataService;

    @PostMapping("/add")
    @Operation(summary = "添加体格数据")
    public R<Void> addPhysicalData(@RequestBody @Valid AddPhysicalDataRequest request) {
        physicalDataService.addPhysicalData(request);
        return R.success(SuccessResultCode.SUCCESS, "添加成功", null);
    }

    @PostMapping("/list")
    @Operation(summary = "查询体格数据列表")
    public R<Page<PhysicalDataResponse>> listPhysicalData(@RequestBody @Valid BaseQueryRequest request) {
        Page<PhysicalDataResponse> response = physicalDataService.listPhysicalData(request);
        return R.success(SuccessResultCode.SUCCESS, "查询成功", response);
    }

    @GetMapping("/bmi")
    @Operation(summary = "计算BMI")
    public R<BigDecimal> calculateBMI(@RequestParam @DecimalMin("100") @DecimalMax("250") BigDecimal height,
                                     @RequestParam @DecimalMin("20") @DecimalMax("300") BigDecimal weight) {
        BigDecimal bmi = physicalDataService.calculateBMI(height, weight);
        return R.success(SuccessResultCode.SUCCESS, "计算成功", bmi);
    }
}

Service层

public interface PhysicalDataService {
    void addPhysicalData(AddPhysicalDataRequest request);
    Page<PhysicalDataResponse> listPhysicalData(BaseQueryRequest request);
    BigDecimal calculateBMI(BigDecimal height, BigDecimal weight);
}

@Service
public class PhysicalDataServiceImpl implements PhysicalDataService {

    @Autowired
    private PhysicalDataMapper physicalDataMapper;

    @Override
    public void addPhysicalData(AddPhysicalDataRequest request) {
        Long userId = getCurrentUserId();

        PhysicalData physicalData = new PhysicalData();
        BeanUtils.copyProperties(request, physicalData);
        physicalData.setUserId(userId);

        physicalDataMapper.insert(physicalData);
    }

    @Override
    public Page<PhysicalDataResponse> listPhysicalData(BaseQueryRequest request) {
        Long userId = getCurrentUserId();

        Page<PhysicalData> page = new Page<>(request.getPageNum(), request.getPageSize());
        LambdaQueryWrapper<PhysicalData> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(PhysicalData::getUserId, userId)
               .ge(request.getStartTime() != null, PhysicalData::getMeasureTime, request.getStartTime())
               .le(request.getEndTime() != null, PhysicalData::getMeasureTime, request.getEndTime())
               .orderByDesc(PhysicalData::getMeasureTime);

        Page<PhysicalData> result = physicalDataMapper.selectPage(page, wrapper);

        // 转换为响应对象,计算BMI
        List<PhysicalDataResponse> responses = result.getRecords().stream()
            .map(this::convertToResponse)
            .collect(Collectors.toList());

        Page<PhysicalDataResponse> responsePage = new Page<>();
        responsePage.setRecords(responses);
        responsePage.setCurrent(result.getCurrent());
        responsePage.setSize(result.getSize());
        responsePage.setTotal(result.getTotal());

        return responsePage;
    }

    @Override
    public BigDecimal calculateBMI(BigDecimal height, BigDecimal weight) {
        if (height == null || weight == null || height.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        }
        // BMI = 体重(kg) / [身高(m)]^2
        BigDecimal heightInMeters = height.divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
        return weight.divide(heightInMeters.pow(2), 2, RoundingMode.HALF_UP);
    }

    private PhysicalDataResponse convertToResponse(PhysicalData physicalData) {
        PhysicalDataResponse response = new PhysicalDataResponse();
        BeanUtils.copyProperties(physicalData, response);
        // 计算BMI
        response.setBmi(calculateBMI(physicalData.getHeight(), physicalData.getWeight()));
        return response;
    }

    private Long getCurrentUserId() {
        // 从安全上下文中获取当前用户ID
        return 1L; // 临时实现
    }
}

Mapper层

@Mapper
public interface PhysicalDataMapper extends BaseMapper<PhysicalData> {
}

血压数据模块接口

Controller层

@RestController
@RequestMapping("/blood-pressure-data")
@Tag(name = "血压数据管理", description = "病人血压数据相关接口")
public class BloodPressureDataController {

    @Autowired
    private BloodPressureDataService bloodPressureDataService;

    @PostMapping("/add")
    @Operation(summary = "添加血压数据")
    public R<Void> addBloodPressureData(@RequestBody @Valid AddBloodPressureDataRequest request) {
        bloodPressureDataService.addBloodPressureData(request);
        return R.success(SuccessResultCode.SUCCESS, "添加成功", null);
    }

    @PostMapping("/list")
    @Operation(summary = "查询血压数据列表")
    public R<Page<BloodPressureDataResponse>> listBloodPressureData(@RequestBody @Valid BaseQueryRequest request) {
        Page<BloodPressureDataResponse> response = bloodPressureDataService.listBloodPressureData(request);
        return R.success(SuccessResultCode.SUCCESS, "查询成功", response);
    }
}

Service层

public interface BloodPressureDataService {
    void addBloodPressureData(AddBloodPressureDataRequest request);
    Page<BloodPressureDataResponse> listBloodPressureData(BaseQueryRequest request);
}

@Service
public class BloodPressureDataServiceImpl implements BloodPressureDataService {

    @Autowired
    private BloodPressureDataMapper bloodPressureDataMapper;

    @Override
    public void addBloodPressureData(AddBloodPressureDataRequest request) {
        Long userId = getCurrentUserId();

        BloodPressureData bloodPressureData = new BloodPressureData();
        BeanUtils.copyProperties(request, bloodPressureData);
        bloodPressureData.setUserId(userId);

        bloodPressureDataMapper.insert(bloodPressureData);
    }

    @Override
    public Page<BloodPressureDataResponse> listBloodPressureData(BaseQueryRequest request) {
        Long userId = getCurrentUserId();

        Page<BloodPressureData> page = new Page<>(request.getPageNum(), request.getPageSize());
        LambdaQueryWrapper<BloodPressureData> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(BloodPressureData::getUserId, userId)
               .ge(request.getStartTime() != null, BloodPressureData::getMeasureTime, request.getStartTime())
               .le(request.getEndTime() != null, BloodPressureData::getMeasureTime, request.getEndTime())
               .orderByDesc(BloodPressureData::getMeasureTime);

        Page<BloodPressureData> result = bloodPressureDataMapper.selectPage(page, wrapper);

        List<BloodPressureDataResponse> responses = result.getRecords().stream()
            .map(record -> {
                BloodPressureDataResponse response = new BloodPressureDataResponse();
                BeanUtils.copyProperties(record, response);
                return response;
            })
            .collect(Collectors.toList());

        Page<BloodPressureDataResponse> responsePage = new Page<>();
        responsePage.setRecords(responses);
        responsePage.setCurrent(result.getCurrent());
        responsePage.setSize(result.getSize());
        responsePage.setTotal(result.getTotal());

        return responsePage;
    }

    private Long getCurrentUserId() {
        // 从安全上下文中获取当前用户ID
        return 1L; // 临时实现
    }
}

Mapper层

@Mapper
public interface BloodPressureDataMapper extends BaseMapper<BloodPressureData> {
}

血糖数据模块接口

Controller层

@RestController
@RequestMapping("/blood-glucose-data")
@Tag(name = "血糖数据管理", description = "病人血糖数据相关接口")
public class BloodGlucoseDataController {

    @Autowired
    private BloodGlucoseDataService bloodGlucoseDataService;

    @PostMapping("/add")
    @Operation(summary = "添加血糖数据")
    public R<Void> addBloodGlucoseData(@RequestBody @Valid AddBloodGlucoseDataRequest request) {
        bloodGlucoseDataService.addBloodGlucoseData(request);
        return R.success(SuccessResultCode.SUCCESS, "添加成功", null);
    }

    @PostMapping("/list")
    @Operation(summary = "查询血糖数据列表")
    public R<Page<BloodGlucoseDataResponse>> listBloodGlucoseData(@RequestBody @Valid BaseQueryRequest request) {
        Page<BloodGlucoseDataResponse> response = bloodGlucoseDataService.listBloodGlucoseData(request);
        return R.success(SuccessResultCode.SUCCESS, "查询成功", response);
    }
}

Service层

public interface BloodGlucoseDataService {
    void addBloodGlucoseData(AddBloodGlucoseDataRequest request);
    Page<BloodGlucoseDataResponse> listBloodGlucoseData(BaseQueryRequest request);
}

@Service
public class BloodGlucoseDataServiceImpl implements BloodGlucoseDataService {

    @Autowired
    private BloodGlucoseDataMapper bloodGlucoseDataMapper;

    @Override
    public void addBloodGlucoseData(AddBloodGlucoseDataRequest request) {
        Long userId = getCurrentUserId();

        BloodGlucoseData bloodGlucoseData = new BloodGlucoseData();
        BeanUtils.copyProperties(request, bloodGlucoseData);
        bloodGlucoseData.setUserId(userId);

        bloodGlucoseDataMapper.insert(bloodGlucoseData);
    }

    @Override
    public Page<BloodGlucoseDataResponse> listBloodGlucoseData(BaseQueryRequest request) {
        Long userId = getCurrentUserId();

        Page<BloodGlucoseData> page = new Page<>(request.getPageNum(), request.getPageSize());
        LambdaQueryWrapper<BloodGlucoseData> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(BloodGlucoseData::getUserId, userId)
               .ge(request.getStartTime() != null, BloodGlucoseData::getMeasureTime, request.getStartTime())
               .le(request.getEndTime() != null, BloodGlucoseData::getMeasureTime, request.getEndTime())
               .orderByDesc(BloodGlucoseData::getMeasureTime);

        Page<BloodGlucoseData> result = bloodGlucoseDataMapper.selectPage(page, wrapper);

        List<BloodGlucoseDataResponse> responses = result.getRecords().stream()
            .map(record -> {
                BloodGlucoseDataResponse response = new BloodGlucoseDataResponse();
                BeanUtils.copyProperties(record, response);
                return response;
            })
            .collect(Collectors.toList());

        Page<BloodGlucoseDataResponse> responsePage = new Page<>();
        responsePage.setRecords(responses);
        responsePage.setCurrent(result.getCurrent());
        responsePage.setSize(result.getSize());
        responsePage.setTotal(result.getTotal());

        return responsePage;
    }

    private Long getCurrentUserId() {
        // 从安全上下文中获取当前用户ID
        return 1L; // 临时实现
    }
}

Mapper层

@Mapper
public interface BloodGlucoseDataMapper extends BaseMapper<BloodGlucoseData> {
}

心率数据模块接口

Controller层

@RestController
@RequestMapping("/heart-rate-data")
@Tag(name = "心率数据管理", description = "病人心率数据相关接口")
public class HeartRateDataController {

    @Autowired
    private HeartRateDataService heartRateDataService;

    @PostMapping("/add")
    @Operation(summary = "添加心率数据")
    public R<Void> addHeartRateData(@RequestBody @Valid AddHeartRateDataRequest request) {
        heartRateDataService.addHeartRateData(request);
        return R.success(SuccessResultCode.SUCCESS, "添加成功", null);
    }

    @PostMapping("/list")
    @Operation(summary = "查询心率数据列表")
    public R<Page<HeartRateDataResponse>> listHeartRateData(@RequestBody @Valid BaseQueryRequest request) {
        Page<HeartRateDataResponse> response = heartRateDataService.listHeartRateData(request);
        return R.success(SuccessResultCode.SUCCESS, "查询成功", response);
    }
}

Service层

public interface HeartRateDataService {
    void addHeartRateData(AddHeartRateDataRequest request);
    Page<HeartRateDataResponse> listHeartRateData(BaseQueryRequest request);
}

@Service
public class HeartRateDataServiceImpl implements HeartRateDataService {

    @Autowired
    private HeartRateDataMapper heartRateDataMapper;

    @Override
    public void addHeartRateData(AddHeartRateDataRequest request) {
        Long userId = getCurrentUserId();

        HeartRateData heartRateData = new HeartRateData();
        BeanUtils.copyProperties(request, heartRateData);
        heartRateData.setUserId(userId);

        heartRateDataMapper.insert(heartRateData);
    }

    @Override
    public Page<HeartRateDataResponse> listHeartRateData(BaseQueryRequest request) {
        Long userId = getCurrentUserId();

        Page<HeartRateData> page = new Page<>(request.getPageNum(), request.getPageSize());
        LambdaQueryWrapper<HeartRateData> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(HeartRateData::getUserId, userId)
               .ge(request.getStartTime() != null, HeartRateData::getMeasureTime, request.getStartTime())
               .le(request.getEndTime() != null, HeartRateData::getMeasureTime, request.getEndTime())
               .orderByDesc(HeartRateData::getMeasureTime);

        Page<HeartRateData> result = heartRateDataMapper.selectPage(page, wrapper);

        List<HeartRateDataResponse> responses = result.getRecords().stream()
            .map(record -> {
                HeartRateDataResponse response = new HeartRateDataResponse();
                BeanUtils.copyProperties(record, response);
                return response;
            })
            .collect(Collectors.toList());

        Page<HeartRateDataResponse> responsePage = new Page<>();
        responsePage.setRecords(responses);
        responsePage.setCurrent(result.getCurrent());
        responsePage.setSize(result.getSize());
        responsePage.setTotal(result.getTotal());

        return responsePage;
    }

    private Long getCurrentUserId() {
        // 从安全上下文中获取当前用户ID
        return 1L; // 临时实现
    }
}

Mapper层

@Mapper
public interface HeartRateDataMapper extends BaseMapper<HeartRateData> {
}

安全考虑

权限控制

  • 患者只能访问自己的健康数据
  • 医生可以访问指定患者的健康数据
  • 系统管理员可以访问所有数据

数据验证

  • 使用Bean Validation进行输入校验
  • 业务逻辑层进行数据合理性校验(如血压高低压关系)

性能优化

索引设计

  • 用户ID + 测量时间复合索引,支持时间范围查询
  • 用户ID索引,支持用户数据隔离

查询优化

  • 分页查询,避免一次性加载大量数据
  • 支持时间范围过滤,减少数据扫描量

扩展性考虑

未来扩展

  • 支持更多健康指标(如血脂、尿酸等)
  • 支持数据统计和趋势分析
  • 支持数据导出功能
  • 支持批量数据导入

版本控制

  • API版本控制,为未来升级预留空间
  • 数据库表结构设计考虑向后兼容

实施计划

本设计文档为第一阶段输出,后续阶段将包括:

  1. 阶段2:创建详细的API接口规范文档(包含所有4个子模块的完整API文档)
  2. 阶段3:创建数据库表结构和迁移脚本文档
  3. 阶段4:创建数据模型(PO/VO)详细设计文档
  4. 阶段5:创建单元测试计划和集成测试用例
  5. 阶段6:创建业务逻辑和异常处理设计文档
  6. 阶段7:创建部署和运维文档

每个阶段都会生成独立的中间文件,确保设计过程可审查和可追溯。

实施顺序建议

  1. 优先实施体格数据模块(包含BMI计算功能)
  2. 其次实施血压数据模块
  3. 然后实施血糖数据模块
  4. 最后实施心率数据模块

这样可以确保核心功能(BMI计算)优先实现,同时为其他模块提供参考模板。

风险评估

技术风险

  • 数据精度问题:使用BigDecimal确保数值计算精度
  • 并发问题:使用乐观锁防止并发更新冲突

业务风险

  • 数据隐私:确保用户数据安全,遵循医疗数据保护规范
  • 数据准确性:通过校验和审核机制确保数据质量

总结

本设计遵循项目现有规范,提供了完整的健康数据模块设计方案。设计考虑了数据完整性、安全性、性能和扩展性,为慢病APP的健康数据管理提供了坚实的基础。

关键特性:

  • 模块化设计:将健康数据拆分为4个独立子模块,每个模块独立存储和管理
  • 数据隔离:每种健康数据类型使用独立的数据库表,确保数据隔离和性能优化
  • 统一架构:所有子模块遵循相同的架构模式,便于维护和扩展
  • BMI计算:体格数据模块提供BMI自动计算功能,不存储计算结果
  • 血糖精细分类:血糖数据按7个标准医疗时间点分类,便于医生诊断分析
  • 权限控制:基于用户角色确保数据访问安全
  • 分页查询:支持时间范围过滤和分页查询,提高查询性能

设计优势:

  1. 高内聚低耦合:每个模块职责单一,易于维护
  2. 扩展性强:新增健康数据类型只需按照模板添加新模块
  3. 性能优化:独立表设计避免了大数据量查询时的性能问题
  4. 医疗专业性:血糖数据按标准医疗时间点分类,便于医生诊断
  5. 数据一致性:每种数据类型独立管理,确保数据一致性
  6. 接口清晰:每个模块提供独立的API,便于前端集成