1. 项目概述与核心需求
烟酒销售管理系统是一个典型的B2C电商平台,专为烟酒类商品在线销售场景设计。这类系统在技术实现上需要兼顾电商平台的通用功能(如商品展示、购物车、订单处理)和烟酒行业的特殊需求(如年龄验证、物流限制等)。
我在实际开发中发现,这类系统与传统电商平台最大的区别在于:
- 严格的用户身份核验机制(需对接公安系统API进行实名认证)
- 特殊的物流配送规则(如部分区域禁运)
- 商品分类的行业标准(如按酒精度数、香烟焦油量等维度划分)
重要提示:开发烟酒电商系统需特别注意合规性要求,所有功能设计必须符合《未成年人保护法》和《烟草专卖法》相关规定。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术栈选型分析
2.1 前端技术选型:Vue.js
选择Vue 3.x版本作为前端框架主要基于以下考量:
- 组件化开发模式适合电商系统的模块化需求
- 响应式数据绑定简化了商品状态管理
- 丰富的生态系统(Vuex状态管理、Vue Router路由控制)
实际开发中推荐使用以下配套工具:
bash复制npm install vue@next vuex@next vue-router@next
npm install element-plus axios --save # UI库和HTTP客户端
2.2 后端技术选型:Spring Boot
Spring Boot 2.7.x + JDK 17的组合提供了:
- 自动配置简化了SSM框架整合
- 内嵌Tomcat服务器便于部署
- 完善的生态支持(Spring Security, MyBatis-Plus等)
典型的基础依赖配置:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
2.3 数据库设计:MySQL 8.0
烟酒销售系统的核心表结构设计要点:
- 商品表设计需包含特殊字段:
sql复制CREATE TABLE `product` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT '商品名称',
`category_id` int NOT NULL COMMENT '分类ID(1-烟草 2-酒类)',
`alcohol_degree` decimal(3,1) DEFAULT NULL COMMENT '酒精度数(%)',
`tar_content` varchar(20) DEFAULT NULL COMMENT '焦油含量(香烟专用)',
`price` decimal(10,2) NOT NULL COMMENT '销售价',
`origin_place` varchar(50) NOT NULL COMMENT '产地',
`restriction_type` tinyint NOT NULL COMMENT '限制类型(1-年龄 2-区域)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- 订单表需要记录特殊的验证信息:
sql复制CREATE TABLE `order` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`idcard_verified` tinyint(1) DEFAULT '0' COMMENT '是否完成实名认证',
`delivery_restriction_passed` tinyint(1) DEFAULT '0' COMMENT '是否通过配送限制检查',
`age_verification_status` tinyint DEFAULT '0' COMMENT '年龄验证状态',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3. 核心功能模块实现
3.1 年龄验证子系统
这是烟酒销售系统特有的核心功能,实现方案包括:
- 前端身份证信息采集组件:
vue复制<template>
<el-upload
action="/api/verify/idcard"
:before-upload="checkIdCard"
:on-success="handleSuccess">
<el-button type="primary">上传身份证照片</el-button>
</el-upload>
</template>
<script>
export default {
methods: {
checkIdCard(file) {
const isJPG = file.type === 'image/jpeg'
const isLt2M = file.size / 1024 / 1024 < 2
if (!isJPG) {
this.$message.error('请上传JPG格式图片')
}
if (!isLt2M) {
this.$message.error('图片大小不能超过2MB')
}
return isJPG && isLt2M
}
}
}
</script>
- 后端验证接口实现:
java复制@RestController
@RequestMapping("/api/verify")
public class VerificationController {
@PostMapping("/idcard")
public Result verifyIdCard(@RequestParam MultipartFile file) {
// 1. 调用OCR服务识别身份证信息
IdCardInfo idCard = ocrService.recognizeIdCard(file);
// 2. 对接公安系统API进行实名核验
boolean valid = policeService.verifyIdCard(
idCard.getName(),
idCard.getIdNumber());
// 3. 计算年龄
int age = calculateAge(idCard.getBirthDate());
// 4. 记录验证结果
verificationService.saveResult(
SecurityUtils.getUserId(),
idCard.getIdNumber(),
age,
valid);
return Result.success(valid);
}
}
3.2 商品展示与搜索
烟酒商品的特殊展示需求:
- 多维度的筛选条件:
vue复制<template>
<div class="filter-container">
<el-select v-model="filters.category" placeholder="商品分类">
<el-option label="白酒" value="1"></el-option>
<el-option label="红酒" value="2"></el-option>
<el-option label="啤酒" value="3"></el-option>
<el-option label="香烟" value="4"></el-option>
</el-select>
<el-select
v-if="filters.category === '4'"
v-model="filters.tarContent"
placeholder="焦油含量">
<el-option label="低焦油(≤8mg)" value="1"></el-option>
<el-option label="中焦油(9-12mg)" value="2"></el-option>
<el-option label="高焦油(≥13mg)" value="3"></el-option>
</el-select>
<el-select
v-if="filters.category !== '4'"
v-model="filters.alcoholDegree"
placeholder="酒精度数">
<el-option label="低度(≤38%)" value="1"></el-option>
<el-option label="中度(39-50%)" value="2"></el-option>
<el-option label="高度(≥51%)" value="3"></el-option>
</el-select>
</div>
</template>
- 后端商品搜索接口:
java复制@GetMapping("/products")
public PageResult<ProductVO> searchProducts(
@RequestParam(required = false) Integer category,
@RequestParam(required = false) String tarContent,
@RequestParam(required = false) String alcoholDegree,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
LambdaQueryWrapper<Product> query = new LambdaQueryWrapper<>();
if (category != null) {
query.eq(Product::getCategoryId, category);
// 香烟类特殊筛选
if (category == 1 && StringUtils.isNotBlank(tarContent)) {
query.apply(getTarContentCondition(tarContent));
}
// 酒类特殊筛选
if (category == 2 && StringUtils.isNotBlank(alcoholDegree)) {
query.apply(getAlcoholDegreeCondition(alcoholDegree));
}
}
Page<Product> page = productService.page(
new Page<>(pageNum, pageSize),
query);
return PageResult.success(page.convert(this::convertToVO));
}
4. 特殊业务逻辑实现
4.1 区域配送限制检查
烟酒商品的配送需要遵守各地法规限制:
- 数据库设计限制区域表:
sql复制CREATE TABLE `restricted_area` (
`id` int NOT NULL AUTO_INCREMENT,
`province` varchar(20) NOT NULL,
`city` varchar(20) DEFAULT NULL,
`district` varchar(20) DEFAULT NULL,
`restriction_type` tinyint NOT NULL COMMENT '1-完全禁止 2-部分限制',
`product_types` varchar(100) DEFAULT NULL COMMENT '受限商品类型',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- 下单前的区域检查逻辑:
java复制public class DeliveryRestrictionChecker {
public CheckResult checkDeliveryRestriction(Long productId, String address) {
// 1. 获取商品类型
Product product = productService.getById(productId);
if (product == null) {
return CheckResult.fail("商品不存在");
}
// 2. 解析收货地址
AddressInfo addressInfo = addressParser.parse(address);
// 3. 查询区域限制规则
List<RestrictedArea> restrictions = restrictedAreaMapper.selectList(
new LambdaQueryWrapper<RestrictedArea>()
.eq(RestrictedArea::getProvince, addressInfo.getProvince())
.and(wrapper -> {
wrapper.isNull(RestrictedArea::getCity)
.or().eq(RestrictedArea::getCity, addressInfo.getCity());
})
.and(wrapper -> {
wrapper.isNull(RestrictedArea::getDistrict)
.or().eq(RestrictedArea::getDistrict, addressInfo.getDistrict());
}));
// 4. 应用限制规则
for (RestrictedArea area : restrictions) {
if (area.getRestrictionType() == 1) {
return CheckResult.fail("该地区完全禁止配送烟酒商品");
}
if (StringUtils.isNotBlank(area.getProductTypes())
&& area.getProductTypes().contains(product.getCategoryId().toString())) {
return CheckResult.fail("该地区限制配送此类商品");
}
}
return CheckResult.success();
}
}
4.2 购物车特殊处理
烟酒商品的购物车需要额外处理:
- 添加商品时的年龄预检查
- 购物车商品的数量限制
- 组合购买限制(如不同酒类不能混购)
实现代码示例:
java复制@PostMapping("/cart/add")
public Result addToCart(@RequestBody AddCartDTO dto) {
// 1. 检查用户年龄状态
User user = userService.getById(SecurityUtils.getUserId());
if (!verificationService.isAdult(user.getId())) {
return Result.fail("未通过年龄验证,无法购买烟酒商品");
}
// 2. 获取商品信息
Product product = productService.getById(dto.getProductId());
if (product == null) {
return Result.fail("商品不存在");
}
// 3. 检查商品购买限制
if (product.getMaxPerUser() > 0) {
long purchased = orderItemMapper.selectCount(
new LambdaQueryWrapper<OrderItem>()
.eq(OrderItem::getUserId, user.getId())
.eq(OrderItem::getProductId, product.getId()));
if (purchased + dto.getQuantity() > product.getMaxPerUser()) {
return Result.fail("超过单人限购数量");
}
}
// 4. 检查购物车中已有商品类型
List<CartItem> existingItems = cartItemMapper.selectList(
new LambdaQueryWrapper<CartItem>()
.eq(CartItem::getUserId, user.getId()));
if (!existingItems.isEmpty()) {
Product existingProduct = productService.getById(
existingItems.get(0).getProductId());
if (!canMixProducts(existingProduct, product)) {
return Result.fail("不同类型烟酒商品不能同时购买");
}
}
// 5. 添加购物车项
CartItem item = new CartItem();
item.setUserId(user.getId());
item.setProductId(product.getId());
item.setQuantity(dto.getQuantity());
cartItemMapper.insert(item);
return Result.success();
}
5. 系统安全与合规设计
5.1 双重年龄验证机制
- 前端实时验证:
vue复制<script>
export default {
methods: {
checkAgeBeforeAddToCart(product) {
if (product.requiresAgeVerification) {
this.$confirm('购买此商品需验证年龄,是否立即验证?', '提示', {
confirmButtonText: '去验证',
cancelButtonText: '取消'
}).then(() => {
this.$router.push('/user/verification')
})
return false
}
return true
}
}
}
</script>
- 后端订单提交时强制验证:
java复制@PostMapping("/order/create")
public Result createOrder(@RequestBody OrderDTO dto) {
// 1. 检查年龄验证状态
VerificationRecord record = verificationService.getLatestRecord(
SecurityUtils.getUserId());
if (record == null || !record.isValid() || !record.isAdult()) {
return Result.fail("请先完成年龄验证");
}
// 2. 检查验证记录时效性(30天有效期)
if (record.getVerifyTime().plusDays(30).isBefore(LocalDateTime.now())) {
return Result.fail("年龄验证已过期,请重新验证");
}
// 3. 继续订单创建流程...
}
5.2 敏感操作日志审计
记录所有关键操作以备监管检查:
java复制@Aspect
@Component
public class AuditLogAspect {
@Autowired
private AuditLogMapper auditLogMapper;
@Pointcut("@annotation(com.example.annotation.AuditLog)")
public void auditPointCut() {}
@AfterReturning(pointcut = "auditPointCut()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
AuditLog auditLog = method.getAnnotation(AuditLog.class);
AuditLogEntity log = new AuditLogEntity();
log.setOperation(auditLog.value());
log.setUserId(SecurityUtils.getUserId());
log.setMethod(method.getName());
log.setParams(JsonUtils.toJson(joinPoint.getArgs()));
log.setResult(JsonUtils.toJson(result));
log.setOperationTime(LocalDateTime.now());
auditLogMapper.insert(log);
}
}
6. 部署与性能优化
6.1 多环境配置
Spring Boot的多环境配置示例:
yaml复制# application-dev.yml
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/tobacco_dev
username: devuser
password: devpass
redis:
host: localhost
port: 6379
# application-prod.yml
server:
port: 80
servlet:
context-path: /tobacco
spring:
datasource:
url: jdbc:mysql://prod-db:3306/tobacco_prod?useSSL=true
username: ${DB_USER}
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 20
redis:
cluster:
nodes: redis-node1:6379,redis-node2:6379,redis-node3:6379
password: ${REDIS_PASSWORD}
6.2 缓存策略设计
烟酒商品的高并发访问优化方案:
- 多级缓存架构:
- 前端本地缓存静态资源
- CDN缓存商品图片
- Redis缓存热点商品数据
- JVM缓存本地配置数据
- Redis缓存示例:
java复制@Service
public class ProductCacheServiceImpl implements ProductService {
@Autowired
private ProductMapper productMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String PRODUCT_CACHE_KEY = "product:";
private static final long CACHE_EXPIRE_HOURS = 24;
@Override
@Cacheable(value = "product", key = "#id")
public Product getById(Long id) {
// 1. 先查Redis
String key = PRODUCT_CACHE_KEY + id;
Product product = (Product) redisTemplate.opsForValue().get(key);
if (product != null) {
return product;
}
// 2. 查数据库
product = productMapper.selectById(id);
if (product != null) {
// 3. 写入Redis
redisTemplate.opsForValue().set(
key,
product,
CACHE_EXPIRE_HOURS,
TimeUnit.HOURS);
}
return product;
}
}
7. 测试与上线准备
7.1 关键测试用例
- 年龄验证测试矩阵:
| 测试场景 | 输入数据 | 预期结果 |
|---|---|---|
| 未成年用户 | 出生日期=当前日期-17年 | 验证失败 |
| 刚成年用户 | 出生日期=当前日期-18年+1天 | 验证成功 |
| 身份证过期 | 过期日期=当前日期-1天 | 验证失败 |
| 身份证信息不匹配 | 姓名与号码不匹配 | 验证失败 |
- 区域限制测试用例:
java复制@Test
public void testDeliveryRestriction() {
// 准备测试数据
RestrictedArea area = new RestrictedArea();
area.setProvince("上海");
area.setRestrictionType(1);
restrictedAreaMapper.insert(area);
Product product = new Product();
product.setCategoryId(1); // 烟草
productMapper.insert(product);
// 执行测试
CheckResult result = checker.checkDeliveryRestriction(
product.getId(),
"上海市浦东新区张江高科技园区");
// 验证结果
assertFalse(result.isAllowed());
assertEquals("该地区完全禁止配送烟酒商品", result.getMessage());
}
7.2 压力测试方案
使用JMeter进行关键接口压测:
- 商品列表接口压测配置:
code复制线程组:500并发用户,持续10分钟
HTTP请求:GET /api/products?pageNum=1&pageSize=10
断言:响应时间<500ms,错误率<0.1%
- 下单接口压测要点:
- 模拟真实用户行为:先登录→加购物车→下单
- 使用CSV数据源提供不同的测试用户
- 监控数据库连接池使用情况
8. 项目总结与扩展方向
在实际开发烟酒销售系统的过程中,有几个关键经验值得分享:
-
合规性设计要前置:在系统设计阶段就要充分考虑各种法律法规限制,避免后期大规模修改。我们最初忽略了某些地区的特殊限制,导致上线前不得不紧急调整配送逻辑。
-
验证流程的平衡:过于严格的验证会导致用户流失,过于宽松则可能违反法律。我们最终采用的方案是:
- 浏览商品:无需验证
- 加入购物车:轻量级提醒
- 下单支付:强制验证
-
性能优化重点:烟酒商品详情页的图片较多,我们通过以下方式优化:
- 使用WebP格式减少图片体积
- 实现懒加载技术
- CDN加速静态资源
未来可能的扩展方向:
- 结合区块链技术实现商品溯源
- 增加AR功能让用户虚拟"试喝"酒类产品
- 对接更多第三方支付渠道
- 开发供应商管理子系统
