首次提交by MimoCode

This commit is contained in:
tang1219
2026-06-15 14:50:15 +08:00
parent 584b31da50
commit ead13f863c
166 changed files with 14908 additions and 1 deletions
+1249
View File
File diff suppressed because it is too large Load Diff
+728
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+834
View File
@@ -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 传递
**方式1Authorization Header(推荐)**
```
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
**方式2Query 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)
```
+530
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff