菜狗商城
一 介绍
菜狗商城 一款Springboot+Vue前后端分离架构的网络电商平台购物系统,包括用户登录,商品推荐,商品搜索,用户评价,购物车,添加订单,收货地址及微信支付等功能。
涉及技术:Springboot+Vue+Mysql+Axios+Bootstrap+qrcode+Websockst+Quartz+Swagger+Jwt
Gitee地址 https://gitee.com/xujiulong/caigoumall
API地址 https://www.apifox.cn/apidoc/shared-52a9790d-b49c-41ce-ab7f-f1a560e46276/api-13546585
二 技术分析
? 整个“菜狗商城”系统采用前后端分离开发Springboot+Vue的架构,数据库表结构采用PDMan依据三范式设计。开发工具的话后端使用IntelliJ IDEA,前端使用VS Code。数据库使用Navicat对Mysql数据库进行操作。开发完后通过FinalShell将项目挂载到云服务器Linux 上运行。 ? 前端采用Vue完成数据渲染,通过Axios实现异步访问,搭配Bootstrap和Element UI来完成页面布局的开发,通过qrcode生成支付二维码。后端使用Springboot这个轻量级的、一站式IOC框架,持久层采用MyBatisPlus完成对数据库的增删改查,一对多及多对一的复杂操作,通过Websockst建立长链接完成消息推送,采用的Quartz任务轮询框架来完成对超时未支付的订单的查询操作。API接口采用Restful风格搭配Swagger进行接口测试和文档开发编写。安全方面采用Jwt以加密加盐的方式生成Token完成用权限户验证。为了保证数据的一致性采用事物管理来实现,考虑到线程并发问题使用同步锁。 ? 整个项目设计众多功能,共计13张数据库表106个不重复表字段,表包括:商品分类、地区字典、轮播图、订单项/快照、订单、商品、商品评价、商品图片、参数、商品规格、购物车、用户地址、用户。
三 功能分析
? 菜狗商城”是一款B2C模式的网络电商平台购物系统,包括用户登录,商品推荐,商品搜索,用户评价,购物车,添加订单,收货地址及微信支付等功能。 ? 游客用户可以通过在首页的商品推荐轮播图、最新商品、分类推荐等对喜欢或着比较有意思的商品来进行概查询,或者在首页商品分类栏中进行更细致的选择,也能够使用搜索栏输入的关键字来对相干的商品检索。登录过的用户可以将喜欢的商品规格如各种包装、口味的商品加入购物车中,当然也可以查看用户的有关评价信息关于当前商品,例如:商品的规格、生产日期、详细归属地、质检等信息。用户对已经收藏的商品在自己的购物车中的,对数量或规格的敲定后点击下单支付,在选择好支付方式及收货信息后进行订单的支付。同时也可以随时在个人中心中对收货地址信息进行查看、修改或者删除,以及对种种状态下订单进行更细致的信息概览,例如:未支付订单,超时未支付订单,已完成订单等。
四 运行环境和开发工具
- 操作系统:Window10,Linux CentOS 7
- Java 语言的软件开发工具包:JDK1.8
- 服务器:apache-tomcat-8.5.57,Nginx-1.16.1
- 项目管理工具:apache-maven-3.6.3
- 数据库:Mysql8.0
- 浏览器:Google Chrome
- 代码编译工具:IntelliJ IDEA 2021.2.1,Visual Studio Code
- 数据库操作工具:Navicat
- 版本控制工具:Git
- 项目管理工具:Maven
- 远程工具:FinalShell
五 安装运行
-
后端部署springboot 数据库导入sql文件 修改.xml文件中数据库连接配置 -
前端 可以使用Vscode部署 访问 本地静态页面地址+caigoumall/caigoumall-vue/index.html -
如要使用微信支付功能 需开通内网穿透 在OrdersServiceImpl文件中修改对应 微信回调地址 将项目部署在云端则修改为对应公网地址即可 -
若要实现支付完成后页面自动跳转显示“支付完成”,则要通过“内网穿透”搭配Websockst实现 内网穿透工具-NATAPP NATAPP客户端命令 natapp -authtoken=a611ed0fefb0ae45 -
首页访问地址:本地静态页面地址+caigoumall/caigoumall-vue/index.html 例如我:file:///D:/Code/caigoumall/caigoumall-vue/index.html (当然也可以云端服务器部署) -
Swagger 文档访问地址:http://localhost:8084/doc.html
六 云端运行
- 将前后端项目中默认访问地址修改为对应云服务器公网IP地址
(如果数据库需要云端部署同理) - 将前端静态项目包放置在nginx目录下
- 后端项目打包成.jar后放置在/usr/local/目录下
- Linux命令
//进入nginx安装目录 启动nginx
cd /usr/local/server/nginx/sbin
./nginx
//启动菜狗商城后端服务器(&--后台运行)
cd /usr/local/caigoumall/
java -jar caigoumall-0.0.1-SNAPSHOT.jar &
七 功能设计
- 用户管理
- 首页实现
- 首页轮播图
- 首页分类列表展示
- 首页商品推荐
- 首页商品搜索
- 商品详情
- 商品详情查询与显示
- 商品评价分页实现
- 商品评价脱敏实现
- 购物车
- 添加购物车
- 购物车商品列表显示
- 选中商品实时价格计算
- 编辑购物车商品数量
- 删除购物车某商品
- 提交购物车至订单结算
- 订单管理
- 展示订单信息
- 订单支付(微信支付)
- 超时取消订单(定时任务)
- 个人中心
- 查询用户信息
- 查询当前用户的一切订单
- 查询当前用户指定状态的订单
- 收货地址
八 功能实现
8.1 用户模块-用户管理
用户注册:用户注册的账号长度必须为8-20给字符,密码长度必须为6-16给字符。后端将其保存在数据库中在MD5将密码加密后。 用户登录(cookie与session):用户输入唯一的账号密码实现登录。前端将账号密码传给后端,后端将密码进行MD5加密后与数据库对应密码进行对比,如果一致,通过JWT将密码和秘钥生成Token返回给前端。前端将Token以Json对象形式存储在sessionStorage中。
8.2 用户模块-收货地址
新建地址:新建收货地址相关信息。包括:收货人姓名,电话,邮政号,详细地址,备注信息。 查看地址列表:展示当前用户所有收货地址的信息。 编辑地址信息:对当前用户某一收货地址信息进行修改编辑。 删除地址:删除当前用户某一收货地址信息。
8.3 用户模块-个人中心订单管理
展示订单信息: 显示当前用户的指定状态订单信息。例如,未支付订单,已支付订单,已发货订单,未付款订单,未评价订单。
-
实现流程 -
页面展示
8.4 首页模块-首页轮播图
首页轮播图: 首页商品推荐轮播图。展示顺序根据商品的轮播顺序值决定。
-
实现流程 -
页面展示
8.5 首页模块-首页分类列表
首页分类列表展示:商品分类分为三级,将商品根据类型进行分布展示。
-
实现流程 -
页面展示
8.6 首页模块-首页最新商品推荐
首页商品推荐:推荐最新上市的三款产品进行推荐展示,依据商品最新上市时间。
-
实现流程 -
页面展示
8.7 首页模块-首页分类商品推荐
首页商品搜索:首页搜索栏可进行商品关键字进行模糊查询并显示相关商品及商品所对应的品牌。
-
实现流程 -
页面展示
8.8 商品模块-商品详情
商品详情查询与显示:商品的详情信息展示。包括商品的原价,现价,套餐,库存,展示图片,产地及生产信息。
-
实现流程 -
页面展示
8.9 商品模块-商品评论
商品评价分页实现:展示用户对商品的评价信息及商家对用户的反馈。实现分页功能,可以根据评价种类进行查看。 商品评价脱敏实现:对用户的账号及用户名信息进行脱敏处理。
-
实现流程 -
页面展示
8.10 商品模块-购物车
添加购物车:将用户对商品的套餐,数量,口味,价格等的选择信息添加到购物车。 购物车商品列表显示:显示购物车中全部商品信详情当前用户的,包括商品种类,数量,加入购物车时价格。 选中商品实时价格计算:根据用户对购物车中的商品选择实时计算应支付的价格。 编辑购物车商品数量:用户可以随意增删购物车中种种商品的购买数量。 删除购物车某商品:可以从购物车中将任意商品删除。 提交购物车至订单结算:将选中的商品生成订单快照去结算。首先要查询每种商品库存是否充足,如果充足生成订单快照,删除购物车中对应商品信息,扣减对应商品库存。
-
购物车登录状态实现流程 -
购物车未登录状态实现流程 -
页面展示
8.11 商品模块-商品搜索
首页商品搜索:首页搜索栏可进行商品关键字进行模糊查询并显示相关商品及商品所对应的品牌。
-
实现流程 -
页面展示
8.12 商品模块-按类别搜索商品
首页按类别商品搜索:首页商品分类栏通过点击商品类别进行模糊查询并显示对应类别商品及商品所对应的品牌。
-
实现流程 -
页面展示
8.13 订单模块-订单提交
展示订单信息: 显示当前订单中的收货地址详情和全部商品详情。包括:每个商品的下单价格,数量,规格。订单总价格。收货人电话,地址,姓名,备注信息。
-
实现流程 -
页面展示 -
核心代码
@Override
@Transactional
public Map<String, String> addOrder(String cids, Orders order) {
synchronized (this) {
List<ShoppingCartVO> list = null;
ResultVO resultVO = shoppingCartService.listShoppingCartsByCids(cids);
if (resultVO.getCode() == 200) {
Object data = resultVO.getData();
list = Convert.convert(new TypeReference<List<ShoppingCartVO>>() {
}, data);
boolean istrue = true;
String untitled = "";
for (ShoppingCartVO sc : list) {
if (Integer.parseInt(sc.getCartNum()) > sc.getSkuStock()) {
istrue = false;
}
untitled += sc.getProductName() + ",";
}
untitled.substring(0, untitled.length() - 1);
if (istrue) {
order.setUntitled(untitled);
order.setStatus("1");
String orderId = IdUtil.simpleUUID();
order.setOrderId(orderId);
Map<String, Object> map = BeanUtil.beanToMap(order);
OrdersVO ordersVO = BeanUtil.mapToBean(map, OrdersVO.class, false);
ordersMapper.insert(ordersVO);
for (ShoppingCartVO sc : list) {
int cnum = Integer.parseInt(sc.getCartNum());
String itemId = RandomUtil.randomNumbers(18);
double totalPrice = sc.getSellPrice() * cnum;
OrderItem orderItem = new OrderItem();
orderItem.setItemId(itemId);
orderItem.setOrderId(orderId);
orderItem.setProductId(sc.getProductId());
orderItem.setProductName(sc.getProductName());
orderItem.setProductImg(sc.getProductImg());
orderItem.setSkuId(sc.getSkuId());
orderItem.setSkuName(sc.getSkuName());
orderItem.setProductPrice(BigDecimal.valueOf(sc.getSellPrice()));
orderItem.setBuyCounts(cnum);
orderItem.setTotalAmount(BigDecimal.valueOf(totalPrice));
orderItem.setBasketDate(sc.getCreateTime());
orderItem.setIsComment(0);
orderItemMapper.insert(orderItem);
ProductVO productVO = productMapper.selectById(sc.getProductId());
productVO.setSoldNum(productVO.getSoldNum() + Integer.parseInt(sc.getCartNum()));
productMapper.updateById(productVO);
ProductSku productSku = productSkuMapper.selectById(sc.getSkuId());
productSku.setStock(sc.getSkuStock() - Integer.parseInt(sc.getCartNum()));
productSkuMapper.updateById(productSku);
}
ResultVO resultVO1 = shoppingCartService.removeByCids(cids);
if (resultVO.getCode() == 200) {
HashMap<String, String> map1 = new HashMap<>();
map1.put("orderId", orderId);
map1.put("productNames", untitled);
return map1;
}
}
}
return null;
}
}
8.14 订单模块-微信支付
订单支付(微信支付):用户可以选择使用微信支付或其他支付方式。订单支付后显示支付二维码,扫码支付完成后,提示支付完成。
-
实现流程 -
微信支付展示图 -
订单支付完成展示图 -
核心代码
@Override
@Transactional
public ResultVO sendToVXPay(String cids, Orders order) {
synchronized (this) {
try {
Map<String, String> orderInfo = addOrder(cids, order);
if (orderInfo != null) {
String orderId = orderInfo.get("orderId");
HashMap<String, String> data = new HashMap<>();
data.put("body", orderInfo.get("productNames"));
data.put("out_trade_no", orderId);
data.put("fee_type", "CNY");
data.put("total_fee", "1");
data.put("trade_type", "NATIVE");
data.put("notify_url", "http://hi7ey8.natappfree.cc/pay/callback");
WXPay wxPay = new WXPay(new MyPayConfig());
Map<String, String> resp = null;
resp = wxPay.unifiedOrder(data);
orderInfo.put("payUrl", resp.get("code_url"));
return new ResultVO(200, "提交订单成功!", orderInfo);
}
} catch (Exception e) {
e.printStackTrace();
}
return new ResultVO(401, "提交订单失败!", null);
}
}
@RequestMapping("/callback")
@ApiOperation("支付回调接口")
public String paySuccess(HttpServletRequest request) throws Exception {
System.out.println("微信回调了!");
ServletInputStream is = request.getInputStream();
byte[] bs = new byte[1024];
int len = -1;
StringBuilder builder = new StringBuilder();
while((len = is.read(bs))!=-1){
builder.append(new String(bs,0,len));
}
String s = builder.toString();
Map<String, String> map = WXPayUtil.xmlToMap(s);
if(map!=null && "success".equalsIgnoreCase(map.get("result_code"))){
String orderId = map.get("out_trade_no");
int i = ordersService.updateOrderStatus(orderId, "2");
System.out.println("订单支付成功!orderId:"+orderId);
WebSocketServer.sendMsg(orderId,"FINISH_PAY");
if(i>0){
HashMap<String,String> resp = new HashMap<>();
resp.put("return_code","success");
resp.put("return_msg","OK");
resp.put("appid",map.get("appid"));
resp.put("result_code","success");
return WXPayUtil.mapToXml(resp);
}
}
return null;
}
@Component
@ServerEndpoint("/webSocket/{oid}")
public class WebSocketServer {
private static ConcurrentHashMap<String,Session> sessionMap = new ConcurrentHashMap<>();
@OnOpen
public void open(@PathParam("oid") String orderId, Session session){
System.out.println("websocket建立连接!:"+orderId);
sessionMap.put(orderId,session);
}
@OnClose
public void close(@PathParam("oid") String orderId){
sessionMap.remove(orderId);
}
public static void sendMsg(String orderId,String msg){
try {
Session session = sessionMap.get(orderId);
session.getBasicRemote().sendText(msg);
}catch (Exception e){
e.printStackTrace();
}
}
}
<script type="text/javascript" src="static/js/qrcode.min.js"></script>
mounted: function () {
var qrcode = new QRCode($("#payQrcodeDiv")[0], {
width: 200,
height: 200
});
qrcode.makeCode(this.orderInfo.payUrl);
var webSocketUrl = webSocketBaseUrl + "webSocket/" + this.orderInfo.orderId;
var websocket = new WebSocket(webSocketUrl);
websocket.onmessage = function (event) {
var msg = event.data;
if (msg == "FINISH_PAY") {
$("#div1").html("<label style='font-size:20px; color:green'>订单支付完成!</label>");
}
}
}
8.15 订单模块-订单超时取消
超时取消订单(定时任务): 通过轮询的方式查询当前用户的未支付订单(默认保留30分钟),过时间后修改订单状态为超时未支付并取消订单。
-
实现流程 -
页面展示 -
核心代码
@Scheduled(cron = "*/50 * * * * *")
public void checkAndCloseOrder() {
System.out.println("订单超时自动取消!");
try {
Date time = new Date(System.currentTimeMillis() - 30 * 60 * 1000);
QueryWrapper<OrdersVO> wrapper = new QueryWrapper<>();
wrapper.eq("status", 1).lt("create_time",time);
List<OrdersVO> ordersVOS = ordersMapper.selectList(wrapper);
for (OrdersVO ordersVO : ordersVOS) {
HashMap<String, String> params = new HashMap<>();
params.put("out_trade_no", ordersVO.getOrderId());
Map<String, String> resp = wxPay.orderQuery(params);
if ("SUCCESS".equalsIgnoreCase(resp.get("trade_state"))) {
OrdersVO order = ordersMapper.selectById(ordersVO.getOrderId());
order.setStatus("2");
ordersMapper.updateById(order);
System.out.println("支付完成的订单关闭支付链接!");
} else if ("NOTPAY".equalsIgnoreCase(resp.get("trade_state"))) {
Map<String, String> map = wxPay.closeOrder(params);
ordersService.closeOrder(ordersVO.getOrderId());
System.out.println("超时未支付订单关闭支付链接!");
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
|