feat(medications): 新增完整的药物管理和服药提醒功能

实现了包含药物信息管理、服药记录追踪、统计分析、自动状态更新和推送提醒的完整药物管理系统。

核心功能:
- 药物 CRUD 操作,支持多种剂型和自定义服药时间
- 惰性生成服药记录策略,查询时才生成当天记录
- 定时任务自动更新过期记录状态(每30分钟)
- 服药前15分钟自动推送提醒(每5分钟检查)
- 每日/范围/总体统计分析功能
- 完整的 API 文档和数据库建表脚本

技术实现:
- 使用 Sequelize ORM 管理 MySQL 数据表
- 集成 @nestjs/schedule 实现定时任务
- 复用现有推送通知系统发送提醒
- 采用软删除和权限验证保障数据安全
This commit is contained in:
richarjiang
2025-11-07 17:29:11 +08:00
parent 37cc2a729b
commit 188b4addca
27 changed files with 3464 additions and 0 deletions

View File

@@ -0,0 +1,946 @@
# 药物管理 API 文档 - 客户端版本
## 基础信息
**Base URL**: `https://your-domain.com/api`
**认证方式**: Bearer Token (JWT)
**Content-Type**: `application/json`
## 认证说明
所有接口都需要在 HTTP Header 中携带 JWT Token
```http
Authorization: Bearer <your_jwt_token>
```
## 数据类型定义
### MedicationForm药物剂型
```typescript
enum MedicationForm {
CAPSULE = "capsule", // 胶囊
PILL = "pill", // 药片
INJECTION = "injection", // 注射
SPRAY = "spray", // 喷雾
DROP = "drop", // 滴剂
SYRUP = "syrup", // 糖浆
OTHER = "other", // 其他
}
```
### RepeatPattern重复模式
```typescript
enum RepeatPattern {
DAILY = "daily", // 每日(目前仅支持每日模式)
}
```
### MedicationStatus服药状态
```typescript
enum MedicationStatus {
UPCOMING = "upcoming", // 待服用
TAKEN = "taken", // 已服用
MISSED = "missed", // 已错过
SKIPPED = "skipped", // 已跳过
}
```
### Medication药物信息
```typescript
interface Medication {
id: string; // 药物唯一标识
userId: string; // 用户ID
name: string; // 药物名称
photoUrl?: string; // 药物照片URL可选
form: MedicationForm; // 药物剂型
dosageValue: number; // 剂量数值(如 1、0.5
dosageUnit: string; // 剂量单位(片、粒、毫升等)
timesPerDay: number; // 每日服用次数
medicationTimes: string[]; // 服药时间列表,格式:["08:00", "12:00", "18:00"]
repeatPattern: RepeatPattern; // 重复模式
startDate: string; // 开始日期ISO 8601 格式
endDate?: string; // 结束日期可选ISO 8601 格式
note?: string; // 备注信息(可选)
isActive: boolean; // 是否激活
deleted: boolean; // 是否已删除
createdAt: string; // 创建时间ISO 8601 格式
updatedAt: string; // 更新时间ISO 8601 格式
}
```
### MedicationRecord服药记录
```typescript
interface MedicationRecord {
id: string; // 记录唯一标识
medicationId: string; // 关联的药物ID
userId: string; // 用户ID
scheduledTime: string; // 计划服药时间ISO 8601 格式
actualTime?: string; // 实际服药时间可选ISO 8601 格式
status: MedicationStatus; // 服药状态
note?: string; // 备注(可选)
deleted: boolean; // 是否已删除
createdAt: string; // 创建时间ISO 8601 格式
updatedAt: string; // 更新时间ISO 8601 格式
medication?: Medication; // 关联的药物信息(可选,用于联表查询)
}
```
### DailyMedicationStats每日统计
```typescript
interface DailyMedicationStats {
date: string; // 日期格式YYYY-MM-DD
totalScheduled: number; // 计划服药总次数
taken: number; // 已服用次数
missed: number; // 已错过次数
upcoming: number; // 待服用次数
completionRate: number; // 完成率百分比0-100保留两位小数
}
```
### 统一响应格式
```typescript
interface ApiResponse<T> {
code: number; // 状态码200成功其他为错误
message: string; // 响应消息
data: T | null; // 响应数据
}
```
---
## 药物管理接口
### 1. 获取药物列表
获取当前用户的药物列表。
**接口**: `GET /medications`
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
| -------- | ------- | ---- | ---------------------------------- |
| isActive | boolean | 否 | 是否只获取激活的药物,默认获取全部 |
| page | number | 否 | 页码,默认 1 |
| pageSize | number | 否 | 每页数量,默认 20 |
**请求示例**:
```http
GET /medications?isActive=true&page=1&pageSize=20
Authorization: Bearer <token>
```
**响应示例**:
```json
{
"code": 200,
"message": "查询成功",
"data": [
{
"id": "med_001",
"userId": "user_123",
"name": "阿司匹林",
"photoUrl": "https://cdn.example.com/medications/aspirin.jpg",
"form": "pill",
"dosageValue": 1,
"dosageUnit": "片",
"timesPerDay": 2,
"medicationTimes": ["08:00", "20:00"],
"repeatPattern": "daily",
"startDate": "2025-01-01T00:00:00.000Z",
"endDate": null,
"note": "饭后服用",
"isActive": true,
"deleted": false,
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
]
}
```
---
### 2. 获取单个药物详情
获取指定药物的详细信息。
**接口**: `GET /medications/{id}`
**路径参数**:
| 参数 | 类型 | 必填 | 说明 |
| ---- | ------ | ---- | ------ |
| id | string | 是 | 药物ID |
**请求示例**:
```http
GET /medications/med_001
Authorization: Bearer <token>
```
**响应**: 同"获取药物列表"中的单个药物对象
---
### 3. 创建药物
创建新的药物信息。
**接口**: `POST /medications`
**请求体**:
```json
{
"name": "阿司匹林",
"photoUrl": "https://cdn.example.com/medications/aspirin.jpg",
"form": "pill",
"dosageValue": 1,
"dosageUnit": "片",
"timesPerDay": 2,
"medicationTimes": ["08:00", "20:00"],
"repeatPattern": "daily",
"startDate": "2025-01-01T00:00:00.000Z",
"endDate": null,
"note": "饭后服用"
}
```
**字段说明**:
| 字段 | 类型 | 必填 | 说明 |
| --------------- | -------------- | ---- | ---------------------------------------------------- |
| name | string | 是 | 药物名称最大100字符 |
| photoUrl | string | 否 | 药物照片URL |
| form | MedicationForm | 是 | 药物剂型 |
| dosageValue | number | 是 | 剂量数值必须大于0 |
| dosageUnit | string | 是 | 剂量单位最大20字符 |
| timesPerDay | number | 是 | 每日服用次数1-10次 |
| medicationTimes | string[] | 是 | 服药时间列表,格式:"HH:mm"数量必须等于timesPerDay |
| repeatPattern | RepeatPattern | 是 | 重复模式,目前仅支持"daily" |
| startDate | string | 是 | 开始日期ISO 8601格式 |
| endDate | string | 否 | 结束日期ISO 8601格式 |
| note | string | 否 | 备注信息 |
**响应示例**:
```json
{
"code": 200,
"message": "创建成功",
"data": {
"id": "med_001",
"userId": "user_123",
"name": "阿司匹林",
"photoUrl": "https://cdn.example.com/medications/aspirin.jpg",
"form": "pill",
"dosageValue": 1,
"dosageUnit": "片",
"timesPerDay": 2,
"medicationTimes": ["08:00", "20:00"],
"repeatPattern": "daily",
"startDate": "2025-01-01T00:00:00.000Z",
"endDate": null,
"note": "饭后服用",
"isActive": true,
"deleted": false,
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
}
```
---
### 4. 更新药物信息
更新指定药物的信息。
**接口**: `PUT /medications/{id}`
**路径参数**:
| 参数 | 类型 | 必填 | 说明 |
| ---- | ------ | ---- | ------ |
| id | string | 是 | 药物ID |
**请求体**: 与创建药物相同,所有字段均为可选
**请求示例**:
```json
{
"dosageValue": 2,
"timesPerDay": 3,
"medicationTimes": ["08:00", "14:00", "20:00"],
"note": "饭后服用,加量"
}
```
**响应**: 同"创建药物"响应
---
### 5. 删除药物
删除指定药物(软删除)。
**接口**: `DELETE /medications/{id}`
**路径参数**:
| 参数 | 类型 | 必填 | 说明 |
| ---- | ------ | ---- | ------ |
| id | string | 是 | 药物ID |
**请求示例**:
```http
DELETE /medications/med_001
Authorization: Bearer <token>
```
**响应示例**:
```json
{
"code": 200,
"message": "删除成功",
"data": null
}
```
---
### 6. 停用药物
停用指定药物(将 isActive 设为 false
**接口**: `POST /medications/{id}/deactivate`
**路径参数**:
| 参数 | 类型 | 必填 | 说明 |
| ---- | ------ | ---- | ------ |
| id | string | 是 | 药物ID |
**请求示例**:
```http
POST /medications/med_001/deactivate
Authorization: Bearer <token>
```
**响应示例**:
```json
{
"code": 200,
"message": "停用成功",
"data": {
"id": "med_001",
"isActive": false,
"updatedAt": "2025-01-15T00:00:00.000Z"
}
}
```
---
## 服药记录接口
### 1. 获取服药记录
查询服药记录,支持多种筛选条件。
**接口**: `GET /medication-records`
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
| ------------ | ---------------- | ---- | -------------------------------------- |
| date | string | 否 | 指定日期YYYY-MM-DD不填则返回所有 |
| startDate | string | 否 | 开始日期YYYY-MM-DD |
| endDate | string | 否 | 结束日期YYYY-MM-DD |
| medicationId | string | 否 | 指定药物ID |
| status | MedicationStatus | 否 | 状态筛选 |
**重要说明**:
- 首次查询某天的记录时,后端会自动生成该天的服药记录(惰性生成)
- 建议使用 `date` 参数查询特定日期,效率更高
**请求示例**:
```http
GET /medication-records?date=2025-01-15&status=upcoming
Authorization: Bearer <token>
```
**响应示例**:
```json
{
"code": 200,
"message": "查询成功",
"data": [
{
"id": "record_001",
"medicationId": "med_001",
"userId": "user_123",
"scheduledTime": "2025-01-15T08:00:00.000Z",
"actualTime": null,
"status": "upcoming",
"note": null,
"deleted": false,
"createdAt": "2025-01-15T00:00:00.000Z",
"updatedAt": "2025-01-15T00:00:00.000Z",
"medication": {
"id": "med_001",
"name": "阿司匹林",
"form": "pill",
"dosageValue": 1,
"dosageUnit": "片"
}
}
]
}
```
---
### 2. 获取今日服药记录
快捷获取今天的所有服药记录。
**接口**: `GET /medication-records/today`
**请求示例**:
```http
GET /medication-records/today
Authorization: Bearer <token>
```
**响应**: 同"获取服药记录"响应
---
### 3. 获取服药记录详情
获取指定服药记录的详细信息。
**接口**: `GET /medication-records/{id}`
**路径参数**:
| 参数 | 类型 | 必填 | 说明 |
| ---- | ------ | ---- | ------ |
| id | string | 是 | 记录ID |
**请求示例**:
```http
GET /medication-records/record_001
Authorization: Bearer <token>
```
**响应**: 同"获取服药记录"中的单个记录对象
---
### 4. 标记为已服用
将服药记录标记为已服用。
**接口**: `POST /medication-records/{id}/take`
**路径参数**:
| 参数 | 类型 | 必填 | 说明 |
| ---- | ------ | ---- | ------ |
| id | string | 是 | 记录ID |
**请求体**:
```json
{
"actualTime": "2025-01-15T08:10:00.000Z"
}
```
**字段说明**:
| 字段 | 类型 | 必填 | 说明 |
| ---------- | ------ | ---- | ---------------------------------------------- |
| actualTime | string | 否 | 实际服药时间ISO 8601格式不填则使用当前时间 |
**响应示例**:
```json
{
"code": 200,
"message": "已记录服药",
"data": {
"id": "record_001",
"medicationId": "med_001",
"userId": "user_123",
"scheduledTime": "2025-01-15T08:00:00.000Z",
"actualTime": "2025-01-15T08:10:00.000Z",
"status": "taken",
"note": null,
"deleted": false,
"createdAt": "2025-01-15T00:00:00.000Z",
"updatedAt": "2025-01-15T08:10:00.000Z"
}
}
```
---
### 5. 跳过服药
跳过本次服药,不计入已错过。
**接口**: `POST /medication-records/{id}/skip`
**路径参数**:
| 参数 | 类型 | 必填 | 说明 |
| ---- | ------ | ---- | ------ |
| id | string | 是 | 记录ID |
**请求体**:
```json
{
"note": "今天身体不适,暂时跳过"
}
```
**字段说明**:
| 字段 | 类型 | 必填 | 说明 |
| ---- | ------ | ---- | ------------ |
| note | string | 否 | 跳过原因备注 |
**响应示例**:
```json
{
"code": 200,
"message": "已跳过服药",
"data": {
"id": "record_001",
"medicationId": "med_001",
"userId": "user_123",
"scheduledTime": "2025-01-15T08:00:00.000Z",
"actualTime": null,
"status": "skipped",
"note": "今天身体不适,暂时跳过",
"deleted": false,
"createdAt": "2025-01-15T00:00:00.000Z",
"updatedAt": "2025-01-15T08:15:00.000Z"
}
}
```
---
### 6. 更新服药记录
更新服药记录的状态和信息。
**接口**: `PUT /medication-records/{id}`
**路径参数**:
| 参数 | 类型 | 必填 | 说明 |
| ---- | ------ | ---- | ------ |
| id | string | 是 | 记录ID |
**请求体**:
```json
{
"status": "taken",
"actualTime": "2025-01-15T08:15:00.000Z",
"note": "延迟服用"
}
```
**字段说明**: 所有字段均为可选
| 字段 | 类型 | 必填 | 说明 |
| ---------- | ---------------- | ---- | -------------------------- |
| status | MedicationStatus | 否 | 服药状态 |
| actualTime | string | 否 | 实际服药时间ISO 8601格式 |
| note | string | 否 | 备注信息 |
**响应**: 同"标记为已服用"响应
---
## 统计接口
### 1. 获取每日统计
获取指定日期的服药统计数据。
**接口**: `GET /medication-stats/daily`
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
| ---- | ------ | ---- | ---------------------- |
| date | string | 是 | 日期格式YYYY-MM-DD |
**请求示例**:
```http
GET /medication-stats/daily?date=2025-01-15
Authorization: Bearer <token>
```
**响应示例**:
```json
{
"code": 200,
"message": "查询成功",
"data": {
"date": "2025-01-15",
"totalScheduled": 6,
"taken": 4,
"missed": 1,
"upcoming": 1,
"completionRate": 66.67
}
}
```
---
### 2. 获取日期范围统计
获取指定日期范围内每天的统计数据。
**接口**: `GET /medication-stats/range`
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
| --------- | ------ | ---- | -------------------------- |
| startDate | string | 是 | 开始日期格式YYYY-MM-DD |
| endDate | string | 是 | 结束日期格式YYYY-MM-DD |
**请求示例**:
```http
GET /medication-stats/range?startDate=2025-01-01&endDate=2025-01-15
Authorization: Bearer <token>
```
**响应示例**:
```json
{
"code": 200,
"message": "查询成功",
"data": [
{
"date": "2025-01-15",
"totalScheduled": 6,
"taken": 4,
"missed": 1,
"upcoming": 1,
"completionRate": 66.67
},
{
"date": "2025-01-14",
"totalScheduled": 6,
"taken": 6,
"missed": 0,
"upcoming": 0,
"completionRate": 100.0
}
]
}
```
---
### 3. 获取总体统计
获取用户的总体服药统计概览。
**接口**: `GET /medication-stats/overall`
**请求示例**:
```http
GET /medication-stats/overall
Authorization: Bearer <token>
```
**响应示例**:
```json
{
"code": 200,
"message": "查询成功",
"data": {
"totalMedications": 5,
"totalRecords": 120,
"completionRate": 85.5,
"streak": 7
}
}
```
**字段说明**:
| 字段 | 类型 | 说明 |
| ---------------- | ------ | ---------------------------------- |
| totalMedications | number | 药物总数 |
| totalRecords | number | 服药记录总数 |
| completionRate | number | 总体完成率(百分比,保留两位小数) |
| streak | number | 连续完成天数 |
---
## 错误码说明
| 错误码 | 说明 |
| ------ | ------------------------- |
| 200 | 操作成功 |
| 400 | 请求参数错误 |
| 401 | 未授权Token无效或已过期 |
| 403 | 权限不足,无法访问该资源 |
| 404 | 资源不存在 |
| 409 | 资源冲突(如重复创建) |
| 500 | 服务器内部错误 |
**错误响应格式**:
```json
{
"code": 400,
"message": "请求参数错误:药物名称不能为空",
"data": null
}
```
---
## 业务逻辑说明
### 1. 服药记录的惰性生成
- 服药记录不会在创建药物时预先生成
- 当首次查询某天的记录时,后端会自动生成该天的所有服药记录
- 生成规则:根据药物的 `timesPerDay``medicationTimes` 生成对应数量的记录
**示例**
- 如果药物设置为每天2次服药时间为 08:00 和 20:00
- 首次查询 2025-01-15 的记录时,会自动生成该天 08:00 和 20:00 两条记录
### 2. 状态自动更新
- 后端每30分钟运行一次定时任务
- 自动将已过期的 `upcoming` 状态更新为 `missed`
- 客户端无需手动更新状态
**示例**
- 08:00 的服药记录,到了 08:30 还未标记为已服用
- 定时任务会自动将其状态改为 `missed`
### 3. 推送提醒
- 后端每5分钟检查一次即将到来的服药时间15-20分钟后
- 自动发送推送通知提醒用户服药
- 客户端需要正确配置推送通知权限
### 4. 时区处理
- **重要**:所有时间字段使用 UTC 时间存储和传输
- 客户端需要:
1. 发送请求时:将本地时间转换为 UTC
2. 接收响应时:将 UTC 时间转换为本地时间显示
**示例代码JavaScript**:
```javascript
// 本地时间转 UTC
const localTime = new Date("2025-01-15 08:00");
const utcTime = localTime.toISOString(); // "2025-01-15T00:00:00.000Z" (假设时区为UTC+8)
// UTC 转本地时间
const utcTime = "2025-01-15T00:00:00.000Z";
const localTime = new Date(utcTime);
console.log(localTime.toLocaleString()); // "2025-01-15 08:00:00" (UTC+8)
```
---
## 最佳实践建议
### 1. 获取今日服药记录
推荐使用专用接口而非通用查询:
```http
✅ 推荐
GET /medication-records/today
❌ 不推荐
GET /medication-records?date=2025-01-15
```
### 2. 批量操作
如果需要更新多个记录,建议单独调用每个接口,后端暂不支持批量操作。
### 3. 错误处理
建议在客户端统一处理 API 错误:
```javascript
async function callApi(url, options) {
try {
const response = await fetch(url, options);
const data = await response.json();
if (data.code !== 200) {
// 显示错误提示
showError(data.message);
return null;
}
return data.data;
} catch (error) {
// 网络错误
showError("网络连接失败,请稍后重试");
return null;
}
}
```
### 4. 数据缓存
建议在客户端缓存以下数据以提升性能:
- 药物列表(有变更时刷新)
- 今日服药记录(实时更新)
- 统计数据(按天缓存)
### 5. 定时刷新
建议定时刷新今日服药记录以获取最新状态:
```javascript
// 每5分钟刷新一次今日记录
setInterval(
async () => {
const records = await getTodayRecords();
updateUI(records);
},
5 * 60 * 1000
);
```
---
## 完整使用流程示例
### 1. 创建药物并查看今日记录
```javascript
// Step 1: 创建药物
const medication = await createMedication({
name: "阿司匹林",
form: "pill",
dosageValue: 1,
dosageUnit: "片",
timesPerDay: 2,
medicationTimes: ["08:00", "20:00"],
repeatPattern: "daily",
startDate: new Date().toISOString(),
note: "饭后服用",
});
// Step 2: 获取今日服药记录(会自动生成)
const todayRecords = await getTodayRecords();
// Step 3: 显示记录列表
showRecordsList(todayRecords);
```
### 2. 标记服用并查看统计
```javascript
// Step 1: 标记为已服用
await takeMedication(recordId, {
actualTime: new Date().toISOString(),
});
// Step 2: 刷新今日记录
const todayRecords = await getTodayRecords();
// Step 3: 获取今日统计
const todayStats = await getDailyStats(formatDate(new Date()));
// Step 4: 显示完成率
showCompletionRate(todayStats.completionRate);
```
### 3. 查看历史统计
```javascript
// 获取最近7天的统计
const startDate = formatDate(daysAgo(7));
const endDate = formatDate(new Date());
const rangeStats = await getRangeStats(startDate, endDate);
// 绘制趋势图表
drawChart(rangeStats);
```
---
## 技术支持
如有疑问或需要帮助,请联系:
- **技术文档**: 本文档
- **API 基础URL**: https://your-domain.com/api
- **更新日期**: 2025-01-15
- **文档版本**: v1.0
---
## 更新日志
### v1.0 (2025-01-15)
- 初始版本发布
- 完整的药物管理功能
- 服药记录追踪
- 统计分析功能
- 自动状态更新
- 推送提醒支持