作为一名长期从事Java Web开发的程序员,我最近完成了一个基于SSM框架的奶茶店管理系统毕业设计项目。这个系统专门针对中小型奶茶店的信息化管理需求,解决了传统手工记账和Excel管理带来的诸多痛点。在实际开发过程中,我发现很多奶茶店老板对数字化管理有着强烈需求,但市面上针对小型店铺的解决方案要么功能过剩,要么价格昂贵。这正是我选择这个课题的主要原因。
系统采用B/S架构,后端使用Spring+SpringMVC+MyBatis三大框架组合,前端基于Vue.js实现响应式界面。主要包含四大核心模块:用户管理、奶茶分类管理、热销统计分析和特价促销管理。相比市面上通用的零售管理系统,我们特别针对奶茶行业的特点做了深度定制,比如支持按茶底、口味、温度的多维度分类,以及针对奶茶行业的促销活动模板。
在技术选型阶段,我对比了多种Java Web开发框架,最终选择SSM组合主要基于以下几点考虑:
Spring框架:提供了全面的IoC和AOP支持,能够很好地管理业务对象的生命周期和依赖关系。通过声明式事务管理,简化了数据库事务处理。在我们的奶茶系统中,商品库存变更和订单创建需要保证事务一致性,Spring的事务管理完美解决了这个问题。
SpringMVC:采用清晰的MVC分层结构,使代码组织更加规范。它的拦截器机制特别适合处理用户权限验证,我们利用这一点实现了管理员和普通用户的分权限访问控制。
MyBatis:相比Hibernate等全自动ORM框架,MyBatis的半自动化特性让我们能够编写更灵活的SQL,这对于复杂的销售统计查询尤为重要。例如,热销排行榜需要多表关联和复杂聚合计算,MyBatis的动态SQL功能大大简化了这类查询的实现。
提示:对于中小型项目,SSM框架组合在开发效率和运行性能之间取得了很好的平衡。如果是超大型分布式系统,可以考虑SpringBoot+SpringCloud组合,但对于毕业设计级别的项目,SSM已经完全够用。
前端采用Vue.js而非传统的JSP或Thymeleaf,主要出于以下考虑:
前后端分离:Vue.js作为现代前端框架,可以实现彻底的前后端分离开发。后端只需提供RESTful API,前端可以独立开发和部署。这种架构大大提升了开发效率,特别是在团队协作时。
响应式界面:奶茶店管理系统需要在不同设备上使用,Vue的响应式特性可以自动适应PC、平板和手机等多种屏幕尺寸。
丰富的生态系统:Vue周边有Element UI、Vuex、Vue Router等成熟组件库和工具,可以快速构建专业级界面。我们在系统中使用了Element UI的表格、表单和图表组件,节省了大量开发时间。
数据库采用MySQL 5.7,主要包含以下核心表:
用户表(user):存储系统用户信息,包括顾客和管理员。采用RBAC权限模型,通过role字段区分用户类型。
sql复制CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '登录账号',
`password` varchar(100) NOT NULL COMMENT 'MD5加密密码',
`real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名',
`phone` varchar(20) DEFAULT NULL COMMENT '联系电话',
`role` tinyint(4) NOT NULL DEFAULT '1' COMMENT '1-顾客 2-管理员',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
商品分类表(category):实现多级分类结构,支持无限级分类。
sql复制CREATE TABLE `category` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`parent_id` int(11) DEFAULT NULL COMMENT '父分类ID',
`name` varchar(50) NOT NULL COMMENT '分类名称',
`type` tinyint(4) NOT NULL COMMENT '1-茶底 2-口味 3-温度',
`sort` int(11) DEFAULT '0' COMMENT '排序权重',
PRIMARY KEY (`id`),
KEY `idx_parent` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
商品表(product):存储奶茶商品信息,通过分类ID关联到分类表。
sql复制CREATE TABLE `product` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`category_id` int(11) NOT NULL COMMENT '所属分类',
`name` varchar(100) NOT NULL COMMENT '商品名称',
`price` decimal(10,2) NOT NULL COMMENT '原价',
`stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存',
`description` text COMMENT '商品描述',
`image_url` varchar(255) DEFAULT NULL COMMENT '图片URL',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '1-上架 0-下架',
PRIMARY KEY (`id`),
KEY `idx_category` (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
在实际开发中,我们针对奶茶店业务特点做了以下数据库优化:
索引优化:在经常查询的字段上建立合适索引,如用户表的username、商品表的category_id等。但要注意避免过度索引,特别是对写操作频繁的表。
连接池配置:使用HikariCP连接池替代传统的DBCP,在application.properties中配置:
properties复制spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.max-lifetime=1800000
SQL优化:对于复杂的销售统计查询,我们使用存储过程实现,减少应用层计算压力。例如,计算日销售排行榜的存储过程:
sql复制DELIMITER //
CREATE PROCEDURE sp_daily_sales_rank(IN query_date DATE)
BEGIN
SELECT p.id, p.name, SUM(oi.quantity) AS sales
FROM order_item oi
JOIN product p ON oi.product_id = p.id
JOIN `order` o ON oi.order_id = o.id
WHERE DATE(o.create_time) = query_date
GROUP BY p.id, p.name
ORDER BY sales DESC
LIMIT 10;
END //
DELIMITER ;
系统采用基于Session的传统认证方式,结合Spring Security实现权限控制。用户密码使用MD5加盐哈希存储,即使数据库泄露也不会直接暴露用户密码。
java复制@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public User login(String username, String password) {
User user = userMapper.selectByUsername(username);
if(user == null) {
throw new BusinessException("用户名不存在");
}
String encrypted = DigestUtils.md5DigestAsHex((password + user.getSalt()).getBytes());
if(!encrypted.equals(user.getPassword())) {
throw new BusinessException("密码错误");
}
return user;
}
// 密码加密方法
public static String encryptPassword(String rawPassword) {
String salt = UUID.randomUUID().toString().replace("-", "").substring(0, 16);
String encrypted = DigestUtils.md5DigestAsHex((rawPassword + salt).getBytes());
return encrypted + ":" + salt;
}
}
对于管理员操作,我们使用自定义注解@RequiresRoles实现方法级权限控制:
java复制@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresRoles {
int[] value() default {2}; // 默认需要管理员角色
}
@Aspect
@Component
public class PermissionAspect {
@Around("@annotation(requiresRoles)")
public Object checkPermission(ProceedingJoinPoint joinPoint, RequiresRoles requiresRoles) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
User user = (User) request.getSession().getAttribute("currentUser");
if(user == null) {
throw new BusinessException("请先登录");
}
boolean hasRole = false;
for(int role : requiresRoles.value()) {
if(user.getRole() == role) {
hasRole = true;
break;
}
}
if(!hasRole) {
throw new BusinessException("权限不足");
}
return joinPoint.proceed();
}
}
奶茶商品需要支持多级分类展示,我们采用递归算法实现无限级分类树。前端使用Element UI的Tree组件展示,后端提供递归查询接口。
java复制public List<CategoryTreeVO> buildCategoryTree(List<Category> allCategories) {
List<CategoryTreeVO> treeList = new ArrayList<>();
// 先找出所有一级分类
List<Category> rootCategories = allCategories.stream()
.filter(c -> c.getParentId() == null)
.sorted(Comparator.comparingInt(Category::getSort))
.collect(Collectors.toList());
// 递归构建子树
for(Category root : rootCategories) {
CategoryTreeVO node = convertToTreeVO(root);
node.setChildren(getChildren(node, allCategories));
treeList.add(node);
}
return treeList;
}
private List<CategoryTreeVO> getChildren(CategoryTreeVO parent, List<Category> allCategories) {
List<CategoryTreeVO> children = new ArrayList<>();
List<Category> childCategories = allCategories.stream()
.filter(c -> parent.getId().equals(c.getParentId()))
.sorted(Comparator.comparingInt(Category::getSort))
.collect(Collectors.toList());
for(Category child : childCategories) {
CategoryTreeVO node = convertToTreeVO(child);
node.setChildren(getChildren(node, allCategories));
children.add(node);
}
return children;
}
热销统计功能通过定时任务每天凌晨计算前一天的销售数据,并将结果缓存到Redis中,避免实时计算带来的性能压力。
java复制@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void calculateSalesRank() {
LocalDate yesterday = LocalDate.now().minusDays(1);
// 查询昨日销量Top10
List<ProductSalesVO> dailyRank = productMapper.selectSalesRank(
Date.from(yesterday.atStartOfDay(ZoneId.systemDefault()).toInstant()),
Date.from(yesterday.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant()),
10);
// 存入Redis,有效期7天
String key = "sales:rank:" + yesterday.toString();
redisTemplate.opsForValue().set(key, dailyRank, 7, TimeUnit.DAYS);
// 更新周榜和月榜
updateWeeklyRank(yesterday);
updateMonthlyRank(yesterday);
}
前端使用ECharts实现销售数据的可视化展示,支持按日、周、月不同时间维度查看:
javascript复制// 销量趋势折线图
initSalesTrendChart() {
const chart = echarts.init(this.$refs.trendChart);
const option = {
title: { text: '近30天销量趋势' },
tooltip: { trigger: 'axis' },
xAxis: {
type: 'category',
data: this.trendData.dates
},
yAxis: { type: 'value' },
series: [{
data: this.trendData.values,
type: 'line',
smooth: true,
areaStyle: {}
}]
};
chart.setOption(option);
}
促销活动是奶茶店营销的重要手段,我们设计了灵活的促销模型,支持多种促销类型:
java复制public class Promotion {
private Integer id;
private String name; // 活动名称
private Integer type; // 1-单品折扣 2-满减 3-第二杯半价
private Date startTime; // 开始时间
private Date endTime; // 结束时间
private Integer status; // 0-未开始 1-进行中 2-已结束
// 不同类型活动特有字段
private BigDecimal discount; // 折扣率(0.8表示8折)
private BigDecimal fullAmount; // 满减条件金额
private BigDecimal minusAmount; // 满减金额
private Integer productId; // 参与活动的商品ID
private String productIds; // 参与活动的商品ID列表(逗号分隔)
// 关联商品信息(非数据库字段)
private List<Product> products;
}
采用策略模式实现不同类型促销的价格计算,便于扩展新的促销类型:
java复制public interface PromotionStrategy {
BigDecimal calculatePrice(BigDecimal originalPrice, Integer quantity, Promotion promotion);
}
@Component
public class DiscountStrategy implements PromotionStrategy {
@Override
public BigDecimal calculatePrice(BigDecimal originalPrice, Integer quantity, Promotion promotion) {
return originalPrice.multiply(promotion.getDiscount())
.multiply(new BigDecimal(quantity))
.setScale(2, RoundingMode.HALF_UP);
}
}
@Component
public class FullReductionStrategy implements PromotionStrategy {
@Override
public BigDecimal calculatePrice(BigDecimal originalPrice, Integer quantity, Promotion promotion) {
BigDecimal total = originalPrice.multiply(new BigDecimal(quantity));
if(total.compareTo(promotion.getFullAmount()) >= 0) {
return total.subtract(promotion.getMinusAmount());
}
return total;
}
}
@Service
public class PromotionService {
private Map<Integer, PromotionStrategy> strategyMap = new HashMap<>();
@Autowired
public PromotionService(List<PromotionStrategy> strategies) {
strategies.forEach(strategy -> {
if(strategy instanceof DiscountStrategy) {
strategyMap.put(1, strategy);
} else if(strategy instanceof FullReductionStrategy) {
strategyMap.put(2, strategy);
}
// 其他策略...
});
}
public BigDecimal calculatePromotionPrice(Promotion promotion, BigDecimal originalPrice, Integer quantity) {
PromotionStrategy strategy = strategyMap.get(promotion.getType());
if(strategy == null) {
return originalPrice.multiply(new BigDecimal(quantity));
}
return strategy.calculatePrice(originalPrice, quantity, promotion);
}
}
系统采用Tomcat + Nginx的经典部署架构:
前端部署:
npm run build生成静态资源nginx复制server {
listen 80;
server_name www.milkshop.com;
gzip on;
gzip_types text/plain application/xml text/css application/javascript;
location / {
root /usr/share/nginx/html/dist;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
后端部署:
bash复制export JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC -XX:MaxGCPauseMillis=200"
数据库查询优化:
应用层缓存:
使用Redis缓存热点数据
商品信息缓存策略:
java复制@Cacheable(value = "product", key = "#id")
public Product getProductById(Integer id) {
return productMapper.selectByPrimaryKey(id);
}
@CacheEvict(value = "product", key = "#product.id")
public void updateProduct(Product product) {
productMapper.updateByPrimaryKey(product);
}
异步处理:
使用Spring的@Async注解异步处理非核心业务
例如订单完成后的通知消息发送:
java复制@Async
public void sendOrderNotification(Order order) {
// 发送短信/邮件通知
notificationService.sendSms(order.getUser().getPhone(),
"您的订单已支付成功,金额:" + order.getTotalAmount());
}
在开发这个奶茶店管理系统的过程中,我积累了一些宝贵的经验教训:
版本兼容性问题:
MyBatis分页插件坑:
PageHelper与某些MyBatis版本冲突,导致分页失效
正确配置方式:
xml复制<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<property name="helperDialect" value="mysql"/>
<property name="reasonable" value="true"/>
</plugin>
</plugins>
Vue跨域问题:
开发环境下前端访问后端API会遇到跨域限制
解决方案一:配置后端CORS
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true)
.maxAge(3600);
}
}
解决方案二:配置vue.config.js代理
javascript复制module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
}
}
时间处理建议:
前后端统一使用UTC时间传输
数据库存储datetime类型
前端展示时根据用户时区转换:
javascript复制import moment from 'moment';
function formatTime(utcTime) {
return moment.utc(utcTime).local().format('YYYY-MM-DD HH:mm:ss');
}
这个奶茶店管理系统从需求分析到最终部署上线,让我对SSM框架的实际应用有了更深入的理解。最大的收获是学会了如何根据业务特点设计合理的系统架构,并在开发过程中不断优化调整。特别是促销模块的策略模式实现,让我体会到设计模式的强大之处。