1. 项目背景与核心需求
作为一名在Java企业级开发领域摸爬滚打多年的老码农,我最近指导了几位应届生的毕业设计项目。其中这个高校超市管理系统让我印象深刻——它完美诠释了如何用SpringBoot解决实际场景中的管理痛点。不同于市面上通用的超市系统,高校场景有着独特的业务特点:
- 用户群体固定:主要服务师生,会员体系与校园卡对接
- 商品结构特殊:文具、教材占比高,存在季节性采购高峰
- 营业时段集中:课间、午休时段会出现爆发式流量
- 管理权限复杂:涉及后勤处、财务处、超市员工多级审批
传统的手工记账方式在面对上千种商品和日流水过万的交易时,经常出现库存不准、对账困难等问题。这正是我们需要开发该系统的核心驱动力。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与架构设计
2.1 技术栈决策过程
选择SpringBoot+MyBatis组合绝非偶然。我们团队在技术选型时做了如下对比:
| 技术方案 | 开发效率 | 性能表现 | 学习成本 | 社区支持 |
|---|---|---|---|---|
| SpringBoot | ★★★★★ | ★★★★ | ★★★ | ★★★★★ |
| 传统SSM | ★★★ | ★★★★ | ★★★★ | ★★★★ |
| PlayFramework | ★★★★ | ★★★★ | ★★★★ | ★★★ |
| JFinal | ★★★★ | ★★★ | ★★ | ★★ |
最终选择SpringBoot基于以下考量:
- 内嵌Tomcat简化部署
- Starter依赖自动配置
- Actuator提供完善监控
- 与MyBatis无缝集成
2.2 数据库设计要点
商品表的设计尤其值得关注:
sql复制CREATE TABLE `product` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`barcode` varchar(20) NOT NULL COMMENT '国际条码',
`name` varchar(100) NOT NULL COMMENT '商品名称',
`category_id` int(11) NOT NULL COMMENT '分类ID',
`spec` varchar(50) DEFAULT NULL COMMENT '规格',
`shelf_life` int(11) DEFAULT NULL COMMENT '保质期(天)',
`purchase_price` decimal(10,2) NOT NULL COMMENT '进价',
`retail_price` decimal(10,2) NOT NULL COMMENT '售价',
`warning_stock` int(11) DEFAULT '10' COMMENT '库存预警值',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_barcode` (`barcode`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
关键设计原则:
- 使用utf8mb4字符集支持emoji商品名
- 条码字段建立唯一索引
- 价格使用DECIMAL避免精度丢失
- 设置库存预警阈值
3. 核心功能实现细节
3.1 入库管理模块
入库流程采用状态机模式设计:
java复制public enum StockInStatus {
DRAFT(0), // 草稿
APPROVING(1), // 审批中
PART_IN(2), // 部分入库
COMPLETED(3), // 已完成
REJECTED(4); // 已驳回
private final int value;
// 省略构造方法
}
入库业务逻辑包含以下关键点:
- 支持Excel批量导入(使用EasyExcel避免OOM)
- 采用乐观锁处理并发修改:
java复制@Update("UPDATE stock_in_detail SET quantity = quantity + #{qty}, version = version + 1
WHERE id = #{id} AND version = #{version}")
int updateStock(@Param("id") Long id,
@Param("qty") Integer qty,
@Param("version") Integer version);
3.2 权限控制方案
基于Shiro实现RBAC模型时,我们扩展了动态权限特性:
java复制public class DynamicFilter extends AuthorizationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request,
ServletResponse response,
Object mappedValue) {
// 从数据库实时校验权限
String requestURI = getPathWithinApplication(request);
Subject subject = getSubject(request, response);
return checkPermission(subject, requestURI);
}
}
权限表关系设计:
- sys_user 用户表
- sys_role 角色表
- sys_menu 菜单表
- sys_user_role 用户角色关联
- sys_role_menu 角色菜单关联
- sys_operation_log 操作日志
4. 性能优化实战
4.1 高并发场景应对
在促销活动期间,我们遭遇了典型的秒杀问题。通过以下方案解决:
- 缓存策略:
java复制@Cacheable(value = "product", key = "#id",
unless = "#result == null")
public Product getProductById(Long id) {
return productMapper.selectById(id);
}
- 库存扣减方案对比:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 数据库乐观锁 | 实现简单 | 高并发时重试次数多 |
| Redis原子操作 | 性能极高 | 存在数据不一致风险 |
| 分布式锁+本地缓存 | 平衡性能与一致性 | 实现复杂度高 |
最终采用Redis Lua脚本方案:
lua复制local key = KEYS[1]
local change = tonumber(ARGV[1])
local current = tonumber(redis.call('GET', key))
if current >= change then
return redis.call('INCRBY', key, -change)
else
return -1
end
4.2 数据库优化案例
商品查询接口从最初的800ms优化到80ms,关键步骤:
- 添加组合索引:
sql复制ALTER TABLE product
ADD INDEX idx_category_price (category_id, retail_price);
- 重构查询语句:
xml复制<select id="searchProducts" resultType="ProductDTO">
SELECT p.*, c.name as categoryName
FROM product p
LEFT JOIN category c ON p.category_id = c.id
<where>
<if test="param.categoryId != null">
AND p.category_id = #{param.categoryId}
</if>
<if test="param.minPrice != null">
AND p.retail_price >= #{param.minPrice}
</if>
<!-- 更多条件 -->
</where>
ORDER BY
<choose>
<when test="param.sortBy == 'price'">p.retail_price</when>
<otherwise>p.sales_volume DESC</otherwise>
</choose>
</select>
5. 踩坑实录与解决方案
5.1 事务失效场景
问题现象:
@Transactional注解的方法内异常被捕获后,事务未回滚
根本原因:
默认只对RuntimeException回滚
解决方案:
java复制@Transactional(rollbackFor = Exception.class)
public void createOrder(Order order) {
try {
orderMapper.insert(order);
inventoryService.reduceStock(order.getItems());
} catch (Exception e) {
log.error("创建订单失败", e);
throw new BusinessException("订单创建失败"); // 转换为非检查异常
}
}
5.2 日期处理陷阱
错误示例:
java复制// 使用DateUtils.parseDate可能抛ParseException
Date expiryDate = DateUtils.parseDate("2023-02-30");
正确做法:
java复制// 使用严格模式校验
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
Date expiryDate = sdf.parse("2023-02-30"); // 抛出ParseException
} catch (ParseException e) {
throw new IllegalArgumentException("无效日期格式");
}
6. 项目部署实践
6.1 多环境配置
采用Spring Profile管理环境差异:
yaml复制# application-dev.yml
server:
port: 8080
datasource:
url: jdbc:mysql://localhost:3306/supermarket_dev
# application-prod.yml
datasource:
url: jdbc:mysql://cluster-xxx.rds.aliyuncs.com:3306/supermarket
hikari:
maximum-pool-size: 20
启动时指定profile:
bash复制java -jar supermarket.jar --spring.profiles.active=prod
6.2 健康检查配置
添加Actuator端点监控:
yaml复制management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
probes:
enabled: true
自定义健康指标:
java复制@Component
public class InventoryHealthIndicator
extends AbstractHealthIndicator {
@Autowired
private InventoryMapper inventoryMapper;
@Override
protected void doHealthCheck(Health.Builder builder) {
long warningCount = inventoryMapper.countWarningStock();
if(warningCount > 0) {
builder.warning().withDetail("warningItems", warningCount);
} else {
builder.up();
}
}
}
7. 项目扩展方向
在实际使用过程中,我们发现还可以进一步优化:
- 移动端适配:开发微信小程序版本,支持扫码购
- 智能补货系统:基于历史销售数据的预测模型
- 视觉识别收银:集成OpenCV实现商品图像识别
- 供应链协同:与供应商系统对接实现自动订货
这个项目让我深刻体会到,一个好的管理系统不仅要考虑技术实现,更要深入理解业务场景。特别是在高校这种特殊环境下,需要平衡管理规范性和操作便捷性。建议后续开发者可以多与一线超市员工交流,收集真实反馈来持续优化系统。
