在当今移动互联网时代,手机销售行业正经历着从传统线下门店向线上电商平台的快速转型。作为一名长期从事电商系统开发的工程师,我观察到几个关键痛点:首先是库存管理混乱,很多商家还在使用Excel表格手工记录;其次是订单处理效率低下,客服人员需要在多个系统间切换;最重要的是缺乏用户行为数据分析能力,无法实现精准营销。
这个手机销售管理系统正是为解决这些问题而设计的。系统采用前后端分离架构,后端使用SpringBoot提供RESTful API服务,前端采用Vue.js构建响应式界面,数据库选用MySQL配合MyBatis进行数据持久化。这种技术组合的选择基于以下考虑:
提示:在实际项目中,我建议使用MyBatis-Plus而不是原生MyBatis,它能显著减少样板代码的编写,后文会具体说明如何集成。
后端技术栈:
前端技术栈:
数据库设计原则:
系统采用六边形架构设计,核心模块包括:
code复制src/
├── main/
│ ├── java/
│ │ └── com/
│ │ └── phoneshop/
│ │ ├── config/ # 配置类
│ │ ├── controller/ # 控制器
│ │ ├── service/ # 业务服务
│ │ ├── mapper/ # MyBatis映射
│ │ ├── entity/ # 实体类
│ │ ├── dto/ # 数据传输对象
│ │ └── exception/ # 异常处理
│ └── resources/
│ ├── mapper/ # XML映射文件
│ └── application.yml # 配置文件
用户表(sys_user)设计考虑了安全性和扩展性:
sql复制CREATE TABLE `sys_user` (
`user_id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '登录账号',
`password_hash` varchar(100) NOT NULL COMMENT 'BCrypt加密密码',
`salt` varchar(20) DEFAULT NULL COMMENT '密码盐',
`email` varchar(100) NOT NULL,
`phone` varchar(20) DEFAULT NULL,
`avatar` varchar(255) DEFAULT NULL COMMENT '头像URL',
`status` tinyint DEFAULT '1' COMMENT '0-禁用 1-正常',
`last_login_ip` varchar(50) DEFAULT NULL,
`last_login_time` datetime DEFAULT NULL,
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`user_id`),
UNIQUE KEY `idx_username` (`username`),
UNIQUE KEY `idx_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';
密码存储采用BCrypt加密算法,这是目前最安全的密码哈希方案之一。相比MD5或SHA,BCrypt专门设计用于密码存储,具有内置的盐值处理和自适应计算成本特性。
商品表(product)和商品分类表(product_category)采用关联设计:
sql复制CREATE TABLE `product` (
`product_id` bigint NOT NULL AUTO_INCREMENT,
`category_id` bigint NOT NULL,
`product_name` varchar(100) NOT NULL,
`product_code` varchar(50) NOT NULL COMMENT '商品编码',
`brand_id` bigint NOT NULL,
`price` decimal(10,2) NOT NULL,
`market_price` decimal(10,2) DEFAULT NULL,
`cost_price` decimal(10,2) DEFAULT NULL,
`stock` int NOT NULL DEFAULT '0',
`sold_num` int DEFAULT '0' COMMENT '已售数量',
`weight` decimal(10,2) DEFAULT NULL COMMENT '商品重量(kg)',
`main_image` varchar(255) DEFAULT NULL,
`sub_images` text COMMENT '逗号分隔的图片URL',
`detail` text COMMENT '商品详情HTML',
`specs` json DEFAULT NULL COMMENT '规格参数JSON',
`status` tinyint DEFAULT '1' COMMENT '0-下架 1-上架',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`product_id`),
KEY `idx_category` (`category_id`),
KEY `idx_brand` (`brand_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
特别说明specs字段使用JSON类型存储商品规格参数,这种设计可以灵活适应不同手机型号的参数差异,如:
json复制{
"屏幕尺寸": "6.7英寸",
"分辨率": "2778×1284",
"处理器": "A16仿生芯片",
"内存": "6GB",
"存储": "256GB",
"摄像头": "4800万像素主摄"
}
采用JWT(JSON Web Token)实现无状态认证,Spring Security配置如下:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/product/list").permitAll()
.antMatchers("/api/**").authenticated()
.anyRequest().permitAll()
.and()
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
JWT生成与验证的核心逻辑:
java复制public class JwtTokenProvider {
private final String secretKey;
private final long validityInMilliseconds;
public String createToken(UserDetails userDetails) {
Claims claims = Jwts.claims().setSubject(userDetails.getUsername());
claims.put("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
Date now = new Date();
Date validity = new Date(now.getTime() + validityInMilliseconds);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(validity)
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
public Authentication getAuthentication(String token) {
UserDetails userDetails = userDetailsService.loadUserByUsername(getUsername(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
// 其他验证方法...
}
商品列表接口实现Elasticsearch集成搜索:
java复制@RestController
@RequestMapping("/api/product")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("/search")
public PageResult<ProductVO> search(
@RequestParam(required = false) String keyword,
@RequestParam(required = false) Long categoryId,
@RequestParam(required = false) Long brandId,
@RequestParam(defaultValue = "0") Integer minPrice,
@RequestParam(defaultValue = "0") Integer maxPrice,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(defaultValue = "create_time") String sort,
@RequestParam(defaultValue = "desc") String order) {
ProductQueryDTO queryDTO = new ProductQueryDTO();
queryDTO.setKeyword(keyword);
queryDTO.setCategoryId(categoryId);
queryDTO.setBrandId(brandId);
queryDTO.setMinPrice(minPrice);
queryDTO.setMaxPrice(maxPrice);
queryDTO.setPageNum(pageNum);
queryDTO.setPageSize(pageSize);
queryDTO.setSort(sort);
queryDTO.setOrder(order);
return productService.searchProducts(queryDTO);
}
}
MyBatis-Plus分页插件配置:
java复制@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
订单创建采用状态机模式确保流程正确性:
java复制@Service
public class OrderServiceImpl implements OrderService {
@Transactional
public OrderCreateResult createOrder(OrderCreateDTO createDTO) {
// 1. 验证商品库存
List<ProductStockDTO> stockList = checkStock(createDTO.getItems());
// 2. 扣减库存(乐观锁)
boolean lockSuccess = productService.reduceStock(stockList);
if (!lockSuccess) {
throw new BusinessException("商品库存不足");
}
// 3. 生成订单
Order order = buildOrder(createDTO);
orderMapper.insert(order);
// 4. 生成订单明细
List<OrderItem> items = buildOrderItems(order.getOrderId(), createDTO);
orderItemMapper.insertBatch(items);
// 5. 清除购物车
cartService.clearCheckedItems(createDTO.getUserId());
// 6. 发送订单创建事件
eventPublisher.publishEvent(new OrderCreatedEvent(order));
return new OrderCreateResult(order.getOrderId(), order.getOrderAmount());
}
private List<ProductStockDTO> checkStock(List<OrderItemDTO> items) {
return items.stream()
.map(item -> {
Product product = productService.getById(item.getProductId());
if (product.getStock() < item.getQuantity()) {
throw new BusinessException(product.getProductName() + "库存不足");
}
return new ProductStockDTO(item.getProductId(), item.getQuantity());
})
.collect(Collectors.toList());
}
}
使用Seata处理跨服务事务:
java复制@GlobalTransactional
public void completeOrder(Long orderId) {
// 1. 更新订单状态为已完成
orderService.updateStatus(orderId, OrderStatus.COMPLETED);
// 2. 增加会员积分
memberService.addPoints(orderId);
// 3. 记录财务流水
accountingService.recordOrderPayment(orderId);
}
Seata配置要点:
file.conf和registry.confDataSourceProxyundo_log表采用多级缓存架构:
java复制@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats());
return cacheManager;
}
java复制@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 使用Jackson2JsonRedisSerializer序列化
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(mapper);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
商品分页查询优化前:
sql复制SELECT * FROM product
WHERE status = 1
ORDER BY create_time DESC
LIMIT 10000, 10;
优化后(使用延迟关联):
sql复制SELECT p.* FROM product p
INNER JOIN (
SELECT product_id FROM product
WHERE status = 1
ORDER BY create_time DESC
LIMIT 10000, 10
) AS tmp ON p.product_id = tmp.product_id;
java复制@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// JSON转换器配置XSS过滤
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
ObjectMapper mapper = new ObjectMapper();
mapper.getFactory().setCharacterEscapes(new HtmlCharacterEscapes());
converter.setObjectMapper(mapper);
converters.add(0, converter);
}
}
java复制http.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringAntMatchers("/api/auth/login")
);
java复制public class SensitiveInfoUtil {
public static String maskPhone(String phone) {
if (StringUtils.isEmpty(phone) || phone.length() < 7) {
return phone;
}
return phone.substring(0, 3) + "****" + phone.substring(7);
}
public static String maskEmail(String email) {
if (StringUtils.isEmpty(email) || !email.contains("@")) {
return email;
}
String[] parts = email.split("@");
if (parts[0].length() <= 1) {
return "*@" + parts[1];
}
return parts[0].charAt(0) + "***@" + parts[1];
}
}
后端Dockerfile示例:
dockerfile复制FROM openjdk:17-jdk-slim
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
使用docker-compose编排服务:
yaml复制version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: phone_shop
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:6.2
ports:
- "6379:6379"
volumes:
- redis_data:/data
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- mysql
- redis
volumes:
mysql_data:
redis_data:
yaml复制management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
yaml复制# prometheus.yml
scrape_configs:
- job_name: 'spring-boot'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['backend:8080']
java复制@Configuration
public class LogbackConfig {
@Bean
public LoggerContext loggerContext() {
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
LogstashTcpSocketAppender appender = new LogstashTcpSocketAppender();
appender.setName("LOGSTASH");
appender.setContext(context);
appender.addDestination("logstash:5044");
LogstashEncoder encoder = new LogstashEncoder();
encoder.setContext(context);
encoder.setIncludeContext(true);
encoder.start();
appender.setEncoder(encoder);
appender.start();
context.getLogger("ROOT").addAppender(appender);
return context;
}
}
在实际开发过程中,我总结了以下几点重要经验:
@TableLogic实现逻辑删除QueryWrapper而不是XML映射saveBatch方法v-memo优化长列表渲染computed缓存计算结果json复制{
"code": 200,
"message": "success",
"data": {},
"timestamp": 1672531200000
}
java复制public enum ErrorCode {
SUCCESS(200, "操作成功"),
BAD_REQUEST(400, "参数错误"),
UNAUTHORIZED(401, "未授权"),
FORBIDDEN(403, "禁止访问"),
NOT_FOUND(404, "资源不存在"),
INTERNAL_ERROR(500, "系统异常");
// ...
}
这个项目从技术选型到架构设计都经过精心考量,在实际运行中表现稳定。特别是在高并发场景下,通过Redis缓存和消息队列的合理使用,系统成功支撑了双十一期间的流量高峰。对于想要学习现代电商系统开发的同行,这个项目提供了完整的实现参考。