作为一名有多年Java开发经验的程序员,我最近完成了一个基于Spring Boot的药店销售管理系统。这个项目源于我观察到传统药店管理中存在的信息滞后、效率低下等问题。通过这个系统,药店可以实现从药品采购、库存管理到销售统计的全流程数字化管理。
系统采用了当前主流的Spring Boot + MySQL技术栈,前端使用Vue.js框架。整个开发周期约3个月,目前已在本地测试环境中稳定运行。系统最大的亮点在于其完善的权限管理和智能预警功能,能够根据不同角色(管理员、药师、收银员、普通用户)提供差异化的操作界面和功能。
在技术选型上,我主要考虑了以下几个因素:
最终确定的技术栈如下:
系统采用经典的三层架构:
code复制┌───────────────────────────────────────┐
│ 表现层 │
│ (Controller/API接口/Vue前端) │
└───────────────────────────────────────┘
↓
┌───────────────────────────────────────┐
│ 业务逻辑层 │
│ (Service层/业务规则处理) │
└───────────────────────────────────────┘
↓
┌───────────────────────────────────────┐
│ 数据访问层 │
│ (DAO层/MyBatis Plus/MySQL) │
└───────────────────────────────────────┘
这种分层设计使得各层职责明确,便于后期维护和扩展。例如,如果需要更换数据库,只需修改数据访问层的实现,不会影响其他层。
权限管理是系统的核心模块之一。我们实现了基于RBAC(基于角色的访问控制)模型的权限系统:
java复制@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
@ManyToOne
@JoinColumn(name = "role_id")
private Role role;
// 其他字段和方法...
}
@Entity
@Table(name = "role")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany
@JoinTable(
name = "role_permission",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "permission_id")
)
private Set<Permission> permissions = new HashSet<>();
// 其他字段和方法...
}
权限验证通过Spring Security实现,关键配置如下:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/pharmacist/**").hasRole("PHARMACIST")
.antMatchers("/cashier/**").hasRole("CASHIER")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
// 其他配置...
}
库存管理是药店系统的核心功能。我们实现了智能预警机制,当库存低于阈值时自动提醒:
java复制@Service
public class InventoryService {
@Value("${inventory.warning.threshold}")
private int warningThreshold;
@Scheduled(cron = "0 0 9 * * ?") // 每天上午9点检查
public void checkInventoryLevels() {
List<Drug> lowStockDrugs = drugRepository
.findByQuantityLessThan(warningThreshold);
if (!lowStockDrugs.isEmpty()) {
String message = "以下药品库存不足:\n";
for (Drug drug : lowStockDrugs) {
message += String.format("%s (当前库存: %d)\n",
drug.getName(), drug.getQuantity());
}
notificationService.sendAlert(message);
}
}
}
这个功能大大减少了因库存不足导致的销售中断问题。在实际测试中,它能提前3-5天预警库存风险,给药店充足的补货时间。
系统数据库包含20多张表,以下是几个核心表的设计:
药品表(drug_warehouse):
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键 |
| drug_number | VARCHAR(64) | 药品编号 |
| name | VARCHAR(64) | 药品名称 |
| category | VARCHAR(64) | 药品分类 |
| specification | VARCHAR(64) | 规格 |
| manufacturer | VARCHAR(64) | 生产厂商 |
| quantity | INT | 库存数量 |
| price | DECIMAL(10,2) | 单价 |
| expiry_date | DATE | 有效期至 |
销售订单表(sales_order):
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键 |
| order_number | VARCHAR(64) | 订单编号 |
| drug_id | BIGINT | 药品ID |
| quantity | INT | 销售数量 |
| total_amount | DECIMAL(10,2) | 总金额 |
| customer_id | BIGINT | 客户ID |
| pharmacist_id | BIGINT | 药师ID |
| status | VARCHAR(16) | 订单状态 |
| create_time | DATETIME | 创建时间 |
为提高查询性能,我们在以下字段上建立了索引:
索引创建示例:
sql复制CREATE INDEX idx_drug_number ON drug_warehouse(drug_number);
CREATE INDEX idx_order_time ON sales_order(create_time DESC);
销售流程是系统的核心业务,主要步骤如下:
关键代码实现:
java复制@Transactional
public SalesOrder createSale(SaleRequest request) {
// 1. 验证库存
Drug drug = drugRepository.findById(request.getDrugId())
.orElseThrow(() -> new DrugNotFoundException(request.getDrugId()));
if (drug.getQuantity() < request.getQuantity()) {
throw new InsufficientStockException(drug.getName(), drug.getQuantity());
}
// 2. 计算金额
BigDecimal unitPrice = drug.getPrice();
BigDecimal total = unitPrice.multiply(BigDecimal.valueOf(request.getQuantity()));
// 应用优惠券折扣
if (request.getCouponId() != null) {
Coupon coupon = couponService.validateCoupon(request.getCouponId(), request.getCustomerId());
total = coupon.applyDiscount(total);
}
// 3. 创建订单
SalesOrder order = new SalesOrder();
order.setOrderNumber(generateOrderNumber());
order.setDrugId(drug.getId());
order.setQuantity(request.getQuantity());
order.setUnitPrice(unitPrice);
order.setTotalAmount(total);
order.setCustomerId(request.getCustomerId());
order.setPharmacistId(request.getPharmacistId());
order.setStatus("COMPLETED");
salesOrderRepository.save(order);
// 4. 更新库存
drug.setQuantity(drug.getQuantity() - request.getQuantity());
drugRepository.save(drug);
// 5. 记录财务
FinancialRecord record = new FinancialRecord();
record.setRecordDate(LocalDate.now());
record.setIncome(total);
record.setType("SALE");
record.setReferenceId(order.getId());
financialRepository.save(record);
return order;
}
采购流程采用工作流设计,包含以下状态:
状态转换通过状态模式实现:
java复制public interface PurchaseOrderState {
void approve(PurchaseOrder order);
void reject(PurchaseOrder order);
void complete(PurchaseOrder order);
}
@Service
@Scope("prototype")
public class PendingState implements PurchaseOrderState {
@Override
public void approve(PurchaseOrder order) {
order.setState(new ApprovedState());
order.setStatus("APPROVED");
order.setUpdateTime(LocalDateTime.now());
// 发送通知...
}
// 其他方法实现...
}
我们使用JMeter对系统进行了压力测试,主要指标如下:
为提高系统性能,我们针对以下数据实施了Redis缓存:
缓存配置示例:
java复制@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.disableCachingNullValues()
.serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
}
系统采用Docker容器化部署,主要包含以下服务:
docker-compose.yml关键配置:
yaml复制version: '3'
services:
app:
image: pharmacy-app:latest
ports:
- "8080:8080"
environment:
- SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/pharmacy
- SPRING_REDIS_HOST=redis
depends_on:
- db
- redis
db:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=root
- MYSQL_DATABASE=pharmacy
volumes:
- db_data:/var/lib/mysql
redis:
image: redis:6.0
ports:
- "6379:6379"
nginx:
image: nginx:1.19
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
volumes:
db_data:
在开发这个系统过程中,我积累了一些有价值的经验:
事务管理:药品销售涉及多个表的更新操作,必须使用@Transactional确保数据一致性。我们遇到过因事务配置不当导致的库存不同步问题。
批量操作优化:初期实现药品导入功能时,采用单条插入方式,性能极差。后改为批量插入,速度提升50倍:
java复制@Transactional
public void importDrugs(List<Drug> drugs) {
int batchSize = 100;
for (int i = 0; i < drugs.size(); i += batchSize) {
List<Drug> batch = drugs.subList(i, Math.min(i + batchSize, drugs.size()));
drugRepository.saveAll(batch);
}
}
java复制@Slf4j
@Aspect
@Component
public class LoggingAspect {
@Around("execution(* com.pharmacy..*(..))")
public Object logMethod(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
MDC.put("method", methodName);
log.info("Entering method: {}", methodName);
try {
Object result = joinPoint.proceed();
log.info("Exiting method: {}", methodName);
return result;
} catch (Exception e) {
log.error("Error in method {}: {}", methodName, e.getMessage());
throw e;
} finally {
MDC.remove("method");
}
}
}
java复制@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.pharmacy.controller"))
.paths(PathSelectors.any())
.build()
.apiInfo(apiInfo());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("药店销售系统API")
.description("药店销售管理系统接口文档")
.version("1.0")
.build();
}
}
智能预警系统:除了基础的库存预警,我们还实现了:
多维度报表:系统提供丰富的统计分析功能:
移动端适配:采用响应式设计,完美适配手机和平板设备,方便药师随时随地查看库存和销售情况。
初期版本在高并发销售场景下会出现库存超卖问题。我们通过以下方案解决:
java复制@Entity
public class Drug {
@Version
private Integer version;
// 其他字段...
}
java复制public boolean sellWithLock(Long drugId, int quantity) {
String lockKey = "drug_lock:" + drugId;
String requestId = UUID.randomUUID().toString();
try {
// 尝试获取锁
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS);
if (!locked) {
return false;
}
// 执行销售逻辑
return sellDrug(drugId, quantity);
} finally {
// 释放锁
if (requestId.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
}
}
当药品记录超过10万条时,某些报表查询变得非常缓慢。优化措施包括:
这个药店销售管理系统项目让我深刻体会到业务需求与技术实现的结合之道。系统从设计到实现历时3个月,过程中遇到了各种挑战,但最终都找到了合适的解决方案。
系统的主要优势在于:
未来可能的改进方向包括:
整个项目的源码已经整理上传到GitHub,包含了详细的部署文档和使用说明,希望能给有类似需求的开发者提供参考。在实际部署时,建议根据药店的具体业务需求调整系统参数,如库存预警阈值、报表统计周期等。