整个工程分为8个工程,细分为26个子工程。
序号 |
项目名称 |
各子系统 |
业务子系统 |
||
1 |
jt-web |
前台商城系统:用户可以访问商城首界面,查看不同分类下的商品,浏览商品的详细信息,并可以查询商品,将商品加入购物车,最终提交订单,还包括用户注册和登录。 |
2 |
jt-manage |
后台管理系统:商品分类管理、商品信息管理、商品规格属性、注册用户管理以及CMS内容发布管理等功能。 包括:jt-manage-mapper/pojo/service/web四个子项目 |
3 |
jt-cart |
购物车系统:未登录商品选择,登录商品选择,修改商品数量,计算支付金额,下单提交到订单系统。 |
4 |
jt-order |
订单系统:提供下单、查询订单、修改订单状态、定时处理订单。 |
5 |
jt-search |
搜索系统:提供商品的搜索功能。 |
支撑子系统 |
||
6 |
jt-parent |
jar包依赖管理 |
7 |
jt-common |
公用工具类 |
8 |
jt-sso |
单点登录系统:为多个系统之间提供用户登录凭证以及查询登录用户的信息。 |
思考:
序号 |
知识点 |
类型 |
|
|
|
购物车表设计,创建复合索引 |
设计 |
|
|
|
商品库存,超卖现象如何解决?虚拟库存。 |
论述 |
|
|
知识点:
序号 |
知识点 |
类型 |
|
|
|
购物车业务接口实现 |
技术 |
|
|
|
购物车表设计 |
设计 |
|
|
|
商品点击”加入购物车“,将页面的buy_num数量也同时提交 |
技术 |
|
|
|
我的购物车列表 |
技术 |
|
|
|
点击+增加商品数量,点击-减少商品数量 |
技术 |
|
|
|
购物车拦截器,获取用户信息 |
技术 |
|
|
购物车的特点:访问压力非常大,查询量非常大,并发量比较高,数据必须做持久化。
问题:当用户登录后购买了一本书,退出登录。然后我们又买了本书,这时由于没有登录,信息是保存到cookie中的,那当登录后,cookie中的信息会怎么处理?
答:可以操作下京东网站,可以看到众多网站都是将cookie中的内容和持久化的购物车内容合并。如果买的书存在则数量加1,如果买的书是新的,就新增。
问题:当用户购买一本书时是100元,存放到购物车,但还未支付。这时搞优惠,这本书价格改为60元。那用户该支付多少?100还是60?
答:购物车时必须保存当时的价格,否则如果不保存,支付时再去获取价格就变了。如果价格低了用户当然高兴,但要高了。用户肯定会投诉的。
1、登录逻辑:
1)数据库表保存,user_id,item_id,num,item_price,item_title等。
2)登录逻辑。
3)购物车系统中实现。
问题:如果不单独创建购物车系统,那购物车功能应该放在哪个系统中?前台?后台?单点?订单?
答:如果不做成单独的系统就应该放在后台系统中。因为在大型系统开发时,维护数据的地方应该尽量是一个地方。否则数据管理容易混乱。比如,商品添加、修改维护、删除都在后台,其他系统要数据时,只需要调用接口就行了。这样就达到一个数据的共享。这是现在主流架构,API化。就是说它把整个系统的数据操作全部提供API,其他系统就调用这些API。我们后台接口都是些API,不仅实现数据共享,还实现数据一致。
购物车系统提供API。
2、未登录逻辑:
1)将商品数据写入cookie(cookie有效期、域、数据安全性)
2)基于cookie实现提供API。
/jt-cart/pom.xml
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
与其他工程类似,这里就不一一概述。
#购物车服务器
server {
listen 80;
server_name cart.jt.com;
#charset koi8-r;
#access_log logs/host.access.log main;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
location / {
proxy_pass http://127.0.0.1:8086;
proxy_connect_timeout 600;
proxy_read_timeout 600;
}
}
创建数据库表
create table tb_cart
(
id bigint(20) not null auto_increment,
user_id bigint(20),
item_id bigint(20),
item_title varchar(100),
item_image varchar(200),
item_price bigint(20) comment '单位:分',
num int(10),
created datetime,
updated datetime,
primary key (id),
key AK_user_itemId (user_id, item_id)
);
这里建立了一个复合关键字索引。购物车一般的查询条件是查询某个用户的所有购物,或者查询某个用户某个商品是否加入到购物车。这两种情况是否用到了这个索引?支持,联合索引支持单查用户id,但不支持按商品id。也支持用户id+商品id。如果按商品id搜索,这个复合索引是失效的。
这就是数据库表设计的一个优化-索引左侧前缀特性。
EXPLAIN SELECT * FROM tb_cart WHERE user_id=1 AND item_id=1
EXPLAIN SELECT * FROM tb_cart WHERE user_id=1
EXPLAIN SELECT * FROM tb_cart WHERE item_id=1
查看SQL的执行计划,可以看出第一句、第二句SQL使用了索引,第三句 SQL未使用索引。很好的证明了索引左侧前缀特性。
注意:
1)的语句违反了左侧前缀的特性,为何仍然可以使用索引呢?因为MYSQL对SQL语句有优化,它会重新组合where条件。
EXPLAIN SELECT * FROM tb_cart WHERE item_id=1 AND user_id=1
2)没有where条件的查询是不会使用索引的。
/jt-cart/src/main/resources/mybatis/mappers/CartMapper.xml
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
SELECT * FROM tb_cart WHERE user_id = #{userId} AND item_id = #{itemId}
INSERT INTO tb_cart (
id,
user_id,
item_id,
item_title,
item_image,
item_price,
num,
created,
updated
)
VALUES
(
NULL,
#{userId},
#{itemId},
#{itemTitle},
#{itemImage},
#{itemPrice},
#{num},
NOW(),
NOW()
);
UPDATE tb_cart
SET
num = #{num},
updated = NOW()
WHERE
id = #{id}
SELECT * FROM tb_cart WHERE user_id = #{userId}
UPDATE tb_cart
SET
num = #{num},
updated = NOW()
WHERE
user_id = #{userId} AND item_id = #{itemId}
1
DELETE FROM tb_cart WHERE user_id = #{userId} AND item_id = #{itemId}
package com.jt.cart.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.jt.cart.pojo.Cart;
public interface CartMapper {
/**
* 根据用户名id和商品id查询购物车数据
*
* @param userId
* @param itemId
* @return
*/
Cart queryCartByUserIdAndItemId(@Param("userId") Long userId, @Param("itemId") Long itemId);
/**
* 新增购物车数据
*
* @param cart
*/
void save(Cart cart);
/**
* 修改商品数量
*
* @param cart
*/
void updateCartNum(Cart cart);
/**
* 根据用户id查询
*
* @param userId
* @return
*/
List
/**
* 根据用户id、商品id修改数量
*
* @param userId
* @param itemId
* @param num
* @return
*/
Integer updateCartNumByUserIdAndItemId(@Param("userId") Long userId, @Param("itemId") Long itemId,@Param("num") Integer num);
/**
* 删除
*
* @param userId
* @param itemId
* @return
*/
Integer delete(@Param("userId") Long userId, @Param("itemId") Long itemId);
}
package com.jt.cart.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.jt.cart.mapper.CartMapper;
import com.jt.cart.pojo.Cart;
import com.jt.common.vo.SysResult;
@Service
public class CartService {
@Autowired
private CartMapper cartMapper;
public SysResult saveItem(Cart newCart) {
// 判断商品是否存在购物车中,如果存在,数量+1
Cart cart = this.cartMapper.queryCartByUserIdAndItemId(newCart.getUserId(), newCart.getItemId());
if (cart == null) {
// 不存在
this.cartMapper.save(newCart);
return SysResult.ok(newCart.getId());
} else {
// 存在
cart.setNum(cart.getNum() + 1);
this.cartMapper.updateCartNum(cart);
return SysResult.build(202, "该商品已经存在购物车中!商品数量+1", null);
}
}
public SysResult queryCartList(Long userId) {
List
return SysResult.ok(carts);
}
/**
* 修改购物车数量
*
* @param userId
* @param itemId
* @param num
* @return
*/
public SysResult updateNum(Long userId, Long itemId, Integer num) {
Integer count = this.cartMapper.updateCartNumByUserIdAndItemId(userId, itemId, num);
if (count == null || count.intValue() == 0) {
return SysResult.build(201, "该商品不存在购物车中!");
}
return SysResult.ok();
}
/**
* 删除商品
*
* @param userId
* @param itemId
* @return
*/
public SysResult delete(Long userId, Long itemId) {
Integer count = this.cartMapper.delete(userId, itemId);
if (count == null || count.intValue() == 0) {
return SysResult.build(201, "该商品不存在购物车中!");
}
return SysResult.ok();
}
}
package com.jt.cart.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.jt.cart.pojo.Cart;
import com.jt.cart.service.CartService;
import com.jt.common.vo.SysResult;
/**
* 购物车相关API
*
*/
@RequestMapping(value = "cart")
@Controller
public class CartController {
@Autowired
private CartService cartService;
/**
* 添加商品到购物车
*
* @param cart
* @return
*/
@RequestMapping(value = "save", method = RequestMethod.POST)
@ResponseBody
public SysResult saveItem(Cart cart) {
return this.cartService.saveItem(cart);
}
/**
* 根据用户ID查询购物车数据
*
* @return
*/
@RequestMapping(value = "query/{userId}", method = RequestMethod.GET)
@ResponseBody
public SysResult queryCartList(@PathVariable("userId") Long userId) {
return this.cartService.queryCartList(userId);
}
/**
* 更新商品数量
*
* @param userId
* @param itemId
* @param num
* @return
*/
@RequestMapping(value = "update/num/{userId}/{itemId}/{num}", method = RequestMethod.POST)
@ResponseBody
public SysResult updateNum(@PathVariable("userId") Long userId, @PathVariable("itemId") Long itemId,
@PathVariable("num") Integer num) {
return this.cartService.updateNum(userId, itemId, num);
}
/**
* 删除购物车中的商品数据
*
* @param userId
* @param itemId
* @return
*/
@RequestMapping(value = "delete/{userId}/{itemId}", method = RequestMethod.POST)
@ResponseBody
public SysResult delete(@PathVariable("userId") Long userId, @PathVariable("itemId") Long itemId) {
return this.cartService.delete(userId, itemId);
}
}
启动后台、sso、前台、搜索、RabbitMQ
com.jt.web.threadlocal.UserThreadLocal
package com.jt.web.threadlocal;
import com.jt.web.pojo.User;
public class UserThreadLocal {
private static final ThreadLocal
public static void set(User user) {
USER.set(user);
}
public static User get() {
return USER.get();
}
public static Long getUserId(){
if(null!=USER){
return USER.get().getId();
}else{
return null;
}
}
}
package com.jt.web.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import com.jt.common.util.CookieUtils;
import com.jt.web.controller.UserController;
import com.jt.web.pojo.User;
import com.jt.web.service.UserService;
import com.jt.web.threadlocal.UserThreadLocal;
public class CartInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String ticket = CookieUtils.getCookieValue(request, UserController.JT_TICKET);
if (null == ticket) {
UserThreadLocal.set(null);
return true;
}
User user = this.userService.queryUserByTicket(ticket);
if (user == null) {
UserThreadLocal.set(null);
return true;
}
// 成功
// 将user对象放到本地线程中,方便后续使用
UserThreadLocal.set(user);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) throws Exception {
}
}
package com.jt.web.controller;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import com.jt.web.pojo.Cart;
import com.jt.web.pojo.User;
import com.jt.web.threadlocal.UserThreadLocal;
@RequestMapping(value = "cart")
@Controller
public class CartController {
@Autowired
private CartService cartService;
@Autowired
private CartCookieService cartCookieService;
@RequestMapping("add/{itemId}")
public String addItemToCart(@PathVariable("itemId") Long itemId,HttpServletRequest request,HttpServletResponse response) {
// 判断用户是否登录
User user = UserThreadLocal.get();
if (user == null) {
// 未登录,将商品数据保存到cookie中
try {
this.cartCookieService.addItemToCart(itemId, request, response);
} catch (Exception e) {
// TODO 出现异常的跳转?
e.printStackTrace();
}
} else {
// 登录,将商品数据保存到数据库中
this.cartService.addItemToCart(user, itemId);
}
return "redirect:/cart/show.html";
}
@RequestMapping("show")
public ModelAndView showCart(HttpServletRequest request,HttpServletResponse response) {
ModelAndView mv = new ModelAndView("cart");
User user = UserThreadLocal.get();
List
if (user == null) {
// 未登录,从cookie中查询商品
try {
carts = this.cartCookieService.queryCartByUser(request,response,true);
} catch (Exception e) {
// TODO
e.printStackTrace();
}
} else {
// 登录,从api查询商品
carts = this.cartService.queryCartByUser(user);
}
mv.addObject("cartList", carts);
return mv;
}
@RequestMapping("delete/{itemId}")
public String deleteCart(@PathVariable("itemId") Long itemId,HttpServletRequest request,HttpServletResponse response) {
User user = UserThreadLocal.get();
if (user == null) {
// 未登录,从cookie中查询商品
try {
this.cartCookieService.deleteCart(itemId, request, response);
} catch (Exception e) {
// TODO
e.printStackTrace();
}
} else {
// 登录,从api查询商品
this.cartService.deleteCart(user, itemId);
}
return "redirect:/cart/show.html";
}
/**
* 更新数量
* @param itemId
* @param num
* @return
*/
@RequestMapping(value = "update/num/{itemId}/{num}")
@ResponseBody
public SysResult updateCart(@PathVariable("itemId") Long itemId, @PathVariable("num") Integer num,HttpServletRequest request,HttpServletResponse response) {
User user = UserThreadLocal.get();
if (user == null) {
// 未登录,从cookie中查询商品
try {
this.cartCookieService.updateCart(request,response,itemId, num);
} catch (Exception e) {
e.printStackTrace();
return SysResult.build(201, "更新数量失败!");
}
} else {
// 登录,从api查询商品
this.cartService.updateCart(user, itemId, num);
}
return SysResult.ok();
}
}
/jt-web/src/main/java/com/jt/web/service/CartService.java
package com.jt.web.service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jt.common.service.HttpClientService;
import com.jt.common.spring.exetend.PropertyConfig;
import com.jt.common.vo.SysResult;
import com.jt.web.pojo.Cart;
import com.jt.web.pojo.Item;
import com.jt.web.pojo.User;
@Service
public class CartService {
@Autowired
private ItemService itemService;
@Autowired
private HttpClientService httpClientService;
@PropertyConfig
private String CART_URL;
private static final ObjectMapper MAPPER = new ObjectMapper();
public Boolean addItemToCart(User user, Long itemId) {
// 通过商品id查询商品数据
Item item = this.itemService.queryItemById(itemId);
// 调用购物车系统的API添加商品
String url = CART_URL + "/cart/save";
Map
params.put("userId", String.valueOf(user.getId()));
params.put("itemId", String.valueOf(itemId));
params.put("itemTitle", item.getTitle());
String[] images = item.getImages();
if (null == images) {
params.put("itemImage", "");
} else {
params.put("itemImage", images[0]);
}
params.put("itemPrice", String.valueOf(item.getPrice()));
params.put("num", "1");// 默认为:1
try {
String jsonData =httpClientService.doPost(url, params, "UTF-8");
JsonNode jsonNode = MAPPER.readTree(jsonData);
Integer status = jsonNode.get("status").intValue();
if (status == 200 || status == 202) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
@SuppressWarnings("unchecked")
public List
String url = CART_URL + "/cart/query/" + user.getId();
try {
String jsonData = this.httpClientService.doGet(url);
SysResult sysResult = SysResult.formatToList(jsonData, Cart.class);
return (List
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 删除
*
* @param user
* @param itemId
* @return
*/
public Boolean deleteCart(User user, Long itemId) {
String url = CART_URL + "/cart/delete/" + user.getId() + "/" + itemId;
try {
String jsonData = this.httpClientService.doPost(url, null);
JsonNode jsonNode = MAPPER.readTree(jsonData);
Integer status = jsonNode.get("status").intValue();
if (status == 200) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* 更新数量
* @param user
* @param itemId
* @param num
* @return
*/
public Boolean updateCart(User user, Long itemId, Integer num) {
String url = CART_URL + "/cart/update/num/" + user.getId() + "/" + itemId + "/" + num;
try {
String jsonData = httpClientService.doPost(url, null);
JsonNode jsonNode = MAPPER.readTree(jsonData);
Integer status = jsonNode.get("status").intValue();
if (status == 200) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
}
当点击货物,增加或者减少数量时,ajax调用完,页面的价格跟着变化,并按上面有人民币前缀、千位符、保留两位小数等格式,可以通过js插件完成。
cart.js实现业务调用链接,jquery.price_format.2.0.min.js实现格式化。
前台系统CartCookieService
/jt-web/src/main/java/com/jt/web/service/CartCookieService.java
package com.jt.web.service;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jt.common.util.CookieUtils;
import com.jt.web.pojo.Cart;
import com.jt.web.pojo.Item;
@Service
public class CartCookieService {
private static final String JT_CART = "JT_CART";
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final Integer COOKIE_TIME = 60 * 60 * 24 * 30;
@Autowired
private ItemService itemService;
public void addItemToCart(Long itemId, HttpServletRequest request, HttpServletResponse response)
throws Exception {
// 读取cookie中购物车的数据
String cookieData = CookieUtils.getCookieValue(request, JT_CART);
List
if (StringUtils.isEmpty(cookieData)) {
// 将商品数据保存到cookie中
carts = new ArrayList
carts.add(itemToCart(itemId));
} else {
carts = queryCartByUser(request, response, false);
// 判断,该商品是否存在购物车中
boolean bool = false;
Cart cart = null;
for (Cart c : carts) {
if (c.getItemId().intValue() == itemId.intValue()) {
bool = true;
cart = c;
break;
}
}
if (bool) {
// 存在
cart.setNum(cart.getNum() + 1);
} else {
carts.add(itemToCart(itemId));
}
}
CookieUtils.setCookie(request, response, JT_CART, MAPPER.writeValueAsString(carts), COOKIE_TIME);
}
private Cart itemToCart(Long itemId) throws Exception {
Item item = this.itemService.queryItemById(itemId);
Cart cart = new Cart();
cart.setItemId(itemId);
String[] images = item.getImages();
if (images == null) {
cart.setItemImage("");
} else {
cart.setItemImage(images[0]); //显示第一张图片
}
cart.setItemPrice(item.getPrice());
// 编码
String title = URLEncoder.encode(item.getTitle(), "UTF-8");
cart.setItemTitle(title);
cart.setNum(1);
cart.setCreated(new Date());
return cart;
}
public List
boolean isDecode) throws Exception {
// 读取cookie中购物车的数据
String cookieData = CookieUtils.getCookieValue(request, JT_CART);
if (null == cookieData) {
return null;
}
List
MAPPER.getTypeFactory().constructCollectionType(List.class, Cart.class));
if (isDecode) {
for (Cart cart : carts) {
String title = URLDecoder.decode(cart.getItemTitle(), "UTF-8");
cart.setItemTitle(title);
}
}
return carts;
}
public void deleteCart(Long itemId, HttpServletRequest request, HttpServletResponse response)
throws Exception {
List
for (int i = 0; i < carts.size(); i++) {
Cart cart = carts.get(i);
if (cart.getItemId().intValue() == itemId.intValue()) {
carts.remove(i);
break;
}
}
// 写入到cookie中
CookieUtils.setCookie(request, response, JT_CART, MAPPER.writeValueAsString(carts), COOKIE_TIME);
}
public void updateCart(HttpServletRequest request, HttpServletResponse response, Long itemId, Integer num)throws Exception {
List
for (int i = 0; i < carts.size(); i++) {
Cart cart = carts.get(i);
if (cart.getItemId().intValue() == itemId.intValue()) {
cart.setNum(num);
break;
}
}
// 写入到cookie中
CookieUtils.setCookie(request, response, JT_CART, MAPPER.writeValueAsString(carts), COOKIE_TIME);
}
}
在写入数据之前编码
String title = URLEncoder.encode(item.getTitle(), "UTF-8");
cart.setItemTitle(title);
从数据库读出,展示前解码
String title = URLDecoder.decode(cart.getItemTitle(), "UTF-8");
实现有什么问题?
1)不安全,明文。
2)清空浏览器(缓存)数据,购物车数据丢失(不登录情况下)。
3)如果用户禁用cookie,功能失效。
4)Cookie是有长度限制。如下图所示:
解决方案:
1)只保存商品ID和购买数量。(问题得不到根本解决)
2)生成key,根据key去后台接口中查询数据。
A)保存数据库中(性能问题)
B)保存redis中(不能持久化)
C)两者结合使用(选定)
/jt-web/src/main/java/com/jt/web/controller/OrderController.java
//创建订单
@RequestMapping("create")
public ModelAndView createOrder() {
User user = UserThreadLocal.get();
// 查询该用户的购物车
List
ModelAndView mv = new ModelAndView("order-cart");
mv.addObject("carts", carts);
return mv;
}
启动订单系统,点击“结算”。
//结算
@RequestMapping("submit")
@ResponseBody
public SysResult create(Order order) {
User user = UserThreadLocal.get();
order.setUserId(user.getId());
order.setBuyerNick(user.getUsername());
String orderNo = this.orderService.createOrder(order);
return SysResult.ok(orderNo);
}