随着教育信息化的不断深入,学习用品的采购方式也迎来了数字化转型。传统的线下文具店采购模式存在品类有限、价格不透明、受时间和地点制约等诸多不便。为此,我们设计并实现了一个名为“学海优品”的在线学习用品商城系统。该系统基于成熟的SSM(Spring + SpringMVC + MyBatis)技术栈构建,旨在为教师、学生及家长提供一个品类齐全、操作便捷、高效安全的一站式学习用品采购平台。
系统架构与技术栈
“学海优品”系统采用经典的三层架构模式,实现了表现层、业务逻辑层和数据持久层的清晰分离,确保了系统的高内聚、低耦合特性。
- 表现层:基于SpringMVC框架构建,负责接收用户HTTP请求、调用业务逻辑处理并渲染视图返回响应。通过
@Controller注解声明控制器,利用@RequestMapping映射请求路径,实现了灵活的请求路由。视图层采用JSP(JavaServer Pages)技术,结合JSTL标签库和EL表达式,实现了动态内容的展示,同时保证了页面逻辑的简洁。 - 业务逻辑层:由Spring Framework的核心IoC(控制反转)容器管理。所有业务逻辑组件,如商品服务、用户服务、订单服务等,均以
@Service注解声明,并通过@Autowired注解实现依赖注入。Spring的声明式事务管理(@Transactional)被广泛应用于订单创建、库存扣减等需要保证数据一致性的核心业务场景,确保了操作的原子性。 - 数据持久层:选用MyBatis作为ORM框架。它通过XML配置文件或注解的方式,将Java对象与数据库表记录进行映射。MyBatis提供了强大的动态SQL能力,能够灵活地构建复杂的查询条件。与Hibernate等全自动ORM框架相比,MyBatis允许开发者对执行的SQL语句进行精确控制,这对于性能要求较高的电商查询操作尤为重要。
- 其他技术:项目依赖管理由Maven完成,数据库采用稳定可靠的MySQL,前端页面使用HTML、CSS和JavaScript进行构建和交互。
数据库设计与核心表分析
数据库是系统的基石,良好的设计是保证系统性能、数据一致性和扩展性的关键。本系统共设计11张核心表,以下对其中的几个关键表进行深入分析。
1. 商品信息表 (stationery)
商品表是电商系统的核心,其设计直接影响到商品展示、搜索和库存管理的效率。
CREATE TABLE `stationery` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`name` varchar(255) NOT NULL COMMENT '商品名称',
`type_id` int(11) NOT NULL COMMENT '商品分类ID',
`price` decimal(10,2) NOT NULL COMMENT '商品价格',
`original_price` decimal(10,2) DEFAULT NULL COMMENT '商品原价',
`stock` int(11) NOT NULL DEFAULT '0' COMMENT '商品库存',
`image_url` varchar(500) DEFAULT NULL COMMENT '商品图片URL',
`description` text COMMENT '商品描述',
`sales_volume` int(11) DEFAULT '0' COMMENT '商品销量',
`status` tinyint(4) DEFAULT '1' COMMENT '商品状态(1:上架,0:下架)',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_type_id` (`type_id`),
KEY `idx_status` (`status`),
KEY `idx_sales_volume` (`sales_volume`),
CONSTRAINT `fk_stationery_type` FOREIGN KEY (`type_id`) REFERENCES `stationery_type` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品信息表';
设计亮点分析:
- 字段完整性:表结构涵盖了商品的基本信息(名称、价格)、业务信息(库存、销量、状态)、扩展信息(描述、图片)以及审计信息(创建/更新时间),设计全面。
- 索引优化:针对高频查询场景,建立了多个索引。
idx_type_id用于加速按分类筛选商品;idx_status用于快速查询已上架商品;idx_sales_volume则便于按销量排序,实现热门商品推荐。合理的索引策略是应对电商系统海量商品数据查询性能挑战的关键。 - 外键约束:通过
FOREIGN KEY与商品分类表(stationery_type)关联,保证了数据的一致性和完整性,避免了“脏数据”的产生。 - 价格精度:使用
DECIMAL(10,2)类型存储价格,精确到分,完全符合金融计算的要求。
2. 订单主表 (orders)
订单表记录了交易的核心信息,其设计需要兼顾查询效率与业务逻辑的复杂性。
CREATE TABLE `orders` (
`id` varchar(32) NOT NULL COMMENT '订单ID(非自增,例如使用UUID或雪花算法ID)',
`user_id` int(11) NOT NULL COMMENT '用户ID',
`total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额',
`actual_amount` decimal(10,2) NOT NULL COMMENT '实付金额',
`payment_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '支付状态(0:待支付,1:已支付,2:已退款)',
`order_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '订单状态(0:待发货,1:已发货,2:已完成,3:已取消)',
`shipping_address` varchar(500) NOT NULL COMMENT '收货地址',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`pay_time` datetime DEFAULT NULL COMMENT '支付时间',
`shipping_time` datetime DEFAULT NULL COMMENT '发货时间',
`end_time` datetime DEFAULT NULL COMMENT '交易完成时间',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_create_time` (`create_time`),
KEY `idx_payment_status` (`payment_status`),
KEY `idx_order_status` (`order_status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表';
设计亮点分析:
- 订单ID设计:主键
id未采用传统的自增整数,而是使用字符串(如UUID或分布式ID生成算法如雪花算法)。这样做的好处是:1)在分布式系统环境下保证ID全局唯一;2)避免通过ID序号推测平台订单量等敏感信息。 - 状态字段分离:将
payment_status(支付状态)和order_status(订单状态)分离。这种设计使得状态流转更加清晰灵活,例如可以处理“已支付但未发货”或“已发货但用户申请退款”等复杂业务场景。 - 时间戳记录:不仅记录了订单创建时间,还详细记录了支付、发货、完成等关键节点的时间。这些数据对于后续的业务分析、用户行为追踪和纠纷处理至关重要。
- 复合索引考虑:虽然当前索引是单字段的,但在实际业务中,查询“某个用户待支付的订单”(
user_id+payment_status)是非常高频的操作。在数据量增大后,可以考虑建立(user_id, payment_status)这样的复合索引来进一步提升查询性能。
3. 购物车表 (cart)
购物车作为用户操作的临时载体,其设计需要关注并发和数据清理。
CREATE TABLE `cart` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL COMMENT '用户ID',
`stationery_id` int(11) NOT NULL COMMENT '商品ID',
`quantity` int(11) NOT NULL COMMENT '商品数量',
`selected` tinyint(1) DEFAULT '1' COMMENT '是否选中(1:是,0:否)',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_stationery` (`user_id`, `stationery_id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='购物车表';
设计亮点分析:
- 唯一性约束:通过
UNIQUE KEY uk_user_stationery (user_id, stationery_id),确保同一个用户不能重复添加同一商品到购物车,而是应该更新已有记录的数量。这有效防止了数据冗余。 - 选中状态:
selected字段支持用户在前端灵活选择购物车中的部分商品进行结算,是实现复杂购物车逻辑的基础。 - 性能考虑:基于
user_id的索引可以快速获取指定用户的整个购物车列表,响应迅速。
核心功能模块深度解析
1. 商品浏览与分类检索
商品展示是商城系统的门面。系统实现了多维度的商品浏览方式,包括分类导航、关键词搜索、排序等。
后端控制器代码示例 (StationeryController.java):
@Controller
@RequestMapping("/stationery")
public class StationeryController {
@Autowired
private StationeryService stationeryService;
/**
* 根据分类ID分页查询商品
* @param typeId 分类ID
* @param pageNum 页码
* @param pageSize 每页大小
* @param model SpringMVC模型
* @return 商品列表页面
*/
@GetMapping("/category/{typeId}")
public String getStationeryByType(@PathVariable("typeId") Integer typeId,
@RequestParam(value = "page", defaultValue = "1") Integer pageNum,
@RequestParam(value = "size", defaultValue = "12") Integer pageSize,
Model model) {
// 构建分页请求
PageRequest pageRequest = PageRequest.of(pageNum - 1, pageSize);
// 调用服务层查询
Page<Stationery> stationeryPage = stationeryService.findByTypeId(typeId, pageRequest);
// 将结果添加到模型,供视图层渲染
model.addAttribute("stationeryPage", stationeryPage);
model.addAttribute("typeId", typeId);
return "stationery/list";
}
/**
* 商品关键词搜索
* @param keyword 搜索关键词
* @param pageNum 页码
* @param pageSize 每页大小
* @param model SpringMVC模型
* @return 搜索结果页面
*/
@GetMapping("/search")
public String searchStationery(@RequestParam("keyword") String keyword,
@RequestParam(value = "page", defaultValue = "1") Integer pageNum,
@RequestParam(value = "size", defaultValue = "12") Integer pageSize,
Model model) {
PageRequest pageRequest = PageRequest.of(pageNum - 1, pageSize);
Page<Stationery> stationeryPage = stationeryService.searchByName(keyword, pageRequest);
model.addAttribute("stationeryPage", stationeryPage);
model.addAttribute("keyword", keyword);
return "stationery/search-result";
}
}
服务层代码片段 (StationeryServiceImpl.java):
@Service
@Transactional(readOnly = true) // 查询操作设置为只读事务,提升性能
public class StationeryServiceImpl implements StationeryService {
@Autowired
private StationeryMapper stationeryMapper;
@Override
public Page<Stationery> findByTypeId(Integer typeId, PageRequest pageRequest) {
// 1. 根据条件查询总记录数
long total = stationeryMapper.countByTypeId(typeId);
// 2. 查询当前页数据列表
List<Stationery> content = stationeryMapper.selectByTypeId(typeId, pageRequest);
// 3. 构造Page对象返回
return new PageImpl<>(content, pageRequest, total);
}
@Override
public Page<Stationery> searchByName(String keyword, PageRequest pageRequest) {
// 处理关键词,例如添加模糊匹配符
String processedKeyword = "%" + keyword.trim() + "%";
long total = stationeryMapper.countByNameLike(processedKeyword);
List<Stationery> content = stationeryMapper.selectByNameLike(processedKeyword, pageRequest);
return new PageImpl<>(content, pageRequest, total);
}
}
MyBatis Mapper XML 片段 (StationeryMapper.xml):
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xuehaiyoupin.mapper.StationeryMapper">
<sql id="Base_Column_List">
id, name, type_id, price, stock, image_url, sales_volume, status
</sql>
<!-- 根据分类ID查询商品(带分页) -->
<select id="selectByTypeId" resultType="com.xuehaiyoupin.entity.Stationery">
SELECT
<include refid="Base_Column_List"/>
FROM stationery
WHERE type_id = #{typeId}
AND status = 1 <!-- 只查询上架商品 -->
ORDER BY sales_volume DESC, create_time DESC
LIMIT #{offset}, #{pageSize}
</select>
<!-- 根据商品名称模糊查询 -->
<select id="selectByNameLike" resultType="com.xuehaiyoupin.entity.Stationery">
SELECT
<include refid="Base_Column_List"/>
FROM stationery
WHERE name LIKE #{keyword}
AND status = 1
ORDER BY
CASE WHEN name LIKE CONCAT(#{keyword}, '%') THEN 1 ELSE 2 END, -- 前缀匹配的优先级更高
sales_volume DESC
LIMIT #{offset}, #{pageSize}
</select>
<select id="countByTypeId" parameterType="int" resultType="long">
SELECT COUNT(*) FROM stationery WHERE type_id = #{typeId} AND status = 1
</select>
</mapper>
图:清晰的商品分类浏览界面,用户可快速定位所需品类。
2. 购物车管理与订单生成
购物车是连接商品浏览和订单结算的桥梁,其核心逻辑包括添加商品、更新数量、选中状态切换以及生成订单。
购物车项实体类 (CartItem.java):
public class CartItem {
private Integer id;
private Integer userId;
private Stationery stationery; // 关联的商品对象,而非仅ID
private Integer quantity;
private Boolean selected;
// ... getters and setters
/**
* 计算当前购物车项的总价
* @return 商品单价 * 数量
*/
public BigDecimal getTotalPrice() {
if (stationery != null && stationery.getPrice() != null) {
return stationery.getPrice().multiply(new BigDecimal(quantity));
}
return BigDecimal.ZERO;
}
}
订单生成服务核心代码 (OrderServiceImpl.java):
@Service
@Transactional // 订单生成涉及多表操作,需要事务管理
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderDetailMapper orderDetailMapper;
@Autowired
private StationeryMapper stationeryMapper;
@Autowired
private CartService cartService;
@Override
public String createOrder(Integer userId, List<Integer> cartItemIds, String shippingAddress) {
// 1. 参数校验
if (userId == null || cartItemIds == null || cartItemIds.isEmpty()) {
throw new IllegalArgumentException("参数错误");
}
// 2. 验证并获取选中的购物车项
List<CartItem> selectedItems = cartService.getSelectedItems(userId, cartItemIds);
if (selectedItems.isEmpty()) {
throw new RuntimeException("购物车中无选中的商品");
}
// 3. 计算订单总金额并校验库存
BigDecimal totalAmount = BigDecimal.ZERO;
for (CartItem item : selectedItems) {
Stationery stationery = item.getStationery();
if (stationery.getStock() < item.getQuantity()) {
throw new RuntimeException("商品【" + stationery.getName() + "】库存不足");
}
totalAmount = totalAmount.add(item.getTotalPrice());
}
// 4. 生成订单ID(这里使用简单UUID,生产环境建议用雪花算法)
String orderId = UUID.randomUUID().toString().replace("-", "").toUpperCase();
// 5. 创建订单主记录
Order order = new Order();
order.setId(orderId);
order.setUserId(userId);
order.setTotalAmount(totalAmount);
order.setActualAmount(totalAmount); // 假设无优惠,实付金额等于总金额
order.setShippingAddress(shippingAddress);
order.setPaymentStatus(0); // 待支付
order.setOrderStatus(0); // 待发货
order.setCreateTime(new Date());
orderMapper.insert(order);
// 6. 创建订单明细记录并扣减库存
for (CartItem item : selectedItems) {
OrderDetail detail = new OrderDetail();
detail.setOrderId(orderId);
detail.setStationeryId(item.getStationery().getId());
detail.setQuantity(item.getQuantity());
detail.setPrice(item.getStationery().getPrice());
orderDetailMapper.insert(detail);
// 扣减库存
int updateCount = stationeryMapper.decreaseStock(item.getStationery().getId(), item.getQuantity());
if (updateCount == 0) {
// 如果扣减失败,抛出异常,事务回滚
throw new RuntimeException("商品库存更新失败,可能已被其他用户购买");
}
}
// 7. 清空已生成订单的购物车项
cartService.deleteItemsByIds(cartItemIds);
return orderId;
}
}
图:用户查看商品详情并可将商品加入购物车。