今天教大家如何设计一个 电子手机商城 , 基于目前主流的技术:前端vue3,后端springboot。学习完这个项目,你将来找工作开发实际项目都会又很大帮助。文章最后部分还带来的项目的部署教程。
我们出去找工作也可以用这个作为实际项目案例。
系统含有现代化商城的设计思想,有着基于用户的协同过滤推荐算法,保证库存不超卖的机制,还有运费管理, 订单管理,购物车等子模块系统的设计,基本上符合将来工作上的实际项目。
视频演示
电子手机商城
图片演示
系统功能概述
分类管理
首先,系统将 市面上手机 按照品牌分为8个大的类目(当然你也可以在管理后台修改):
小米,苹果,糖果,OPPO,华为,VIVO,魅族,三星
对应着后台就是分类管理, 前端会在头部导航栏的 "全部手机" 板块展示这个分类。
首页管理
首页有轮播图,资讯,人气分类,精品推荐,新品上架几大模块。
轮播图: 在后台的轮播图管理,能够对前端首页的轮播广告进行管理, 支持点击跳转到外部链接。
资讯: 查询最近10条手机新闻资讯。这个可以在后台管理的菜单 “资讯管理” 里面配置手机文章。
人气分类: 这个可以在后台管理的首页管理的人气分类里面配置,目前配置的手机品牌分类有:小米,苹果,糖果,OPPO,华为,VIVO,魅族,三星。
精品推荐: 查询销量的top前8个手机产品。销量会在用户支付接口调用后新增商品销量,并且用户退款不支持减去销量。
新品上架: 查询商品按照创建时间倒叙的top前8个手机产品。
手机商品管理
这个是系统的核心, 商品的很多属性,主要有: 手机的售卖标题,所属的分类,商品详情的动态属性,商品详情,spu等。在后台管理的商品管理里面可以对商品进行上下架,商品新增,修改,切不可删除。因为商品关联了很多表,如果不卖该商品了,只能作下架处理。
SPU库存,价格等管理
spu是商品的最小单元,比如当用户购买小米手机时,有可能是红色,黑色,内存256GB,1TB等等很多系列,这些都是不同的规格, 每个规格有不同的库存和价格。用户购买时,都需要选中到具体的哪个规格才能保证下单。
在后台管理的库存管理,就可以对所有商品的spu规格进行管理,可以对每个spu上传不同的商详图片。可以新增,减少库存。还有详细的库存日志可查。
评价管理
评价是商城不可或缺的一部分,很多用户就是看评价下单的,我们的评价有内容,还支持多张图片,还有评分等级。可以说是一个很丰富的评价体系 。这个评分还参与了用户协同过滤推荐系统。用户只有当订单处与完成状态时,才可以对商品进行评价。
在后台管理的商品管理下面的评价管理可以查看和删除用户的评价。
用户管理
用户有头像,用户名,邮箱,密码,是否禁用,当前收获地址等属性。用户前端注册是通过邮箱验证码来注册,所以系统初始化时,需要在后台管理的字典管理的系统字典里面设置好邮箱服务器,然后才可以进行正确的注册。
用户登录可以通过用户名或者邮箱登录。当后台管理将用户禁用后,登录失效。
前端可以对用户的用户名,头像进行修改, 还可以修改默认的收获地址。
运费管理
运费首先是有一个运费模板,商品上架时选中这个运费模板, 如果模板是包邮的,则前端显示电子手机商品的邮费就是包邮,下单接口也会计算这个邮费。
如果模板里面是不包邮的,那么就需要关联多个运费规则,每个运费规则有省市区 和 运费 两个属性,这样当用户下单的收获地址匹配到了运费规则 的地区,就可以拿到运费进行计算 订单价格。
资讯管理
这个板块是电子手机商城的一个吸引模块, 主要承载一些电子手机的资讯文章,可以在后台进行新增和编辑,还有删除 文章。
购物车+收藏
用户浏览手机商品时, 可以对手机进行收藏,添加购物车处理,收藏后可以在后台管理的用户列表的操作列看到查看收藏。前端用户自己也可以进行收藏取消等。
同一个商品购物车添加有数量有上限。
订单管理
我们将订单的状态设计成以下几种:0-待付款 1-待发货 2-待收货 3-待评价 4-已完成 5-退款中 6-已退款 7-已取消。 订单状态扭转流程:
1. 用户点击购买商品或从购物车点击,则商品进入待付款状态,此时,商品库存被锁,也就是实实在在的扣减了销售库存。
2. 当30分钟超过后,用户未支付上面的待付款订单,则订单状态扭转为已取消,库存回流,此笔订单结束。
3. 当用户30分钟内支付后,订单扭转为代发货。
4. 管理员登录管理后台,将待发货订单进行发货操作后,订单状态变成待收货。
5. 用户可以对待收获订单进行收获和退款操作,如果是退款,则变成退款中,管理员进行退款确认,确认后,订单变成已退款,退款成功后,库存回流。此订单结束。
6. 如果用户确认收获,则订单变成待评价,用户可以进行评价,评价完成后,订单变成已完成,此订单结束。
收货地址管理
每个用户新注册是没有收货地址的,如果此时下单手机商品,也是无法下单。需要在个人中心去新增收获地区,收货地址有:省市区,具体的联系人,手机号,具体地址信息。
用户可以增加很多收货地址, 同时用户可以在修改资料时,切换已经存在的收货地址。
技术栈说明
后端技术栈
java语言采用JDK8 。 数据库有Mysql8 ,Redis 。 项目用到了SpringBoot+Mybatis-plus+SpringMVC 框架。
前端技术栈
Vue 3 + Vue Router 4 + Element Plus
- 采用模块化开发,按功能划分API文件
- 使用ES6+语法和模块系统
- 响应式设计,适配移动端H5
- 统一的错误处理和消息提示
- 支持Token认证的用户系统
核心功能实现
库存系统的设计
库存最大的问题就是超卖,也就是说有多个人同时并发下单,库存需要保持一致性,不会扣减到小于0的情况。普通的设计就是加一个全局锁。每个人下单都需要等待上一个人下单完成。
这样严重影响效率。这里我们库存的设计流程如下:
1. 首先我们将库存分为 数据库库存 和 销售库存。 数据库库存就是存储到数据库的商品库存值,销售库存就是用户下单,页面所在的库存值。
2. 后台管理上架商品时,会设置一个初始库存,我们将初始库存存储到数据库库存 和 销售库存 。
3.当用户下单时,不是直接扣减的数据库库存,而是通过redis的 decrement 方法,对销售库存进行扣减。但是redis的扣减操作这里还不是一个原子性操作,需要先从redis查出库存,然后进行decrment操作。这两步操作我们用reddsion的分布式锁来控制原子性,同时,我们将加锁的维度控制到了商品id。这样大大提高了并发效率。
4. 库存扣减后,我们又通过redis消费队列,实现了对数据库库存的同步。这样保持了redis库存和数据库库存的一致性。
5. 后台我们设计的是对商品只能加加库存,和减少库存的操作,而不是直接修改库存值。如果你直接修改库存值,就有可能会导致库存数据不一致,难以跟踪。
6. 我们还设计了库存的扣减,新增日志,方便对库存进行跟踪管理。
库存扣减的部分代码:
/** * 扣减库存(使用Redisson分布式锁) * @param quantity 扣减数量 * @return true-扣减成功,false-扣减失败(库存不足) */ public boolean deductInventory(Integer spuId, int quantity) { String lockKey = "lock:inventory:" + spuId; String inventoryKey = "inventory:" + spuId; RLock lock = redissonClient.getLock(lockKey); try { // 尝试加锁,最多等待10秒,锁过期时间30秒 boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS); if (locked) { String stock = (String) redisTemplate.opsForValue().get(inventoryKey); if (StringUtils.isEmpty(stock)) { return false; } if (Integer.parseInt(stock) < quantity) { return false; } // 扣减库存 redisTemplate.opsForValue().decrement(inventoryKey, quantity); return true; } return false; } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } finally { // 释放锁 if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }
用户协同过滤算法的设计
协同过滤(Collaborative Filtering, CF)是推荐系统中最经典的算法之一,其核心思想是通过用户的历史行为数据(如评分、点击、购买等)发现用户或物品的相似性,并基于这种相似性进行推荐。协同过滤分为两大类:基于用户的协同过滤和基于物品的协同过滤。
算法的步骤
1. 获取所有用户行为数据,构建用户-物品评分矩阵。
2. 目标用户与其它用户的相似度计算: 将用户对商品的评分视为向量,计算余弦相似度。
3. 选取与目标用户相似度最高的 k 个用户作为邻居 。
4. 通过邻居用户的评分进行加权平均预测(权重为用户相似度)。
5. 将预测评分按降序排序,选择评分最高的N个物品作为推荐结果。
举例说明
用户评分矩阵的构建
需要借助Array2DRowRealMatrix算法工具,Array2DRowRealMatrix
是 Apache Commons Math 库中的一个类,用于表示二维实数矩阵,并提供矩阵运算功能。
maven依赖如下:
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-math3</artifactId> <version>3.6.1</version> </dependency>
构建评分矩阵的代码:
// 获取所有用户的行为数据,用于构建用户-物品评分矩阵 List<UserBehavior> allBehaviors = userBehaviorRepository.selectList(null); if(CollectionUtils.isEmpty(allBehaviors)) { return Collections.emptyList(); } // 构建用户和物品的索引映射,方便后续构建评分矩阵 Map<Long, Integer> userIndex = new HashMap<>(); Map<Long, Integer> itemIndex = new HashMap<>(); // 提取用户id List<Long> users = allBehaviors.stream().map(UserBehavior::getUserId).distinct().collect(Collectors.toList()); // 提取物品id List<Long> items = allBehaviors.stream().map(UserBehavior::getItemId).distinct().collect(Collectors.toList()); for (int i = 0; i < users.size(); i++) { userIndex.put(users.get(i), i); } for (int i = 0; i < items.size(); i++) { itemIndex.put(items.get(i), i); } // 初始化评分矩阵,行表示用户,列表示物品 一个 users.size() x items.size() 大小的矩阵 RealMatrix ratingMatrix = new Array2DRowRealMatrix(users.size(), items.size()); // 根据用户行为数据填充评分矩阵 for (UserBehavior behavior : allBehaviors) { if (behavior.getRating() != null) { int uIndex = userIndex.get(behavior.getUserId()); int iIndex = itemIndex.get(behavior.getItemId()); // 设置 矩阵的 行,列 值 为 评分 ratingMatrix.setEntry(uIndex, iIndex, behavior.getRating()); } }
余弦相似度计算
/** * 计算两个向量的余弦相似度 * 余弦相似度用于衡量两个用户的评分模式的相似程度 * @param vector1 第一个用户的评分向量 * @param vector2 第二个用户的评分向量 * @return 相似度值,范围[-1,1],值越大表示越相似 */ private double calculateCosineSimilarity(double[] vector1, double[] vector2) { double dotProduct = 0.0; double norm1 = 0.0; double norm2 = 0.0; for (int i = 0; i < vector1.length; i++) { dotProduct += vector1[i] * vector2[i]; norm1 += vector1[i] * vector1[i]; norm2 += vector2[i] * vector2[i]; } if (norm1 == 0 || norm2 == 0) return 0; return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2)); }
根据余弦相似度计算取5个相似的用户作为邻居
// 计算目标用户与其他用户的相似度 int userIdx = userIndex.get(user.getId()); Map<Integer, Double> userSimilarities = new HashMap<>(); for (int i = 0; i < users.size(); i++) { if (i != userIdx) { // 计算 当前用户 与其他的每一个用户的评分向量的 余弦相似度 double similarity = calculateCosineSimilarity(ratingMatrix.getRow(userIdx), ratingMatrix.getRow(i)); userSimilarities.put(i, similarity); } } // 选择最相似的5个用户作为邻居用户 List<Integer> similarUsers = userSimilarities.entrySet().stream() // 按相似度值降序排序 .sorted(Map.Entry.<Integer, Double>comparingByValue().reversed()) // 取前5个最相似用户 .limit(5) // 提取用户索引 .map(Map.Entry::getKey) .collect(Collectors.toList());
最后是计算加权平均,当中还需要进行 归一化处理, 来避免了因用户群体整体相似度偏高/偏低导致的预测偏差,使得推荐结果更贴近用户的真实偏好。 整体代码较长,我就不贴了。
商品的设计
手机商品的相关表结构如下:
CREATE TABLE `product` ( `product_id` int NOT NULL AUTO_INCREMENT COMMENT '商品id', `product_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '商品名称', `category_id` int NOT NULL COMMENT '类目id', `product_title` varchar(300) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '商品标题', `product_intro` text CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '商品详情', `product_picture` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '商品封面图', `spu0_price` double NOT NULL COMMENT '参考价,商品第一个spu的价格', `product_sales` int NOT NULL COMMENT '销量', `state` tinyint DEFAULT '0' COMMENT '0-上架 1- 下架', PRIMARY KEY (`product_id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=67 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC COMMENT='商品'; CREATE TABLE `product_spu` ( `id` int NOT NULL AUTO_INCREMENT COMMENT 'id', `product_id` int NOT NULL COMMENT '商品id', `spu_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '规格的key名称,比如尺码', `spu_value` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '规格的key的值,比如尺码的大小是S', `spu_price` double NOT NULL COMMENT '商品售卖价', `spu_stock` int NOT NULL COMMENT 'spu库存', `state` tinyint DEFAULT '0' COMMENT '0-上架 1- 下架', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=101 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC COMMENT='商品最小单元'; CREATE TABLE `spu_picture` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '主键id', `spu_id` int NOT NULL COMMENT '商品id', `product_picture` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '商品图片', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=136 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='商品图片';
订单表设计如下:
CREATE TABLE `orders` ( `order_id` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, `user_id` int NOT NULL COMMENT '用户id', `spu_id` int NOT NULL COMMENT '商品id', `spu_name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'spu的名称', `spu_value` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'spu的值', `product_num` int NOT NULL COMMENT '商品数量', `order_state` int NOT NULL COMMENT '订单状态 0-待付款 1-待发货 2-待收货 3-待评价 4-已完成 5-退款中 6-已退款 7-已取消', `product_price` double NOT NULL COMMENT '下单商品价格', `shipping_price` double NOT NULL COMMENT '下单运费价格', `refund_cause` varchar(2255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '退款原因', `order_remark` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '订单备注', `pay_type` int DEFAULT NULL COMMENT '支付方式:0-支付宝 1-微信', `address` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '收获地址', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`order_id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC COMMENT='订单';
运费的设计
首先需要有一个模板表 , 上架商品时, 直接选中到这个运费模板
CREATE TABLE `shipping_template` ( `template_id` int NOT NULL AUTO_INCREMENT, `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '模板名称', `is_free_shipping` tinyint(1) DEFAULT '0' COMMENT '是否包邮(0否1是)', PRIMARY KEY (`template_id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC COMMENT='运费模板表';
每个模板有着自己的运费规则, 规则里面重要的就是城市信息。每个城市的运费都不一样。
CREATE TABLE `shipping_rule` ( `id` int NOT NULL AUTO_INCREMENT, `template_id` int NOT NULL COMMENT '运费模板ID', `city_id` int NOT NULL COMMENT '地区编码(可多级)', `first_fee` double NOT NULL COMMENT '该地域的运费', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC COMMENT='运费规则表';
项目部署安装
执行sql
自己安装好数据库,注意,必须是mysql8 ,否则代码运行会出错。新建一个 wxhadluo-phone 数据库, 然后执行 “wxhadluo-phone.sql”
Redis安装
项目需要安装redis,直接下载一个windows版本的redis即可,没有的联系我。
前端部署
管理后端
安装node , 版本:v22.15.0 , 安装完成后。 进入到项目 hadluo-shop-webadmin 目录下,这个项目是vue的管理后台, 右键,运行cmd,运行下面命令:
npm run dev
由于我已经跟你npm install好了,所以你无需执行,直接run就可以了!!
用户前端
进入到项目 hadluo-shop-h5 目录下,这个项目是vue的前端, 右键,运行cmd,运行下面命令:
npm run dev
由于我已经跟你npm install好了,所以你无需执行,直接run就可以了!! 运行项目
到此前端项目部署完成。
启动后端项目
然后部署后端 , 打开idea, 导入maven工程 hadluo-bookshop。
打开resources目录, 修改 application.yml 配置文件,主要修改下面几个信息:
数据库信息:
spring: datasource: url: jdbc:mysql://192.168.200.131:3306/wxhadluo-phone?characterEncoding=UTF8&serverTimezone=Asia/Shanghai username: root password: qq123456
redis信息
redis: host: 192.168.200.131 port: 6379 database: 1
图片存储磁盘路径
# 文件上传路径配置 file: upload: path: D:\ftp\wxhadluo-phone #文件上传的路径 url-prefix: http://localhost:${server.port}/file/get # 文件下载接口的地址
然后启动 main 启动类 : Application.class
浏览器访问
管理后端:
http://localhost:3001/
账号:wx-hadluo,
密码: 123456
前端:
http://localhost:3000/
用户: wxhadluo / 123456
你也可以自己注册。