曲师商城
曲师商城
1、项目需求分析
用户需求分析
- 注册和登录功能:
- 用户可以通过填写注册表单完成账号注册。
- 用户可以使用注册的账号和密码进行登录。
- 提供忘记密码功能,用户可以通过邮箱或手机验证码重置密码。
- 商品浏览和搜索功能:
- 用户可以浏览商品列表,查看商品的基本信息、价格、图片等。
- 用户可以通过关键词搜索商品,系统返回相关商品的结果。
- 购物车管理功能:
- 用户可以将感兴趣的商品添加到购物车。
- 用户可以修改购物车中商品的数量。
- 用户可以删除购物车中的商品。
- 用户可以清空购物车。
- 下单和支付功能:
- 用户可以选择购物车中的商品完成下单。
- 用户需要填写配送地址和选择支付方式。
- 系统生成订单并显示订单详情。
- 用户进行支付操作,可以选择在线支付或货到付款。
- 查看订单状态功能:
- 用户可以查看订单的物流信息和状态,了解订单的进展情况。
- 用户可以查看订单的详细信息,如商品清单、收货地址等。
- 取消订单功能:
- 用户可以在一定条件下取消未发货的订单,如退货期未过等。
- 评价和回复功能:
- 用户可以对购买的商品进行评价,并选择评分和文字评论。
- 商家可以回复用户的评价。
- 优惠券和积分功能:
- 用户可以领取优惠券和积分,用于抵扣购物金额或兑换商品。
- 促销活动功能:
- 商家可以发布促销活动,如限时折扣、满减等。
- 用户可以参与促销活动,享受相应的优惠。
- 客服支持功能:
- 提供在线客服功能,解答用户的问题和处理售后问题。
- 搜索和推荐功能:
- 用户可以通过关键词搜索商品,系统根据用户的浏览和购买记录推荐相关商品。
- 移动端适配功能:
- 商城需要适配不同的移动设备,提供良好的移动端用户体验。
商家入驻代办
商家需求分析
- 商户入驻功能:
- 商家可以申请入驻商城,并在平台上销售自己的商品。
- 商家需要提供相关的营业执照、品牌授权等信息进行认证。
- 商品管理功能:
- 商家可以发布、修改、删除自己的商品。
- 商家可以设置商品的基本信息、价格、库存等。
- 商家可以管理商品分类和品牌信息。
- 订单管理功能:
- 商家可以查看自己店铺的订单列表,包括待处理订单和已完成订单。
- 商家可以接单、发货、退款等操作。
- 物流管理功能:
- 商家可以更新自己店铺的物流信息,如发货状态、快递单号等。
2、技术选型
针对曲师商城项目,考虑以下技术选型:
- 前端开发框架:Vue主流的前端框架。
- 后端开发语言:Java常用的后端开发语言。
- 数据库:MySQL常见的关系型型数据库。
- 服务器端框架:Spring Boot、SpringCloud常用的后端框架,简化开发流程和提高开发效率。
- 缓存技术:Redis常用的缓存技术,用于提高系统性能和减轻数据库压力。
- 消息队列:RabbitMQ常用的消息队列技术,用于处理异步任务和解耦系统模块之间的通信。
- 搜索引擎:Elasticsearch用于全文搜索和数据分析的搜索引擎技术。
- 版本控制工具:Git作为代码版本控制工具,用于团管理代码的版本。
- 容器化技术:Docker用于将应用打包成容器,实现快速部署和可移植性。
- 云服务:根据实际需求选择合适的云服务提供商(如AWS、阿里云、腾讯云等),用于部署和托管商城系统。
3、项目设计
用户模块
系统架构设计:
- 前端采用React框架,使用Redux进行状态管理。
- 后端采用Node.js和Express框架,使用MongoDB作为数据库。
- 使用Nginx作为反向代理服务器,负载均衡和静态资源处理。
- 使用Redis作为缓存技术,提高系统性能。
- 使用RabbitMQ作为消息队列,处理异步任务和解耦系统模块之间的通信。
(1)功能模块设计:
- 用户模块:包括用户注册、登录、个人信息管理等功能。
- 商品模块:包括商品的添加、修改、删除、查询等功能。
- 订单模块:包括下单、支付、查看订单状态等功能。
- 物流模块:用于商家更新订单的物流信息。
- 评价模块:用户可以对购买的商品进行评价,商家可以回复评价。
- 优惠券和积分模块:用户可以领取优惠券和积分,用于抵扣购物金额或兑换商品。
- 促销活动模块:商家可以发布促销活动,如限时折扣、满减等。
- 客服支持模块:提供在线客服功能,解答用户的问题和处理售后问题。
- 搜索和推荐模块:用户可以通过关键词搜索商品,系统会根据用户的浏览和购买记录推荐相关商品。
- 数据统计和分析模块:统计商城的访问量、销售额等数据,为商家提供数据分析和决策支持。
(2)数据库设计:
- 用户表:存储用户的基本信息,如用户名、密码、邮箱等。
- 商品表:存储商品的基本信息,如商品名称、价格、库存等。
- 分类表:存储商品的分类信息,如服装、电子产品等。
- 品牌表:存储商品的品牌信息,如Nike、Apple等。
- 订单表:存储订单的基本信息,如订单号、下单时间、支付状态等。
- 物流表:存储订单的物流信息,如物流公司、运单号等。
- 评价表:存储用户对商品的评价信息,如评分、评论内容等。
- 优惠券表:存储优惠券的基本信息,如优惠码、面额等。
- 积分表:存储用户的积分信息,如积分余额、获取途径等。
(3)接口设计:
- 用户接口:包括注册接口、登录接口、个人信息接口等。
- 商品接口:包括添加商品接口、修改商品接口、删除商品接口、查询商品接口等。
- 订单接口:包括下单接口、支付接口、查看订单接口等。
- 物流接口:用于商家更新订单的物流信息接口。
- 评价接口:包括添加评价接口、回复评价接口等。
- 优惠券和积分接口:包括领取优惠券接口、使用优惠券接口、积分获取接口等。
- 促销活动接口:包括发布促销活动接口、参与促销活动接口等。
- 客服支持接口:提供在线客服功能的相关接口。
- 搜索和推荐接口:包括商品搜索接口、推荐商品接口等。
商家模块
- 商家注册与认证流程:明确商家注册的步骤和要求,如填写公司信息、上传证件等。同时,设计一个认证流程,确保商家的身份和资质真实有效。
- 商品管理功能:细化商品管理的界面和操作流程,包括添加商品、编辑商品信息、删除商品等功能。可以考虑提供批量操作和导入导出功能,方便商家进行大量商品的管理。
- 订单管理功能:细化订单管理的界面和操作流程,包括查看订单详情、接单、发货、退款等功能。可以考虑提供订单筛选和排序功能,方便商家快速找到需要处理的订单。
- 物流管理功能:细化物流管理的界面和操作流程,包括查询物流状态、更新物流信息等功能。可以考虑提供物流公司选择和运单号生成功能,方便商家与物流公司进行交互。
- 评价管理功能:细化评价管理的界面和操作流程,包括查看买家的评价、回复评价等功能。可以考虑提供评价分类和搜索功能,方便商家查找和管理评价信息。
- 数据统计与分析功能:细化数据统计与分析的界面和报表展示,包括销售额统计、订单量统计、用户评价统计等功能。可以考虑提供图表展示和数据导出功能,方便商家进行数据分析和决策。
- 营销活动支持功能:细化营销活动的设置和管理流程,包括优惠券、满减活动等。可以考虑提供活动规则设置、活动发布和推广等功能,方便商家进行营销活动的管理和执行。
- 客服支持功能:细化客服支持的界面和沟通方式,包括在线聊天工具、电话支持等。可以考虑提供智能客服机器人和问题解答库,提高客服效率和质量。
技术积累
后端
一、gateway路由路径匹配
匹配多个路径
routes:
- id: service-user
uri: lb://service-user
predicates:
- Path= /common/**,/admin/** #匹配多个路径
二、redis缓存
1、缓存更新策略
内存淘汰 | 超时剔除 | 主动更新 | |
---|---|---|---|
说明 | 不用自己维护,利用Redis的内存淘汰机制,当内存不足时自动淘汰部分数据。下次查询时更新缓存。 | 给缓存数据添加TTL时间,到期后自动删除缓存。下次查询时更新缓存。 | 编写业务逻辑,在修改数据库的同时,更新缓存。 |
一致性 | 差 | 一般 | 好 |
维护成本 | 无 | 低 | 高 |
业务场景
- 低一致性需求:使用内存淘汰机制。
- 高一致性需求:主动更新,并以超时剔除作为兜底方案。
三个问题需要考虑:
(1)删除缓存还是更新缓存?
- 更新缓存:每次更新数据库都更新缓存,无效写操作较多
- 删除缓存:更新数据库时让缓存失效,查询时再更新缓存(建议)
(2)如何保证缓存与数据库的操作的同时成功或失败?
- 单体系统:将缓存与数据库操作放在一个事务
- 分布式系统:利用TCC等分布式事务方案
(3)先操作缓存还是先操作数据库?
- 先删除缓存,再操作数据库
- 先操作数据库,再删除缓存(建议)
本项目使用主动更新 + 删除缓存 + 先操作数据库,再删除缓存
2、缓存三大问题解决方案
(1)、缓存穿透
(2)、缓存击穿
(3)、缓存雪崩
缓存穿透
简单理解:缓存穿透是指客户端请求的数据在缓存中和数据库都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

解决方案
(1)缓存空对象
- 优点:实现简单,维护方便
- 缺点:
- 额外的内存消耗
- 可能造成短期的数据不一致

(2)布隆过滤器
- 优点:内存占用较少,没有多余key
- 缺点:
- 实现复杂
- 存在误判可能

本项目采用缓存空对象解决
代码片段
//如果数据中不存在则缓存空对象返回,两分钟后过期
redisTemplate.opsForValue().set(GoodsConstant.GOODS + userId,"",RedisConstant.CACHE_NULL_EXPIRE, TimeUnit.MINUTES);
其他解决方案:
- 增强id的复杂度,避免被猜测id规律
- 做好数据的基础格式校验
- 加强用户权限校验
- 做好热点参数的限流
缓存雪崩
**简单理解:缓存雪崩**是指同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案
- 给不同的key的TTL添加随机值
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
本项目使用给不同的key的TTL添加随机值
缓存击穿
简单理解:缓存击穿问题也叫热点key问题,即使也给被高并发访问并且缓存重建页较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

解决方案
- 互斥锁
- 逻辑过期
解决方案 | 优点 | 缺点 |
---|---|---|
互斥锁 | (1)没有额外的内存消耗。(2)保证一致性。(3)实现简单 | (1)线程需要等待,性能受影响。(2)可能有死锁风险 |
逻辑过期 | 线程不需要等待,性能较好。 | (1)有额外的内存消耗。(2)不保证一致性。(3)实现复杂 |
互斥锁

一下是一个简单的使用互斥锁解决的流程

获取互斥锁方法
/**
* 获取锁方法
* @param key
*/
private boolean tryLock(String key,Integer userId){
//获取锁,要对锁设置一个过期时间,防止某些异常导致死锁
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, userId, RedisConstant.CACHE_LOCK_EXPIRE, TimeUnit.SECONDS);
//如果为true则说明获取到了锁,若返回false则说明前面已经有线程获取到了锁
return BooleanUtil.isTrue(flag);
}
释放互斥锁方法
/**
* 释放锁
* @param key
*/
private void unLock(String key){
//直接删除锁的键即可
redisTemplate.delete(key);
}
核心代码
@Override
public List<GoodsDto> getList(Integer userId) {
//先去redis中查询是否有缓存的数据
List<GoodsDto> GoodsDtoList = (List<GoodsDto>) redisTemplate.opsForValue().get(GoodsConstant.GOODS + userId);
if(!CollectionUtils.isEmpty(GoodsDtoList)){
log.info("命中了redis中的商品缓存数据,成功进入缓存---------------");
return GoodsDtoList;
}else{
//未命中redis中的缓存数据
String lockKey = RedisConstant.GOODS_LOCK + userId;
//尝试获取锁
try {
boolean isLock = this.tryLock(lockKey, userId);
if(!isLock){
//未获取到锁,休眠一段时间等待前一个线程重建缓存
Thread.sleep(RedisConstant.THREAD_SLEEP);
//重新获取,递归
return getList(userId);
}
//获取到了锁
//再去redis中查询是否有缓存的数据
List<GoodsDto> GoodsList = (List<GoodsDto>) redisTemplate.opsForValue().get(GoodsConstant.GOODS + userId);
if(!CollectionUtils.isEmpty(GoodsList)){
log.info("命中了双重检测redis中的商品缓存数据,成功进入缓存---------------");
return GoodsList;
}
//根据商户id获取商户所属的店铺
QueryWrapper<Store> wrapper = new QueryWrapper<>();
wrapper.eq("user_id",userId);
List<Store> stores = storeMapper.selectList(wrapper);
if(CollectionUtils.isEmpty(stores)){
//如果数据中不存在则缓存空对象返回来解决redis缓存穿透,两分钟后过期
List<UserDto> dtoList = new ArrayList<>();
dtoList.add(new UserDto());
redisTemplate.opsForValue().set(GoodsConstant.GOODS + userId,dtoList,RedisConstant.CACHE_NULL_EXPIRE, TimeUnit.MINUTES);
}else{
//进行缓存重建
List<GoodsDto> goodsDtoList = this.getGoodsDtoList(stores,userId);
return goodsDtoList;
}
}catch (Exception e){
throw new ServiceException(ResponseEnum.ERROR);
}finally {
//释放锁
this.unLock(lockKey);
}
}
return null;
}
逻辑过期

