- 新增Redis缓存功能,用于优化仪表板统计数据查询性能 - 添加Redis连接池管理和自动降级机制,无连接时自动跳过缓存 - 配置Dockerfile支持前后端容器化部署 - 添加docker-compose编排文件实现一键部署 - 更新README文档包含新的部署方式和架构说明 - 在入库出库操作后清除相关缓存以保证数据一致性
422 lines
15 KiB
Markdown
422 lines
15 KiB
Markdown
# 药品管理系统 (MedicineManagerSystem)
|
||
|
||
基于 **Vue 3 + Spring Boot** 的全栈药品进销存管理系统,支持采购入库、库存管理、销售/领用出库、效期预警、供应商管理及 RBAC 权限控制。
|
||
|
||
---
|
||
|
||
## 技术栈
|
||
|
||
| 类别 | 技术 | 版本 |
|
||
|------|------|------|
|
||
| 前端框架 | Vue 3 (Composition API) | 3.5+ |
|
||
| 构建工具 | Vite | 8.x |
|
||
| UI 组件库 | Element Plus | 2.x |
|
||
| 路由 / 状态 | Vue Router 4 / Pinia 2 | 4.x / 2.x |
|
||
| HTTP / 样式 | Axios 1.x / Sass | 1.x |
|
||
| 后端框架 | Spring Boot (Web + Security + Validation) | 4.1.0 |
|
||
| JDK | Java 21 | LTS |
|
||
| ORM | MyBatis-Plus | 3.5.11 |
|
||
| 安全认证 | Spring Security + JWT (jjwt 0.13.0) | — |
|
||
| 数据库 | MySQL 8.0 (JDBC + HikariCP) | — |
|
||
| 缓存 | Redis 7 + Jedis(可选,无连接自动跳过) | — |
|
||
| 工具库 | Hutool 5.8 / Lombok | — |
|
||
| 构建 / 部署 | Maven 3.9+ / Docker + Docker Compose | — |
|
||
|
||
---
|
||
|
||
## 系统架构
|
||
|
||
```mermaid
|
||
graph TB
|
||
Client["🖥️ 浏览器<br/>Vue 3 + Element Plus SPA"]
|
||
|
||
subgraph Backend["☕ Spring Boot 4.1.0"]
|
||
Security["🔐 Spring Security + JWT Filter<br/>无状态认证"]
|
||
Controller["📡 Controller Layer<br/>REST API · 参数校验 · 统一响应格式 Result-T"]
|
||
Service["⚙️ Service Layer<br/>业务逻辑 · 事务管理"]
|
||
Mapper["🗄️ MyBatis-Plus Mapper<br/>分页 · 逻辑删除 · 自动填充"]
|
||
end
|
||
|
||
DB["🐬 MySQL 8.0<br/>InnoDB · utf8mb4"]
|
||
Cache["⚡ Redis 7<br/>可选 · 自动降级"]
|
||
|
||
Client -->|"HTTPS + JWT Token"| Security
|
||
Service --> Cache
|
||
Security --> Controller
|
||
Controller --> Service
|
||
Service --> Mapper
|
||
Mapper -->|"JDBC"| DB
|
||
```
|
||
|
||
### 请求响应流程
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant F as 🖥️ Vue 前端
|
||
participant A as Axios 拦截器
|
||
participant SF as JWT Filter
|
||
participant C as Controller
|
||
participant SV as Service
|
||
participant M as Mapper
|
||
participant DB as MySQL
|
||
|
||
F->>A: 发起 API 请求
|
||
A->>A: 自动注入 JWT Token<br/>到 Authorization Header
|
||
A->>SF: HTTP Request (Bearer Token)
|
||
|
||
alt Token 有效
|
||
SF->>C: 放行请求
|
||
C->>C: @Valid 参数校验
|
||
C->>SV: 调用业务方法
|
||
SV->>M: 调用数据访问
|
||
M->>DB: 执行 SQL
|
||
DB-->>M: 返回结果集
|
||
M-->>SV: 返回实体对象
|
||
SV-->>C: 返回处理结果
|
||
C-->>A: "Result<T> JSON"
|
||
A->>A: 响应拦截器处理<br/>(统一错误提示 / Token 刷新)
|
||
A-->>F: 展示数据
|
||
else Token 过期或无效
|
||
SF-->>A: 401 Unauthorized
|
||
A->>A: 尝试 RefreshToken
|
||
alt 刷新失败
|
||
A->>F: 跳转登录页
|
||
end
|
||
else 权限不足
|
||
SF-->>A: 403 Forbidden
|
||
A-->>F: 提示"权限不足"
|
||
end
|
||
```
|
||
|
||
---
|
||
|
||
### RBAC 权限模型
|
||
|
||
```mermaid
|
||
graph LR
|
||
U["👤 User"] -->|N:M| UR["user_role"]
|
||
UR -->|N:M| R["🔑 Role"]
|
||
R -->|N:M| RM["role_menu"]
|
||
RM -->|N:M| M["📋 Menu"]
|
||
|
||
M --> D["📁 目录 type=1<br/>侧边栏分组"]
|
||
M --> P["📄 菜单 type=2<br/>页面路由"]
|
||
M --> B["🔘 按钮 type=3<br/>permission_code"]
|
||
|
||
subgraph 三层权限
|
||
L1["路由守卫"] -.-> P
|
||
L2["v-permission 指令"] -.-> B
|
||
L3["@PreAuthorize"] -.-> B
|
||
end
|
||
```
|
||
|
||
---
|
||
|
||
### 认证流程
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant U as 👤 用户
|
||
participant F as 🖥️ Vue 前端
|
||
participant B as ☕ Spring Boot
|
||
participant DB as 🐬 MySQL
|
||
|
||
rect rgb(240, 248, 255)
|
||
Note over U,DB: === 登录阶段 ===
|
||
U->>F: 输入用户名 + 密码
|
||
F->>B: POST /api/auth/login<br/>{username, password}
|
||
B->>DB: 查询用户信息
|
||
DB-->>B: 返回用户实体
|
||
B->>B: BCrypt.matches(password, hash)
|
||
alt 验证成功
|
||
B->>B: 生成 AccessToken (有效期 24h)
|
||
B->>B: 生成 RefreshToken (有效期 7天)
|
||
B-->>F: { token, refreshToken, userInfo, menus }
|
||
F->>F: localStorage.setItem('token', ...)
|
||
F-->>U: 跳转首页,渲染菜单
|
||
else 验证失败
|
||
B-->>F: 401 "用户名或密码错误"
|
||
F-->>U: ElMessage.error 提示
|
||
end
|
||
end
|
||
|
||
rect rgb(255, 248, 240)
|
||
Note over U,DB: === 正常请求阶段 ===
|
||
F->>B: GET /api/medicines<br/>Authorization: Bearer <AccessToken>
|
||
B->>B: JwtFilter 解析 Token<br/>提取 userId + permissions
|
||
B->>B: SecurityContextHolder 设置认证信息
|
||
B-->>F: Result<Page<Medicine> >
|
||
end
|
||
|
||
rect rgb(255, 240, 240)
|
||
Note over U,DB: === Token 过期刷新 ===
|
||
F->>B: POST /api/auth/refresh<br/>{ refreshToken }
|
||
B->>B: 校验 RefreshToken 有效性
|
||
alt RefreshToken 有效
|
||
B->>B: 签发新 AccessToken
|
||
B-->>F: { token: "newToken" }
|
||
F->>F: 更新 localStorage
|
||
else RefreshToken 过期
|
||
B-->>F: 401
|
||
F->>F: 清空 Token,跳转登录页
|
||
end
|
||
end
|
||
```
|
||
|
||
---
|
||
|
||
## 数据库设计
|
||
|
||
```mermaid
|
||
erDiagram
|
||
medicine_category ||--o{ medicine : "分类"
|
||
medicine ||--o{ stock : "库存批次"
|
||
medicine ||--o{ stock_in_detail : ""
|
||
medicine ||--o{ stock_out_detail : ""
|
||
warehouse ||--o{ stock : ""
|
||
warehouse ||--o{ stock_in_record : ""
|
||
warehouse ||--o{ stock_out_record : ""
|
||
supplier ||--o{ stock_in_record : ""
|
||
customer ||--o{ stock_out_record : ""
|
||
stock_in_record ||--o{ stock_in_detail : ""
|
||
stock_out_record ||--o{ stock_out_detail : ""
|
||
user ||--o{ stock_in_record : "操作人"
|
||
user ||--o{ stock_out_record : "操作人"
|
||
user ||--o{ user_role : ""
|
||
role ||--o{ user_role : ""
|
||
role ||--o{ role_menu : ""
|
||
menu ||--o{ role_menu : ""
|
||
menu ||--o{ menu : "父子菜单"
|
||
|
||
medicine_category { bigint id PK "分类ID" varchar name "名称" varchar code UK "编码" }
|
||
medicine { bigint id PK "药品ID" varchar name "通用名称" varchar specification "规格" varchar manufacturer "厂家" varchar approval_number UK "批准文号" decimal retail_price "零售价" decimal wholesale_price "批发价" int shelf_life "有效期(月)" int warning_stock "预警阈值" }
|
||
supplier { bigint id PK "供应商ID" varchar name "名称" varchar contact_person "联系人" varchar phone "电话" tinyint status "状态" }
|
||
customer { bigint id PK "客户ID" varchar name "名称" varchar phone "电话" varchar type "类型" }
|
||
warehouse { bigint id PK "仓库ID" varchar name "名称" varchar code UK "编码" }
|
||
stock { bigint id PK "库存ID" bigint medicine_id FK "药品" bigint warehouse_id FK "仓库" varchar batch_no "批号" int quantity "数量" decimal cost_price "成本价" date expiry_date "有效期至" }
|
||
stock_in_record { bigint id PK "入库单ID" varchar record_no UK "单号" bigint supplier_id FK "供应商" decimal total_amount "总金额" date in_date "入库日期" tinyint status "0待审1已入库2已取消" }
|
||
stock_in_detail { bigint id PK bigint record_id FK bigint medicine_id FK varchar batch_no int quantity decimal cost_price "进价" decimal total_price "小计" date expiry_date }
|
||
stock_out_record { bigint id PK "出库单ID" varchar record_no UK "单号" bigint customer_id FK "客户" decimal total_amount "总金额" date out_date "出库日期" tinyint out_type "1销售2领用3报损4退货" tinyint status "0待审1已出库2已取消" }
|
||
stock_out_detail { bigint id PK bigint record_id FK bigint medicine_id FK varchar batch_no int quantity decimal unit_price "单价" decimal total_price "小计" }
|
||
user { bigint id PK "用户ID" varchar username UK "用户名" varchar password "BCrypt" varchar real_name "姓名" varchar phone "手机" tinyint status "状态" }
|
||
role { bigint id PK "角色ID" varchar name "名称" varchar code UK "编码" }
|
||
user_role { bigint id PK bigint user_id FK bigint role_id FK }
|
||
menu { bigint id PK "菜单ID" bigint parent_id "父ID" varchar name "名称" tinyint type "1目录2菜单3按钮" varchar path "路由" varchar icon "图标" varchar permission_code UK "权限编码" }
|
||
role_menu { bigint id PK bigint role_id FK bigint menu_id FK }
|
||
```
|
||
|
||
---
|
||
|
||
## 功能模块
|
||
|
||
| 模块 | 功能 |
|
||
|------|------|
|
||
| 🔐 认证 | 登录 / 注册 / JWT 刷新 / 退出 |
|
||
| 📊 仪表盘 | 4 项统计卡片 · **效期预警**(<30天红 · 30-90天橙) · 最近出入库 |
|
||
| 💊 药品管理 | CRUD · 分类树管理 · 按名称/批准文号/分类搜索 · 分页 |
|
||
| 📦 库存管理 | 批次跟踪 · 效期颜色标记 · 库存盘点 |
|
||
| 📥 入库管理 | 新建入库单(供应商+动态明细行) · **审核通过自动增加库存** · 驳回 |
|
||
| 📤 出库管理 | 新建出库单(客户+库存批次选择) · **审核通过 FIFO 扣减库存** · 驳回 |
|
||
| 🏢 供应商 | CRUD · 启用/停用开关 |
|
||
| 👥 客户 | CRUD · 类型(个人/医院/药店/诊所) |
|
||
| 👤 用户管理 | CRUD · 角色分配 |
|
||
| 🔑 角色管理 | CRUD · 权限树分配 |
|
||
| 📋 菜单管理 | 菜单树 CRUD · 三级(目录/菜单/按钮) |
|
||
|
||
---
|
||
|
||
## 核心业务流程
|
||
|
||
### 入库审核
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant F as 前端
|
||
participant B as 后端
|
||
participant DB as MySQL
|
||
|
||
Note over F,DB: 1. 创建入库单
|
||
F->>B: POST /api/stock-in<br/>{ supplierId, details: [...] }
|
||
B->>DB: INSERT stock_in_record (status=0 待审核)
|
||
B->>DB: INSERT stock_in_detail (明细)
|
||
B-->>F: 入库单已创建
|
||
|
||
Note over F,DB: 2. 审核通过
|
||
F->>B: PUT /api/stock-in/{id}/status<br/>{ status: 1 }
|
||
B->>DB: 查同药品+同批号库存
|
||
alt 库存已存在
|
||
B->>DB: UPDATE stock SET quantity += N
|
||
else 新批次
|
||
B->>DB: INSERT stock (新批次)
|
||
end
|
||
B->>DB: UPDATE status=1
|
||
B-->>F: 审核通过 · 库存已更新
|
||
```
|
||
|
||
### 出库审核
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant F as 前端
|
||
participant B as 后端
|
||
participant DB as MySQL
|
||
|
||
Note over F,DB: 1. 创建出库单
|
||
F->>B: POST /api/stock-out<br/>{ customerId, details: [...] }
|
||
B->>DB: INSERT stock_out_record (status=0 待审核)
|
||
|
||
Note over F,DB: 2. 审核通过
|
||
F->>B: PUT /api/stock-out/{id}/status<br/>{ status: 1 }
|
||
B->>DB: 查可用库存 (FIFO 按效期升序)
|
||
alt 库存充足
|
||
B->>DB: UPDATE stock SET quantity -= N
|
||
B->>DB: UPDATE status=1
|
||
B-->>F: 审核通过 · 库存已扣减
|
||
else 库存不足
|
||
B-->>F: 错误: 库存不足,需要 X,可用 Y
|
||
end
|
||
```
|
||
|
||
---
|
||
|
||
## 快速开始
|
||
|
||
### 方式一:Docker Compose(推荐)
|
||
|
||
```bash
|
||
# 1. 配置环境变量
|
||
cp .env.example .env
|
||
# 编辑 .env 设置 DB_PASSWORD 和 JWT_SECRET
|
||
|
||
# 2. 一键启动(MySQL + Redis + 后端 + 前端)
|
||
docker-compose up -d
|
||
|
||
# 3. 访问
|
||
open http://localhost
|
||
```
|
||
|
||
> 首次启动 MySQL 会自动执行 `init.sql` 建表和数据。Redis 不可用时后端自动降级跳过缓存。
|
||
|
||
### 方式二:手动启动
|
||
|
||
**环境要求**
|
||
- JDK 21+ · Node.js 18+ · MySQL 8.0+ · Maven 3.9+
|
||
|
||
#### 1. 初始化数据库
|
||
|
||
```bash
|
||
mysql -u root -p < backend/src/main/resources/database/init.sql
|
||
```
|
||
|
||
### 2. 配置后端环境变量
|
||
|
||
```shell
|
||
cp backend/.env.example backend/.env
|
||
```
|
||
|
||
在 `backend/.env` 中填写:
|
||
|
||
```properties
|
||
DB_HOST=localhost
|
||
DB_PORT=3306
|
||
DB_NAME=medicine_manager
|
||
DB_USERNAME=root
|
||
DB_PASSWORD=你的数据库密码
|
||
JWT_SECRET=你的256位Base64密钥
|
||
```
|
||
|
||
### 3. 启动后端
|
||
|
||
```bash
|
||
cd backend
|
||
./mvnw spring-boot:run -DskipTests
|
||
```
|
||
|
||
> 后端运行在 `http://localhost:8080`,启动时会自动加载 `.env`
|
||
|
||
### 4. 启动前端
|
||
|
||
```bash
|
||
cd frontend
|
||
npm install
|
||
npm run dev
|
||
```
|
||
|
||
> 前端运行在 `http://localhost:5173`,API 请求自动代理到后端
|
||
|
||
### 5. 登录
|
||
|
||
| 用户名 | 密码 | 角色 | 权限范围 |
|
||
|--------|------|------|----------|
|
||
| `admin` | `admin123` | 超级管理员 | 全部 |
|
||
| `warehouse01` | `admin123` | 仓库管理员 | 出入库 + 库存 + 供应商客户查看 |
|
||
| `staff01` | `admin123` | 普通员工 | 仪表盘 + 只读查看 |
|
||
|
||
---
|
||
|
||
## 项目结构
|
||
|
||
```
|
||
MedicineManagerSystem/
|
||
├── README.md
|
||
├── .env.example
|
||
├── docker-compose.yml # Docker 编排
|
||
├── projectDesigning.md # 详细架构设计文档
|
||
│
|
||
├── frontend/ # Vue 3 SPA
|
||
│ ├── Dockerfile
|
||
│ ├── nginx.conf
|
||
│ └── src/
|
||
│ ├── api/ (11) # API 模块
|
||
│ ├── directives/ (1) # v-permission
|
||
│ ├── layout/ (1) # MainLayout
|
||
│ ├── router/ (1) # 路由 + 动态菜单
|
||
│ ├── stores/ (2) # Pinia
|
||
│ ├── styles/ (1) # SCSS
|
||
│ ├── utils/ (2) # Axios · Redis 缓存
|
||
│ └── views/ (16) # 页面组件
|
||
│
|
||
└── backend/ # Spring Boot
|
||
├── Dockerfile
|
||
└── src/main/
|
||
├── java/com/kronecker/backend/
|
||
│ ├── common/ (4) # Result · 异常
|
||
│ ├── config/ (3) # Security · MP · CORS
|
||
│ ├── controller/ (12) # REST API
|
||
│ ├── dto/ (4) # 请求对象
|
||
│ ├── entity/ (15) # 数据库实体
|
||
│ ├── mapper/ (15) # MP Mapper
|
||
│ ├── security/ (3) # JWT
|
||
│ ├── service/ (23) # 业务逻辑
|
||
│ └── utils/ (1) # Redis 缓存
|
||
└── resources/
|
||
├── application.yml
|
||
└── database/init.sql # 建表 + 种子数据
|
||
```
|
||
|
||
---
|
||
|
||
## API 端点
|
||
|
||
| 模块 | 方法 | 路径 | 权限 |
|
||
|------|------|------|------|
|
||
| 认证 | `POST` | `/api/auth/login` | 公开 |
|
||
| 认证 | `POST` | `/api/auth/register` | 公开 |
|
||
| 认证 | `POST` | `/api/auth/refresh` | 公开 |
|
||
| 认证 | `GET` | `/api/auth/userinfo` | 登录 |
|
||
| 仪表盘 | `GET` | `/api/dashboard/statistics` | 登录 |
|
||
| 仪表盘 | `GET` | `/api/dashboard/expiry-warning` | 登录 |
|
||
| 药品 | `GET POST PUT DELETE` | `/api/medicines[/{id}]` | medicine:* |
|
||
| 分类 | `GET POST PUT DELETE` | `/api/categories[/{id}]` | category:* |
|
||
| 供应商 | `GET POST PUT DELETE` | `/api/suppliers[/{id}]` | supplier:* |
|
||
| 客户 | `GET POST PUT DELETE` | `/api/customers[/{id}]` | customer:* |
|
||
| 库存 | `GET` | `/api/stocks` | stock:list |
|
||
| 入库 | `GET POST` | `/api/stock-in` | stock-in:* |
|
||
| 入库 | `PUT` | `/api/stock-in/{id}/status` | stock-in:audit |
|
||
| 出库 | `GET POST` | `/api/stock-out` | stock-out:* |
|
||
| 出库 | `PUT` | `/api/stock-out/{id}/status` | stock-out:audit |
|
||
| 用户 | `GET POST PUT DELETE` | `/api/users[/{id}]` | user:* |
|
||
| 角色 | `GET POST PUT DELETE` | `/api/roles[/{id}]` | role:* |
|
||
| 菜单 | `GET POST PUT DELETE` | `/api/menus[/{id}]` | menu:* |
|
||
| 菜单 | `GET` | `/api/menus/tree` | 登录 |
|