这个基于JSP和SSM框架的网上购物商城系统,是我在电商领域摸爬滚打多年后总结出的一个典型实现方案。采用Spring+SpringMVC+MyBatis三大框架组合,配合EasyUI前端组件,构建了一个功能完备的B2C电商平台。系统前后端分离的设计思路,既保证了后台管理的高效性,又兼顾了前端用户的交互体验。
在实际开发中,我选择了Eclipse作为IDE,JDK1.8提供基础运行环境,Tomcat7.0作为应用服务器,MySQL5.7存储业务数据。这套技术栈组合经过多个项目的验证,在稳定性、开发效率和性能表现上达到了很好的平衡。特别是SSM框架的轻量级特性,使得系统在中小型电商场景下能够快速部署和迭代。
选择SSM框架组合(Spring+SpringMVC+MyBatis)主要基于以下几个考量:
Spring框架:作为整个应用的IoC容器,管理着所有Bean的生命周期。通过声明式事务管理(@Transactional)确保订单、库存等核心业务的原子性操作。在实际项目中,我特别配置了多数据源支持,为后续可能的读写分离做准备。
SpringMVC:采用基于注解的控制器设计,RESTful风格的API接口使得前后端交互更加清晰。例如商品搜索接口设计为:
java复制@GetMapping("/products/search")
public ResponseEntity<List<Product>> searchProducts(
@RequestParam String keyword,
@RequestParam(defaultValue = "0") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
// 实现逻辑
}
xml复制<select id="selectByConditions" resultMap="productResultMap">
SELECT * FROM product
<where>
<if test="categoryId != null">
AND category_id = #{categoryId}
</if>
<if test="minPrice != null">
AND price >= #{minPrice}
</if>
<!-- 更多条件 -->
</where>
ORDER BY ${orderBy} ${order}
</select>
前端采用JSP+EasyUI的组合主要考虑以下因素:
javascript复制$('#productGrid').datagrid({
url:'/admin/products',
pagination:true,
pageSize:20,
columns:[[
{field:'id',title:'ID',width:50},
{field:'name',title:'商品名称',width:200},
// 更多列配置
]]
});
电商系统的数据库设计需要特别注意扩展性和性能。主要表结构包括:
sql复制CREATE TABLE `product` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`category_id` bigint(20) NOT NULL,
`name` varchar(100) NOT NULL,
`price` decimal(10,2) NOT NULL,
`stock` int(11) NOT NULL,
`status` tinyint(4) DEFAULT '1',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_category` (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
sql复制CREATE TABLE `order_master` (
`order_id` varchar(32) NOT NULL,
`user_id` bigint(20) NOT NULL,
`total_amount` decimal(10,2) NOT NULL,
`order_status` tinyint(4) NOT NULL DEFAULT '0',
`pay_status` tinyint(4) NOT NULL DEFAULT '0',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`order_id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
sql复制ALTER TABLE `user` ADD INDEX `idx_mobile` (`mobile`);
分表分库考虑:虽然当前项目使用单库,但在表设计时已经预留了分片字段。例如订单表中的user_id既可以用于查询优化,也方便后续按用户ID分库分表。
字段类型选择:金额字段使用DECIMAL(10,2)避免浮点精度问题,状态字段使用TINYINT而不是VARCHAR节省存储空间。
购物车实现需要考虑未登录用户和已登录用户的不同场景:
json复制{
"cartItems": [
{
"productId": 123,
"quantity": 2,
"selected": true
}
]
}
sql复制CREATE TABLE `cart_item` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL,
`product_id` bigint(20) NOT NULL,
`quantity` int(11) NOT NULL,
`selected` tinyint(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_product` (`user_id`,`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
java复制public void mergeCart(Long userId, String cookieCart) {
List<CartItem> cookieItems = parseCookieCart(cookieCart);
List<CartItem> dbItems = cartMapper.selectByUserId(userId);
// 合并逻辑
Map<Long, CartItem> productMap = new HashMap<>();
dbItems.forEach(item -> productMap.put(item.getProductId(), item));
for (CartItem cookieItem : cookieItems) {
CartItem dbItem = productMap.get(cookieItem.getProductId());
if (dbItem != null) {
dbItem.setQuantity(dbItem.getQuantity() + cookieItem.getQuantity());
} else {
cookieItem.setUserId(userId);
cartMapper.insert(cookieItem);
}
}
// 更新已有商品数量
productMap.values().forEach(item -> cartMapper.updateQuantity(
item.getId(), item.getQuantity()));
}
订单创建是电商系统的核心难点,需要处理库存、优惠、支付等多个环节:
mermaid复制graph TD
A[验证购物车商品] --> B[锁定库存]
B --> C[计算订单金额]
C --> D[生成订单]
D --> E[清除购物车]
E --> F[跳转支付]
java复制@Transactional
public String createOrder(OrderDTO orderDTO) {
// 1. 查询商品信息
List<OrderDetail> orderDetails = orderDTO.getOrderDetails();
List<Long> productIds = orderDetails.stream()
.map(OrderDetail::getProductId)
.collect(Collectors.toList());
List<Product> products = productRepository.findByIdIn(productIds);
// 2. 计算总价
BigDecimal orderAmount = calculateOrderAmount(orderDetails, products);
// 3. 扣减库存
reduceStock(orderDetails);
// 4. 订单入库
OrderMaster orderMaster = new OrderMaster();
orderMaster.setOrderId(generateOrderId());
orderMaster.setOrderAmount(orderAmount);
// 其他字段设置...
orderMasterRepository.save(orderMaster);
// 5. 发送订单创建事件
eventPublisher.publishEvent(new OrderCreatedEvent(orderMaster.getOrderId()));
return orderMaster.getOrderId();
}
java复制public static String generateOrderId() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
String timeStr = sdf.format(new Date());
Random random = new Random();
int randomNum = random.nextInt(900) + 100; // 100-999
return timeStr + randomNum;
}
后台商品管理采用CRUD标准操作,重点在于:
jsp复制<script type="text/javascript" src="/ueditor/ueditor.config.js"></script>
<script type="text/javascript" src="/ueditor/ueditor.all.js"></script>
<script>
var ue = UE.getEditor('editor', {
initialFrameHeight: 300
});
</script>
<textarea id="editor" name="detail">${product.detail}</textarea>
java复制@PostMapping("/upload")
public ResponseEntity<Map<String, String>> uploadImage(
@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return ResponseEntity.badRequest().build();
}
try {
String fileName = file.getOriginalFilename();
String ext = fileName.substring(fileName.lastIndexOf("."));
String newFileName = UUID.randomUUID() + ext;
Path path = Paths.get(uploadPath, newFileName);
Files.copy(file.getInputStream(), path, StandardCopyOption.REPLACE_EXISTING);
Map<String, String> result = new HashMap<>();
result.put("url", "/uploads/" + newFileName);
return ResponseEntity.ok(result);
} catch (IOException e) {
return ResponseEntity.status(500).build();
}
}
使用ECharts实现销售数据可视化:
java复制@GetMapping("/stats/sales")
public ResponseEntity<SalesStatsVO> getSalesStats(
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date start,
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date end) {
if (start == null) {
start = DateUtils.addDays(new Date(), -30);
}
if (end == null) {
end = new Date();
}
List<DailySales> dailySales = orderRepository.findDailySales(start, end);
BigDecimal totalAmount = orderRepository.sumAmountByPeriod(start, end);
SalesStatsVO vo = new SalesStatsVO();
vo.setDailySales(dailySales);
vo.setTotalAmount(totalAmount);
return ResponseEntity.ok(vo);
}
javascript复制$.get('/admin/stats/sales', function(response) {
var dates = response.dailySales.map(item => item.date);
var amounts = response.dailySales.map(item => item.amount);
var chart = echarts.init(document.getElementById('salesChart'));
var option = {
title: { text: '近30天销售趋势' },
tooltip: { trigger: 'axis' },
xAxis: { data: dates },
yAxis: { type: 'value' },
series: [{
name: '销售额',
type: 'line',
data: amounts
}]
};
chart.setOption(option);
});
xml复制<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
maxThreads="200"
minSpareThreads="20"
acceptCount="100"
enableLookups="false"
URIEncoding="UTF-8" />
properties复制spring.datasource.url=jdbc:mysql://localhost:3306/mall?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=yourpassword
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.tomcat.max-active=50
spring.datasource.tomcat.max-idle=10
spring.datasource.tomcat.min-idle=5
spring.datasource.tomcat.max-wait=10000
java复制@Cacheable(value = "product", key = "#productId")
public Product getProductById(Long productId) {
return productMapper.selectByPrimaryKey(productId);
}
@CacheEvict(value = "product", key = "#productId")
public void updateProduct(Product product) {
productMapper.updateByPrimaryKeySelective(product);
}
java复制@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
jsp复制<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
${fn:escapeXml(userInput)}
java复制@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
java复制// 错误示范
String sql = "SELECT * FROM user WHERE username = '" + username + "'";
// 正确做法
@Select("SELECT * FROM user WHERE username = #{username}")
User findByUsername(@Param("username") String username);
java复制public class AESUtil {
private static final String KEY = "your-secret-key";
public static String encrypt(String data) throws Exception {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(), "AES");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] encrypted = cipher.doFinal(data.getBytes());
return Base64.getEncoder().encodeToString(encrypted);
}
// 解密方法类似
}
java复制public boolean verifySign(Map<String, String> params, String sign) {
// 1. 过滤空值和签名参数
Map<String, String> filtered = params.entrySet().stream()
.filter(e -> e.getValue() != null && !e.getKey().equals("sign"))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
// 2. 按参数名排序
String sorted = filtered.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&"));
// 3. MD5签名
String calculatedSign = DigestUtils.md5Hex(sorted + "your-secret-key");
return calculatedSign.equals(sign);
}
随着业务规模扩大,可以考虑将单体应用拆分为微服务:
在实际开发过程中,我总结了以下几点重要经验:
事务边界控制:订单创建涉及多个数据库操作,必须合理设置事务边界。过大的事务范围会导致锁持有时间过长,影响系统并发性能。建议将非核心操作(如日志记录)放在事务外执行。
库存扣减策略:直接使用数据库行锁(SELECT FOR UPDATE)虽然简单,但在高并发场景下性能较差。可以考虑:
缓存一致性:商品信息变更后,必须及时清除相关缓存。我采用"先更新数据库,再删除缓存"的策略,并通过消息队列异步处理缓存更新,降低对主流程的影响。
日志监控:完善的日志系统对问题排查至关重要:
这套电商系统经过多次迭代,已经形成了相对稳定的架构。在开发过程中,我深刻体会到良好的设计模式和规范的编码习惯对项目可维护性的重要性。特别是在电商这种业务复杂的系统中,清晰的模块划分和合理的抽象层次能显著降低后期维护成本。