Files
2026-06-15 14:50:15 +08:00

531 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 数据库设计文档
## 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
```