# 📊 医药管理系统 - 项目文档
## 📋 项目概述
### 项目名称
**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