作为一名有10年全栈开发经验的工程师,我最近完成了一个基于SpringBoot+Vue的医疗用品销售网站项目。这个项目不仅包含了完整的电商功能模块,还针对医疗行业的特殊需求做了定制化开发。从技术架构到业务逻辑实现,整个项目采用了我多年积累的最佳实践方案。
医疗电商平台相比普通电商有着更高的合规性要求和更复杂的业务流程。在开发过程中,我特别注重了以下几个关键点:
这个项目采用了前后端分离架构,后端使用SpringBoot+MyBatisPlus,前端使用Vue.js+ElementUI,数据库选用MySQL 8.0。整套系统经过严格测试,性能稳定,完全满足毕业设计或商业项目的要求。
选择合适的技术栈是项目成功的基础。经过多方比较,我最终确定了以下技术组合:
后端技术栈:
前端技术栈:
数据库:
这个技术组合的优势在于:
系统采用经典的三层架构,但针对电商特点做了优化:
code复制┌───────────────────────────────────────┐
│ 客户端层 │
│ ┌─────────┐ ┌─────────┐ ┌───────┐ │
│ │ Web │ │ Mobile │ │ Admin │ │
│ └─────────┘ └─────────┘ └───────┘ │
└───────────────────┬───────────────────┘
│ HTTP/HTTPS
┌───────────────────▼───────────────────┐
│ 应用服务层 │
│ ┌─────────┐ ┌─────────┐ ┌───────┐ │
│ │ API网关 │ │ 业务服务 │ │ 消息队列│ │
│ └─────────┘ └─────────┘ └───────┘ │
└───────────────────┬───────────────────┘
│ JDBC/NoSQL
┌───────────────────▼───────────────────┐
│ 数据存储层 │
│ ┌─────────┐ ┌─────────┐ ┌───────┐ │
│ │ MySQL │ │ MongoDB │ │ Redis │ │
│ └─────────┘ └─────────┘ └───────┘ │
└───────────────────────────────────────┘
关键设计决策:
数据库设计遵循第三范式,但针对查询性能做了适当反范式化。主要表结构如下:
用户相关表:
sql复制CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '登录名',
`password` varchar(100) NOT NULL COMMENT '密码',
`real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
`avatar` varchar(255) DEFAULT NULL COMMENT '头像',
`status` tinyint DEFAULT '1' COMMENT '状态 0-禁用 1-正常',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
药品信息表:
sql复制CREATE TABLE `medicine` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT '药品名称',
`code` varchar(50) NOT NULL COMMENT '药品编码',
`spec` varchar(100) DEFAULT NULL COMMENT '规格',
`manufacturer` varchar(200) DEFAULT NULL COMMENT '生产厂家',
`category_id` bigint DEFAULT NULL COMMENT '分类ID',
`price` decimal(10,2) DEFAULT NULL COMMENT '售价',
`cost` decimal(10,2) DEFAULT NULL COMMENT '成本价',
`stock` int DEFAULT '0' COMMENT '库存',
`image` varchar(255) DEFAULT NULL COMMENT '主图',
`description` text COMMENT '描述',
`status` tinyint DEFAULT '1' COMMENT '状态 0-下架 1-上架',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_code` (`code`),
KEY `idx_category` (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='药品信息表';
订单表:
sql复制CREATE TABLE `order` (
`id` bigint NOT NULL AUTO_INCREMENT,
`order_no` varchar(50) NOT NULL COMMENT '订单编号',
`user_id` bigint NOT NULL COMMENT '用户ID',
`total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额',
`payment_amount` decimal(10,2) NOT NULL COMMENT '实付金额',
`shipping_fee` decimal(10,2) DEFAULT '0.00' COMMENT '运费',
`payment_type` tinyint DEFAULT NULL COMMENT '支付方式 1-支付宝 2-微信',
`status` tinyint DEFAULT '0' COMMENT '状态 0-待支付 1-已支付 2-已发货 3-已完成 4-已取消',
`shipping_address` varchar(500) DEFAULT NULL COMMENT '收货地址',
`receiver_name` varchar(50) DEFAULT NULL COMMENT '收货人',
`receiver_phone` varchar(20) DEFAULT NULL COMMENT '收货电话',
`note` varchar(500) DEFAULT NULL COMMENT '订单备注',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_order_no` (`order_no`),
KEY `idx_user_id` (`user_id`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
数据库设计时特别注意了以下几点:
医疗电商系统对安全性要求极高,我采用Shiro+JWT实现认证授权:
java复制// JWT工具类
public class JwtUtil {
private static final String SECRET_KEY = "medical-secret-key";
private static final long EXPIRATION = 86400000; // 24小时
public static String generateToken(User user) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId());
claims.put("username", user.getUsername());
claims.put("roles", user.getRoles());
return Jwts.builder()
.setClaims(claims)
.setSubject(user.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
public static Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
}
}
// Shiro配置
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/api/login", "anon");
filterMap.put("/api/register", "anon");
filterMap.put("/api/**", "jwt");
factoryBean.setFilterChainDefinitionMap(filterMap);
return factoryBean;
}
@Bean
public DefaultWebSecurityManager securityManager(Realm realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
return securityManager;
}
@Bean
public Realm realm() {
return new JwtRealm();
}
}
安全设计要点:
药品管理是核心功能,包含以下子模块:
药品添加接口实现:
java复制@RestController
@RequestMapping("/api/medicine")
public class MedicineController {
@Autowired
private MedicineService medicineService;
@PostMapping
@RequiresRoles("admin")
public Result addMedicine(@Valid @RequestBody MedicineDTO dto) {
// 检查药品编码是否已存在
if (medicineService.existsByCode(dto.getCode())) {
return Result.fail("药品编码已存在");
}
Medicine medicine = new Medicine();
BeanUtils.copyProperties(dto, medicine);
// 处理药品图片上传
if (dto.getImageFile() != null) {
String imageUrl = fileService.upload(dto.getImageFile());
medicine.setImage(imageUrl);
}
medicineService.save(medicine);
return Result.success();
}
@GetMapping("/{id}")
public Result getMedicine(@PathVariable Long id) {
Medicine medicine = medicineService.getById(id);
if (medicine == null) {
return Result.fail("药品不存在");
}
return Result.success(medicine);
}
}
药品搜索实现:
java复制@Service
public class MedicineServiceImpl extends ServiceImpl<MedicineMapper, Medicine> implements MedicineService {
@Override
public Page<MedicineVO> search(MedicineQuery query) {
return lambdaQuery()
.like(StringUtils.isNotBlank(query.getKeyword()), Medicine::getName, query.getKeyword())
.or()
.like(StringUtils.isNotBlank(query.getKeyword()), Medicine::getCode, query.getKeyword())
.eq(query.getCategoryId() != null, Medicine::getCategoryId, query.getCategoryId())
.eq(Medicine::getStatus, 1)
.page(query.toPage())
.convert(this::toVO);
}
private MedicineVO toVO(Medicine medicine) {
MedicineVO vo = new MedicineVO();
BeanUtils.copyProperties(medicine, vo);
// 补充分类名称等额外信息
if (medicine.getCategoryId() != null) {
Category category = categoryService.getById(medicine.getCategoryId());
if (category != null) {
vo.setCategoryName(category.getName());
}
}
return vo;
}
}
购物车和订单是电商系统的核心,我设计了以下流程:
java复制@Service
public class CartServiceImpl implements CartService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private String getCartKey(Long userId) {
return "cart:" + userId;
}
@Override
public void addItem(Long userId, CartItem item) {
String key = getCartKey(userId);
// 检查药品是否存在且库存充足
Medicine medicine = medicineService.getById(item.getMedicineId());
if (medicine == null || medicine.getStatus() != 1) {
throw new BusinessException("药品不存在或已下架");
}
if (medicine.getStock() < item.getQuantity()) {
throw new BusinessException("库存不足");
}
// 使用Hash存储购物车商品
redisTemplate.opsForHash().put(key,
item.getMedicineId().toString(),
item);
// 设置购物车过期时间
redisTemplate.expire(key, 30, TimeUnit.DAYS);
}
@Override
public List<CartItem> getCart(Long userId) {
String key = getCartKey(userId);
List<Object> values = redisTemplate.opsForHash().values(key);
return values.stream()
.map(obj -> (CartItem) obj)
.collect(Collectors.toList());
}
}
java复制@Service
@Transactional
public class OrderServiceImpl implements OrderService {
@Override
public Order createOrder(Long userId, OrderCreateDTO dto) {
// 1. 验证用户
User user = userService.getById(userId);
if (user == null) {
throw new BusinessException("用户不存在");
}
// 2. 获取购物车商品
List<CartItem> cartItems = cartService.getCart(userId);
if (cartItems.isEmpty()) {
throw new BusinessException("购物车为空");
}
// 3. 检查库存并计算总价
List<Long> medicineIds = cartItems.stream()
.map(CartItem::getMedicineId)
.collect(Collectors.toList());
Map<Long, Medicine> medicineMap = medicineService.listByIds(medicineIds)
.stream()
.collect(Collectors.toMap(Medicine::getId, m -> m));
BigDecimal totalAmount = BigDecimal.ZERO;
List<OrderItem> orderItems = new ArrayList<>();
for (CartItem item : cartItems) {
Medicine medicine = medicineMap.get(item.getMedicineId());
if (medicine == null || medicine.getStatus() != 1) {
throw new BusinessException("药品" + medicine.getName() + "已下架");
}
if (medicine.getStock() < item.getQuantity()) {
throw new BusinessException("药品" + medicine.getName() + "库存不足");
}
BigDecimal itemTotal = medicine.getPrice().multiply(
new BigDecimal(item.getQuantity()));
OrderItem orderItem = new OrderItem();
orderItem.setMedicineId(medicine.getId());
orderItem.setMedicineName(medicine.getName());
orderItem.setMedicineImage(medicine.getImage());
orderItem.setPrice(medicine.getPrice());
orderItem.setQuantity(item.getQuantity());
orderItem.setTotalAmount(itemTotal);
orderItems.add(orderItem);
totalAmount = totalAmount.add(itemTotal);
}
// 4. 创建订单
Order order = new Order();
order.setOrderNo(generateOrderNo());
order.setUserId(userId);
order.setTotalAmount(totalAmount);
order.setPaymentAmount(totalAmount); // 暂不考虑优惠
order.setStatus(OrderStatus.UNPAID.getCode());
order.setShippingAddress(dto.getAddress());
order.setReceiverName(dto.getReceiverName());
order.setReceiverPhone(dto.getReceiverPhone());
order.setNote(dto.getNote());
orderMapper.insert(order);
// 5. 保存订单明细
orderItems.forEach(item -> {
item.setOrderId(order.getId());
orderItemMapper.insert(item);
// 扣减库存
medicineMapper.deductStock(
item.getMedicineId(),
item.getQuantity());
});
// 6. 清空购物车
cartService.clearCart(userId);
// 7. 发送订单创建通知
rabbitTemplate.convertAndSend(
"order.event.exchange",
"order.created",
order.getId());
return order;
}
private String generateOrderNo() {
// 时间戳+随机数生成订单号
return "M" + System.currentTimeMillis() +
ThreadLocalRandom.current().nextInt(1000, 9999);
}
}
生产环境推荐使用Docker Compose部署,架构如下:
yaml复制version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: medical
MYSQL_USER: medical
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
networks:
- medical-net
redis:
image: redis:6.2
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- medical-net
rabbitmq:
image: rabbitmq:3.9-management
ports:
- "5672:5672"
- "15672:15672"
volumes:
- rabbitmq_data:/var/lib/rabbitmq
networks:
- medical-net
backend:
build: ./backend
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- DB_URL=jdbc:mysql://mysql:3306/medical
- DB_USERNAME=medical
- DB_PASSWORD=${DB_PASSWORD}
- REDIS_HOST=redis
- RABBITMQ_HOST=rabbitmq
depends_on:
- mysql
- redis
- rabbitmq
networks:
- medical-net
frontend:
build: ./frontend
ports:
- "80:80"
networks:
- medical-net
volumes:
mysql_data:
redis_data:
rabbitmq_data:
networks:
medical-net:
driver: bridge
数据库优化:
缓存策略:
接口优化:
前端优化:
完善的监控是系统稳定的保障:
yaml复制# application-monitor.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
export:
prometheus:
enabled: true
tags:
application: medical-system
在开发这个医疗用品销售网站的过程中,我积累了一些宝贵的经验:
医疗行业特殊性:
技术选型心得:
开发建议:
常见问题解决:
这个项目从技术架构到业务实现都经过精心设计,代码规范清晰,文档完整,非常适合作为毕业设计参考,也可以作为商业项目的基础进行二次开发。