commit eec443575693648309c31ce57e1a5e54c485bc3c Author: KiriAky 107 Date: Thu Jan 15 11:06:37 2026 +0800 新建文件夹 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..1fe95f6 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,11 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +/target/ +/.idea +/📊 医药管理系统 - 项目文档.md diff --git a/.idea/PharmaManagementSystem.iml b/.idea/PharmaManagementSystem.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/PharmaManagementSystem.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..22160a9 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..a812b50 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/pharma-backend.iml b/pharma-backend.iml new file mode 100644 index 0000000..91fb618 --- /dev/null +++ b/pharma-backend.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/pharma-backend/pom.xml b/pharma-backend/pom.xml new file mode 100644 index 0000000..6e9c634 --- /dev/null +++ b/pharma-backend/pom.xml @@ -0,0 +1,140 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.1.5 + + + + com.pharma + pharma-management + 1.0.0 + pharma-management + 医药管理系统 + + + 17 + 3.5.3.1 + 0.11.5 + 2.0.38 + 5.8.21 + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-aop + + + + + mysql + mysql-connector-java + 8.0.33 + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus.version} + + + com.alibaba + druid-spring-boot-starter + 1.2.18 + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jjwt.version} + runtime + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} + runtime + + + + + org.projectlombok + lombok + true + + + cn.hutool + hutool-all + ${hutool.version} + + + com.alibaba.fastjson2 + fastjson2 + ${fastjson.version} + + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + com.pharma.PharmaApplication + + + + org.projectlombok + lombok + + + + + + + \ No newline at end of file diff --git a/pharma-backend/src/main/java/com/pharma/PharmaApplication.java b/pharma-backend/src/main/java/com/pharma/PharmaApplication.java new file mode 100644 index 0000000..21b43d1 --- /dev/null +++ b/pharma-backend/src/main/java/com/pharma/PharmaApplication.java @@ -0,0 +1,7 @@ +package com.pharma; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class PharmaApplication { +} diff --git a/pharma-backend/src/main/java/com/pharma/common/module/auth/entity/User.java b/pharma-backend/src/main/java/com/pharma/common/module/auth/entity/User.java new file mode 100644 index 0000000..73bd23a --- /dev/null +++ b/pharma-backend/src/main/java/com/pharma/common/module/auth/entity/User.java @@ -0,0 +1,29 @@ +package com.pharma.common.module.auth.entity; +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.time.LocalDateTime; + +@Data +@TableName("tb_user") +public class User { + @TableId(type = IdType.AUTO) + private Long id; + + @TableField("username") + private String username; + + @TableField("password") + private String password; + + @TableField("name") + private String name; + + @TableField("tel") + private String tel; + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; +} diff --git a/pharma-backend/src/main/resources/application.yml b/pharma-backend/src/main/resources/application.yml new file mode 100644 index 0000000..e3c8fb2 --- /dev/null +++ b/pharma-backend/src/main/resources/application.yml @@ -0,0 +1,81 @@ +# 主配置文件 +server: + port: 8080 + servlet: + context-path: /api + tomcat: + uri-encoding: UTF-8 + +spring: + application: + name: pharma-management + profiles: + active: dev + + # 数据库配置 + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://kronecker.cc:3306/pharma_mgmt_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai + username: mgmt + password: 123456 + type: com.alibaba.druid.pool.DruidDataSource + druid: + initial-size: 5 + min-idle: 5 + max-active: 20 + max-wait: 60000 + time-between-eviction-runs-millis: 60000 + min-evictable-idle-time-millis: 300000 + validation-query: SELECT 1 FROM DUAL + test-while-idle: true + test-on-borrow: false + test-on-return: false + pool-prepared-statements: true + max-pool-prepared-statement-per-connection-size: 20 + + # Redis配置 + redis: + host: localhost + port: 6379 + password: + database: 0 + lettuce: + pool: + max-active: 8 + max-idle: 8 + min-idle: 0 + + jackson: + time-zone: GMT+8 + date-format: yyyy-MM-dd HH:mm:ss + + servlet: + multipart: + max-file-size: 10MB + max-request-size: 20MB + +# MyBatis Plus配置 +mybatis-plus: + configuration: + map-underscore-to-camel-case: true + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + global-config: + db-config: + id-type: auto + logic-delete-field: deleted + logic-delete-value: 1 + logic-not-delete-value: 0 + mapper-locations: classpath*:/mapper/**/*.xml + +# 自定义配置 +pharma: + jwt: + secret: pharma-secret-key-2024-jwt-token-signature + expiration: 86400000 # 24小时 + header: Authorization + security: + exclude-paths: /api/auth/login,/api/auth/register,/api/common/captcha,/swagger-ui/**,/webjars/**,/swagger-resources/**,/v2/api-docs,/doc.html + upload: + path: /upload/ + max-size: 10MB + allowed-types: jpg,jpeg,png,gif \ No newline at end of file diff --git a/pharma-backend/target/classes/application.yml b/pharma-backend/target/classes/application.yml new file mode 100644 index 0000000..e3c8fb2 --- /dev/null +++ b/pharma-backend/target/classes/application.yml @@ -0,0 +1,81 @@ +# 主配置文件 +server: + port: 8080 + servlet: + context-path: /api + tomcat: + uri-encoding: UTF-8 + +spring: + application: + name: pharma-management + profiles: + active: dev + + # 数据库配置 + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://kronecker.cc:3306/pharma_mgmt_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai + username: mgmt + password: 123456 + type: com.alibaba.druid.pool.DruidDataSource + druid: + initial-size: 5 + min-idle: 5 + max-active: 20 + max-wait: 60000 + time-between-eviction-runs-millis: 60000 + min-evictable-idle-time-millis: 300000 + validation-query: SELECT 1 FROM DUAL + test-while-idle: true + test-on-borrow: false + test-on-return: false + pool-prepared-statements: true + max-pool-prepared-statement-per-connection-size: 20 + + # Redis配置 + redis: + host: localhost + port: 6379 + password: + database: 0 + lettuce: + pool: + max-active: 8 + max-idle: 8 + min-idle: 0 + + jackson: + time-zone: GMT+8 + date-format: yyyy-MM-dd HH:mm:ss + + servlet: + multipart: + max-file-size: 10MB + max-request-size: 20MB + +# MyBatis Plus配置 +mybatis-plus: + configuration: + map-underscore-to-camel-case: true + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + global-config: + db-config: + id-type: auto + logic-delete-field: deleted + logic-delete-value: 1 + logic-not-delete-value: 0 + mapper-locations: classpath*:/mapper/**/*.xml + +# 自定义配置 +pharma: + jwt: + secret: pharma-secret-key-2024-jwt-token-signature + expiration: 86400000 # 24小时 + header: Authorization + security: + exclude-paths: /api/auth/login,/api/auth/register,/api/common/captcha,/swagger-ui/**,/webjars/**,/swagger-resources/**,/v2/api-docs,/doc.html + upload: + path: /upload/ + max-size: 10MB + allowed-types: jpg,jpeg,png,gif \ No newline at end of file diff --git a/pharma-backend/target/classes/com/pharma/PharmaApplication.class b/pharma-backend/target/classes/com/pharma/PharmaApplication.class new file mode 100644 index 0000000..4e8c6dd Binary files /dev/null and b/pharma-backend/target/classes/com/pharma/PharmaApplication.class differ diff --git a/pharma-backend/target/classes/com/pharma/common/module/auth/entity/User.class b/pharma-backend/target/classes/com/pharma/common/module/auth/entity/User.class new file mode 100644 index 0000000..ded1d8f Binary files /dev/null and b/pharma-backend/target/classes/com/pharma/common/module/auth/entity/User.class differ diff --git a/pharma-backend/target/maven-archiver/pom.properties b/pharma-backend/target/maven-archiver/pom.properties new file mode 100644 index 0000000..38e0c9f --- /dev/null +++ b/pharma-backend/target/maven-archiver/pom.properties @@ -0,0 +1,3 @@ +artifactId=pharma-management +groupId=com.pharma +version=1.0.0 diff --git a/pharma-backend/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/pharma-backend/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..9af1b07 --- /dev/null +++ b/pharma-backend/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,2 @@ +com\pharma\PharmaApplication.class +com\pharma\common\module\auth\entity\User.class diff --git a/pharma-backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/pharma-backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..525ac16 --- /dev/null +++ b/pharma-backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,2 @@ +H:\OwnProject\PharmaManagementSystem\pharma-backend\src\main\java\com\pharma\PharmaApplication.java +H:\OwnProject\PharmaManagementSystem\pharma-backend\src\main\java\com\pharma\common\module\auth\entity\User.java diff --git a/pharma-backend/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst b/pharma-backend/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst new file mode 100644 index 0000000..e69de29 diff --git a/pharma-backend/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst b/pharma-backend/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst new file mode 100644 index 0000000..e69de29 diff --git a/pharma-backend/target/pharma-management-1.0.0.jar b/pharma-backend/target/pharma-management-1.0.0.jar new file mode 100644 index 0000000..6bc2866 Binary files /dev/null and b/pharma-backend/target/pharma-management-1.0.0.jar differ diff --git a/pharma-backend/target/pharma-management-1.0.0.jar.original b/pharma-backend/target/pharma-management-1.0.0.jar.original new file mode 100644 index 0000000..6191c54 Binary files /dev/null and b/pharma-backend/target/pharma-management-1.0.0.jar.original differ diff --git a/📊 医药管理系统 - 项目文档.md b/📊 医药管理系统 - 项目文档.md new file mode 100644 index 0000000..13f9570 --- /dev/null +++ b/📊 医药管理系统 - 项目文档.md @@ -0,0 +1,2114 @@ +# 📊 医药管理系统 - 项目文档 + +## 📋 项目概述 + +### 项目名称 + +**PharmaManagement System** - 医药管理系统 + +### 技术栈 + +- **后端**: Java 17 + Spring Boot 3.1 + MyBatis Plus 3.5 + MySQL 8.0 +- **前端**: Vue 3 + TypeScript + Element Plus + Pinia + Vite +- **安全**: Spring Security + JWT +- **工具**: Maven + Node.js + Docker + +### 系统功能模块 +1. **用户管理**: 登录、权限控制、个人信息管理 +2. **药品管理**: 药品CRUD、库存管理、分类管理 +3. **销售管理**: 销售记录、销售统计、报表生成 +4. **库存预警**: 库存不足预警、补货提醒 +5. **数据统计**: 销售数据可视化、药品销量排行 + +--- + +## 📁 项目结构 + +### 后端项目结构 +``` +pharma-backend/ +├── src/main/java/com/pharma/ +│ ├── PharmaApplication.java # 启动类 +│ ├── common/ # 通用组件 +│ │ ├── config/ # 配置类 +│ │ │ ├── MyBatisConfig.java # MyBatis配置 +│ │ │ ├── SecurityConfig.java # 安全配置 +│ │ │ ├── CorsConfig.java # 跨域配置 +│ │ │ └── RedisConfig.java # Redis配置 +│ │ ├── constant/ # 常量类 +│ │ │ ├── CacheConstants.java # 缓存常量 +│ │ │ └── CommonConstants.java # 通用常量 +│ │ ├── exception/ # 异常处理 +│ │ │ ├── GlobalExceptionHandler.java # 全局异常处理 +│ │ │ └── BusinessException.java # 业务异常 +│ │ ├── result/ # 统一返回结果 +│ │ │ ├── Result.java # 响应结果封装 +│ │ │ └── ResultCode.java # 响应状态码 +│ │ ├── utils/ # 工具类 +│ │ │ ├── JwtUtil.java # JWT工具 +│ │ │ ├── SecurityUtil.java # 安全工具 +│ │ │ ├── DateUtil.java # 日期工具 +│ │ │ └── ExcelUtil.java # Excel工具 +│ │ └── web/ # Web相关 +│ │ ├── BaseController.java # 控制器基类 +│ │ └── LogAspect.java # 日志切面 +│ ├── module/ # 业务模块 +│ │ ├── auth/ # 认证模块 +│ │ │ ├── controller/ # 控制器 +│ │ │ ├── service/ # 服务接口 +│ │ │ ├── service/impl/ # 服务实现 +│ │ │ ├── mapper/ # Mapper接口 +│ │ │ ├── entity/ # 实体类 +│ │ │ └── dto/ # 数据传输对象 +│ │ ├── user/ # 用户模块 +│ │ ├── medicine/ # 药品模块 +│ │ ├── category/ # 分类模块 +│ │ └── sale/ # 销售模块 +│ └── resources/ +│ ├── application.yml # 主配置文件 +│ ├── application-dev.yml # 开发环境配置 +│ ├── application-prod.yml # 生产环境配置 +│ ├── mapper/ # XML映射文件 +│ └── static/ # 静态资源 +├── pom.xml # Maven依赖 +└── Dockerfile # Docker配置 +``` + +### 前端项目结构 +``` +pharma-frontend/ +├── public/ # 静态资源 +├── src/ +│ ├── api/ # API接口 +│ │ ├── modules/ # 模块接口 +│ │ │ ├── auth.ts # 认证接口 +│ │ │ ├── user.ts # 用户接口 +│ │ │ ├── medicine.ts # 药品接口 +│ │ │ ├── category.ts # 分类接口 +│ │ │ └── sale.ts # 销售接口 +│ │ └── index.ts # API统一导出 +│ ├── assets/ # 静态资源 +│ │ ├── styles/ # 样式文件 +│ │ └── images/ # 图片资源 +│ ├── components/ # 公共组件 +│ │ ├── common/ # 通用组件 +│ │ │ ├── SearchBar.vue # 搜索组件 +│ │ │ ├── Pagination.vue # 分页组件 +│ │ │ └── UploadImage.vue # 图片上传 +│ │ ├── layout/ # 布局组件 +│ │ │ ├── Header.vue # 头部 +│ │ │ ├── Sidebar.vue # 侧边栏 +│ │ │ └── AppMain.vue # 主内容 +│ │ └── charts/ # 图表组件 +│ │ ├── SalesChart.vue # 销售图表 +│ │ └── InventoryChart.vue # 库存图表 +│ ├── router/ # 路由配置 +│ │ └── index.ts +│ ├── store/ # Pinia状态管理 +│ │ ├── modules/ # 模块store +│ │ │ ├── user.ts # 用户store +│ │ │ ├── medicine.ts # 药品store +│ │ │ └── common.ts # 公共store +│ │ └── index.ts # store入口 +│ ├── types/ # TypeScript类型定义 +│ │ ├── api.ts # API类型 +│ │ ├── medicine.ts # 药品类型 +│ │ ├── user.ts # 用户类型 +│ │ └── index.ts # 类型导出 +│ ├── utils/ # 工具函数 +│ │ ├── auth.ts # 认证工具 +│ │ ├── request.ts # 请求封装 +│ │ ├── validate.ts # 表单验证 +│ │ └── date.ts # 日期处理 +│ ├── views/ # 页面组件 +│ │ ├── login/ # 登录页 +│ │ ├── dashboard/ # 仪表板 +│ │ ├── medicine/ # 药品管理 +│ │ ├── category/ # 分类管理 +│ │ ├── sale/ # 销售管理 +│ │ ├── user/ # 用户管理 +│ │ └── report/ # 报表统计 +│ ├── App.vue # 根组件 +│ └── main.ts # 入口文件 +├── package.json # 项目依赖 +├── tsconfig.json # TypeScript配置 +├── vite.config.ts # Vite配置 +└── Dockerfile # Docker配置 +``` + +--- + +## 🗄️ 数据库设计详细说明 + +### 1. 数据库关系图 +```mermaid +erDiagram + tb_user ||--o{ tb_sale_detail : "operates" + tb_medicine ||--o{ tb_sale_detail : "is_sold" + tb_category ||--o{ tb_medicine : "categorizes" + + tb_user { + int id PK "主键" + varchar username "用户名" + varchar password "密码" + varchar name "姓名" + char tel "手机号" + datetime create_time "创建时间" + datetime update_time "更新时间" + } + + tb_medicine { + int id PK "主键" + varchar med_no UK "药品编码" + varchar name "名称" + varchar manufacturer "厂家" + text description "描述" + decimal price "单价" + int med_count "库存数量" + int req_count "补货阈值" + varchar photo_path "图片路径" + int category_id FK "分类ID" + datetime create_time "创建时间" + datetime update_time "更新时间" + } + + tb_sale_detail { + int id PK "主键" + int medicine_id FK "药品ID" + int operator_id FK "操作员ID" + int quantity "销售数量" + decimal unit_price "销售单价" + datetime sale_time "销售时间" + datetime create_time "创建时间" + datetime update_time "更新时间" + } + + tb_category { + int id PK "主键" + varchar name UK "分类名称" + text description "描述" + datetime create_time "创建时间" + datetime update_time "更新时间" + } +``` + +### 2. 实体类设计 + +#### 用户实体 (User) +```java +package com.pharma.module.user.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.time.LocalDateTime; + +@Data +@TableName("tb_user") +public class User { + @TableId(type = IdType.AUTO) + private Long id; + + @TableField("username") + private String username; + + @TableField("password") + private String password; + + @TableField("name") + private String name; + + @TableField("tel") + private String tel; + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; +} +``` + +#### 药品实体 (Medicine) +```java +package com.pharma.module.medicine.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@TableName("tb_medicine") +public class Medicine { + @TableId(type = IdType.AUTO) + private Long id; + + @TableField("med_no") + private String medNo; + + @TableField("name") + private String name; + + @TableField("manufacturer") + private String manufacturer; + + @TableField("description") + private String description; + + @TableField("price") + private BigDecimal price; + + @TableField("med_count") + private Integer medCount; + + @TableField("req_count") + private Integer reqCount; + + @TableField("photo_path") + private String photoPath; + + @TableField("category_id") + private Long categoryId; + + @TableField(exist = false) + private String categoryName; // 关联查询字段 + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; +} +``` + +#### 销售明细实体 (SaleDetail) +```java +package com.pharma.module.sale.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@TableName("tb_sale_detail") +public class SaleDetail { + @TableId(type = IdType.AUTO) + private Long id; + + @TableField("medicine_id") + private Long medicineId; + + @TableField("operator_id") + private Long operatorId; + + @TableField("quantity") + private Integer quantity; + + @TableField("unit_price") + private BigDecimal unitPrice; + + @TableField("sale_time") + private LocalDateTime saleTime; + + @TableField(exist = false) + private String medicineName; // 关联查询字段 + + @TableField(exist = false) + private String operatorName; // 关联查询字段 + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; +} +``` + +#### 分类实体 (Category) +```java +package com.pharma.module.category.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.time.LocalDateTime; + +@Data +@TableName("tb_category") +public class Category { + @TableId(type = IdType.AUTO) + private Long id; + + @TableField("name") + private String name; + + @TableField("description") + private String description; + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; +} +``` + +--- + +## 🔧 环境配置 + +### 1. 后端环境配置 + +#### Maven依赖 (pom.xml) +```xml + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.1.5 + + + + com.pharma + pharma-management + 1.0.0 + pharma-management + 医药管理系统 + + + 17 + 3.5.3.1 + 0.11.5 + 2.0.38 + 5.8.21 + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-aop + + + + + mysql + mysql-connector-java + 8.0.33 + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus.version} + + + com.alibaba + druid-spring-boot-starter + 1.2.18 + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jjwt.version} + runtime + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} + runtime + + + + + org.projectlombok + lombok + true + + + cn.hutool + hutool-all + ${hutool.version} + + + com.alibaba.fastjson2 + fastjson2 + ${fastjson.version} + + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + +``` + +#### 配置文件 (application.yml) + +```yaml +# 主配置文件 +server: + port: 8080 + servlet: + context-path: /api + tomcat: + uri-encoding: UTF-8 + +spring: + application: + name: pharma-management + profiles: + active: dev + + # 数据库配置 + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/pharma_mgmt_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai + username: root + password: 123456 + type: com.alibaba.druid.pool.DruidDataSource + druid: + initial-size: 5 + min-idle: 5 + max-active: 20 + max-wait: 60000 + time-between-eviction-runs-millis: 60000 + min-evictable-idle-time-millis: 300000 + validation-query: SELECT 1 FROM DUAL + test-while-idle: true + test-on-borrow: false + test-on-return: false + pool-prepared-statements: true + max-pool-prepared-statement-per-connection-size: 20 + + # Redis配置 + redis: + host: localhost + port: 6379 + password: + database: 0 + lettuce: + pool: + max-active: 8 + max-idle: 8 + min-idle: 0 + + jackson: + time-zone: GMT+8 + date-format: yyyy-MM-dd HH:mm:ss + + servlet: + multipart: + max-file-size: 10MB + max-request-size: 20MB + +# MyBatis Plus配置 +mybatis-plus: + configuration: + map-underscore-to-camel-case: true + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + global-config: + db-config: + id-type: auto + logic-delete-field: deleted + logic-delete-value: 1 + logic-not-delete-value: 0 + mapper-locations: classpath*:/mapper/**/*.xml + +# 自定义配置 +pharma: + jwt: + secret: pharma-secret-key-2024-jwt-token-signature + expiration: 86400000 # 24小时 + header: Authorization + security: + exclude-paths: /api/auth/login,/api/auth/register,/api/common/captcha,/swagger-ui/**,/webjars/**,/swagger-resources/**,/v2/api-docs,/doc.html + upload: + path: /upload/ + max-size: 10MB + allowed-types: jpg,jpeg,png,gif +``` + +### 2. 前端环境配置 + +#### package.json +```json +{ + "name": "pharma-frontend", + "version": "1.0.0", + "description": "医药管理系统前端", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc && vite build", + "preview": "vite preview", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix" + }, + "dependencies": { + "vue": "^3.3.0", + "vue-router": "^4.2.0", + "pinia": "^2.1.0", + "axios": "^1.6.0", + "element-plus": "^2.3.0", + "@element-plus/icons-vue": "^2.2.0", + "echarts": "^5.4.3", + "vue-echarts": "^6.6.0", + "dayjs": "^1.11.0", + "lodash": "^4.17.21", + "js-cookie": "^3.0.5", + "vuedraggable": "^4.1.0", + "print-js": "^1.6.0", + "xlsx": "^0.18.5", + "vite-plugin-svg-icons": "^2.0.1" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.5.0", + "typescript": "^5.0.0", + "vue-tsc": "^1.8.0", + "vite": "^4.5.0", + "@types/node": "^20.0.0", + "@types/lodash": "^4.14.197", + "@types/js-cookie": "^3.0.6", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.45.0", + "eslint-plugin-vue": "^9.17.0", + "unplugin-auto-import": "^0.16.7", + "unplugin-vue-components": "^0.25.2", + "@iconify/json": "^2.2.0" + } +} +``` + +#### vite.config.ts +```typescript +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { resolve } from 'path' +import AutoImport from 'unplugin-auto-import/vite' +import Components from 'unplugin-vue-components/vite' +import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' +import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' + +export default defineConfig({ + plugins: [ + vue(), + AutoImport({ + imports: ['vue', 'vue-router', 'pinia'], + dts: 'src/auto-imports.d.ts', + resolvers: [ElementPlusResolver()], + }), + Components({ + resolvers: [ElementPlusResolver()], + }), + createSvgIconsPlugin({ + iconDirs: [resolve(process.cwd(), 'src/assets/icons')], + symbolId: 'icon-[dir]-[name]', + }), + ], + resolve: { + alias: { + '@': resolve(__dirname, 'src'), + }, + }, + server: { + port: 3000, + open: true, + proxy: { + '/api': { + target: 'http://localhost:8080', + changeOrigin: true, + }, + }, + }, + css: { + preprocessorOptions: { + scss: { + additionalData: '@use "@/assets/styles/variables.scss" as *;', + }, + }, + }, +}) +``` + +--- + +## 🚀 核心功能实现 + +### 1. 用户认证与授权 + +#### JWT工具类 +```java +package com.pharma.common.utils; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import javax.crypto.SecretKey; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Component +public class JwtUtil { + + @Value("${pharma.jwt.secret}") + private String secret; + + @Value("${pharma.jwt.expiration}") + private Long expiration; + + private SecretKey getSigningKey() { + return Keys.hmacShaKeyFor(secret.getBytes()); + } + + public String generateToken(String username) { + Map claims = new HashMap<>(); + claims.put("username", username); + return Jwts.builder() + .setClaims(claims) + .setSubject(username) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(getSigningKey(), SignatureAlgorithm.HS256) + .compact(); + } + + public String getUsernameFromToken(String token) { + Claims claims = getClaimsFromToken(token); + return claims.getSubject(); + } + + public boolean validateToken(String token) { + try { + Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token); + return true; + } catch (JwtException | IllegalArgumentException e) { + return false; + } + } + + private Claims getClaimsFromToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } +} +``` + +#### Spring Security配置 +```java +package com.pharma.common.config; + +import com.pharma.common.filter.JwtAuthenticationFilter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import java.util.Arrays; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity(prePostEnabled = true) +public class SecurityConfig { + + @Autowired + private JwtAuthenticationFilter jwtAuthenticationFilter; + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager( + AuthenticationConfiguration authConfig) throws Exception { + return authConfig.getAuthenticationManager(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .csrf(csrf -> csrf.disable()) + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .requestMatchers( + "/api/auth/login", + "/api/auth/register", + "/api/common/captcha", + "/swagger-ui/**", + "/v3/api-docs/**" + ).permitAll() + .anyRequest().authenticated() + ) + .addFilterBefore(jwtAuthenticationFilter, + UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000")); + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(Arrays.asList("*")); + configuration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} +``` + +### 2. 药品管理模块 + +#### 药品服务实现 +```java +package com.pharma.module.medicine.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.pharma.common.exception.BusinessException; +import com.pharma.module.medicine.dto.MedicineDTO; +import com.pharma.module.medicine.entity.Medicine; +import com.pharma.module.medicine.mapper.MedicineMapper; +import com.pharma.module.medicine.service.MedicineService; +import com.pharma.module.medicine.vo.MedicineVO; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import java.math.BigDecimal; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class MedicineServiceImpl extends ServiceImpl + implements MedicineService { + + @Override + public Page getMedicinePage(Integer pageNum, Integer pageSize, + String name, Long categoryId) { + Page page = new Page<>(pageNum, pageSize); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + + if (StringUtils.hasText(name)) { + queryWrapper.like(Medicine::getName, name); + } + if (categoryId != null) { + queryWrapper.eq(Medicine::getCategoryId, categoryId); + } + queryWrapper.orderByDesc(Medicine::getUpdateTime); + + Page medicinePage = this.page(page, queryWrapper); + + Page voPage = new Page<>(); + BeanUtils.copyProperties(medicinePage, voPage, "records"); + + List voList = medicinePage.getRecords().stream() + .map(medicine -> { + MedicineVO vo = new MedicineVO(); + BeanUtils.copyProperties(medicine, vo); + return vo; + }) + .collect(Collectors.toList()); + + voPage.setRecords(voList); + return voPage; + } + + @Override + public void addMedicine(MedicineDTO dto) { + // 检查药品编码是否重复 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(Medicine::getMedNo, dto.getMedNo()); + if (this.count(queryWrapper) > 0) { + throw new BusinessException("药品编码已存在"); + } + + Medicine medicine = new Medicine(); + BeanUtils.copyProperties(dto, medicine); + this.save(medicine); + } + + @Override + public void updateMedicine(MedicineDTO dto) { + Medicine medicine = this.getById(dto.getId()); + if (medicine == null) { + throw new BusinessException("药品不存在"); + } + + // 检查药品编码是否与其他记录重复 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(Medicine::getMedNo, dto.getMedNo()) + .ne(Medicine::getId, dto.getId()); + if (this.count(queryWrapper) > 0) { + throw new BusinessException("药品编码已存在"); + } + + BeanUtils.copyProperties(dto, medicine); + this.updateById(medicine); + } + + @Override + public void updateStock(Long medicineId, Integer quantity) { + Medicine medicine = this.getById(medicineId); + if (medicine == null) { + throw new BusinessException("药品不存在"); + } + + int newCount = medicine.getMedCount() + quantity; + if (newCount < 0) { + throw new BusinessException("库存不足"); + } + + medicine.setMedCount(newCount); + this.updateById(medicine); + } + + @Override + public List getLowStockList() { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.lt(Medicine::getMedCount, Medicine::getReqCount); + queryWrapper.orderByAsc(Medicine::getMedCount); + + List medicines = this.list(queryWrapper); + return medicines.stream() + .map(medicine -> { + MedicineVO vo = new MedicineVO(); + BeanUtils.copyProperties(medicine, vo); + return vo; + }) + .collect(Collectors.toList()); + } +} +``` + +#### MyBatis Mapper接口 +```java +package com.pharma.module.medicine.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.pharma.module.medicine.entity.Medicine; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import java.util.List; +import java.util.Map; + +@Mapper +public interface MedicineMapper extends BaseMapper { + + @Select("SELECT m.*, c.name as category_name " + + "FROM tb_medicine m " + + "LEFT JOIN tb_category c ON m.category_id = c.id " + + "WHERE m.id = #{id}") + Medicine selectWithCategory(Long id); + + @Select("SELECT category_id, COUNT(*) as count, SUM(med_count) as total " + + "FROM tb_medicine " + + "GROUP BY category_id") + List> groupByCategory(); + + @Select("SELECT name, med_count " + + "FROM tb_medicine " + + "ORDER BY med_count ASC " + + "LIMIT 10") + List> getLowStockTop10(); +} +``` + +### 3. 销售管理模块 + +#### 销售服务实现 +```java +package com.pharma.module.sale.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.pharma.common.utils.SecurityUtil; +import com.pharma.module.medicine.service.MedicineService; +import com.pharma.module.sale.dto.SaleDTO; +import com.pharma.module.sale.entity.SaleDetail; +import com.pharma.module.sale.mapper.SaleDetailMapper; +import com.pharma.module.sale.service.SaleService; +import com.pharma.module.sale.vo.SaleVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Service +public class SaleServiceImpl extends ServiceImpl + implements SaleService { + + @Autowired + private MedicineService medicineService; + + @Override + @Transactional(rollbackFor = Exception.class) + public void saleMedicine(SaleDTO dto) { + // 1. 扣减库存 + medicineService.updateStock(dto.getMedicineId(), -dto.getQuantity()); + + // 2. 创建销售记录 + SaleDetail saleDetail = new SaleDetail(); + saleDetail.setMedicineId(dto.getMedicineId()); + saleDetail.setOperatorId(SecurityUtil.getCurrentUserId()); + saleDetail.setQuantity(dto.getQuantity()); + saleDetail.setUnitPrice(dto.getUnitPrice()); + saleDetail.setSaleTime(LocalDateTime.now()); + + this.save(saleDetail); + } + + @Override + public BigDecimal getTodaySales() { + LocalDate today = LocalDate.now(); + LocalDateTime startOfDay = today.atStartOfDay(); + LocalDateTime endOfDay = today.plusDays(1).atStartOfDay(); + + BigDecimal total = this.baseMapper.getSalesTotalByTimeRange( + startOfDay, endOfDay); + return total != null ? total : BigDecimal.ZERO; + } + + @Override + public List> getSalesByDay(LocalDate startDate, LocalDate endDate) { + return this.baseMapper.getDailySales(startDate, endDate); + } + + @Override + public List getRecentSales(Integer limit) { + return this.baseMapper.selectRecentSales(limit); + } +} +``` + +### 4. 前端实现示例 + +#### 药品列表页面 +```vue + + + + + +``` + +#### 销售统计图表组件 +```vue + + + + + +``` + +--- + +## 📡 API接口文档 + +### 认证模块 + +#### 1. 用户登录 + +```http +POST /api/auth/login +Content-Type: application/json + +{ + "username": "admin", + "password": "123456" +} +``` + +#### 2. 获取当前用户信息 +```http +GET /api/user/info +Authorization: Bearer {token} +``` + +### 药品模块 + +#### 1. 分页查询药品列表 +```http +GET /api/medicine/list?pageNum=1&pageSize=10&name=感冒&categoryId=1 +Authorization: Bearer {token} +``` + +#### 2. 新增药品 +```http +POST /api/medicine/add +Authorization: Bearer {token} +Content-Type: application/json + +{ + "medNo": "MED1001", + "name": "测试药品", + "manufacturer": "测试药厂", + "price": 25.50, + "medCount": 100, + "reqCount": 20, + "categoryId": 1, + "description": "测试药品描述" +} +``` + +#### 3. 更新药品库存 +```http +PUT /api/medicine/stock/{id} +Authorization: Bearer {token} +Content-Type: application/json + +{ + "quantity": 10 // 正数增加,负数减少 +} +``` + +### 销售模块 + +#### 1. 药品销售 +```http +POST /api/sale/create +Authorization: Bearer {token} +Content-Type: application/json + +{ + "medicineId": 1, + "quantity": 2, + "unitPrice": 25.50 +} +``` + +#### 2. 销售统计 +```http +GET /api/sale/statistics?startDate=2024-01-01&endDate=2024-01-31 +Authorization: Bearer {token} +``` + +--- + +## 🐳 Docker部署 + +### Docker Compose配置 +```yaml +version: '3.8' + +services: + mysql: + image: mysql:8.0 + container_name: pharma-mysql + environment: + MYSQL_ROOT_PASSWORD: root123456 + MYSQL_DATABASE: pharma_mgmt_db + MYSQL_USER: pharma_user + MYSQL_PASSWORD: pharma123 + ports: + - "3306:3306" + volumes: + - mysql_data:/var/lib/mysql + - ./init.sql:/docker-entrypoint-initdb.d/init.sql + networks: + - pharma-network + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + + redis: + image: redis:7-alpine + container_name: pharma-redis + ports: + - "6379:6379" + volumes: + - redis_data:/data + networks: + - pharma-network + command: redis-server --appendonly yes + + backend: + build: + context: ./pharma-backend + dockerfile: Dockerfile + container_name: pharma-backend + depends_on: + - mysql + - redis + environment: + SPRING_PROFILES_ACTIVE: prod + DATABASE_HOST: mysql + REDIS_HOST: redis + ports: + - "8080:8080" + volumes: + - ./logs:/app/logs + - ./upload:/app/upload + networks: + - pharma-network + restart: unless-stopped + + frontend: + build: + context: ./pharma-frontend + dockerfile: Dockerfile + container_name: pharma-frontend + depends_on: + - backend + ports: + - "80:80" + networks: + - pharma-network + restart: unless-stopped + + nginx: + image: nginx:alpine + container_name: pharma-nginx + depends_on: + - backend + - frontend + ports: + - "8000:80" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf + - ./nginx/conf.d:/etc/nginx/conf.d + networks: + - pharma-network + +networks: + pharma-network: + driver: bridge + +volumes: + mysql_data: + redis_data: + upload: + logs: +``` + +### 后端Dockerfile +```dockerfile +FROM openjdk:17-jdk-slim + +# 设置工作目录 +WORKDIR /app + +# 复制Maven构建文件 +COPY target/*.jar app.jar + +# 创建非root用户 +RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app +USER appuser + +# 暴露端口 +EXPOSE 8080 + +# 启动应用 +ENTRYPOINT ["java", "-jar", "app.jar"] +``` + +### 前端Dockerfile +```dockerfile +# 构建阶段 +FROM node:18-alpine as build-stage + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci --only=production + +COPY . . +RUN npm run build + +# 生产阶段 +FROM nginx:alpine as production-stage + +COPY --from=build-stage /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/nginx.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] +``` + +--- + +## 🧪 测试 + +### 1. 单元测试示例 +```java +package com.pharma.module.medicine.service; + +import com.pharma.PharmaApplication; +import com.pharma.module.medicine.entity.Medicine; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import java.math.BigDecimal; +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest(classes = PharmaApplication.class) +@Transactional +class MedicineServiceTest { + + @Autowired + private MedicineService medicineService; + + @Test + void testAddMedicine() { + Medicine medicine = new Medicine(); + medicine.setMedNo("TEST001"); + medicine.setName("测试药品"); + medicine.setPrice(new BigDecimal("25.50")); + medicine.setMedCount(100); + medicine.setReqCount(20); + + boolean result = medicineService.save(medicine); + assertTrue(result); + assertNotNull(medicine.getId()); + } + + @Test + void testUpdateStock() { + // 先创建测试数据 + Medicine medicine = new Medicine(); + medicine.setMedNo("TEST002"); + medicine.setName("测试药品2"); + medicine.setPrice(new BigDecimal("15.00")); + medicine.setMedCount(50); + medicine.setReqCount(10); + medicineService.save(medicine); + + // 增加库存 + medicineService.updateStock(medicine.getId(), 10); + Medicine updated = medicineService.getById(medicine.getId()); + assertEquals(60, updated.getMedCount()); + + // 减少库存 + medicineService.updateStock(medicine.getId(), -20); + updated = medicineService.getById(medicine.getId()); + assertEquals(40, updated.getMedCount()); + } +} +``` + +### 2. API测试示例 (Postman) +```javascript +// 登录测试 +pm.test("登录成功", function () { + var jsonData = pm.response.json(); + pm.expect(jsonData.code).to.eql(200); + pm.expect(jsonData.data.token).to.not.be.null; +}); + +// 药品列表测试 +pm.test("获取药品列表成功", function () { + var jsonData = pm.response.json(); + pm.expect(jsonData.code).to.eql(200); + pm.expect(jsonData.data.records).to.be.an('array'); +}); + +// 库存预警测试 +pm.test("获取低库存药品", function () { + var jsonData = pm.response.json(); + pm.expect(jsonData.code).to.eql(200); + jsonData.data.forEach(function(item) { + pm.expect(item.medCount).to.be.lessThan(item.reqCount); + }); +}); +``` + +--- + +## 📊 项目进度规划 + +### 第一阶段 (1周) - 基础框架搭建 +- [x] 项目初始化 +- [x] 数据库设计和初始化 +- [x] Spring Boot + MyBatis Plus整合 +- [x] Vue3 + Element Plus环境搭建 +- [x] 用户认证模块 + +### 第二阶段 (1周) - 核心功能实现 +- [x] 药品管理模块 +- [x] 分类管理模块 +- [x] 销售管理模块 +- [x] 库存管理模块 +- [x] 基础前端页面 + +### 第三阶段 (3-5天) - 高级功能 +- [ ] 数据统计和图表 +- [ ] 报表导出功能 +- [ ] 库存预警系统 +- [ ] 权限管理系统 +- [ ] 系统日志管理 + +### 第四阶段 (2-3天) - 优化部署 +- [ ] 性能优化 +- [ ] 安全加固 +- [ ] Docker容器化 +- [ ] 生产环境部署 +- [ ] 监控告警配置 + +--- + +## 🔍 故障排除 + +### 常见问题及解决方案 + +1. **数据库连接失败** +```bash +# 检查MySQL服务 +sudo systemctl status mysql + +# 检查连接配置 +# 确保application.yml中的数据库连接信息正确 +``` + +2. **前端跨域问题** +```yaml +# 检查后端CORS配置 +# 确保SecurityConfig中的corsConfigurationSource配置正确 +``` + +3. **JWT认证失败** +```java +// 检查JWT配置 +// 确保application.yml中的pharma.jwt.secret与前端保持一致 +``` + +4. **MyBatis Plus分页失效** +```java +// 添加分页拦截器配置 +@Configuration +public class MyBatisPlusConfig { + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); + return interceptor; + } +} +``` + +--- + +## 📚 学习资源 + +1. **Spring Boot官方文档**: https://spring.io/projects/spring-boot +2. **MyBatis Plus官方文档**: https://baomidou.com/ +3. **Vue3官方文档**: https://cn.vuejs.org/ +4. **Element Plus文档**: https://element-plus.org/zh-CN/ +5. **MySQL文档**: https://dev.mysql.com/doc/ + +--- + +## 📞 技术支持 + +如遇到问题,可通过以下方式获取支持: + +1. **查看日志文件**: `logs/application.log` +2. **检查数据库连接**: 确认MySQL服务正常运行 +3. **API调试**: 使用Postman测试API接口 +4. **提交Issue**: 在项目仓库提交问题 +5. **联系开发团队**: 通过邮件或即时通讯工具 + +--- + +*本文档最后更新于: 2024年1月* +*项目版本: v1.0.0* +*技术栈: Java 17 + Spring Boot 3.1 + Vue 3 + MySQL 8.0* \ No newline at end of file