三、防止重复提交
使用redis + lua脚本保证原子性操作
lua脚本
if(redis.call('get', KEYS[1]) == ARGV[1]) then
return redis.call('del', KEYS[1])
else return 0 end
核心代码
private static final DefaultRedisScript<Boolean> REDIS_SCRIPT;
/**
* 静态代码块中初始化lua脚本
*/
static {
REDIS_SCRIPT = new DefaultRedisScript<Boolean>();
//读取项目中的lua文件
REDIS_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
REDIS_SCRIPT.setResultType(Boolean.class);
}
@Override
public void saveOrderInfo(OrderDto orderDto,String orderId) {
//TODO: 需要做防止重复提交和锁定库存操作
if(StringUtils.isEmpty(orderId)){
throw new ServiceException(ResponseEnum.ORDER_ID_EXCEPTION);
}
//lua脚本保证原子性操作
//拿着orderId到redis中查询
//如果redis中有相同orderId,则说明是正常提交订单,把redis中的orderId删除,
// 这个过程要保证原子性操作,由lua脚本保证。
Boolean flag = (Boolean) redisTemplate.execute(REDIS_SCRIPT, Arrays.asList(RedisConstant.ORDER_ID + orderId), orderId);
//如果redis中没有相同orderId,则说明是重复提交订单,不再往下进行
if(!flag){
//返回false则说明是重复提交
throw new ServiceException(ResponseEnum.DO_NOT_REPLACE_SUBMIT);
}
}
}
(四)、分布式锁实现库存锁定,解决商品超卖问题
使用redisson实现分布式锁解决
maven依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.11.2</version>
</dependency>
redisson配置类
@Data
@Configuration
@ConfigurationProperties("spring.redis")
public class RedissonConfig {
private String host;
private String addresses;
private String password;
private String port;
private int timeout = 3000;
private int connectionPoolSize = 64;
private int connectionMinimumIdleSize=10;
private int pingConnectionInterval = 60000;
private static String ADDRESS_PREFIX = "redis://";
/**
* 自动装配
*
*/
@Bean
RedissonClient redissonSingle() {
Config config = new Config();
// 判断redis 的host是否为空
if(StringUtils.isEmpty(host)){
throw new RuntimeException("host is empty");
}
// 配置host,port等参数
SingleServerConfig serverConfig = config.useSingleServer()
//redis://127.0.0.1:7181
.setAddress(ADDRESS_PREFIX + this.host + ":" + port)
.setTimeout(this.timeout)
.setPingConnectionInterval(pingConnectionInterval)
.setConnectionPoolSize(this.connectionPoolSize)
.setConnectionMinimumIdleSize(this.connectionMinimumIdleSize);
// 判断进入redis 是否密码
if(!StringUtils.isEmpty(this.password)) {
serverConfig.setPassword(this.password);
}
// RedissonClient redisson = Redisson.create(config);
return Redisson.create(config);
}
}
核心代码
前端
一、后台:由ant design vue开发
(一)、页面过渡效果
在路由文件index.js中引入具体的动画效果,推荐Animate.css | A cross-browser library of CSS animations. Animate.css动画
安装Animate.css
npm install animate.css --save
引入到路由的index.js文件中
const router = createRouter({
history,
routes: [
{
//登录页面
path: "/login",
name: "Login",
component: () => import("@/views/login/Login.vue"),
meta: {
title: "登录页",
//这里的这个动画直接去Animate.css中复制即可
transition: "animate__bounceInRight",
},
},
{
//后台
path: "/",
name: "Layout",
component: () => import("@/components/layout/Home.vue"),
redirect: "/index",
children: [
{
//首页
path: "/index",
name: "Index",
component: () => import("@/views/index/Index.vue"),
meta: {
title: "首页",
//这里的这个动画直接去Animate.css中复制即可
transition: "animate__fadeInLeftBig",
},
},
{
//首页
path: "/user",
name: "User",
component: () => import("@/views/user/User.vue"),
meta: {
title: "用户",
//这里的这个动画直接去Animate.css中复制即可
transition: "animate__backInDown",
},
},
],
},
{
path: "/:pathMatch(.*)*",
name: "NotFound",
component: () => import("@/views/404/NotFound.vue"),
},
],
});
在App.vue中修改
<template>
<router-view #default="{route,Component}">
<---->一定要加这个前缀animate__animated</---->
<transition name="fade" :enter-active-class="`animate__animated ${route.meta.transition}`">
<component :is="Component" />
</transition>
</router-view>
</template>
<script setup>
//要引入Animate.css的样式
import 'animate.css'
</script>
(二)、ant desgin vue表格渲染时报错
当 data
判断为空,且 horizonScroll
判断为 true
时,ResizeObserver
包裹下的 children
的 width
可能会发生变化,这时候就有可能会触发 ResizeObserver loop completed with undelivered notifications
报错。
所以,如果我们传入非空的值的话,是不是就消除这个报错了呢?我们来实践 一下。
在 antd
版本大于等于 4.0 的时候
- 当初始化
table
,dataSource
赋值空数组时:
react
复制代码<Table columns={[]} dataSource={[]} scroll={{ x: 100 }} />
控制台就会报错:ResizeObserver loop limit exceeded
。
- 当初始化
table
,dataSource
赋值[{}]
时:
react
复制代码<Table columns={[]} dataSource={[{}]} scroll={{ x: 100 }} />
控制台的报错就没了(如果想要更清楚地观察结果,可以在右侧的界面上刷新一下)。
那当 antd
版本小于 4.0 的时候,dataSource
的不同赋值会不会触发这个报错呢?
答案是:不会。
因为 antd 3.x 或以下的版本,没用上 ResizeObserver
这个功能。
二、前台:有Ant design vue开发
(一)、关于如何展示金额小数点问题解决
1、写一个函数,然后使用插值语法调用函数
//展示价格的小数方法
function towNumber(val) {
return val.toFixed(2);
}
2、比如这个位置展示金额
<span style="font-size: 18px; color: red; font-weight: bold">{{towNumber(item.price)}}</span>
(二)、关于vue3.0 引入 ant-design-vue 报Uncaught TypeError: Cannot read properties of undefined (reading 'value') 的解决办法
其实很简单 就是因为版本的问题
根目录执行下面这个命令就行了
cnpm i --save ant-design-vue@next -S
cnpm 和 npm 都行
(三)、验证手机号正则表达式
//数据校验
var reg_tel =
/^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/;
function doPhoneLogin() {
PhoneFormRef.value
.validate()
.then(() => {
//手机号验证
if (!reg_tel.test(user.value.phone)) {
message.error("请输入正确的手机号");
} else {
message.success("表单验证成功");
console.log(user.value);
}
})
.catch((error) => {
console.log("error", error);
});
}
(四)、验证码倒计时设计
<a-form-item name="code">
<a-input-search
v-model:value="certification.code"
placeholder="请输入验证码"
@search="onSearch"
style="width: 100%"
maxlength="6"
minlength="6"
size="large"
addon-before="验证码"
>
<template #enterButton>
<a-button
@click="getCode"
style="color: #f56c6c"
width="20px"
:disabled="disabled"
>
{{ content }}
</a-button>
</template>
</a-input-search>
</a-form-item>
//获取验证码前确认是否是真实手机号
const disabled = ref(false);
const content = ref('获取验证码');
function getCode() {
Modal.confirm({
title: "确认此手机号有效?",
icon: createVNode(ExclamationCircleOutlined),
content: "确保您所输入的手机号真实有效,否则影响后续操作!",
okText: "确认",
cancelText: "取消",
onOk() {
var time = 120;
//禁用获取验证码按钮
disabled.value = true;
// 开启定时器
var timer = setInterval(function () {
// 判断剩余秒数
if (time == 0) {
// 清除定时器和复原按钮
clearInterval(timer);
disabled.value = false;
content.value = "获取验证码";
} else {
content.value = `${time}秒后重新获取`;
time--;
}
}, 1000);
},
onCancel() {
message.success("验证码发送失败");
},
});
}
(五)、悬浮阴影
<div class="address_style"></div>
.address_style:hover {
box-shadow: 10px 10px 5px #a0cfff, 10px 10px 5px #fab6b6;
}
.address_style {
height: 120px;
background-color: #fff;
width: 230px;
opacity: 0.8;
transition: all 0.5s;
}
(六)、三种路由传参方式
(1)、传字面量
function toDetail(id) {
router.push({ name: "Detail", query: { id: id } });
}
接收
import { useRoute } from "vue-router";
const route = useRoute();
let id = null;
id = route.query.id;
(2)、传数组
const cartIds = ref([]);
function toSubmitOrder() {
router.push({ path: "submitorder", query: { id: cartIds.value } });
}
接收
import { useRoute } from "vue-router";
const route = useRoute();
const cartIds = ref([]);
cartIds.value = route.query.id;
(3)、传对象
saveOrder(orderDto.value).then((res) => {
if (res.code === 200) {
orderInfoVo.value = res.data;
message.success("提交订单成功");
router.push({
name: "PayFor",
query: { item: JSON.stringify(orderInfoVo.value) },
});
}
});
接收
import { useRoute } from "vue-router";
const route = useRoute();
const orderInfoVo = JSON.parse(route.query.item);
三、小程序端:由uniapp + uview-plus开发
(一)使用uview-plus模板开发
1、引入uview-plus
在uniapp插件市场搜索uview plus,选择插件后点击右边的“下载插件并导入HBuilderX”
选择创建的项目进行导入即可

2、在项目根目录中的main.js
中,引入并使用uview-plus
// main.js
import uviewPlus from '@/uni_modules/uview-plus'
// #ifdef VUE3
import { createSSRApp } from 'vue'
export function createApp() {
const app = createSSRApp(App)
app.use(uviewPlus)
return {
app
}
}
// #endif

3、在项目根目录的uni.scss
中引入uview-plus的全局SCSS主题文件
/* uni.scss */
@import '@/uni_modules/uview-plus/theme.scss';

4、在App.vue的style中引入uview-plus基础样式
<style lang="scss">
/* 需要给style标签加入lang="scss"属性 */
@import "@/uni_modules/uview-plus/index.scss";
</style>

5、在使用的页面中使用uview-plus的组件,效果展示
<!-- index.vue -->
<template>
<view>
<u-button type="primary" :plain="true" :hairline="true" text="细边"></u-button>
</view>
</template>
效果展示
