首次提交by MimoCode
This commit is contained in:
+1249
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,728 @@
|
||||
# 系统架构设计文档
|
||||
|
||||
## 1. 系统概述
|
||||
|
||||
家庭药品与应急物资管理系统(YaoXiang)是一个基于 Web 的家庭药品库存管理系统,支持 AI 自动录入、智能搜索、到期提醒等功能。系统采用前后端分离架构,支持 Docker 单容器部署。
|
||||
|
||||
## 2. 技术栈
|
||||
|
||||
### 前端
|
||||
- **框架**: React 18+
|
||||
- **语言**: TypeScript
|
||||
- **构建工具**: Vite
|
||||
- **UI 库**: Ant Design Mobile 5.x(移动端优化)
|
||||
- **状态管理**: Zustand
|
||||
- **路由**: React Router 6
|
||||
- **HTTP 客户端**: Axios
|
||||
- **PWA**: vite-plugin-pwa
|
||||
|
||||
### 后端
|
||||
- **框架**: FastAPI
|
||||
- **语言**: Python 3.11+
|
||||
- **ORM**: SQLAlchemy 2.0
|
||||
- **数据库迁移**: Alembic
|
||||
- **文件存储**: 本地文件系统
|
||||
- **任务队列**: 无(采用异步任务)
|
||||
|
||||
### 数据库
|
||||
- **主数据库**: SQLite(可升级 PostgreSQL)
|
||||
- **缓存**: 无(可选 Redis)
|
||||
|
||||
### AI Provider
|
||||
- **多模态模型**: OpenAI GPT-4o / Gemini / Claude
|
||||
- **文本模型**: DeepSeek / Ollama
|
||||
- **抽象层**: 统一 Provider 接口
|
||||
|
||||
### 部署
|
||||
- **容器化**: Docker
|
||||
- **编排**: Docker Compose
|
||||
- **反向代理**: Nginx(前端静态文件)
|
||||
|
||||
## 3. 系统架构图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 客户端层 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ 手机浏览器 │ │ 平板浏览器 │ │ PC 浏览器 │ │
|
||||
│ │ (PWA) │ │ (大屏模式) │ │ │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 前端应用层 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ React App │ │ PWA 配置 │ │ 状态管理 │ │
|
||||
│ │ (Vite) │ │ Service Worker │ │ (Zustand) │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ 页面组件 │ │ 业务组件 │ │ API 调用层 │ │
|
||||
│ │ (Router) │ │ (Ant Design)│ │ (Axios) │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ API 通信层 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ RESTful API │ │
|
||||
│ │ POST /api/auth/login │ │
|
||||
│ │ GET /api/medicines │ │
|
||||
│ │ POST /api/medicines │ │
|
||||
│ │ POST /api/medicines/recognize │ │
|
||||
│ │ ... │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 后端应用层 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ FastAPI │ │ 路由层 │ │ 中间件 │ │
|
||||
│ │ (Router) │ │ (APIRouter)│ │ (Auth) │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ 服务层 │ │ 数据访问层 │ │ 模型层 │ │
|
||||
│ │ (Service) │ │ (Repository)│ │ (Model) │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ AI Provider│ │ 通知系统 │ │ 文件存储 │ │
|
||||
│ │ (Abstract) │ │ (Provider) │ │ (Local) │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 数据存储层 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ SQLite │ │ 文件系统 │ │ 缓存 │ │
|
||||
│ │ (Database) │ │ (Uploads) │ │ (可选) │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 4. 模块划分
|
||||
|
||||
### 4.1 前端模块
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── src/
|
||||
│ ├── api/ # API 调用层
|
||||
│ │ ├── client.ts # Axios 实例配置
|
||||
│ │ ├── auth.ts # 认证相关 API
|
||||
│ │ ├── medicines.ts # 药品管理 API
|
||||
│ │ ├── categories.ts # 分类管理 API
|
||||
│ │ ├── batches.ts # 批次管理 API
|
||||
│ │ └── notifications.ts # 通知相关 API
|
||||
│ ├── components/ # 业务组件
|
||||
│ │ ├── MedicineCard/ # 药品卡片
|
||||
│ │ ├── BatchForm/ # 批次表单
|
||||
│ │ ├── CategoryTree/ # 分类树
|
||||
│ │ └── SearchBar/ # 搜索栏
|
||||
│ ├── pages/ # 页面组件
|
||||
│ │ ├── Home/ # 首页(库存概览)
|
||||
│ │ ├── MedicineList/ # 药品列表
|
||||
│ │ ├── MedicineDetail/ # 药品详情
|
||||
│ │ ├── AddMedicine/ # 添加药品
|
||||
│ │ ├── QuickDispense/ # 快速取药(大屏模式)
|
||||
│ │ ├── Scanner/ # AI 识别
|
||||
│ │ ├── Search/ # 搜索页面
|
||||
│ │ ├── Notifications/ # 通知中心
|
||||
│ │ ├── Settings/ # 设置页面
|
||||
│ │ └── Login/ # 登录页面
|
||||
│ ├── stores/ # 状态管理
|
||||
│ │ ├── authStore.ts # 认证状态
|
||||
│ │ ├── medicineStore.ts # 药品状态
|
||||
│ │ └── uiStore.ts # UI 状态
|
||||
│ ├── hooks/ # 自定义 Hooks
|
||||
│ │ ├── useAuth.ts # 认证 Hook
|
||||
│ │ ├── useMedicine.ts # 药品 Hook
|
||||
│ │ └── useCamera.ts # 摄像头 Hook
|
||||
│ ├── utils/ # 工具函数
|
||||
│ │ ├── date.ts # 日期处理
|
||||
│ │ ├── storage.ts # 本地存储
|
||||
│ │ └── validators.ts # 表单验证
|
||||
│ ├── types/ # TypeScript 类型
|
||||
│ │ ├── medicine.ts # 药品类型
|
||||
│ │ ├── batch.ts # 批次类型
|
||||
│ │ ├── user.ts # 用户类型
|
||||
│ │ └── api.ts # API 响应类型
|
||||
│ ├── styles/ # 样式文件
|
||||
│ │ ├── global.css # 全局样式
|
||||
│ │ └── variables.css # CSS 变量
|
||||
│ ├── App.tsx # 根组件
|
||||
│ ├── main.tsx # 入口文件
|
||||
│ └── router.tsx # 路由配置
|
||||
├── public/ # 静态资源
|
||||
├── index.html # HTML 模板
|
||||
├── vite.config.ts # Vite 配置
|
||||
├── tsconfig.json # TypeScript 配置
|
||||
└── package.json # 依赖配置
|
||||
```
|
||||
|
||||
### 4.2 后端模块
|
||||
|
||||
```
|
||||
backend/
|
||||
├── app/
|
||||
│ ├── api/ # 路由层
|
||||
│ │ ├── v1/ # API 版本
|
||||
│ │ │ ├── auth.py # 认证路由
|
||||
│ │ │ ├── medicines.py # 药品路由
|
||||
│ │ │ ├── batches.py # 批次路由
|
||||
│ │ │ ├── categories.py # 分类路由
|
||||
│ │ │ ├── search.py # 搜索路由
|
||||
│ │ │ ├── notifications.py # 通知路由
|
||||
│ │ │ ├── ai.py # AI 识别路由
|
||||
│ │ │ └── users.py # 用户管理路由
|
||||
│ │ └── router.py # 路由汇总
|
||||
│ ├── core/ # 核心配置
|
||||
│ │ ├── config.py # 配置管理
|
||||
│ │ ├── security.py # 安全工具
|
||||
│ │ └── deps.py # 依赖注入
|
||||
│ ├── models/ # 数据模型
|
||||
│ │ ├── medicine.py # 药品模型
|
||||
│ │ ├── batch.py # 批次模型
|
||||
│ │ ├── category.py # 分类模型
|
||||
│ │ ├── user.py # 用户模型
|
||||
│ │ ├── notification.py # 通知模型
|
||||
│ │ └── audit.py # 审计日志模型
|
||||
│ ├── schemas/ # Pydantic 模型
|
||||
│ │ ├── medicine.py # 药品 Schema
|
||||
│ │ ├── batch.py # 批次 Schema
|
||||
│ │ ├── category.py # 分类 Schema
|
||||
│ │ ├── user.py # 用户 Schema
|
||||
│ │ └── auth.py # 认证 Schema
|
||||
│ ├── services/ # 服务层
|
||||
│ │ ├── medicine.py # 药品服务
|
||||
│ │ ├── batch.py # 批次服务
|
||||
│ │ ├── category.py # 分类服务
|
||||
│ │ ├── user.py # 用户服务
|
||||
│ │ ├── auth.py # 认证服务
|
||||
│ │ ├── notification.py # 通知服务
|
||||
│ │ └── search.py # 搜索服务
|
||||
│ ├── repositories/ # 数据访问层
|
||||
│ │ ├── medicine.py # 药品仓库
|
||||
│ │ ├── batch.py # 批次仓库
|
||||
│ │ ├── category.py # 分类仓库
|
||||
│ │ └── user.py # 用户仓库
|
||||
│ ├── ai/ # AI Provider
|
||||
│ │ ├── provider.py # 抽象基类
|
||||
│ │ ├── openai.py # OpenAI 实现
|
||||
│ │ ├── gemini.py # Gemini 实现
|
||||
│ │ ├── claude.py # Claude 实现
|
||||
│ │ ├── deepseek.py # DeepSeek 实现
|
||||
│ │ ├── ollama.py # Ollama 实现
|
||||
│ │ └── manager.py # Provider 管理器
|
||||
│ ├── notifications/ # 通知系统
|
||||
│ │ ├── provider.py # 抽象基类
|
||||
│ │ ├── serverchan.py # Server酱
|
||||
│ │ ├── pushplus.py # PushPlus
|
||||
│ │ ├── bark.py # Bark
|
||||
│ │ ├── wechat.py # 企业微信
|
||||
│ │ ├── telegram.py # Telegram
|
||||
│ │ ├── email.py # 邮件
|
||||
│ │ └── manager.py # 通知管理器
|
||||
│ ├── storage/ # 文件存储
|
||||
│ │ ├── local.py # 本地存储
|
||||
│ │ └── manager.py # 存储管理器
|
||||
│ ├── tasks/ # 异步任务
|
||||
│ │ ├── expiry_check.py # 到期检查任务
|
||||
│ │ └── stock_check.py # 库存检查任务
|
||||
│ ├── database.py # 数据库连接
|
||||
│ └── main.py # 应用入口
|
||||
├── alembic/ # 数据库迁移
|
||||
│ ├── versions/
|
||||
│ └── env.py
|
||||
├── tests/ # 测试文件
|
||||
├── requirements.txt # 依赖配置
|
||||
├── alembic.ini # Alembic 配置
|
||||
├── Dockerfile # Docker 配置
|
||||
└── .env.example # 环境变量示例
|
||||
```
|
||||
|
||||
## 5. 数据流设计
|
||||
|
||||
### 5.1 AI 自动录入流程
|
||||
|
||||
```
|
||||
用户上传图片
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 图片预处理 │
|
||||
│ (压缩/格式化) │
|
||||
└─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 调用 Vision │
|
||||
│ Provider │
|
||||
└─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 返回识别结果 │
|
||||
│ (JSON) │
|
||||
└─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 用户确认/编辑 │
|
||||
└─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 保存到数据库 │
|
||||
│ + 保存图片 │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
### 5.2 快速取药流程
|
||||
|
||||
```
|
||||
用户选择药品
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 显示药品详情 │
|
||||
│ (可用批次列表) │
|
||||
└─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 用户选择批次 │
|
||||
│ 输入取药数量 │
|
||||
└─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 扣减库存 │
|
||||
│ 记录审计日志 │
|
||||
└─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 检查库存阈值 │
|
||||
│ 触发通知(可选) │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
### 5.3 到期提醒流程
|
||||
|
||||
```
|
||||
定时任务触发
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 查询即将过期 │
|
||||
│ 批次 (90/30/7天)│
|
||||
└─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 生成提醒内容 │
|
||||
└─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 调用通知系统 │
|
||||
│ 发送提醒 │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
## 6. AI Provider 抽象层设计
|
||||
|
||||
### 6.1 抽象接口
|
||||
|
||||
```python
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
class VisionResult(BaseModel):
|
||||
"""视觉识别结果"""
|
||||
generic_name: str
|
||||
brand_name: Optional[str]
|
||||
manufacturer: Optional[str]
|
||||
specification: Optional[str]
|
||||
|
||||
class DateResult(BaseModel):
|
||||
"""日期识别结果"""
|
||||
production_date: Optional[str]
|
||||
expiry_date: Optional[str]
|
||||
|
||||
class LeafletResult(BaseModel):
|
||||
"""说明书识别结果"""
|
||||
indications: str
|
||||
adult_dose: str
|
||||
child_dose: Optional[str]
|
||||
contraindications: str
|
||||
notes: Optional[str]
|
||||
|
||||
class VisionProvider(ABC):
|
||||
"""视觉模型提供者抽象基类"""
|
||||
|
||||
@abstractmethod
|
||||
async def recognize_medicine(self, image_bytes: bytes) -> VisionResult:
|
||||
"""识别药盒信息"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def recognize_dates(self, image_bytes: bytes) -> DateResult:
|
||||
"""识别日期信息"""
|
||||
pass
|
||||
|
||||
class TextProvider(ABC):
|
||||
"""文本模型提供者抽象基类"""
|
||||
|
||||
@abstractmethod
|
||||
async def summarize_leaflet(self, text: str) -> LeafletResult:
|
||||
"""总结说明书内容"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def natural_language_search(self, query: str, medicines: list) -> list:
|
||||
"""自然语言搜索"""
|
||||
pass
|
||||
```
|
||||
|
||||
### 6.2 Provider 管理器
|
||||
|
||||
```python
|
||||
class AIManager:
|
||||
"""AI Provider 管理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.vision_providers: dict[str, VisionProvider] = {}
|
||||
self.text_providers: dict[str, TextProvider] = {}
|
||||
|
||||
def register_vision_provider(self, name: str, provider: VisionProvider):
|
||||
"""注册视觉模型提供者"""
|
||||
self.vision_providers[name] = provider
|
||||
|
||||
def register_text_provider(self, name: str, provider: TextProvider):
|
||||
"""注册文本模型提供者"""
|
||||
self.text_providers[name] = provider
|
||||
|
||||
def get_vision_provider(self, name: str) -> VisionProvider:
|
||||
"""获取视觉模型提供者"""
|
||||
return self.vision_providers.get(name)
|
||||
|
||||
def get_text_provider(self, name: str) -> TextProvider:
|
||||
"""获取文本模型提供者"""
|
||||
return self.text_providers.get(name)
|
||||
```
|
||||
|
||||
## 7. 通知系统设计
|
||||
|
||||
### 7.1 通知 Provider 抽象
|
||||
|
||||
```python
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
class NotificationProvider(ABC):
|
||||
"""通知提供者抽象基类"""
|
||||
|
||||
@abstractmethod
|
||||
async def send(self, title: str, content: str) -> bool:
|
||||
"""发送通知"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def validate_config(self) -> bool:
|
||||
"""验证配置"""
|
||||
pass
|
||||
```
|
||||
|
||||
### 7.2 通知管理器
|
||||
|
||||
```python
|
||||
class NotificationManager:
|
||||
"""通知管理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.providers: list[NotificationProvider] = []
|
||||
|
||||
def add_provider(self, provider: NotificationProvider):
|
||||
"""添加通知提供者"""
|
||||
self.providers.append(provider)
|
||||
|
||||
async def send_notification(self, title: str, content: str):
|
||||
"""发送通知到所有提供者"""
|
||||
for provider in self.providers:
|
||||
try:
|
||||
await provider.send(title, content)
|
||||
except Exception as e:
|
||||
# 记录错误但不中断
|
||||
pass
|
||||
```
|
||||
|
||||
## 8. 认证授权设计
|
||||
|
||||
### 8.1 用户角色
|
||||
|
||||
- **admin**: 管理员,拥有所有权限
|
||||
- **user**: 普通用户,可查看、添加库存、取药
|
||||
- **readonly**: 只读用户,仅可查看
|
||||
|
||||
### 8.2 权限矩阵
|
||||
|
||||
| 功能 | admin | user | readonly |
|
||||
|------|-------|------|----------|
|
||||
| 查看药品 | ✓ | ✓ | ✓ |
|
||||
| 添加药品 | ✓ | ✓ | ✗ |
|
||||
| 修改药品 | ✓ | ✓ | ✗ |
|
||||
| 删除药品 | ✓ | ✗ | ✗ |
|
||||
| 取药 | ✓ | ✓ | ✗ |
|
||||
| 用户管理 | ✓ | ✗ | ✗ |
|
||||
| 系统设置 | ✓ | ✗ | ✗ |
|
||||
| 通知管理 | ✓ | ✓ | ✗ |
|
||||
|
||||
### 8.3 JWT 认证流程
|
||||
|
||||
```
|
||||
用户登录
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 验证用户名密码 │
|
||||
└─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 生成 JWT Token │
|
||||
│ (Access Token) │
|
||||
└─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 返回 Token │
|
||||
└─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 前端存储 Token │
|
||||
│ (localStorage) │
|
||||
└─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 后续请求携带 │
|
||||
│ Authorization │
|
||||
│ Header │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
## 9. 插件系统设计
|
||||
|
||||
### 9.1 插件接口
|
||||
|
||||
```python
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
class Plugin(ABC):
|
||||
"""插件抽象基类"""
|
||||
|
||||
@abstractmethod
|
||||
def get_name(self) -> str:
|
||||
"""获取插件名称"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_description(self) -> str:
|
||||
"""获取插件描述"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def initialize(self, app):
|
||||
"""初始化插件"""
|
||||
pass
|
||||
```
|
||||
|
||||
### 9.2 插件管理器
|
||||
|
||||
```python
|
||||
class PluginManager:
|
||||
"""插件管理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.plugins: dict[str, Plugin] = {}
|
||||
|
||||
def register_plugin(self, plugin: Plugin):
|
||||
"""注册插件"""
|
||||
name = plugin.get_name()
|
||||
self.plugins[name] = plugin
|
||||
|
||||
def get_plugin(self, name: str) -> Plugin:
|
||||
"""获取插件"""
|
||||
return self.plugins.get(name)
|
||||
|
||||
def list_plugins(self) -> list[str]:
|
||||
"""列出所有插件"""
|
||||
return list(self.plugins.keys())
|
||||
```
|
||||
|
||||
## 10. MCP 协议支持
|
||||
|
||||
### 10.1 MCP 工具定义
|
||||
|
||||
```python
|
||||
from mcp import Tool
|
||||
|
||||
# 查询库存工具
|
||||
query_inventory_tool = Tool(
|
||||
name="query_inventory",
|
||||
description="查询家庭药品库存",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"medicine_name": {
|
||||
"type": "string",
|
||||
"description": "药品名称"
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# 取药工具
|
||||
dispense_medicine_tool = Tool(
|
||||
name="dispense_medicine",
|
||||
description="取药操作",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"medicine_id": {
|
||||
"type": "integer",
|
||||
"description": "药品ID"
|
||||
},
|
||||
"quantity": {
|
||||
"type": "integer",
|
||||
"description": "取药数量"
|
||||
}
|
||||
},
|
||||
"required": ["medicine_id", "quantity"]
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## 11. 部署架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Docker 容器 │
|
||||
├─────────────────────────────────────────┤
|
||||
│ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ Nginx │ │ FastAPI │ │
|
||||
│ │ (静态文件) │ │ (后端) │ │
|
||||
│ │ :80 │ │ :8000 │ │
|
||||
│ └─────────────┘ └─────────────┘ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ SQLite │ │ 文件存储 │ │
|
||||
│ │ (数据库) │ │ (图片) │ │
|
||||
│ └─────────────┘ └─────────────┘ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 12. 环境变量配置
|
||||
|
||||
```bash
|
||||
# 数据库配置
|
||||
DATABASE_URL=sqlite:///./data/yaoxiang.db
|
||||
|
||||
# AI Provider 配置
|
||||
AI_PROVIDER=openai
|
||||
OPENAI_API_KEY=sk-xxx
|
||||
OPENAI_MODEL=gpt-4o
|
||||
|
||||
# 通知配置
|
||||
NOTIFICATION_PROVIDERS=serverchan,pushplus
|
||||
SERVERCHAN_KEY=xxx
|
||||
PUSHPLUS_TOKEN=xxx
|
||||
|
||||
# 安全配置
|
||||
JWT_SECRET_KEY=xxx
|
||||
JWT_ALGORITHM=HS256
|
||||
JWT_EXPIRATION_HOURS=24
|
||||
|
||||
# 文件存储配置
|
||||
UPLOAD_DIR=./data/uploads
|
||||
MAX_UPLOAD_SIZE=10485760 # 10MB
|
||||
|
||||
# 应用配置
|
||||
APP_NAME=药箱
|
||||
APP_VERSION=1.0.0
|
||||
DEBUG=false
|
||||
```
|
||||
|
||||
## 13. 开发规范
|
||||
|
||||
### 13.1 代码风格
|
||||
|
||||
- **Python**: 遵循 PEP 8,使用 Black 格式化
|
||||
- **TypeScript**: 遵循 ESLint 规则,使用 Prettier 格式化
|
||||
- **Git**: 使用 Conventional Commits 规范
|
||||
|
||||
### 13.2 分支管理
|
||||
|
||||
- `main`: 生产分支
|
||||
- `develop`: 开发分支
|
||||
- `feature/*`: 功能分支
|
||||
- `bugfix/*`: 修复分支
|
||||
- `release/*`: 发布分支
|
||||
|
||||
### 13.3 提交规范
|
||||
|
||||
```
|
||||
feat: 新功能
|
||||
fix: 修复 bug
|
||||
docs: 文档更新
|
||||
style: 代码格式调整
|
||||
refactor: 重构
|
||||
test: 测试相关
|
||||
chore: 构建/工具相关
|
||||
```
|
||||
|
||||
## 14. 性能优化
|
||||
|
||||
### 14.1 前端优化
|
||||
|
||||
- 路由懒加载
|
||||
- 图片懒加载
|
||||
- 虚拟列表(长列表优化)
|
||||
- Service Worker 缓存
|
||||
|
||||
### 14.2 后端优化
|
||||
|
||||
- 数据库连接池
|
||||
- 查询优化(索引、分页)
|
||||
- 异步处理耗时任务
|
||||
- 响应缓存
|
||||
|
||||
## 15. 安全设计
|
||||
|
||||
### 15.1 认证安全
|
||||
|
||||
- 密码使用 bcrypt 加密存储
|
||||
- JWT Token 定期轮换
|
||||
- 登录失败次数限制
|
||||
|
||||
### 15.2 数据安全
|
||||
|
||||
- 敏感配置使用环境变量
|
||||
- 文件上传类型验证
|
||||
- SQL 注入防护(ORM)
|
||||
- XSS 防护(前端)
|
||||
|
||||
### 15.3 传输安全
|
||||
|
||||
- 支持 HTTPS
|
||||
- CORS 配置
|
||||
- 请求限流
|
||||
+1439
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,834 @@
|
||||
# 通信协议文档
|
||||
|
||||
## 1. 概述
|
||||
|
||||
本文档定义了药箱系统前后端之间的通信协议,包括数据格式、错误处理、文件上传等内容。
|
||||
|
||||
## 2. 通信架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 前端应用 │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ API 调用层 │ │
|
||||
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
||||
│ │ │ Axios │ │ 请求拦截器 │ │ 响应拦截器 │ │ │
|
||||
│ │ │ Client │ │ (Auth) │ │ (Error) │ │ │
|
||||
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼ HTTP/HTTPS
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 后端服务 │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ FastAPI │ │
|
||||
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
||||
│ │ │ 路由层 │ │ 中间件 │ │ 依赖注入 │ │ │
|
||||
│ │ │ (Router) │ │ (Auth) │ │ (Deps) │ │ │
|
||||
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 3. 数据格式规范
|
||||
|
||||
### 3.1 请求格式
|
||||
|
||||
**Content-Type:**
|
||||
- JSON: `application/json`
|
||||
- 文件上传: `multipart/form-data`
|
||||
- 表单: `application/x-www-form-urlencoded`
|
||||
|
||||
**请求头:**
|
||||
```
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer <token>
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
### 3.2 响应格式
|
||||
|
||||
**成功响应(单个对象):**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"name": "布洛芬"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**成功响应(列表):**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"data": [...],
|
||||
"total": 100,
|
||||
"page": 1,
|
||||
"page_size": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**成功响应(无数据):**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "删除成功"
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应:**
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "请求参数错误",
|
||||
"detail": "name 字段不能为空"
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 HTTP 状态码
|
||||
|
||||
| 状态码 | 说明 | 使用场景 |
|
||||
|--------|------|----------|
|
||||
| 200 | OK | 请求成功 |
|
||||
| 201 | Created | 创建成功 |
|
||||
| 204 | No Content | 删除成功(无响应体) |
|
||||
| 400 | Bad Request | 请求参数错误 |
|
||||
| 401 | Unauthorized | 未认证或令牌过期 |
|
||||
| 403 | Forbidden | 权限不足 |
|
||||
| 404 | Not Found | 资源不存在 |
|
||||
| 409 | Conflict | 资源冲突(如用户名已存在) |
|
||||
| 413 | Payload Too Large | 文件过大 |
|
||||
| 415 | Unsupported Media Type | 不支持的文件类型 |
|
||||
| 422 | Unprocessable Entity | 请求体格式正确但语义错误 |
|
||||
| 500 | Internal Server Error | 服务器内部错误 |
|
||||
|
||||
### 3.4 业务状态码
|
||||
|
||||
| 状态码 | 说明 |
|
||||
|--------|------|
|
||||
| 1000 | 成功 |
|
||||
| 2000 | 参数错误 |
|
||||
| 3000 | 认证错误 |
|
||||
| 4000 | 权限错误 |
|
||||
| 5000 | 业务逻辑错误 |
|
||||
| 6000 | 外部服务错误 |
|
||||
| 9000 | 系统错误 |
|
||||
|
||||
## 4. 认证协议
|
||||
|
||||
### 4.1 JWT Token 格式
|
||||
|
||||
**Header:**
|
||||
```json
|
||||
{
|
||||
"alg": "HS256",
|
||||
"typ": "JWT"
|
||||
}
|
||||
```
|
||||
|
||||
**Payload:**
|
||||
```json
|
||||
{
|
||||
"sub": "1",
|
||||
"username": "admin",
|
||||
"role": "admin",
|
||||
"iat": 1704067200,
|
||||
"exp": 1704153600
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Token 传递
|
||||
|
||||
**方式1:Authorization Header(推荐)**
|
||||
```
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
```
|
||||
|
||||
**方式2:Query Parameter(不推荐,仅用于特殊情况)**
|
||||
```
|
||||
GET /api/v1/medicines?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
```
|
||||
|
||||
### 4.3 Token 过期处理
|
||||
|
||||
**前端处理流程:**
|
||||
```
|
||||
1. 发送请求
|
||||
2. 收到 401 响应
|
||||
3. 尝试刷新 Token(如果有 Refresh Token)
|
||||
4. 刷新失败 → 跳转到登录页
|
||||
5. 刷新成功 → 重新发送原请求
|
||||
```
|
||||
|
||||
**前端代码示例:**
|
||||
```typescript
|
||||
// api/client.ts
|
||||
client.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true;
|
||||
|
||||
try {
|
||||
// 尝试刷新 Token
|
||||
const refreshToken = useAuthStore.getState().refreshToken;
|
||||
if (refreshToken) {
|
||||
const response = await axios.post('/api/v1/auth/refresh', {
|
||||
refresh_token: refreshToken
|
||||
});
|
||||
|
||||
const { access_token } = response.data.data;
|
||||
useAuthStore.getState().setToken(access_token);
|
||||
|
||||
originalRequest.headers.Authorization = `Bearer ${access_token}`;
|
||||
return client(originalRequest);
|
||||
}
|
||||
} catch (refreshError) {
|
||||
// 刷新失败,跳转到登录页
|
||||
useAuthStore.getState().logout();
|
||||
window.location.href = '/login';
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## 5. 文件上传协议
|
||||
|
||||
### 5.1 单文件上传
|
||||
|
||||
**请求格式:**
|
||||
```http
|
||||
POST /api/v1/upload/image HTTP/1.1
|
||||
Host: localhost:8000
|
||||
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||
|
||||
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||
Content-Disposition: form-data; name="file"; filename="medicine.jpg"
|
||||
Content-Type: image/jpeg
|
||||
|
||||
<二进制数据>
|
||||
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||
Content-Disposition: form-data; name="category"
|
||||
|
||||
medicine
|
||||
------WebKitFormBoundary7MA4YWxkTrZu0gW--
|
||||
```
|
||||
|
||||
**前端实现:**
|
||||
```typescript
|
||||
const uploadImage = async (file: File, category: string) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('category', category);
|
||||
|
||||
const response = await client.post('/upload/image', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
```
|
||||
|
||||
### 5.2 多文件上传
|
||||
|
||||
**请求格式:**
|
||||
```http
|
||||
POST /api/v1/upload/images HTTP/1.1
|
||||
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||
|
||||
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||
Content-Disposition: form-data; name="files"; filename="image1.jpg"
|
||||
Content-Type: image/jpeg
|
||||
|
||||
<二进制数据>
|
||||
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||
Content-Disposition: form-data; name="files"; filename="image2.jpg"
|
||||
Content-Type: image/jpeg
|
||||
|
||||
<二进制数据>
|
||||
------WebKitFormBoundary7MA4YWxkTrZu0gW--
|
||||
```
|
||||
|
||||
### 5.3 文件大小限制
|
||||
|
||||
- 图片文件:最大 10MB
|
||||
- 说明书图片:最大 20MB
|
||||
|
||||
**前端检查:**
|
||||
```typescript
|
||||
const validateFileSize = (file: File, maxSize: number): boolean => {
|
||||
return file.size <= maxSize;
|
||||
};
|
||||
|
||||
const validateImageFile = (file: File): boolean => {
|
||||
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
|
||||
|
||||
if (!validateFileSize(file, maxSize)) {
|
||||
Toast.show({ content: '文件大小不能超过10MB' });
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
Toast.show({ content: '只支持 JPG、PNG、WebP 格式' });
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
```
|
||||
|
||||
### 5.4 图片压缩
|
||||
|
||||
**前端压缩实现:**
|
||||
```typescript
|
||||
const compressImage = async (
|
||||
file: File,
|
||||
maxWidth: number = 1920,
|
||||
quality: number = 0.8
|
||||
): Promise<File> => {
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
let width = img.width;
|
||||
let height = img.height;
|
||||
|
||||
if (width > maxWidth) {
|
||||
height = (height * maxWidth) / width;
|
||||
width = maxWidth;
|
||||
}
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx?.drawImage(img, 0, 0, width, height);
|
||||
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
const compressedFile = new File([blob!], file.name, {
|
||||
type: 'image/jpeg',
|
||||
lastModified: Date.now()
|
||||
});
|
||||
resolve(compressedFile);
|
||||
},
|
||||
'image/jpeg',
|
||||
quality
|
||||
);
|
||||
};
|
||||
img.src = e.target?.result as string;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
## 6. 分页协议
|
||||
|
||||
### 6.1 请求分页参数
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| page | integer | 1 | 页码(从1开始) |
|
||||
| page_size | integer | 20 | 每页数量(最大100) |
|
||||
|
||||
**示例:**
|
||||
```
|
||||
GET /api/v1/medicines?page=2&page_size=10
|
||||
```
|
||||
|
||||
### 6.2 响应分页数据
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"data": [...],
|
||||
"total": 100,
|
||||
"page": 2,
|
||||
"page_size": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 前端分页实现
|
||||
|
||||
```typescript
|
||||
// 使用 antd-mobile 的 InfiniteScroll
|
||||
const MedicineList: React.FC = () => {
|
||||
const [page, setPage] = useState(1);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [medicines, setMedicines] = useState<Medicine[]>([]);
|
||||
|
||||
const loadMore = async () => {
|
||||
try {
|
||||
const response = await medicineApi.list({ page, page_size: 20 });
|
||||
const newData = response.data.data;
|
||||
|
||||
setMedicines(prev => [...prev, ...newData]);
|
||||
setPage(prev => prev + 1);
|
||||
setHasMore(newData.length === 20);
|
||||
} catch (error) {
|
||||
console.error('加载失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<InfiniteScroll loadMore={loadMore} hasMore={hasMore}>
|
||||
{medicines.map(medicine => (
|
||||
<MedicineCard key={medicine.id} medicine={medicine} />
|
||||
))}
|
||||
</InfiniteScroll>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## 7. 错误处理协议
|
||||
|
||||
### 7.1 错误响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "请求参数错误",
|
||||
"detail": {
|
||||
"field": "name",
|
||||
"error": "不能为空"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 前端错误处理
|
||||
|
||||
```typescript
|
||||
// api/client.ts
|
||||
client.interceptors.response.use(
|
||||
(response) => {
|
||||
return response.data;
|
||||
},
|
||||
(error) => {
|
||||
const { response } = error;
|
||||
|
||||
if (response) {
|
||||
switch (response.status) {
|
||||
case 400:
|
||||
Toast.show({ content: response.data.message || '请求参数错误' });
|
||||
break;
|
||||
case 401:
|
||||
useAuthStore.getState().logout();
|
||||
window.location.href = '/login';
|
||||
break;
|
||||
case 403:
|
||||
Toast.show({ content: '权限不足' });
|
||||
break;
|
||||
case 404:
|
||||
Toast.show({ content: '资源不存在' });
|
||||
break;
|
||||
case 500:
|
||||
Toast.show({ content: '服务器错误,请稍后重试' });
|
||||
break;
|
||||
default:
|
||||
Toast.show({ content: '请求失败' });
|
||||
}
|
||||
} else {
|
||||
Toast.show({ content: '网络错误,请检查网络连接' });
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 7.3 表单验证错误
|
||||
|
||||
**错误响应格式:**
|
||||
```json
|
||||
{
|
||||
"code": 422,
|
||||
"message": "请求体格式正确但语义错误",
|
||||
"detail": [
|
||||
{
|
||||
"field": "name",
|
||||
"message": "字段不能为空",
|
||||
"type": "value_error.missing"
|
||||
},
|
||||
{
|
||||
"field": "expiry_date",
|
||||
"message": "日期格式错误",
|
||||
"type": "value_error.date"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**前端处理:**
|
||||
```typescript
|
||||
const handleFormError = (error: any) => {
|
||||
if (error.response?.status === 422) {
|
||||
const details = error.response.data.detail;
|
||||
if (Array.isArray(details)) {
|
||||
details.forEach((item: any) => {
|
||||
form.setFields([
|
||||
{
|
||||
name: item.field,
|
||||
errors: [item.message]
|
||||
}
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## 8. 搜索协议
|
||||
|
||||
### 8.1 关键词搜索
|
||||
|
||||
**请求:**
|
||||
```
|
||||
GET /api/v1/search?q=发烧&type=indications
|
||||
```
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "布洛芬",
|
||||
"match_type": "indications",
|
||||
"match_text": "用于退热",
|
||||
"relevance_score": 0.95
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 自然语言搜索
|
||||
|
||||
**请求:**
|
||||
```json
|
||||
POST /api/v1/search/natural
|
||||
{
|
||||
"query": "孩子发烧了应该吃什么药?"
|
||||
}
|
||||
```
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"results": [
|
||||
{
|
||||
"medicine_id": 1,
|
||||
"name": "布洛芬",
|
||||
"reason": "适用于退热,可缓解发热症状",
|
||||
"match_score": 0.95,
|
||||
"recommendation": "建议在医生指导下使用"
|
||||
}
|
||||
],
|
||||
"ai_response": "根据您的描述,家中有布洛芬可用于退热。请注意按照说明书用量使用,如果症状持续请就医。"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 9. 实时更新协议
|
||||
|
||||
### 9.1 轮询机制
|
||||
|
||||
**库存变化轮询:**
|
||||
```typescript
|
||||
const useInventoryPolling = (interval: number = 30000) => {
|
||||
const { fetchMedicines } = useMedicineStore();
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
fetchMedicines();
|
||||
}, interval);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [interval]);
|
||||
};
|
||||
```
|
||||
|
||||
### 9.2 通知轮询
|
||||
|
||||
```typescript
|
||||
const useNotificationPolling = (interval: number = 60000) => {
|
||||
const { fetchNotifications } = useNotificationStore();
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
fetchNotifications({ is_read: false });
|
||||
}, interval);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [interval]);
|
||||
};
|
||||
```
|
||||
|
||||
## 10. 缓存协议
|
||||
|
||||
### 10.1 前端缓存策略
|
||||
|
||||
**localStorage 缓存:**
|
||||
```typescript
|
||||
const CACHE_KEYS = {
|
||||
AUTH_TOKEN: 'auth_token',
|
||||
USER_INFO: 'user_info',
|
||||
SETTINGS: 'app_settings'
|
||||
};
|
||||
|
||||
const cache = {
|
||||
get: (key: string) => {
|
||||
const value = localStorage.getItem(key);
|
||||
return value ? JSON.parse(value) : null;
|
||||
},
|
||||
set: (key: string, value: any) => {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
},
|
||||
remove: (key: string) => {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Session Storage 缓存:**
|
||||
```typescript
|
||||
const sessionCache = {
|
||||
get: (key: string) => {
|
||||
const value = sessionStorage.getItem(key);
|
||||
return value ? JSON.parse(value) : null;
|
||||
},
|
||||
set: (key: string, value: any) => {
|
||||
sessionStorage.setItem(key, JSON.stringify(value));
|
||||
},
|
||||
remove: (key: string) => {
|
||||
sessionStorage.removeItem(key);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 10.2 HTTP 缓存头
|
||||
|
||||
**后端响应头:**
|
||||
```python
|
||||
@router.get("/medicines")
|
||||
async def list_medicines(
|
||||
# ...
|
||||
response: Response
|
||||
):
|
||||
# 设置缓存头
|
||||
response.headers["Cache-Control"] = "private, max-age=60"
|
||||
response.headers["ETag"] = generate_etag(data)
|
||||
|
||||
return data
|
||||
```
|
||||
|
||||
**前端缓存处理:**
|
||||
```typescript
|
||||
const fetchWithCache = async (url: string, options?: RequestInit) => {
|
||||
const cacheKey = `cache_${url}`;
|
||||
const cached = sessionCache.get(cacheKey);
|
||||
|
||||
if (cached && Date.now() - cached.timestamp < 60000) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
const response = await fetch(url, options);
|
||||
const data = await response.json();
|
||||
|
||||
sessionCache.set(cacheKey, {
|
||||
data,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
```
|
||||
|
||||
## 11. WebSocket 协议(可选)
|
||||
|
||||
### 11.1 连接建立
|
||||
|
||||
```typescript
|
||||
const useWebSocket = (url: string) => {
|
||||
const [socket, setSocket] = useState<WebSocket | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const ws = new WebSocket(url);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket 连接已建立');
|
||||
setSocket(ws);
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
handleMessage(data);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket 连接已关闭');
|
||||
setSocket(null);
|
||||
};
|
||||
|
||||
return () => {
|
||||
ws.close();
|
||||
};
|
||||
}, [url]);
|
||||
|
||||
return socket;
|
||||
};
|
||||
```
|
||||
|
||||
### 11.2 消息格式
|
||||
|
||||
**客户端发送:**
|
||||
```json
|
||||
{
|
||||
"type": "subscribe",
|
||||
"channel": "inventory_updates"
|
||||
}
|
||||
```
|
||||
|
||||
**服务端推送:**
|
||||
```json
|
||||
{
|
||||
"type": "inventory_update",
|
||||
"data": {
|
||||
"medicine_id": 1,
|
||||
"medicine_name": "布洛芬",
|
||||
"old_quantity": 30,
|
||||
"new_quantity": 25,
|
||||
"action": "dispense",
|
||||
"user": "admin",
|
||||
"timestamp": "2024-01-01T12:00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 12. API 版本控制
|
||||
|
||||
### 12.1 URL 路径版本
|
||||
|
||||
```
|
||||
/api/v1/medicines
|
||||
/api/v2/medicines
|
||||
```
|
||||
|
||||
### 12.2 请求头版本
|
||||
|
||||
```
|
||||
Accept: application/vnd.yaoxiang.v1+json
|
||||
```
|
||||
|
||||
### 12.3 版本迁移策略
|
||||
|
||||
```python
|
||||
# 旧版本路由(v1)
|
||||
@router_v1.get("/medicines")
|
||||
async def list_medicines_v1():
|
||||
# v1 逻辑
|
||||
pass
|
||||
|
||||
# 新版本路由(v2)
|
||||
@router_v2.get("/medicines")
|
||||
async def list_medicines_v2():
|
||||
# v2 逻辑
|
||||
pass
|
||||
```
|
||||
|
||||
## 13. 安全协议
|
||||
|
||||
### 13.1 CORS 配置
|
||||
|
||||
```python
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=[
|
||||
"http://localhost:5173", # 开发环境
|
||||
"http://localhost:3000", # 生产环境
|
||||
"https://your-domain.com" # 域名
|
||||
],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
```
|
||||
|
||||
### 13.2 请求限流
|
||||
|
||||
```python
|
||||
from fastapi import Request, HTTPException
|
||||
from collections import defaultdict
|
||||
import time
|
||||
|
||||
class RateLimiter:
|
||||
def __init__(self, max_requests: int = 100, window_seconds: int = 60):
|
||||
self.max_requests = max_requests
|
||||
self.window_seconds = window_seconds
|
||||
self.requests = defaultdict(list)
|
||||
|
||||
def check(self, client_ip: str):
|
||||
now = time.time()
|
||||
window_start = now - self.window_seconds
|
||||
|
||||
# 清理过期记录
|
||||
self.requests[client_ip] = [
|
||||
req_time for req_time in self.requests[client_ip]
|
||||
if req_time > window_start
|
||||
]
|
||||
|
||||
if len(self.requests[client_ip]) >= self.max_requests:
|
||||
raise HTTPException(status_code=429, detail="请求过于频繁")
|
||||
|
||||
self.requests[client_ip].append(now)
|
||||
|
||||
limiter = RateLimiter()
|
||||
|
||||
@app.middleware("http")
|
||||
async def rate_limit_middleware(request: Request, call_next):
|
||||
client_ip = request.client.host
|
||||
limiter.check(client_ip)
|
||||
response = await call_next(request)
|
||||
return response
|
||||
```
|
||||
|
||||
### 13.3 输入验证
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, Field, validator
|
||||
|
||||
class MedicineCreate(BaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=200)
|
||||
expiry_date: str = Field(..., pattern=r'^\d{4}-\d{2}-\d{2}$')
|
||||
|
||||
@validator('name')
|
||||
def validate_name(cls, v):
|
||||
# 防止 XSS
|
||||
import html
|
||||
return html.escape(v)
|
||||
```
|
||||
@@ -0,0 +1,530 @@
|
||||
# 数据库设计文档
|
||||
|
||||
## 1. 数据库概述
|
||||
|
||||
系统使用 SQLite 作为主数据库,支持未来升级到 PostgreSQL。数据库设计遵循第三范式,确保数据一致性和查询效率。
|
||||
|
||||
## 2. ER 图
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐
|
||||
│ users │ │ categories │
|
||||
├─────────────────┤ ├─────────────────┤
|
||||
│ id (PK) │ │ id (PK) │
|
||||
│ username │ │ name │
|
||||
│ password_hash │ │ parent_id (FK) │
|
||||
│ role │ │ level │
|
||||
│ display_name │ │ icon │
|
||||
│ email │ │ sort_order │
|
||||
│ notification_ │ │ created_at │
|
||||
│ level │ │ updated_at │
|
||||
│ is_active │ └─────────────────┘
|
||||
│ created_at │ │
|
||||
│ updated_at │ │
|
||||
└─────────────────┘ │
|
||||
│ │
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ medicines │
|
||||
├─────────────────────────────────────────┤
|
||||
│ id (PK) │
|
||||
│ name │
|
||||
│ generic_name │
|
||||
│ brand_name │
|
||||
│ manufacturer │
|
||||
│ specification │
|
||||
│ category_id (FK) │
|
||||
│ description │
|
||||
│ indications │
|
||||
│ adult_dose │
|
||||
│ child_dose │
|
||||
│ contraindications │
|
||||
│ notes │
|
||||
│ image_front_path │
|
||||
│ image_expiry_path │
|
||||
│ image_leaflet_paths (JSON) │
|
||||
│ expiry_grace_days (默认0,最大60) │
|
||||
│ created_by (FK → users) │
|
||||
│ created_at │
|
||||
│ updated_at │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐ ┌─────────────────┐
|
||||
│ batches │ │ audit_logs │
|
||||
├─────────────────┤ ├─────────────────┤
|
||||
│ id (PK) │ │ id (PK) │
|
||||
│ medicine_id (FK)│ │ medicine_id (FK)│
|
||||
│ batch_no │ │ batch_id (FK) │
|
||||
│ production_date │ │ user_id (FK) │
|
||||
│ expiry_date │ │ action │
|
||||
│ quantity │ │ quantity_change │
|
||||
│ location │ │ quantity_after │
|
||||
│ is_expired │ │ remark │
|
||||
│ created_at │ │ created_at │
|
||||
│ updated_at │ └─────────────────┘
|
||||
└─────────────────┘
|
||||
│
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐ ┌─────────────────┐
|
||||
│notifications │ │ settings │
|
||||
├─────────────────┤ ├─────────────────┤
|
||||
│ id (PK) │ │ id (PK) │
|
||||
│ type │ │ key │
|
||||
│ title │ │ value │
|
||||
│ content │ │ description │
|
||||
│ is_read │ │ updated_at │
|
||||
│ user_id (FK) │ └─────────────────┘
|
||||
│ created_at │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
## 3. 表结构定义
|
||||
|
||||
### 3.1 users 表(用户表)
|
||||
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(20) NOT NULL DEFAULT 'user' CHECK(role IN ('admin', 'user', 'readonly')),
|
||||
display_name VARCHAR(100),
|
||||
email VARCHAR(100),
|
||||
notification_level VARCHAR(20) DEFAULT 'normal' CHECK(notification_level IN ('none', 'low', 'normal', 'high')),
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX idx_users_username ON users(username);
|
||||
CREATE INDEX idx_users_role ON users(role);
|
||||
```
|
||||
|
||||
**字段说明:**
|
||||
|
||||
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|------|------|------|--------|------|
|
||||
| id | INTEGER | 是 | 自增 | 主键 |
|
||||
| username | VARCHAR(50) | 是 | - | 用户名,唯一 |
|
||||
| password_hash | VARCHAR(255) | 是 | - | 密码哈希值 |
|
||||
| role | VARCHAR(20) | 是 | 'user' | 角色:admin/user/readonly |
|
||||
| display_name | VARCHAR(100) | 否 | NULL | 显示名称 |
|
||||
| email | VARCHAR(100) | 否 | NULL | 邮箱 |
|
||||
| notification_level | VARCHAR(20) | 否 | 'normal' | 通知等级 |
|
||||
| is_active | BOOLEAN | 否 | 1 | 是否启用 |
|
||||
| created_at | TIMESTAMP | 否 | CURRENT_TIMESTAMP | 创建时间 |
|
||||
| updated_at | TIMESTAMP | 否 | CURRENT_TIMESTAMP | 更新时间 |
|
||||
|
||||
### 3.2 categories 表(分类表)
|
||||
|
||||
```sql
|
||||
CREATE TABLE categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
parent_id INTEGER,
|
||||
level INTEGER NOT NULL DEFAULT 1 CHECK(level IN (1, 2)),
|
||||
icon VARCHAR(50),
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (parent_id) REFERENCES categories(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX idx_categories_parent_id ON categories(parent_id);
|
||||
CREATE INDEX idx_categories_level ON categories(level);
|
||||
```
|
||||
|
||||
**字段说明:**
|
||||
|
||||
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|------|------|------|--------|------|
|
||||
| id | INTEGER | 是 | 自增 | 主键 |
|
||||
| name | VARCHAR(100) | 是 | - | 分类名称 |
|
||||
| parent_id | INTEGER | 否 | NULL | 父分类ID |
|
||||
| level | INTEGER | 是 | 1 | 分类层级:1或2 |
|
||||
| icon | VARCHAR(50) | 否 | NULL | 图标 |
|
||||
| sort_order | INTEGER | 否 | 0 | 排序顺序 |
|
||||
| created_at | TIMESTAMP | 否 | CURRENT_TIMESTAMP | 创建时间 |
|
||||
| updated_at | TIMESTAMP | 否 | CURRENT_TIMESTAMP | 更新时间 |
|
||||
|
||||
**预设分类数据:**
|
||||
|
||||
```sql
|
||||
-- 一级分类
|
||||
INSERT INTO categories (name, level, icon, sort_order) VALUES
|
||||
('药品', 1, 'medicine', 1),
|
||||
('医疗器械', 1, 'medical', 2),
|
||||
('应急用品', 1, 'emergency', 3),
|
||||
('消耗品', 1, 'consumable', 4);
|
||||
|
||||
-- 二级分类 - 药品
|
||||
INSERT INTO categories (name, parent_id, level, sort_order) VALUES
|
||||
('感冒药', 1, 2, 1),
|
||||
('退烧药', 1, 2, 2),
|
||||
('止泻药', 1, 2, 3),
|
||||
('消炎药', 1, 2, 4),
|
||||
('外用药', 1, 2, 5);
|
||||
|
||||
-- 二级分类 - 医疗器械
|
||||
INSERT INTO categories (name, parent_id, level, sort_order) VALUES
|
||||
('血压计', 2, 2, 1),
|
||||
('血糖仪', 2, 2, 2),
|
||||
('体温计', 2, 2, 3);
|
||||
|
||||
-- 二级分类 - 应急用品
|
||||
INSERT INTO categories (name, parent_id, level, sort_order) VALUES
|
||||
('创可贴', 3, 2, 1),
|
||||
('绷带', 3, 2, 2),
|
||||
('止血带', 3, 2, 3);
|
||||
|
||||
-- 二级分类 - 消耗品
|
||||
INSERT INTO categories (name, parent_id, level, sort_order) VALUES
|
||||
('酒精棉片', 4, 2, 1),
|
||||
('N95', 4, 2, 2),
|
||||
('医用手套', 4, 2, 3);
|
||||
```
|
||||
|
||||
### 3.3 medicines 表(药品表)
|
||||
|
||||
```sql
|
||||
CREATE TABLE medicines (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
generic_name VARCHAR(200),
|
||||
brand_name VARCHAR(200),
|
||||
manufacturer VARCHAR(200),
|
||||
specification VARCHAR(200),
|
||||
category_id INTEGER,
|
||||
description TEXT,
|
||||
indications TEXT,
|
||||
adult_dose TEXT,
|
||||
child_dose TEXT,
|
||||
contraindications TEXT,
|
||||
notes TEXT,
|
||||
image_front_path VARCHAR(500),
|
||||
image_expiry_path VARCHAR(500),
|
||||
image_leaflet_paths JSON,
|
||||
expiry_grace_days INTEGER DEFAULT 0 CHECK(expiry_grace_days >= 0 AND expiry_grace_days <= 60),
|
||||
created_by INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX idx_medicines_name ON medicines(name);
|
||||
CREATE INDEX idx_medicines_category_id ON medicines(category_id);
|
||||
CREATE INDEX idx_medicines_created_by ON medicines(created_by);
|
||||
CREATE INDEX idx_medicines_generic_name ON medicines(generic_name);
|
||||
```
|
||||
|
||||
**字段说明:**
|
||||
|
||||
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|------|------|------|--------|------|
|
||||
| id | INTEGER | 是 | 自增 | 主键 |
|
||||
| name | VARCHAR(200) | 是 | - | 药品名称 |
|
||||
| generic_name | VARCHAR(200) | 否 | NULL | 通用名称 |
|
||||
| brand_name | VARCHAR(200) | 否 | NULL | 商品名称 |
|
||||
| manufacturer | VARCHAR(200) | 否 | NULL | 生产厂家 |
|
||||
| specification | VARCHAR(200) | 否 | NULL | 规格 |
|
||||
| category_id | INTEGER | 否 | NULL | 分类ID |
|
||||
| description | TEXT | 否 | NULL | 描述 |
|
||||
| indications | TEXT | 否 | NULL | 适应症(用于搜索) |
|
||||
| adult_dose | TEXT | 否 | NULL | 成人用量 |
|
||||
| child_dose | TEXT | 否 | NULL | 儿童用量 |
|
||||
| contraindications | TEXT | 否 | NULL | 禁忌 |
|
||||
| notes | TEXT | 否 | NULL | 注意事项 |
|
||||
| image_front_path | VARCHAR(500) | 否 | NULL | 药盒正面图片路径 |
|
||||
| image_expiry_path | VARCHAR(500) | 否 | NULL | 有效期图片路径 |
|
||||
| image_leaflet_paths | JSON | 否 | NULL | 说明书图片路径数组 |
|
||||
| expiry_grace_days | INTEGER | 否 | 0 | 有效期宽限天数(最大60天) |
|
||||
| created_by | INTEGER | 否 | NULL | 创建者用户ID |
|
||||
| created_at | TIMESTAMP | 否 | CURRENT_TIMESTAMP | 创建时间 |
|
||||
| updated_at | TIMESTAMP | 否 | CURRENT_TIMESTAMP | 更新时间 |
|
||||
|
||||
### 3.4 batches 表(批次表)
|
||||
|
||||
```sql
|
||||
CREATE TABLE batches (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
medicine_id INTEGER NOT NULL,
|
||||
batch_no VARCHAR(100),
|
||||
production_date DATE,
|
||||
expiry_date DATE NOT NULL,
|
||||
quantity INTEGER NOT NULL DEFAULT 0 CHECK(quantity >= 0),
|
||||
location VARCHAR(200),
|
||||
is_expired BOOLEAN DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (medicine_id) REFERENCES medicines(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX idx_batches_medicine_id ON batches(medicine_id);
|
||||
CREATE INDEX idx_batches_expiry_date ON batches(expiry_date);
|
||||
CREATE INDEX idx_batches_is_expired ON batches(is_expired);
|
||||
```
|
||||
|
||||
**字段说明:**
|
||||
|
||||
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|------|------|------|--------|------|
|
||||
| id | INTEGER | 是 | 自增 | 主键 |
|
||||
| medicine_id | INTEGER | 是 | - | 药品ID |
|
||||
| batch_no | VARCHAR(100) | 否 | NULL | 批次号 |
|
||||
| production_date | DATE | 否 | NULL | 生产日期 |
|
||||
| expiry_date | DATE | 是 | - | 过期日期 |
|
||||
| quantity | INTEGER | 是 | 0 | 库存数量 |
|
||||
| location | VARCHAR(200) | 否 | NULL | 存放位置 |
|
||||
| is_expired | BOOLEAN | 否 | 0 | 是否已过期 |
|
||||
| created_at | TIMESTAMP | 否 | CURRENT_TIMESTAMP | 创建时间 |
|
||||
| updated_at | TIMESTAMP | 否 | CURRENT_TIMESTAMP | 更新时间 |
|
||||
|
||||
### 3.5 audit_logs 表(审计日志表)
|
||||
|
||||
```sql
|
||||
CREATE TABLE audit_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
medicine_id INTEGER NOT NULL,
|
||||
batch_id INTEGER,
|
||||
user_id INTEGER,
|
||||
action VARCHAR(50) NOT NULL CHECK(action IN ('add_stock', 'dispense', 'adjust', 'delete', 'modify')),
|
||||
quantity_change INTEGER NOT NULL,
|
||||
quantity_after INTEGER NOT NULL,
|
||||
remark TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (medicine_id) REFERENCES medicines(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (batch_id) REFERENCES batches(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX idx_audit_logs_medicine_id ON audit_logs(medicine_id);
|
||||
CREATE INDEX idx_audit_logs_user_id ON audit_logs(user_id);
|
||||
CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at);
|
||||
CREATE INDEX idx_audit_logs_action ON audit_logs(action);
|
||||
```
|
||||
|
||||
**字段说明:**
|
||||
|
||||
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|------|------|------|--------|------|
|
||||
| id | INTEGER | 是 | 自增 | 主键 |
|
||||
| medicine_id | INTEGER | 是 | - | 药品ID |
|
||||
| batch_id | INTEGER | 否 | NULL | 批次ID |
|
||||
| user_id | INTEGER | 否 | NULL | 操作用户ID |
|
||||
| action | VARCHAR(50) | 是 | - | 操作类型 |
|
||||
| quantity_change | INTEGER | 是 | - | 数量变化(正数增加,负数减少) |
|
||||
| quantity_after | INTEGER | 是 | - | 操作后数量 |
|
||||
| remark | TEXT | 否 | NULL | 备注 |
|
||||
| created_at | TIMESTAMP | 否 | CURRENT_TIMESTAMP | 创建时间 |
|
||||
|
||||
### 3.6 notifications 表(通知表)
|
||||
|
||||
```sql
|
||||
CREATE TABLE notifications (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
type VARCHAR(50) NOT NULL CHECK(type IN ('expiry_warning', 'low_stock', 'system')),
|
||||
title VARCHAR(200) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
is_read BOOLEAN DEFAULT 0,
|
||||
user_id INTEGER,
|
||||
related_id INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX idx_notifications_user_id ON notifications(user_id);
|
||||
CREATE INDEX idx_notifications_type ON notifications(type);
|
||||
CREATE INDEX idx_notifications_is_read ON notifications(is_read);
|
||||
CREATE INDEX idx_notifications_created_at ON notifications(created_at);
|
||||
```
|
||||
|
||||
**字段说明:**
|
||||
|
||||
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|------|------|------|--------|------|
|
||||
| id | INTEGER | 是 | 自增 | 主键 |
|
||||
| type | VARCHAR(50) | 是 | - | 通知类型 |
|
||||
| title | VARCHAR(200) | 是 | - | 通知标题 |
|
||||
| content | TEXT | 是 | - | 通知内容 |
|
||||
| is_read | BOOLEAN | 否 | 0 | 是否已读 |
|
||||
| user_id | INTEGER | 否 | NULL | 用户ID |
|
||||
| related_id | INTEGER | 否 | NULL | 关联ID(药品/批次) |
|
||||
| created_at | TIMESTAMP | 否 | CURRENT_TIMESTAMP | 创建时间 |
|
||||
|
||||
### 3.7 settings 表(系统设置表)
|
||||
|
||||
```sql
|
||||
CREATE TABLE settings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
key VARCHAR(100) NOT NULL UNIQUE,
|
||||
value TEXT,
|
||||
description VARCHAR(500),
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX idx_settings_key ON settings(key);
|
||||
```
|
||||
|
||||
**字段说明:**
|
||||
|
||||
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|------|------|------|--------|------|
|
||||
| id | INTEGER | 是 | 自增 | 主键 |
|
||||
| key | VARCHAR(100) | 是 | - | 设置键名,唯一 |
|
||||
| value | TEXT | 否 | NULL | 设置值 |
|
||||
| description | VARCHAR(500) | 否 | NULL | 设置描述 |
|
||||
| updated_at | TIMESTAMP | 否 | CURRENT_TIMESTAMP | 更新时间 |
|
||||
|
||||
**预设设置数据:**
|
||||
|
||||
```sql
|
||||
INSERT INTO settings (key, value, description) VALUES
|
||||
('ai_provider', 'openai', 'AI 服务提供者'),
|
||||
('openai_api_key', '', 'OpenAI API Key'),
|
||||
('openai_model', 'gpt-4o', 'OpenAI 模型'),
|
||||
('notification_providers', '[]', '启用的通知提供者列表'),
|
||||
('expiry_warning_days', '90,30,7', '到期提醒天数(逗号分隔)'),
|
||||
('low_stock_threshold', '5', '低库存阈值'),
|
||||
('max_upload_size', '10485760', '最大上传文件大小(字节)'),
|
||||
('expiry_grace_days_max', '60', '有效期宽限最大天数');
|
||||
```
|
||||
|
||||
## 4. 关系说明
|
||||
|
||||
### 4.1 一对多关系
|
||||
|
||||
- **users → medicines**: 一个用户可以创建多个药品
|
||||
- **users → audit_logs**: 一个用户可以有多条审计日志
|
||||
- **users → notifications**: 一个用户可以有多条通知
|
||||
- **categories → medicines**: 一个分类可以包含多个药品
|
||||
- **categories → categories**: 一个分类可以有多个子分类
|
||||
- **medicines → batches**: 一个药品可以有多个批次
|
||||
- **medicines → audit_logs**: 一个药品可以有多条审计日志
|
||||
|
||||
### 4.2 级联操作
|
||||
|
||||
- 删除用户:相关药品、审计日志、通知保留(created_by/set NULL)
|
||||
- 删除分类:相关药品的 category_id 设为 NULL
|
||||
- 删除药品:相关批次、审计日志级联删除
|
||||
- 删除批次:相关审计日志的 batch_id 设为 NULL
|
||||
|
||||
## 5. 视图设计
|
||||
|
||||
### 5.1 药品库存视图
|
||||
|
||||
```sql
|
||||
CREATE VIEW v_medicine_stock AS
|
||||
SELECT
|
||||
m.id,
|
||||
m.name,
|
||||
m.generic_name,
|
||||
m.brand_name,
|
||||
m.specification,
|
||||
c.name as category_name,
|
||||
COALESCE(SUM(b.quantity), 0) as total_quantity,
|
||||
MIN(b.expiry_date) as nearest_expiry_date,
|
||||
COUNT(b.id) as batch_count
|
||||
FROM medicines m
|
||||
LEFT JOIN batches b ON m.id = b.medicine_id AND b.is_expired = 0
|
||||
LEFT JOIN categories c ON m.category_id = c.id
|
||||
GROUP BY m.id;
|
||||
```
|
||||
|
||||
### 5.2 即将过期药品视图
|
||||
|
||||
```sql
|
||||
CREATE VIEW v_expiring_medicines AS
|
||||
SELECT
|
||||
m.id,
|
||||
m.name,
|
||||
m.expiry_grace_days,
|
||||
b.id as batch_id,
|
||||
b.batch_no,
|
||||
b.expiry_date,
|
||||
b.quantity,
|
||||
julianday(b.expiry_date) - julianday('now') as days_until_expiry
|
||||
FROM medicines m
|
||||
JOIN batches b ON m.id = b.medicine_id
|
||||
WHERE b.is_expired = 0
|
||||
AND b.expiry_date <= date('now', '+' || (90 + m.expiry_grace_days) || ' days');
|
||||
```
|
||||
|
||||
## 6. 索引策略
|
||||
|
||||
### 6.1 主要索引
|
||||
|
||||
| 表名 | 索引名 | 字段 | 用途 |
|
||||
|------|--------|------|------|
|
||||
| users | idx_users_username | username | 用户登录查询 |
|
||||
| medicines | idx_medicines_name | name | 药品搜索 |
|
||||
| medicines | idx_medicines_category_id | category_id | 分类筛选 |
|
||||
| batches | idx_batches_medicine_id | medicine_id | 药品批次查询 |
|
||||
| batches | idx_batches_expiry_date | expiry_date | 到期提醒查询 |
|
||||
| audit_logs | idx_audit_logs_created_at | created_at | 审计日志时间查询 |
|
||||
|
||||
### 6.2 复合索引
|
||||
|
||||
```sql
|
||||
-- 药品搜索复合索引
|
||||
CREATE INDEX idx_medicines_search ON medicines(name, generic_name, brand_name);
|
||||
|
||||
-- 批次库存查询复合索引
|
||||
CREATE INDEX idx_batches_stock ON batches(medicine_id, is_expired, expiry_date);
|
||||
```
|
||||
|
||||
## 7. 数据迁移策略
|
||||
|
||||
### 7.1 使用 Alembic
|
||||
|
||||
```bash
|
||||
# 初始化 Alembic
|
||||
alembic init alembic
|
||||
|
||||
# 生成迁移脚本
|
||||
alembic revision --autogenerate -m "initial"
|
||||
|
||||
# 执行迁移
|
||||
alembic upgrade head
|
||||
|
||||
# 回滚迁移
|
||||
alembic downgrade -1
|
||||
```
|
||||
|
||||
### 7.2 版本控制
|
||||
|
||||
- 每次数据库变更都生成迁移脚本
|
||||
- 迁移脚本存储在 `alembic/versions/` 目录
|
||||
- 支持向前和向后迁移
|
||||
|
||||
## 8. 数据备份策略
|
||||
|
||||
### 8.1 备份方案
|
||||
|
||||
```bash
|
||||
# SQLite 备份
|
||||
cp data/yaoxiang.db data/yaoxiang_backup_$(date +%Y%m%d).db
|
||||
|
||||
# 或使用 sqlite3 命令
|
||||
sqlite3 data/yaoxiang.db ".backup 'data/yaoxiang_backup_$(date +%Y%m%d).db'"
|
||||
```
|
||||
|
||||
### 8.2 自动备份
|
||||
|
||||
可通过 cron 任务定期备份:
|
||||
|
||||
```bash
|
||||
# 每天凌晨2点备份
|
||||
0 2 * * * /path/to/backup_script.sh
|
||||
```
|
||||
+1246
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user